33 */
44
55import { statsCommand , StatsCommandOptions } from './logs-stats' ;
6- import * as logDiscovery from '../logs/log-discovery' ;
7- import * as logAggregator from '../logs/log-aggregator' ;
8- import * as statsFormatter from '../logs/stats-formatter' ;
6+ import { logger } from '../logger' ;
97import { LogSource } from '../types' ;
8+ import { createLogCommandTestHarness } from './test-helpers.test-utils' ;
109
1110// Mock dependencies
1211jest . mock ( '../logs/log-discovery' ) ;
@@ -21,26 +20,8 @@ jest.mock('../logger', () => ({
2120 } ,
2221} ) ) ;
2322
24- const mockedDiscovery = logDiscovery as jest . Mocked < typeof logDiscovery > ;
25- const mockedAggregator = logAggregator as jest . Mocked < typeof logAggregator > ;
26- const mockedFormatter = statsFormatter as jest . Mocked < typeof statsFormatter > ;
27-
2823describe ( 'logs-stats command' , ( ) => {
29- let mockExit : jest . SpyInstance ;
30- let mockConsoleLog : jest . SpyInstance ;
31-
32- beforeEach ( ( ) => {
33- jest . clearAllMocks ( ) ;
34- mockExit = jest . spyOn ( process , 'exit' ) . mockImplementation ( ( ) => {
35- throw new Error ( 'process.exit called' ) ;
36- } ) ;
37- mockConsoleLog = jest . spyOn ( console , 'log' ) . mockImplementation ( ) ;
38- } ) ;
39-
40- afterEach ( ( ) => {
41- mockExit . mockRestore ( ) ;
42- mockConsoleLog . mockRestore ( ) ;
43- } ) ;
24+ const harness = createLogCommandTestHarness ( ) ;
4425
4526 it ( 'should discover and use most recent log source' , async ( ) => {
4627 const mockSource : LogSource = {
@@ -50,29 +31,29 @@ describe('logs-stats command', () => {
5031 dateStr : new Date ( ) . toLocaleString ( ) ,
5132 } ;
5233
53- mockedDiscovery . discoverLogSources . mockResolvedValue ( [ mockSource ] ) ;
54- mockedDiscovery . selectMostRecent . mockReturnValue ( mockSource ) ;
55- mockedAggregator . loadAndAggregate . mockResolvedValue ( {
34+ harness . mockedDiscovery . discoverLogSources . mockResolvedValue ( [ mockSource ] ) ;
35+ harness . mockedDiscovery . selectMostRecent . mockReturnValue ( mockSource ) ;
36+ harness . mockedAggregator . loadAndAggregate . mockResolvedValue ( {
5637 totalRequests : 10 ,
5738 allowedRequests : 8 ,
5839 deniedRequests : 2 ,
5940 uniqueDomains : 3 ,
6041 byDomain : new Map ( ) ,
6142 timeRange : { start : 1000 , end : 2000 } ,
6243 } ) ;
63- mockedFormatter . formatStats . mockReturnValue ( 'formatted output' ) ;
44+ harness . mockedFormatter . formatStats . mockReturnValue ( 'formatted output' ) ;
6445
6546 const options : StatsCommandOptions = {
6647 format : 'pretty' ,
6748 } ;
6849
6950 await statsCommand ( options ) ;
7051
71- expect ( mockedDiscovery . discoverLogSources ) . toHaveBeenCalled ( ) ;
72- expect ( mockedDiscovery . selectMostRecent ) . toHaveBeenCalled ( ) ;
73- expect ( mockedAggregator . loadAndAggregate ) . toHaveBeenCalledWith ( mockSource ) ;
74- expect ( mockedFormatter . formatStats ) . toHaveBeenCalled ( ) ;
75- expect ( mockConsoleLog ) . toHaveBeenCalledWith ( 'formatted output' ) ;
52+ expect ( harness . mockedDiscovery . discoverLogSources ) . toHaveBeenCalled ( ) ;
53+ expect ( harness . mockedDiscovery . selectMostRecent ) . toHaveBeenCalled ( ) ;
54+ expect ( harness . mockedAggregator . loadAndAggregate ) . toHaveBeenCalledWith ( mockSource ) ;
55+ expect ( harness . mockedFormatter . formatStats ) . toHaveBeenCalled ( ) ;
56+ expect ( harness . mockConsoleLog ) . toHaveBeenCalledWith ( 'formatted output' ) ;
7657 } ) ;
7758
7859 it ( 'should use specified source when provided' , async ( ) => {
@@ -81,17 +62,17 @@ describe('logs-stats command', () => {
8162 path : '/custom/path' ,
8263 } ;
8364
84- mockedDiscovery . discoverLogSources . mockResolvedValue ( [ ] ) ;
85- mockedDiscovery . validateSource . mockResolvedValue ( mockSource ) ;
86- mockedAggregator . loadAndAggregate . mockResolvedValue ( {
65+ harness . mockedDiscovery . discoverLogSources . mockResolvedValue ( [ ] ) ;
66+ harness . mockedDiscovery . validateSource . mockResolvedValue ( mockSource ) ;
67+ harness . mockedAggregator . loadAndAggregate . mockResolvedValue ( {
8768 totalRequests : 5 ,
8869 allowedRequests : 5 ,
8970 deniedRequests : 0 ,
9071 uniqueDomains : 2 ,
9172 byDomain : new Map ( ) ,
9273 timeRange : null ,
9374 } ) ;
94- mockedFormatter . formatStats . mockReturnValue ( 'formatted' ) ;
75+ harness . mockedFormatter . formatStats . mockReturnValue ( 'formatted' ) ;
9576
9677 const options : StatsCommandOptions = {
9778 format : 'json' ,
@@ -100,67 +81,67 @@ describe('logs-stats command', () => {
10081
10182 await statsCommand ( options ) ;
10283
103- expect ( mockedDiscovery . validateSource ) . toHaveBeenCalledWith ( '/custom/path' ) ;
104- expect ( mockedAggregator . loadAndAggregate ) . toHaveBeenCalledWith ( mockSource ) ;
84+ expect ( harness . mockedDiscovery . validateSource ) . toHaveBeenCalledWith ( '/custom/path' ) ;
85+ expect ( harness . mockedAggregator . loadAndAggregate ) . toHaveBeenCalledWith ( mockSource ) ;
10586 } ) ;
10687
10788 it ( 'should exit with error if no sources found' , async ( ) => {
108- mockedDiscovery . discoverLogSources . mockResolvedValue ( [ ] ) ;
89+ harness . mockedDiscovery . discoverLogSources . mockResolvedValue ( [ ] ) ;
10990
11091 const options : StatsCommandOptions = {
11192 format : 'pretty' ,
11293 } ;
11394
11495 await expect ( statsCommand ( options ) ) . rejects . toThrow ( 'process.exit called' ) ;
115- expect ( mockExit ) . toHaveBeenCalledWith ( 1 ) ;
96+ expect ( harness . mockExit ) . toHaveBeenCalledWith ( 1 ) ;
11697 } ) ;
11798
11899 it ( 'should exit with error if specified source is invalid' , async ( ) => {
119- mockedDiscovery . discoverLogSources . mockResolvedValue ( [ ] ) ;
120- mockedDiscovery . validateSource . mockRejectedValue ( new Error ( 'Source not found' ) ) ;
100+ harness . mockedDiscovery . discoverLogSources . mockResolvedValue ( [ ] ) ;
101+ harness . mockedDiscovery . validateSource . mockRejectedValue ( new Error ( 'Source not found' ) ) ;
121102
122103 const options : StatsCommandOptions = {
123104 format : 'pretty' ,
124105 source : '/invalid/path' ,
125106 } ;
126107
127108 await expect ( statsCommand ( options ) ) . rejects . toThrow ( 'process.exit called' ) ;
128- expect ( mockExit ) . toHaveBeenCalledWith ( 1 ) ;
109+ expect ( harness . mockExit ) . toHaveBeenCalledWith ( 1 ) ;
129110 } ) ;
130111
131112 it ( 'should pass correct format to formatter' , async ( ) => {
132113 const mockSource : LogSource = { type : 'running' , containerName : 'awf-squid' } ;
133114
134- mockedDiscovery . discoverLogSources . mockResolvedValue ( [ mockSource ] ) ;
135- mockedDiscovery . selectMostRecent . mockReturnValue ( mockSource ) ;
136- mockedAggregator . loadAndAggregate . mockResolvedValue ( {
115+ harness . mockedDiscovery . discoverLogSources . mockResolvedValue ( [ mockSource ] ) ;
116+ harness . mockedDiscovery . selectMostRecent . mockReturnValue ( mockSource ) ;
117+ harness . mockedAggregator . loadAndAggregate . mockResolvedValue ( {
137118 totalRequests : 0 ,
138119 allowedRequests : 0 ,
139120 deniedRequests : 0 ,
140121 uniqueDomains : 0 ,
141122 byDomain : new Map ( ) ,
142123 timeRange : null ,
143124 } ) ;
144- mockedFormatter . formatStats . mockReturnValue ( '{}' ) ;
125+ harness . mockedFormatter . formatStats . mockReturnValue ( '{}' ) ;
145126
146127 await statsCommand ( { format : 'json' } ) ;
147- expect ( mockedFormatter . formatStats ) . toHaveBeenCalledWith (
128+ expect ( harness . mockedFormatter . formatStats ) . toHaveBeenCalledWith (
148129 expect . anything ( ) ,
149130 'json' ,
150131 expect . any ( Boolean )
151132 ) ;
152133
153- mockedFormatter . formatStats . mockClear ( ) ;
134+ harness . mockedFormatter . formatStats . mockClear ( ) ;
154135 await statsCommand ( { format : 'markdown' } ) ;
155- expect ( mockedFormatter . formatStats ) . toHaveBeenCalledWith (
136+ expect ( harness . mockedFormatter . formatStats ) . toHaveBeenCalledWith (
156137 expect . anything ( ) ,
157138 'markdown' ,
158139 expect . any ( Boolean )
159140 ) ;
160141
161- mockedFormatter . formatStats . mockClear ( ) ;
142+ harness . mockedFormatter . formatStats . mockClear ( ) ;
162143 await statsCommand ( { format : 'pretty' } ) ;
163- expect ( mockedFormatter . formatStats ) . toHaveBeenCalledWith (
144+ expect ( harness . mockedFormatter . formatStats ) . toHaveBeenCalledWith (
164145 expect . anything ( ) ,
165146 'pretty' ,
166147 expect . any ( Boolean )
@@ -170,15 +151,50 @@ describe('logs-stats command', () => {
170151 it ( 'should handle aggregation errors gracefully' , async ( ) => {
171152 const mockSource : LogSource = { type : 'running' , containerName : 'awf-squid' } ;
172153
173- mockedDiscovery . discoverLogSources . mockResolvedValue ( [ mockSource ] ) ;
174- mockedDiscovery . selectMostRecent . mockReturnValue ( mockSource ) ;
175- mockedAggregator . loadAndAggregate . mockRejectedValue ( new Error ( 'Failed to load' ) ) ;
154+ harness . mockedDiscovery . discoverLogSources . mockResolvedValue ( [ mockSource ] ) ;
155+ harness . mockedDiscovery . selectMostRecent . mockReturnValue ( mockSource ) ;
156+ harness . mockedAggregator . loadAndAggregate . mockRejectedValue ( new Error ( 'Failed to load' ) ) ;
176157
177158 const options : StatsCommandOptions = {
178159 format : 'pretty' ,
179160 } ;
180161
181162 await expect ( statsCommand ( options ) ) . rejects . toThrow ( 'process.exit called' ) ;
182- expect ( mockExit ) . toHaveBeenCalledWith ( 1 ) ;
163+ expect ( harness . mockExit ) . toHaveBeenCalledWith ( 1 ) ;
164+ } ) ;
165+
166+ it ( 'should emit source-selection info logs for non-JSON formats but suppress them for JSON' , async ( ) => {
167+ const mockSource : LogSource = {
168+ type : 'preserved' ,
169+ path : '/tmp/squid-logs-123' ,
170+ dateStr : 'Mon Jan 01 2024' ,
171+ } ;
172+ const emptyStats = {
173+ totalRequests : 0 ,
174+ allowedRequests : 0 ,
175+ deniedRequests : 0 ,
176+ uniqueDomains : 0 ,
177+ byDomain : new Map ( ) ,
178+ timeRange : null ,
179+ } ;
180+
181+ harness . mockedDiscovery . discoverLogSources . mockResolvedValue ( [ mockSource ] ) ;
182+ harness . mockedDiscovery . selectMostRecent . mockReturnValue ( mockSource ) ;
183+ harness . mockedAggregator . loadAndAggregate . mockResolvedValue ( emptyStats ) ;
184+ harness . mockedFormatter . formatStats . mockReturnValue ( '' ) ;
185+
186+ // pretty: shouldLog returns true → logger.info should be called
187+ await statsCommand ( { format : 'pretty' } ) ;
188+ expect ( ( logger . info as jest . Mock ) ) . toHaveBeenCalled ( ) ;
189+ ( logger . info as jest . Mock ) . mockClear ( ) ;
190+
191+ // markdown: shouldLog returns true → logger.info should be called
192+ await statsCommand ( { format : 'markdown' } ) ;
193+ expect ( ( logger . info as jest . Mock ) ) . toHaveBeenCalled ( ) ;
194+ ( logger . info as jest . Mock ) . mockClear ( ) ;
195+
196+ // json: shouldLog returns false → logger.info should NOT be called
197+ await statsCommand ( { format : 'json' } ) ;
198+ expect ( ( logger . info as jest . Mock ) ) . not . toHaveBeenCalled ( ) ;
183199 } ) ;
184200} ) ;
0 commit comments