Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
kardolus
GitHub Repository: kardolus/chatgpt-cli
Path: blob/main/api/http/http_test.go
3436 views
1
package http_test
2
3
import (
4
"bytes"
5
"io"
6
stdhttp "net/http"
7
"net/http/httptest"
8
"strings"
9
"testing"
10
11
"github.com/kardolus/chatgpt-cli/api/http"
12
chatgpthttp "github.com/kardolus/chatgpt-cli/api/http"
13
"github.com/kardolus/chatgpt-cli/config"
14
. "github.com/onsi/gomega"
15
"github.com/sclevine/spec"
16
"github.com/sclevine/spec/report"
17
)
18
19
func TestUnitHTTP(t *testing.T) {
20
spec.Run(t, "Testing the HTTP Client", testHTTP, spec.Report(report.Terminal{}))
21
}
22
23
func testHTTP(t *testing.T, when spec.G, it spec.S) {
24
var subject http.RestCaller
25
26
const responsesPath = "/v1/responses" // use http.ResponsesPath if you export it
27
28
it.Before(func() {
29
RegisterTestingT(t)
30
subject = http.RestCaller{}
31
})
32
33
when("ProcessResponse()", func() {
34
it("parses a legacy stream as expected (any endpoint)", func() {
35
buf := &bytes.Buffer{}
36
// legacy works via both branches; use a non-responses endpoint to
37
// ensure we exercise the original/legacy code path.
38
subject.ProcessResponse(strings.NewReader(legacyStream), buf, "/v1/chat/completions")
39
output := buf.String()
40
Expect(output).To(Equal("a b c\n"))
41
})
42
43
it("parses a GPT-5 SSE stream when endpoint is /v1/responses", func() {
44
buf := &bytes.Buffer{}
45
subject.ProcessResponse(strings.NewReader(gpt5Stream), buf, responsesPath)
46
output := buf.String()
47
// deltas are "a", " b", " c" then response.completed -> newline
48
Expect(output).To(Equal("a b c\n"))
49
})
50
51
it("throws an error when the legacy json is invalid", func() {
52
input := `data: {"invalid":"json"` // missing closing brace
53
expectedOutput := "Error: unexpected end of JSON input\n"
54
55
var buf bytes.Buffer
56
subject.ProcessResponse(strings.NewReader(input), &buf, "/v1/chat/completions")
57
output := buf.String()
58
Expect(output).To(Equal(expectedOutput))
59
})
60
})
61
62
when("Get()", func() {
63
it("returns the response body on success", func() {
64
t.Parallel()
65
66
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
67
Expect(r.Method).To(Equal(stdhttp.MethodGet))
68
w.WriteHeader(stdhttp.StatusOK)
69
_, _ = w.Write([]byte(`{"ok":true}`))
70
}))
71
defer server.Close()
72
73
subject := chatgpthttp.New(config.Config{})
74
75
body, err := subject.Get(server.URL)
76
Expect(err).NotTo(HaveOccurred())
77
Expect(string(body)).To(Equal(`{"ok":true}`))
78
})
79
})
80
81
when("Post()", func() {
82
it("POSTs the body and returns the response body on success", func() {
83
t.Parallel()
84
85
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
86
Expect(r.Method).To(Equal(stdhttp.MethodPost))
87
88
b, err := io.ReadAll(r.Body)
89
Expect(err).NotTo(HaveOccurred())
90
Expect(string(b)).To(Equal(`{"hello":"world"}`))
91
92
w.WriteHeader(stdhttp.StatusOK)
93
_, _ = w.Write([]byte(`{"ok":true}`))
94
}))
95
defer server.Close()
96
97
subject := chatgpthttp.New(config.Config{})
98
99
out, err := subject.Post(server.URL, []byte(`{"hello":"world"}`), false)
100
Expect(err).NotTo(HaveOccurred())
101
Expect(string(out)).To(Equal(`{"ok":true}`))
102
})
103
})
104
105
when("PostWithHeaders()", func() {
106
it("attaches headers and returns the response body on success", func() {
107
t.Parallel()
108
109
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
110
Expect(r.Method).To(Equal(stdhttp.MethodPost))
111
Expect(r.Header.Get("X-Test")).To(Equal("abc"))
112
113
w.WriteHeader(stdhttp.StatusOK)
114
_, _ = w.Write([]byte(`{"ok":true}`))
115
}))
116
defer server.Close()
117
118
subject := chatgpthttp.New(config.Config{})
119
120
out, err := subject.PostWithHeaders(server.URL, []byte(`{}`), map[string]string{
121
"X-Test": "abc",
122
})
123
Expect(err).NotTo(HaveOccurred())
124
Expect(string(out)).To(Equal(`{"ok":true}`))
125
})
126
127
it("returns body + error on non-2xx with OpenAI-style error json", func() {
128
t.Parallel()
129
130
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
131
w.WriteHeader(stdhttp.StatusUnauthorized)
132
_, _ = w.Write([]byte(`{"error":{"message":"nope"}}`))
133
}))
134
defer server.Close()
135
136
subject := chatgpthttp.New(config.Config{})
137
138
out, err := subject.PostWithHeaders(server.URL, []byte(`{}`), nil)
139
Expect(err).To(HaveOccurred())
140
Expect(string(out)).To(ContainSubstring(`"nope"`))
141
})
142
})
143
144
when("PostWithHeadersResponse()", func() {
145
it("returns status, headers, and body on success", func() {
146
t.Parallel()
147
148
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
149
Expect(r.Method).To(Equal(stdhttp.MethodPost))
150
Expect(r.Header.Get("X-Test")).To(Equal("abc"))
151
152
w.Header().Set("mcp-session-id", "sid-123")
153
w.WriteHeader(stdhttp.StatusOK)
154
_, _ = w.Write([]byte(`{"ok":true}`))
155
}))
156
defer server.Close()
157
158
rc := http.RestCaller{} // ✅ no NewRestCaller
159
160
resp, err := rc.PostWithHeadersResponse(server.URL, []byte(`{"hello":"world"}`), map[string]string{
161
"X-Test": "abc",
162
})
163
164
Expect(err).NotTo(HaveOccurred())
165
166
Expect(resp.Status).To(Equal(stdhttp.StatusOK))
167
Expect(resp.Headers).NotTo(BeNil())
168
Expect(string(resp.Body)).To(Equal(`{"ok":true}`))
169
170
v, ok := headerGetCI(resp.Headers, "mcp-session-id")
171
Expect(ok).To(BeTrue())
172
Expect(v).To(Equal("sid-123"))
173
})
174
175
it("returns response + error on non-2xx with OpenAI-style error json", func() {
176
t.Parallel()
177
178
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
179
w.Header().Set("mcp-session-id", "sid-rotated")
180
w.WriteHeader(stdhttp.StatusUnauthorized)
181
_, _ = w.Write([]byte(`{"error":{"message":"nope"}}`))
182
}))
183
defer server.Close()
184
185
rc := http.RestCaller{}
186
187
resp, err := rc.PostWithHeadersResponse(server.URL, []byte(`{}`), nil)
188
189
Expect(err).To(HaveOccurred())
190
191
Expect(resp.Status).To(Equal(stdhttp.StatusUnauthorized))
192
Expect(resp.Headers).NotTo(BeNil())
193
Expect(resp.Body).NotTo(BeEmpty())
194
195
v, ok := headerGetCI(resp.Headers, "mcp-session-id")
196
Expect(ok).To(BeTrue())
197
Expect(v).To(Equal("sid-rotated"))
198
})
199
})
200
}
201
202
const legacyStream = `
203
data: {"id":"id-1","object":"chat.completion.chunk","created":1,"model":"model-1","choices":[{"delta":{"role":"assistant"},"index":0,"finish_reason":null}]}
204
205
data: {"id":"id-2","object":"chat.completion.chunk","created":2,"model":"model-1","choices":[{"delta":{"content":"a"},"index":0,"finish_reason":null}]}
206
207
data: {"id":"id-3","object":"chat.completion.chunk","created":3,"model":"model-1","choices":[{"delta":{"content":" b"},"index":0,"finish_reason":null}]}
208
209
data: {"id":"id-4","object":"chat.completion.chunk","created":4,"model":"model-1","choices":[{"delta":{"content":" c"},"index":0,"finish_reason":null}]}
210
211
data: {"id":"id-5","object":"chat.completion.chunk","created":5,"model":"model-1","choices":[{"delta":{},"index":0,"finish_reason":"stop"}]}
212
213
data: [DONE]
214
`
215
216
// Minimal GPT-5 SSE that your new parser should handle
217
const gpt5Stream = `
218
event: response.created
219
data: {"type":"response.created"}
220
221
event: response.output_item.added
222
data: {"type":"response.output_item.added","output_index":0,"item":{"id":"msg_1","type":"message","status":"in_progress","content":[],"role":"assistant"}}
223
224
event: response.content_part.added
225
data: {"type":"response.content_part.added","item_id":"msg_1","output_index":0,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}}
226
227
event: response.output_text.delta
228
data: {"type":"response.output_text.delta","item_id":"msg_1","output_index":0,"content_index":0,"delta":"a"}
229
230
event: response.output_text.delta
231
data: {"type":"response.output_text.delta","item_id":"msg_1","output_index":0,"content_index":0,"delta":" b"}
232
233
event: response.output_text.delta
234
data: {"type":"response.output_text.delta","item_id":"msg_1","output_index":0,"content_index":0,"delta":" c"}
235
236
event: response.completed
237
data: {"type":"response.completed","response":{"status":"completed"}}
238
`
239
240
func TestUnitCustomHeaders(t *testing.T) {
241
spec.Run(t, "Testing Custom Headers", testCustomHeaders, spec.Report(report.Terminal{}))
242
}
243
244
func testCustomHeaders(t *testing.T, when spec.G, it spec.S) {
245
it.Before(func() {
246
RegisterTestingT(t)
247
})
248
249
when("custom headers are configured", func() {
250
it("attaches custom headers to POST requests", func() {
251
t.Parallel()
252
253
var receivedHeaders stdhttp.Header
254
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
255
receivedHeaders = r.Header
256
w.WriteHeader(stdhttp.StatusOK)
257
_, _ = w.Write([]byte(`{"success": true}`))
258
}))
259
defer server.Close()
260
261
cfg := config.Config{
262
CustomHeaders: map[string]string{
263
"X-Custom-Header": "custom-value",
264
"X-Another-Header": "another-value",
265
},
266
}
267
268
subject := chatgpthttp.New(cfg)
269
_, err := subject.Post(server.URL, []byte(`{"test": "data"}`), false)
270
271
Expect(err).ToNot(HaveOccurred())
272
Expect(receivedHeaders.Get("X-Custom-Header")).To(Equal("custom-value"))
273
Expect(receivedHeaders.Get("X-Another-Header")).To(Equal("another-value"))
274
})
275
276
it("attaches custom headers to GET requests", func() {
277
t.Parallel()
278
279
var receivedHeaders stdhttp.Header
280
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
281
receivedHeaders = r.Header
282
w.WriteHeader(stdhttp.StatusOK)
283
_, _ = w.Write([]byte(`{"success": true}`))
284
}))
285
defer server.Close()
286
287
cfg := config.Config{
288
CustomHeaders: map[string]string{
289
"X-API-Version": "v2",
290
"X-Client-ID": "test-client",
291
},
292
}
293
294
subject := chatgpthttp.New(cfg)
295
_, err := subject.Get(server.URL)
296
297
Expect(err).ToNot(HaveOccurred())
298
Expect(receivedHeaders.Get("X-API-Version")).To(Equal("v2"))
299
Expect(receivedHeaders.Get("X-Client-ID")).To(Equal("test-client"))
300
})
301
302
it("works with empty custom headers map", func() {
303
t.Parallel()
304
305
var receivedHeaders stdhttp.Header
306
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
307
receivedHeaders = r.Header
308
w.WriteHeader(stdhttp.StatusOK)
309
_, _ = w.Write([]byte(`{"success": true}`))
310
}))
311
defer server.Close()
312
313
cfg := config.Config{
314
CustomHeaders: map[string]string{},
315
}
316
317
subject := chatgpthttp.New(cfg)
318
_, err := subject.Post(server.URL, []byte(`{"test": "data"}`), false)
319
320
Expect(err).ToNot(HaveOccurred())
321
Expect(receivedHeaders).ToNot(BeNil())
322
})
323
324
it("works with nil custom headers map", func() {
325
t.Parallel()
326
327
var receivedHeaders stdhttp.Header
328
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
329
receivedHeaders = r.Header
330
w.WriteHeader(stdhttp.StatusOK)
331
_, _ = w.Write([]byte(`{"success": true}`))
332
}))
333
defer server.Close()
334
335
cfg := config.Config{
336
CustomHeaders: nil,
337
}
338
339
subject := chatgpthttp.New(cfg)
340
_, err := subject.Post(server.URL, []byte(`{"test": "data"}`), false)
341
342
Expect(err).ToNot(HaveOccurred())
343
Expect(receivedHeaders).ToNot(BeNil())
344
})
345
346
it("does not override standard headers with custom headers", func() {
347
t.Parallel()
348
349
var receivedHeaders stdhttp.Header
350
server := httptest.NewServer(stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
351
receivedHeaders = r.Header
352
w.WriteHeader(stdhttp.StatusOK)
353
_, _ = w.Write([]byte(`{"success": true}`))
354
}))
355
defer server.Close()
356
357
cfg := config.Config{
358
APIKey: "test-key",
359
AuthHeader: "Authorization",
360
AuthTokenPrefix: "Bearer ",
361
UserAgent: "TestAgent/1.0",
362
CustomHeaders: map[string]string{
363
"X-Custom-Header": "custom-value",
364
},
365
}
366
367
subject := chatgpthttp.New(cfg)
368
_, err := subject.Post(server.URL, []byte(`{"test": "data"}`), false)
369
370
Expect(err).ToNot(HaveOccurred())
371
Expect(receivedHeaders.Get("Authorization")).To(Equal("Bearer test-key"))
372
Expect(receivedHeaders.Get("User-Agent")).To(Equal("TestAgent/1.0"))
373
Expect(receivedHeaders.Get("X-Custom-Header")).To(Equal("custom-value"))
374
})
375
})
376
}
377
378
func headerGetCI(h map[string]string, key string) (string, bool) {
379
for k, v := range h {
380
if strings.EqualFold(k, key) {
381
return v, true
382
}
383
}
384
return "", false
385
}
386
387