1111
1212use OCA \ServerInfo \LogTailReader ;
1313use OCP \IConfig ;
14+ use OCP \Log \IFileBased ;
15+ use OCP \Log \ILogFactory ;
16+ use OCP \Log \IWriter ;
1417use PHPUnit \Framework \MockObject \MockObject ;
1518use Test \TestCase ;
1619
1720class LogTailReaderTest extends TestCase {
1821 private IConfig &MockObject $ config ;
22+ private ILogFactory &MockObject $ logFactory ;
1923 private LogTailReader $ instance ;
20- private string $ tmpDir ;
2124
2225 protected function setUp (): void {
2326 parent ::setUp ();
2427 $ this ->config = $ this ->createMock (IConfig::class);
25- $ this ->instance = new LogTailReader ( $ this ->config );
26- $ this ->tmpDir = sys_get_temp_dir ( );
28+ $ this ->logFactory = $ this ->createMock (ILogFactory::class );
29+ $ this ->instance = new LogTailReader ( $ this -> config , $ this -> logFactory );
2730 }
2831
29- private function configReturns (string $ logType , string $ logFile = '' ): void {
30- $ this ->config ->method ('getSystemValue ' )
31- ->willReturnCallback (function (string $ key ) use ($ logType , $ logFile ): string {
32- return match ($ key ) {
33- 'log_type ' => $ logType ,
34- 'datadirectory ' => '' ,
35- 'logfile ' => $ logFile ,
36- default => '' ,
37- };
38- });
39- }
40-
41- private function setupFileLog (string $ path ): void {
42- $ this ->configReturns ('file ' , $ path );
32+ /** @param list<array<string, mixed>> $entries */
33+ private function setupFileLog (array $ entries = []): void {
34+ $ this ->config ->method ('getSystemValue ' )->with ('log_type ' , 'file ' )->willReturn ('file ' );
35+ $ log = $ this ->createMockForIntersectionOfInterfaces ([IWriter::class, IFileBased::class]);
36+ $ log ->method ('getEntries ' )->willReturn ($ entries );
37+ $ this ->logFactory ->method ('get ' )->with ('file ' )->willReturn ($ log );
4338 }
4439
4540 public function testNonFileLogTypeReturnsUnavailable (): void {
46- $ this ->configReturns ('syslog ' );
41+ $ this ->config -> method ( ' getSystemValue ' )-> with ( ' log_type ' , ' file ' )-> willReturn ('syslog ' );
4742
4843 $ result = $ this ->instance ->recentErrors ();
4944
@@ -52,152 +47,98 @@ public function testNonFileLogTypeReturnsUnavailable(): void {
5247 $ this ->assertSame ([], $ result ['entries ' ]);
5348 }
5449
55- public function testUnreadablePathReturnsUnavailable (): void {
56- $ this ->configReturns ('file ' , '/nonexistent/path/nextcloud.log ' );
50+ public function testLogNotFileBasedReturnsUnavailable (): void {
51+ $ this ->config ->method ('getSystemValue ' )->with ('log_type ' , 'file ' )->willReturn ('file ' );
52+ $ writer = $ this ->createMock (IWriter::class);
53+ $ this ->logFactory ->method ('get ' )->with ('file ' )->willReturn ($ writer );
5754
5855 $ result = $ this ->instance ->recentErrors ();
5956
6057 $ this ->assertFalse ($ result ['available ' ]);
6158 $ this ->assertSame ('log_not_readable ' , $ result ['reason ' ]);
6259 }
6360
64- public function testEmptyLogFileReturnsAvailableWithNoEntries (): void {
65- $ path = tempnam ($ this ->tmpDir , 'nc_log_test_ ' );
66- file_put_contents ($ path , '' );
67- $ this ->setupFileLog ($ path );
61+ public function testEmptyEntriesReturnsAvailableWithNoEntries (): void {
62+ $ this ->setupFileLog ([]);
6863
69- try {
70- $ result = $ this ->instance ->recentErrors ();
64+ $ result = $ this ->instance ->recentErrors ();
7165
72- $ this ->assertTrue ($ result ['available ' ]);
73- $ this ->assertSame ([], $ result ['entries ' ]);
74- } finally {
75- unlink ($ path );
76- }
66+ $ this ->assertTrue ($ result ['available ' ]);
67+ $ this ->assertSame ([], $ result ['entries ' ]);
68+ }
69+
70+ public function testReturnShape (): void {
71+ $ this ->setupFileLog ([
72+ ['time ' => '2026-01-01T00:00:00+00:00 ' , 'level ' => 3 , 'app ' => 'core ' , 'message ' => 'something failed ' ],
73+ ]);
74+
75+ $ result = $ this ->instance ->recentErrors ();
76+
77+ $ this ->assertArrayHasKey ('entries ' , $ result );
78+ $ this ->assertArrayHasKey ('available ' , $ result );
79+ $ this ->assertTrue ($ result ['available ' ]);
80+ $ this ->assertCount (1 , $ result ['entries ' ]);
81+ $ entry = $ result ['entries ' ][0 ];
82+ $ this ->assertArrayHasKey ('time ' , $ entry );
83+ $ this ->assertArrayHasKey ('level ' , $ entry );
84+ $ this ->assertArrayHasKey ('app ' , $ entry );
85+ $ this ->assertArrayHasKey ('message ' , $ entry );
86+ $ this ->assertSame (3 , $ entry ['level ' ]);
87+ $ this ->assertSame ('core ' , $ entry ['app ' ]);
7788 }
7889
7990 public function testEntriesBelowMinLevelAreFiltered (): void {
80- $ path = tempnam ($ this ->tmpDir , 'nc_log_test_ ' );
81- $ lines = [
82- json_encode (['time ' => '2026-01-01T00:00:00+00:00 ' , 'level ' => 0 , 'app ' => 'a ' , 'message ' => 'debug ' ]),
83- json_encode (['time ' => '2026-01-01T00:00:01+00:00 ' , 'level ' => 1 , 'app ' => 'a ' , 'message ' => 'info ' ]),
84- json_encode (['time ' => '2026-01-01T00:00:02+00:00 ' , 'level ' => 2 , 'app ' => 'a ' , 'message ' => 'warn ' ]),
85- json_encode (['time ' => '2026-01-01T00:00:03+00:00 ' , 'level ' => 3 , 'app ' => 'a ' , 'message ' => 'error ' ]),
86- ];
87- file_put_contents ($ path , implode ("\n" , $ lines ) . "\n" );
88- $ this ->setupFileLog ($ path );
89-
90- try {
91- $ result = $ this ->instance ->recentErrors (limit: 10 , minLevel: 2 );
92-
93- $ this ->assertTrue ($ result ['available ' ]);
94- $ this ->assertCount (2 , $ result ['entries ' ]);
95- foreach ($ result ['entries ' ] as $ entry ) {
96- $ this ->assertGreaterThanOrEqual (2 , $ entry ['level ' ]);
97- }
98- } finally {
99- unlink ($ path );
91+ $ this ->setupFileLog ([
92+ ['time ' => '2026-01-01T00:00:03+00:00 ' , 'level ' => 3 , 'app ' => 'a ' , 'message ' => 'error ' ],
93+ ['time ' => '2026-01-01T00:00:02+00:00 ' , 'level ' => 2 , 'app ' => 'a ' , 'message ' => 'warn ' ],
94+ ['time ' => '2026-01-01T00:00:01+00:00 ' , 'level ' => 1 , 'app ' => 'a ' , 'message ' => 'info ' ],
95+ ['time ' => '2026-01-01T00:00:00+00:00 ' , 'level ' => 0 , 'app ' => 'a ' , 'message ' => 'debug ' ],
96+ ]);
97+
98+ $ result = $ this ->instance ->recentErrors (limit: 10 , minLevel: 2 );
99+
100+ $ this ->assertTrue ($ result ['available ' ]);
101+ $ this ->assertCount (2 , $ result ['entries ' ]);
102+ foreach ($ result ['entries ' ] as $ entry ) {
103+ $ this ->assertGreaterThanOrEqual (2 , $ entry ['level ' ]);
100104 }
101105 }
102106
103107 public function testLimitIsRespected (): void {
104- $ path = tempnam ($ this ->tmpDir , 'nc_log_test_ ' );
105- $ lines = [];
108+ $ entries = [];
106109 for ($ i = 0 ; $ i < 10 ; $ i ++) {
107- $ lines [] = json_encode ( ['time ' => "2026-01-01T00:00: {$ i }0+00:00 " , 'level ' => 3 , 'app ' => 'test ' , 'message ' => "error $ i " ]) ;
110+ $ entries [] = ['time ' => "2026-01-01T00:00: {$ i }0+00:00 " , 'level ' => 3 , 'app ' => 'test ' , 'message ' => "error $ i " ];
108111 }
109- file_put_contents ($ path , implode ("\n" , $ lines ) . "\n" );
110- $ this ->setupFileLog ($ path );
112+ $ this ->setupFileLog ($ entries );
111113
112- try {
113- $ result = $ this ->instance ->recentErrors (limit: 3 );
114+ $ result = $ this ->instance ->recentErrors (limit: 3 );
114115
115- $ this ->assertTrue ($ result ['available ' ]);
116- $ this ->assertCount (3 , $ result ['entries ' ]);
117- } finally {
118- unlink ($ path );
119- }
120- }
121-
122- public function testReturnShape (): void {
123- $ path = tempnam ($ this ->tmpDir , 'nc_log_test_ ' );
124- $ line = json_encode (['time ' => '2026-01-01T00:00:00+00:00 ' , 'level ' => 3 , 'app ' => 'core ' , 'message ' => 'something failed ' ]);
125- file_put_contents ($ path , $ line . "\n" );
126- $ this ->setupFileLog ($ path );
127-
128- try {
129- $ result = $ this ->instance ->recentErrors ();
130-
131- $ this ->assertArrayHasKey ('entries ' , $ result );
132- $ this ->assertArrayHasKey ('available ' , $ result );
133- $ this ->assertCount (1 , $ result ['entries ' ]);
134- $ entry = $ result ['entries ' ][0 ];
135- $ this ->assertArrayHasKey ('time ' , $ entry );
136- $ this ->assertArrayHasKey ('level ' , $ entry );
137- $ this ->assertArrayHasKey ('app ' , $ entry );
138- $ this ->assertArrayHasKey ('message ' , $ entry );
139- $ this ->assertSame (3 , $ entry ['level ' ]);
140- $ this ->assertSame ('core ' , $ entry ['app ' ]);
141- } finally {
142- unlink ($ path );
143- }
116+ $ this ->assertTrue ($ result ['available ' ]);
117+ $ this ->assertCount (3 , $ result ['entries ' ]);
144118 }
145119
146120 public function testLongMessageIsTruncated (): void {
147- $ path = tempnam ($ this ->tmpDir , 'nc_log_test_ ' );
148- $ longMsg = str_repeat ('a ' , 300 );
149- $ line = json_encode (['time ' => '2026-01-01T00:00:00+00:00 ' , 'level ' => 3 , 'app ' => 'core ' , 'message ' => $ longMsg ]);
150- file_put_contents ($ path , $ line . "\n" );
151- $ this ->setupFileLog ($ path );
152-
153- try {
154- $ result = $ this ->instance ->recentErrors ();
155-
156- $ this ->assertCount (1 , $ result ['entries ' ]);
157- // snippet() uses mb_strlen/mb_substr so measure in characters, not bytes
158- $ this ->assertLessThanOrEqual (200 , mb_strlen ($ result ['entries ' ][0 ]['message ' ]));
159- } finally {
160- unlink ($ path );
161- }
162- }
121+ $ this ->setupFileLog ([
122+ ['time ' => '2026-01-01T00:00:00+00:00 ' , 'level ' => 3 , 'app ' => 'core ' , 'message ' => str_repeat ('a ' , 300 )],
123+ ]);
163124
164- public function testInvalidJsonLinesAreSkipped (): void {
165- $ path = tempnam ($ this ->tmpDir , 'nc_log_test_ ' );
166- $ lines = [
167- 'not valid json ' ,
168- json_encode (['time ' => '2026-01-01T00:00:00+00:00 ' , 'level ' => 3 , 'app ' => 'core ' , 'message ' => 'real error ' ]),
169- '{broken ' ,
170- ];
171- file_put_contents ($ path , implode ("\n" , $ lines ) . "\n" );
172- $ this ->setupFileLog ($ path );
173-
174- try {
175- $ result = $ this ->instance ->recentErrors ();
176-
177- $ this ->assertCount (1 , $ result ['entries ' ]);
178- } finally {
179- unlink ($ path );
180- }
125+ $ result = $ this ->instance ->recentErrors ();
126+
127+ $ this ->assertCount (1 , $ result ['entries ' ]);
128+ $ this ->assertLessThanOrEqual (200 , mb_strlen ($ result ['entries ' ][0 ]['message ' ]));
181129 }
182130
183- public function testEntriesReturnedNewestFirst (): void {
184- $ path = tempnam ($ this ->tmpDir , 'nc_log_test_ ' );
185- $ lines = [
186- json_encode (['time ' => '2026-01-01T00:00:00+00:00 ' , 'level ' => 3 , 'app ' => 'a ' , 'message ' => 'first ' ]),
187- json_encode (['time ' => '2026-01-01T00:00:01+00:00 ' , 'level ' => 3 , 'app ' => 'a ' , 'message ' => 'second ' ]),
188- json_encode (['time ' => '2026-01-01T00:00:02+00:00 ' , 'level ' => 3 , 'app ' => 'a ' , 'message ' => 'third ' ]),
189- ];
190- file_put_contents ($ path , implode ("\n" , $ lines ) . "\n" );
191- $ this ->setupFileLog ($ path );
192-
193- try {
194- $ result = $ this ->instance ->recentErrors ();
195-
196- $ this ->assertCount (3 , $ result ['entries ' ]);
197- $ this ->assertSame ('third ' , $ result ['entries ' ][0 ]['message ' ]);
198- $ this ->assertSame ('first ' , $ result ['entries ' ][2 ]['message ' ]);
199- } finally {
200- unlink ($ path );
201- }
131+ public function testOrderFromGetEntriesIsPreserved (): void {
132+ $ this ->setupFileLog ([
133+ ['time ' => '2026-01-01T00:00:02+00:00 ' , 'level ' => 3 , 'app ' => 'a ' , 'message ' => 'third ' ],
134+ ['time ' => '2026-01-01T00:00:01+00:00 ' , 'level ' => 3 , 'app ' => 'a ' , 'message ' => 'second ' ],
135+ ['time ' => '2026-01-01T00:00:00+00:00 ' , 'level ' => 3 , 'app ' => 'a ' , 'message ' => 'first ' ],
136+ ]);
137+
138+ $ result = $ this ->instance ->recentErrors ();
139+
140+ $ this ->assertCount (3 , $ result ['entries ' ]);
141+ $ this ->assertSame ('third ' , $ result ['entries ' ][0 ]['message ' ]);
142+ $ this ->assertSame ('first ' , $ result ['entries ' ][2 ]['message ' ]);
202143 }
203144}
0 commit comments