Framework-agnostic OAuth integration for AT Protocol (Bluesky) applications.
at main 219 lines 6.5 kB view raw
1import { assertEquals, assertThrows } from "@std/assert"; 2import { createATProtoOAuth } from "./oauth.ts"; 3import { MemoryStorage } from "@tijs/atproto-storage"; 4 5Deno.test("createATProtoOAuth - throws on missing baseUrl", () => { 6 assertThrows( 7 () => { 8 createATProtoOAuth({ 9 baseUrl: "", 10 appName: "Test App", 11 cookieSecret: "a".repeat(32), 12 storage: new MemoryStorage(), 13 }); 14 }, 15 Error, 16 "baseUrl is required", 17 ); 18}); 19 20Deno.test("createATProtoOAuth - throws on missing appName", () => { 21 assertThrows( 22 () => { 23 createATProtoOAuth({ 24 baseUrl: "https://myapp.example.com", 25 appName: "", 26 cookieSecret: "a".repeat(32), 27 storage: new MemoryStorage(), 28 }); 29 }, 30 Error, 31 "appName is required", 32 ); 33}); 34 35Deno.test("createATProtoOAuth - throws on missing cookieSecret", () => { 36 assertThrows( 37 () => { 38 createATProtoOAuth({ 39 baseUrl: "https://myapp.example.com", 40 appName: "Test App", 41 cookieSecret: "", 42 storage: new MemoryStorage(), 43 }); 44 }, 45 Error, 46 "cookieSecret is required", 47 ); 48}); 49 50Deno.test("createATProtoOAuth - throws on short cookieSecret", () => { 51 assertThrows( 52 () => { 53 createATProtoOAuth({ 54 baseUrl: "https://myapp.example.com", 55 appName: "Test App", 56 cookieSecret: "short", 57 storage: new MemoryStorage(), 58 }); 59 }, 60 Error, 61 "cookieSecret must be at least 32 characters", 62 ); 63}); 64 65Deno.test("createATProtoOAuth - throws on missing storage", () => { 66 assertThrows( 67 () => { 68 createATProtoOAuth({ 69 baseUrl: "https://myapp.example.com", 70 appName: "Test App", 71 cookieSecret: "a".repeat(32), 72 storage: undefined as unknown as MemoryStorage, 73 }); 74 }, 75 Error, 76 "storage is required", 77 ); 78}); 79 80Deno.test("createATProtoOAuth - returns instance with all methods", () => { 81 const oauth = createATProtoOAuth({ 82 baseUrl: "https://myapp.example.com", 83 appName: "Test App", 84 cookieSecret: "a".repeat(32), 85 storage: new MemoryStorage(), 86 }); 87 88 // Check all methods exist 89 assertEquals(typeof oauth.handleLogin, "function"); 90 assertEquals(typeof oauth.handleCallback, "function"); 91 assertEquals(typeof oauth.handleClientMetadata, "function"); 92 assertEquals(typeof oauth.handleLogout, "function"); 93 assertEquals(typeof oauth.getSessionFromRequest, "function"); 94 assertEquals(typeof oauth.getClientMetadata, "function"); 95 assertEquals(typeof oauth.sessions.getOAuthSession, "function"); 96 assertEquals(typeof oauth.sessions.saveOAuthSession, "function"); 97 assertEquals(typeof oauth.sessions.deleteOAuthSession, "function"); 98}); 99 100Deno.test("createATProtoOAuth - handleClientMetadata returns JSON response", () => { 101 const oauth = createATProtoOAuth({ 102 baseUrl: "https://myapp.example.com", 103 appName: "Test App", 104 cookieSecret: "a".repeat(32), 105 storage: new MemoryStorage(), 106 }); 107 108 const response = oauth.handleClientMetadata(); 109 110 assertEquals(response.status, 200); 111 assertEquals(response.headers.get("Content-Type"), "application/json"); 112}); 113 114Deno.test("createATProtoOAuth - getClientMetadata returns correct metadata", () => { 115 const oauth = createATProtoOAuth({ 116 baseUrl: "https://myapp.example.com", 117 appName: "Test App", 118 cookieSecret: "a".repeat(32), 119 storage: new MemoryStorage(), 120 logoUri: "https://myapp.example.com/logo.png", 121 }); 122 123 const metadata = oauth.getClientMetadata(); 124 125 assertEquals(metadata.client_name, "Test App"); 126 assertEquals( 127 metadata.client_id, 128 "https://myapp.example.com/oauth-client-metadata.json", 129 ); 130 assertEquals(metadata.logo_uri, "https://myapp.example.com/logo.png"); 131}); 132 133Deno.test("createATProtoOAuth - handleLogin returns 400 on missing handle", async () => { 134 const oauth = createATProtoOAuth({ 135 baseUrl: "https://myapp.example.com", 136 appName: "Test App", 137 cookieSecret: "a".repeat(32), 138 storage: new MemoryStorage(), 139 }); 140 141 const request = new Request("https://myapp.example.com/login"); 142 const response = await oauth.handleLogin(request); 143 144 assertEquals(response.status, 400); 145 assertEquals(await response.text(), "Invalid handle"); 146}); 147 148Deno.test("createATProtoOAuth - handleLogin returns 400 on invalid handle format", async () => { 149 const oauth = createATProtoOAuth({ 150 baseUrl: "https://myapp.example.com", 151 appName: "Test App", 152 cookieSecret: "a".repeat(32), 153 storage: new MemoryStorage(), 154 }); 155 156 const request = new Request( 157 "https://myapp.example.com/login?handle=invalid@@@handle", 158 ); 159 const response = await oauth.handleLogin(request); 160 161 assertEquals(response.status, 400); 162 assertEquals(await response.text(), "Invalid handle format"); 163}); 164 165Deno.test("createATProtoOAuth - getSessionFromRequest returns error on no cookie", async () => { 166 const oauth = createATProtoOAuth({ 167 baseUrl: "https://myapp.example.com", 168 appName: "Test App", 169 cookieSecret: "a".repeat(32), 170 storage: new MemoryStorage(), 171 }); 172 173 const request = new Request("https://myapp.example.com/api/test"); 174 const result = await oauth.getSessionFromRequest(request); 175 176 assertEquals(result.session, null); 177 assertEquals(result.error?.type, "NO_COOKIE"); 178}); 179 180Deno.test("createATProtoOAuth - localhost uses loopback client metadata", () => { 181 const oauth = createATProtoOAuth({ 182 baseUrl: "http://localhost:8000", 183 appName: "Dev App", 184 cookieSecret: "a".repeat(32), 185 storage: new MemoryStorage(), 186 }); 187 188 const metadata = oauth.getClientMetadata(); 189 190 // client_id should be loopback format 191 assertEquals(metadata.client_id.startsWith("http://localhost?"), true); 192 // redirect_uris should use 127.0.0.1 193 assertEquals(metadata.redirect_uris, [ 194 "http://127.0.0.1:8000/oauth/callback", 195 ]); 196}); 197 198Deno.test("createATProtoOAuth - handleLogout clears session", async () => { 199 const oauth = createATProtoOAuth({ 200 baseUrl: "https://myapp.example.com", 201 appName: "Test App", 202 cookieSecret: "a".repeat(32), 203 storage: new MemoryStorage(), 204 }); 205 206 const request = new Request("https://myapp.example.com/api/auth/logout", { 207 method: "POST", 208 }); 209 const response = await oauth.handleLogout(request); 210 211 assertEquals(response.status, 200); 212 213 const body = await response.json(); 214 assertEquals(body.success, true); 215 216 // Should have Set-Cookie header to clear cookie 217 const setCookie = response.headers.get("Set-Cookie"); 218 assertEquals(setCookie?.includes("Max-Age=0"), true); 219});