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}