@lightupai/polaris 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +38 -0
- package/.mcp.json +12 -0
- package/LICENSE +201 -0
- package/Makefile +38 -0
- package/PLAN.md +438 -0
- package/docker-compose.yml +14 -0
- package/docs/README.md +377 -0
- package/hooks/capture.sh +19 -0
- package/hooks/statusline.sh +30 -0
- package/package.json +22 -0
- package/scripts/setup-google-oauth.sh +111 -0
- package/scripts/setup-slack-app.sh +115 -0
- package/skills/polaris/SKILL.md +29 -0
- package/src/cli/cli.ts +294 -0
- package/src/client/client.ts +245 -0
- package/src/daemon/daemon.ts +275 -0
- package/src/service/auth.ts +45 -0
- package/src/service/db.ts +275 -0
- package/src/service/server.ts +406 -0
- package/src/slack/system.ts +107 -0
- package/src/types.ts +108 -0
- package/src/web/app.ts +397 -0
- package/src/web/fixtures.ts +121 -0
- package/src/web/layout.ts +68 -0
- package/src/web/pages.ts +156 -0
- package/src/web/serve.ts +13 -0
- package/src/web/views.ts +356 -0
- package/tests/auth.test.ts +37 -0
- package/tests/client.test.ts +187 -0
- package/tests/daemon.test.ts +220 -0
- package/tests/db.test.ts +282 -0
- package/tests/e2e.test.ts +415 -0
- package/tests/service.test.ts +365 -0
- package/tests/types.test.ts +240 -0
- package/tests/web.test.ts +420 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { describe, expect, test, beforeAll, afterAll, beforeEach } from "bun:test";
|
|
2
|
+
import { renderSetupView, renderActiveView, renderProfileView, renderErrorView } from "../src/web/views";
|
|
3
|
+
import { nav } from "../src/web/layout";
|
|
4
|
+
import {
|
|
5
|
+
mockUser,
|
|
6
|
+
mockOrg,
|
|
7
|
+
mockOrgNoSlack,
|
|
8
|
+
mockProjects,
|
|
9
|
+
mockActiveSessions,
|
|
10
|
+
mockDevices,
|
|
11
|
+
} from "../src/web/fixtures";
|
|
12
|
+
import { createApp } from "../src/web/app";
|
|
13
|
+
import { createDb, createOrg, createUser, type Sql } from "../src/service/db";
|
|
14
|
+
import { createToken } from "../src/service/auth";
|
|
15
|
+
|
|
16
|
+
const DATABASE_URL = process.env.DATABASE_URL ?? "postgres://polaris:polaris@localhost:5432/polaris";
|
|
17
|
+
|
|
18
|
+
// --- View context helpers ---
|
|
19
|
+
|
|
20
|
+
const base = { token: "test-token", userName: "Manu Bansal", orgName: "Lightup", email: "manu@lightup.ai" };
|
|
21
|
+
const fresh = { ...base, slackConnected: false, cliInstalled: false, hasConnectedSession: false };
|
|
22
|
+
const slackDone = { ...base, slackConnected: true, cliInstalled: false, hasConnectedSession: false };
|
|
23
|
+
const cliDone = { ...base, slackConnected: true, cliInstalled: true, hasConnectedSession: false };
|
|
24
|
+
const allDone = { ...base, slackConnected: true, cliInstalled: true, hasConnectedSession: true };
|
|
25
|
+
|
|
26
|
+
// --- Setup view ---
|
|
27
|
+
|
|
28
|
+
describe("renderSetupView", () => {
|
|
29
|
+
test("fresh state: floor is highlighted, devices and sessions are grayed out", () => {
|
|
30
|
+
const html = renderSetupView(fresh);
|
|
31
|
+
// Floor section has active highlight
|
|
32
|
+
expect(html).toContain("border-polaris-300");
|
|
33
|
+
// Devices and sessions are wrapped with opacity
|
|
34
|
+
expect(html).toContain("opacity-40");
|
|
35
|
+
// Connect Slack button is present
|
|
36
|
+
expect(html).toContain("Connect Slack");
|
|
37
|
+
// Install CLI command is present
|
|
38
|
+
expect(html).toContain("npx @lightup/polaris login");
|
|
39
|
+
// Connect session command is present
|
|
40
|
+
expect(html).toContain("/polaris join my-project my-session");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("slack done: floor shows connected, devices is highlighted, sessions grayed", () => {
|
|
44
|
+
const html = renderSetupView(slackDone);
|
|
45
|
+
// Floor shows connected badge
|
|
46
|
+
expect(html).toContain("Connected");
|
|
47
|
+
// No Connect Slack button
|
|
48
|
+
expect(html).not.toContain("Connect Slack");
|
|
49
|
+
// Devices section has highlight (CLI not installed yet)
|
|
50
|
+
const floorIdx = html.indexOf("Floor");
|
|
51
|
+
const devicesIdx = html.indexOf("Devices");
|
|
52
|
+
const highlightIdx = html.indexOf("border-polaris-300");
|
|
53
|
+
expect(highlightIdx).toBeGreaterThan(devicesIdx);
|
|
54
|
+
// Install CLI command present
|
|
55
|
+
expect(html).toContain("npx @lightup/polaris login");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("cli done: floor and devices done, sessions is highlighted", () => {
|
|
59
|
+
const html = renderSetupView(cliDone, mockDevices);
|
|
60
|
+
// Device list is shown (not install prompt)
|
|
61
|
+
expect(html).toContain("Manu's MacBook Pro");
|
|
62
|
+
// No install CLI command
|
|
63
|
+
expect(html).not.toContain("npx @lightup/polaris login");
|
|
64
|
+
// Session section has highlight
|
|
65
|
+
const sessIdx = html.indexOf("Projects & Sessions");
|
|
66
|
+
const lastHighlight = html.lastIndexOf("border-polaris-300");
|
|
67
|
+
expect(lastHighlight).toBeGreaterThan(sessIdx);
|
|
68
|
+
// Connect session command present
|
|
69
|
+
expect(html).toContain("/polaris join my-project my-session");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("includes nav with user info", () => {
|
|
73
|
+
const html = renderSetupView(fresh);
|
|
74
|
+
expect(html).toContain("Manu Bansal");
|
|
75
|
+
expect(html).toContain("Lightup");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// --- Active view ---
|
|
80
|
+
|
|
81
|
+
describe("renderActiveView", () => {
|
|
82
|
+
test("shows compact floor bar", () => {
|
|
83
|
+
const html = renderActiveView(allDone, mockActiveSessions, mockProjects, mockDevices);
|
|
84
|
+
expect(html).toContain("Live");
|
|
85
|
+
// No Connect Slack button
|
|
86
|
+
expect(html).not.toContain("Connect Slack");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("shows session cards with roles", () => {
|
|
90
|
+
const html = renderActiveView(allDone, mockActiveSessions, mockProjects, mockDevices);
|
|
91
|
+
expect(html).toContain("polaris/auth");
|
|
92
|
+
expect(html).toContain("polaris/slack-bridge");
|
|
93
|
+
// Role badges are rendered (Advisor shown when participant ID derivation
|
|
94
|
+
// from display name doesn't match fixture — expected for mock data)
|
|
95
|
+
expect(html).toContain("Advisor");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("shows session descriptions and event counts", () => {
|
|
99
|
+
const html = renderActiveView(allDone, mockActiveSessions, mockProjects, mockDevices);
|
|
100
|
+
expect(html).toContain("Google SSO + JWT auth");
|
|
101
|
+
expect(html).toContain("42 events");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("shows other participants in sessions", () => {
|
|
105
|
+
const html = renderActiveView(allDone, mockActiveSessions, mockProjects, mockDevices);
|
|
106
|
+
expect(html).toContain("agent:security-reviewer");
|
|
107
|
+
expect(html).toContain("user:krishna");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("shows project cards with slack channels", () => {
|
|
111
|
+
const html = renderActiveView(allDone, mockActiveSessions, mockProjects, mockDevices);
|
|
112
|
+
expect(html).toContain("#polaris");
|
|
113
|
+
expect(html).toContain("#data-pipeline");
|
|
114
|
+
expect(html).toContain("2 sessions");
|
|
115
|
+
expect(html).toContain("1 session");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("shows device list with online status", () => {
|
|
119
|
+
const html = renderActiveView(allDone, mockActiveSessions, mockProjects, mockDevices);
|
|
120
|
+
expect(html).toContain("Manu's MacBook Pro");
|
|
121
|
+
expect(html).toContain("Manu's iMac");
|
|
122
|
+
expect(html).toContain("Active now");
|
|
123
|
+
expect(html).toContain("polaris/auth");
|
|
124
|
+
expect(html).toContain("Last seen");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("hides devices section when no devices", () => {
|
|
128
|
+
const html = renderActiveView(allDone, mockActiveSessions, mockProjects, []);
|
|
129
|
+
expect(html).not.toContain("Manu's MacBook Pro");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// --- Profile view ---
|
|
134
|
+
|
|
135
|
+
describe("renderProfileView", () => {
|
|
136
|
+
test("shows user identity", () => {
|
|
137
|
+
const html = renderProfileView(allDone, "user:manu");
|
|
138
|
+
expect(html).toContain("Manu Bansal");
|
|
139
|
+
expect(html).toContain("manu@lightup.ai");
|
|
140
|
+
expect(html).toContain("user:manu");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("shows organization", () => {
|
|
144
|
+
const html = renderProfileView(allDone, "user:manu");
|
|
145
|
+
expect(html).toContain("Lightup");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("shows API token", () => {
|
|
149
|
+
const html = renderProfileView(allDone, "user:manu");
|
|
150
|
+
expect(html).toContain("API token");
|
|
151
|
+
expect(html).toContain("test-token");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("shows avatar initial", () => {
|
|
155
|
+
const html = renderProfileView(allDone, "user:manu");
|
|
156
|
+
expect(html).toContain(">M</");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// --- Navigation ---
|
|
161
|
+
|
|
162
|
+
describe("nav", () => {
|
|
163
|
+
test("logged out: shows sign in link", () => {
|
|
164
|
+
const html = nav();
|
|
165
|
+
expect(html).toContain("Sign in");
|
|
166
|
+
expect(html).not.toContain("Dashboard");
|
|
167
|
+
expect(html).not.toContain("Log out");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("logged in: shows user dropdown with name and org", () => {
|
|
171
|
+
const html = nav("tok123", { userName: "Manu Bansal", orgName: "Lightup", email: "manu@lightup.ai" });
|
|
172
|
+
expect(html).toContain("Manu Bansal");
|
|
173
|
+
expect(html).toContain("Lightup");
|
|
174
|
+
expect(html).toContain("manu@lightup.ai");
|
|
175
|
+
expect(html).toContain("Dashboard");
|
|
176
|
+
expect(html).toContain("Profile");
|
|
177
|
+
expect(html).toContain("Log out");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("logged in: polaris logo links to dashboard", () => {
|
|
181
|
+
const html = nav("tok123");
|
|
182
|
+
expect(html).toContain('href="/dashboard?token=tok123"');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("logged out: polaris logo links to home", () => {
|
|
186
|
+
const html = nav();
|
|
187
|
+
expect(html).toContain('href="/"');
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// --- Fixture integrity ---
|
|
192
|
+
|
|
193
|
+
describe("fixtures", () => {
|
|
194
|
+
test("mock sessions reference existing projects", () => {
|
|
195
|
+
const projectNames = mockProjects.map((p) => p.name);
|
|
196
|
+
for (const session of mockActiveSessions) {
|
|
197
|
+
expect(projectNames).toContain(session.project);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("mock devices have required fields", () => {
|
|
202
|
+
for (const device of mockDevices) {
|
|
203
|
+
expect(device.name).toBeTruthy();
|
|
204
|
+
expect(device.os).toBeTruthy();
|
|
205
|
+
expect(device.lastSeen).toBeTruthy();
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("mock user participant_id matches format", () => {
|
|
210
|
+
expect(mockUser.participant_id).toMatch(/^user:[a-z]/);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("mockActiveSessions only includes sessions where mockUser participates", () => {
|
|
214
|
+
for (const session of mockActiveSessions) {
|
|
215
|
+
const userInSession = session.participants.some((p) => p.id === mockUser.participant_id);
|
|
216
|
+
expect(userInSession).toBe(true);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// --- Error view ---
|
|
222
|
+
|
|
223
|
+
describe("renderErrorView", () => {
|
|
224
|
+
test("renders error message", () => {
|
|
225
|
+
const html = renderErrorView("Something went wrong.");
|
|
226
|
+
expect(html).toContain("Something went wrong.");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("renders with action link", () => {
|
|
230
|
+
const html = renderErrorView("Auth failed.", "Try again", "/login");
|
|
231
|
+
expect(html).toContain("Auth failed.");
|
|
232
|
+
expect(html).toContain("Try again");
|
|
233
|
+
expect(html).toContain('href="/login"');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("renders without action link when not provided", () => {
|
|
237
|
+
const html = renderErrorView("Oops.");
|
|
238
|
+
// The error card itself should not contain an action link (nav has its own links)
|
|
239
|
+
const cardHtml = html.split("bg-red-100")[1] ?? "";
|
|
240
|
+
expect(cardHtml).not.toContain("Try again");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("renders warning icon", () => {
|
|
244
|
+
const html = renderErrorView("Error");
|
|
245
|
+
expect(html).toContain("bg-red-100");
|
|
246
|
+
expect(html).toContain("text-red-600");
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// --- Route behavior ---
|
|
251
|
+
|
|
252
|
+
describe("routes", () => {
|
|
253
|
+
let sql: Sql;
|
|
254
|
+
let app: ReturnType<typeof createApp>;
|
|
255
|
+
let validToken: string;
|
|
256
|
+
|
|
257
|
+
beforeAll(async () => {
|
|
258
|
+
sql = await createDb(DATABASE_URL);
|
|
259
|
+
app = createApp(sql);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
afterAll(async () => {
|
|
263
|
+
await sql.end();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
beforeEach(async () => {
|
|
267
|
+
await sql`DROP TABLE IF EXISTS events`;
|
|
268
|
+
await sql`DROP TABLE IF EXISTS sessions`;
|
|
269
|
+
await sql`DROP TABLE IF EXISTS projects`;
|
|
270
|
+
await sql`DROP TABLE IF EXISTS users`;
|
|
271
|
+
await sql`DROP TABLE IF EXISTS orgs`;
|
|
272
|
+
await sql`CREATE TABLE IF NOT EXISTS orgs (id TEXT PRIMARY KEY, name TEXT NOT NULL, domain TEXT, slack_team_id TEXT, slack_bot_token TEXT, slack_system_channel_id TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())`;
|
|
273
|
+
await sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, org_id TEXT NOT NULL REFERENCES orgs(id), participant_id TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())`;
|
|
274
|
+
await sql`CREATE TABLE IF NOT EXISTS projects (name TEXT NOT NULL, org_id TEXT NOT NULL REFERENCES orgs(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (org_id, name))`;
|
|
275
|
+
await sql`CREATE TABLE IF NOT EXISTS sessions (name TEXT NOT NULL, project TEXT NOT NULL, org_id TEXT NOT NULL, driver TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (org_id, project, name), FOREIGN KEY (org_id, project) REFERENCES projects(org_id, name))`;
|
|
276
|
+
await sql`CREATE TABLE IF NOT EXISTS events (id UUID PRIMARY KEY, org_id TEXT NOT NULL, project TEXT NOT NULL, session TEXT NOT NULL, timestamp TIMESTAMPTZ NOT NULL, source TEXT NOT NULL, sender TEXT NOT NULL, payload JSONB NOT NULL)`;
|
|
277
|
+
|
|
278
|
+
await createOrg(sql, "test-org", "Test Org", "test.com");
|
|
279
|
+
await createUser(sql, "user-1", "test@test.com", "Test User", "test-org", "user:test");
|
|
280
|
+
validToken = await createToken({
|
|
281
|
+
sub: "user-1",
|
|
282
|
+
email: "test@test.com",
|
|
283
|
+
name: "Test User",
|
|
284
|
+
org_id: "test-org",
|
|
285
|
+
participant_id: "user:test",
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("GET / returns 200", async () => {
|
|
290
|
+
const res = await app.request("/");
|
|
291
|
+
expect(res.status).toBe(200);
|
|
292
|
+
const body = await res.text();
|
|
293
|
+
expect(body).toContain("Multiplayer AI collaboration");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("GET /preview returns 200", async () => {
|
|
297
|
+
const res = await app.request("/preview");
|
|
298
|
+
expect(res.status).toBe(200);
|
|
299
|
+
const body = await res.text();
|
|
300
|
+
expect(body).toContain("View Preview");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("GET /dashboard without token redirects to /login", async () => {
|
|
304
|
+
const res = await app.request("/dashboard");
|
|
305
|
+
expect(res.status).toBe(302);
|
|
306
|
+
expect(res.headers.get("location")).toBe("/login");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("GET /dashboard with invalid token redirects to /login", async () => {
|
|
310
|
+
const res = await app.request("/dashboard?token=bad-token");
|
|
311
|
+
expect(res.status).toBe(302);
|
|
312
|
+
expect(res.headers.get("location")).toBe("/login");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("GET /dashboard with valid token returns 200", async () => {
|
|
316
|
+
const res = await app.request(`/dashboard?token=${validToken}`);
|
|
317
|
+
expect(res.status).toBe(200);
|
|
318
|
+
const body = await res.text();
|
|
319
|
+
expect(body).toContain("Floor");
|
|
320
|
+
expect(body).toContain("Devices");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("GET /profile without token redirects to /login", async () => {
|
|
324
|
+
const res = await app.request("/profile");
|
|
325
|
+
expect(res.status).toBe(302);
|
|
326
|
+
expect(res.headers.get("location")).toBe("/login");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("GET /profile with valid token returns 200", async () => {
|
|
330
|
+
const res = await app.request(`/profile?token=${validToken}`);
|
|
331
|
+
expect(res.status).toBe(200);
|
|
332
|
+
const body = await res.text();
|
|
333
|
+
expect(body).toContain("Test User");
|
|
334
|
+
expect(body).toContain("test@test.com");
|
|
335
|
+
expect(body).toContain("user:test");
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("GET /auth/token with valid token returns user info", async () => {
|
|
339
|
+
const res = await app.request(`/auth/token?token=${validToken}`);
|
|
340
|
+
expect(res.status).toBe(200);
|
|
341
|
+
const body = await res.json();
|
|
342
|
+
expect(body.email).toBe("test@test.com");
|
|
343
|
+
expect(body.org_id).toBe("test-org");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("GET /auth/token with invalid token returns 401", async () => {
|
|
347
|
+
const res = await app.request("/auth/token?token=bad");
|
|
348
|
+
expect(res.status).toBe(401);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("GET /auth/token without token returns 400", async () => {
|
|
352
|
+
const res = await app.request("/auth/token");
|
|
353
|
+
expect(res.status).toBe(400);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// --- Slack routes ---
|
|
357
|
+
|
|
358
|
+
test("GET /slack/install without SLACK_CLIENT_ID shows error", async () => {
|
|
359
|
+
const origId = process.env.SLACK_CLIENT_ID;
|
|
360
|
+
delete process.env.SLACK_CLIENT_ID;
|
|
361
|
+
const res = await app.request(`/slack/install?token=${validToken}`);
|
|
362
|
+
expect(res.status).toBe(200);
|
|
363
|
+
const body = await res.text();
|
|
364
|
+
expect(body).toContain("Slack integration not configured");
|
|
365
|
+
if (origId) process.env.SLACK_CLIENT_ID = origId;
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("GET /slack/install with SLACK_CLIENT_ID redirects to Slack", async () => {
|
|
369
|
+
process.env.SLACK_CLIENT_ID = "fake-client-id";
|
|
370
|
+
const res = await app.request(`/slack/install?token=${validToken}`);
|
|
371
|
+
expect(res.status).toBe(302);
|
|
372
|
+
const location = res.headers.get("location") ?? "";
|
|
373
|
+
expect(location).toContain("slack.com/oauth");
|
|
374
|
+
expect(location).toContain("fake-client-id");
|
|
375
|
+
delete process.env.SLACK_CLIENT_ID;
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("GET /slack/callback without code/state shows error", async () => {
|
|
379
|
+
const res = await app.request("/slack/callback");
|
|
380
|
+
expect(res.status).toBe(200);
|
|
381
|
+
const body = await res.text();
|
|
382
|
+
expect(body).toContain("Missing code or state");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("GET /slack/callback with invalid token redirects to login", async () => {
|
|
386
|
+
const res = await app.request("/slack/callback?code=test&state=bad-token");
|
|
387
|
+
expect(res.status).toBe(302);
|
|
388
|
+
expect(res.headers.get("location")).toBe("/login");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// --- Unified auth ---
|
|
392
|
+
|
|
393
|
+
test("GET /signup redirects to Google OAuth", async () => {
|
|
394
|
+
const res = await app.request("/signup");
|
|
395
|
+
expect(res.status).toBe(302);
|
|
396
|
+
const location = res.headers.get("location") ?? "";
|
|
397
|
+
expect(location).toContain("accounts.google.com");
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("GET /login redirects to Google OAuth", async () => {
|
|
401
|
+
const res = await app.request("/login");
|
|
402
|
+
expect(res.status).toBe(302);
|
|
403
|
+
const location = res.headers.get("location") ?? "";
|
|
404
|
+
expect(location).toContain("accounts.google.com");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("GET /auth/google/callback without params shows error", async () => {
|
|
408
|
+
const res = await app.request("/auth/google/callback");
|
|
409
|
+
expect(res.status).toBe(200);
|
|
410
|
+
const body = await res.text();
|
|
411
|
+
expect(body).toContain("Missing code or state");
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("GET /auth/google/callback with invalid state shows error", async () => {
|
|
415
|
+
const res = await app.request("/auth/google/callback?code=test&state=bad-state");
|
|
416
|
+
expect(res.status).toBe(200);
|
|
417
|
+
const body = await res.text();
|
|
418
|
+
expect(body).toContain("Invalid or expired OAuth state");
|
|
419
|
+
});
|
|
420
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"types": ["bun-types"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"]
|
|
16
|
+
}
|