test(frontend): golden fixtures for DNS, Balancer, Rule schemas

Adds JSON fixtures under golden/fixtures/{dns,dns-server,balancer,rule}
plus three vitest files that parse them through the new schemas and
snapshot the result.

dns/: minimal (servers as strings) + full (every top-level field plus
hosts with geosite/domain/full prefixes and 5 mixed string/object
servers covering fakedns, localhost, https://, tcp://, quic+local://).

dns-server/: full (every DnsServerObject field) + legacy-expectips
(asserts the z.preprocess that migrates the legacy `expectIPs` key
into the canonical `expectedIPs`).

balancer/: random-minimal (default strategy by omission), roundrobin,
leastping, leastload-full (covers all StrategySettings fields and both
regexp=true|false costs).

rule/: minimal, full (exercises every RuleObject field including
localPort, localIP, process aliases like `self/`, all four protocol
enum values, ip negation `!geoip:`, attrs with regexp value, and the
WebhookObject with deduplication+headers), balancer-routed (uses
balancerTag instead of outboundTag), port-number (port as a number to
prove the union(number,string) accepts both).
This commit is contained in:
MHSanaei
2026-05-26 23:36:27 +02:00
parent 0208396802
commit a6a3ef8e64
18 changed files with 568 additions and 0 deletions

View File

@@ -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",
}
`;

View File

@@ -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,
}
`;

View File

@@ -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",
}
`;

View File

@@ -0,0 +1,26 @@
/// <reference types="vite/client" />
import { describe, expect, it } from 'vitest';
import { BalancerObjectSchema } from '@/schemas/routing';
const fixtures = import.meta.glob<unknown>(
'./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();
});
}
});

View File

@@ -0,0 +1,43 @@
/// <reference types="vite/client" />
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<unknown>(
'./golden/fixtures/dns/*.json',
{ eager: true, import: 'default' },
);
const serverFixtures = import.meta.glob<unknown>(
'./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();
});
}
});

View File

@@ -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 }
]
}
}
}

View File

@@ -0,0 +1,8 @@
{
"tag": "balancer-ping",
"selector": ["proxy-"],
"fallbackTag": "fallback-out",
"strategy": {
"type": "leastPing"
}
}

View File

@@ -0,0 +1,4 @@
{
"tag": "balancer-random",
"selector": ["proxy-"]
}

View File

@@ -0,0 +1,8 @@
{
"tag": "balancer-rr",
"selector": ["proxy-a", "proxy-b", "proxy-c"],
"fallbackTag": "direct",
"strategy": {
"type": "roundRobin"
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,6 @@
{
"address": "8.8.8.8",
"port": 53,
"domains": ["geosite:cn"],
"expectIPs": ["geoip:cn", "10.0.0.0/8"]
}

View File

@@ -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
}
]
}

View File

@@ -0,0 +1,6 @@
{
"servers": [
"8.8.8.8",
"1.1.1.1"
]
}

View File

@@ -0,0 +1,6 @@
{
"type": "field",
"domain": ["geosite:geolocation-!cn"],
"balancerTag": "balancer-load",
"ruleTag": "outbound-load-balance"
}

View File

@@ -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"
}
}
}

View File

@@ -0,0 +1,4 @@
{
"type": "field",
"outboundTag": "direct"
}

View File

@@ -0,0 +1,6 @@
{
"type": "field",
"port": 443,
"network": "tcp",
"outboundTag": "tls-out"
}

View File

@@ -0,0 +1,26 @@
/// <reference types="vite/client" />
import { describe, expect, it } from 'vitest';
import { RuleObjectSchema } from '@/schemas/routing';
const fixtures = import.meta.glob<unknown>(
'./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();
});
}
});