@podosoft/podokit 1.4.2 → 1.6.0
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 +2 -1
- package/dist/app-proxy-timeouts.d.ts +27 -0
- package/dist/app-proxy-timeouts.js +180 -0
- package/dist/assemble.d.ts +5 -2
- package/dist/assemble.js +14 -4
- package/dist/deploy-checks.d.ts +44 -0
- package/dist/deploy-checks.js +267 -0
- package/dist/deploy-compose-profile.d.ts +6 -2
- package/dist/deploy-compose-profile.js +15 -6
- package/dist/deploy-compose-render.d.ts +12 -4
- package/dist/deploy-compose-render.js +47 -17
- package/dist/deploy-compose.d.ts +2 -5
- package/dist/deploy-compose.js +7 -2
- package/dist/deploy-profile.d.ts +6 -2
- package/dist/deploy-profile.js +11 -7
- package/dist/deploy-render.d.ts +4 -1
- package/dist/deploy-render.js +28 -20
- package/dist/deploy-schema.d.ts +61 -0
- package/dist/deploy-schema.js +157 -1
- package/dist/deploy.d.ts +2 -5
- package/dist/deploy.js +5 -2
- package/dist/dev.d.ts +21 -0
- package/dist/dev.js +78 -3
- package/dist/eject.js +2 -0
- package/dist/index.js +85 -5
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +5 -2
- package/dist/lockfile.d.ts +24 -0
- package/dist/lockfile.js +77 -5
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +12 -4
- package/dist/remove.js +1 -1
- package/dist/template-files.d.ts +42 -0
- package/dist/template-files.js +102 -0
- package/dist/templates/fullstack/AGENTS.md +44 -7
- package/dist/templates/fullstack/README.md +8 -7
- package/dist/templates/fullstack/apps/api/package.json +4 -4
- package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
- package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
- package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +5 -1
- package/dist/templates/fullstack/apps/api/src/app.extensions.ts +7 -4
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +47 -28
- package/dist/templates/fullstack/apps/api/src/build.spec.ts +118 -0
- package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +46 -3
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
- package/dist/templates/fullstack/apps/api/src/config/providers.ts +8 -2
- package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +17 -6
- package/dist/templates/fullstack/apps/api/src/config/server-options.ts +5 -8
- package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +29 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +39 -2
- package/dist/templates/fullstack/apps/api/src/core/client-address.spec.ts +82 -0
- package/dist/templates/fullstack/apps/api/src/core/client-address.ts +68 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +43 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
- package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
- package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
- package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
- package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
- package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
- package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
- package/dist/templates/fullstack/apps/web/Dockerfile +4 -0
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/app.css +3 -0
- package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
- package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +370 -36
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +250 -41
- package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +5 -2
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +23 -0
- package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
- package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
- package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
- package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
- package/dist/templates/fullstack/compose.dev.yaml +7 -4
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +12 -3
- package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
- package/dist/templates/fullstack/dot-env.docker.example +7 -2
- package/dist/templates/fullstack/dot-env.example +21 -1
- package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
- package/dist/templates/fullstack/infra/k3s/configmap.yaml +4 -0
- package/dist/templates/fullstack/tests/README.md +40 -1
- package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
- package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
- package/dist/templates/fullstack/tests/package.json +2 -2
- package/dist/templates/fullstack/tests/playwright.config.ts +7 -3
- package/dist/templates/fullstack/tests/playwright.extensions.ts +88 -1
- package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.spec.ts +152 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.ts +112 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +18 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +36 -13
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +13 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.test.ts +33 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.ts +34 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +73 -39
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/account-sessions.api.spec.ts +55 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +29 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +55 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +11 -5
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +3 -10
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-sessions.ui.spec.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +62 -59
- package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
- package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +33 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
- package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +33 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +20 -1
- package/dist/templates/modules/audit-log/module.manifest.json +15 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +6 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +54 -6
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -8
- package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.spec.ts +56 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.ts +45 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
- package/dist/templates/modules/auth/module.manifest.json +8 -2
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +57 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +93 -9
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
- package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
- package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
- package/dist/templates/modules/bullmq/module.manifest.json +5 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +87 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +93 -3
- package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +1 -1
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +1 -1
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +2 -59
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +0 -65
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +82 -21
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +54 -15
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +43 -5
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +10 -2
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +61 -3
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +48 -15
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +63 -27
- package/dist/templates/modules/rate-limit/module.manifest.json +6 -9
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
- package/dist/templates/todo/AGENTS.md +46 -6
- package/dist/templates/todo/README.md +6 -4
- package/dist/text-diff.d.ts +12 -0
- package/dist/text-diff.js +111 -0
- package/dist/update.d.ts +61 -3
- package/dist/update.js +152 -27
- package/package.json +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/exclusive.ts +0 -80
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { createPublicKey, verify, type JsonWebKey } from "node:crypto";
|
|
3
3
|
import { ADMIN, USER } from "../helpers/accounts";
|
|
4
|
-
import {
|
|
5
|
-
import { clearSms, smsSinkReachable, waitForSmsOtp } from "../helpers/sms";
|
|
4
|
+
import { smsSinkReachable, waitForSmsOtp } from "../helpers/sms";
|
|
6
5
|
import { totpCode } from "../helpers/totp";
|
|
7
6
|
|
|
8
7
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
@@ -32,7 +31,6 @@ test("phone number: verify with the code delivered to the SMS sink", async ({ pl
|
|
|
32
31
|
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
33
32
|
test.skip(!caps?.phoneNumber, "phone number not enabled");
|
|
34
33
|
test.skip(!(await smsSinkReachable()), "SMS sink not reachable");
|
|
35
|
-
await clearSms();
|
|
36
34
|
const email = `phone-${Date.now()}@example.com`;
|
|
37
35
|
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Phone" } });
|
|
38
36
|
// Unique number per run so the "phone already exists" guard never trips.
|
|
@@ -144,50 +142,48 @@ test("two-factor: regenerating backup codes invalidates the old set", async ({ p
|
|
|
144
142
|
await ctxNew.dispose();
|
|
145
143
|
});
|
|
146
144
|
|
|
147
|
-
test("require-2fa: gates an un-enrolled user until they enrol", async ({ playwright }) => {
|
|
145
|
+
test("require-2fa: gates an un-enrolled user until they enrol", { tag: "@exclusive" }, async ({ playwright }) => {
|
|
148
146
|
test.slow();
|
|
149
|
-
await
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
await
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
await
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
});
|
|
147
|
+
const admin = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
148
|
+
const caps = await (await admin.get("/api/account/capabilities")).json();
|
|
149
|
+
test.skip(!caps?.twoFactor, "two-factor not enabled");
|
|
150
|
+
await admin.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
151
|
+
|
|
152
|
+
const user = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
153
|
+
const email = `rq-${Date.now()}@example.com`;
|
|
154
|
+
const pw = "Podokit3e-Str0ng!pw";
|
|
155
|
+
await user.post("/api/auth/sign-up/email", { data: { email, password: pw, name: "RQ" } });
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Turn the policy on (admin; the toggle endpoint is allow-listed by the guard).
|
|
159
|
+
await admin.put("/api/account/settings", { data: { require2fa: true } });
|
|
160
|
+
// Capabilities carry the same policy as the dedicated endpoint.
|
|
161
|
+
await expect(async () => {
|
|
162
|
+
expect((await (await admin.get("/api/account/capabilities")).json()).require2fa).toBe(true);
|
|
163
|
+
expect(await (await admin.get("/api/account/require-2fa")).json()).toEqual({ require2fa: true });
|
|
164
|
+
}).toPass({ timeout: 8000 });
|
|
165
|
+
// The guard caches for a few seconds — poll until the un-enrolled user is gated.
|
|
166
|
+
await expect(async () => {
|
|
167
|
+
expect((await user.get("/api/account/me")).status()).toBe(403);
|
|
168
|
+
}).toPass({ timeout: 8000 });
|
|
169
|
+
|
|
170
|
+
// Enrolling restores access.
|
|
171
|
+
const enable = await (await user.post("/api/auth/two-factor/enable", { data: { password: pw } })).json();
|
|
172
|
+
await user.post("/api/auth/two-factor/verify-totp", { data: { code: totpCode(enable.totpURI) } });
|
|
173
|
+
expect((await user.get("/api/account/me")).status()).toBe(200);
|
|
174
|
+
} finally {
|
|
175
|
+
// Always turn it back off so the shared (un-enrolled) sessions aren't gated.
|
|
176
|
+
await admin.put("/api/account/settings", { data: { require2fa: false } });
|
|
177
|
+
await expect(async () => {
|
|
178
|
+
const probe = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
179
|
+
await probe.post("/api/auth/sign-in/email", { data: { email: USER.email, password: USER.password } });
|
|
180
|
+
const status = (await probe.get("/api/account/me")).status();
|
|
181
|
+
await probe.dispose();
|
|
182
|
+
expect(status).toBe(200);
|
|
183
|
+
}).toPass({ timeout: 8000 });
|
|
184
|
+
await admin.dispose();
|
|
185
|
+
await user.dispose();
|
|
186
|
+
}
|
|
191
187
|
});
|
|
192
188
|
|
|
193
189
|
test("multi-session holds several accounts and switches between them", async ({ playwright }) => {
|
|
@@ -384,10 +380,9 @@ test("an organization can have a parent and manager members", async ({ playwrigh
|
|
|
384
380
|
await ctx.dispose();
|
|
385
381
|
});
|
|
386
382
|
|
|
387
|
-
test("the OIDC provider's discovery and endpoints follow its setting", async ({ playwright }) => {
|
|
383
|
+
test("the OIDC provider's discovery and endpoints follow its setting", { tag: "@exclusive" }, async ({ playwright }) => {
|
|
388
384
|
// The provider's metadata is answered before endpoint hooks run, so it needs its
|
|
389
|
-
// own check that "off" hides it.
|
|
390
|
-
// also toggles the provider, from running at the same time.
|
|
385
|
+
// own check that "off" hides it.
|
|
391
386
|
test.slow();
|
|
392
387
|
const admin = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
393
388
|
await admin.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
@@ -399,22 +394,20 @@ test("the OIDC provider's discovery and endpoints follow its setting", async ({
|
|
|
399
394
|
const allStatuses = (): Promise<number[]> =>
|
|
400
395
|
Promise.all([...discovery, "/api/auth/oauth2/authorize?client_id=probe"].map(status));
|
|
401
396
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
expect((await admin.put("/api/account/settings", { data: { oidcProvider: false } })).ok()).toBeTruthy();
|
|
405
|
-
await expect.poll(allStatuses, { timeout: 8000 }).toEqual([404, 404, 404]);
|
|
406
|
-
// The jwt plugin's keys and session tokens are not part of the provider.
|
|
407
|
-
expect(await status("/api/auth/jwks")).toBe(200);
|
|
408
|
-
|
|
409
|
-
expect((await admin.put("/api/account/settings", { data: { oidcProvider: true } })).ok()).toBeTruthy();
|
|
410
|
-
await expect.poll(() => Promise.all(discovery.map(status)), { timeout: 8000 }).toEqual([200, 200]);
|
|
411
|
-
const metadata = await (await admin.get(discovery[0]!)).json();
|
|
412
|
-
expect(metadata.authorization_endpoint).toContain("/api/auth/oauth2/authorize");
|
|
413
|
-
} finally {
|
|
414
|
-
expect((await admin.put("/api/account/settings", { data: { oidcProvider: false } })).ok()).toBeTruthy();
|
|
415
|
-
}
|
|
397
|
+
try {
|
|
398
|
+
expect((await admin.put("/api/account/settings", { data: { oidcProvider: false } })).ok()).toBeTruthy();
|
|
416
399
|
await expect.poll(allStatuses, { timeout: 8000 }).toEqual([404, 404, 404]);
|
|
417
|
-
|
|
400
|
+
// The jwt plugin's keys and session tokens are not part of the provider.
|
|
401
|
+
expect(await status("/api/auth/jwks")).toBe(200);
|
|
402
|
+
|
|
403
|
+
expect((await admin.put("/api/account/settings", { data: { oidcProvider: true } })).ok()).toBeTruthy();
|
|
404
|
+
await expect.poll(() => Promise.all(discovery.map(status)), { timeout: 8000 }).toEqual([200, 200]);
|
|
405
|
+
const metadata = await (await admin.get(discovery[0]!)).json();
|
|
406
|
+
expect(metadata.authorization_endpoint).toContain("/api/auth/oauth2/authorize");
|
|
407
|
+
} finally {
|
|
408
|
+
expect((await admin.put("/api/account/settings", { data: { oidcProvider: false } })).ok()).toBeTruthy();
|
|
409
|
+
}
|
|
410
|
+
await expect.poll(allStatuses, { timeout: 8000 }).toEqual([404, 404, 404]);
|
|
418
411
|
await admin.dispose();
|
|
419
412
|
});
|
|
420
413
|
|
|
@@ -426,28 +419,26 @@ test("only admins can change settings @smoke", async ({ playwright }) => {
|
|
|
426
419
|
await userCtx.dispose();
|
|
427
420
|
});
|
|
428
421
|
|
|
429
|
-
test("disabling a feature blocks its endpoint server-side and re-enabling restores it @smoke", async ({ playwright }) => {
|
|
422
|
+
test("disabling a feature blocks its endpoint server-side and re-enabling restores it @smoke", { tag: "@exclusive" }, async ({ playwright }) => {
|
|
430
423
|
test.slow();
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
test.skip(!caps?.magicLink, "magic link not enabled");
|
|
424
|
+
// Uses a fresh admin context (not the shared storageState) so it never rotates
|
|
425
|
+
// the seeded session. Toggles a flag and confirms the feature gate enforces it.
|
|
426
|
+
const admin = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
427
|
+
await admin.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
428
|
+
const caps = await (await admin.get("/api/account/capabilities")).json();
|
|
429
|
+
test.skip(!caps?.magicLink, "magic link not enabled");
|
|
438
430
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
431
|
+
// A disabled feature's endpoint 404s; the gate reads the flag through a short cache.
|
|
432
|
+
const status = (): Promise<number> =>
|
|
433
|
+
admin.post("/api/auth/sign-in/magic-link", { data: { email: "probe@example.com" } }).then((r) => r.status());
|
|
442
434
|
|
|
443
|
-
|
|
444
|
-
|
|
435
|
+
expect((await admin.put("/api/account/settings", { data: { magicLink: false } })).ok()).toBeTruthy();
|
|
436
|
+
await expect.poll(status, { timeout: 8000 }).toBe(404);
|
|
445
437
|
|
|
446
|
-
|
|
447
|
-
|
|
438
|
+
expect((await admin.put("/api/account/settings", { data: { magicLink: true } })).ok()).toBeTruthy();
|
|
439
|
+
await expect.poll(status, { timeout: 8000 }).not.toBe(404);
|
|
448
440
|
|
|
449
|
-
|
|
450
|
-
});
|
|
441
|
+
await admin.dispose();
|
|
451
442
|
});
|
|
452
443
|
|
|
453
444
|
test("sign-in with a wrong password is rejected", async ({ request }) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
|
-
import {
|
|
2
|
+
import { mailpitReachable, waitForLink, waitForOtp } from "../helpers/mailpit";
|
|
3
3
|
|
|
4
4
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
5
5
|
const origin = { origin: base };
|
|
@@ -11,7 +11,6 @@ test("email one-time code signs a user in", async ({ playwright }) => {
|
|
|
11
11
|
test.skip(!caps?.emailOtp, "email OTP not enabled");
|
|
12
12
|
const email = `otp-${Date.now()}@example.com`;
|
|
13
13
|
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Otp" } });
|
|
14
|
-
await clearMailpit();
|
|
15
14
|
const requested = await ctx.post("/api/auth/email-otp/send-verification-otp", { data: { email, type: "sign-in" } });
|
|
16
15
|
expect(requested.ok()).toBeTruthy();
|
|
17
16
|
const otp = await waitForOtp(email);
|
|
@@ -58,7 +57,6 @@ test("magic link signs a user in", async ({ playwright }) => {
|
|
|
58
57
|
// rotate the account's sessions, which would invalidate the seeded storageState.
|
|
59
58
|
const email = `magic-${Date.now()}@example.com`;
|
|
60
59
|
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Magic" } });
|
|
61
|
-
await clearMailpit();
|
|
62
60
|
const requested = await ctx.post("/api/auth/sign-in/magic-link", { data: { email, callbackURL: `${base}/admin` } });
|
|
63
61
|
expect(requested.ok()).toBeTruthy();
|
|
64
62
|
const link = await waitForLink(email);
|
|
@@ -84,8 +82,6 @@ test("password reset delivers an email and updates the password @smoke", async (
|
|
|
84
82
|
// With verification on, sign-up returns no session and the new password still
|
|
85
83
|
// can't sign in until confirmed — so only assert sign-in when it's off.
|
|
86
84
|
const verificationOn = !(await signup.json())?.token;
|
|
87
|
-
|
|
88
|
-
await clearMailpit();
|
|
89
85
|
const requested = await ctx.post("/api/auth/request-password-reset", { data: { email, redirectTo: `${base}/reset-password` } });
|
|
90
86
|
expect(requested.ok()).toBeTruthy();
|
|
91
87
|
|
|
@@ -106,7 +102,6 @@ test("email verification blocks sign-in until the address is confirmed", async (
|
|
|
106
102
|
test.skip(!(await mailpitReachable()), "Mailpit not available");
|
|
107
103
|
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
108
104
|
const email = `verify-${Date.now()}@example.com`;
|
|
109
|
-
await clearMailpit();
|
|
110
105
|
const signup = await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Verify", callbackURL: `${base}/admin` } });
|
|
111
106
|
expect(signup.ok()).toBeTruthy();
|
|
112
107
|
// When verification is off, sign-up returns a session — nothing to test here.
|
|
@@ -30,7 +30,7 @@ test("profile image upload requires authentication", async ({ playwright }) => {
|
|
|
30
30
|
await anonymous.dispose();
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
test("profile image upload, replacement, public read, and removal", async ({ playwright }) => {
|
|
33
|
+
test("profile image upload, replacement, public read, and removal", { tag: "@exclusive" }, async ({ playwright }) => {
|
|
34
34
|
const admin = await adminContext(playwright);
|
|
35
35
|
await admin.delete("/api/account/profile-image");
|
|
36
36
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { ADMIN } from "../helpers/accounts";
|
|
3
3
|
import { expectObjectLength } from "../helpers/object-response";
|
|
4
|
-
import { exclusive } from "../helpers/exclusive";
|
|
5
4
|
|
|
6
5
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
7
6
|
const origin = { origin: base };
|
|
@@ -18,36 +17,30 @@ async function adminCtx(playwright: import("@playwright/test").PlaywrightWorkerA
|
|
|
18
17
|
return ctx;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
test("theme settings: valid preset/radius/accent round-trip", async ({ playwright }) => {
|
|
22
|
-
|
|
23
|
-
await
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
});
|
|
28
|
-
expect(put.ok()).toBeTruthy();
|
|
20
|
+
test("theme settings: valid preset/radius/accent round-trip", { tag: "@exclusive" }, async ({ playwright }) => {
|
|
21
|
+
const admin = await adminCtx(playwright);
|
|
22
|
+
const put = await admin.put("/api/site/settings", {
|
|
23
|
+
data: { themePreset: "slate", themeRadius: "0.75", brandColor: "#2563eb" },
|
|
24
|
+
});
|
|
25
|
+
expect(put.ok()).toBeTruthy();
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
27
|
+
const anon = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
28
|
+
const settings = await (await anon.get("/api/site/settings")).json();
|
|
29
|
+
expect(settings.themePreset).toBe("slate");
|
|
30
|
+
expect(settings.themeRadius).toBe("0.75");
|
|
31
|
+
expect(settings.brandColor).toBe("#2563eb");
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
});
|
|
33
|
+
// restore
|
|
34
|
+
await admin.put("/api/site/settings", { data: { themePreset: "", themeRadius: "", brandColor: "" } });
|
|
35
|
+
await anon.dispose();
|
|
40
36
|
});
|
|
41
37
|
|
|
42
|
-
test("theme settings: valid per-token overrides accepted", async ({ playwright }) => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
expect(put.ok()).toBeTruthy();
|
|
49
|
-
await admin.put("/api/site/settings", { data: { themeOverrides: "" } }); // restore
|
|
50
|
-
});
|
|
38
|
+
test("theme settings: valid per-token overrides accepted", { tag: "@exclusive" }, async ({ playwright }) => {
|
|
39
|
+
const admin = await adminCtx(playwright);
|
|
40
|
+
const overrides = JSON.stringify({ light: { primary: "#123456" }, dark: { background: "#0a0a0a" } });
|
|
41
|
+
const put = await admin.put("/api/site/settings", { data: { themeOverrides: overrides } });
|
|
42
|
+
expect(put.ok()).toBeTruthy();
|
|
43
|
+
await admin.put("/api/site/settings", { data: { themeOverrides: "" } }); // restore
|
|
51
44
|
});
|
|
52
45
|
|
|
53
46
|
test("theme settings: invalid values are rejected (no CSS injection)", async ({ playwright }) => {
|
|
@@ -68,36 +61,33 @@ test("theme settings: invalid values are rejected (no CSS injection)", async ({
|
|
|
68
61
|
}
|
|
69
62
|
});
|
|
70
63
|
|
|
71
|
-
test("site settings: one response shape and immediate reads after each write", async ({ playwright }) => {
|
|
72
|
-
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
expect(typeof before.hasFavicon).toBe("boolean");
|
|
83
|
-
expect(before.faviconVersion === null || typeof before.faviconVersion === "string").toBe(true);
|
|
64
|
+
test("site settings: one response shape and immediate reads after each write", { tag: "@exclusive" }, async ({ playwright }) => {
|
|
65
|
+
const admin = await adminCtx(playwright);
|
|
66
|
+
const anon = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
67
|
+
const before = await (await anon.get("/api/site/settings")).json() as Record<string, unknown>;
|
|
68
|
+
expect(Object.keys(before)).toEqual([
|
|
69
|
+
"name", "description", "supportEmail", "footerText", "brandColor", "themePreset", "themeRadius",
|
|
70
|
+
"themeOverrides", "termsUrl", "privacyUrl", "locale", "timezone", "maintenanceMode", "allowSignup",
|
|
71
|
+
"hasFavicon", "faviconVersion",
|
|
72
|
+
]);
|
|
73
|
+
expect(typeof before.hasFavicon).toBe("boolean");
|
|
74
|
+
expect(before.faviconVersion === null || typeof before.faviconVersion === "string").toBe(true);
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
} finally {
|
|
92
|
-
const footerText = typeof before.footerText === "string" ? before.footerText : "";
|
|
93
|
-
await admin.put("/api/site/settings", { data: { footerText } });
|
|
94
|
-
await anon.dispose();
|
|
95
|
-
await admin.dispose();
|
|
76
|
+
try {
|
|
77
|
+
// The second write lands while the first read is still cached; it must not be hidden.
|
|
78
|
+
for (const footerText of ["first footer", "second footer"]) {
|
|
79
|
+
expect((await admin.put("/api/site/settings", { data: { footerText } })).ok()).toBeTruthy();
|
|
80
|
+
expect((await (await anon.get("/api/site/settings")).json()).footerText).toBe(footerText);
|
|
96
81
|
}
|
|
97
|
-
}
|
|
82
|
+
} finally {
|
|
83
|
+
const footerText = typeof before.footerText === "string" ? before.footerText : "";
|
|
84
|
+
await admin.put("/api/site/settings", { data: { footerText } });
|
|
85
|
+
await anon.dispose();
|
|
86
|
+
await admin.dispose();
|
|
87
|
+
}
|
|
98
88
|
});
|
|
99
89
|
|
|
100
|
-
test("site favicon: the versioned URL is cached for good and other URLs revalidate", async ({ playwright }) => {
|
|
90
|
+
test("site favicon: the versioned URL is cached for good and other URLs revalidate", { tag: "@exclusive" }, async ({ playwright }) => {
|
|
101
91
|
const admin = await adminCtx(playwright);
|
|
102
92
|
const anon = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
103
93
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "../helpers/disposable-users";
|
|
2
2
|
import { ADMIN, USER } from "../helpers/accounts";
|
|
3
|
+
import { signInOnSeparateDevices } from "../helpers/devices";
|
|
3
4
|
|
|
4
5
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
5
6
|
const origin = { origin: base };
|
|
@@ -41,11 +42,13 @@ test("admin can create a user, set its password, and remove it", async ({ playwr
|
|
|
41
42
|
await admin.dispose();
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
test("a normal user cannot create users (403)", async ({ playwright }) => {
|
|
45
|
+
test("a normal user cannot create users (403)", async ({ playwright, disposableUsers }) => {
|
|
45
46
|
const user = await signedIn(playwright, USER);
|
|
46
47
|
const res = await user.post("/api/auth/admin/create-user", {
|
|
47
48
|
data: { email: `nope-${Date.now()}@example.com`, password: "Podokit3e-Str0ng!pw", name: "Nope", role: "user" },
|
|
48
49
|
});
|
|
50
|
+
// Remove the account if the request wrongly succeeded, before failing on it.
|
|
51
|
+
if (res.ok()) await disposableUsers.trackResponse(res);
|
|
49
52
|
expect(res.status()).toBe(403);
|
|
50
53
|
await user.dispose();
|
|
51
54
|
});
|
|
@@ -125,6 +128,31 @@ test("revokeOtherSessions keeps the current session valid", async ({ playwright,
|
|
|
125
128
|
await ctx.dispose();
|
|
126
129
|
});
|
|
127
130
|
|
|
131
|
+
test("revokeOtherSessions removes every other session, past Better Auth's page of 100", async ({
|
|
132
|
+
playwright,
|
|
133
|
+
disposableUsers,
|
|
134
|
+
}) => {
|
|
135
|
+
test.slow();
|
|
136
|
+
const email = `api-revoke-many-${Date.now()}@example.com`;
|
|
137
|
+
const password = "Podokit3e-Str0ng!pw";
|
|
138
|
+
await disposableUsers.create({ email, password, name: "Many sessions" });
|
|
139
|
+
|
|
140
|
+
const statuses = await signInOnSeparateDevices(email, password, 105);
|
|
141
|
+
test.skip(statuses.includes(429), "needs an authentication rate limit above 105 sign-ins per window");
|
|
142
|
+
expect(statuses.every((status) => status === 200)).toBe(true);
|
|
143
|
+
|
|
144
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
145
|
+
expect((await ctx.post("/api/auth/sign-in/email", { data: { email, password } })).ok()).toBeTruthy();
|
|
146
|
+
// Better Auth lists at most 100 sessions, so a full page shows the backlog.
|
|
147
|
+
expect(((await (await ctx.get("/api/auth/list-sessions")).json()) as unknown[]).length).toBe(100);
|
|
148
|
+
|
|
149
|
+
expect((await ctx.post("/api/auth/revoke-other-sessions")).ok()).toBeTruthy();
|
|
150
|
+
const left = (await (await ctx.get("/api/auth/list-sessions")).json()) as Array<{ token?: string }>;
|
|
151
|
+
expect(left).toHaveLength(1);
|
|
152
|
+
expect((await (await ctx.get("/api/auth/get-session")).json())?.user?.email).toBe(email);
|
|
153
|
+
await ctx.dispose();
|
|
154
|
+
});
|
|
155
|
+
|
|
128
156
|
test("sessions record a client IP (forwarded through the proxy)", async ({ playwright }) => {
|
|
129
157
|
const ctx = await signedIn(playwright, ADMIN);
|
|
130
158
|
const res = await ctx.get("/api/auth/list-sessions");
|
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import { appAccounts } from "./app-accounts";
|
|
2
|
+
|
|
1
3
|
export type Account = { name: string; email: string; password: string };
|
|
2
4
|
|
|
5
|
+
const DEFAULT_PASSWORD = "Podokit3e-Str0ng!pw";
|
|
6
|
+
|
|
7
|
+
function fromEnv(name: string): string | undefined {
|
|
8
|
+
const value = process.env[name];
|
|
9
|
+
return value !== undefined && value.length > 0 ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
/**
|
|
4
|
-
* The
|
|
13
|
+
* The seeded accounts the suite signs in as. Each value comes from, in order,
|
|
14
|
+
* an `E2E_*` environment variable, the application-owned `app-accounts.ts`, and
|
|
15
|
+
* the generated default.
|
|
5
16
|
*
|
|
6
17
|
* A test must never change an existing account's credentials. `seed.setup.ts` signs up
|
|
7
18
|
* idempotently and then signs in with these values, which is right on a stack the suite
|
|
@@ -19,13 +30,20 @@ export type Account = { name: string; email: string; password: string };
|
|
|
19
30
|
* Without the override the run fails with `INVALID_EMAIL_OR_PASSWORD`, which is the correct
|
|
20
31
|
* outcome — a refusal, not a silent overwrite. Do not commit a real value here.
|
|
21
32
|
*/
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
function seededAccount(
|
|
34
|
+
role: "ADMIN" | "USER",
|
|
35
|
+
configured: Partial<Account> | undefined,
|
|
36
|
+
defaults: Pick<Account, "name" | "email">,
|
|
37
|
+
): Account {
|
|
38
|
+
return {
|
|
39
|
+
name: configured?.name ?? defaults.name,
|
|
40
|
+
email: fromEnv(`E2E_${role}_EMAIL`) ?? configured?.email ?? defaults.email,
|
|
41
|
+
password: fromEnv(`E2E_${role}_PASSWORD`) ?? configured?.password ?? DEFAULT_PASSWORD,
|
|
42
|
+
};
|
|
25
43
|
}
|
|
26
44
|
|
|
27
|
-
export const ADMIN = { name: "Admin", email: "admin@example.com"
|
|
28
|
-
export const USER = { name: "Normal User", email: "user@example.com"
|
|
45
|
+
export const ADMIN = seededAccount("ADMIN", appAccounts.admin, { name: "Admin", email: "admin@example.com" });
|
|
46
|
+
export const USER = seededAccount("USER", appAccounts.user, { name: "Normal User", email: "user@example.com" });
|
|
29
47
|
export const adminState = "playwright/.auth/admin.json";
|
|
30
48
|
export const userState = "playwright/.auth/user.json";
|
|
31
49
|
export const userBaselineState = "playwright/.auth/user-baseline.json";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Account } from "./accounts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The seeded accounts' names, addresses, or password sources when this
|
|
5
|
+
* application's differ from the generated `admin@example.com` and
|
|
6
|
+
* `user@example.com`. PodoKit creates this file once and never overwrites it.
|
|
7
|
+
* `E2E_ADMIN_EMAIL`, `E2E_USER_EMAIL`, `E2E_ADMIN_PASSWORD`, and
|
|
8
|
+
* `E2E_USER_PASSWORD` still win over what is set here.
|
|
9
|
+
*
|
|
10
|
+
* The seed signs the accounts up, so the API must promote the administrator:
|
|
11
|
+
* list its address in `ADMIN_EMAILS`. For example, accounts named by the
|
|
12
|
+
* application's own variables:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* export const appAccounts: AppAccounts = {
|
|
16
|
+
* admin: { email: process.env.MYAPP_ADMIN_EMAIL, password: process.env.MYAPP_ADMIN_PASSWORD },
|
|
17
|
+
* };
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Never commit a real password here.
|
|
21
|
+
*/
|
|
22
|
+
export type AppAccounts = { admin?: Partial<Account>; user?: Partial<Account> };
|
|
23
|
+
|
|
24
|
+
export const appAccounts: AppAccounts = {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
2
|
+
|
|
3
|
+
/** The `name=value` of the session cookie a sign-in response sets. */
|
|
4
|
+
function sessionCookie(response: Response): string | undefined {
|
|
5
|
+
for (const header of response.headers.getSetCookie()) {
|
|
6
|
+
const [pair = ""] = header.split(";");
|
|
7
|
+
const name = pair.slice(0, pair.indexOf("="));
|
|
8
|
+
// Better Auth names it `better-auth.session_token`, with a `__Secure-`
|
|
9
|
+
// prefix on HTTPS; the multi-session plugin's cookies end in the token.
|
|
10
|
+
if (name.endsWith("session_token")) return pair;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function signIn(email: string, password: string, cookie?: string): Promise<Response> {
|
|
16
|
+
return fetch(`${base}/api/auth/sign-in/email`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { origin: base, "content-type": "application/json", ...(cookie ? { cookie } : {}) },
|
|
19
|
+
body: JSON.stringify({ email, password }),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Signs in `count` times, each as a separate device, and returns the response
|
|
25
|
+
* statuses.
|
|
26
|
+
*
|
|
27
|
+
* Sign-ins are authentication writes. From an anonymous client the rate
|
|
28
|
+
* limiter counts them against the per-address budget that every other spec's
|
|
29
|
+
* sign-ins share, so a hundred of them refused unrelated sign-ins elsewhere in
|
|
30
|
+
* the run. Only the first one is anonymous here; every later one carries the
|
|
31
|
+
* first sign-in's session cookie, so the limiter counts it against that
|
|
32
|
+
* session instead.
|
|
33
|
+
*
|
|
34
|
+
* A request carries that one cookie and nothing else: Better Auth's
|
|
35
|
+
* multi-session plugin deletes a same-user session that a sign-in carries in
|
|
36
|
+
* its multi-session cookies, so a client that kept every cookie would end with
|
|
37
|
+
* one session instead of `count`.
|
|
38
|
+
*/
|
|
39
|
+
export async function signInOnSeparateDevices(email: string, password: string, count: number): Promise<number[]> {
|
|
40
|
+
if (count < 1) return [];
|
|
41
|
+
const first = await signIn(email, password);
|
|
42
|
+
await first.body?.cancel();
|
|
43
|
+
const statuses = [first.status];
|
|
44
|
+
const cookie = sessionCookie(first);
|
|
45
|
+
if (first.status !== 200 || !cookie) return statuses;
|
|
46
|
+
for (let start = 1; start < count; start += 10) {
|
|
47
|
+
const batch = Array.from({ length: Math.min(10, count - start) }, async () => {
|
|
48
|
+
const response = await signIn(email, password, cookie);
|
|
49
|
+
await response.body?.cancel();
|
|
50
|
+
return response.status;
|
|
51
|
+
});
|
|
52
|
+
statuses.push(...(await Promise.all(batch)));
|
|
53
|
+
}
|
|
54
|
+
return statuses;
|
|
55
|
+
}
|
|
@@ -34,17 +34,19 @@ async function readCreatedUserId(response: CreatedUserResponse): Promise<string>
|
|
|
34
34
|
|
|
35
35
|
export const test = base.extend<{ disposableUsers: DisposableUsers }>({
|
|
36
36
|
disposableUsers: async ({ context, playwright }, use, testInfo) => {
|
|
37
|
-
// API projects do not inherit the UI
|
|
37
|
+
// API projects (`api` and its `api-exclusive` twin) do not inherit the UI
|
|
38
|
+
// project's admin storageState, while a
|
|
38
39
|
// second admin sign-in can invalidate the shared UI session. API tests sign in
|
|
39
40
|
// an isolated request context. UI tests copy the browser's admin cookies into
|
|
40
41
|
// their own context once, so a test that later signs its page in as another
|
|
41
42
|
// user does not turn cleanup into that user's (forbidden) requests.
|
|
43
|
+
const requestOnly = testInfo.project.name.replace(/-exclusive$/, "") === "api";
|
|
42
44
|
const request = await playwright.request.newContext({
|
|
43
45
|
baseURL,
|
|
44
46
|
extraHTTPHeaders: origin,
|
|
45
|
-
...(
|
|
47
|
+
...(requestOnly ? {} : { storageState: await context.storageState() }),
|
|
46
48
|
});
|
|
47
|
-
if (
|
|
49
|
+
if (requestOnly) {
|
|
48
50
|
const signIn = await request.post("/api/auth/sign-in/email", {
|
|
49
51
|
data: { email: ADMIN.email, password: ADMIN.password },
|
|
50
52
|
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Helpers for the local Mailpit catcher (docker-compose / CI service). Tests that
|
|
2
|
-
// exercise email flows skip when it isn't reachable (see MAILPIT_URL).
|
|
2
|
+
// exercise email flows skip when it isn't reachable (see MAILPIT_URL). Each test
|
|
3
|
+
// mails an address of its own and reads the newest message to it; the inbox is
|
|
4
|
+
// shared with tests on other workers, so no test empties it.
|
|
3
5
|
const MAILPIT = process.env.MAILPIT_URL ?? "http://localhost:8025";
|
|
4
6
|
|
|
5
7
|
export async function mailpitReachable(): Promise<boolean> {
|
|
@@ -10,14 +12,6 @@ export async function mailpitReachable(): Promise<boolean> {
|
|
|
10
12
|
}
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
export async function clearMailpit(): Promise<void> {
|
|
14
|
-
try {
|
|
15
|
-
await fetch(`${MAILPIT}/api/v1/messages`, { method: "DELETE" });
|
|
16
|
-
} catch {
|
|
17
|
-
/* ignore */
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
15
|
type MailpitList = { messages: Array<{ ID: string; To: Array<{ Address: string }> }> };
|
|
22
16
|
type MailpitMessage = { Text?: string; HTML?: string };
|
|
23
17
|
|