package web_test import ( "context" "net/http" "net/http/httptest" "net/url" "strings" "testing" "time" "github.com/netcell-it/deklarix/internal/socialconnect" ) // fakeConnector ist ein socialconnect.Connector-Fake für Tests — kein // echter HTTP-Aufruf gegen Instagram/TikTok nötig. type fakeConnector struct { platform string token socialconnect.Token err error } func (f fakeConnector) Platform() string { return f.platform } func (f fakeConnector) AuthorizationURL(state string) string { return "https://provider.example/authorize?state=" + state + "&platform=" + f.platform } func (f fakeConnector) Exchange(ctx context.Context, code string) (socialconnect.Token, error) { if f.err != nil { return socialconnect.Token{}, f.err } return f.token, nil } func TestConnectionsListShowsUnconfiguredPlatformsWithoutConnectButton(t *testing.T) { s, _, cookie := newAuthedTestServer(t, fakeExtractor{}) resp := getWithCookie(t, s, cookie, "/verbindungen") if resp.Code != http.StatusOK { t.Fatalf("status = %d, body: %s", resp.Code, resp.Body.String()) } body := resp.Body.String() if strings.Contains(body, "/oauth/instagram/start") || strings.Contains(body, "/oauth/tiktok/start") { t.Errorf("expected no connect links when no connector is configured, got: %s", body) } if !strings.Contains(body, "instagram") || !strings.Contains(body, "tiktok") { t.Errorf("expected both platforms to be listed regardless of configuration, got: %s", body) } } func TestConnectionsListShowsConnectButtonWhenConfigured(t *testing.T) { fs := newFakeStore() connectors := map[string]socialconnect.Connector{"instagram": fakeConnector{platform: "instagram"}} s := newServerWithConnectors(t, fakeExtractor{}, fs, connectors) cookie := seedAccount(t, fs, "Mandant", "mandant@example.com") resp := getWithCookie(t, s, cookie, "/verbindungen") body := resp.Body.String() if !strings.Contains(body, "/oauth/instagram/start") { t.Errorf("expected an Instagram connect link, got: %s", body) } if strings.Contains(body, "/oauth/tiktok/start") { t.Errorf("expected no TikTok connect link (not configured), got: %s", body) } } func TestOAuthStartRedirectsToProviderAndSetsStateCookie(t *testing.T) { fs := newFakeStore() connectors := map[string]socialconnect.Connector{"instagram": fakeConnector{platform: "instagram"}} s := newServerWithConnectors(t, fakeExtractor{}, fs, connectors) cookie := seedAccount(t, fs, "Mandant", "mandant@example.com") req := httptest.NewRequest(http.MethodGet, "/oauth/instagram/start", nil) req.AddCookie(cookie) w := httptest.NewRecorder() s.ServeHTTP(w, req) if w.Code != http.StatusSeeOther { t.Fatalf("status = %d, want 303", w.Code) } loc := w.Header().Get("Location") if !strings.HasPrefix(loc, "https://provider.example/authorize?") { t.Fatalf("Location = %q, want a redirect to the provider", loc) } var stateCookie *http.Cookie for _, c := range w.Result().Cookies() { if c.Name == "deklarix_oauth_state" { stateCookie = c } } if stateCookie == nil || stateCookie.Value == "" { t.Fatal("expected a non-empty deklarix_oauth_state cookie to be set") } } func TestOAuthStartRejectsUnconfiguredPlatform(t *testing.T) { s, _, cookie := newAuthedTestServer(t, fakeExtractor{}) resp := getWithCookie(t, s, cookie, "/oauth/instagram/start") if resp.Code != http.StatusNotFound { t.Fatalf("status = %d, want 404 for an unconfigured platform", resp.Code) } } func TestOAuthCallbackStoresConnectionOnValidState(t *testing.T) { fs := newFakeStore() expires := time.Now().Add(60 * 24 * time.Hour) connectors := map[string]socialconnect.Connector{ "instagram": fakeConnector{platform: "instagram", token: socialconnect.Token{ AccessToken: "ig-token", PlatformUserID: "ig-user-1", ExpiresAt: expires, }}, } s := newServerWithConnectors(t, fakeExtractor{}, fs, connectors) cookie := seedAccount(t, fs, "Mandant", "mandant@example.com") req := httptest.NewRequest(http.MethodGet, "/oauth/instagram/callback?code=der-code&state=gueltiger-state", nil) req.AddCookie(cookie) req.AddCookie(&http.Cookie{Name: "deklarix_oauth_state", Value: "gueltiger-state"}) w := httptest.NewRecorder() s.ServeHTTP(w, req) if w.Code != http.StatusSeeOther { t.Fatalf("status = %d, want 303, body: %s", w.Code, w.Body.String()) } if loc := w.Header().Get("Location"); loc != "/verbindungen" { t.Fatalf("Location = %q, want /verbindungen", loc) } var accID string for id := range fs.accounts { accID = id } conn, err := fs.GetPlatformConnection(context.Background(), accID, "instagram") if err != nil { t.Fatalf("expected a stored connection, got err: %v", err) } if conn.AccessToken != "ig-token" || conn.PlatformUserID != "ig-user-1" { t.Errorf("unexpected connection: %+v", conn) } } func TestOAuthCallbackRejectsMismatchedState(t *testing.T) { fs := newFakeStore() connectors := map[string]socialconnect.Connector{ "instagram": fakeConnector{platform: "instagram", token: socialconnect.Token{AccessToken: "x", PlatformUserID: "y"}}, } s := newServerWithConnectors(t, fakeExtractor{}, fs, connectors) cookie := seedAccount(t, fs, "Mandant", "mandant@example.com") req := httptest.NewRequest(http.MethodGet, "/oauth/instagram/callback?code=der-code&state=falscher-state", nil) req.AddCookie(cookie) req.AddCookie(&http.Cookie{Name: "deklarix_oauth_state", Value: "anderer-state"}) w := httptest.NewRecorder() s.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Fatalf("status = %d, want 400 for a state mismatch", w.Code) } } func TestOAuthCallbackHandlesUserDenial(t *testing.T) { fs := newFakeStore() connectors := map[string]socialconnect.Connector{ "instagram": fakeConnector{platform: "instagram"}, } s := newServerWithConnectors(t, fakeExtractor{}, fs, connectors) cookie := seedAccount(t, fs, "Mandant", "mandant@example.com") req := httptest.NewRequest(http.MethodGet, "/oauth/instagram/callback?error=access_denied", nil) req.AddCookie(cookie) w := httptest.NewRecorder() s.ServeHTTP(w, req) if w.Code != http.StatusSeeOther { t.Fatalf("status = %d, want 303 (redirect back, no error page)", w.Code) } } func TestDisconnectRemovesConnection(t *testing.T) { fs := newFakeStore() connectors := map[string]socialconnect.Connector{"instagram": fakeConnector{platform: "instagram"}} s := newServerWithConnectors(t, fakeExtractor{}, fs, connectors) cookie := seedAccount(t, fs, "Mandant", "mandant@example.com") var accID string for id := range fs.accounts { accID = id } if _, err := fs.UpsertPlatformConnection(context.Background(), accID, "instagram", "u1", "tok", "", nil); err != nil { t.Fatalf("UpsertPlatformConnection: %v", err) } resp := postForm(t, s, cookie, "/verbindungen/instagram/trennen", url.Values{}) if resp.Code != http.StatusSeeOther { t.Fatalf("status = %d, want 303, body: %s", resp.Code, resp.Body.String()) } if _, err := fs.GetPlatformConnection(context.Background(), accID, "instagram"); err == nil { t.Fatal("expected the connection to be gone after disconnect") } } func TestConnectionsIsolatedPerTenant(t *testing.T) { fs := newFakeStore() connectors := map[string]socialconnect.Connector{"instagram": fakeConnector{platform: "instagram"}} s := newServerWithConnectors(t, fakeExtractor{}, fs, connectors) cookieA := seedAccount(t, fs, "Mandant A", "a@example.com") cookieB := seedAccount(t, fs, "Mandant B", "b@example.com") var accA string for id, acc := range fs.accounts { if acc.Name == "Mandant A" { accA = id } } if _, err := fs.UpsertPlatformConnection(context.Background(), accA, "instagram", "u1", "tok", "", nil); err != nil { t.Fatalf("UpsertPlatformConnection: %v", err) } respA := getWithCookie(t, s, cookieA, "/verbindungen") if !strings.Contains(respA.Body.String(), "verbunden seit") { t.Errorf("expected Mandant A to see their own connection, got: %s", respA.Body.String()) } respB := getWithCookie(t, s, cookieB, "/verbindungen") if strings.Contains(respB.Body.String(), "verbunden seit") { t.Errorf("expected Mandant B to see no connection, got: %s", respB.Body.String()) } }