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.

feat(api): add test:login for real session testing

- saves session to .env.test

byarielm.fyi 5c14866a e58c4c0e

verified
+232
+232
packages/api/scripts/test-login.ts
··· 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 + 17 + import "dotenv/config"; 18 + import * as readline from "readline"; 19 + import { db } from "../src/db/client"; 20 + import app from "../src/server"; 21 + 22 + const rl = readline.createInterface({ 23 + input: process.stdin, 24 + output: process.stdout, 25 + }); 26 + 27 + function ask(question: string): Promise<string> { 28 + return new Promise((resolve) => { 29 + rl.question(question, (answer) => { 30 + resolve(answer.trim()); 31 + }); 32 + }); 33 + } 34 + 35 + async 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 + 232 + main().catch(console.error);