Files
AutistMask/tests/holders.test.js
2026-08-12 10:34:45 +02:00

167 lines
5.8 KiB
JavaScript

// Tests for src/shared/holders.js and the balance-list spam gate that reads
// it (issue #230).
//
// The rule these pin down: an explorer that reports no holders_count has told
// us nothing, and "nothing" must not be recorded as "zero holders". Zero is
// the strongest spam signal the wallet has, so handing it out for free turns
// a missing field into a hidden asset.
jest.mock("../src/shared/log", () => ({
log: {
debugf: () => {},
infof: () => {},
warnf: () => {},
errorf: () => {},
},
debugFetch: jest.fn(),
setRuntimeDebug: () => {},
isDebug: () => false,
}));
global.fetch = jest.fn(() => {
throw new Error("tests must not perform network requests");
});
global.chrome = { storage: { local: {} } };
const {
LOW_HOLDER_THRESHOLD,
parseHoldersCount,
isLowHolderCount,
} = require("../src/shared/holders");
const { fetchTokenBalances } = require("../src/shared/balances");
const { debugFetch } = require("../src/shared/log");
const BLOCKSCOUT = "https://eth.blockscout.com/api/v2";
const HOLDER = "0x66133e8ea0f5d1d612d2502a968757d1048c214a";
const USDC_CONTRACT = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48";
const NOVEL_TOKEN = "0x1111111111111111111111111111111111111111";
describe("parseHoldersCount", () => {
test("a reported count parses to that number", () => {
expect(parseHoldersCount("3500000")).toBe(3500000);
expect(parseHoldersCount(3500000)).toBe(3500000);
});
test('a reported "0" parses to 0, which is not null', () => {
expect(parseHoldersCount("0")).toBe(0);
expect(parseHoldersCount(0)).toBe(0);
});
test("an omitted, null or empty count is unknown", () => {
expect(parseHoldersCount(undefined)).toBeNull();
expect(parseHoldersCount(null)).toBeNull();
expect(parseHoldersCount("")).toBeNull();
});
test("an unparseable count is unknown rather than zero", () => {
expect(parseHoldersCount("many")).toBeNull();
expect(parseHoldersCount(NaN)).toBeNull();
});
});
describe("isLowHolderCount", () => {
test("the threshold is the documented 1,000 holders", () => {
expect(LOW_HOLDER_THRESHOLD).toBe(1000);
});
test("a reported count below the threshold is low", () => {
expect(isLowHolderCount(0)).toBe(true);
expect(isLowHolderCount(999)).toBe(true);
});
test("a reported count at or above the threshold is not low", () => {
expect(isLowHolderCount(1000)).toBe(false);
expect(isLowHolderCount(1001)).toBe(false);
});
test("an unknown count is not low", () => {
expect(isLowHolderCount(null)).toBe(false);
expect(isLowHolderCount(undefined)).toBe(false);
});
});
// fetchTokenBalances applies its own spam gate, which is not the low-holder
// display filter: it has no setting behind it and decides what the balance
// list contains at all. It stays strict on an unknown count — see the
// comment at the gate — but must stop recording that unknown as zero.
describe("the balance-list spam gate", () => {
function respondWith(items) {
debugFetch.mockImplementation(async () => ({
ok: true,
status: 200,
statusText: "OK",
json: async () => items,
}));
}
function item(overrides = {}) {
const { token, ...rest } = overrides;
return {
value: "12500000",
...rest,
token: {
type: "ERC-20",
address_hash: NOVEL_TOKEN,
symbol: "SPAMTKN",
name: "Spam Token",
decimals: "6",
holders_count: "50000",
...token,
},
};
}
beforeEach(() => {
debugFetch.mockReset();
});
test("a token with plenty of reported holders is listed", async () => {
respondWith([item()]);
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
expect(balances).toHaveLength(1);
expect(balances[0].holders).toBe(50000);
});
test("a token reporting zero holders is still excluded", async () => {
respondWith([item({ token: { holders_count: "0" } })]);
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
});
test("an unknown holder count does not admit an unvouched token", async () => {
respondWith([item({ token: { holders_count: null } })]);
expect(await fetchTokenBalances(HOLDER, BLOCKSCOUT, [])).toEqual([]);
});
// The path that reaches the send selector and the history filter: a token
// the user vouched for by tracking it is listed whatever the explorer
// says, and it must carry the unknown count through as null, not as the
// zero that would then hide it downstream.
test("a tracked token with an unknown count is listed with holders null", async () => {
respondWith([item({ token: { holders_count: undefined } })]);
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, [
{ address: NOVEL_TOKEN.toUpperCase() },
]);
expect(balances).toHaveLength(1);
expect(balances[0].holders).toBeNull();
});
test("a known-list token with an unknown count is listed with holders null", async () => {
respondWith([
item({
token: {
address_hash: USDC_CONTRACT,
symbol: "USDC",
holders_count: null,
},
}),
]);
const balances = await fetchTokenBalances(HOLDER, BLOCKSCOUT, []);
expect(balances).toHaveLength(1);
expect(balances[0].holders).toBeNull();
});
test("no test in this file performed a network request", () => {
expect(global.fetch).not.toHaveBeenCalled();
});
});