Framework-agnostic OAuth integration for AT Protocol (Bluesky) applications.
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});