@opensecurity/zonzon-core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,685 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ import { validateServerConfig, validateHostConfig } from "./schema.js";
4
+
5
+ describe("Schema Validation - Server Config", () => {
6
+ it("accepts valid server config with minimal hosts", async () => {
7
+ const config = {
8
+ port: 53,
9
+ hosts: {
10
+ "localhost": {
11
+ records: [{ type: "A", address: "127.0.0.1" }],
12
+ },
13
+ },
14
+ };
15
+
16
+ const result = validateServerConfig(config);
17
+ assert.strictEqual(result.port, 53);
18
+ assert.ok("localhost" in result.hosts);
19
+ assert.strictEqual(result.hosts["localhost"].records.length, 1);
20
+ });
21
+
22
+ it("accepts server config with HTTP proxy headers", async () => {
23
+ const config = {
24
+ port: 53,
25
+ hosts: {
26
+ "app.loop": {
27
+ records: [{ type: "A", address: "10.0.0.1" }],
28
+ http_proxy: {
29
+ enabled: true,
30
+ upstream: "http://localhost:8080",
31
+ headers: {
32
+ "X-Debug": "true",
33
+ "X-Environment": "dev",
34
+ },
35
+ },
36
+ },
37
+ },
38
+ };
39
+
40
+ const result = validateServerConfig(config);
41
+ assert.ok(result.hosts["app.loop"].http_proxy?.enabled);
42
+ assert.strictEqual(result.hosts["app.loop"].http_proxy!.headers["X-Debug"], "true");
43
+ });
44
+
45
+ it("accepts server config with redirect", async () => {
46
+ const config = {
47
+ port: 53,
48
+ hosts: {
49
+ "redirect.loop": {
50
+ records: [{ type: "A", address: "10.0.0.2" }],
51
+ redirect: {
52
+ code: 302,
53
+ target: "https://production.example.com",
54
+ },
55
+ },
56
+ },
57
+ };
58
+
59
+ const result = validateServerConfig(config);
60
+ assert.strictEqual(result.hosts["redirect.loop"].redirect?.code, 302);
61
+ assert.strictEqual(result.hosts["redirect.loop"].redirect?.target, "https://production.example.com");
62
+ });
63
+
64
+ it("rejects invalid port", async () => {
65
+ const config = {
66
+ port: 99999,
67
+ hosts: {},
68
+ };
69
+
70
+ assert.throws(() => validateServerConfig(config), /error/i);
71
+ });
72
+
73
+ it("rejects non-numeric port", async () => {
74
+ const config = {
75
+ port: "abc" as unknown as number,
76
+ hosts: {},
77
+ };
78
+
79
+ assert.throws(() => validateServerConfig(config), /error/i);
80
+ });
81
+
82
+ it("rejects host with invalid hostname format", async () => {
83
+ const config = {
84
+ port: 53,
85
+ hosts: {
86
+ "invalid hostname": {
87
+ records: [{ type: "A", address: "127.0.0.1" }],
88
+ },
89
+ },
90
+ };
91
+
92
+ assert.throws(() => validateServerConfig(config), /error/i);
93
+ });
94
+
95
+ it("accepts host with empty records array (no DNS responses)", async () => {
96
+ const config = {
97
+ port: 53,
98
+ hosts: {
99
+ "test.loop": {
100
+ records: [],
101
+ },
102
+ },
103
+ };
104
+
105
+ const result = validateServerConfig(config);
106
+ assert.strictEqual(result.hosts["test.loop"].records.length, 0);
107
+ });
108
+
109
+ it("rejects TXT with CRLF injection", async () => {
110
+ const config = {
111
+ port: 53,
112
+ hosts: {
113
+ "evil.loop": {
114
+ records: [{ type: "TXT", data: ["value\r\nInjected"] }],
115
+ },
116
+ },
117
+ };
118
+
119
+ assert.throws(() => validateServerConfig(config), /error/i);
120
+ });
121
+
122
+ it("rejects header values with CRLF injection", async () => {
123
+ const config = {
124
+ port: 53,
125
+ hosts: {
126
+ "app.loop": {
127
+ records: [{ type: "A", address: "10.0.0.1" }],
128
+ http_proxy: {
129
+ enabled: true,
130
+ upstream: "http://localhost:8080",
131
+ headers: {
132
+ "X-Evil": "value\r\nInjected-Header: yes",
133
+ },
134
+ },
135
+ },
136
+ },
137
+ };
138
+
139
+ assert.throws(() => validateServerConfig(config), /error/i);
140
+ });
141
+
142
+ it("rejects header names with special characters", async () => {
143
+ const config = {
144
+ port: 53,
145
+ hosts: {
146
+ "app.loop": {
147
+ records: [{ type: "A", address: "10.0.0.1" }],
148
+ http_proxy: {
149
+ enabled: true,
150
+ upstream: "http://localhost:8080",
151
+ headers: {
152
+ "X- Evil Header": "value",
153
+ },
154
+ },
155
+ },
156
+ },
157
+ };
158
+
159
+ assert.throws(() => validateServerConfig(config), /error/i);
160
+ });
161
+
162
+ it("rejects invalid IPv4 address", async () => {
163
+ const config = {
164
+ port: 53,
165
+ hosts: {
166
+ "test.loop": {
167
+ records: [{ type: "A", address: "256.0.0.1" }],
168
+ },
169
+ },
170
+ };
171
+
172
+ assert.throws(() => validateServerConfig(config), /error/i);
173
+ });
174
+
175
+ it("rejects invalid IPv6 address", async () => {
176
+ const config = {
177
+ port: 53,
178
+ hosts: {
179
+ "test.loop": {
180
+ records: [{ type: "AAAA", address: "zzzz::1" }],
181
+ },
182
+ },
183
+ };
184
+
185
+ assert.throws(() => validateServerConfig(config), /error/i);
186
+ });
187
+
188
+ it("rejects TXT exceeding 255 characters per segment", async () => {
189
+ const longValue = "x".repeat(256);
190
+ const config = {
191
+ port: 53,
192
+ hosts: {
193
+ "txt.loop": {
194
+ records: [{ type: "TXT", data: [longValue] }],
195
+ },
196
+ },
197
+ };
198
+
199
+ assert.throws(() => validateServerConfig(config), /error/i);
200
+ });
201
+
202
+ it("rejects redirect with invalid code", async () => {
203
+ const config = {
204
+ port: 53,
205
+ hosts: {
206
+ "redir.loop": {
207
+ records: [{ type: "A", address: "1.2.3.4" }],
208
+ redirect: {
209
+ code: 999,
210
+ target: "https://example.com",
211
+ },
212
+ },
213
+ },
214
+ };
215
+
216
+ assert.throws(() => validateServerConfig(config), /error/i);
217
+ });
218
+
219
+ it("accepts multiple record types for same host", async () => {
220
+ const config = {
221
+ port: 53,
222
+ hosts: {
223
+ "multi.loop": {
224
+ records: [
225
+ { type: "A", address: "10.0.0.1" },
226
+ { type: "AAAA", address: "::1" },
227
+ { type: "TXT", data: ["v=spf1 ~all"] },
228
+ ],
229
+ },
230
+ },
231
+ };
232
+
233
+ const result = validateServerConfig(config);
234
+ assert.strictEqual(result.hosts["multi.loop"].records.length, 3);
235
+ });
236
+
237
+ it("rejects CNAME pointing to invalid hostname", async () => {
238
+ const config = {
239
+ port: 53,
240
+ hosts: {
241
+ "bad.loop": {
242
+ records: [{ type: "CNAME", target: "not a valid host!" }],
243
+ },
244
+ },
245
+ };
246
+
247
+ assert.throws(() => validateServerConfig(config), /error/i);
248
+ });
249
+
250
+ it("accepts _dmarc hostnames (valid DKIM/DKSP record pattern)", async () => {
251
+ const config = {
252
+ port: 53,
253
+ hosts: {
254
+ "_dmarc.example.loop": {
255
+ records: [{ type: "TXT", data: ["v=DMARC1"] }],
256
+ },
257
+ },
258
+ };
259
+
260
+ const result = validateServerConfig(config);
261
+ assert.ok("_dmarc.example.loop" in result.hosts);
262
+ });
263
+
264
+ it("accepts properly formatted underscore hostnames (valid per RFC)", async () => {
265
+ const config = {
266
+ port: 53,
267
+ hosts: {
268
+ "_service.loop": {
269
+ records: [{ type: "SRV", priority: 10, weight: 5, port: 8080, target: "app.loop" }],
270
+ },
271
+ },
272
+ };
273
+
274
+ const result = validateServerConfig(config);
275
+ assert.ok("_service.loop" in result.hosts);
276
+ });
277
+ });
278
+
279
+ describe("Schema Validation - Host Config", () => {
280
+ it("accepts minimal host config with single A record", async () => {
281
+ const config = {
282
+ records: [{ type: "A", address: "127.0.0.1" }],
283
+ };
284
+
285
+ const result = validateHostConfig(config);
286
+ assert.strictEqual(result.records.length, 1);
287
+ assert.strictEqual(result.records[0].type, "A");
288
+ });
289
+
290
+ it("accepts host config with CNAME and A records", async () => {
291
+ const config = {
292
+ records: [
293
+ { type: "CNAME", target: "target.loop" },
294
+ { type: "A", address: "10.0.0.1" },
295
+ ],
296
+ };
297
+
298
+ const result = validateHostConfig(config);
299
+ assert.strictEqual(result.records.length, 2);
300
+ });
301
+
302
+ it("defaults records to empty array if omitted", async () => {
303
+ const config = {};
304
+ const result = validateHostConfig(config);
305
+ assert.strictEqual(Array.isArray(result.records), true);
306
+ assert.strictEqual(result.records.length, 0);
307
+ });
308
+
309
+ it("rejects proxy enabled without upstream", async () => {
310
+ const config = {
311
+ records: [{ type: "A", address: "127.0.0.1" }],
312
+ http_proxy: {
313
+ enabled: true,
314
+ headers: {},
315
+ },
316
+ };
317
+
318
+ assert.throws(() => validateHostConfig(config), /error/i);
319
+ });
320
+
321
+ it("rejects proxy with invalid header value containing newline", async () => {
322
+ const config = {
323
+ records: [{ type: "A", address: "127.0.0.1" }],
324
+ http_proxy: {
325
+ enabled: true,
326
+ upstream: "http://localhost:8080",
327
+ headers: {
328
+ "X-Bad": "value\nwith\nnewlines",
329
+ },
330
+ },
331
+ };
332
+
333
+ assert.throws(() => validateHostConfig(config), /error/i);
334
+ });
335
+
336
+ it("accepts host config with redirect to valid URL", async () => {
337
+ const config = {
338
+ records: [{ type: "A", address: "127.0.0.1" }],
339
+ redirect: {
340
+ code: 301,
341
+ target: "https://target.com/path?query=value",
342
+ },
343
+ };
344
+
345
+ const result = validateHostConfig(config);
346
+ assert.strictEqual(result.redirect?.code, 301);
347
+ });
348
+
349
+ it("rejects redirect with missing target", async () => {
350
+ const config = {
351
+ records: [{ type: "A", address: "127.0.0.1" }],
352
+ redirect: {
353
+ code: 302,
354
+ },
355
+ };
356
+
357
+ assert.throws(() => validateHostConfig(config), /error/i);
358
+ });
359
+ });
360
+
361
+ describe("Schema Validation - NS Record", () => {
362
+ it("accepts valid NS record", () => {
363
+ const config = validateHostConfig({
364
+ records: [{ type: "NS", target: "ns1.example.loop" }],
365
+ });
366
+ assert.strictEqual(config.records[0].type, "NS");
367
+ assert.strictEqual((config.records[0] as { type: "NS"; target: string }).target, "ns1.example.loop");
368
+ });
369
+
370
+ it("rejects NS record without target", () => {
371
+ const config = { records: [{ type: "NS" }] as unknown };
372
+ assert.throws(() => validateHostConfig(config), /error/i);
373
+ });
374
+
375
+ it("rejects NS record with invalid hostname target", () => {
376
+ const config = { records: [{ type: "NS", target: "invalid hostname!" }] as unknown };
377
+ assert.throws(() => validateHostConfig(config), /error/i);
378
+ });
379
+
380
+ it("rejects non-object NS record", () => {
381
+ assert.throws(() => validateHostConfig({ records: ["not an object"] }), /error/i);
382
+ });
383
+ });
384
+
385
+ describe("Schema Validation - MX Record Edge Cases", () => {
386
+ it("accepts MX with priority 0", () => {
387
+ const config = validateHostConfig({
388
+ records: [{ type: "MX", priority: 0, exchange: "mail.example.loop" }],
389
+ });
390
+ assert.strictEqual((config.records[0] as { type: "MX"; priority: number }).priority, 0);
391
+ });
392
+
393
+ it("accepts MX with maximum priority (65535)", () => {
394
+ const config = validateHostConfig({
395
+ records: [{ type: "MX", priority: 65535, exchange: "mail.example.loop" }],
396
+ });
397
+ assert.strictEqual((config.records[0] as { type: "MX"; priority: number }).priority, 65535);
398
+ });
399
+
400
+ it("rejects MX with negative priority", () => {
401
+ const config = { records: [{ type: "MX", priority: -1, exchange: "mail.loop" }] };
402
+ assert.throws(() => validateHostConfig(config), /error/i);
403
+ });
404
+
405
+ it("rejects MX with priority exceeding 65535", () => {
406
+ const config = { records: [{ type: "MX", priority: 65536, exchange: "mail.loop" }] };
407
+ assert.throws(() => validateHostConfig(config), /error/i);
408
+ });
409
+
410
+ it("rejects MX without exchange field", () => {
411
+ const config = { records: [{ type: "MX", priority: 10 }] };
412
+ assert.throws(() => validateHostConfig(config), /error/i);
413
+ });
414
+ });
415
+
416
+ describe("Schema Validation - SRV Record Edge Cases", () => {
417
+ it("accepts valid SRV record with minimum values", () => {
418
+ const config = validateHostConfig({
419
+ records: [{ type: "SRV", priority: 0, weight: 0, port: 1, target: "target.loop" }],
420
+ });
421
+ assert.strictEqual((config.records[0] as { type: "SRV"; priority: number }).priority, 0);
422
+ assert.strictEqual((config.records[0] as { type: "SRV"; weight: number }).weight, 0);
423
+ assert.strictEqual((config.records[0] as { type: "SRV"; port: number }).port, 1);
424
+ });
425
+
426
+ it("accepts SRV with maximum values", () => {
427
+ const config = validateHostConfig({
428
+ records: [{ type: "SRV", priority: 65535, weight: 65535, port: 65535, target: "target.loop" }],
429
+ });
430
+ assert.strictEqual((config.records[0] as { type: "SRV"; priority: number }).priority, 65535);
431
+ assert.strictEqual((config.records[0] as { type: "SRV"; port: number }).port, 65535);
432
+ });
433
+
434
+ it("rejects SRV with port 0", () => {
435
+ const config = { records: [{ type: "SRV", priority: 10, weight: 5, port: 0, target: "target.loop" }] };
436
+ assert.throws(() => validateHostConfig(config), /error/i);
437
+ });
438
+
439
+ it("rejects SRV with missing port", () => {
440
+ const config = { records: [{ type: "SRV", priority: 10, weight: 5, target: "target.loop" }] };
441
+ assert.throws(() => validateHostConfig(config), /error/i);
442
+ });
443
+
444
+ it("rejects SRV with negative priority", () => {
445
+ const config = { records: [{ type: "SRV", priority: -1, weight: 5, port: 80, target: "target.loop" }] };
446
+ assert.throws(() => validateHostConfig(config), /error/i);
447
+ });
448
+
449
+ it("rejects SRV with missing target", () => {
450
+ const config = { records: [{ type: "SRV", priority: 10, weight: 5, port: 80 }] };
451
+ assert.throws(() => validateHostConfig(config), /error/i);
452
+ });
453
+ });
454
+
455
+ describe("Schema Validation - PTR Record", () => {
456
+ it("accepts valid PTR record", () => {
457
+ const config = validateHostConfig({
458
+ records: [{ type: "PTR", target: "localhost" }],
459
+ });
460
+ assert.strictEqual(config.records[0].type, "PTR");
461
+ });
462
+
463
+ it("rejects PTR without target", () => {
464
+ const config = { records: [{ type: "PTR" }] as unknown };
465
+ assert.throws(() => validateHostConfig(config), /error/i);
466
+ });
467
+ });
468
+
469
+ describe("Schema Validation - IPv4 Edge Cases", () => {
470
+ it("rejects IPv4 with leading zeros (strict validator)", () => {
471
+ const config = { records: [{ type: "A", address: "010.0.0.1" }] };
472
+ assert.throws(() => validateHostConfig(config), /error/i);
473
+ });
474
+
475
+ it("rejects IPv4 with too many octets", () => {
476
+ const config = { records: [{ type: "A", address: "1.2.3.4.5" }] };
477
+ assert.throws(() => validateHostConfig(config), /error/i);
478
+ });
479
+
480
+ it("rejects IPv4 with too few octets", () => {
481
+ const config = { records: [{ type: "A", address: "1.2.3" }] };
482
+ assert.throws(() => validateHostConfig(config), /error/i);
483
+ });
484
+
485
+ it("rejects IPv4 with non-numeric octets", () => {
486
+ const config = { records: [{ type: "A", address: "1.2.x.4" }] };
487
+ assert.throws(() => validateHostConfig(config), /error/i);
488
+ });
489
+
490
+ it("accepts IPv4 minimum boundary (0.0.0.0)", () => {
491
+ const config = validateHostConfig({
492
+ records: [{ type: "A", address: "0.0.0.0" }],
493
+ });
494
+ assert.strictEqual((config.records[0] as { type: "A"; address: string }).address, "0.0.0.0");
495
+ });
496
+
497
+ it("accepts IPv4 maximum boundary (255.255.255.255)", () => {
498
+ const config = validateHostConfig({
499
+ records: [{ type: "A", address: "255.255.255.255" }],
500
+ });
501
+ assert.strictEqual((config.records[0] as { type: "A"; address: string }).address, "255.255.255.255");
502
+ });
503
+ });
504
+
505
+ describe("Schema Validation - IPv6 Edge Cases", () => {
506
+ it("rejects :: with too many explicit groups (9 total)", () => {
507
+ const config = { records: [{ type: "AAAA", address: "::1:2:3:4:5:6:7:8" }] };
508
+ assert.throws(() => validateHostConfig(config), /error/i);
509
+ });
510
+
511
+ it("accepts :: as all-zeros shorthand", () => {
512
+ const config = validateHostConfig({
513
+ records: [{ type: "AAAA", address: "::" }],
514
+ });
515
+ assert.strictEqual((config.records[0] as { type: "AAAA"; address: string }).address, "::");
516
+ });
517
+
518
+ it("rejects partial expansion with :: but too many groups (11 total)", () => {
519
+ const config = { records: [{ type: "AAAA", address: "2001:db8::1:2:3:4:5:6" }] };
520
+ assert.throws(() => validateHostConfig(config), /error/i);
521
+ });
522
+
523
+ it("rejects full form with wrong number of groups (9 instead of 8)", () => {
524
+ const config = { records: [{ type: "AAAA", address: "2001:db8:85a3:0:0:8a2e:370:7334:1" }] };
525
+ assert.throws(() => validateHostConfig(config), /error/i);
526
+ });
527
+
528
+ it("accepts ::1 shorthand for loopback", () => {
529
+ const config = validateHostConfig({
530
+ records: [{ type: "AAAA", address: "::1" }],
531
+ });
532
+ assert.strictEqual((config.records[0] as { type: "AAAA"; address: string }).address, "::1");
533
+ });
534
+ });
535
+
536
+ describe("Schema Validation - CRLF Injection Vectors", () => {
537
+ it("rejects hostname with CR/LF in host key", () => {
538
+ const config = {
539
+ port: 53,
540
+ hosts: {
541
+ "evil.loop\r\nInjected: header": { records: [{ type: "A", address: "127.0.0.1" }] },
542
+ },
543
+ };
544
+ assert.throws(() => validateServerConfig(config), /error/i);
545
+ });
546
+
547
+ it("rejects redirect target with CRLF injection", () => {
548
+ const config = {
549
+ port: 53,
550
+ hosts: {
551
+ "redir.loop": {
552
+ records: [{ type: "A", address: "1.2.3.4" }],
553
+ redirect: { code: 302, target: "https://evil.com\r\nSet-Cookie: hacked=true" },
554
+ },
555
+ },
556
+ };
557
+ assert.throws(() => validateServerConfig(config), /error/i);
558
+ });
559
+
560
+ it("rejects proxy upstream with CRLF", () => {
561
+ const config = {
562
+ port: 53,
563
+ hosts: {
564
+ "proxy.loop": {
565
+ records: [{ type: "A", address: "10.0.0.1" }],
566
+ http_proxy: {
567
+ enabled: true,
568
+ upstream: "http://evil.com\r\nInjected-Header: yes",
569
+ headers: {},
570
+ },
571
+ },
572
+ },
573
+ };
574
+ assert.throws(() => validateServerConfig(config), /error/i);
575
+ });
576
+ });
577
+
578
+ describe("Schema Validation - Port Boundaries", () => {
579
+ it("rejects port 0", () => {
580
+ const config = { port: 0, hosts: {} };
581
+ assert.throws(() => validateServerConfig(config), /error/i);
582
+ });
583
+
584
+ it("accepts minimum valid port (1)", () => {
585
+ const config = validateServerConfig({ port: 1, hosts: {} });
586
+ assert.strictEqual(config.port, 1);
587
+ });
588
+
589
+ it("accepts maximum valid port (65535)", () => {
590
+ const config = validateServerConfig({ port: 65535, hosts: {} });
591
+ assert.strictEqual(config.port, 65535);
592
+ });
593
+
594
+ it("rejects port above 65535", () => {
595
+ const config = { port: 65536, hosts: {} };
596
+ assert.throws(() => validateServerConfig(config), /error/i);
597
+ });
598
+
599
+ it("rejects port below 1", () => {
600
+ const config = { port: -1, hosts: {} };
601
+ assert.throws(() => validateServerConfig(config), /error/i);
602
+ });
603
+ });
604
+
605
+ describe("Schema Validation - Hostname Length Limits", () => {
606
+ it("accepts hostname at exactly 253 characters total (with dots)", () => {
607
+ const adjustedLong = `${"a".repeat(62)}.${"b".repeat(62)}.${"c".repeat(62)}.${"d".repeat(62)}.x`;
608
+ assert.strictEqual(adjustedLong.length, 253);
609
+ const config = validateServerConfig({
610
+ port: 53,
611
+ hosts: { [adjustedLong]: { records: [{ type: "A", address: "127.0.0.1" }] } },
612
+ });
613
+ assert.ok(adjustedLong in config.hosts);
614
+ });
615
+
616
+ it("rejects hostname at 254 characters (exceeds limit)", () => {
617
+ const tooLong = "a".repeat(254);
618
+ const config = { port: 53, hosts: { [tooLong]: { records: [{ type: "A", address: "127.0.0.1" }] } } };
619
+ assert.throws(() => validateServerConfig(config), /error/i);
620
+ });
621
+
622
+ it("accepts single-character labels (minimum)", () => {
623
+ const config = validateServerConfig({
624
+ port: 53,
625
+ hosts: { "a.b.c": { records: [{ type: "A", address: "127.0.0.1" }] } },
626
+ });
627
+ assert.ok("a.b.c" in config.hosts);
628
+ });
629
+
630
+ it("rejects label exceeding 63 characters", () => {
631
+ const tooLongLabel = "a".repeat(64);
632
+ const config = { port: 53, hosts: { [tooLongLabel]: { records: [{ type: "A", address: "127.0.0.1" }] } } };
633
+ assert.throws(() => validateServerConfig(config), /error/i);
634
+ });
635
+ });
636
+
637
+ describe("Schema Validation - Unsupported Record Types", () => {
638
+ it("rejects SOA record type", () => {
639
+ const config = { records: [{ type: "SOA", mname: "ns1.loop", rname: "admin.loop" }] as unknown };
640
+ assert.throws(() => validateHostConfig(config), /error/i);
641
+ });
642
+
643
+ it("rejects UNKNOWN record type", () => {
644
+ const config = { records: [{ type: "UNKNOWN" }] as unknown };
645
+ assert.throws(() => validateHostConfig(config), /error/i);
646
+ });
647
+
648
+ it("rejects empty string record type", () => {
649
+ const config = { records: [{ type: "" }] as unknown };
650
+ assert.throws(() => validateHostConfig(config), /error/i);
651
+ });
652
+ });
653
+
654
+ describe("Schema Validation - Config Type Safety", () => {
655
+ it("rejects null config", () => {
656
+ assert.throws(() => validateServerConfig(null as unknown), /error/i);
657
+ });
658
+
659
+ it("rejects undefined config", () => {
660
+ assert.throws(() => validateServerConfig(undefined as unknown), /error/i);
661
+ });
662
+
663
+ it("rejects string config", () => {
664
+ assert.throws(() => validateServerConfig("not a config" as unknown), /error/i);
665
+ });
666
+
667
+ it("rejects number config", () => {
668
+ assert.throws(() => validateServerConfig(42 as unknown), /error/i);
669
+ });
670
+
671
+ it("rejects array config (missing hosts)", () => {
672
+ assert.throws(() => validateServerConfig([1, 2, 3] as unknown), /error/i);
673
+ });
674
+
675
+ it("accepts default port when not specified", () => {
676
+ const config = { hosts: {} };
677
+ const validated = validateServerConfig(config);
678
+ assert.strictEqual(validated.port, 53);
679
+ });
680
+
681
+ it("rejects hosts that are not an object", () => {
682
+ const config = { port: 53, hosts: "not an object" };
683
+ assert.throws(() => validateServerConfig(config), /error/i);
684
+ });
685
+ });