15 "github.com/stretchr/testify/require"
18 // chatgptTestAccount is the fake ChatGPT account ID in the fake tokens.
19 const chatgptTestAccount = "account-test"
21 // ChatGPTFake is an httptest login service. The device token endpoint answers
22 // 403 pending times before it grants the code. Each token answer has a new
23 // access token, kept in access.
24 type ChatGPTFake struct {
38 // token returns a new access token that expires after ttl, for a token file
39 // that a test writes.
40 func (f *ChatGPTFake) token(ttl time.Duration) string {
42 defer f.mutex.Unlock()
44 return f.newToken(ttl)
47 // newToken returns a new fake JWT with the expiry and the account claim, and
48 // keeps it in access. The caller holds the mutex.
49 func (f *ChatGPTFake) newToken(ttl time.Duration) string {
50 claims, err := json.Marshal(map[string]any{
51 "exp": time.Now().Add(ttl).Unix(),
52 chatgptAuthClaim: map[string]any{
53 "chatgpt_account_id": chatgptTestAccount,
55 "serial": len(f.access),
61 encode := base64.RawURLEncoding.EncodeToString
62 token := encode([]byte(`{"alg":"none"}`)) + "." + encode(claims) + "." + encode([]byte("fake"))
63 f.access = append(f.access, token)
68 func (f *ChatGPTFake) userCode(w http.ResponseWriter, r *http.Request) {
69 var body map[string]string
70 err := json.NewDecoder(r.Body).Decode(&body)
71 if err != nil || body["client_id"] != chatgptClientID {
72 http.Error(w, "bad client", http.StatusBadRequest)
76 chatgptWriteJSON(w, map[string]any{
77 "device_auth_id": "device-1",
78 "user_code": "CODE-1234",
83 func (f *ChatGPTFake) deviceToken(w http.ResponseWriter, r *http.Request) {
85 defer f.mutex.Unlock()
87 var body map[string]string
88 err := json.NewDecoder(r.Body).Decode(&body)
89 if err != nil || body["device_auth_id"] != "device-1" || body["user_code"] != "CODE-1234" {
90 http.Error(w, "bad device", http.StatusBadRequest)
96 http.Error(w, "denied", http.StatusBadRequest)
100 if f.polls <= f.pending {
101 http.Error(w, "pending", http.StatusForbidden)
105 chatgptWriteJSON(w, map[string]any{
106 "authorization_code": "auth-code",
107 "code_challenge": "challenge",
108 "code_verifier": "verifier",
112 func (f *ChatGPTFake) oauthToken(w http.ResponseWriter, r *http.Request) {
114 defer f.mutex.Unlock()
116 if r.Header.Get("Content-Type") == "application/x-www-form-urlencoded" {
119 http.Error(w, err.Error(), http.StatusBadRequest)
123 f.exchange = r.PostForm
124 chatgptWriteJSON(w, map[string]any{
125 "access_token": f.newToken(time.Hour),
126 "refresh_token": "refresh-1",
127 "id_token": f.newToken(time.Hour),
133 var body map[string]string
134 err := json.NewDecoder(r.Body).Decode(&body)
135 if err != nil || body["grant_type"] != "refresh_token" || body["client_id"] != chatgptClientID {
136 http.Error(w, "bad refresh", http.StatusBadRequest)
141 f.refreshed = body["refresh_token"]
143 http.Error(w, `{"error": "invalid_grant", "token": "`+body["refresh_token"]+`"}`, http.StatusUnauthorized)
147 answer := map[string]any{
148 "access_token": f.newToken(time.Hour),
149 "id_token": f.newToken(time.Hour),
152 if f.newRefresh != "" {
153 answer["refresh_token"] = f.newRefresh
156 chatgptWriteJSON(w, answer)
159 func newChatGPTFake(t *testing.T) *ChatGPTFake {
163 mux := http.NewServeMux()
164 mux.HandleFunc("POST /api/accounts/deviceauth/usercode", f.userCode)
165 mux.HandleFunc("POST /api/accounts/deviceauth/token", f.deviceToken)
166 mux.HandleFunc("POST /oauth/token", f.oauthToken)
167 f.Server = httptest.NewServer(mux)
173 func newTestChatGPTAuth(t *testing.T, server *ChatGPTFake) *ChatGPTAuth {
176 return newChatGPTAuth(filepath.Join(t.TempDir(), chatgptTokenFileName), server.URL)
179 func chatgptWriteTestTokens(t *testing.T, auth *ChatGPTAuth, access, refresh string) {
182 err := auth.write(ChatGPTTokenFile{
184 RefreshToken: refresh,
186 AccountID: chatgptTestAccount,
188 require.NoError(t, err)
191 func chatgptWriteJSON(w http.ResponseWriter, value any) {
192 w.Header().Set("Content-Type", "application/json")
193 err := json.NewEncoder(w).Encode(value)
195 panic(fmt.Sprint("encode answer: ", err))