package handlers import ( "encoding/json" "net/http" "net/http/httptest" "testing" "golang.org/x/oauth2" "task-dashboard/internal/api" ) func withSession(t *testing.T, h *Handler, req *http.Request) *http.Request { t.Helper() ctx, err := h.sessions.Load(req.Context(), "") if err != nil { t.Fatal(err) } return req.WithContext(ctx) } func TestHandleGoogleTasksOAuthStart_NotConfigured_Returns503(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() req := withSession(t, h, httptest.NewRequest("GET", "/settings/google-tasks/connect", nil)) w := httptest.NewRecorder() h.HandleGoogleTasksOAuthStart(w, req) if w.Code != http.StatusServiceUnavailable { t.Errorf("status = %d, want 503", w.Code) } } func TestHandleGoogleTasksOAuthStart_RedirectsToGoogleAndStoresState(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() h.googleTasksOAuthConfig = api.GoogleTasksOAuthConfig("client-id", "client-secret", "https://doot.terst.org/settings/google-tasks/callback") req := withSession(t, h, httptest.NewRequest("GET", "/settings/google-tasks/connect", nil)) w := httptest.NewRecorder() h.HandleGoogleTasksOAuthStart(w, req) if w.Code != http.StatusFound { t.Fatalf("status = %d, want 302", w.Code) } loc := w.Header().Get("Location") if loc == "" { t.Fatal("no Location header set") } state := h.sessions.GetString(req.Context(), sessionKeyGoogleOAuthState) if state == "" { t.Error("expected a state value to be stored in session") } } func TestHandleGoogleTasksOAuthCallback_StateMismatch_Returns400(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() h.googleTasksOAuthConfig = api.GoogleTasksOAuthConfig("client-id", "client-secret", "https://doot.terst.org/settings/google-tasks/callback") req := httptest.NewRequest("GET", "/settings/google-tasks/callback?state=wrong&code=abc", nil) req = withSession(t, h, req) h.sessions.Put(req.Context(), sessionKeyGoogleOAuthState, "expected-state") w := httptest.NewRecorder() h.HandleGoogleTasksOAuthCallback(w, req) if w.Code != http.StatusBadRequest { t.Errorf("status = %d, want 400", w.Code) } if tok, _ := h.store.GetOAuthToken(api.GoogleTasksOAuthSource); tok != nil { t.Error("no token should have been saved on state mismatch") } } func TestHandleGoogleTasksOAuthCallback_GoogleReturnedError_RedirectsWithError(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() h.googleTasksOAuthConfig = api.GoogleTasksOAuthConfig("client-id", "client-secret", "https://doot.terst.org/settings/google-tasks/callback") req := withSession(t, h, httptest.NewRequest("GET", "/settings/google-tasks/callback?error=access_denied", nil)) w := httptest.NewRecorder() h.HandleGoogleTasksOAuthCallback(w, req) if w.Code != http.StatusFound { t.Fatalf("status = %d, want 302", w.Code) } loc := w.Header().Get("Location") if loc != "/settings?google_tasks_error=access_denied" { t.Errorf("Location = %q", loc) } } func TestHandleGoogleTasksOAuthDisconnect_RemovesStoredToken(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() if err := h.store.SaveOAuthToken(api.GoogleTasksOAuthSource, &oauth2.Token{AccessToken: "a", RefreshToken: "r"}); err != nil { t.Fatal(err) } req := withSession(t, h, httptest.NewRequest("POST", "/settings/google-tasks/disconnect", nil)) w := httptest.NewRecorder() h.HandleGoogleTasksOAuthDisconnect(w, req) if w.Code != http.StatusFound { t.Fatalf("status = %d, want 302", w.Code) } tok, err := h.store.GetOAuthToken(api.GoogleTasksOAuthSource) if err != nil { t.Fatal(err) } if tok != nil { t.Error("token should be gone after disconnect") } } func TestHandleSettingsPage_ReflectsGoogleTasksConnectionState(t *testing.T) { h, cleanup := setupTestHandler(t) defer cleanup() h.googleTasksOAuthConfig = api.GoogleTasksOAuthConfig("client-id", "client-secret", "https://doot.terst.org/settings/google-tasks/callback") req := withSession(t, h, httptest.NewRequest("GET", "/settings", nil)) w := httptest.NewRecorder() h.HandleSettingsPage(w, req) if w.Code != http.StatusOK { t.Fatalf("status = %d, want 200, body=%s", w.Code, w.Body.String()) } mock := h.renderer.(*MockRenderer) data := mock.Calls[len(mock.Calls)-1].Data type settingsData struct { GoogleTasksOAuthReady bool GoogleTasksConnected bool } jsonBytes, _ := json.Marshal(data) var got settingsData _ = json.Unmarshal(jsonBytes, &got) if !got.GoogleTasksOAuthReady { t.Error("GoogleTasksOAuthReady = false, want true (config was set)") } if got.GoogleTasksConnected { t.Error("GoogleTasksConnected = true, want false (no token saved)") } if err := h.store.SaveOAuthToken(api.GoogleTasksOAuthSource, &oauth2.Token{AccessToken: "a", RefreshToken: "r"}); err != nil { t.Fatal(err) } w2 := httptest.NewRecorder() h.HandleSettingsPage(w2, req) data2 := mock.Calls[len(mock.Calls)-1].Data jsonBytes2, _ := json.Marshal(data2) var got2 settingsData _ = json.Unmarshal(jsonBytes2, &got2) if !got2.GoogleTasksConnected { t.Error("GoogleTasksConnected = false, want true after saving a token") } }