1
0

routing.test.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132
  1. import { describe, it, expect } from 'vitest';
  2. import {
  3. buildBalancer,
  4. buildRule,
  5. buildObservatory,
  6. buildRouting,
  7. buildRoutingJson,
  8. type RoutingInput,
  9. } from './routing';
  10. describe('buildBalancer', () => {
  11. it('emits tag, selector, and strategy.type; omits fallbackTag when empty', () => {
  12. const b = buildBalancer({ tag: 'lb', selector: ['proxy', 'hk-'], strategy: 'roundRobin' });
  13. expect(b.tag).toBe('lb');
  14. expect(b.selector).toEqual(['proxy', 'hk-']);
  15. expect((b.strategy as Record<string, unknown>).type).toBe('roundRobin');
  16. expect('fallbackTag' in b).toBe(false);
  17. });
  18. it('includes fallbackTag when set', () => {
  19. const b = buildBalancer({
  20. tag: 'lb',
  21. selector: ['a'],
  22. strategy: 'random',
  23. fallbackTag: 'direct',
  24. });
  25. expect(b.fallbackTag).toBe('direct');
  26. });
  27. });
  28. describe('buildRule', () => {
  29. it('emits only the matchers that are set, with outboundTag', () => {
  30. const r = buildRule({ domain: ['geosite:google'], target: { kind: 'outbound', tag: 'warp' } });
  31. expect(r.type).toBe('field');
  32. expect(r.domain).toEqual(['geosite:google']);
  33. expect('ip' in r).toBe(false);
  34. expect(r.outboundTag).toBe('warp');
  35. expect('balancerTag' in r).toBe(false);
  36. });
  37. it('uses balancerTag (and not outboundTag) when the target is a balancer', () => {
  38. const r = buildRule({ ip: ['geoip:cn'], target: { kind: 'balancer', tag: 'lb' } });
  39. expect(r.balancerTag).toBe('lb');
  40. expect('outboundTag' in r).toBe(false);
  41. expect(r.ip).toEqual(['geoip:cn']);
  42. });
  43. it('passes port + network through and carries ruleTag', () => {
  44. const r = buildRule({
  45. port: '443,8443',
  46. network: 'tcp,udp',
  47. ruleTag: 'r1',
  48. target: { kind: 'outbound', tag: 'direct' },
  49. });
  50. expect(r.port).toBe('443,8443');
  51. expect(r.network).toBe('tcp,udp');
  52. expect(r.ruleTag).toBe('r1');
  53. });
  54. });
  55. describe('buildObservatory', () => {
  56. it('observatory mode emits probeURL default + enableConcurrency, no burst', () => {
  57. const o = buildObservatory({ mode: 'observatory', subjectSelector: ['proxy'] });
  58. const obs = (o as Record<string, Record<string, unknown>>).observatory;
  59. expect(obs.probeURL).toBe('https://www.google.com/generate_204');
  60. expect(obs.subjectSelector).toEqual(['proxy']);
  61. expect(obs.enableConcurrency).toBe(true);
  62. expect('burstObservatory' in o).toBe(false);
  63. });
  64. it('burst mode emits pingConfig.destination default and no observatory', () => {
  65. const o = buildObservatory({ mode: 'burst', subjectSelector: ['proxy'] });
  66. const burst = (o as Record<string, Record<string, Record<string, unknown>>>).burstObservatory;
  67. expect(burst.pingConfig.destination).toBe('https://www.google.com/generate_204');
  68. expect('observatory' in o).toBe(false);
  69. });
  70. });
  71. describe('buildRouting', () => {
  72. const base: RoutingInput = {
  73. balancers: [{ tag: 'lb', selector: ['proxy'], strategy: 'random' }],
  74. rules: [{ domain: ['geosite:category-ads-all'], target: { kind: 'outbound', tag: 'block' } }],
  75. };
  76. it('nests rules and balancers under routing', () => {
  77. const out = buildRouting(base) as { routing: { rules: unknown[]; balancers: unknown[] } };
  78. expect(out.routing.rules).toHaveLength(1);
  79. expect(out.routing.balancers).toHaveLength(1);
  80. });
  81. it('puts observatory at the TOP level (not under routing) for a leastPing balancer', () => {
  82. const out = buildRouting({
  83. ...base,
  84. balancers: [{ tag: 'lb', selector: ['proxy'], strategy: 'leastPing' }],
  85. }) as Record<string, Record<string, unknown>>;
  86. expect(out.observatory).toBeDefined();
  87. expect('observatory' in out.routing).toBe(false);
  88. });
  89. it('uses a burstObservatory for a leastLoad balancer', () => {
  90. const out = buildRouting({
  91. ...base,
  92. balancers: [{ tag: 'lb', selector: ['proxy'], strategy: 'leastLoad' }],
  93. }) as Record<string, unknown>;
  94. expect(out.burstObservatory).toBeDefined();
  95. });
  96. it('carries domainStrategy when set', () => {
  97. const out = buildRouting({ ...base, domainStrategy: 'IPIfNonMatch' }) as {
  98. routing: Record<string, unknown>;
  99. };
  100. expect(out.routing.domainStrategy).toBe('IPIfNonMatch');
  101. });
  102. it('does not auto-add an observatory for random/roundRobin balancers', () => {
  103. const out = buildRouting(base) as Record<string, unknown>;
  104. expect('observatory' in out).toBe(false);
  105. expect('burstObservatory' in out).toBe(false);
  106. });
  107. });
  108. describe('buildRoutingJson', () => {
  109. it('round-trips and is 2-space indented', () => {
  110. const input: RoutingInput = {
  111. balancers: [],
  112. rules: [{ ip: ['geoip:private'], target: { kind: 'outbound', tag: 'direct' } }],
  113. };
  114. const json = buildRoutingJson(input);
  115. expect(json).toContain('\n "');
  116. expect(JSON.parse(json)).toEqual(buildRouting(input));
  117. });
  118. });