@logtape/syslog 1.3.4 → 1.4.0-dev.408

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,1675 @@
1
+ import { suite } from "@alinea/suite";
2
+ import {
3
+ assert,
4
+ assertEquals,
5
+ assertInstanceOf,
6
+ assertRejects,
7
+ assertThrows,
8
+ } from "@std/assert";
9
+ import type { LogRecord, Sink } from "@logtape/logtape";
10
+ import { createSocket } from "node:dgram";
11
+ import { createServer } from "node:net";
12
+ import {
13
+ DenoTcpSyslogConnection,
14
+ DenoUdpSyslogConnection,
15
+ getSyslogSink,
16
+ NodeTcpSyslogConnection,
17
+ NodeUdpSyslogConnection,
18
+ type SyslogFacility,
19
+ } from "./syslog.ts";
20
+
21
+ const test = suite(import.meta);
22
+
23
+ type TestSink = Sink & AsyncDisposable & {
24
+ readonly _internal_lastPromise: Promise<void>;
25
+ };
26
+
27
+ // RFC 5424 syslog message parser for testing
28
+ interface ParsedSyslogMessage {
29
+ priority: number;
30
+ version: number;
31
+ timestamp: string;
32
+ hostname: string;
33
+ appName: string;
34
+ procId: string;
35
+ msgId: string;
36
+ structuredData: string;
37
+ message: string;
38
+ }
39
+
40
+ function parseSyslogMessage(rawMessage: string): ParsedSyslogMessage {
41
+ // RFC 5424 format: <PRI>VERSION TIMESTAMP HOSTNAME APP-NAME PROCID MSGID STRUCTURED-DATA MSG
42
+ const regex = /^<(\d+)>(\d+) (\S+) (\S+) (\S+) (\S+) (\S+) (.*)$/;
43
+ const match = rawMessage.match(regex);
44
+
45
+ if (!match) {
46
+ throw new Error(`Invalid syslog message format: ${rawMessage}`);
47
+ }
48
+
49
+ const remaining = match[8];
50
+ let structuredData: string;
51
+ let message: string;
52
+
53
+ if (remaining.startsWith("[")) {
54
+ // Parse structured data - need to handle escaped brackets properly
55
+ let pos = 0;
56
+ let bracketCount = 0;
57
+ let inQuotes = false;
58
+ let escaped = false;
59
+
60
+ for (let i = 0; i < remaining.length; i++) {
61
+ const char = remaining[i];
62
+
63
+ if (escaped) {
64
+ escaped = false;
65
+ continue;
66
+ }
67
+
68
+ if (char === "\\") {
69
+ escaped = true;
70
+ continue;
71
+ }
72
+
73
+ if (char === '"') {
74
+ inQuotes = !inQuotes;
75
+ continue;
76
+ }
77
+
78
+ if (!inQuotes) {
79
+ if (char === "[") {
80
+ bracketCount++;
81
+ } else if (char === "]") {
82
+ bracketCount--;
83
+ if (bracketCount === 0) {
84
+ // Found the end of structured data
85
+ pos = i + 1;
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ if (pos > 0 && pos < remaining.length && remaining[pos] === " ") {
93
+ structuredData = remaining.substring(0, pos);
94
+ message = remaining.substring(pos + 1);
95
+ } else {
96
+ // No message after structured data or structured data extends to end
97
+ structuredData = remaining.substring(0, pos || remaining.length);
98
+ message = "";
99
+ }
100
+ } else {
101
+ // No structured data, it's just "-"
102
+ const spaceIndex = remaining.indexOf(" ");
103
+ if (spaceIndex === -1) {
104
+ structuredData = remaining;
105
+ message = "";
106
+ } else {
107
+ structuredData = remaining.substring(0, spaceIndex);
108
+ message = remaining.substring(spaceIndex + 1);
109
+ }
110
+ }
111
+
112
+ return {
113
+ priority: parseInt(match[1]),
114
+ version: parseInt(match[2]),
115
+ timestamp: match[3],
116
+ hostname: match[4],
117
+ appName: match[5],
118
+ procId: match[6],
119
+ msgId: match[7],
120
+ structuredData,
121
+ message,
122
+ };
123
+ }
124
+
125
+ function parseStructuredData(
126
+ structuredDataStr: string,
127
+ ): Record<string, string> {
128
+ if (structuredDataStr === "-") {
129
+ return {};
130
+ }
131
+
132
+ // Parse [id key1="value1" key2="value2"] format
133
+ const match = structuredDataStr.match(/^\[([^\s]+)\s+(.*)\]$/);
134
+ if (!match) {
135
+ throw new Error(`Invalid structured data format: ${structuredDataStr}`);
136
+ }
137
+
138
+ const result: Record<string, string> = {};
139
+ const keyValuePairs = match[2];
140
+
141
+ // Parse key="value" pairs, handling escaped quotes
142
+ const kvRegex = /(\w+)="([^"\\]*(\\.[^"\\]*)*)"/g;
143
+ let kvMatch;
144
+ while ((kvMatch = kvRegex.exec(keyValuePairs)) !== null) {
145
+ const key = kvMatch[1];
146
+ const value = kvMatch[2]
147
+ .replace(/\\"/g, '"')
148
+ .replace(/\\\\/g, "\\")
149
+ .replace(/\\]/g, "]");
150
+ result[key] = value;
151
+ }
152
+
153
+ return result;
154
+ }
155
+
156
+ // Create a mock log record for testing
157
+ function createMockLogRecord(
158
+ level: "trace" | "debug" | "info" | "warning" | "error" | "fatal" = "info",
159
+ message: (string | unknown)[] = ["Test message"],
160
+ properties?: Record<string, unknown>,
161
+ ): LogRecord {
162
+ return {
163
+ category: ["test"],
164
+ level,
165
+ message,
166
+ rawMessage: "Test message",
167
+ timestamp: new Date("2024-01-01T12:00:00.000Z").getTime(),
168
+ properties: properties ?? {},
169
+ };
170
+ }
171
+
172
+ test("getSyslogSink() creates a sink function", () => {
173
+ const sink = getSyslogSink();
174
+ assertEquals(typeof sink, "function");
175
+ assertInstanceOf(sink, Function);
176
+ });
177
+
178
+ test("getSyslogSink() creates an AsyncDisposable sink", () => {
179
+ const sink = getSyslogSink();
180
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
181
+ });
182
+
183
+ // Deno-specific UDP test
184
+ if (typeof Deno !== "undefined") {
185
+ test("getSyslogSink() with actual UDP message transmission (Deno)", async () => {
186
+ // For Deno, test UDP transmission to non-existent server (just verify no crash)
187
+ const sink = getSyslogSink({
188
+ hostname: "127.0.0.1",
189
+ port: 12345, // Use a fixed port that likely has no server
190
+ protocol: "udp",
191
+ facility: "local1",
192
+ appName: "test-app",
193
+ timeout: 100, // Short timeout
194
+ });
195
+
196
+ try {
197
+ // Send a log record - this should not crash even if server doesn't exist
198
+ const logRecord = createMockLogRecord("info", ["Test message"], {
199
+ userId: 123,
200
+ });
201
+ sink(logRecord);
202
+
203
+ // Wait for transmission attempt
204
+ await new Promise((resolve) => setTimeout(resolve, 150));
205
+
206
+ // Test passes if no crash occurs
207
+ assertEquals(true, true);
208
+ } finally {
209
+ await sink[Symbol.asyncDispose]();
210
+ }
211
+ });
212
+ } else {
213
+ test("getSyslogSink() with actual UDP message transmission (Node.js)", async () => {
214
+ // Create a mock UDP server to receive messages
215
+ let receivedMessage = "";
216
+
217
+ // Node.js UDP server
218
+ const server = createSocket("udp4");
219
+
220
+ await new Promise<void>((resolve) => {
221
+ server.bind(0, "127.0.0.1", resolve);
222
+ });
223
+
224
+ const address = server.address() as { port: number };
225
+
226
+ server.on("message", (msg) => {
227
+ receivedMessage = msg.toString();
228
+ });
229
+
230
+ try {
231
+ // Create sink with UDP
232
+ const sink = getSyslogSink({
233
+ hostname: "127.0.0.1",
234
+ port: address.port,
235
+ protocol: "udp",
236
+ facility: "local1",
237
+ appName: "test-app",
238
+ timeout: 1000,
239
+ });
240
+
241
+ // Send a log record
242
+ const logRecord = createMockLogRecord("info", ["Test message"], {
243
+ userId: 123,
244
+ });
245
+ sink(logRecord);
246
+
247
+ // Wait for message transmission
248
+ await new Promise((resolve) => setTimeout(resolve, 100));
249
+ await sink[Symbol.asyncDispose]();
250
+
251
+ // Wait for server to receive
252
+ await new Promise((resolve) => setTimeout(resolve, 100));
253
+
254
+ // Verify the message format
255
+ assertEquals(receivedMessage.includes("Test message"), true);
256
+ assertEquals(receivedMessage.includes("test-app"), true);
257
+ // Priority should be local1 (17) * 8 + info (6) = 142
258
+ assertEquals(receivedMessage.includes("<142>1"), true);
259
+ } finally {
260
+ server.close();
261
+ }
262
+ });
263
+ }
264
+
265
+ // Deno-specific TCP test
266
+ if (typeof Deno !== "undefined") {
267
+ test("getSyslogSink() with actual TCP message transmission (Deno)", async () => {
268
+ // Create a mock TCP server to receive messages
269
+ let receivedMessage = "";
270
+
271
+ // Deno TCP server
272
+ const server = Deno.listen({ port: 0 });
273
+ const serverAddr = server.addr as Deno.NetAddr;
274
+
275
+ const serverTask = (async () => {
276
+ try {
277
+ const conn = await server.accept();
278
+ const buffer = new Uint8Array(1024);
279
+ const bytesRead = await conn.read(buffer);
280
+ if (bytesRead) {
281
+ receivedMessage = new TextDecoder().decode(
282
+ buffer.subarray(0, bytesRead),
283
+ );
284
+ }
285
+ conn.close();
286
+ } catch {
287
+ // Server closed
288
+ }
289
+ })();
290
+
291
+ try {
292
+ // Create sink with TCP
293
+ const sink = getSyslogSink({
294
+ hostname: "127.0.0.1",
295
+ port: serverAddr.port,
296
+ protocol: "tcp",
297
+ facility: "daemon",
298
+ appName: "test-daemon",
299
+ timeout: 0, // No timeout to avoid timer leaks
300
+ includeStructuredData: true,
301
+ structuredDataId: "test@12345",
302
+ });
303
+
304
+ // Send a log record with properties
305
+ const logRecord = createMockLogRecord("error", [
306
+ "Critical error occurred",
307
+ ], {
308
+ errorCode: 500,
309
+ component: "auth",
310
+ });
311
+ sink(logRecord);
312
+
313
+ // Wait for message transmission and disposal
314
+ await new Promise((resolve) => setTimeout(resolve, 100));
315
+ await sink[Symbol.asyncDispose]();
316
+
317
+ // Wait for server task
318
+ await serverTask;
319
+
320
+ // Verify the message format
321
+ assertEquals(receivedMessage.includes("Critical error occurred"), true);
322
+ assertEquals(receivedMessage.includes("test-daemon"), true);
323
+ // Priority should be daemon (3) * 8 + error (3) = 27
324
+ assertEquals(receivedMessage.includes("<27>1"), true);
325
+ // Should include structured data
326
+ assertEquals(receivedMessage.includes("[test@12345"), true);
327
+ assertEquals(receivedMessage.includes('errorCode="500"'), true);
328
+ assertEquals(receivedMessage.includes('component="auth"'), true);
329
+ } finally {
330
+ server.close();
331
+ await serverTask.catch(() => {});
332
+ }
333
+ });
334
+ } else {
335
+ test("getSyslogSink() with actual TCP message transmission (Node.js)", async () => {
336
+ // Create a mock TCP server to receive messages
337
+ let receivedMessage = "";
338
+
339
+ // Node.js TCP server
340
+ const server = createServer((socket) => {
341
+ socket.on("data", (data) => {
342
+ receivedMessage = data.toString();
343
+ socket.end();
344
+ });
345
+ });
346
+
347
+ await new Promise<void>((resolve) => {
348
+ server.listen(0, "127.0.0.1", resolve);
349
+ });
350
+
351
+ const address = server.address() as { port: number };
352
+
353
+ try {
354
+ // Create sink with TCP
355
+ const sink = getSyslogSink({
356
+ hostname: "127.0.0.1",
357
+ port: address.port,
358
+ protocol: "tcp",
359
+ facility: "daemon",
360
+ appName: "test-daemon",
361
+ timeout: 5000,
362
+ includeStructuredData: true,
363
+ structuredDataId: "test@12345",
364
+ });
365
+
366
+ // Send a log record with properties
367
+ const logRecord = createMockLogRecord("error", [
368
+ "Critical error occurred",
369
+ ], {
370
+ errorCode: 500,
371
+ component: "auth",
372
+ });
373
+ sink(logRecord);
374
+
375
+ // Wait for message transmission
376
+ await new Promise((resolve) => setTimeout(resolve, 100));
377
+ await sink[Symbol.asyncDispose]();
378
+
379
+ // Wait for server to receive
380
+ await new Promise((resolve) => setTimeout(resolve, 100));
381
+
382
+ // Verify the message format
383
+ assertEquals(receivedMessage.includes("Critical error occurred"), true);
384
+ assertEquals(receivedMessage.includes("test-daemon"), true);
385
+ // Priority should be daemon (3) * 8 + error (3) = 27
386
+ assertEquals(receivedMessage.includes("<27>1"), true);
387
+ // Should include structured data
388
+ assertEquals(receivedMessage.includes("[test@12345"), true);
389
+ assertEquals(receivedMessage.includes('errorCode="500"'), true);
390
+ assertEquals(receivedMessage.includes('component="auth"'), true);
391
+ } finally {
392
+ server.close();
393
+ }
394
+ });
395
+ }
396
+
397
+ // Deno-specific multiple messages test
398
+ if (typeof Deno !== "undefined") {
399
+ test("getSyslogSink() with multiple messages and proper sequencing (Deno)", async () => {
400
+ // Test that multiple messages are sent in sequence without blocking
401
+ // Deno version - use UDP for simpler testing
402
+ const sink = getSyslogSink({
403
+ hostname: "127.0.0.1",
404
+ port: 1234, // Non-existent port, but should handle gracefully
405
+ protocol: "udp",
406
+ facility: "local0",
407
+ timeout: 100, // Short timeout
408
+ });
409
+
410
+ try {
411
+ // Send multiple messages quickly
412
+ const record1 = createMockLogRecord("info", ["Message 1"]);
413
+ const record2 = createMockLogRecord("warning", ["Message 2"]);
414
+ const record3 = createMockLogRecord("error", ["Message 3"]);
415
+
416
+ // These should not block each other
417
+ sink(record1);
418
+ sink(record2);
419
+ sink(record3);
420
+
421
+ // All messages should be queued and attempted
422
+ // Even if they fail due to no server, the sink should handle it gracefully
423
+ } finally {
424
+ await sink[Symbol.asyncDispose]();
425
+ }
426
+
427
+ // Test passes if no hanging or crashes occur
428
+ assertEquals(true, true);
429
+ });
430
+ } else {
431
+ test("getSyslogSink() with multiple messages and proper sequencing (Node.js)", async () => {
432
+ // Test that multiple messages are sent in sequence without blocking
433
+ // Node.js version
434
+ const sink = getSyslogSink({
435
+ hostname: "127.0.0.1",
436
+ port: 1234, // Non-existent port
437
+ protocol: "udp",
438
+ facility: "local0",
439
+ timeout: 100,
440
+ });
441
+
442
+ try {
443
+ // Send multiple messages quickly
444
+ const record1 = createMockLogRecord("info", ["Message 1"]);
445
+ const record2 = createMockLogRecord("warning", ["Message 2"]);
446
+ const record3 = createMockLogRecord("error", ["Message 3"]);
447
+
448
+ sink(record1);
449
+ sink(record2);
450
+ sink(record3);
451
+ } finally {
452
+ await sink[Symbol.asyncDispose]();
453
+ }
454
+
455
+ assertEquals(true, true);
456
+ });
457
+ }
458
+
459
+ // RFC 5424 Message Format Validation Tests
460
+ if (typeof Deno !== "undefined") {
461
+ test("getSyslogSink() RFC 5424 message format validation (Deno)", async () => {
462
+ const receivedMessages: string[] = [];
463
+
464
+ // Use Node.js dgram API for UDP server (available in Deno)
465
+ const server = createSocket("udp4");
466
+
467
+ await new Promise<void>((resolve) => {
468
+ server.bind(0, "127.0.0.1", resolve);
469
+ });
470
+
471
+ const address = server.address() as { port: number };
472
+
473
+ server.on("message", (msg) => {
474
+ receivedMessages.push(msg.toString());
475
+ });
476
+
477
+ try {
478
+ const sink = getSyslogSink({
479
+ hostname: "127.0.0.1",
480
+ port: address.port,
481
+ protocol: "udp",
482
+ facility: "daemon", // 3
483
+ appName: "test-app",
484
+ syslogHostname: "test-host",
485
+ processId: "12345",
486
+ timeout: 1000,
487
+ includeStructuredData: true,
488
+ structuredDataId: "test@54321",
489
+ });
490
+
491
+ // Test different log levels with specific data
492
+ const infoRecord = createMockLogRecord("info", ["Info message"], {
493
+ requestId: "req-123",
494
+ userId: 456,
495
+ });
496
+ const errorRecord = createMockLogRecord("error", ["Error occurred"], {
497
+ errorCode: 500,
498
+ component: "auth",
499
+ });
500
+ const warningRecord = createMockLogRecord("warning", [
501
+ "Warning: disk space low",
502
+ ], {
503
+ diskUsage: "85%",
504
+ partition: "/var",
505
+ });
506
+
507
+ sink(infoRecord);
508
+ sink(errorRecord);
509
+ sink(warningRecord);
510
+
511
+ await new Promise((resolve) => setTimeout(resolve, 200));
512
+ await sink[Symbol.asyncDispose]();
513
+
514
+ // Wait for server to receive all messages
515
+ await new Promise((resolve) => setTimeout(resolve, 200));
516
+
517
+ // Validate we received 3 messages
518
+ assertEquals(receivedMessages.length, 3);
519
+
520
+ // Parse and validate each message structure
521
+ const parsedMessages = receivedMessages.map((msg) =>
522
+ parseSyslogMessage(msg)
523
+ );
524
+
525
+ for (const parsed of parsedMessages) {
526
+ // Validate RFC 5424 format structure
527
+ assertEquals(parsed.version, 1);
528
+ assertEquals(parsed.hostname, "test-host");
529
+ assertEquals(parsed.appName, "test-app");
530
+ assertEquals(parsed.procId, "12345");
531
+ assertEquals(parsed.msgId, "-");
532
+
533
+ // Validate timestamp format (ISO 8601)
534
+ assert(
535
+ parsed.timestamp.match(
536
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/,
537
+ ) !== null,
538
+ );
539
+
540
+ // Validate structured data is present
541
+ assert(parsed.structuredData.startsWith("[test@54321"));
542
+ }
543
+
544
+ // Find specific messages and validate their content
545
+ const infoMessage = parsedMessages.find((p) =>
546
+ p.message === "Info message"
547
+ );
548
+ const errorMessage = parsedMessages.find((p) =>
549
+ p.message === "Error occurred"
550
+ );
551
+ const warningMessage = parsedMessages.find((p) =>
552
+ p.message === "Warning: disk space low"
553
+ );
554
+
555
+ // Validate priorities: daemon (3) * 8 + severity
556
+ assertEquals(infoMessage?.priority, 30); // daemon (3) * 8 + info (6) = 30
557
+ assertEquals(errorMessage?.priority, 27); // daemon (3) * 8 + error (3) = 27
558
+ assertEquals(warningMessage?.priority, 28); // daemon (3) * 8 + warning (4) = 28
559
+
560
+ // Parse and validate structured data content
561
+ const infoStructuredData = parseStructuredData(
562
+ infoMessage!.structuredData,
563
+ );
564
+ assertEquals(infoStructuredData.requestId, "req-123");
565
+ assertEquals(infoStructuredData.userId, "456");
566
+
567
+ const errorStructuredData = parseStructuredData(
568
+ errorMessage!.structuredData,
569
+ );
570
+ assertEquals(errorStructuredData.errorCode, "500");
571
+ assertEquals(errorStructuredData.component, "auth");
572
+
573
+ const warningStructuredData = parseStructuredData(
574
+ warningMessage!.structuredData,
575
+ );
576
+ assertEquals(warningStructuredData.diskUsage, "85%");
577
+ assertEquals(warningStructuredData.partition, "/var");
578
+ } finally {
579
+ server.close();
580
+ }
581
+ });
582
+ } else {
583
+ test("getSyslogSink() RFC 5424 message format validation (Node.js)", async () => {
584
+ const receivedMessages: string[] = [];
585
+
586
+ // Create UDP server to capture actual messages
587
+ const server = createSocket("udp4");
588
+
589
+ await new Promise<void>((resolve) => {
590
+ server.bind(0, "127.0.0.1", resolve);
591
+ });
592
+
593
+ const address = server.address() as { port: number };
594
+
595
+ server.on("message", (msg) => {
596
+ receivedMessages.push(msg.toString());
597
+ });
598
+
599
+ try {
600
+ const sink = getSyslogSink({
601
+ hostname: "127.0.0.1",
602
+ port: address.port,
603
+ protocol: "udp",
604
+ facility: "daemon", // 3
605
+ appName: "test-app",
606
+ syslogHostname: "test-host",
607
+ processId: "12345",
608
+ timeout: 1000,
609
+ includeStructuredData: true,
610
+ structuredDataId: "test@54321",
611
+ });
612
+
613
+ // Test different log levels with specific data
614
+ const infoRecord = createMockLogRecord("info", ["Info message"], {
615
+ requestId: "req-123",
616
+ userId: 456,
617
+ });
618
+ const errorRecord = createMockLogRecord("error", ["Error occurred"], {
619
+ errorCode: 500,
620
+ component: "auth",
621
+ });
622
+ const warningRecord = createMockLogRecord("warning", [
623
+ "Warning: disk space low",
624
+ ], {
625
+ diskUsage: "85%",
626
+ partition: "/var",
627
+ });
628
+
629
+ sink(infoRecord);
630
+ sink(errorRecord);
631
+ sink(warningRecord);
632
+
633
+ await new Promise((resolve) => setTimeout(resolve, 200));
634
+ await sink[Symbol.asyncDispose]();
635
+
636
+ // Wait for server to receive all messages
637
+ await new Promise((resolve) => setTimeout(resolve, 200));
638
+
639
+ // Validate we received 3 messages
640
+ assertEquals(receivedMessages.length, 3);
641
+
642
+ // Validate RFC 5424 format for each message
643
+ for (const message of receivedMessages) {
644
+ // Should start with priority in angle brackets
645
+ assertEquals(message.match(/^<\d+>/) !== null, true);
646
+
647
+ // Should have version number 1
648
+ assertEquals(message.includes(">1 "), true);
649
+
650
+ // Should contain timestamp in ISO format
651
+ assertEquals(
652
+ message.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/) !== null,
653
+ true,
654
+ );
655
+
656
+ // Should contain our hostname
657
+ assertEquals(message.includes("test-host"), true);
658
+
659
+ // Should contain our app name
660
+ assertEquals(message.includes("test-app"), true);
661
+
662
+ // Should contain our process ID
663
+ assertEquals(message.includes("12345"), true);
664
+
665
+ // Should contain structured data
666
+ assertEquals(message.includes("[test@54321"), true);
667
+ }
668
+
669
+ // Check specific priorities: daemon (3) * 8 + level
670
+ const infoMessage = receivedMessages.find((m) =>
671
+ m.includes("Info message")
672
+ );
673
+ const errorMessage = receivedMessages.find((m) =>
674
+ m.includes("Error occurred")
675
+ );
676
+ const warningMessage = receivedMessages.find((m) =>
677
+ m.includes("Warning: disk space low")
678
+ );
679
+
680
+ // Info: daemon (3) * 8 + info (6) = 30
681
+ assertEquals(infoMessage?.includes("<30>1"), true);
682
+
683
+ // Error: daemon (3) * 8 + error (3) = 27
684
+ assertEquals(errorMessage?.includes("<27>1"), true);
685
+
686
+ // Warning: daemon (3) * 8 + warning (4) = 28
687
+ assertEquals(warningMessage?.includes("<28>1"), true);
688
+
689
+ // Check structured data content
690
+ assertEquals(infoMessage?.includes('requestId="req-123"'), true);
691
+ assertEquals(infoMessage?.includes('userId="456"'), true);
692
+ assertEquals(errorMessage?.includes('errorCode="500"'), true);
693
+ assertEquals(errorMessage?.includes('component="auth"'), true);
694
+ assertEquals(warningMessage?.includes('diskUsage="85%"'), true);
695
+ assertEquals(warningMessage?.includes('partition="/var"'), true);
696
+ } finally {
697
+ server.close();
698
+ }
699
+ });
700
+ }
701
+
702
+ // Structured Data Escaping Tests
703
+ if (typeof Deno !== "undefined") {
704
+ test("getSyslogSink() structured data escaping validation (Deno)", async () => {
705
+ let receivedMessage = "";
706
+
707
+ const server = createSocket("udp4");
708
+
709
+ await new Promise<void>((resolve) => {
710
+ server.bind(0, "127.0.0.1", resolve);
711
+ });
712
+
713
+ const address = server.address() as { port: number };
714
+
715
+ server.on("message", (msg) => {
716
+ receivedMessage = msg.toString();
717
+ });
718
+
719
+ try {
720
+ const sink = getSyslogSink({
721
+ hostname: "127.0.0.1",
722
+ port: address.port,
723
+ protocol: "udp",
724
+ facility: "local0",
725
+ appName: "escape-test",
726
+ timeout: 1000,
727
+ includeStructuredData: true,
728
+ structuredDataId: "escape@12345",
729
+ });
730
+
731
+ // Test message with special characters that need escaping
732
+ const testRecord = createMockLogRecord("info", ["Test escaping"], {
733
+ quote: 'Has "quotes" in value',
734
+ backslash: "Has \\ backslash",
735
+ bracket: "Has ] bracket",
736
+ combined: 'Mix of "quotes", \\ and ] chars',
737
+ });
738
+
739
+ sink(testRecord);
740
+ await new Promise((resolve) => setTimeout(resolve, 200));
741
+ await sink[Symbol.asyncDispose]();
742
+
743
+ await new Promise((resolve) => setTimeout(resolve, 100));
744
+
745
+ // Parse and validate the complete message
746
+ const parsed = parseSyslogMessage(receivedMessage);
747
+
748
+ assertEquals(parsed.version, 1);
749
+ assertEquals(parsed.hostname, Deno.hostname());
750
+ assertEquals(parsed.appName, "escape-test");
751
+ assertEquals(parsed.message, "Test escaping");
752
+
753
+ // Parse structured data and verify proper unescaping
754
+ const structuredData = parseStructuredData(parsed.structuredData);
755
+ assertEquals(structuredData.quote, 'Has "quotes" in value');
756
+ assertEquals(structuredData.backslash, "Has \\ backslash");
757
+ assertEquals(structuredData.bracket, "Has ] bracket");
758
+ assertEquals(structuredData.combined, 'Mix of "quotes", \\ and ] chars');
759
+ } finally {
760
+ server.close();
761
+ }
762
+ });
763
+ } else {
764
+ test("getSyslogSink() structured data escaping validation (Node.js)", async () => {
765
+ let receivedMessage = "";
766
+
767
+ const server = createSocket("udp4");
768
+
769
+ await new Promise<void>((resolve) => {
770
+ server.bind(0, "127.0.0.1", resolve);
771
+ });
772
+
773
+ const address = server.address() as { port: number };
774
+
775
+ server.on("message", (msg) => {
776
+ receivedMessage = msg.toString();
777
+ });
778
+
779
+ try {
780
+ const sink = getSyslogSink({
781
+ hostname: "127.0.0.1",
782
+ port: address.port,
783
+ protocol: "udp",
784
+ facility: "local0",
785
+ appName: "escape-test",
786
+ timeout: 1000,
787
+ includeStructuredData: true,
788
+ structuredDataId: "escape@12345",
789
+ });
790
+
791
+ // Test message with special characters that need escaping
792
+ const testRecord = createMockLogRecord("info", ["Test escaping"], {
793
+ quote: 'Has "quotes" in value',
794
+ backslash: "Has \\ backslash",
795
+ bracket: "Has ] bracket",
796
+ combined: 'Mix of "quotes", \\ and ] chars',
797
+ });
798
+
799
+ sink(testRecord);
800
+ await new Promise((resolve) => setTimeout(resolve, 200));
801
+ await sink[Symbol.asyncDispose]();
802
+
803
+ await new Promise((resolve) => setTimeout(resolve, 100));
804
+
805
+ // Parse and verify the message like the Deno test
806
+ const parsed = parseSyslogMessage(receivedMessage);
807
+
808
+ assertEquals(parsed.version, 1);
809
+ assertEquals(parsed.message, "Test escaping");
810
+ assertEquals(parsed.appName, "escape-test");
811
+
812
+ // Parse structured data and verify escaping
813
+ const structuredData = parseStructuredData(parsed.structuredData);
814
+ assertEquals(structuredData.quote, 'Has "quotes" in value');
815
+ assertEquals(structuredData.backslash, "Has \\ backslash");
816
+ assertEquals(structuredData.bracket, "Has ] bracket");
817
+ assertEquals(structuredData.combined, 'Mix of "quotes", \\ and ] chars');
818
+ } finally {
819
+ server.close();
820
+ }
821
+ });
822
+ }
823
+
824
+ // Template Literal Message Formatting Tests
825
+ if (typeof Deno !== "undefined") {
826
+ test("getSyslogSink() template literal message formatting (Deno)", async () => {
827
+ let receivedMessage = "";
828
+
829
+ const server = createSocket("udp4");
830
+
831
+ await new Promise<void>((resolve) => {
832
+ server.bind(0, "127.0.0.1", resolve);
833
+ });
834
+
835
+ const address = server.address() as { port: number };
836
+
837
+ server.on("message", (msg) => {
838
+ receivedMessage = msg.toString();
839
+ });
840
+
841
+ try {
842
+ const sink = getSyslogSink({
843
+ hostname: "127.0.0.1",
844
+ port: address.port,
845
+ protocol: "udp",
846
+ facility: "local0",
847
+ appName: "template-test",
848
+ timeout: 1000,
849
+ includeStructuredData: false,
850
+ });
851
+
852
+ // Test LogTape template literal style message
853
+ const templateRecord = createMockLogRecord("info", [
854
+ "User ",
855
+ { userId: 123, name: "Alice" },
856
+ " performed action ",
857
+ "login",
858
+ " with result ",
859
+ { success: true, duration: 150 },
860
+ ]);
861
+
862
+ sink(templateRecord);
863
+ await new Promise((resolve) => setTimeout(resolve, 200));
864
+ await sink[Symbol.asyncDispose]();
865
+
866
+ await new Promise((resolve) => setTimeout(resolve, 100));
867
+
868
+ // Parse and validate the complete message
869
+ const parsed = parseSyslogMessage(receivedMessage);
870
+ assertEquals(parsed.appName, "template-test");
871
+ assertEquals(parsed.structuredData, "-"); // No structured data for this test
872
+
873
+ // Verify template literal message is correctly formatted
874
+ const expectedMessage =
875
+ 'User {"userId":123,"name":"Alice"} performed action "login" with result {"success":true,"duration":150}';
876
+ assertEquals(parsed.message, expectedMessage);
877
+ } finally {
878
+ server.close();
879
+ }
880
+ });
881
+ } else {
882
+ test("getSyslogSink() template literal message formatting (Node.js)", async () => {
883
+ let receivedMessage = "";
884
+
885
+ const server = createSocket("udp4");
886
+
887
+ await new Promise<void>((resolve) => {
888
+ server.bind(0, "127.0.0.1", resolve);
889
+ });
890
+
891
+ const address = server.address() as { port: number };
892
+
893
+ server.on("message", (msg) => {
894
+ receivedMessage = msg.toString();
895
+ });
896
+
897
+ try {
898
+ const sink = getSyslogSink({
899
+ hostname: "127.0.0.1",
900
+ port: address.port,
901
+ protocol: "udp",
902
+ facility: "local0",
903
+ appName: "template-test",
904
+ timeout: 1000,
905
+ includeStructuredData: false,
906
+ });
907
+
908
+ // Test LogTape template literal style message
909
+ const templateRecord = createMockLogRecord("info", [
910
+ "User ",
911
+ { userId: 123, name: "Alice" },
912
+ " performed action ",
913
+ "login",
914
+ " with result ",
915
+ { success: true, duration: 150 },
916
+ ]);
917
+
918
+ sink(templateRecord);
919
+ await new Promise((resolve) => setTimeout(resolve, 200));
920
+ await sink[Symbol.asyncDispose]();
921
+
922
+ await new Promise((resolve) => setTimeout(resolve, 100));
923
+
924
+ // Verify template literal parts are properly formatted
925
+ assertEquals(receivedMessage.includes("User "), true);
926
+ assertEquals(
927
+ receivedMessage.includes('{"userId":123,"name":"Alice"}'),
928
+ true,
929
+ );
930
+ assertEquals(receivedMessage.includes(" performed action "), true);
931
+ assertEquals(receivedMessage.includes('"login"'), true);
932
+ assertEquals(receivedMessage.includes(" with result "), true);
933
+ assertEquals(
934
+ receivedMessage.includes('{"success":true,"duration":150}'),
935
+ true,
936
+ );
937
+ } finally {
938
+ server.close();
939
+ }
940
+ });
941
+ }
942
+
943
+ // Facility and Priority Calculation Tests
944
+ if (typeof Deno !== "undefined") {
945
+ test("getSyslogSink() facility and priority calculation (Deno)", async () => {
946
+ const receivedMessages: string[] = [];
947
+
948
+ const server = createSocket("udp4");
949
+
950
+ await new Promise<void>((resolve) => {
951
+ server.bind(0, "127.0.0.1", resolve);
952
+ });
953
+
954
+ const address = server.address() as { port: number };
955
+
956
+ server.on("message", (msg) => {
957
+ receivedMessages.push(msg.toString());
958
+ });
959
+
960
+ try {
961
+ // Test different facilities
962
+ const facilities = [
963
+ { name: "kernel", code: 0 },
964
+ { name: "user", code: 1 },
965
+ { name: "daemon", code: 3 },
966
+ { name: "local0", code: 16 },
967
+ { name: "local7", code: 23 },
968
+ ] as const;
969
+
970
+ for (const facility of facilities) {
971
+ const sink = getSyslogSink({
972
+ hostname: "127.0.0.1",
973
+ port: address.port,
974
+ protocol: "udp",
975
+ facility: facility.name,
976
+ appName: `${facility.name}-test`,
977
+ timeout: 1000,
978
+ });
979
+
980
+ // Test with error level (severity 3)
981
+ const errorRecord = createMockLogRecord("error", [
982
+ `${facility.name} error message`,
983
+ ]);
984
+ sink(errorRecord);
985
+
986
+ await new Promise((resolve) => setTimeout(resolve, 50));
987
+ await sink[Symbol.asyncDispose]();
988
+ }
989
+
990
+ // Test one additional level combination
991
+ const warningSink = getSyslogSink({
992
+ hostname: "127.0.0.1",
993
+ port: address.port,
994
+ protocol: "udp",
995
+ facility: "mail", // code 2
996
+ appName: "mail-test",
997
+ timeout: 1000,
998
+ });
999
+
1000
+ const warningRecord = createMockLogRecord("warning", [
1001
+ "mail warning message",
1002
+ ]);
1003
+ warningSink(warningRecord);
1004
+ await new Promise((resolve) => setTimeout(resolve, 50));
1005
+ await warningSink[Symbol.asyncDispose]();
1006
+
1007
+ await new Promise((resolve) => setTimeout(resolve, 200));
1008
+
1009
+ // Verify priority calculations: Priority = Facility * 8 + Severity
1010
+ assertEquals(receivedMessages.length, 6);
1011
+
1012
+ // Parse all messages and verify priorities
1013
+ const parsedMessages = receivedMessages.map((msg) =>
1014
+ parseSyslogMessage(msg)
1015
+ );
1016
+
1017
+ // Find and verify each facility message
1018
+ const kernelMsg = parsedMessages.find((p) =>
1019
+ p.message === "kernel error message"
1020
+ );
1021
+ const userMsg = parsedMessages.find((p) =>
1022
+ p.message === "user error message"
1023
+ );
1024
+ const daemonMsg = parsedMessages.find((p) =>
1025
+ p.message === "daemon error message"
1026
+ );
1027
+ const local0Msg = parsedMessages.find((p) =>
1028
+ p.message === "local0 error message"
1029
+ );
1030
+ const local7Msg = parsedMessages.find((p) =>
1031
+ p.message === "local7 error message"
1032
+ );
1033
+ const mailMsg = parsedMessages.find((p) =>
1034
+ p.message === "mail warning message"
1035
+ );
1036
+
1037
+ // Verify exact priority calculations
1038
+ assertEquals(kernelMsg?.priority, 3); // kernel (0) * 8 + error (3) = 3
1039
+ assertEquals(userMsg?.priority, 11); // user (1) * 8 + error (3) = 11
1040
+ assertEquals(daemonMsg?.priority, 27); // daemon (3) * 8 + error (3) = 27
1041
+ assertEquals(local0Msg?.priority, 131); // local0 (16) * 8 + error (3) = 131
1042
+ assertEquals(local7Msg?.priority, 187); // local7 (23) * 8 + error (3) = 187
1043
+ assertEquals(mailMsg?.priority, 20); // mail (2) * 8 + warning (4) = 20
1044
+
1045
+ // Verify app names match facilities
1046
+ assertEquals(kernelMsg?.appName, "kernel-test");
1047
+ assertEquals(userMsg?.appName, "user-test");
1048
+ assertEquals(daemonMsg?.appName, "daemon-test");
1049
+ assertEquals(local0Msg?.appName, "local0-test");
1050
+ assertEquals(local7Msg?.appName, "local7-test");
1051
+ assertEquals(mailMsg?.appName, "mail-test");
1052
+ } finally {
1053
+ server.close();
1054
+ }
1055
+ });
1056
+ }
1057
+
1058
+ test("Syslog message format follows RFC 5424", () => {
1059
+ // Test that the sink can be created and called without throwing
1060
+ const sink = getSyslogSink({
1061
+ facility: "local0",
1062
+ appName: "test-app",
1063
+ syslogHostname: "test-host",
1064
+ processId: "1234",
1065
+ includeStructuredData: false,
1066
+ });
1067
+
1068
+ // This should not throw during sink creation and call
1069
+ // We don't send the message to avoid network operations
1070
+ assertEquals(typeof sink, "function");
1071
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1072
+ });
1073
+
1074
+ test("Syslog priority calculation", () => {
1075
+ // Test priority calculation: Priority = Facility * 8 + Severity
1076
+ // local0 (16) + info (6) = 16 * 8 + 6 = 134
1077
+
1078
+ const sink = getSyslogSink({
1079
+ facility: "local0", // 16
1080
+ appName: "test",
1081
+ });
1082
+
1083
+ // Test that sink is created correctly
1084
+ assertEquals(typeof sink, "function");
1085
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1086
+ });
1087
+
1088
+ test("Syslog facility codes mapping", () => {
1089
+ const facilities = [
1090
+ "kernel", // 0
1091
+ "user", // 1
1092
+ "mail", // 2
1093
+ "daemon", // 3
1094
+ "local0", // 16
1095
+ "local7", // 23
1096
+ ];
1097
+
1098
+ for (const facility of facilities) {
1099
+ const sink = getSyslogSink({
1100
+ facility: facility as SyslogFacility,
1101
+ appName: "test",
1102
+ });
1103
+
1104
+ // Should not throw for any valid facility
1105
+ assertEquals(typeof sink, "function");
1106
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1107
+ }
1108
+ });
1109
+
1110
+ test("Syslog severity levels mapping", () => {
1111
+ // Test that the sink works with all severity levels
1112
+ const _levels: Array<
1113
+ "fatal" | "error" | "warning" | "info" | "debug" | "trace"
1114
+ > = [
1115
+ "fatal", // 0 (Emergency)
1116
+ "error", // 3 (Error)
1117
+ "warning", // 4 (Warning)
1118
+ "info", // 6 (Informational)
1119
+ "debug", // 7 (Debug)
1120
+ "trace", // 7 (Debug)
1121
+ ];
1122
+
1123
+ const sink = getSyslogSink({
1124
+ facility: "local0",
1125
+ appName: "test",
1126
+ });
1127
+
1128
+ // Should work with all valid levels
1129
+ assertEquals(typeof sink, "function");
1130
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1131
+ });
1132
+
1133
+ test("Structured data formatting", () => {
1134
+ const sink = getSyslogSink({
1135
+ facility: "local0",
1136
+ appName: "test",
1137
+ includeStructuredData: true,
1138
+ });
1139
+
1140
+ // Should not throw when including structured data
1141
+ assertEquals(typeof sink, "function");
1142
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1143
+ });
1144
+
1145
+ test("Message with template literals", () => {
1146
+ const sink = getSyslogSink({
1147
+ facility: "local0",
1148
+ appName: "test",
1149
+ });
1150
+
1151
+ // Should not throw with template literal style messages
1152
+ assertEquals(typeof sink, "function");
1153
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1154
+ });
1155
+
1156
+ test("Default options", () => {
1157
+ const sink = getSyslogSink();
1158
+
1159
+ // Should work with default options
1160
+ assertEquals(typeof sink, "function");
1161
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1162
+ });
1163
+
1164
+ test("Custom options", () => {
1165
+ const sink = getSyslogSink({
1166
+ protocol: "tcp",
1167
+ facility: "mail",
1168
+ appName: "my-app",
1169
+ syslogHostname: "web-server-01",
1170
+ processId: "9999",
1171
+ timeout: 1000,
1172
+ includeStructuredData: true,
1173
+ });
1174
+
1175
+ // Should work with custom options
1176
+ assertEquals(typeof sink, "function");
1177
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1178
+ });
1179
+
1180
+ test("AsyncDisposable cleanup", async () => {
1181
+ const sink = getSyslogSink();
1182
+
1183
+ // Send a message
1184
+ const record = createMockLogRecord();
1185
+ sink(record);
1186
+
1187
+ // Should be able to dispose without throwing
1188
+ await sink[Symbol.asyncDispose]();
1189
+ });
1190
+
1191
+ // Basic format validation test
1192
+ test("Syslog message format validation", () => {
1193
+ // Test the internal formatting functions directly
1194
+ const timestamp = new Date("2024-01-01T12:00:00.000Z").getTime();
1195
+
1196
+ // Test priority calculation: local0 (16) * 8 + info (6) = 134
1197
+ const expectedPriority = 16 * 8 + 6; // 134
1198
+ assertEquals(expectedPriority, 134);
1199
+
1200
+ // Test timestamp formatting
1201
+ const timestampStr = new Date(timestamp).toISOString();
1202
+ assertEquals(timestampStr, "2024-01-01T12:00:00.000Z");
1203
+ });
1204
+
1205
+ // Runtime-specific connection tests
1206
+ if (typeof Deno !== "undefined") {
1207
+ // Deno-specific tests
1208
+ test("DenoUdpSyslogConnection instantiation", () => {
1209
+ const connection = new DenoUdpSyslogConnection("localhost", 514, 5000);
1210
+ assertInstanceOf(connection, DenoUdpSyslogConnection);
1211
+ });
1212
+
1213
+ test("DenoUdpSyslogConnection connect and close", () => {
1214
+ const connection = new DenoUdpSyslogConnection("localhost", 514, 5000);
1215
+ // connect() should not throw for UDP
1216
+ connection.connect();
1217
+ // close() should not throw for UDP
1218
+ connection.close();
1219
+ });
1220
+
1221
+ test("DenoUdpSyslogConnection send timeout", async () => {
1222
+ // Use a non-routable IP to trigger timeout
1223
+ const connection = new DenoUdpSyslogConnection("10.255.255.1", 9999, 50); // Very short timeout
1224
+ connection.connect();
1225
+
1226
+ try {
1227
+ await connection.send("test message");
1228
+ // If we reach here, the send didn't timeout as expected
1229
+ // This might happen if the system is very fast or network conditions are unusual
1230
+ } catch (error) {
1231
+ // This is expected - either timeout or network unreachable
1232
+ assertEquals(typeof (error as Error).message, "string");
1233
+ } finally {
1234
+ connection.close();
1235
+ }
1236
+ });
1237
+
1238
+ test("DenoTcpSyslogConnection instantiation", () => {
1239
+ const connection = new DenoTcpSyslogConnection(
1240
+ "localhost",
1241
+ 514,
1242
+ 5000,
1243
+ false,
1244
+ );
1245
+ assertInstanceOf(connection, DenoTcpSyslogConnection);
1246
+ });
1247
+
1248
+ test("DenoTcpSyslogConnection close without connection", () => {
1249
+ const connection = new DenoTcpSyslogConnection(
1250
+ "localhost",
1251
+ 514,
1252
+ 5000,
1253
+ false,
1254
+ );
1255
+ // close() should not throw even without connection
1256
+ connection.close();
1257
+ });
1258
+
1259
+ test("DenoTcpSyslogConnection connection timeout", async () => {
1260
+ // Use a non-routable IP address to ensure connection failure
1261
+ const connection = new DenoTcpSyslogConnection(
1262
+ "10.255.255.1",
1263
+ 9999,
1264
+ 100,
1265
+ false,
1266
+ ); // Very short timeout
1267
+
1268
+ try {
1269
+ await assertRejects(
1270
+ () => connection.connect(),
1271
+ Error,
1272
+ );
1273
+ } finally {
1274
+ // Ensure cleanup
1275
+ connection.close();
1276
+ }
1277
+ });
1278
+
1279
+ test("DenoTcpSyslogConnection send without connection", async () => {
1280
+ const connection = new DenoTcpSyslogConnection(
1281
+ "localhost",
1282
+ 514,
1283
+ 5000,
1284
+ false,
1285
+ );
1286
+
1287
+ await assertRejects(
1288
+ () => connection.send("test message"),
1289
+ Error,
1290
+ "Connection not established",
1291
+ );
1292
+ });
1293
+
1294
+ test("DenoUdpSyslogConnection actual send test", async () => {
1295
+ // Try to send to a local UDP echo server or just verify the call doesn't crash
1296
+ const connection = new DenoUdpSyslogConnection("127.0.0.1", 1514, 1000); // Non-privileged port
1297
+ connection.connect();
1298
+
1299
+ try {
1300
+ // This will likely fail (no server listening), but should handle gracefully
1301
+ await connection.send("test syslog message");
1302
+ // If it succeeds, that's also fine - might have a server running
1303
+ } catch (error) {
1304
+ // Expected - likely no server listening, but the send mechanism should work
1305
+ const errorMessage = (error as Error).message;
1306
+ // Should contain either timeout or connection/network error
1307
+ assertEquals(typeof errorMessage, "string");
1308
+ } finally {
1309
+ connection.close();
1310
+ }
1311
+ });
1312
+
1313
+ test("DenoTcpSyslogConnection actual send test with mock server", async () => {
1314
+ // Create a simple TCP server to receive the message
1315
+ let receivedData = "";
1316
+
1317
+ const server = Deno.listen({ port: 0 }); // Let system choose port
1318
+ const serverAddr = server.addr as Deno.NetAddr;
1319
+
1320
+ // Handle one connection in background
1321
+ const serverTask = (async () => {
1322
+ try {
1323
+ const conn = await server.accept();
1324
+ const buffer = new Uint8Array(1024);
1325
+ const bytesRead = await conn.read(buffer);
1326
+ if (bytesRead) {
1327
+ receivedData = new TextDecoder().decode(
1328
+ buffer.subarray(0, bytesRead),
1329
+ );
1330
+ }
1331
+ conn.close();
1332
+ } catch {
1333
+ // Server closed or connection error
1334
+ }
1335
+ })();
1336
+
1337
+ try {
1338
+ // Give server a moment to start
1339
+ await new Promise((resolve) => setTimeout(resolve, 10));
1340
+
1341
+ // Connect and send message - create new connection with no timeout to avoid timer leaks
1342
+ const connection = new DenoTcpSyslogConnection(
1343
+ "127.0.0.1",
1344
+ serverAddr.port,
1345
+ 0,
1346
+ false,
1347
+ ); // No timeout
1348
+
1349
+ await connection.connect();
1350
+ await connection.send("test syslog message from Deno TCP");
1351
+ connection.close();
1352
+
1353
+ // Give server time to process
1354
+ await new Promise((resolve) => setTimeout(resolve, 50));
1355
+
1356
+ // Verify message was received
1357
+ assertEquals(
1358
+ receivedData.includes("test syslog message from Deno TCP"),
1359
+ true,
1360
+ );
1361
+ } finally {
1362
+ server.close();
1363
+ await serverTask.catch(() => {}); // Wait for server cleanup
1364
+ }
1365
+ });
1366
+
1367
+ test("DenoTcpSyslogConnection secure connection attempt (TLS)", {
1368
+ // Disable sanitizers because TLS connection cleanup on Windows can take
1369
+ // longer than the test, causing false positive leak detection
1370
+ sanitizeOps: false,
1371
+ sanitizeResources: false,
1372
+ }, async () => {
1373
+ // Attempt to connect to a port where no TLS server is listening
1374
+ const connection = new DenoTcpSyslogConnection(
1375
+ "127.0.0.1",
1376
+ 1515,
1377
+ 100,
1378
+ true,
1379
+ ); // secure: true
1380
+ try {
1381
+ await assertRejects(
1382
+ () => connection.connect(),
1383
+ Error,
1384
+ // Expected error message for TLS connection failure (e.g., handshake error)
1385
+ // Deno's error for TLS connection failure can be generic "connection reset" or similar if no server
1386
+ // The important part is it should NOT connect successfully if unsecured
1387
+ );
1388
+ } finally {
1389
+ connection.close();
1390
+ }
1391
+ });
1392
+
1393
+ test("DenoTcpSyslogSink secure connection (TLS) with getSyslogSink", {
1394
+ // Disable sanitizers because TLS connection cleanup on Windows can take
1395
+ // longer than the test, causing false positive leak detection
1396
+ sanitizeOps: false,
1397
+ sanitizeResources: false,
1398
+ }, async () => {
1399
+ // This test would require a mock TLS server to properly verify data transmission.
1400
+ // For now, we'll verify that the sink attempts a secure connection.
1401
+ // Given no mock TLS server, this should reject.
1402
+ const sink = getSyslogSink({
1403
+ hostname: "127.0.0.1",
1404
+ port: 1516, // Different port for TLS test
1405
+ protocol: "tcp",
1406
+ secure: true,
1407
+ timeout: 100,
1408
+ });
1409
+ const sinkWithPromise = sink as TestSink;
1410
+
1411
+ try {
1412
+ await assertRejects(
1413
+ async () => {
1414
+ sink(createMockLogRecord("info", ["Test secure sink connection"]));
1415
+ await sinkWithPromise._internal_lastPromise;
1416
+ },
1417
+ Error,
1418
+ // The error message might vary depending on Deno's TLS implementation and OS.
1419
+ // It should indicate a connection problem, not a successful plaintext connection.
1420
+ );
1421
+ } finally {
1422
+ await sink[Symbol.asyncDispose]();
1423
+ }
1424
+ });
1425
+ }
1426
+
1427
+ // Node.js/Bun-specific tests
1428
+ if (typeof Deno === "undefined") {
1429
+ test("NodeUdpSyslogConnection instantiation", () => {
1430
+ const connection = new NodeUdpSyslogConnection("localhost", 514, 5000);
1431
+ assertInstanceOf(connection, NodeUdpSyslogConnection);
1432
+ });
1433
+
1434
+ test("NodeUdpSyslogConnection connect and close", () => {
1435
+ const connection = new NodeUdpSyslogConnection("localhost", 514, 5000);
1436
+ // connect() should not throw for UDP
1437
+ connection.connect();
1438
+ // close() should not throw for UDP
1439
+ connection.close();
1440
+ });
1441
+
1442
+ test("NodeUdpSyslogConnection send timeout", async () => {
1443
+ // Use a non-routable IP to trigger timeout
1444
+ const connection = new NodeUdpSyslogConnection("10.255.255.1", 9999, 50); // Very short timeout
1445
+ connection.connect();
1446
+
1447
+ try {
1448
+ await connection.send("test message");
1449
+ // If we reach here, the send didn't timeout as expected
1450
+ // This might happen if the system is very fast or network conditions are unusual
1451
+ } catch (error) {
1452
+ // This is expected - either timeout or network unreachable
1453
+ assertEquals(typeof (error as Error).message, "string");
1454
+ } finally {
1455
+ connection.close();
1456
+ }
1457
+ });
1458
+
1459
+ test("NodeTcpSyslogConnection instantiation", () => {
1460
+ const connection = new NodeTcpSyslogConnection(
1461
+ "localhost",
1462
+ 514,
1463
+ 5000,
1464
+ false,
1465
+ );
1466
+ assertInstanceOf(connection, NodeTcpSyslogConnection);
1467
+ });
1468
+
1469
+ test("NodeTcpSyslogConnection close without connection", () => {
1470
+ const connection = new NodeTcpSyslogConnection(
1471
+ "localhost",
1472
+ 514,
1473
+ 5000,
1474
+ false,
1475
+ );
1476
+ // close() should not throw even without connection
1477
+ connection.close();
1478
+ });
1479
+
1480
+ test("NodeTcpSyslogConnection connection timeout", {
1481
+ sanitizeResources: false,
1482
+ sanitizeOps: false,
1483
+ }, async () => {
1484
+ // Use a non-routable IP address to ensure connection failure
1485
+ const connection = new NodeTcpSyslogConnection(
1486
+ "10.255.255.1",
1487
+ 9999,
1488
+ 100,
1489
+ false,
1490
+ ); // Very short timeout
1491
+
1492
+ try {
1493
+ await assertRejects(
1494
+ () => connection.connect(),
1495
+ Error,
1496
+ );
1497
+ } finally {
1498
+ // Ensure cleanup
1499
+ connection.close();
1500
+ }
1501
+ });
1502
+
1503
+ test("NodeTcpSyslogConnection send without connection", () => {
1504
+ const connection = new NodeTcpSyslogConnection(
1505
+ "localhost",
1506
+ 514,
1507
+ 5000,
1508
+ false,
1509
+ );
1510
+
1511
+ assertThrows(
1512
+ () => connection.send("test message"),
1513
+ Error,
1514
+ "Connection not established",
1515
+ );
1516
+ });
1517
+
1518
+ test("NodeUdpSyslogConnection actual send test", async () => {
1519
+ // Try to send to a local UDP port
1520
+ const connection = new NodeUdpSyslogConnection("127.0.0.1", 1514, 1000); // Non-privileged port
1521
+ connection.connect();
1522
+
1523
+ try {
1524
+ // This will likely fail (no server listening), but should handle gracefully
1525
+ await connection.send("test syslog message");
1526
+ // If it succeeds, that's also fine - might have a server running
1527
+ } catch (error) {
1528
+ // Expected - likely no server listening, but the send mechanism should work
1529
+ const errorMessage = (error as Error).message;
1530
+ // Should contain either timeout or connection/network error
1531
+ assertEquals(typeof errorMessage, "string");
1532
+ } finally {
1533
+ connection.close();
1534
+ }
1535
+ });
1536
+
1537
+ test("NodeTcpSyslogConnection actual send test with mock server", async () => {
1538
+ // Import Node.js modules for creating a server
1539
+
1540
+ let receivedData = "";
1541
+
1542
+ // Create a simple TCP server
1543
+ const server = createServer((socket) => {
1544
+ socket.on("data", (data) => {
1545
+ receivedData = data.toString();
1546
+ socket.end();
1547
+ });
1548
+ });
1549
+
1550
+ // Start server on random port
1551
+ await new Promise<void>((resolve) => {
1552
+ server.listen(0, "127.0.0.1", resolve);
1553
+ });
1554
+
1555
+ const address = server.address() as { port: number };
1556
+
1557
+ try {
1558
+ // Connect and send message
1559
+ const connection = new NodeTcpSyslogConnection(
1560
+ "127.0.0.1",
1561
+ address.port,
1562
+ 5000,
1563
+ false,
1564
+ );
1565
+
1566
+ await connection.connect();
1567
+ await connection.send("test syslog message from Node TCP");
1568
+ connection.close();
1569
+
1570
+ // Wait a bit for server to receive data
1571
+ await new Promise((resolve) => setTimeout(resolve, 100));
1572
+
1573
+ // Verify message was received
1574
+ assertEquals(
1575
+ receivedData.includes("test syslog message from Node TCP"),
1576
+ true,
1577
+ );
1578
+ } finally {
1579
+ server.close();
1580
+ }
1581
+ });
1582
+
1583
+ test("NodeTcpSyslogConnection secure connection attempt (TLS)", async () => {
1584
+ // Attempt to connect to a port where no TLS server is listening
1585
+ const connection = new NodeTcpSyslogConnection(
1586
+ "127.0.0.1",
1587
+ 1515,
1588
+ 100,
1589
+ true,
1590
+ ); // secure: true
1591
+ try {
1592
+ await assertRejects(
1593
+ () => connection.connect(),
1594
+ Error,
1595
+ // Expected error message for TLS connection failure (e.g., handshake error)
1596
+ // Node.js TLS errors can be quite specific like "ECONNREFUSED" or "ERR_TLS_CERT_ALTNAME_INVALID" etc.
1597
+ // The key is that it should NOT connect successfully if unsecured
1598
+ );
1599
+ } finally {
1600
+ connection.close();
1601
+ }
1602
+ });
1603
+
1604
+ test("NodeTcpSyslogSink secure connection (TLS) with getSyslogSink", async () => {
1605
+ // Similar to Deno, this requires a mock TLS server for full verification.
1606
+ // For now, we verify that it attempts a secure connection and rejects if no TLS server.
1607
+ const sink = getSyslogSink({
1608
+ hostname: "127.0.0.1",
1609
+ port: 1516, // Different port for TLS test
1610
+ protocol: "tcp",
1611
+ secure: true,
1612
+ timeout: 100,
1613
+ });
1614
+ const sinkWithPromise = sink as TestSink;
1615
+
1616
+ try {
1617
+ await assertRejects(
1618
+ async () => {
1619
+ sink(createMockLogRecord("info", ["Test secure sink connection"]));
1620
+ await sinkWithPromise._internal_lastPromise;
1621
+ },
1622
+ Error,
1623
+ // Error messages might vary, but should indicate a connection or TLS issue.
1624
+ );
1625
+ } finally {
1626
+ await sink[Symbol.asyncDispose]();
1627
+ }
1628
+ });
1629
+ }
1630
+
1631
+ // TLS options configuration tests
1632
+ test("getSyslogSink() with TLS options", () => {
1633
+ const sink = getSyslogSink({
1634
+ protocol: "tcp",
1635
+ secure: true,
1636
+ tlsOptions: {
1637
+ rejectUnauthorized: false,
1638
+ ca: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
1639
+ },
1640
+ });
1641
+
1642
+ assertEquals(typeof sink, "function");
1643
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1644
+ });
1645
+
1646
+ test("getSyslogSink() with TLS options and multiple CA certs", () => {
1647
+ const sink = getSyslogSink({
1648
+ protocol: "tcp",
1649
+ secure: true,
1650
+ tlsOptions: {
1651
+ rejectUnauthorized: true,
1652
+ ca: [
1653
+ "-----BEGIN CERTIFICATE-----\ncert1\n-----END CERTIFICATE-----",
1654
+ "-----BEGIN CERTIFICATE-----\ncert2\n-----END CERTIFICATE-----",
1655
+ ],
1656
+ },
1657
+ });
1658
+
1659
+ assertEquals(typeof sink, "function");
1660
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1661
+ });
1662
+
1663
+ test("getSyslogSink() TLS options ignored for UDP", () => {
1664
+ // TLS options should be ignored for UDP connections
1665
+ const sink = getSyslogSink({
1666
+ protocol: "udp",
1667
+ secure: true, // This will be ignored for UDP
1668
+ tlsOptions: {
1669
+ rejectUnauthorized: false,
1670
+ },
1671
+ });
1672
+
1673
+ assertEquals(typeof sink, "function");
1674
+ assertEquals(typeof sink[Symbol.asyncDispose], "function");
1675
+ });