]> git.giacomolli.uk - onechat/blob - chatgpt_test.go
Check spacing between Go statement blocks
[onechat] / chatgpt_test.go
1 package main
2
3 import (
4         "encoding/json"
5         "io"
6         "net/http"
7         "os"
8         "path/filepath"
9         "strings"
10         "sync"
11         "testing"
12         "time"
13
14         "github.com/stretchr/testify/require"
15 )
16
17 func TestLoginWritesTheTokenFile(t *testing.T) {
18         server := newChatGPTFake(t)
19         server.pending = 2
20         auth := newTestChatGPTAuth(t, server)
21
22         var out strings.Builder
23
24         err := auth.Login(t.Context(), &out)
25         require.NoError(t, err)
26
27         require.Contains(t, out.String(), server.URL+"/codex/device")
28         require.Contains(t, out.String(), "CODE-1234")
29         require.Equal(t, 3, server.polls)
30
31         form := server.exchange
32         require.Equal(t, "authorization_code", form.Get("grant_type"))
33         require.Equal(t, "auth-code", form.Get("code"))
34         require.Equal(t, "verifier", form.Get("code_verifier"))
35         require.Equal(t, chatgptClientID, form.Get("client_id"))
36         require.Equal(t, server.URL+"/deviceauth/callback", form.Get("redirect_uri"))
37
38         tokens := chatgptReadTokenFile(t, auth.path)
39         require.Equal(t, "refresh-1", tokens.RefreshToken)
40         require.Equal(t, chatgptTestAccount, tokens.AccountID)
41         require.Equal(t, server.access[0], tokens.AccessToken)
42         require.NotContains(t, out.String(), tokens.AccessToken)
43         require.NotContains(t, out.String(), tokens.RefreshToken)
44
45         info, err := os.Stat(auth.path)
46         require.NoError(t, err)
47         require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
48 }
49
50 func TestLoginFailsWhenTheCodeIsDenied(t *testing.T) {
51         server := newChatGPTFake(t)
52         server.deny = true
53         auth := newTestChatGPTAuth(t, server)
54
55         err := auth.Login(t.Context(), io.Discard)
56         require.ErrorContains(t, err, "HTTP 400")
57         require.NoFileExists(t, auth.path)
58 }
59
60 func TestAuthorizeUsesAFreshTokenWithoutRefresh(t *testing.T) {
61         server := newChatGPTFake(t)
62         auth := newTestChatGPTAuth(t, server)
63         chatgptWriteTestTokens(t, auth, server.token(time.Hour), "refresh-0")
64
65         header := http.Header{}
66         err := auth.Authorize(t.Context(), header)
67         require.NoError(t, err)
68         require.Equal(t, "Bearer "+server.access[0], header.Get("Authorization"))
69         require.Equal(t, chatgptTestAccount, header.Get("ChatGPT-Account-Id"))
70         require.Equal(t, 0, server.refreshes)
71 }
72
73 func TestAuthorizeRefreshesNearTheExpiry(t *testing.T) {
74         testCases := []struct {
75                 name        string
76                 newRefresh  string
77                 wantRefresh string
78         }{
79                 {
80                         name:        "new refresh token kept",
81                         newRefresh:  "refresh-new",
82                         wantRefresh: "refresh-new",
83                 },
84                 {
85                         name:        "old refresh token kept without a new one",
86                         newRefresh:  "",
87                         wantRefresh: "refresh-0",
88                 },
89         }
90
91         for _, tc := range testCases {
92                 t.Run(tc.name, func(t *testing.T) {
93                         server := newChatGPTFake(t)
94                         server.newRefresh = tc.newRefresh
95                         auth := newTestChatGPTAuth(t, server)
96                         chatgptWriteTestTokens(t, auth, server.token(59*time.Second), "refresh-0")
97
98                         header := http.Header{}
99                         err := auth.Authorize(t.Context(), header)
100                         require.NoError(t, err)
101                         require.Equal(t, 1, server.refreshes)
102                         require.Equal(t, "refresh-0", server.refreshed)
103
104                         tokens := chatgptReadTokenFile(t, auth.path)
105                         require.Equal(t, server.access[1], tokens.AccessToken)
106                         require.Equal(t, tc.wantRefresh, tokens.RefreshToken)
107                         require.Equal(t, chatgptTestAccount, tokens.AccountID)
108                         require.Equal(t, "Bearer "+server.access[1], header.Get("Authorization"))
109
110                         err = auth.Authorize(t.Context(), header)
111                         require.NoError(t, err)
112                         require.Equal(t, 1, server.refreshes)
113                 })
114         }
115 }
116
117 func TestAuthorizeRunsOneRefreshAtATime(t *testing.T) {
118         server := newChatGPTFake(t)
119         server.newRefresh = "refresh-new"
120         auth := newTestChatGPTAuth(t, server)
121         chatgptWriteTestTokens(t, auth, server.token(time.Second), "refresh-0")
122
123         var group sync.WaitGroup
124
125         errs := make(chan error, 8)
126         for range 8 {
127                 group.Go(func() {
128                         errs <- auth.Authorize(t.Context(), http.Header{})
129                 })
130         }
131
132         group.Wait()
133         close(errs)
134
135         for err := range errs {
136                 require.NoError(t, err)
137         }
138
139         require.Equal(t, 1, server.refreshes)
140 }
141
142 func TestAuthorizeNeedsALogin(t *testing.T) {
143         server := newChatGPTFake(t)
144         auth := newTestChatGPTAuth(t, server)
145
146         err := auth.Authorize(t.Context(), http.Header{})
147         require.ErrorIs(t, err, errChatGPTLoginNeeded)
148
149         server.failRefresh = true
150         chatgptWriteTestTokens(t, auth, server.token(-time.Minute), "refresh-0")
151
152         before, err := os.ReadFile(auth.path)
153         require.NoError(t, err)
154
155         err = auth.Authorize(t.Context(), http.Header{})
156         require.ErrorIs(t, err, errChatGPTLoginNeeded)
157         require.NotContains(t, err.Error(), "refresh-0")
158
159         after, err := os.ReadFile(auth.path)
160         require.NoError(t, err)
161         require.Equal(t, before, after)
162 }
163
164 func TestForcedRefreshLetsTheNextCallWork(t *testing.T) {
165         server := newChatGPTFake(t)
166         auth := newTestChatGPTAuth(t, server)
167         chatgptWriteTestTokens(t, auth, server.token(-time.Hour), "refresh-0")
168
169         responses := newModelFake(t)
170         client := newModelClient(responses.URL, auth, "agent-key", filepath.Join(t.TempDir(), "usage.jsonl"))
171         _, err := client.Create(t.Context(), modelTestRequest())
172         require.NoError(t, err)
173
174         require.Equal(t, 1, server.refreshes)
175
176         call := responses.Call(0)
177         require.Equal(t, "Bearer "+server.access[1], call.header.Get("Authorization"))
178         require.Equal(t, chatgptTestAccount, call.header.Get("ChatGPT-Account-Id"))
179         require.Equal(t, server.access[1], chatgptReadTokenFile(t, auth.path).AccessToken)
180 }
181
182 func TestModelEndpointFollowsTheConfig(t *testing.T) {
183         dir := t.TempDir()
184         config := defaultConfig()
185
186         t.Setenv("OPENAI_API_KEY", modelTestKey)
187
188         address, auth, err := modelEndpoint(config, dir)
189         require.NoError(t, err)
190         require.Equal(t, modelChatGPTURL, address)
191         require.IsType(t, &ChatGPTAuth{}, auth)
192         require.Equal(t, filepath.Join(dir, chatgptTokenFileName), auth.(*ChatGPTAuth).path)
193
194         config.Endpoint = configEndpointAPI
195         address, auth, err = modelEndpoint(config, dir)
196         require.NoError(t, err)
197         require.Equal(t, modelAPIURL, address)
198         require.Equal(t, ModelAPIKey(modelTestKey), auth)
199
200         t.Setenv("OPENAI_API_KEY", "")
201
202         _, _, err = modelEndpoint(config, dir)
203         require.ErrorContains(t, err, "OPENAI_API_KEY")
204 }
205
206 func chatgptReadTokenFile(t *testing.T, path string) ChatGPTTokenFile {
207         t.Helper()
208
209         data, err := os.ReadFile(path)
210         require.NoError(t, err)
211
212         var tokens ChatGPTTokenFile
213
214         require.NoError(t, json.Unmarshal(data, &tokens))
215
216         return tokens
217 }