@aaronshaf/ger 0.2.2 → 0.2.5

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.
@@ -1,26 +1,48 @@
1
- import { describe, test, expect, beforeEach } from 'bun:test'
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, mock, test } from 'bun:test'
2
2
  import { Effect, Layer } from 'effect'
3
+ import { HttpResponse, http } from 'msw'
4
+ import { setupServer } from 'msw/node'
5
+ import { GerritApiServiceLive } from '@/api/gerrit'
3
6
  import { mineCommand } from '@/cli/commands/mine'
4
- import { GerritApiService, type ApiError } from '@/api/gerrit'
7
+ import { ConfigService } from '@/services/config'
5
8
  import { generateMockChange } from '@/test-utils/mock-generator'
6
9
  import type { ChangeInfo } from '@/schemas/gerrit'
10
+ import { createMockConfigService } from './helpers/config-mock'
7
11
 
8
- // Mock console.log to capture output
9
- const mockConsole = {
10
- logs: [] as string[],
11
- log: function (message: string) {
12
- this.logs.push(message)
13
- },
14
- clear: function () {
15
- this.logs = []
16
- },
17
- }
12
+ // Create MSW server
13
+ const server = setupServer(
14
+ // Default handler for auth check
15
+ http.get('*/a/accounts/self', ({ request }) => {
16
+ const auth = request.headers.get('Authorization')
17
+ if (!auth || !auth.startsWith('Basic ')) {
18
+ return HttpResponse.text('Unauthorized', { status: 401 })
19
+ }
20
+ return HttpResponse.json({
21
+ _account_id: 1000,
22
+ name: 'Test User',
23
+ email: 'test@example.com',
24
+ })
25
+ }),
26
+ )
18
27
 
19
28
  describe('mine command', () => {
29
+ let mockConsoleLog: ReturnType<typeof mock>
30
+
31
+ beforeAll(() => {
32
+ server.listen({ onUnhandledRequest: 'bypass' })
33
+ })
34
+
35
+ afterAll(() => {
36
+ server.close()
37
+ })
38
+
20
39
  beforeEach(() => {
21
- mockConsole.clear()
22
- // Replace console.log for tests
23
- global.console.log = mockConsole.log.bind(mockConsole)
40
+ mockConsoleLog = mock(() => {})
41
+ console.log = mockConsoleLog
42
+ })
43
+
44
+ afterEach(() => {
45
+ server.resetHandlers()
24
46
  })
25
47
 
26
48
  test('should fetch and display my changes in pretty format', async () => {
@@ -41,32 +63,26 @@ describe('mine command', () => {
41
63
  }),
42
64
  ]
43
65
 
44
- const mockApi = GerritApiService.of({
45
- listChanges: (query?: string) => {
46
- expect(query).toBe('owner:self status:open')
47
- return Effect.succeed(mockChanges)
48
- },
49
- getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
50
- postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
51
- abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
52
- testConnection: Effect.fail(new Error('Not implemented') as ApiError),
53
- getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
54
- getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
55
- getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
56
- getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
57
- getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
58
- getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
59
- getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
60
- getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
61
- })
66
+ server.use(
67
+ http.get('*/a/changes/', ({ request }) => {
68
+ const url = new URL(request.url)
69
+ expect(url.searchParams.get('q')).toBe('owner:self status:open')
70
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChanges)}`)
71
+ }),
72
+ )
62
73
 
74
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
63
75
  await Effect.runPromise(
64
- mineCommand({ xml: false }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
76
+ mineCommand({ xml: false }).pipe(
77
+ Effect.provide(GerritApiServiceLive),
78
+ Effect.provide(mockConfigLayer),
79
+ ),
65
80
  )
66
81
 
67
- expect(mockConsole.logs.length).toBeGreaterThan(0)
68
- expect(mockConsole.logs.some((log) => log.includes('My test change'))).toBe(true)
69
- expect(mockConsole.logs.some((log) => log.includes('Another change'))).toBe(true)
82
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
83
+ expect(output.length).toBeGreaterThan(0)
84
+ expect(output).toContain('My test change')
85
+ expect(output).toContain('Another change')
70
86
  })
71
87
 
72
88
  test('should output XML format when --xml flag is used', async () => {
@@ -80,30 +96,23 @@ describe('mine command', () => {
80
96
  }),
81
97
  ]
82
98
 
83
- const mockApi = GerritApiService.of({
84
- listChanges: (query?: string) => {
85
- expect(query).toBe('owner:self status:open')
86
- return Effect.succeed(mockChanges)
87
- },
88
- getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
89
- postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
90
- abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
91
- testConnection: Effect.fail(new Error('Not implemented') as ApiError),
92
- getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
93
- getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
94
- getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
95
- getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
96
- getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
97
- getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
98
- getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
99
- getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
100
- })
99
+ server.use(
100
+ http.get('*/a/changes/', ({ request }) => {
101
+ const url = new URL(request.url)
102
+ expect(url.searchParams.get('q')).toBe('owner:self status:open')
103
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChanges)}`)
104
+ }),
105
+ )
101
106
 
107
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
102
108
  await Effect.runPromise(
103
- mineCommand({ xml: true }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
109
+ mineCommand({ xml: true }).pipe(
110
+ Effect.provide(GerritApiServiceLive),
111
+ Effect.provide(mockConfigLayer),
112
+ ),
104
113
  )
105
114
 
106
- const output = mockConsole.logs.join('\n')
115
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
107
116
  expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
108
117
  expect(output).toContain('<changes count="1">')
109
118
  expect(output).toContain('<change>')
@@ -117,113 +126,83 @@ describe('mine command', () => {
117
126
  })
118
127
 
119
128
  test('should handle no changes gracefully', async () => {
120
- const mockApi = GerritApiService.of({
121
- listChanges: () => Effect.succeed([]),
122
- getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
123
- postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
124
- abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
125
- testConnection: Effect.fail(new Error('Not implemented') as ApiError),
126
- getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
127
- getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
128
- getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
129
- getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
130
- getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
131
- getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
132
- getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
133
- getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
134
- })
129
+ server.use(
130
+ http.get('*/a/changes/', () => {
131
+ return HttpResponse.text(")]}'\n[]")
132
+ }),
133
+ )
135
134
 
135
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
136
136
  await Effect.runPromise(
137
- mineCommand({ xml: false }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
137
+ mineCommand({ xml: false }).pipe(
138
+ Effect.provide(GerritApiServiceLive),
139
+ Effect.provide(mockConfigLayer),
140
+ ),
138
141
  )
139
142
 
140
143
  // Mine command returns early for empty results, so no output is expected
141
- expect(mockConsole.logs).toEqual([])
144
+ expect(mockConsoleLog.mock.calls).toEqual([])
142
145
  })
143
146
 
144
147
  test('should handle no changes gracefully in XML format', async () => {
145
- const mockApi = GerritApiService.of({
146
- listChanges: () => Effect.succeed([]),
147
- getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
148
- postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
149
- abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
150
- testConnection: Effect.fail(new Error('Not implemented') as ApiError),
151
- getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
152
- getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
153
- getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
154
- getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
155
- getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
156
- getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
157
- getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
158
- getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
159
- })
148
+ server.use(
149
+ http.get('*/a/changes/', () => {
150
+ return HttpResponse.text(")]}'\n[]")
151
+ }),
152
+ )
160
153
 
154
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
161
155
  await Effect.runPromise(
162
- mineCommand({ xml: true }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
156
+ mineCommand({ xml: true }).pipe(
157
+ Effect.provide(GerritApiServiceLive),
158
+ Effect.provide(mockConfigLayer),
159
+ ),
163
160
  )
164
161
 
165
- const output = mockConsole.logs.join('\n')
162
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
166
163
  expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
167
164
  expect(output).toContain('<changes count="0">')
168
165
  expect(output).toContain('</changes>')
169
166
  })
170
167
 
171
168
  test('should handle network failures gracefully', async () => {
172
- const mockApi = GerritApiService.of({
173
- listChanges: () => Effect.fail(new Error('Network error') as ApiError),
174
- getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
175
- postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
176
- abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
177
- testConnection: Effect.fail(new Error('Not implemented') as ApiError),
178
- getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
179
- getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
180
- getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
181
- getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
182
- getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
183
- getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
184
- getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
185
- getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
186
- })
169
+ server.use(
170
+ http.get('*/a/changes/', () => {
171
+ return HttpResponse.text('Network error', { status: 500 })
172
+ }),
173
+ )
187
174
 
175
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
188
176
  const result = await Effect.runPromise(
189
177
  Effect.either(
190
- mineCommand({ xml: false }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
178
+ mineCommand({ xml: false }).pipe(
179
+ Effect.provide(GerritApiServiceLive),
180
+ Effect.provide(mockConfigLayer),
181
+ ),
191
182
  ),
192
183
  )
193
184
 
194
185
  expect(result._tag).toBe('Left')
195
- if (result._tag === 'Left') {
196
- expect(result.left.message).toBe('Network error')
197
- }
198
186
  })
199
187
 
200
188
  test('should handle network failures gracefully in XML format', async () => {
201
- const mockApi = GerritApiService.of({
202
- listChanges: () => Effect.fail(new Error('API error') as ApiError),
203
- getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
204
- postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
205
- abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
206
- testConnection: Effect.fail(new Error('Not implemented') as ApiError),
207
- getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
208
- getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
209
- getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
210
- getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
211
- getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
212
- getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
213
- getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
214
- getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
215
- })
189
+ server.use(
190
+ http.get('*/a/changes/', () => {
191
+ return HttpResponse.text('API error', { status: 500 })
192
+ }),
193
+ )
216
194
 
195
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
217
196
  const result = await Effect.runPromise(
218
197
  Effect.either(
219
- mineCommand({ xml: true }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
198
+ mineCommand({ xml: true }).pipe(
199
+ Effect.provide(GerritApiServiceLive),
200
+ Effect.provide(mockConfigLayer),
201
+ ),
220
202
  ),
221
203
  )
222
204
 
223
205
  expect(result._tag).toBe('Left')
224
- if (result._tag === 'Left') {
225
- expect(result.left.message).toBe('API error')
226
- }
227
206
  })
228
207
 
229
208
  test('should properly escape XML special characters', async () => {
@@ -237,27 +216,21 @@ describe('mine command', () => {
237
216
  }),
238
217
  ]
239
218
 
240
- const mockApi = GerritApiService.of({
241
- listChanges: () => Effect.succeed(mockChanges),
242
- getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
243
- postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
244
- abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
245
- testConnection: Effect.fail(new Error('Not implemented') as ApiError),
246
- getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
247
- getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
248
- getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
249
- getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
250
- getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
251
- getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
252
- getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
253
- getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
254
- })
219
+ server.use(
220
+ http.get('*/a/changes/', () => {
221
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChanges)}`)
222
+ }),
223
+ )
255
224
 
225
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
256
226
  await Effect.runPromise(
257
- mineCommand({ xml: true }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
227
+ mineCommand({ xml: true }).pipe(
228
+ Effect.provide(GerritApiServiceLive),
229
+ Effect.provide(mockConfigLayer),
230
+ ),
258
231
  )
259
232
 
260
- const output = mockConsole.logs.join('\n')
233
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
261
234
  // CDATA sections should preserve special characters
262
235
  expect(output).toContain('<![CDATA[Test with <special> & "characters"]]>')
263
236
  expect(output).toContain('<branch>feature/test&update</branch>')
@@ -288,27 +261,21 @@ describe('mine command', () => {
288
261
  }),
289
262
  ]
290
263
 
291
- const mockApi = GerritApiService.of({
292
- listChanges: () => Effect.succeed(mockChanges),
293
- getChange: () => Effect.fail(new Error('Not implemented') as ApiError),
294
- postReview: () => Effect.fail(new Error('Not implemented') as ApiError),
295
- abandonChange: () => Effect.fail(new Error('Not implemented') as ApiError),
296
- testConnection: Effect.fail(new Error('Not implemented') as ApiError),
297
- getRevision: () => Effect.fail(new Error('Not implemented') as ApiError),
298
- getFiles: () => Effect.fail(new Error('Not implemented') as ApiError),
299
- getFileDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
300
- getFileContent: () => Effect.fail(new Error('Not implemented') as ApiError),
301
- getPatch: () => Effect.fail(new Error('Not implemented') as ApiError),
302
- getDiff: () => Effect.fail(new Error('Not implemented') as ApiError),
303
- getComments: () => Effect.fail(new Error('Not implemented') as ApiError),
304
- getMessages: () => Effect.fail(new Error('Not implemented') as ApiError),
305
- })
264
+ server.use(
265
+ http.get('*/a/changes/', () => {
266
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChanges)}`)
267
+ }),
268
+ )
306
269
 
270
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
307
271
  await Effect.runPromise(
308
- mineCommand({ xml: false }).pipe(Effect.provide(Layer.succeed(GerritApiService, mockApi))),
272
+ mineCommand({ xml: false }).pipe(
273
+ Effect.provide(GerritApiServiceLive),
274
+ Effect.provide(mockConfigLayer),
275
+ ),
309
276
  )
310
277
 
311
- const output = mockConsole.logs.join('\n')
278
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
312
279
  expect(output).toContain('Change in project A')
313
280
  expect(output).toContain('Change in project B')
314
281
  expect(output).toContain('Another change in project A')
@@ -75,6 +75,7 @@ ${JSON.stringify(mockChange)}`)
75
75
 
76
76
  let capturedLogs: string[] = []
77
77
  let capturedErrors: string[] = []
78
+ let capturedStdout: string[] = []
78
79
 
79
80
  const mockConsoleLog = mock((...args: any[]) => {
80
81
  capturedLogs.push(args.join(' '))
@@ -83,8 +84,19 @@ const mockConsoleError = mock((...args: any[]) => {
83
84
  capturedErrors.push(args.join(' '))
84
85
  })
85
86
 
87
+ // Mock process.stdout.write to capture JSON/XML output and handle callbacks
88
+ const mockStdoutWrite = mock((chunk: any, callback?: any) => {
89
+ capturedStdout.push(String(chunk))
90
+ // Call the callback synchronously if provided
91
+ if (typeof callback === 'function') {
92
+ callback()
93
+ }
94
+ return true
95
+ })
96
+
86
97
  const originalConsoleLog = console.log
87
98
  const originalConsoleError = console.error
99
+ const originalStdoutWrite = process.stdout.write
88
100
 
89
101
  let spawnSpy: ReturnType<typeof spyOn>
90
102
 
@@ -94,20 +106,26 @@ beforeAll(() => {
94
106
  console.log = mockConsoleLog
95
107
  // @ts-ignore
96
108
  console.error = mockConsoleError
109
+ // @ts-ignore
110
+ process.stdout.write = mockStdoutWrite
97
111
  })
98
112
 
99
113
  afterAll(() => {
100
114
  server.close()
101
115
  console.log = originalConsoleLog
102
116
  console.error = originalConsoleError
117
+ // @ts-ignore
118
+ process.stdout.write = originalStdoutWrite
103
119
  })
104
120
 
105
121
  afterEach(() => {
106
122
  server.resetHandlers()
107
123
  mockConsoleLog.mockClear()
108
124
  mockConsoleError.mockClear()
125
+ mockStdoutWrite.mockClear()
109
126
  capturedLogs = []
110
127
  capturedErrors = []
128
+ capturedStdout = []
111
129
 
112
130
  if (spawnSpy) {
113
131
  spawnSpy.mockRestore()
@@ -184,7 +202,7 @@ Change-Id: If5a3ae8cb5a107e187447802358417f311d0c4b1`
184
202
 
185
203
  await resultPromise
186
204
 
187
- const output = capturedLogs.join('\n')
205
+ const output = capturedStdout.join('')
188
206
  expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
189
207
  expect(output).toContain('<show_result>')
190
208
  expect(output).toContain('<status>success</status>')
@@ -297,7 +315,7 @@ This commit has no Change-ID footer.`
297
315
 
298
316
  await resultPromise
299
317
 
300
- const output = capturedLogs.join('\n')
318
+ const output = capturedStdout.join('')
301
319
  expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
302
320
  expect(output).toContain('<show_result>')
303
321
  expect(output).toContain('<status>error</status>')