summaryrefslogtreecommitdiff
path: root/internal/api/google_tasks_oauth_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/api/google_tasks_oauth_test.go')
-rw-r--r--internal/api/google_tasks_oauth_test.go91
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")
+ }
+}