Skip to content

Commit 463cbdd

Browse files
committed
AG-45427 Add mocked tests
Squashed commit of the following: commit 6a001b3 Author: Anatolii Khmarskii <a.khmarskii@adguard.com> Date: Tue Aug 26 16:31:01 2025 +0300 AG-45427 Add mocked tests commit 65b2bcd Author: Anatolii Khmarskii <a.khmarskii@adguard.com> Date: Tue Aug 26 16:13:46 2025 +0300 AG-45427 Add mocked tests
1 parent b643b6e commit 463cbdd

13 files changed

Lines changed: 463 additions & 92 deletions

test/filelinter.test.js

Lines changed: 0 additions & 17 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const dnscheck = require('../src/dnscheck');
1+
const dnscheck = require('../../src/dnscheck');
22

33
describe('dnscheck', () => {
44
it('check a known existing domain', async () => {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const fileLinter = require('../../src/filelinter');
2+
3+
describe('File linter', () => {
4+
it('test a simple automatic run', async () => {
5+
const fileResult = await fileLinter.lintFile('test/resources/filter.txt', {
6+
auto: true,
7+
ignoreDomains: new Set(),
8+
});
9+
10+
expect(fileResult).toBeDefined();
11+
expect(fileResult.listAst).toBeDefined();
12+
expect(fileResult.results).toBeDefined();
13+
expect(fileResult.results).toHaveLength(4);
14+
});
15+
16+
it('should ignore domains in ignoreDomains set with real API', async () => {
17+
const fileResult = await fileLinter.lintFile('test/resources/filter.txt', {
18+
auto: true,
19+
ignoreDomains: new Set(['anotherdeaddomain.examplee']),
20+
});
21+
22+
expect(fileResult).toBeDefined();
23+
expect(fileResult.results).toHaveLength(3);
24+
}, 30000);
25+
26+
it('should handle empty file', async () => {
27+
const fileResult = await fileLinter.lintFile('test/resources/empty.txt', {
28+
auto: true,
29+
ignoreDomains: new Set(),
30+
});
31+
32+
expect(fileResult).toBeNull();
33+
}, 10000);
34+
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const agtree = require('@adguard/agtree');
2-
const checker = require('../src/linter');
2+
const checker = require('../../src/linter');
33

44
const testLintRule = (rule, expected, useDNS = false, ignoreDomains = new Set()) => {
55
return async () => {
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const urlfilter = require('../src/urlfilter');
1+
const urlfilter = require('../../src/urlfilter');
22

33
describe('urlfilter', () => {
44
it('check a domain that we know does exist', async () => {

test/mocked/dnscheck.test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
jest.mock('dns', () => {
2+
const actualDns = jest.requireActual('dns');
3+
4+
const mockResolver = {
5+
resolve: jest.fn(),
6+
setServers: jest.fn(),
7+
};
8+
9+
return {
10+
...actualDns,
11+
Resolver: jest.fn(() => mockResolver),
12+
};
13+
});
14+
15+
const dns = require('dns');
16+
const dnscheck = require('../../src/dnscheck');
17+
18+
describe('dnscheck mocked tests', () => {
19+
let mockResolver;
20+
21+
beforeEach(() => {
22+
mockResolver = new dns.Resolver();
23+
jest.clearAllMocks();
24+
});
25+
26+
it('check a known existing domain with mocked DNS', async () => {
27+
// Mock successful DNS resolution
28+
mockResolver.resolve.mockImplementation((domain, rrtype, callback) => {
29+
callback(null, ['93.184.216.34']);
30+
});
31+
32+
const result = await dnscheck.checkDomain('example.org');
33+
expect(result).toBe(true);
34+
expect(mockResolver.resolve).toHaveBeenCalledWith('example.org', 'A', expect.any(Function));
35+
});
36+
37+
it('check a known non-existing domain with mocked DNS', async () => {
38+
// Mock DNS resolution failure
39+
mockResolver.resolve.mockImplementation((domain, rrtype, callback) => {
40+
callback(new Error('ENOTFOUND'));
41+
});
42+
43+
const result = await dnscheck.checkDomain('example.nonexistingdomain');
44+
expect(result).toBe(false);
45+
expect(mockResolver.resolve).toHaveBeenCalledWith('example.nonexistingdomain', 'A', expect.any(Function));
46+
});
47+
48+
it('check a domain that only has a www. record with mocked DNS', async () => {
49+
// Mock: base domain fails, www version succeeds
50+
mockResolver.resolve.mockImplementation((domain, rrtype, callback) => {
51+
if (domain === 'city.kawasaki.jp') {
52+
callback(new Error('ENOTFOUND'));
53+
} else if (domain === 'www.city.kawasaki.jp') {
54+
callback(null, ['192.0.2.1']);
55+
} else {
56+
callback(new Error('Unexpected domain'));
57+
}
58+
});
59+
60+
const noWwwExists = await dnscheck.domainExists('city.kawasaki.jp');
61+
expect(noWwwExists).toBe(false);
62+
63+
const result = await dnscheck.checkDomain('city.kawasaki.jp');
64+
expect(result).toBe(true);
65+
66+
expect(mockResolver.resolve).toHaveBeenCalledWith('city.kawasaki.jp', 'A', expect.any(Function));
67+
expect(mockResolver.resolve).toHaveBeenCalledWith('www.city.kawasaki.jp', 'A', expect.any(Function));
68+
});
69+
70+
it('should not check www version for www domains', async () => {
71+
// Mock DNS resolution failure
72+
mockResolver.resolve.mockImplementation((domain, rrtype, callback) => {
73+
callback(new Error('ENOTFOUND'));
74+
});
75+
76+
const result = await dnscheck.checkDomain('www.example.nonexisting');
77+
expect(result).toBe(false);
78+
79+
// Should only be called once for the www domain
80+
expect(mockResolver.resolve).toHaveBeenCalledTimes(1);
81+
expect(mockResolver.resolve).toHaveBeenCalledWith('www.example.nonexisting', 'A', expect.any(Function));
82+
});
83+
});

test/mocked/filelinter.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
jest.mock('node-fetch');
2+
const fetch = require('node-fetch');
3+
const fileLinter = require('../../src/filelinter');
4+
const { createSuccessResponse } = require('./mockresponse');
5+
6+
describe('File linter with mocked API', () => {
7+
beforeEach(() => {
8+
fetch.mockReset();
9+
});
10+
11+
it('test a simple automatic run with mocked API', async () => {
12+
fetch.mockResolvedValue(createSuccessResponse(
13+
['example.notexisting', 'anotherdeaddomain.examplee'],
14+
['example.org'],
15+
));
16+
17+
const fileResult = await fileLinter.lintFile('test/resources/filter.txt', {
18+
auto: true,
19+
ignoreDomains: new Set(),
20+
});
21+
22+
expect(fileResult).toBeDefined();
23+
expect(fileResult.listAst).toBeDefined();
24+
expect(fileResult.results).toBeDefined();
25+
26+
// Should find 4 issues:
27+
// 1. ||example.org^$domain=example.notexisting (dead domain in $domain)
28+
// 2. ||example.org^$domain=~example.notexisting (dead negated domain in $domain)
29+
// 3. example.notexisting##banner (dead domain in cosmetic rule)
30+
// 4. ||anotherdeaddomain.examplee^ (dead domain in network rule)
31+
expect(fileResult.results).toHaveLength(4);
32+
});
33+
34+
it('should ignore domains in ignoreDomains set', async () => {
35+
fetch.mockResolvedValue(createSuccessResponse(
36+
['example.notexisting', 'anotherdeaddomain.examplee'],
37+
['example.org'],
38+
));
39+
40+
const fileResult = await fileLinter.lintFile('test/resources/filter.txt', {
41+
auto: true,
42+
ignoreDomains: new Set(['example.notexisting']),
43+
});
44+
45+
expect(fileResult).toBeDefined();
46+
47+
// Should only find 1 issue now (the rule with 'anotherdeaddomain.examplee' which is not ignored)
48+
// The rules with example.notexisting should be ignored
49+
expect(fileResult.results).toHaveLength(1);
50+
});
51+
});

test/mocked/linter.test.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
jest.mock('node-fetch');
2+
const fetch = require('node-fetch');
3+
const agtree = require('@adguard/agtree');
4+
const punycode = require('node:punycode');
5+
const checker = require('../../src/linter');
6+
7+
describe('Linter mocked tests', () => {
8+
beforeEach(() => {
9+
fetch.mockReset();
10+
});
11+
12+
const testLintRule = (rule, expected, ignoreDomains = new Set()) => {
13+
return async () => {
14+
const deadDomainsToMock = expected?.deadDomains.map((domain) => punycode.toASCII(domain)) || [];
15+
16+
fetch.mockImplementation(async (url) => {
17+
const urlObj = new URL(url);
18+
const requestedDomains = urlObj.searchParams.getAll('domain');
19+
const responseData = {};
20+
21+
requestedDomains.forEach((domain) => {
22+
const isDead = deadDomainsToMock.includes(domain);
23+
responseData[domain] = {
24+
info: {
25+
domain_name: domain,
26+
registered_domain: domain,
27+
registered_domain_used_last_24_hours: !isDead,
28+
used_last_24_hours: !isDead,
29+
},
30+
matches: [],
31+
};
32+
});
33+
34+
return {
35+
status: 200,
36+
ok: true,
37+
headers: { get: () => 'application/json' },
38+
json: jest.fn().mockResolvedValue(responseData),
39+
};
40+
});
41+
42+
const ast = agtree.RuleParser.parse(rule);
43+
const result = await checker.lintRule(ast, { useDNS: false, ignoreDomains });
44+
45+
if (expected === null) {
46+
expect(result).toEqual(null);
47+
return;
48+
}
49+
50+
if (expected.remove) {
51+
expect(result.suggestedRule).toEqual(null);
52+
} else {
53+
const ruleText = agtree.RuleParser.generate(result.suggestedRule);
54+
expect(ruleText).toEqual(expected.suggestedRuleText);
55+
}
56+
57+
expect(result.deadDomains).toEqual(expected.deadDomains);
58+
};
59+
};
60+
61+
it('suggest removing rule with a dead domain in the pattern', testLintRule(
62+
'||example.notexistingdomain^',
63+
{ remove: true, deadDomains: ['example.notexistingdomain'] },
64+
));
65+
66+
it('suggest removing rule with a dead domain in the pattern URL', testLintRule(
67+
'||example.notexistingdomain/thisissomepath/tosomewhere',
68+
{ remove: true, deadDomains: ['example.notexistingdomain'] },
69+
));
70+
71+
it('ignore removing rule with a dead domain in the pattern URL', testLintRule(
72+
'||example.notexistingdomain/thisissomepath/tosomewhere',
73+
null,
74+
new Set(['example.notexistingdomain']),
75+
));
76+
77+
it('do not suggest removing IP addresses', testLintRule(
78+
'||1.2.3.4^',
79+
null,
80+
));
81+
82+
it('do not suggest removing .onion domains', testLintRule(
83+
'||example.onion^',
84+
null,
85+
));
86+
87+
it('suggest removing dead non ASCII domain from modifier', testLintRule(
88+
'||example.org^$domain=ппример2.рф',
89+
{ remove: true, deadDomains: ['ппример2.рф'] },
90+
));
91+
92+
it('do nothing with a simple rule with existing domain', testLintRule(
93+
'||example.org^$third-party',
94+
null,
95+
));
96+
97+
it('suggest removing negated domain from $domain', testLintRule(
98+
'||example.org^$domain=example.org|example.notexistingdomain',
99+
{ suggestedRuleText: '||example.org^$domain=example.org', deadDomains: ['example.notexistingdomain'] },
100+
));
101+
102+
it('suggest removing the whole rule when all permitted domains are dead', testLintRule(
103+
'||example.org^$domain=example.notexisting1|example.notexisting2',
104+
{ remove: true, deadDomains: ['example.notexisting1', 'example.notexisting2'] },
105+
));
106+
107+
it('ignore dead domain as part of $domain modifier', testLintRule(
108+
'||example.org^$domain=example.notexisting1|google.com|example.notexisting2',
109+
{
110+
suggestedRuleText: '||example.org^$domain=example.notexisting1|google.com',
111+
deadDomains: ['example.notexisting2'],
112+
},
113+
new Set(['example.notexisting1']),
114+
));
115+
116+
// Cosmetic rules tests
117+
it('suggest removing an element hiding rule which was only for dead domains', testLintRule(
118+
'example.notexistingdomain##banner',
119+
{ remove: true, deadDomains: ['example.notexistingdomain'] },
120+
));
121+
122+
it('ignore removing an element hiding rule for dead domains', testLintRule(
123+
'example.notexistingdomain##banner',
124+
null,
125+
new Set(['example.notexistingdomain']),
126+
));
127+
128+
it('keep the rule if there are permitted domains left', testLintRule(
129+
'example.org,example.notexistingdomain##banner',
130+
{ suggestedRuleText: 'example.org##banner', deadDomains: ['example.notexistingdomain'] },
131+
));
132+
133+
it('keep the rule if all dead domains were negated', testLintRule(
134+
'~example.notexistingdomain##banner',
135+
{ suggestedRuleText: '##banner', deadDomains: ['example.notexistingdomain'] },
136+
));
137+
138+
it('suggest removing a scriptlet rule', testLintRule(
139+
'example.notexistingdomain#%#//scriptlet("set-constant", "a", "1")',
140+
{ remove: true, deadDomains: ['example.notexistingdomain'] },
141+
));
142+
143+
it('ignore removing a scriptlet rule with ignore domain', testLintRule(
144+
'example.notexistingdomain#%#//scriptlet("set-constant", "a", "1")',
145+
null,
146+
new Set(['example.notexistingdomain']),
147+
));
148+
149+
it('suggest modifying a scriptlet rule', testLintRule(
150+
'example.org,example.notexistingdomain#%#//scriptlet("set-constant", "a", "1")',
151+
{
152+
suggestedRuleText: 'example.org#%#//scriptlet("set-constant", "a", "1")',
153+
deadDomains: ['example.notexistingdomain'],
154+
},
155+
));
156+
157+
it('ignore modifying a scriptlet rule with ignore domain', testLintRule(
158+
'example.org,example.notexistingdomain#%#//scriptlet("set-constant", "a", "1")',
159+
null,
160+
new Set(['example.notexistingdomain']),
161+
));
162+
});

0 commit comments

Comments
 (0)