Vorbereitung für automatische Beweissicherung statt manuellem
Screenshot-Upload: ein Kunde kann künftig seinen eigenen Instagram-
oder TikTok-Account per Standard-OAuth-Consent verbinden. Bewusst nur
das Grundgerüst — Meta/TikTok verlangen vor öffentlicher Nutzung eine
einmalige Business-Verification/App-Review (Wochen Vorlauf, siehe
CLAUDE.md-Abschnitt "Plattform-Verbindung (OAuth)"), die separat von
dieser Codeänderung läuft.
- internal/socialconnect: Connector-Interface + InstagramConnector/
TikTokConnector (reiner Authorization-Code-Flow, kein DB-Zugriff).
Instagram tauscht den Code zweistufig (kurzlebiges → 60-Tage-Token),
TikTok liefert Access-/Refresh-Token direkt. Endpunkte/Scopes wurden
gegen aktuelle Entwicklerdokumentation gebaut, nie gegen die echte
API verifiziert (keine Zugangsdaten vorhanden) — Hinweis dazu im
Paket- und CLAUDE.md-Kommentar.
- Migration 0006: platform_connection (NICHT append-only, anders als
finding/extraction/asset — ein Token wird ersetzt, keine Korrektur-
Zeile), höchstens eine Verbindung pro Account+Plattform.
- internal/web: GET /verbindungen (Übersicht je Plattform: verbunden/
nicht verbunden/nicht konfiguriert), GET /oauth/{platform}/start
(State-Cookie gegen CSRF, Redirect zum Consent-Screen),
GET /oauth/{platform}/callback (State prüfen, Code tauschen,
Verbindung speichern), POST /verbindungen/{platform}/trennen.
- Ohne gesetzte Client-Credentials + PUBLIC_BASE_URL bleibt die
Funktion inaktiv (kein Connector konfiguriert, /verbindungen zeigt
"nicht konfiguriert", kein Absturz) — main.go loggt das beim Start.
Volle Testsuite inkl. echter Postgres-Tests grün; OAuth-Flow gegen
Fake-Connector/httptest-Server verifiziert (State-Mismatch, Ablehnung
durch Nutzer, Token-Speicherung, Mandantentrennung). Kein Live-Test
gegen echte Meta-/TikTok-Endpunkte möglich, da noch keine echten
Client-Credentials existieren.
115 lines
3.5 KiB
Go
115 lines
3.5 KiB
Go
package store_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/netcell-it/deklarix/internal/store"
|
|
)
|
|
|
|
func TestPlatformConnectionUpsertGetDelete(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
|
|
expires := time.Now().Add(60 * 24 * time.Hour).Truncate(time.Millisecond)
|
|
c, err := s.UpsertPlatformConnection(ctx, accID, "instagram", "ig-user-1", "access-1", "", &expires)
|
|
if err != nil {
|
|
t.Fatalf("UpsertPlatformConnection: %v", err)
|
|
}
|
|
if c.Platform != "instagram" || c.PlatformUserID != "ig-user-1" {
|
|
t.Fatalf("UpsertPlatformConnection = %+v, unerwartete Werte", c)
|
|
}
|
|
|
|
got, err := s.GetPlatformConnection(ctx, accID, "instagram")
|
|
if err != nil {
|
|
t.Fatalf("GetPlatformConnection: %v", err)
|
|
}
|
|
if got.AccessToken != "access-1" {
|
|
t.Fatalf("AccessToken = %q, want access-1", got.AccessToken)
|
|
}
|
|
|
|
list, err := s.ListPlatformConnectionsForAccount(ctx, accID)
|
|
if err != nil {
|
|
t.Fatalf("ListPlatformConnectionsForAccount: %v", err)
|
|
}
|
|
if len(list) != 1 {
|
|
t.Fatalf("expected exactly 1 connection, got %d", len(list))
|
|
}
|
|
|
|
if err := s.DeletePlatformConnection(ctx, accID, "instagram"); err != nil {
|
|
t.Fatalf("DeletePlatformConnection: %v", err)
|
|
}
|
|
if _, err := s.GetPlatformConnection(ctx, accID, "instagram"); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("err nach Delete = %v, want store.ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestPlatformConnectionUpsertReplacesExisting(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
|
|
first, err := s.UpsertPlatformConnection(ctx, accID, "tiktok", "tt-user-1", "access-alt", "refresh-alt", nil)
|
|
if err != nil {
|
|
t.Fatalf("UpsertPlatformConnection (1): %v", err)
|
|
}
|
|
second, err := s.UpsertPlatformConnection(ctx, accID, "tiktok", "tt-user-1", "access-neu", "refresh-neu", nil)
|
|
if err != nil {
|
|
t.Fatalf("UpsertPlatformConnection (2): %v", err)
|
|
}
|
|
if second.ID != first.ID {
|
|
t.Fatalf("expected the same row to be updated (same account+platform), got a new ID")
|
|
}
|
|
if second.AccessToken != "access-neu" || second.RefreshToken != "refresh-neu" {
|
|
t.Fatalf("UpsertPlatformConnection (2) = %+v, tokens wurden nicht ersetzt", second)
|
|
}
|
|
|
|
list, err := s.ListPlatformConnectionsForAccount(ctx, accID)
|
|
if err != nil {
|
|
t.Fatalf("ListPlatformConnectionsForAccount: %v", err)
|
|
}
|
|
if len(list) != 1 {
|
|
t.Fatalf("expected exactly 1 connection after upsert-replace, got %d", len(list))
|
|
}
|
|
}
|
|
|
|
func TestGetPlatformConnectionNotFound(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
|
|
_, err := s.GetPlatformConnection(ctx, accID, "instagram")
|
|
if !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("err = %v, want store.ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestDeletePlatformConnectionNotFound(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accID := testAccountID(t, s)
|
|
|
|
err := s.DeletePlatformConnection(ctx, accID, "tiktok")
|
|
if !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("err = %v, want store.ErrNotFound", err)
|
|
}
|
|
}
|
|
|
|
func TestPlatformConnectionIsolatedPerAccount(t *testing.T) {
|
|
s := openTestStore(t)
|
|
ctx := context.Background()
|
|
accA := testAccountID(t, s)
|
|
accB := testAccountID(t, s)
|
|
|
|
if _, err := s.UpsertPlatformConnection(ctx, accA, "instagram", "ig-a", "token-a", "", nil); err != nil {
|
|
t.Fatalf("UpsertPlatformConnection (A): %v", err)
|
|
}
|
|
|
|
if _, err := s.GetPlatformConnection(ctx, accB, "instagram"); !errors.Is(err, store.ErrNotFound) {
|
|
t.Fatalf("Mandant B sollte keine Verbindung von Mandant A sehen, err = %v", err)
|
|
}
|
|
}
|