@openparachute/hub 0.5.13 → 0.5.14-rc.10
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/README.md +109 -15
- package/package.json +2 -2
- package/src/__tests__/account-home-ui.test.ts +205 -0
- package/src/__tests__/admin-handlers.test.ts +74 -0
- package/src/__tests__/admin-host-admin-token.test.ts +62 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +44 -0
- package/src/__tests__/admin-vaults.test.ts +70 -4
- package/src/__tests__/api-account.test.ts +191 -1
- package/src/__tests__/api-mint-token.test.ts +682 -3
- package/src/__tests__/api-modules-config.test.ts +16 -10
- package/src/__tests__/api-modules-ops.test.ts +97 -0
- package/src/__tests__/api-modules.test.ts +100 -83
- package/src/__tests__/api-ready.test.ts +135 -0
- package/src/__tests__/api-revoke-token.test.ts +384 -0
- package/src/__tests__/api-users.test.ts +390 -13
- package/src/__tests__/chrome-strip.test.ts +15 -15
- package/src/__tests__/cli.test.ts +7 -5
- package/src/__tests__/cloudflare-detect.test.ts +60 -5
- package/src/__tests__/expose-auth-preflight.test.ts +58 -50
- package/src/__tests__/expose-cloudflare.test.ts +114 -3
- package/src/__tests__/expose-interactive.test.ts +10 -4
- package/src/__tests__/expose-public-auto.test.ts +5 -1
- package/src/__tests__/expose.test.ts +49 -1
- package/src/__tests__/hub-db.test.ts +194 -29
- package/src/__tests__/hub-server.test.ts +322 -33
- package/src/__tests__/hub.test.ts +11 -0
- package/src/__tests__/init.test.ts +827 -0
- package/src/__tests__/lifecycle.test.ts +33 -1
- package/src/__tests__/migrate.test.ts +433 -51
- package/src/__tests__/notes-redirect.test.ts +20 -20
- package/src/__tests__/oauth-handlers.test.ts +1060 -29
- package/src/__tests__/oauth-ui.test.ts +12 -1
- package/src/__tests__/proxy-error-ui.test.ts +212 -0
- package/src/__tests__/proxy-state.test.ts +192 -0
- package/src/__tests__/resource-binding.test.ts +97 -0
- package/src/__tests__/scope-explanations.test.ts +36 -0
- package/src/__tests__/serve.test.ts +9 -9
- package/src/__tests__/services-manifest.test.ts +40 -40
- package/src/__tests__/setup-wizard.test.ts +1114 -66
- package/src/__tests__/setup.test.ts +1 -1
- package/src/__tests__/status.test.ts +39 -0
- package/src/__tests__/users.test.ts +396 -9
- package/src/__tests__/vault-auth-status.test.ts +271 -11
- package/src/__tests__/vault-hub-origin-env.test.ts +126 -0
- package/src/__tests__/well-known.test.ts +9 -9
- package/src/__tests__/wizard.test.ts +372 -0
- package/src/account-home-ui.ts +547 -0
- package/src/admin-handlers.ts +49 -17
- package/src/admin-host-admin-token.ts +25 -0
- package/src/admin-login-ui.ts +4 -4
- package/src/admin-vault-admin-token.ts +17 -0
- package/src/admin-vaults.ts +48 -15
- package/src/api-account.ts +72 -6
- package/src/api-mint-token.ts +132 -24
- package/src/api-modules-ops.ts +52 -16
- package/src/api-modules.ts +31 -14
- package/src/api-ready.ts +102 -0
- package/src/api-revoke-token.ts +107 -21
- package/src/api-users.ts +497 -58
- package/src/bun-link.ts +55 -0
- package/src/chrome-strip.ts +6 -6
- package/src/cli.ts +93 -24
- package/src/cloudflare/config.ts +10 -4
- package/src/cloudflare/detect.ts +73 -6
- package/src/commands/expose-auth-preflight.ts +55 -63
- package/src/commands/expose-cloudflare.ts +114 -10
- package/src/commands/expose-interactive.ts +10 -11
- package/src/commands/expose-public-auto.ts +6 -4
- package/src/commands/expose.ts +8 -0
- package/src/commands/init.ts +563 -0
- package/src/commands/install.ts +41 -23
- package/src/commands/lifecycle.ts +12 -0
- package/src/commands/migrate.ts +293 -41
- package/src/commands/status.ts +10 -1
- package/src/commands/wizard.ts +843 -0
- package/src/env-file.ts +10 -0
- package/src/help.ts +157 -17
- package/src/hub-db.ts +42 -0
- package/src/hub-server.ts +136 -23
- package/src/hub-settings.ts +13 -2
- package/src/hub.ts +16 -9
- package/src/notes-redirect.ts +5 -5
- package/src/oauth-handlers.ts +342 -173
- package/src/oauth-ui.ts +28 -2
- package/src/proxy-error-ui.ts +506 -0
- package/src/proxy-state.ts +131 -0
- package/src/resource-binding.ts +134 -0
- package/src/scope-attenuation.ts +85 -0
- package/src/scope-explanations.ts +94 -5
- package/src/service-spec.ts +39 -18
- package/src/setup-wizard.ts +1173 -117
- package/src/users.ts +307 -29
- package/src/vault/auth-status.ts +152 -25
- package/src/vault-hub-origin-env.ts +100 -0
- package/web/ui/dist/assets/index-2SSK7JbM.js +61 -0
- package/web/ui/dist/assets/index-B28SdMSz.css +1 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/vault-tokens-create-interactive.test.ts +0 -183
- package/src/commands/vault-tokens-create-interactive.ts +0 -143
- package/web/ui/dist/assets/index-7DtAXz7y.css +0 -1
- package/web/ui/dist/assets/index-Dzrbe6EP.js +0 -61
|
@@ -8,11 +8,21 @@ import { signAccessToken } from "../jwt-sign.ts";
|
|
|
8
8
|
import { upsertService, writeManifest } from "../services-manifest.ts";
|
|
9
9
|
import { rotateSigningKey } from "../signing-keys.ts";
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Build the JSON shape `parachute-vault create --json` emits (PR #184).
|
|
13
|
+
* Post the pvt_* DROP the `token` is a hub-issued access JWT (scoped
|
|
14
|
+
* `vault:<name>:admin`), and may be the empty string when the vault
|
|
15
|
+
* couldn't mint — in which case `token_guidance` carries the reason.
|
|
16
|
+
*/
|
|
17
|
+
function vaultCreateJson(
|
|
18
|
+
name: string,
|
|
19
|
+
token = `hubjwt.${name}.access`,
|
|
20
|
+
tokenGuidance?: string,
|
|
21
|
+
): string {
|
|
13
22
|
return JSON.stringify({
|
|
14
23
|
name,
|
|
15
24
|
token,
|
|
25
|
+
...(tokenGuidance ? { token_guidance: tokenGuidance } : {}),
|
|
16
26
|
paths: {
|
|
17
27
|
vault_dir: `/home/test/.parachute/vault/${name}`,
|
|
18
28
|
vault_db: `/home/test/.parachute/vault/${name}/vault.db`,
|
|
@@ -404,7 +414,7 @@ describe("POST /vaults — orchestration", () => {
|
|
|
404
414
|
);
|
|
405
415
|
return {
|
|
406
416
|
exitCode: 0,
|
|
407
|
-
stdout: vaultCreateJson("work", "
|
|
417
|
+
stdout: vaultCreateJson("work", "hubjwt.work.access"),
|
|
408
418
|
stderr: "",
|
|
409
419
|
};
|
|
410
420
|
};
|
|
@@ -420,7 +430,7 @@ describe("POST /vaults — orchestration", () => {
|
|
|
420
430
|
token?: string;
|
|
421
431
|
paths?: { vault_dir: string; vault_db: string; vault_config: string };
|
|
422
432
|
};
|
|
423
|
-
expect(body.token).toBe("
|
|
433
|
+
expect(body.token).toBe("hubjwt.work.access");
|
|
424
434
|
expect(body.paths).toEqual({
|
|
425
435
|
vault_dir: "/home/test/.parachute/vault/work",
|
|
426
436
|
vault_db: "/home/test/.parachute/vault/work/vault.db",
|
|
@@ -434,6 +444,62 @@ describe("POST /vaults — orchestration", () => {
|
|
|
434
444
|
}
|
|
435
445
|
});
|
|
436
446
|
|
|
447
|
+
test("201 forwards an empty token + token_guidance when the vault couldn't mint (post-DROP)", async () => {
|
|
448
|
+
// The vault emits `token: ""` + a `token_guidance` reason when no hub
|
|
449
|
+
// origin was reachable to mint against (e.g. loopback create). The hub
|
|
450
|
+
// must forward both verbatim so the SPA can render the
|
|
451
|
+
// created-but-no-token state instead of confusing it with a re-POST.
|
|
452
|
+
const h = makeHarness();
|
|
453
|
+
try {
|
|
454
|
+
const db = openHubDb(hubDbPath(h.dir));
|
|
455
|
+
try {
|
|
456
|
+
rotateSigningKey(db);
|
|
457
|
+
upsertService(
|
|
458
|
+
{
|
|
459
|
+
name: "parachute-vault",
|
|
460
|
+
port: 1940,
|
|
461
|
+
paths: ["/vault/default"],
|
|
462
|
+
health: "/health",
|
|
463
|
+
version: "0.3.5",
|
|
464
|
+
},
|
|
465
|
+
h.manifestPath,
|
|
466
|
+
);
|
|
467
|
+
const runCommand = async (_cmd: readonly string[]): Promise<RunResult> => {
|
|
468
|
+
upsertService(
|
|
469
|
+
{
|
|
470
|
+
name: "parachute-vault",
|
|
471
|
+
port: 1940,
|
|
472
|
+
paths: ["/vault/default", "/vault/work"],
|
|
473
|
+
health: "/health",
|
|
474
|
+
version: "0.3.5",
|
|
475
|
+
},
|
|
476
|
+
h.manifestPath,
|
|
477
|
+
);
|
|
478
|
+
return {
|
|
479
|
+
exitCode: 0,
|
|
480
|
+
stdout: vaultCreateJson("work", "", "no hub origin reachable to mint against"),
|
|
481
|
+
stderr: "",
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
const res = await call({
|
|
485
|
+
db,
|
|
486
|
+
manifestPath: h.manifestPath,
|
|
487
|
+
body: { name: "work" },
|
|
488
|
+
runCommand,
|
|
489
|
+
});
|
|
490
|
+
// Still a fresh create — HTTP 201, NOT 200.
|
|
491
|
+
expect(res.status).toBe(201);
|
|
492
|
+
const body = (await res.json()) as { token?: string; token_guidance?: string };
|
|
493
|
+
expect(body.token).toBe("");
|
|
494
|
+
expect(body.token_guidance).toBe("no hub origin reachable to mint against");
|
|
495
|
+
} finally {
|
|
496
|
+
db.close();
|
|
497
|
+
}
|
|
498
|
+
} finally {
|
|
499
|
+
h.cleanup();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
437
503
|
test("500 when `parachute-vault create --json` exits 0 but stdout is unparseable", async () => {
|
|
438
504
|
const h = makeHarness();
|
|
439
505
|
try {
|
|
@@ -25,6 +25,7 @@ import { join } from "node:path";
|
|
|
25
25
|
import {
|
|
26
26
|
handleAccountChangePasswordGet,
|
|
27
27
|
handleAccountChangePasswordPost,
|
|
28
|
+
handleAccountHomeGet,
|
|
28
29
|
markPasswordChanged,
|
|
29
30
|
} from "../api-account.ts";
|
|
30
31
|
import { CSRF_COOKIE_NAME, CSRF_FIELD_NAME } from "../csrf.ts";
|
|
@@ -256,7 +257,110 @@ describe("POST /account/change-password", () => {
|
|
|
256
257
|
}
|
|
257
258
|
});
|
|
258
259
|
|
|
259
|
-
test("
|
|
260
|
+
test("non-admin user with no next defaults to /account/ (no admin-shell flash)", async () => {
|
|
261
|
+
// Without this rewrite, a friend's change-password POST would 302 to
|
|
262
|
+
// /admin/vaults, the SPA would load, the 403 from
|
|
263
|
+
// /admin/host-admin-token would bounce them to /account/ — a visible
|
|
264
|
+
// two-hop flash. Mirror the login-redirect rewrite in admin-handlers.ts.
|
|
265
|
+
await createUser(harness.db, "operator", "operator-strong-passphrase");
|
|
266
|
+
const { cookie } = await sessionCookieFor(harness.db, "friend", "old-default-pw", {
|
|
267
|
+
passwordChanged: false,
|
|
268
|
+
allowMulti: true,
|
|
269
|
+
});
|
|
270
|
+
const { body, headers } = formBody({
|
|
271
|
+
[CSRF_FIELD_NAME]: TEST_CSRF,
|
|
272
|
+
current_password: "old-default-pw",
|
|
273
|
+
new_password: "user-chosen-strong-passphrase",
|
|
274
|
+
new_password_confirm: "user-chosen-strong-passphrase",
|
|
275
|
+
});
|
|
276
|
+
const req = new Request("http://hub.test/account/change-password", {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: { ...headers, cookie },
|
|
279
|
+
body,
|
|
280
|
+
});
|
|
281
|
+
const res = await handleAccountChangePasswordPost(req, { db: harness.db });
|
|
282
|
+
expect(res.status).toBe(302);
|
|
283
|
+
expect(res.headers.get("location")).toBe("/account/");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("non-admin user with next=/admin/users gets rewritten to /account/", async () => {
|
|
287
|
+
await createUser(harness.db, "operator", "operator-strong-passphrase");
|
|
288
|
+
const { cookie } = await sessionCookieFor(harness.db, "friend", "old-default-pw", {
|
|
289
|
+
passwordChanged: false,
|
|
290
|
+
allowMulti: true,
|
|
291
|
+
});
|
|
292
|
+
const { body, headers } = formBody({
|
|
293
|
+
[CSRF_FIELD_NAME]: TEST_CSRF,
|
|
294
|
+
current_password: "old-default-pw",
|
|
295
|
+
new_password: "user-chosen-strong-passphrase",
|
|
296
|
+
new_password_confirm: "user-chosen-strong-passphrase",
|
|
297
|
+
next: "/admin/users",
|
|
298
|
+
});
|
|
299
|
+
const req = new Request("http://hub.test/account/change-password", {
|
|
300
|
+
method: "POST",
|
|
301
|
+
headers: { ...headers, cookie },
|
|
302
|
+
body,
|
|
303
|
+
});
|
|
304
|
+
const res = await handleAccountChangePasswordPost(req, { db: harness.db });
|
|
305
|
+
expect(res.status).toBe(302);
|
|
306
|
+
expect(res.headers.get("location")).toBe("/account/");
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("non-admin user with non-admin next= passes through unchanged", async () => {
|
|
310
|
+
// Friends with a legitimate non-admin destination (e.g. /oauth/authorize
|
|
311
|
+
// mid-flow) should land where they intended — the rewrite only catches
|
|
312
|
+
// /admin/* targets.
|
|
313
|
+
await createUser(harness.db, "operator", "operator-strong-passphrase");
|
|
314
|
+
const { cookie } = await sessionCookieFor(harness.db, "friend", "old-default-pw", {
|
|
315
|
+
passwordChanged: false,
|
|
316
|
+
allowMulti: true,
|
|
317
|
+
});
|
|
318
|
+
const { body, headers } = formBody({
|
|
319
|
+
[CSRF_FIELD_NAME]: TEST_CSRF,
|
|
320
|
+
current_password: "old-default-pw",
|
|
321
|
+
new_password: "user-chosen-strong-passphrase",
|
|
322
|
+
new_password_confirm: "user-chosen-strong-passphrase",
|
|
323
|
+
next: "/oauth/authorize?client_id=abc",
|
|
324
|
+
});
|
|
325
|
+
const req = new Request("http://hub.test/account/change-password", {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: { ...headers, cookie },
|
|
328
|
+
body,
|
|
329
|
+
});
|
|
330
|
+
const res = await handleAccountChangePasswordPost(req, { db: harness.db });
|
|
331
|
+
expect(res.status).toBe(302);
|
|
332
|
+
expect(res.headers.get("location")).toBe("/oauth/authorize?client_id=abc");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("non-admin with exact next=/admin (no trailing slash) rewrites to /account/", async () => {
|
|
336
|
+
// Pins the exact-match arm of the prefix gate. Tests in #426 cover
|
|
337
|
+
// /admin/users (prefix match) and the no-next case (POST_CHANGE_DEFAULT
|
|
338
|
+
// → rewrite). This is the third arm.
|
|
339
|
+
await createUser(harness.db, "operator", "operator-strong-passphrase");
|
|
340
|
+
const { cookie } = await sessionCookieFor(harness.db, "friend", "old-default-pw", {
|
|
341
|
+
passwordChanged: false,
|
|
342
|
+
allowMulti: true,
|
|
343
|
+
});
|
|
344
|
+
const { body, headers } = formBody({
|
|
345
|
+
[CSRF_FIELD_NAME]: TEST_CSRF,
|
|
346
|
+
current_password: "old-default-pw",
|
|
347
|
+
new_password: "user-chosen-strong-passphrase",
|
|
348
|
+
new_password_confirm: "user-chosen-strong-passphrase",
|
|
349
|
+
next: "/admin",
|
|
350
|
+
});
|
|
351
|
+
const req = new Request("http://hub.test/account/change-password", {
|
|
352
|
+
method: "POST",
|
|
353
|
+
headers: { ...headers, cookie },
|
|
354
|
+
body,
|
|
355
|
+
});
|
|
356
|
+
const res = await handleAccountChangePasswordPost(req, { db: harness.db });
|
|
357
|
+
expect(res.status).toBe(302);
|
|
358
|
+
expect(res.headers.get("location")).toBe("/account/");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("first admin with no next still defaults to /admin/vaults", async () => {
|
|
362
|
+
// Existing behavior — preserved by the non-admin gate. The first user
|
|
363
|
+
// is the admin under Phase 1; admin SPA is the intended landing.
|
|
260
364
|
const { cookie } = await sessionCookieFor(harness.db, "newbie", "old-default-pw", {
|
|
261
365
|
passwordChanged: false,
|
|
262
366
|
});
|
|
@@ -628,3 +732,89 @@ describe("markPasswordChanged", () => {
|
|
|
628
732
|
expect(after?.passwordChanged).toBe(true);
|
|
629
733
|
});
|
|
630
734
|
});
|
|
735
|
+
|
|
736
|
+
describe("handleAccountHomeGet", () => {
|
|
737
|
+
// Integration smoke for `GET /account/` — verifies session gating
|
|
738
|
+
// (302 → /login when missing) and the happy path (200 + rendered HTML
|
|
739
|
+
// with the user's vault). The pure-renderer assertions live in
|
|
740
|
+
// `account-home-ui.test.ts`; this suite pins handler-level wiring.
|
|
741
|
+
|
|
742
|
+
const HUB_ORIGIN = "https://hub.test";
|
|
743
|
+
|
|
744
|
+
test("302 → /login when no session cookie is present", async () => {
|
|
745
|
+
const req = new Request(`${HUB_ORIGIN}/account/`);
|
|
746
|
+
const res = handleAccountHomeGet(req, { db: harness.db, hubOrigin: HUB_ORIGIN });
|
|
747
|
+
expect(res.status).toBe(302);
|
|
748
|
+
const location = res.headers.get("location") ?? "";
|
|
749
|
+
// Round-trip /account/ as the `next` param so post-login lands back.
|
|
750
|
+
expect(location).toBe(`/login?next=${encodeURIComponent("/account/")}`);
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("200 + HTML for a signed-in friend with an assigned vault", async () => {
|
|
754
|
+
// Create the admin first (so the friend is NOT the first admin),
|
|
755
|
+
// then a friend with an assigned vault.
|
|
756
|
+
await createUser(harness.db, "admin", "admin-passphrase", { passwordChanged: true });
|
|
757
|
+
const friend = await createUser(harness.db, "alice", "alice-passphrase", {
|
|
758
|
+
allowMulti: true,
|
|
759
|
+
passwordChanged: true,
|
|
760
|
+
assignedVaults: ["alice"],
|
|
761
|
+
});
|
|
762
|
+
const session = createSession(harness.db, { userId: friend.id });
|
|
763
|
+
const cookie = buildSessionCookie(session.id, Math.floor(SESSION_TTL_MS / 1000));
|
|
764
|
+
const req = new Request(`${HUB_ORIGIN}/account/`, { headers: { cookie } });
|
|
765
|
+
const res = handleAccountHomeGet(req, { db: harness.db, hubOrigin: HUB_ORIGIN });
|
|
766
|
+
expect(res.status).toBe(200);
|
|
767
|
+
expect(res.headers.get("content-type")).toContain("text/html");
|
|
768
|
+
const html = await res.text();
|
|
769
|
+
expect(html).toContain("Welcome, alice");
|
|
770
|
+
// Vault name visible.
|
|
771
|
+
expect(html).toContain("<strong>alice</strong>");
|
|
772
|
+
// Notes CTA carries the hub-origin-encoded vault URL.
|
|
773
|
+
const encoded = encodeURIComponent(`${HUB_ORIGIN}/vault/alice`);
|
|
774
|
+
expect(html).toContain(`https://notes.parachute.computer/add?url=${encoded}`);
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
test("200 + admin branch when the first-admin signs in (no vault assignments)", async () => {
|
|
778
|
+
// The first-created user with no vault pin is the admin posture.
|
|
779
|
+
const admin = await createUser(harness.db, "admin", "admin-passphrase", {
|
|
780
|
+
passwordChanged: true,
|
|
781
|
+
});
|
|
782
|
+
const session = createSession(harness.db, { userId: admin.id });
|
|
783
|
+
const cookie = buildSessionCookie(session.id, Math.floor(SESSION_TTL_MS / 1000));
|
|
784
|
+
const req = new Request(`${HUB_ORIGIN}/account/`, { headers: { cookie } });
|
|
785
|
+
const res = handleAccountHomeGet(req, { db: harness.db, hubOrigin: HUB_ORIGIN });
|
|
786
|
+
expect(res.status).toBe(200);
|
|
787
|
+
const html = await res.text();
|
|
788
|
+
expect(html).toContain("Welcome, admin");
|
|
789
|
+
expect(html).toContain("hub administrator");
|
|
790
|
+
expect(html).toContain('href="/admin/"');
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
test("302 → /login when the session points at a deleted user", async () => {
|
|
794
|
+
// Stale-session shape: a session row outlives its user. The handler
|
|
795
|
+
// hands back to /login rather than rendering against null.
|
|
796
|
+
//
|
|
797
|
+
// Construction note: `deleteUser` drops the session as part of its
|
|
798
|
+
// cleanup transaction, and the sessions.user_id FK is RESTRICT, so
|
|
799
|
+
// we briefly drop FK enforcement to fabricate the orphan-session
|
|
800
|
+
// shape. The handler's job is robustness against an externally-
|
|
801
|
+
// induced orphan (e.g. a race between session-read and user-delete
|
|
802
|
+
// on a different connection); the test exercises that defensive
|
|
803
|
+
// branch directly.
|
|
804
|
+
const user = await createUser(harness.db, "ghost", "ghost-passphrase", {
|
|
805
|
+
passwordChanged: true,
|
|
806
|
+
});
|
|
807
|
+
const session = createSession(harness.db, { userId: user.id });
|
|
808
|
+
const cookie = buildSessionCookie(session.id, Math.floor(SESSION_TTL_MS / 1000));
|
|
809
|
+
harness.db.exec("PRAGMA foreign_keys = OFF");
|
|
810
|
+
try {
|
|
811
|
+
harness.db.prepare("DELETE FROM users WHERE id = ?").run(user.id);
|
|
812
|
+
} finally {
|
|
813
|
+
harness.db.exec("PRAGMA foreign_keys = ON");
|
|
814
|
+
}
|
|
815
|
+
const req = new Request(`${HUB_ORIGIN}/account/`, { headers: { cookie } });
|
|
816
|
+
const res = handleAccountHomeGet(req, { db: harness.db, hubOrigin: HUB_ORIGIN });
|
|
817
|
+
expect(res.status).toBe(302);
|
|
818
|
+
expect(res.headers.get("location")).toBe("/login");
|
|
819
|
+
});
|
|
820
|
+
});
|