diff --git a/frontend/src/test/__snapshots__/balancer.test.ts.snap b/frontend/src/test/__snapshots__/balancer.test.ts.snap new file mode 100644 index 00000000..9558e5dd --- /dev/null +++ b/frontend/src/test/__snapshots__/balancer.test.ts.snap @@ -0,0 +1,73 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`BalancerObjectSchema fixtures > parses leastload-full byte-stably 1`] = ` +{ + "fallbackTag": "fallback-out", + "selector": [ + "proxy-", + ], + "strategy": { + "settings": { + "baselines": [ + "500ms", + "1s", + "2s", + ], + "costs": [ + { + "match": "proxy-premium", + "regexp": false, + "value": 0.1, + }, + { + "match": "^proxy-cheap-.+$", + "regexp": true, + "value": 5, + }, + ], + "expected": 3, + "maxRTT": "1s", + "tolerance": 0.05, + }, + "type": "leastLoad", + }, + "tag": "balancer-load", +} +`; + +exports[`BalancerObjectSchema fixtures > parses leastping byte-stably 1`] = ` +{ + "fallbackTag": "fallback-out", + "selector": [ + "proxy-", + ], + "strategy": { + "type": "leastPing", + }, + "tag": "balancer-ping", +} +`; + +exports[`BalancerObjectSchema fixtures > parses random-minimal byte-stably 1`] = ` +{ + "selector": [ + "proxy-", + ], + "tag": "balancer-random", +} +`; + +exports[`BalancerObjectSchema fixtures > parses roundrobin byte-stably 1`] = ` +{ + "fallbackTag": "direct", + "selector": [ + "proxy-a", + "proxy-b", + "proxy-c", + ], + "strategy": { + "type": "roundRobin", + }, + "tag": "balancer-rr", +} +`; diff --git a/frontend/src/test/__snapshots__/dns.test.ts.snap b/frontend/src/test/__snapshots__/dns.test.ts.snap new file mode 100644 index 00000000..8ecafbb0 --- /dev/null +++ b/frontend/src/test/__snapshots__/dns.test.ts.snap @@ -0,0 +1,116 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`DnsObjectSchema fixtures > parses full byte-stably 1`] = ` +{ + "clientIp": "1.2.3.4", + "disableCache": false, + "disableFallback": false, + "disableFallbackIfMatch": true, + "enableParallelQuery": true, + "hosts": { + "domain:example.com": [ + "10.0.0.1", + "10.0.0.2", + ], + "full:dns.google": "8.8.8.8", + "geosite:category-ads-all": "127.0.0.1", + }, + "queryStrategy": "UseIP", + "serveExpiredTTL": 300, + "serveStale": true, + "servers": [ + "fakedns", + "localhost", + "https://dns.google/dns-query", + { + "address": "tcp://1.1.1.1", + "clientIP": "8.8.4.4", + "domains": [ + "geosite:cn", + ], + "expectedIPs": [ + "geoip:cn", + ], + "port": 53, + "queryStrategy": "UseIPv4", + "skipFallback": true, + "tag": "cn-dns", + "timeoutMs": 3000, + }, + { + "address": "quic+local://dns.adguard.com", + "disableCache": true, + "finalQuery": true, + "port": 53, + "serveExpiredTTL": 60, + "serveStale": false, + "timeoutMs": 5000, + "unexpectedIPs": [ + "geoip:private", + ], + }, + ], + "tag": "dns_inbound", + "useSystemHosts": true, +} +`; + +exports[`DnsObjectSchema fixtures > parses minimal byte-stably 1`] = ` +{ + "disableCache": false, + "disableFallback": false, + "disableFallbackIfMatch": false, + "enableParallelQuery": false, + "queryStrategy": "UseIP", + "serveExpiredTTL": 0, + "serveStale": false, + "servers": [ + "8.8.8.8", + "1.1.1.1", + ], + "useSystemHosts": false, +} +`; + +exports[`DnsServerObjectSchema fixtures > parses full byte-stably 1`] = ` +{ + "address": "https://dns.google/dns-query", + "clientIP": "9.9.9.9", + "disableCache": false, + "domains": [ + "domain:google.com", + "domain:youtube.com", + "geosite:google", + ], + "expectedIPs": [ + "geoip:us", + "1.2.3.0/24", + ], + "finalQuery": false, + "port": 443, + "queryStrategy": "UseIPv6", + "serveExpiredTTL": 600, + "serveStale": true, + "skipFallback": false, + "tag": "google-doh", + "timeoutMs": 4000, + "unexpectedIPs": [ + "geoip:private", + ], +} +`; + +exports[`DnsServerObjectSchema fixtures > parses legacy-expectips byte-stably 1`] = ` +{ + "address": "8.8.8.8", + "domains": [ + "geosite:cn", + ], + "expectedIPs": [ + "geoip:cn", + "10.0.0.0/8", + ], + "port": 53, + "timeoutMs": 4000, +} +`; diff --git a/frontend/src/test/__snapshots__/rule.test.ts.snap b/frontend/src/test/__snapshots__/rule.test.ts.snap new file mode 100644 index 00000000..cace97ed --- /dev/null +++ b/frontend/src/test/__snapshots__/rule.test.ts.snap @@ -0,0 +1,91 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`RuleObjectSchema fixtures > parses balancer-routed byte-stably 1`] = ` +{ + "balancerTag": "balancer-load", + "domain": [ + "geosite:geolocation-!cn", + ], + "ruleTag": "outbound-load-balance", + "type": "field", +} +`; + +exports[`RuleObjectSchema fixtures > parses full byte-stably 1`] = ` +{ + "attrs": { + "Host": "example.com", + "User-Agent": "regexp:^Mozilla.*", + }, + "domain": [ + "domain:google.com", + "full:example.com", + "keyword:cdn", + "regexp:^api\\.example\\.com$", + "geosite:cn", + ], + "inboundTag": [ + "inbound-1", + "inbound-2", + ], + "ip": [ + "10.0.0.0/8", + "geoip:cn", + "geoip:private", + "!geoip:cn", + ], + "localIP": [ + "10.10.10.0/24", + ], + "localPort": "5353", + "network": "tcp,udp", + "outboundTag": "proxy-out", + "port": "80,443,1000-2000", + "process": [ + "chrome.exe", + "curl", + "self/", + ], + "protocol": [ + "http", + "tls", + "quic", + "bittorrent", + ], + "ruleTag": "main-policy-rule", + "sourceIP": [ + "192.168.0.0/16", + "geoip:private", + ], + "sourcePort": "53", + "type": "field", + "user": [ + "user@example.com", + "regexp:^.+@admin\\..+$", + ], + "vlessRoute": "443,8443", + "webhook": { + "deduplication": 30, + "headers": { + "X-Auth-Token": "secret", + }, + "url": "https://hook.example.com/events", + }, +} +`; + +exports[`RuleObjectSchema fixtures > parses minimal byte-stably 1`] = ` +{ + "outboundTag": "direct", + "type": "field", +} +`; + +exports[`RuleObjectSchema fixtures > parses port-number byte-stably 1`] = ` +{ + "network": "tcp", + "outboundTag": "tls-out", + "port": 443, + "type": "field", +} +`; diff --git a/frontend/src/test/balancer.test.ts b/frontend/src/test/balancer.test.ts new file mode 100644 index 00000000..4ab1c208 --- /dev/null +++ b/frontend/src/test/balancer.test.ts @@ -0,0 +1,26 @@ +/// +import { describe, expect, it } from 'vitest'; + +import { BalancerObjectSchema } from '@/schemas/routing'; + +const fixtures = import.meta.glob( + './golden/fixtures/balancer/*.json', + { eager: true, import: 'default' }, +); + +function fixtureName(path: string): string { + const file = path.split('/').pop() ?? path; + return file.replace(/\.json$/, ''); +} + +describe('BalancerObjectSchema fixtures', () => { + const entries = Object.entries(fixtures).sort(([a], [b]) => a.localeCompare(b)); + expect(entries.length, 'expected at least one fixture under golden/fixtures/balancer').toBeGreaterThan(0); + + for (const [path, raw] of entries) { + it(`parses ${fixtureName(path)} byte-stably`, () => { + const parsed = BalancerObjectSchema.parse(raw); + expect(parsed).toMatchSnapshot(); + }); + } +}); diff --git a/frontend/src/test/dns.test.ts b/frontend/src/test/dns.test.ts new file mode 100644 index 00000000..6d9b894f --- /dev/null +++ b/frontend/src/test/dns.test.ts @@ -0,0 +1,43 @@ +/// +import { describe, expect, it } from 'vitest'; + +import { DnsObjectSchema, DnsServerObjectSchema } from '@/schemas/dns'; + +function fixtureName(path: string): string { + const file = path.split('/').pop() ?? path; + return file.replace(/\.json$/, ''); +} + +const dnsFixtures = import.meta.glob( + './golden/fixtures/dns/*.json', + { eager: true, import: 'default' }, +); + +const serverFixtures = import.meta.glob( + './golden/fixtures/dns-server/*.json', + { eager: true, import: 'default' }, +); + +describe('DnsObjectSchema fixtures', () => { + const entries = Object.entries(dnsFixtures).sort(([a], [b]) => a.localeCompare(b)); + expect(entries.length, 'expected at least one fixture under golden/fixtures/dns').toBeGreaterThan(0); + + for (const [path, raw] of entries) { + it(`parses ${fixtureName(path)} byte-stably`, () => { + const parsed = DnsObjectSchema.parse(raw); + expect(parsed).toMatchSnapshot(); + }); + } +}); + +describe('DnsServerObjectSchema fixtures', () => { + const entries = Object.entries(serverFixtures).sort(([a], [b]) => a.localeCompare(b)); + expect(entries.length, 'expected at least one fixture under golden/fixtures/dns-server').toBeGreaterThan(0); + + for (const [path, raw] of entries) { + it(`parses ${fixtureName(path)} byte-stably`, () => { + const parsed = DnsServerObjectSchema.parse(raw); + expect(parsed).toMatchSnapshot(); + }); + } +}); diff --git a/frontend/src/test/golden/fixtures/balancer/leastload-full.json b/frontend/src/test/golden/fixtures/balancer/leastload-full.json new file mode 100644 index 00000000..9cea1f67 --- /dev/null +++ b/frontend/src/test/golden/fixtures/balancer/leastload-full.json @@ -0,0 +1,18 @@ +{ + "tag": "balancer-load", + "selector": ["proxy-"], + "fallbackTag": "fallback-out", + "strategy": { + "type": "leastLoad", + "settings": { + "expected": 3, + "maxRTT": "1s", + "tolerance": 0.05, + "baselines": ["500ms", "1s", "2s"], + "costs": [ + { "regexp": false, "match": "proxy-premium", "value": 0.1 }, + { "regexp": true, "match": "^proxy-cheap-.+$", "value": 5 } + ] + } + } +} diff --git a/frontend/src/test/golden/fixtures/balancer/leastping.json b/frontend/src/test/golden/fixtures/balancer/leastping.json new file mode 100644 index 00000000..c7465c5b --- /dev/null +++ b/frontend/src/test/golden/fixtures/balancer/leastping.json @@ -0,0 +1,8 @@ +{ + "tag": "balancer-ping", + "selector": ["proxy-"], + "fallbackTag": "fallback-out", + "strategy": { + "type": "leastPing" + } +} diff --git a/frontend/src/test/golden/fixtures/balancer/random-minimal.json b/frontend/src/test/golden/fixtures/balancer/random-minimal.json new file mode 100644 index 00000000..53a32f21 --- /dev/null +++ b/frontend/src/test/golden/fixtures/balancer/random-minimal.json @@ -0,0 +1,4 @@ +{ + "tag": "balancer-random", + "selector": ["proxy-"] +} diff --git a/frontend/src/test/golden/fixtures/balancer/roundrobin.json b/frontend/src/test/golden/fixtures/balancer/roundrobin.json new file mode 100644 index 00000000..f21aca1d --- /dev/null +++ b/frontend/src/test/golden/fixtures/balancer/roundrobin.json @@ -0,0 +1,8 @@ +{ + "tag": "balancer-rr", + "selector": ["proxy-a", "proxy-b", "proxy-c"], + "fallbackTag": "direct", + "strategy": { + "type": "roundRobin" + } +} diff --git a/frontend/src/test/golden/fixtures/dns-server/full.json b/frontend/src/test/golden/fixtures/dns-server/full.json new file mode 100644 index 00000000..94efebb6 --- /dev/null +++ b/frontend/src/test/golden/fixtures/dns-server/full.json @@ -0,0 +1,25 @@ +{ + "address": "https://dns.google/dns-query", + "port": 443, + "domains": [ + "domain:google.com", + "domain:youtube.com", + "geosite:google" + ], + "expectedIPs": [ + "geoip:us", + "1.2.3.0/24" + ], + "unexpectedIPs": [ + "geoip:private" + ], + "skipFallback": false, + "finalQuery": false, + "tag": "google-doh", + "clientIP": "9.9.9.9", + "queryStrategy": "UseIPv6", + "disableCache": false, + "timeoutMs": 4000, + "serveStale": true, + "serveExpiredTTL": 600 +} diff --git a/frontend/src/test/golden/fixtures/dns-server/legacy-expectips.json b/frontend/src/test/golden/fixtures/dns-server/legacy-expectips.json new file mode 100644 index 00000000..07eb2e03 --- /dev/null +++ b/frontend/src/test/golden/fixtures/dns-server/legacy-expectips.json @@ -0,0 +1,6 @@ +{ + "address": "8.8.8.8", + "port": 53, + "domains": ["geosite:cn"], + "expectIPs": ["geoip:cn", "10.0.0.0/8"] +} diff --git a/frontend/src/test/golden/fixtures/dns/full.json b/frontend/src/test/golden/fixtures/dns/full.json new file mode 100644 index 00000000..a8a5cb4f --- /dev/null +++ b/frontend/src/test/golden/fixtures/dns/full.json @@ -0,0 +1,42 @@ +{ + "tag": "dns_inbound", + "clientIp": "1.2.3.4", + "queryStrategy": "UseIP", + "disableCache": false, + "disableFallback": false, + "disableFallbackIfMatch": true, + "enableParallelQuery": true, + "useSystemHosts": true, + "serveStale": true, + "serveExpiredTTL": 300, + "hosts": { + "geosite:category-ads-all": "127.0.0.1", + "domain:example.com": ["10.0.0.1", "10.0.0.2"], + "full:dns.google": "8.8.8.8" + }, + "servers": [ + "fakedns", + "localhost", + "https://dns.google/dns-query", + { + "address": "tcp://1.1.1.1", + "port": 53, + "domains": ["geosite:cn"], + "expectedIPs": ["geoip:cn"], + "queryStrategy": "UseIPv4", + "skipFallback": true, + "tag": "cn-dns", + "clientIP": "8.8.4.4", + "timeoutMs": 3000 + }, + { + "address": "quic+local://dns.adguard.com", + "unexpectedIPs": ["geoip:private"], + "finalQuery": true, + "disableCache": true, + "serveStale": false, + "serveExpiredTTL": 60, + "timeoutMs": 5000 + } + ] +} diff --git a/frontend/src/test/golden/fixtures/dns/minimal.json b/frontend/src/test/golden/fixtures/dns/minimal.json new file mode 100644 index 00000000..f1fda9d0 --- /dev/null +++ b/frontend/src/test/golden/fixtures/dns/minimal.json @@ -0,0 +1,6 @@ +{ + "servers": [ + "8.8.8.8", + "1.1.1.1" + ] +} diff --git a/frontend/src/test/golden/fixtures/rule/balancer-routed.json b/frontend/src/test/golden/fixtures/rule/balancer-routed.json new file mode 100644 index 00000000..c6d1fed1 --- /dev/null +++ b/frontend/src/test/golden/fixtures/rule/balancer-routed.json @@ -0,0 +1,6 @@ +{ + "type": "field", + "domain": ["geosite:geolocation-!cn"], + "balancerTag": "balancer-load", + "ruleTag": "outbound-load-balance" +} diff --git a/frontend/src/test/golden/fixtures/rule/full.json b/frontend/src/test/golden/fixtures/rule/full.json new file mode 100644 index 00000000..6a92bdd0 --- /dev/null +++ b/frontend/src/test/golden/fixtures/rule/full.json @@ -0,0 +1,60 @@ +{ + "type": "field", + "domain": [ + "domain:google.com", + "full:example.com", + "keyword:cdn", + "regexp:^api\\.example\\.com$", + "geosite:cn" + ], + "ip": [ + "10.0.0.0/8", + "geoip:cn", + "geoip:private", + "!geoip:cn" + ], + "port": "80,443,1000-2000", + "sourcePort": "53", + "localPort": "5353", + "network": "tcp,udp", + "sourceIP": [ + "192.168.0.0/16", + "geoip:private" + ], + "localIP": [ + "10.10.10.0/24" + ], + "user": [ + "user@example.com", + "regexp:^.+@admin\\..+$" + ], + "vlessRoute": "443,8443", + "inboundTag": [ + "inbound-1", + "inbound-2" + ], + "protocol": [ + "http", + "tls", + "quic", + "bittorrent" + ], + "attrs": { + "User-Agent": "regexp:^Mozilla.*", + "Host": "example.com" + }, + "process": [ + "chrome.exe", + "curl", + "self/" + ], + "outboundTag": "proxy-out", + "ruleTag": "main-policy-rule", + "webhook": { + "url": "https://hook.example.com/events", + "deduplication": 30, + "headers": { + "X-Auth-Token": "secret" + } + } +} diff --git a/frontend/src/test/golden/fixtures/rule/minimal.json b/frontend/src/test/golden/fixtures/rule/minimal.json new file mode 100644 index 00000000..f79896f6 --- /dev/null +++ b/frontend/src/test/golden/fixtures/rule/minimal.json @@ -0,0 +1,4 @@ +{ + "type": "field", + "outboundTag": "direct" +} diff --git a/frontend/src/test/golden/fixtures/rule/port-number.json b/frontend/src/test/golden/fixtures/rule/port-number.json new file mode 100644 index 00000000..7104df11 --- /dev/null +++ b/frontend/src/test/golden/fixtures/rule/port-number.json @@ -0,0 +1,6 @@ +{ + "type": "field", + "port": 443, + "network": "tcp", + "outboundTag": "tls-out" +} diff --git a/frontend/src/test/rule.test.ts b/frontend/src/test/rule.test.ts new file mode 100644 index 00000000..a4fa519c --- /dev/null +++ b/frontend/src/test/rule.test.ts @@ -0,0 +1,26 @@ +/// +import { describe, expect, it } from 'vitest'; + +import { RuleObjectSchema } from '@/schemas/routing'; + +const fixtures = import.meta.glob( + './golden/fixtures/rule/*.json', + { eager: true, import: 'default' }, +); + +function fixtureName(path: string): string { + const file = path.split('/').pop() ?? path; + return file.replace(/\.json$/, ''); +} + +describe('RuleObjectSchema fixtures', () => { + const entries = Object.entries(fixtures).sort(([a], [b]) => a.localeCompare(b)); + expect(entries.length, 'expected at least one fixture under golden/fixtures/rule').toBeGreaterThan(0); + + for (const [path, raw] of entries) { + it(`parses ${fixtureName(path)} byte-stably`, () => { + const parsed = RuleObjectSchema.parse(raw); + expect(parsed).toMatchSnapshot(); + }); + } +});