diff options
Diffstat (limited to 'internal/api/google_tasks_oauth_test.go')
| -rw-r--r-- | internal/api/google_tasks_oauth_test.go | 91 |
1 files changed, 88 insertions, 3 deletions
diff --git a/internal/api/google_tasks_oauth_test.go b/internal/api/google_tasks_oauth_test.go index 93acd76..222baab 100644 --- a/internal/api/google_tasks_oauth_test.go +++ b/internal/api/google_tasks_oauth_test.go @@ -15,9 +15,10 @@ import ( // memTokenStore is an in-memory TokenStore. type memTokenStore struct { - mu sync.Mutex - token *oauth2.Token - saved int + mu sync.Mutex + token *oauth2.Token + saved int + lastError string } func (m *memTokenStore) GetOAuthToken(string) (*oauth2.Token, error) { @@ -30,6 +31,13 @@ func (m *memTokenStore) GetOAuthToken(string) (*oauth2.Token, error) { return &cp, nil } +func (m *memTokenStore) RecordOAuthTokenError(_ string, message string) error { + m.mu.Lock() + defer m.mu.Unlock() + m.lastError = message + return nil +} + func (m *memTokenStore) SaveOAuthToken(_ string, tok *oauth2.Token) error { m.mu.Lock() defer m.mu.Unlock() @@ -153,3 +161,80 @@ func TestDBTokenSource_NotConnected(t *testing.T) { t.Errorf("error = %q, want it to mention not being connected", err) } } + +// invalid_grant means the saved authorisation is finished — retrying cannot +// help, and the user has to reconnect. Anything else is worth retrying, so +// misclassifying it would push the user through consent for a network blip. +func TestNeedsReconnect(t *testing.T) { + cases := []struct { + err error + want bool + }{ + {errors.New(`oauth2: "invalid_grant" "Bad Request"`), true}, + {errors.New("refreshing token: oauth2: \"invalid_grant\" \"Token has been expired or revoked.\""), true}, + {errors.New("Post \"https://oauth2.googleapis.com/token\": dial tcp: i/o timeout"), false}, + {errors.New("oauth2: cannot fetch token: 503 Service Unavailable"), false}, + {nil, false}, + } + for _, c := range cases { + if got := NeedsReconnect(c.err); got != c.want { + t.Errorf("NeedsReconnect(%v) = %v, want %v", c.err, got, c.want) + } + } +} + +// A failed refresh must be recorded, or settings keeps showing "connected" +// while every background fetch fails — which is how Google Tasks appeared +// healthy for a month while returning nothing. +func TestDBTokenSource_RecordsRefreshFailure(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"error":"invalid_grant","error_description":"Token has been expired or revoked."}`)) + })) + defer srv.Close() + + store := &memTokenStore{token: expiredToken()} + src := &dbTokenSource{ + store: store, + source: GoogleTasksOAuthSource, + config: &oauth2.Config{ + ClientID: "id", + ClientSecret: "secret", + Endpoint: oauth2.Endpoint{TokenURL: srv.URL}, + }, + } + + if _, err := src.Token(); err == nil { + t.Fatal("expected the refresh to fail") + } + if store.lastError == "" { + t.Fatal("the failure was not recorded") + } + if !strings.Contains(store.lastError, "reconnect") { + t.Errorf("recorded error should tell the user to reconnect, got %q", store.lastError) + } +} + +// A successful refresh must clear a previously recorded failure; the store's +// SaveOAuthToken does that, so this pins that saving is what happens on success. +func TestDBTokenSource_SuccessSavesToken(t *testing.T) { + hits := 0 + srv := tokenServer(t, &hits) + store := &memTokenStore{token: expiredToken()} + src := &dbTokenSource{ + store: store, + source: GoogleTasksOAuthSource, + config: &oauth2.Config{ + ClientID: "id", + ClientSecret: "secret", + Endpoint: oauth2.Endpoint{TokenURL: srv.URL}, + }, + } + if _, err := src.Token(); err != nil { + t.Fatalf("Token(): %v", err) + } + if store.saved == 0 { + t.Error("a refreshed token should be persisted") + } +} |
