import { describe, expect, it } from 'vitest'; import { formatTokens, mergeSelection, parseTokens, selectionFromValue, tokenFor, } from '@/lib/xray/geoTokens'; const siteKnown = new Set([ 'geosite:google', 'geosite:google@ads', 'geosite:cn', 'ext:my_rules.dat:corp', ]); const ipKnown = new Set(['geoip:cn', 'geoip:private', 'ext:my_ips.dat:office']); describe('parseTokens / formatTokens', () => { const cases: Array<[string, string, string[]]> = [ ['empty value', '', []], ['single token', 'geosite:google', ['geosite:google']], [ 'trims and drops blanks', ' geosite:google , , google.com ,', ['geosite:google', 'google.com'], ], ['keeps negation', '!geoip:cn, 10.0.0.0/8', ['!geoip:cn', '10.0.0.0/8']], ]; it.each(cases)('%s', (_name, value, expected) => { expect(parseTokens(value)).toEqual(expected); }); it('joins with a comma and a space', () => { expect(formatTokens(['geosite:google', 'google.com'])).toBe('geosite:google, google.com'); expect(formatTokens([])).toBe(''); }); }); describe('tokenFor', () => { const cases: Array<[string, string, string, 'site' | 'ip', string]> = [ [ 'default site database uses the geosite shorthand', 'geosite.dat', 'google', 'site', 'geosite:google', ], ['default ip database uses the geoip shorthand', 'geoip.dat', 'cn', 'ip', 'geoip:cn'], [ 'custom site database falls back to ext', 'my_rules.dat', 'corp', 'site', 'ext:my_rules.dat:corp', ], ['custom ip database falls back to ext', 'my_ips.dat', 'office', 'ip', 'ext:my_ips.dat:office'], [ 'ip kind on the site database is not shorthand', 'geosite.dat', 'cn', 'ip', 'ext:geosite.dat:cn', ], [ 'site kind on the ip database is not shorthand', 'geoip.dat', 'cn', 'site', 'ext:geoip.dat:cn', ], ]; it.each(cases)('%s', (_name, file, code, kind, expected) => { expect(tokenFor(file, code, kind)).toBe(expected); }); }); describe('selectionFromValue', () => { const cases: Array<[string, string, ReadonlySet, string[]]> = [ ['empty value selects nothing', '', siteKnown, []], ['plain values are not selectable', 'google.com, keyword:ads', siteKnown, []], [ 'picks known tokens only', 'google.com, geosite:google, geosite:blabla', siteKnown, ['geosite:google'], ], [ 'keeps the value order', 'geosite:cn, google.com, geosite:google', siteKnown, ['geosite:cn', 'geosite:google'], ], ['drops duplicates', 'geosite:google, geosite:google', siteKnown, ['geosite:google']], ['attributes are distinct tokens', 'geosite:google@ads', siteKnown, ['geosite:google@ads']], [ 'ext tokens are selectable', 'ext:my_rules.dat:corp, ext:other.dat:x', siteKnown, ['ext:my_rules.dat:corp'], ], ['negated ip tokens stay unselected', '!geoip:cn, geoip:private', ipKnown, ['geoip:private']], ]; it.each(cases)('%s', (_name, value, known, expected) => { expect(selectionFromValue(value, known)).toEqual(expected); }); }); describe('mergeSelection', () => { const cases: Array<[string, string, string[], ReadonlySet, string]> = [ ['adds to an empty field', '', ['geosite:google'], siteKnown, 'geosite:google'], [ 'adds after a plain domain', 'google.com', ['geosite:cn'], siteKnown, 'google.com, geosite:cn', ], [ 'keeps plain and unknown tokens when a category is unchecked', 'google.com, geosite:google, geosite:blabla', [], siteKnown, 'google.com, geosite:blabla', ], [ 'unchecking one known token leaves the other known token', 'geosite:google, geosite:cn', ['geosite:cn'], siteKnown, 'geosite:cn', ], [ 'preserves the original order of surviving tokens', 'geosite:cn, google.com, geosite:google', ['geosite:google', 'geosite:cn'], siteKnown, 'geosite:cn, google.com, geosite:google', ], [ 'appends new selections in selection order', 'google.com', ['geosite:cn', 'geosite:google'], siteKnown, 'google.com, geosite:cn, geosite:google', ], [ 'never duplicates an already present token', 'geosite:google, google.com', ['geosite:google'], siteKnown, 'geosite:google, google.com', ], [ 'collapses duplicates already in the field', 'google.com, google.com, geosite:google', ['geosite:google'], siteKnown, 'google.com, geosite:google', ], [ 'handles ext tokens like shorthand ones', 'ext:my_rules.dat:corp, google.com', [], siteKnown, 'google.com', ], [ 'adds an ext token from a custom database', '10.0.0.0/8', ['ext:my_ips.dat:office'], ipKnown, '10.0.0.0/8, ext:my_ips.dat:office', ], [ 'leaves a negated ip token untouched while dropping a plain one', '!geoip:cn, geoip:private, 192.168.0.0/16', [], ipKnown, '!geoip:cn, 192.168.0.0/16', ], [ 'adds a geoip token next to an existing negation', '!geoip:cn', ['geoip:private'], ipKnown, '!geoip:cn, geoip:private', ], [ 'ignores whitespace around field tokens', ' google.com , geosite:google ', ['geosite:google'], siteKnown, 'google.com, geosite:google', ], ['clearing every known token can empty the field', 'geosite:google', [], siteKnown, ''], ]; it.each(cases)('%s', (_name, value, selected, known, expected) => { expect(mergeSelection(value, selected, known)).toBe(expected); }); it('round-trips with selectionFromValue', () => { const value = mergeSelection( 'google.com, geosite:blabla', ['geosite:google', 'geosite:cn'], siteKnown, ); expect(value).toBe('google.com, geosite:blabla, geosite:google, geosite:cn'); expect(selectionFromValue(value, siteKnown)).toEqual(['geosite:google', 'geosite:cn']); }); }); describe('token matching tolerates the spellings Xray accepts', () => { const cases: Array<[string, string, string[], string]> = [ [ 'an uppercase token is recognised instead of duplicated', 'GEOSITE:GOOGLE', ['geosite:google'], 'GEOSITE:GOOGLE', ], [ 'the long ext form of a default database is the same token as its shorthand', 'ext:geosite.dat:google', ['geosite:google'], 'ext:geosite.dat:google', ], [ 'clearing a category written in its long form removes it', 'google.com, ext:geosite.dat:google', [], 'google.com', ], [ 'clearing a category written in uppercase removes it', 'GEOSITE:GOOGLE, google.com', [], 'google.com', ], [ 'a token from a database that was never opened survives untouched', 'ext:other.dat:x, geosite:google', ['geosite:cn'], 'ext:other.dat:x, geosite:cn', ], ['a value of separators alone collapses to empty', ',,, ,', [], ''], ]; it.each(cases)('%s', (_name, value, selected, expected) => { expect(mergeSelection(value, selected, siteKnown)).toBe(expected); }); it('seeds the selection from tokens written in another spelling', () => { expect(selectionFromValue('GEOSITE:GOOGLE, ext:geosite.dat:cn', siteKnown)).toEqual([ 'GEOSITE:GOOGLE', 'ext:geosite.dat:cn', ]); }); });