@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,4 +1,11 @@
1
- import { beforeEach, describe, expect, it, mock } from 'bun:test'
1
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test'
2
+ import { Effect, Layer } from 'effect'
3
+ import { HttpResponse, http } from 'msw'
4
+ import { setupServer } from 'msw/node'
5
+ import { GerritApiServiceLive } from '@/api/gerrit'
6
+ import { abandonCommand } from '@/cli/commands/abandon'
7
+ import { ConfigService } from '@/services/config'
8
+ import { createMockConfigService } from './helpers/config-mock'
2
9
  import type { ChangeInfo } from '@/schemas/gerrit'
3
10
 
4
11
  const mockChange: ChangeInfo = {
@@ -28,136 +35,196 @@ const mockChange: ChangeInfo = {
28
35
  submittable: false,
29
36
  }
30
37
 
38
+ // Create MSW server
39
+ const server = setupServer(
40
+ // Default handler for auth check
41
+ http.get('*/a/accounts/self', ({ request }) => {
42
+ const auth = request.headers.get('Authorization')
43
+ if (!auth || !auth.startsWith('Basic ')) {
44
+ return HttpResponse.text('Unauthorized', { status: 401 })
45
+ }
46
+ return HttpResponse.json({
47
+ _account_id: 1000,
48
+ name: 'Test User',
49
+ email: 'test@example.com',
50
+ })
51
+ }),
52
+ )
53
+
31
54
  describe('abandon command', () => {
32
- let mockFetch: ReturnType<typeof mock>
55
+ let mockConsoleLog: ReturnType<typeof mock>
56
+ let mockConsoleError: ReturnType<typeof mock>
57
+
58
+ beforeAll(() => {
59
+ server.listen({ onUnhandledRequest: 'bypass' })
60
+ })
61
+
62
+ afterAll(() => {
63
+ server.close()
64
+ })
33
65
 
34
66
  beforeEach(() => {
35
- // Reset fetch mock for each test
36
- mockFetch = mock(() =>
37
- Promise.resolve({
38
- ok: true,
39
- status: 200,
40
- text: () => Promise.resolve(')]}\n{}'),
67
+ mockConsoleLog = mock(() => {})
68
+ mockConsoleError = mock(() => {})
69
+ console.log = mockConsoleLog
70
+ console.error = mockConsoleError
71
+ })
72
+
73
+ afterEach(() => {
74
+ server.resetHandlers()
75
+ })
76
+
77
+ it('should abandon a change with a message', async () => {
78
+ server.use(
79
+ http.get('*/a/changes/12345', () => {
80
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
81
+ }),
82
+ http.post('*/a/changes/12345/abandon', async ({ request }) => {
83
+ const body = (await request.json()) as { message?: string }
84
+ expect(body.message).toBe('No longer needed')
85
+ return HttpResponse.text(")]}'\n{}")
41
86
  }),
42
87
  )
43
- global.fetch = mockFetch as unknown as typeof fetch
88
+
89
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
90
+ const program = abandonCommand('12345', {
91
+ message: 'No longer needed',
92
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
93
+
94
+ await Effect.runPromise(program)
95
+
96
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
97
+ expect(output).toContain('Abandoned change 12345')
98
+ expect(output).toContain('Test change to abandon')
99
+ expect(output).toContain('Message: No longer needed')
44
100
  })
45
101
 
46
- it('should call abandon API endpoint with correct parameters', async () => {
47
- // Mock successful responses
48
- mockFetch
49
- .mockResolvedValueOnce({
50
- ok: true,
51
- text: async () => `)]}'
52
- {
53
- "id": "test-project~master~I123",
54
- "project": "test-project",
55
- "branch": "master",
56
- "change_id": "I123",
57
- "subject": "Test change to abandon",
58
- "status": "NEW",
59
- "_number": 12345
60
- }`,
61
- })
62
- .mockResolvedValueOnce({
63
- ok: true,
64
- text: async () => ')]}\n{}',
65
- })
66
-
67
- // Note: This is a unit test demonstrating the API calls
68
- // Actual integration would require running the full command
69
- // which we avoid to prevent hitting production
70
-
71
- // Verify the mock setup
72
- const response = await mockFetch('https://test.gerrit.com/a/changes/12345')
73
- const text = await response.text()
74
- expect(text).toContain('Test change to abandon')
75
-
76
- // Verify abandon endpoint would be called
77
- const abandonResponse = await mockFetch('https://test.gerrit.com/a/changes/12345/abandon', {
78
- method: 'POST',
79
- body: JSON.stringify({ message: 'No longer needed' }),
80
- })
81
- expect(abandonResponse.ok).toBe(true)
102
+ it('should abandon a change without a message', async () => {
103
+ server.use(
104
+ http.get('*/a/changes/12345', () => {
105
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
106
+ }),
107
+ http.post('*/a/changes/12345/abandon', async ({ request }) => {
108
+ const body = (await request.json()) as { message?: string }
109
+ expect(body.message).toBeUndefined()
110
+ return HttpResponse.text(")]}'\n{}")
111
+ }),
112
+ )
82
113
 
83
- // Verify calls were made
84
- expect(mockFetch).toHaveBeenCalledTimes(2)
114
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
115
+ const program = abandonCommand('12345', {}).pipe(
116
+ Effect.provide(GerritApiServiceLive),
117
+ Effect.provide(mockConfigLayer),
118
+ )
119
+
120
+ await Effect.runPromise(program)
121
+
122
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
123
+ expect(output).toContain('Abandoned change 12345')
124
+ expect(output).toContain('Test change to abandon')
125
+ expect(output).not.toContain('Message:')
85
126
  })
86
127
 
87
- it('should handle abandon without message', async () => {
88
- mockFetch.mockResolvedValueOnce({
89
- ok: true,
90
- text: async () => ')]}\n{}',
91
- })
128
+ it('should output XML format when --xml flag is used', async () => {
129
+ server.use(
130
+ http.get('*/a/changes/12345', () => {
131
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
132
+ }),
133
+ http.post('*/a/changes/12345/abandon', async ({ request }) => {
134
+ const body = (await request.json()) as { message?: string }
135
+ expect(body.message).toBe('Abandoning for testing')
136
+ return HttpResponse.text(")]}'\n{}")
137
+ }),
138
+ )
92
139
 
93
- const response = await mockFetch('https://test.gerrit.com/a/changes/12345/abandon', {
94
- method: 'POST',
95
- body: JSON.stringify({}),
96
- })
140
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
141
+ const program = abandonCommand('12345', {
142
+ xml: true,
143
+ message: 'Abandoning for testing',
144
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
145
+
146
+ await Effect.runPromise(program)
147
+
148
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
149
+ expect(output).toContain('<?xml version="1.0" encoding="UTF-8"?>')
150
+ expect(output).toContain('<abandon_result>')
151
+ expect(output).toContain('<status>success</status>')
152
+ expect(output).toContain('<change_number>12345</change_number>')
153
+ expect(output).toContain('<subject><![CDATA[Test change to abandon]]></subject>')
154
+ expect(output).toContain('<message><![CDATA[Abandoning for testing]]></message>')
155
+ expect(output).toContain('</abandon_result>')
156
+ })
97
157
 
98
- expect(response.ok).toBe(true)
99
- expect(mockFetch).toHaveBeenCalledWith('https://test.gerrit.com/a/changes/12345/abandon', {
100
- method: 'POST',
101
- body: JSON.stringify({}),
102
- })
158
+ it('should output XML format without message when no message provided', async () => {
159
+ server.use(
160
+ http.get('*/a/changes/12345', () => {
161
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
162
+ }),
163
+ http.post('*/a/changes/12345/abandon', async () => {
164
+ return HttpResponse.text(")]}'\n{}")
165
+ }),
166
+ )
167
+
168
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
169
+ const program = abandonCommand('12345', { xml: true }).pipe(
170
+ Effect.provide(GerritApiServiceLive),
171
+ Effect.provide(mockConfigLayer),
172
+ )
173
+
174
+ await Effect.runPromise(program)
175
+
176
+ const output = mockConsoleLog.mock.calls.map((call) => call[0]).join('\n')
177
+ expect(output).toContain('<abandon_result>')
178
+ expect(output).toContain('<status>success</status>')
179
+ expect(output).not.toContain('<message>')
103
180
  })
104
181
 
105
- it('should handle API errors', async () => {
106
- mockFetch.mockResolvedValueOnce({
107
- ok: false,
108
- status: 404,
109
- text: async () => 'Change not found',
110
- })
182
+ it('should handle not found errors gracefully', async () => {
183
+ server.use(
184
+ http.get('*/a/changes/99999', () => {
185
+ return HttpResponse.text('Change not found', { status: 404 })
186
+ }),
187
+ )
111
188
 
112
- const response = await mockFetch('https://test.gerrit.com/a/changes/99999/abandon')
113
- expect(response.ok).toBe(false)
114
- expect(response.status).toBe(404)
189
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
190
+ const program = abandonCommand('99999', {
191
+ message: 'Test message',
192
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
115
193
 
116
- const errorText = await response.text()
117
- expect(errorText).toBe('Change not found')
194
+ // Should fail when change is not found
195
+ await expect(Effect.runPromise(program)).rejects.toThrow()
118
196
  })
119
197
 
120
- it('should format message correctly in request body', () => {
121
- const testCases = [
122
- { input: undefined, expected: {} },
123
- { input: '', expected: {} },
124
- { input: 'Abandoning this change', expected: { message: 'Abandoning this change' } },
125
- { input: 'Multi\nline\nmessage', expected: { message: 'Multi\nline\nmessage' } },
126
- ]
127
-
128
- for (const testCase of testCases) {
129
- const body = testCase.input ? { message: testCase.input } : {}
130
- expect(body).toEqual(testCase.expected)
131
- }
198
+ it('should show error when change ID is not provided', async () => {
199
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
200
+ const program = abandonCommand(undefined, {}).pipe(
201
+ Effect.provide(GerritApiServiceLive),
202
+ Effect.provide(mockConfigLayer),
203
+ )
204
+
205
+ await Effect.runPromise(program)
206
+
207
+ const errorOutput = mockConsoleError.mock.calls.map((call) => call[0]).join('\n')
208
+ expect(errorOutput).toContain('Change ID is required')
209
+ expect(errorOutput).toContain('Usage: ger abandon <change-id>')
132
210
  })
133
211
 
134
- describe('interactive mode API patterns', () => {
135
- it('should fetch changes for interactive mode', async () => {
136
- mockFetch.mockResolvedValueOnce({
137
- ok: true,
138
- text: async () => `)]}'\n[${JSON.stringify(mockChange)}]`,
139
- })
140
-
141
- // Test the API call pattern for interactive mode
142
- const response = await mockFetch(
143
- 'https://test.gerrit.com/a/changes/?q=owner:self+status:open',
144
- )
145
- const text = await response.text()
146
- expect(text).toContain('Test change to abandon')
147
- })
212
+ it('should handle abandon API failure', async () => {
213
+ server.use(
214
+ http.get('*/a/changes/12345', () => {
215
+ return HttpResponse.text(`)]}'\n${JSON.stringify(mockChange)}`)
216
+ }),
217
+ http.post('*/a/changes/12345/abandon', () => {
218
+ return HttpResponse.text('Forbidden', { status: 403 })
219
+ }),
220
+ )
148
221
 
149
- it('should handle empty changes list response', async () => {
150
- mockFetch.mockResolvedValueOnce({
151
- ok: true,
152
- text: async () => ")]}'\n[]",
153
- })
154
-
155
- const response = await mockFetch(
156
- 'https://test.gerrit.com/a/changes/?q=owner:self+status:open',
157
- )
158
- const text = await response.text()
159
- const parsed = JSON.parse(text.replace(")]}'\n", ''))
160
- expect(parsed).toEqual([])
161
- })
222
+ const mockConfigLayer = Layer.succeed(ConfigService, createMockConfigService())
223
+ const program = abandonCommand('12345', {
224
+ message: 'Test',
225
+ }).pipe(Effect.provide(GerritApiServiceLive), Effect.provide(mockConfigLayer))
226
+
227
+ // Should throw/fail
228
+ await expect(Effect.runPromise(program)).rejects.toThrow()
162
229
  })
163
230
  })