ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

at master 232 lines 7.6 kB view raw
1#!/usr/bin/env tsx 2/** 3 * Test Login Helper 4 * 5 * This script helps you obtain a real Bluesky session for integration testing. 6 * It guides you through the OAuth flow and saves the session ID for use in tests. 7 * 8 * Usage: 9 * pnpm test:login 10 * 11 * After running, set the TEST_SESSION environment variable: 12 * PowerShell: $env:TEST_SESSION="<session-id>"; pnpm test 13 * CMD: set TEST_SESSION=<session-id> && pnpm test 14 * Bash: TEST_SESSION=<session-id> pnpm test 15 */ 16 17import "dotenv/config"; 18import * as readline from "readline"; 19import { db } from "../src/db/client"; 20import app from "../src/server"; 21 22const rl = readline.createInterface({ 23 input: process.stdin, 24 output: process.stdout, 25}); 26 27function ask(question: string): Promise<string> { 28 return new Promise((resolve) => { 29 rl.question(question, (answer) => { 30 resolve(answer.trim()); 31 }); 32 }); 33} 34 35async function main() { 36 console.log("\n🔐 ATlast Test Login Helper\n"); 37 console.log("This will help you get a real Bluesky session for testing."); 38 console.log("You'll need to authorize ATlast with your Bluesky account.\n"); 39 console.log("─".repeat(60)); 40 41 // Step 1: Get user's handle 42 const handle = await ask( 43 "\n📝 Enter your Bluesky handle (e.g., yourname.bsky.social): " 44 ); 45 46 if (!handle) { 47 console.error("❌ Handle is required"); 48 process.exit(1); 49 } 50 51 console.log(`\n⏳ Starting OAuth flow for: ${handle}`); 52 53 try { 54 // Step 2: Call our OAuth start endpoint 55 const startRes = await app.request("/api/auth/oauth-start", { 56 method: "POST", 57 headers: { "Content-Type": "application/json" }, 58 body: JSON.stringify({ login_hint: handle }), 59 }); 60 61 if (!startRes.ok) { 62 const error = await startRes.json(); 63 throw new Error(error.error || "Failed to start OAuth"); 64 } 65 66 const { data } = await startRes.json(); 67 const authUrl = data.url; 68 69 console.log("\n" + "─".repeat(60)); 70 console.log("\n🌐 Step 1: Open this URL in your browser:\n"); 71 console.log(` ${authUrl}`); 72 console.log( 73 "\n📋 (Copy the URL above - it's also copied to clipboard if supported)" 74 ); 75 76 // Try to copy to clipboard (Windows) 77 try { 78 const { exec } = await import("child_process"); 79 exec(`echo ${authUrl} | clip`, (err) => { 80 if (!err) console.log(" ✅ URL copied to clipboard!"); 81 }); 82 } catch { 83 // Clipboard copy failed, that's ok 84 } 85 86 console.log("\n" + "─".repeat(60)); 87 console.log("\n🔑 Step 2: After authorizing, you'll be redirected to a URL."); 88 console.log(" Copy the FULL URL from your browser's address bar."); 89 console.log( 90 " It will look like: http://127.0.0.1:3000/api/auth/oauth-callback?code=...&state=...\n" 91 ); 92 93 const callbackUrl = await ask("📋 Paste the full callback URL here: "); 94 95 if (!callbackUrl) { 96 console.error("❌ Callback URL is required"); 97 process.exit(1); 98 } 99 100 // Step 3: Parse the callback URL and extract session 101 const url = new URL(callbackUrl); 102 let sessionId: string | null = null; 103 104 // Check if this is the final redirect (has session param) 105 if (url.searchParams.has("session")) { 106 sessionId = url.searchParams.get("session"); 107 } 108 // Check if this is the OAuth callback (has code and state) 109 else if (url.searchParams.has("code") && url.searchParams.has("state")) { 110 console.log("\n⏳ Processing OAuth callback..."); 111 112 // Call our callback endpoint to exchange the code 113 const callbackPath = url.pathname + url.search; 114 const callbackRes = await app.request(callbackPath, { 115 method: "GET", 116 headers: { 117 // Simulate the host header for proper OAuth config 118 Host: url.host, 119 }, 120 }); 121 122 // The callback returns a redirect with the session in the URL 123 if (callbackRes.status === 302 || callbackRes.status === 301) { 124 const redirectUrl = callbackRes.headers.get("Location"); 125 if (redirectUrl) { 126 const redirectParsed = new URL(redirectUrl, url.origin); 127 sessionId = redirectParsed.searchParams.get("session"); 128 129 if (!sessionId && redirectParsed.searchParams.has("error")) { 130 throw new Error( 131 `OAuth failed: ${redirectParsed.searchParams.get("error")}` 132 ); 133 } 134 } 135 } else { 136 // Try to get error from response body 137 const body = await callbackRes.text(); 138 throw new Error(`Callback failed (${callbackRes.status}): ${body}`); 139 } 140 } 141 // Maybe they pasted just the session ID 142 else if (callbackUrl.match(/^[0-9a-f-]{36}$/i)) { 143 sessionId = callbackUrl; 144 } 145 146 if (!sessionId) { 147 console.error("\n❌ Could not extract session from URL"); 148 console.error(" Expected either:"); 149 console.error( 150 " - Callback URL: http://127.0.0.1:3000/api/auth/oauth-callback?code=...&state=..." 151 ); 152 console.error(" - Final URL: http://127.0.0.1:3000/?session=<uuid>"); 153 console.error(" - Just the session ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"); 154 process.exit(1); 155 } 156 157 // Step 4: Verify the session works 158 console.log("\n⏳ Verifying session..."); 159 160 const verifyRes = await app.request(`/api/auth/session?session=${sessionId}`); 161 const verifyData = await verifyRes.json(); 162 163 if (!verifyData.success) { 164 console.error("❌ Session verification failed:", verifyData.error); 165 process.exit(1); 166 } 167 168 console.log("\n" + "─".repeat(60)); 169 console.log("\n✅ Session verified successfully!\n"); 170 console.log(` DID: ${verifyData.data.did}`); 171 console.log(` Session ID: ${sessionId}`); 172 173 // Step 5: Show how to use it 174 console.log("\n" + "─".repeat(60)); 175 console.log("\n📋 To use this session in tests:\n"); 176 177 console.log(" PowerShell:"); 178 console.log(` $env:TEST_SESSION="${sessionId}"; pnpm test\n`); 179 180 console.log(" CMD:"); 181 console.log(` set TEST_SESSION=${sessionId} && pnpm test\n`); 182 183 console.log(" Bash/Zsh:"); 184 console.log(` TEST_SESSION=${sessionId} pnpm test\n`); 185 186 console.log(" Or add to your .env file:"); 187 console.log(` TEST_SESSION=${sessionId}\n`); 188 189 console.log("─".repeat(60)); 190 console.log( 191 "\n💡 Tip: The session lasts 7 days. Run this script again to get a new one.\n" 192 ); 193 194 // Offer to save to .env.test 195 const saveToFile = await ask("Save session to .env.test file? (y/N): "); 196 197 if (saveToFile.toLowerCase() === "y") { 198 const fs = await import("fs"); 199 const path = await import("path"); 200 const envTestPath = path.join(process.cwd(), ".env.test"); 201 202 let content = ""; 203 try { 204 content = fs.readFileSync(envTestPath, "utf-8"); 205 } catch { 206 // File doesn't exist yet 207 } 208 209 // Update or add TEST_SESSION 210 if (content.includes("TEST_SESSION=")) { 211 content = content.replace(/TEST_SESSION=.*/g, `TEST_SESSION=${sessionId}`); 212 } else { 213 content += `\nTEST_SESSION=${sessionId}\n`; 214 } 215 216 fs.writeFileSync(envTestPath, content.trim() + "\n"); 217 console.log(`\n✅ Saved to ${envTestPath}`); 218 console.log( 219 ' Load it with: source .env.test (Bash) or Get-Content .env.test | ForEach-Object { $_ -replace "^", "`$env:" } | Invoke-Expression (PowerShell)\n' 220 ); 221 } 222 } catch (error) { 223 console.error("\n❌ Error:", error instanceof Error ? error.message : error); 224 process.exit(1); 225 } finally { 226 rl.close(); 227 // Close database connection 228 await db.destroy(); 229 } 230} 231 232main().catch(console.error);