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.go122
1 files changed, 122 insertions, 0 deletions
diff --git a/internal/api/google_tasks_oauth_test.go b/internal/api/google_tasks_oauth_test.go
new file mode 100644
index 0000000..b4ea6f3
--- /dev/null
+++ b/internal/api/google_tasks_oauth_test.go
@@ -0,0 +1,122 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+ "time"
+
+ "golang.org/x/oauth2"
+)
+
+// fakeTokenStore is a minimal in-memory TokenStore for tests.
+type fakeTokenStore struct {
+ tokens map[string]*oauth2.Token
+ saved []*oauth2.Token // records every SaveOAuthToken call, in order
+}
+
+func newFakeTokenStore() *fakeTokenStore {
+ return &fakeTokenStore{tokens: make(map[string]*oauth2.Token)}
+}
+
+func (f *fakeTokenStore) GetOAuthToken(source string) (*oauth2.Token, error) {
+ return f.tokens[source], nil
+}
+
+func (f *fakeTokenStore) SaveOAuthToken(source string, tok *oauth2.Token) error {
+ f.tokens[source] = tok
+ f.saved = append(f.saved, tok)
+ return nil
+}
+
+func TestDbTokenSource_NotConnected_ReturnsError(t *testing.T) {
+ store := newFakeTokenStore()
+ src := &dbTokenSource{ctx: context.Background(), store: store, source: "google_tasks", config: &oauth2.Config{}}
+
+ if _, err := src.Token(); err == nil {
+ t.Error("expected an error when no token is stored, got nil")
+ }
+}
+
+func TestDbTokenSource_NotExpired_ReturnsWithoutPersisting(t *testing.T) {
+ store := newFakeTokenStore()
+ future := time.Now().Add(time.Hour)
+ store.tokens["google_tasks"] = &oauth2.Token{
+ AccessToken: "still-valid", RefreshToken: "r1", TokenType: "Bearer", Expiry: future,
+ }
+ src := &dbTokenSource{ctx: context.Background(), store: store, source: "google_tasks", config: &oauth2.Config{}}
+
+ tok, err := src.Token()
+ if err != nil {
+ t.Fatalf("Token: %v", err)
+ }
+ if tok.AccessToken != "still-valid" {
+ t.Errorf("AccessToken = %q, want unchanged %q", tok.AccessToken, "still-valid")
+ }
+ if len(store.saved) != 0 {
+ t.Errorf("SaveOAuthToken called %d times, want 0 (nothing changed, no refresh needed)", len(store.saved))
+ }
+}
+
+func TestDbTokenSource_Refresh_PreservesOriginalRefreshTokenWhenOmitted(t *testing.T) {
+ // Simulates Google's real behavior: a refresh response often omits
+ // refresh_token entirely. Without the fix, that would overwrite the
+ // stored refresh token with "" and permanently break future refreshes.
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]interface{}{
+ "access_token": "refreshed-access-token",
+ "token_type": "Bearer",
+ "expires_in": 3600,
+ // no refresh_token in the response, matching Google's real behavior
+ })
+ }))
+ defer server.Close()
+
+ store := newFakeTokenStore()
+ expired := time.Now().Add(-time.Hour)
+ store.tokens["google_tasks"] = &oauth2.Token{
+ AccessToken: "stale-access-token", RefreshToken: "original-refresh-token", TokenType: "Bearer", Expiry: expired,
+ }
+
+ cfg := &oauth2.Config{
+ ClientID: "test-client",
+ ClientSecret: "test-secret",
+ Endpoint: oauth2.Endpoint{TokenURL: server.URL},
+ }
+ src := &dbTokenSource{ctx: context.Background(), store: store, source: "google_tasks", config: cfg}
+
+ tok, err := src.Token()
+ if err != nil {
+ t.Fatalf("Token: %v", err)
+ }
+ if tok.AccessToken != "refreshed-access-token" {
+ t.Errorf("AccessToken = %q, want %q", tok.AccessToken, "refreshed-access-token")
+ }
+ if tok.RefreshToken != "original-refresh-token" {
+ t.Errorf("RefreshToken = %q, want original preserved %q", tok.RefreshToken, "original-refresh-token")
+ }
+
+ if len(store.saved) != 1 {
+ t.Fatalf("SaveOAuthToken called %d times, want 1", len(store.saved))
+ }
+ if store.saved[0].RefreshToken != "original-refresh-token" {
+ t.Errorf("persisted RefreshToken = %q, want %q", store.saved[0].RefreshToken, "original-refresh-token")
+ }
+}
+
+func TestGoogleTasksOAuthConfig_HasTasksScopeAndEndpoint(t *testing.T) {
+ cfg := GoogleTasksOAuthConfig("id", "secret", "https://doot.terst.org/settings/google-tasks/callback")
+
+ if cfg.ClientID != "id" || cfg.ClientSecret != "secret" {
+ t.Errorf("ClientID/Secret = %q/%q, want id/secret", cfg.ClientID, cfg.ClientSecret)
+ }
+ if cfg.RedirectURL != "https://doot.terst.org/settings/google-tasks/callback" {
+ t.Errorf("RedirectURL = %q", cfg.RedirectURL)
+ }
+ if len(cfg.Scopes) != 1 || cfg.Scopes[0] != "https://www.googleapis.com/auth/tasks" {
+ t.Errorf("Scopes = %v, want [tasks scope]", cfg.Scopes)
+ }
+}