summaryrefslogtreecommitdiff
path: root/internal/handlers/google_oauth_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/handlers/google_oauth_test.go')
-rw-r--r--internal/handlers/google_oauth_test.go161
1 files changed, 161 insertions, 0 deletions
diff --git a/internal/handlers/google_oauth_test.go b/internal/handlers/google_oauth_test.go
new file mode 100644
index 0000000..c01bbc4
--- /dev/null
+++ b/internal/handlers/google_oauth_test.go
@@ -0,0 +1,161 @@
+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")
+ }
+}