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 ClientSecret: "", // Public client
73 ClientKID: "", // Public client
74 SealSecret: sealSecretB64, // For sealing mobile tokens
75 Scopes: []string{"atproto", "transition:generic"},
76 DevMode: false, // Production mode for HTTPS PDS
77 AllowPrivateIPs: false, // No private IPs in production mode
78 PLCURL: "", // Use default PLC directory (plc.directory)
79 }
80 t.Logf("🌐 OAuth client configured for production PDS: %s", pdsURL)
81 } else {
82 // Dev mode: localhost PDS with HTTP
83 config = &oauth.OAuthConfig{
84 PublicURL: "http://localhost:3000", // Match the callback URL expected by PDS
85 ClientSecret: "", // Empty for public client in dev mode
86 ClientKID: "", // Empty for public client
87 SealSecret: sealSecretB64, // For sealing mobile tokens
88 Scopes: []string{"atproto", "transition:generic"},
89 DevMode: true, // Enable dev mode for localhost testing
90 AllowPrivateIPs: true, // Allow private IPs for local testing
91 PLCURL: getTestPLCURL(), // Use local PLC directory for DID resolution
92 }
93 t.Logf("🔧 OAuth client configured for local PDS: %s", pdsURL)
94 }
95
96 client, err := oauth.NewOAuthClient(config, store)
97 require.NoError(t, err, "Failed to create OAuth client")
98 require.NotNil(t, client, "OAuth client should not be nil")
99
100 return client
101}
102
103// SetupOAuthTestStore creates a test OAuth store backed by the test database.
104// The store is wrapped with MobileAwareStoreWrapper to support mobile OAuth flows.
105func SetupOAuthTestStore(t *testing.T, db *sql.DB) oauthlib.ClientAuthStore {
106 t.Helper()
107
108 baseStore := oauth.NewPostgresOAuthStore(db, 0) // Use default TTL
109 require.NotNil(t, baseStore, "OAuth base store should not be nil")
110
111 // Wrap with MobileAwareStoreWrapper to support mobile OAuth
112 // Without this, mobile OAuth silently fails (no server-side CSRF data is stored)
113 wrappedStore := oauth.NewMobileAwareStoreWrapper(baseStore)
114 require.NotNil(t, wrappedStore, "OAuth wrapped store should not be nil")
115
116 return wrappedStore
117}
118
119// CleanupOAuthTestData removes OAuth test data from the database
120func CleanupOAuthTestData(t *testing.T, db *sql.DB, did string) {
121 t.Helper()
122
123 ctx := context.Background()
124
125 // Delete sessions for this DID
126 _, err := db.ExecContext(ctx, "DELETE FROM oauth_sessions WHERE did = $1", did)
127 if err != nil {
128 t.Logf("Warning: Failed to cleanup OAuth sessions: %v", err)
129 }
130
131 // Delete auth requests (cleanup all expired ones)
132 _, err = db.ExecContext(ctx, "DELETE FROM oauth_requests WHERE created_at < NOW() - INTERVAL '1 hour'")
133 if err != nil {
134 t.Logf("Warning: Failed to cleanup OAuth auth requests: %v", err)
135 }
136}
137
138// VerifySessionData verifies that session data is properly stored and retrievable
139func VerifySessionData(t *testing.T, store oauthlib.ClientAuthStore, did syntax.DID, sessionID string) {
140 t.Helper()
141
142 ctx := context.Background()
143
144 sessData, err := store.GetSession(ctx, did, sessionID)
145 require.NoError(t, err, "Should be able to retrieve saved session")
146 require.NotNil(t, sessData, "Session data should not be nil")
147 require.Equal(t, did, sessData.AccountDID, "Session DID should match")
148 require.Equal(t, sessionID, sessData.SessionID, "Session ID should match")
149 require.NotEmpty(t, sessData.AccessToken, "Access token should be present")
150}
151
152// NOTE: Full OAuth redirect flow testing requires both HTTPS PDS and HTTPS Coves.
153// The following functions would be used for end-to-end OAuth flow testing with a real PDS:
154//
155// SimulatePDSOAuthApproval would simulate the PDS OAuth authorization flow:
156// - User logs into PDS
157// - User approves OAuth request
158// - PDS redirects back to Coves with authorization code
159//
160// WaitForOAuthCallback would wait for async OAuth callback processing:
161// - Poll database for auth request deletion
162// - Wait for session creation
163// - Timeout if callback doesn't complete
164//
165// These helpers are NOT implemented because:
166// 1. OAuth spec requires HTTPS for authorization servers (no localhost testing)
167// 2. The indigo library enforces this requirement strictly
168// 3. Component tests (using mocked sessions) provide sufficient coverage
169// 4. Full OAuth flow requires production-like HTTPS setup
170//
171// For full OAuth flow testing, use a production PDS with HTTPS and update
172// the integration tests to handle the redirect flow.
173
174// GenerateTestSealSecret generates a test seal secret for OAuth token sealing
175func GenerateTestSealSecret() string {
176 secret := make([]byte, 32)
177 _, err := rand.Read(secret)
178 if err != nil {
179 panic(fmt.Sprintf("Failed to generate seal secret: %v", err))
180 }
181 return base64.StdEncoding.EncodeToString(secret)
182}