}> */ public array $rQueries = []; public function __construct(private bool $rResult) { } public function query(string $rSql, mixed ...$rParams): bool { $this->rQueries[] = [$rSql, $rParams]; return $this->rResult; } }; } protected function tearDown(): void { LogSink::useSink(null); } public function testStreamLogsAreOneMultiRowInsert(): void { $rDb = $this->recorder(); $this->assertTrue(LogSink::write('stream', [ ['stream_id' => 5, 'server_id' => 2, 'action' => 'STREAM_START', 'source' => "it's", 'date' => '100'], ['stream_id' => 6, 'server_id' => 2, 'action' => 'STREAM_STOP', 'source' => '', 'date' => '101'], ], $rDb)); $this->assertSame([[ 'INSERT INTO `streams_logs` (`stream_id`,`server_id`,`action`,`source`,`date`) VALUES (?,?,?,?,?),(?,?,?,?,?);', [5, 2, 'STREAM_START', "it's", '100', 6, 2, 'STREAM_STOP', '', '101'], ]], $rDb->rQueries); } public function testPanelErrorsUseInsertIgnoreAndMissingColumnsAreNull(): void { $rDb = $this->recorder(); LogSink::write('panel_error', [['server_id' => 1, 'type' => 'php', 'log_message' => 'x', 'unique' => 'h']], $rDb); [$rSql, $rParams] = $rDb->rQueries[0]; $this->assertStringStartsWith('INSERT IGNORE INTO `panel_logs` (`server_id`,`type`,`log_message`,`log_extra`,`line`,`date`,`file`,`env`,`version`,`unique`)', $rSql); $this->assertSame([1, 'php', 'x', null, null, null, null, null, null, 'h'], $rParams); } public function testLargeBatchesAreChunkedAndFailureIsReported(): void { $rRows = array_fill(0, LogSink::CHUNK + 1, ['user_id' => 1, 'stream_id' => 2, 'ip' => '1.1.1.1', 'time' => 3]); $rDb = $this->recorder(); $this->assertTrue(LogSink::write('restream', $rRows, $rDb)); $this->assertCount(2, $rDb->rQueries); $this->assertCount(4, $rDb->rQueries[1][1]); $this->assertFalse(LogSink::write('restream', $rRows, $this->recorder(false))); } public function testEmptyAndUnknown(): void { $rDb = $this->recorder(); $this->assertTrue(LogSink::write('client', [], $rDb)); $this->assertSame([], $rDb->rQueries); $this->expectException(InvalidArgumentException::class); LogSink::write('nope', [['a' => 1]], $rDb); } public function testSinkReceivesTypeAndRows(): void { $rSeen = []; LogSink::useSink(function (string $rType, array $rRows) use (&$rSeen): bool { $rSeen[] = [$rType, $rRows]; return true; }); LogSink::write('client', [['stream_id' => 1]]); $this->assertSame([['client', [['stream_id' => 1]]]], $rSeen); } public function testEveryLogWriterGoesThroughTheSink(): void { $rRoot = dirname(__DIR__, 2) . '/src/'; foreach (['streams_logs', 'lines_logs', 'streams_errors', 'panel_logs', 'detect_restream_logs'] as $rTable) { foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($rRoot, FilesystemIterator::SKIP_DOTS)) as $rFile) { if ($rFile->getExtension() !== 'php' || str_contains($rFile->getPathname(), '/vendor/') || str_ends_with($rFile->getPathname(), 'Cluster/LogSink.php')) { continue; } $this->assertDoesNotMatchRegularExpression('/INSERT\s+(IGNORE\s+)?INTO\s+`?' . $rTable . '`?/i', (string) file_get_contents($rFile->getPathname()), $rFile->getPathname()); } } } public function testRedactorStripsCredentials(): void { $this->assertSame('http://h/get.php?username=***&password=***&type=m3u', Redactor::redact('http://h/get.php?username=bob&password=s3cret&type=m3u')); $this->assertSame('http://h:80/live/***/***/12.ts', Redactor::redact('http://h:80/live/bob/s3cret/12.ts')); $this->assertSame('ffmpeg -i rtmp://***@src/app/key -f mpegts', Redactor::redact('ffmpeg -i rtmp://bob:s3cret@src/app/key -f mpegts')); $this->assertSame('/api?token=***', Redactor::redact('/api?token=abcDEF123')); $this->assertSame('no credentials here', Redactor::redact('no credentials here')); $rOnce = Redactor::redact('http://u:p@h/live/a/b/1.ts?password=x'); $this->assertSame($rOnce, Redactor::redact($rOnce), 'idempotent'); $this->assertSame(['id' => 5, 'source' => 'http://h/movie/***/***/9.mkv'], Redactor::redactRow(['id' => 5, 'source' => 'http://h/movie/u/p/9.mkv'])); } }