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}