소스 검색

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).
MHSanaei 21 시간 전
부모
커밋
a6a3ef8e64

+ 73 - 0
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",
+}
+`;

+ 116 - 0
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,
+}
+`;

+ 91 - 0
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": [
+    "[email protected]",
+    "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",
+}
+`;

+ 26 - 0
frontend/src/test/balancer.test.ts

@@ -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();
+    });
+  }
+});

+ 43 - 0
frontend/src/test/dns.test.ts

@@ -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();
+    });
+  }
+});

+ 18 - 0
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 }
+      ]
+    }
+  }
+}

+ 8 - 0
frontend/src/test/golden/fixtures/balancer/leastping.json

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

+ 4 - 0
frontend/src/test/golden/fixtures/balancer/random-minimal.json

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

+ 8 - 0
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"
+  }
+}

+ 25 - 0
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
+}

+ 6 - 0
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"]
+}

+ 42 - 0
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
+    }
+  ]
+}

+ 6 - 0
frontend/src/test/golden/fixtures/dns/minimal.json

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

+ 6 - 0
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"
+}

+ 60 - 0
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": [
+    "[email protected]",
+    "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"
+    }
+  }
+}

+ 4 - 0
frontend/src/test/golden/fixtures/rule/minimal.json

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

+ 6 - 0
frontend/src/test/golden/fixtures/rule/port-number.json

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

+ 26 - 0
frontend/src/test/rule.test.ts

@@ -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();
+    });
+  }
+});