14 "github.com/stretchr/testify/require"
17 func TestLoginWritesTheTokenFile(t *testing.T) {
18 server := newChatGPTFake(t)
20 auth := newTestChatGPTAuth(t, server)
22 var out strings.Builder
23 err := auth.Login(t.Context(), &out)
24 require.NoError(t, err)
26 require.Contains(t, out.String(), server.URL+"/codex/device")
27 require.Contains(t, out.String(), "CODE-1234")
28 require.Equal(t, 3, server.polls)
30 form := server.exchange
31 require.Equal(t, "authorization_code", form.Get("grant_type"))
32 require.Equal(t, "auth-code", form.Get("code"))
33 require.Equal(t, "verifier", form.Get("code_verifier"))
34 require.Equal(t, chatgptClientID, form.Get("client_id"))
35 require.Equal(t, server.URL+"/deviceauth/callback", form.Get("redirect_uri"))
37 tokens := chatgptReadTokenFile(t, auth.path)
38 require.Equal(t, "refresh-1", tokens.RefreshToken)
39 require.Equal(t, chatgptTestAccount, tokens.AccountID)
40 require.Equal(t, server.access[0], tokens.AccessToken)
41 require.NotContains(t, out.String(), tokens.AccessToken)
42 require.NotContains(t, out.String(), tokens.RefreshToken)
44 info, err := os.Stat(auth.path)
45 require.NoError(t, err)
46 require.Equal(t, os.FileMode(0o600), info.Mode().Perm())
49 func TestLoginFailsWhenTheCodeIsDenied(t *testing.T) {
50 server := newChatGPTFake(t)
52 auth := newTestChatGPTAuth(t, server)
54 err := auth.Login(t.Context(), io.Discard)
55 require.ErrorContains(t, err, "HTTP 400")
56 require.NoFileExists(t, auth.path)
59 func TestAuthorizeUsesAFreshTokenWithoutRefresh(t *testing.T) {
60 server := newChatGPTFake(t)
61 auth := newTestChatGPTAuth(t, server)
62 chatgptWriteTestTokens(t, auth, server.token(time.Hour), "refresh-0")
64 header := http.Header{}
65 err := auth.Authorize(t.Context(), header)
66 require.NoError(t, err)
67 require.Equal(t, "Bearer "+server.access[0], header.Get("Authorization"))
68 require.Equal(t, chatgptTestAccount, header.Get("ChatGPT-Account-Id"))
69 require.Equal(t, 0, server.refreshes)
72 func TestAuthorizeRefreshesNearTheExpiry(t *testing.T) {
73 testCases := []struct {
79 name: "new refresh token kept",
80 newRefresh: "refresh-new",
81 wantRefresh: "refresh-new",
84 name: "old refresh token kept without a new one",
86 wantRefresh: "refresh-0",
90 for _, tc := range testCases {
91 t.Run(tc.name, func(t *testing.T) {
92 server := newChatGPTFake(t)
93 server.newRefresh = tc.newRefresh
94 auth := newTestChatGPTAuth(t, server)
95 chatgptWriteTestTokens(t, auth, server.token(59*time.Second), "refresh-0")
97 header := http.Header{}
98 err := auth.Authorize(t.Context(), header)
99 require.NoError(t, err)
100 require.Equal(t, 1, server.refreshes)
101 require.Equal(t, "refresh-0", server.refreshed)
103 tokens := chatgptReadTokenFile(t, auth.path)
104 require.Equal(t, server.access[1], tokens.AccessToken)
105 require.Equal(t, tc.wantRefresh, tokens.RefreshToken)
106 require.Equal(t, chatgptTestAccount, tokens.AccountID)
107 require.Equal(t, "Bearer "+server.access[1], header.Get("Authorization"))
109 err = auth.Authorize(t.Context(), header)
110 require.NoError(t, err)
111 require.Equal(t, 1, server.refreshes)
116 func TestAuthorizeRunsOneRefreshAtATime(t *testing.T) {
117 server := newChatGPTFake(t)
118 server.newRefresh = "refresh-new"
119 auth := newTestChatGPTAuth(t, server)
120 chatgptWriteTestTokens(t, auth, server.token(time.Second), "refresh-0")
122 var group sync.WaitGroup
123 errs := make(chan error, 8)
126 errs <- auth.Authorize(t.Context(), http.Header{})
132 for err := range errs {
133 require.NoError(t, err)
136 require.Equal(t, 1, server.refreshes)
139 func TestAuthorizeNeedsALogin(t *testing.T) {
140 server := newChatGPTFake(t)
141 auth := newTestChatGPTAuth(t, server)
143 err := auth.Authorize(t.Context(), http.Header{})
144 require.ErrorIs(t, err, errChatGPTLoginNeeded)
146 server.failRefresh = true
147 chatgptWriteTestTokens(t, auth, server.token(-time.Minute), "refresh-0")
148 before, err := os.ReadFile(auth.path)
149 require.NoError(t, err)
151 err = auth.Authorize(t.Context(), http.Header{})
152 require.ErrorIs(t, err, errChatGPTLoginNeeded)
153 require.NotContains(t, err.Error(), "refresh-0")
155 after, err := os.ReadFile(auth.path)
156 require.NoError(t, err)
157 require.Equal(t, before, after)
160 func TestForcedRefreshLetsTheNextCallWork(t *testing.T) {
161 server := newChatGPTFake(t)
162 auth := newTestChatGPTAuth(t, server)
163 chatgptWriteTestTokens(t, auth, server.token(-time.Hour), "refresh-0")
165 responses := newModelFake(t)
166 client := newModelClient(responses.URL, auth, "agent-key", filepath.Join(t.TempDir(), "usage.jsonl"))
167 _, err := client.Create(t.Context(), modelTestRequest())
168 require.NoError(t, err)
170 require.Equal(t, 1, server.refreshes)
171 call := responses.Call(0)
172 require.Equal(t, "Bearer "+server.access[1], call.header.Get("Authorization"))
173 require.Equal(t, chatgptTestAccount, call.header.Get("ChatGPT-Account-Id"))
174 require.Equal(t, server.access[1], chatgptReadTokenFile(t, auth.path).AccessToken)
177 func TestModelEndpointFollowsTheConfig(t *testing.T) {
179 config := defaultConfig()
181 t.Setenv("OPENAI_API_KEY", modelTestKey)
182 address, auth, err := modelEndpoint(config, dir)
183 require.NoError(t, err)
184 require.Equal(t, modelChatGPTURL, address)
185 require.IsType(t, &ChatGPTAuth{}, auth)
186 require.Equal(t, filepath.Join(dir, chatgptTokenFileName), auth.(*ChatGPTAuth).path)
188 config.Endpoint = configEndpointAPI
189 address, auth, err = modelEndpoint(config, dir)
190 require.NoError(t, err)
191 require.Equal(t, modelAPIURL, address)
192 require.Equal(t, ModelAPIKey(modelTestKey), auth)
194 t.Setenv("OPENAI_API_KEY", "")
195 _, _, err = modelEndpoint(config, dir)
196 require.ErrorContains(t, err, "OPENAI_API_KEY")
199 func chatgptReadTokenFile(t *testing.T, path string) ChatGPTTokenFile {
202 data, err := os.ReadFile(path)
203 require.NoError(t, err)
205 var tokens ChatGPTTokenFile
206 require.NoError(t, json.Unmarshal(data, &tokens))