A community based topic aggregation platform built on atproto
1package integration
2
3import (
4 "Coves/internal/atproto/oauth"
5 "context"
6 "crypto/rand"
7 "database/sql"
8 "encoding/base64"
9 "fmt"
10 "os"
11 "strings"
12 "testing"
13
14 oauthlib "github.com/bluesky-social/indigo/atproto/auth/oauth"
15 "github.com/bluesky-social/indigo/atproto/syntax"
16 "github.com/stretchr/testify/require"
17)
18
19// CreateTestUserOnPDS creates a user on the local PDS for OAuth testing
20// Returns the DID, access token, and refresh token
21func CreateTestUserOnPDS(t *testing.T, handle, email, password string) (did, accessToken, refreshToken string) {
22 t.Helper()
23
24 pdsURL := getTestPDSURL()
25
26 // Use the existing createPDSAccount helper which returns accessToken and DID
27 accessToken, did, err := createPDSAccount(pdsURL, handle, email, password)
28 require.NoError(t, err, "Failed to create PDS account for OAuth test")
29 require.NotEmpty(t, accessToken, "Access token should not be empty")
30 require.NotEmpty(t, did, "DID should not be empty")
31
32 // Note: The PDS createAccount endpoint may not return refresh token directly
33 // For OAuth flow testing, we'll get the refresh token from the OAuth callback
34 // For now, return empty refresh token
35 refreshToken = ""
36
37 return did, accessToken, refreshToken
38}
39
40// getTestPLCURL returns the PLC directory URL for testing from env var or default
41func getTestPLCURL() string {
42 plcURL := os.Getenv("PLC_DIRECTORY_URL")
43 if plcURL == "" {
44 plcURL = "http://localhost:3002" // Local PLC directory for testing
45 }
46 return plcURL
47}
48
49// SetupOAuthTestClient creates an OAuth client configured for testing with a PDS
50// When PDS_URL starts with https://, production mode is used (DevMode=false)
51// Otherwise, dev mode is used for localhost testing
52func SetupOAuthTestClient(t *testing.T, store oauthlib.ClientAuthStore) *oauth.OAuthClient {
53 t.Helper()
54
55 // Generate a seal secret for testing (32 bytes)
56 sealSecret := make([]byte, 32)
57 _, err := rand.Read(sealSecret)
58 require.NoError(t, err, "Failed to generate seal secret")
59
60 sealSecretB64 := base64.StdEncoding.EncodeToString(sealSecret)
61
62 // Detect if we're testing against a production (HTTPS) PDS
63 pdsURL := getTestPDSURL()
64 isProductionPDS := strings.HasPrefix(pdsURL, "https://")
65
66 // Configure based on PDS type
67 var config *oauth.OAuthConfig
68 if isProductionPDS {
69 // Production mode: HTTPS PDS, use real PLC directory
70 config = &oauth.OAuthConfig{
71 PublicURL: "http://localhost:3000", // Test server callback URL
72 SealSecret: sealSecretB64, // For sealing mobile tokens
73 Scopes: []string{"atproto", "transition:generic"},
74 DevMode: false, // Production mode for HTTPS PDS
75 AllowPrivateIPs: false, // No private IPs in production mode
76 PLCURL: "", // Use default PLC directory (plc.directory)
77 }
78 t.Logf("🌐 OAuth client configured for production PDS: %s", pdsURL)
79 } else {
80 // Dev mode: localhost PDS with HTTP
81 config = &oauth.OAuthConfig{
82 PublicURL: "http://localhost:3000", // Match the callback URL expected by PDS
83 SealSecret: sealSecretB64, // For sealing mobile tokens
84 Scopes: []string{"atproto", "transition:generic"},
85 DevMode: true, // Enable dev mode for localhost testing
86 AllowPrivateIPs: true, // Allow private IPs for local testing
87 PLCURL: getTestPLCURL(), // Use local PLC directory for DID resolution
88 }
89 t.Logf("🔧 OAuth client configured for local PDS: %s", pdsURL)
90 }
91
92 client, err := oauth.NewOAuthClient(config, store)
93 require.NoError(t, err, "Failed to create OAuth client")
94 require.NotNil(t, client, "OAuth client should not be nil")
95
96 return client
97}
98
99// SetupOAuthTestStore creates a test OAuth store backed by the test database.
100// The store is wrapped with MobileAwareStoreWrapper to support mobile OAuth flows.
101func SetupOAuthTestStore(t *testing.T, db *sql.DB) oauthlib.ClientAuthStore {
102 t.Helper()
103
104 baseStore := oauth.NewPostgresOAuthStore(db, 0) // Use default TTL
105 require.NotNil(t, baseStore, "OAuth base store should not be nil")
106
107 // Wrap with MobileAwareStoreWrapper to support mobile OAuth
108 // Without this, mobile OAuth silently fails (no server-side CSRF data is stored)
109 wrappedStore := oauth.NewMobileAwareStoreWrapper(baseStore)
110 require.NotNil(t, wrappedStore, "OAuth wrapped store should not be nil")
111
112 return wrappedStore
113}
114
115// CleanupOAuthTestData removes OAuth test data from the database
116func CleanupOAuthTestData(t *testing.T, db *sql.DB, did string) {
117 t.Helper()
118
119 ctx := context.Background()
120
121 // Delete sessions for this DID
122 _, err := db.ExecContext(ctx, "DELETE FROM oauth_sessions WHERE did = $1", did)
123 if err != nil {
124 t.Logf("Warning: Failed to cleanup OAuth sessions: %v", err)
125 }
126
127 // Delete auth requests (cleanup all expired ones)
128 _, err = db.ExecContext(ctx, "DELETE FROM oauth_requests WHERE created_at < NOW() - INTERVAL '1 hour'")
129 if err != nil {
130 t.Logf("Warning: Failed to cleanup OAuth auth requests: %v", err)
131 }
132}
133
134// VerifySessionData verifies that session data is properly stored and retrievable
135func VerifySessionData(t *testing.T, store oauthlib.ClientAuthStore, did syntax.DID, sessionID string) {
136 t.Helper()
137
138 ctx := context.Background()
139
140 sessData, err := store.GetSession(ctx, did, sessionID)
141 require.NoError(t, err, "Should be able to retrieve saved session")
142 require.NotNil(t, sessData, "Session data should not be nil")
143 require.Equal(t, did, sessData.AccountDID, "Session DID should match")
144 require.Equal(t, sessionID, sessData.SessionID, "Session ID should match")
145 require.NotEmpty(t, sessData.AccessToken, "Access token should be present")
146}
147
148// NOTE: Full OAuth redirect flow testing requires both HTTPS PDS and HTTPS Coves.
149// The following functions would be used for end-to-end OAuth flow testing with a real PDS:
150//
151// SimulatePDSOAuthApproval would simulate the PDS OAuth authorization flow:
152// - User logs into PDS
153// - User approves OAuth request
154// - PDS redirects back to Coves with authorization code
155//
156// WaitForOAuthCallback would wait for async OAuth callback processing:
157// - Poll database for auth request deletion
158// - Wait for session creation
159// - Timeout if callback doesn't complete
160//
161// These helpers are NOT implemented because:
162// 1. OAuth spec requires HTTPS for authorization servers (no localhost testing)
163// 2. The indigo library enforces this requirement strictly
164// 3. Component tests (using mocked sessions) provide sufficient coverage
165// 4. Full OAuth flow requires production-like HTTPS setup
166//
167// For full OAuth flow testing, use a production PDS with HTTPS and update
168// the integration tests to handle the redirect flow.
169
170// GenerateTestSealSecret generates a test seal secret for OAuth token sealing
171func GenerateTestSealSecret() string {
172 secret := make([]byte, 32)
173 _, err := rand.Read(secret)
174 if err != nil {
175 panic(fmt.Sprintf("Failed to generate seal secret: %v", err))
176 }
177 return base64.StdEncoding.EncodeToString(secret)
178}