feat: technisches Grundgerüst für Instagram-/TikTok-OAuth (Plattform-Verbindung)
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.
This commit is contained in:
70
internal/socialconnect/tiktok_test.go
Normal file
70
internal/socialconnect/tiktok_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package socialconnect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTikTokAuthorizationURL(t *testing.T) {
|
||||
c := NewTikTokConnector("client-key-123", "secret", "https://app.deklarix.de/oauth/tiktok/callback")
|
||||
u := c.AuthorizationURL("state-xyz")
|
||||
|
||||
for _, want := range []string{
|
||||
"https://www.tiktok.com/v2/auth/authorize/?",
|
||||
"client_key=client-key-123",
|
||||
"state=state-xyz",
|
||||
"response_type=code",
|
||||
} {
|
||||
if !strings.Contains(u, want) {
|
||||
t.Errorf("AuthorizationURL = %q, want it to contain %q", u, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTikTokExchangeSuccess(t *testing.T) {
|
||||
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
t.Fatalf("ParseForm: %v", err)
|
||||
}
|
||||
if r.FormValue("client_key") != "client-key-123" {
|
||||
t.Errorf("client_key = %q, want client-key-123", r.FormValue("client_key"))
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"access_token":"tt-access","refresh_token":"tt-refresh","expires_in":86400,"open_id":"tt-open-id-1"}`))
|
||||
}))
|
||||
defer tokenServer.Close()
|
||||
|
||||
c := NewTikTokConnector("client-key-123", "secret", "https://app.deklarix.de/oauth/tiktok/callback")
|
||||
c.TokenURL = tokenServer.URL
|
||||
|
||||
tok, err := c.Exchange(context.Background(), "der-code")
|
||||
if err != nil {
|
||||
t.Fatalf("Exchange: %v", err)
|
||||
}
|
||||
if tok.AccessToken != "tt-access" || tok.RefreshToken != "tt-refresh" || tok.PlatformUserID != "tt-open-id-1" {
|
||||
t.Errorf("unexpected token: %+v", tok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTikTokExchangePropagatesPlatformError(t *testing.T) {
|
||||
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// TikTok liefert Fehler oft mit Status 200, Fehlerfeldern im Body.
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"error":"invalid_grant","error_description":"code abgelaufen"}`))
|
||||
}))
|
||||
defer tokenServer.Close()
|
||||
|
||||
c := NewTikTokConnector("client-key-123", "secret", "https://app.deklarix.de/oauth/tiktok/callback")
|
||||
c.TokenURL = tokenServer.URL
|
||||
|
||||
_, err := c.Exchange(context.Background(), "abgelaufener-code")
|
||||
if err == nil {
|
||||
t.Fatal("expected an error when the platform response contains an error field")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid_grant") {
|
||||
t.Errorf("error = %v, want it to mention invalid_grant", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user