@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,14 +1,12 @@
|
|
|
1
1
|
import { expect, test, type Page } from "@playwright/test";
|
|
2
2
|
import { anonState } from "../helpers/accounts";
|
|
3
3
|
import { ready } from "../helpers/hydration";
|
|
4
|
-
import {
|
|
4
|
+
import { paths } from "../helpers/paths";
|
|
5
5
|
|
|
6
6
|
// Verifies that the General settings actually take effect (not just persist).
|
|
7
7
|
// The admin storageState (project default) drives the toggles; a fresh anonymous
|
|
8
|
-
// context checks the public-facing result.
|
|
9
|
-
//
|
|
10
|
-
// 1; restores run in a `finally` so a mid-test failure still leaves the shared
|
|
11
|
-
// site settings clean.
|
|
8
|
+
// context checks the public-facing result. Restores run in a `finally` so a
|
|
9
|
+
// mid-test failure still leaves the shared site settings clean.
|
|
12
10
|
|
|
13
11
|
async function saveGeneral(
|
|
14
12
|
page: Page,
|
|
@@ -30,110 +28,103 @@ async function saveGeneral(
|
|
|
30
28
|
await expect(page.getByText("General settings saved.").last()).toBeVisible();
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
test("general: empty locale displays the application default", async ({ page }) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const settingsUrl = `${origin}/api/site/settings`;
|
|
39
|
-
const previous = await (await page.request.get(settingsUrl)).json() as { locale?: string };
|
|
31
|
+
test("general: empty locale displays the application default", { tag: "@exclusive" }, async ({ page }) => {
|
|
32
|
+
await ready(page, "/admin/settings");
|
|
33
|
+
const origin = new URL(page.url()).origin;
|
|
34
|
+
const settingsUrl = `${origin}/api/site/settings`;
|
|
35
|
+
const previous = await (await page.request.get(settingsUrl)).json() as { locale?: string };
|
|
40
36
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
});
|
|
37
|
+
try {
|
|
38
|
+
expect((await page.request.put(settingsUrl, { data: { locale: "" } })).ok()).toBeTruthy();
|
|
39
|
+
await ready(page, "/admin/settings");
|
|
40
|
+
await expect(page.locator("#locale")).toContainText("English");
|
|
41
|
+
} finally {
|
|
42
|
+
await page.request.put(settingsUrl, { data: { locale: previous.locale ?? "" } });
|
|
43
|
+
}
|
|
49
44
|
});
|
|
50
45
|
|
|
51
|
-
test("general: footer text and links render on public pages", async ({ page, browser }) => {
|
|
46
|
+
test("general: footer text and links render on public pages", { tag: "@exclusive" }, async ({ page, browser }) => {
|
|
47
|
+
test.skip(paths.footer === null, "no public page renders the site footer (tests/helpers/app-paths.ts)");
|
|
52
48
|
test.slow();
|
|
53
|
-
await
|
|
49
|
+
await ready(page, "/admin/settings");
|
|
50
|
+
const origin = new URL(page.url()).origin;
|
|
51
|
+
const footer = page.getByLabel("Footer text");
|
|
52
|
+
const support = page.getByLabel("Support email");
|
|
53
|
+
const origFooter = await footer.inputValue();
|
|
54
|
+
const origSupport = await support.inputValue();
|
|
55
|
+
await saveGeneral(
|
|
56
|
+
page,
|
|
57
|
+
async () => {
|
|
58
|
+
await footer.fill("© 2026 PodoKit Test");
|
|
59
|
+
await support.fill("help@example.com");
|
|
60
|
+
},
|
|
61
|
+
{ footerText: "© 2026 PodoKit Test", supportEmail: "help@example.com" },
|
|
62
|
+
);
|
|
63
|
+
try {
|
|
64
|
+
const ctx = await browser.newContext({ storageState: anonState });
|
|
65
|
+
const anon = await ctx.newPage();
|
|
66
|
+
await anon.goto(`${origin}${paths.footer}`);
|
|
67
|
+
const footerBlock = anon.getByTestId("site-footer");
|
|
68
|
+
await expect(footerBlock.getByText("© 2026 PodoKit Test")).toBeVisible();
|
|
69
|
+
await expect(footerBlock.getByRole("link", { name: "Support" })).toHaveAttribute("href", "mailto:help@example.com");
|
|
70
|
+
await ctx.close();
|
|
71
|
+
} finally {
|
|
54
72
|
await ready(page, "/admin/settings");
|
|
55
|
-
const origin = new URL(page.url()).origin;
|
|
56
|
-
const footer = page.getByLabel("Footer text");
|
|
57
|
-
const support = page.getByLabel("Support email");
|
|
58
|
-
const origFooter = await footer.inputValue();
|
|
59
|
-
const origSupport = await support.inputValue();
|
|
60
73
|
await saveGeneral(
|
|
61
74
|
page,
|
|
62
75
|
async () => {
|
|
63
|
-
await
|
|
64
|
-
await
|
|
76
|
+
await page.getByLabel("Footer text").fill(origFooter);
|
|
77
|
+
await page.getByLabel("Support email").fill(origSupport);
|
|
65
78
|
},
|
|
66
|
-
{ footerText:
|
|
79
|
+
{ footerText: origFooter, supportEmail: origSupport },
|
|
67
80
|
);
|
|
68
|
-
|
|
69
|
-
const ctx = await browser.newContext({ storageState: anonState });
|
|
70
|
-
const anon = await ctx.newPage();
|
|
71
|
-
await anon.goto(`${origin}/login`);
|
|
72
|
-
await expect(anon.getByText("© 2026 PodoKit Test")).toBeVisible();
|
|
73
|
-
await expect(anon.getByRole("link", { name: "Support" })).toHaveAttribute("href", "mailto:help@example.com");
|
|
74
|
-
await ctx.close();
|
|
75
|
-
} finally {
|
|
76
|
-
await ready(page, "/admin/settings");
|
|
77
|
-
await saveGeneral(
|
|
78
|
-
page,
|
|
79
|
-
async () => {
|
|
80
|
-
await page.getByLabel("Footer text").fill(origFooter);
|
|
81
|
-
await page.getByLabel("Support email").fill(origSupport);
|
|
82
|
-
},
|
|
83
|
-
{ footerText: origFooter, supportEmail: origSupport },
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
81
|
+
}
|
|
87
82
|
});
|
|
88
83
|
|
|
89
|
-
test("general: maintenance mode holds non-admins on the maintenance page", async ({ page, browser }) => {
|
|
84
|
+
test("general: maintenance mode holds non-admins on the maintenance page", { tag: "@exclusive" }, async ({ page, browser }) => {
|
|
90
85
|
test.slow();
|
|
91
|
-
await
|
|
86
|
+
await ready(page, "/admin/settings");
|
|
87
|
+
const origin = new URL(page.url()).origin;
|
|
88
|
+
await page.getByRole("switch", { name: "Maintenance mode" }).click();
|
|
89
|
+
await saveGeneral(page);
|
|
90
|
+
try {
|
|
91
|
+
const ctx = await browser.newContext({ storageState: anonState });
|
|
92
|
+
const anon = await ctx.newPage();
|
|
93
|
+
await anon.goto(`${origin}${paths.home}`);
|
|
94
|
+
await expect(anon).toHaveURL(/\/maintenance$/);
|
|
95
|
+
await expect(anon.getByRole("heading", { name: "Under maintenance" })).toBeVisible();
|
|
96
|
+
await ctx.close();
|
|
97
|
+
// the admin is not held back
|
|
98
|
+
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
|
99
|
+
} finally {
|
|
92
100
|
await ready(page, "/admin/settings");
|
|
93
|
-
const
|
|
94
|
-
await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const ctx = await browser.newContext({ storageState: anonState });
|
|
98
|
-
const anon = await ctx.newPage();
|
|
99
|
-
await anon.goto(`${origin}/`);
|
|
100
|
-
await expect(anon).toHaveURL(/\/maintenance$/);
|
|
101
|
-
await expect(anon.getByRole("heading", { name: "Under maintenance" })).toBeVisible();
|
|
102
|
-
await ctx.close();
|
|
103
|
-
// the admin is not held back
|
|
104
|
-
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
|
105
|
-
} finally {
|
|
106
|
-
await ready(page, "/admin/settings");
|
|
107
|
-
const t = page.getByRole("switch", { name: "Maintenance mode" });
|
|
108
|
-
if (await t.isChecked()) {
|
|
109
|
-
await t.click();
|
|
110
|
-
await saveGeneral(page);
|
|
111
|
-
}
|
|
101
|
+
const t = page.getByRole("switch", { name: "Maintenance mode" });
|
|
102
|
+
if (await t.isChecked()) {
|
|
103
|
+
await t.click();
|
|
104
|
+
await saveGeneral(page);
|
|
112
105
|
}
|
|
113
|
-
}
|
|
106
|
+
}
|
|
114
107
|
});
|
|
115
108
|
|
|
116
|
-
test("general: closing sign-up redirects /signup and hides the link", async ({ page, browser }) => {
|
|
109
|
+
test("general: closing sign-up redirects /signup and hides the link", { tag: "@exclusive" }, async ({ page, browser }) => {
|
|
117
110
|
test.slow();
|
|
118
|
-
await
|
|
111
|
+
await ready(page, "/admin/settings");
|
|
112
|
+
const origin = new URL(page.url()).origin;
|
|
113
|
+
await page.getByRole("switch", { name: "Allow public sign-up" }).click();
|
|
114
|
+
await saveGeneral(page);
|
|
115
|
+
try {
|
|
116
|
+
const ctx = await browser.newContext({ storageState: anonState });
|
|
117
|
+
const anon = await ctx.newPage();
|
|
118
|
+
await anon.goto(`${origin}/signup`);
|
|
119
|
+
await expect(anon).toHaveURL(/\/login/);
|
|
120
|
+
await expect(anon.getByRole("link", { name: "Sign up" })).toHaveCount(0);
|
|
121
|
+
await ctx.close();
|
|
122
|
+
} finally {
|
|
119
123
|
await ready(page, "/admin/settings");
|
|
120
|
-
const
|
|
121
|
-
await
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const ctx = await browser.newContext({ storageState: anonState });
|
|
125
|
-
const anon = await ctx.newPage();
|
|
126
|
-
await anon.goto(`${origin}/signup`);
|
|
127
|
-
await expect(anon).toHaveURL(/\/login/);
|
|
128
|
-
await expect(anon.getByRole("link", { name: "Sign up" })).toHaveCount(0);
|
|
129
|
-
await ctx.close();
|
|
130
|
-
} finally {
|
|
131
|
-
await ready(page, "/admin/settings");
|
|
132
|
-
const t = page.getByRole("switch", { name: "Allow public sign-up" });
|
|
133
|
-
if (!(await t.isChecked())) {
|
|
134
|
-
await t.click();
|
|
135
|
-
await saveGeneral(page);
|
|
136
|
-
}
|
|
124
|
+
const t = page.getByRole("switch", { name: "Allow public sign-up" });
|
|
125
|
+
if (!(await t.isChecked())) {
|
|
126
|
+
await t.click();
|
|
127
|
+
await saveGeneral(page);
|
|
137
128
|
}
|
|
138
|
-
}
|
|
129
|
+
}
|
|
139
130
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { anonState, userState } from "../helpers/accounts";
|
|
3
|
+
import { paths } from "../helpers/paths";
|
|
3
4
|
|
|
4
5
|
test.describe("unauthenticated", () => {
|
|
5
6
|
test.use({ storageState: anonState });
|
|
@@ -16,8 +17,11 @@ test.describe("unauthenticated", () => {
|
|
|
16
17
|
expect(response.headers()["x-robots-tag"]).toBe("noindex, nofollow");
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
// Public pages come from tests/helpers/app-paths.ts.
|
|
21
|
+
for (const route of paths.indexable) {
|
|
22
|
+
const response = await request.get(route);
|
|
23
|
+
expect(response.headers()["x-robots-tag"], route).toBeUndefined();
|
|
24
|
+
}
|
|
21
25
|
});
|
|
22
26
|
});
|
|
23
27
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { ADMIN, adminState, anonState } from "../helpers/accounts";
|
|
3
3
|
import { ready } from "../helpers/hydration";
|
|
4
|
-
import { exclusive } from "../helpers/exclusive";
|
|
5
4
|
|
|
6
5
|
test.use({ storageState: anonState });
|
|
7
6
|
|
|
@@ -23,27 +22,24 @@ test("language switch updates messages and <html lang>", async ({ page }) => {
|
|
|
23
22
|
}
|
|
24
23
|
});
|
|
25
24
|
|
|
26
|
-
test("explicit English overrides a Korean site fallback", async ({ page, playwright }) => {
|
|
27
|
-
|
|
28
|
-
await
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
const previous = await (await admin.get("/api/site/settings")).json() as { locale?: string };
|
|
25
|
+
test("explicit English overrides a Korean site fallback", { tag: "@exclusive" }, async ({ page, playwright }) => {
|
|
26
|
+
const admin = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
27
|
+
await admin.post("/api/auth/sign-in/email", {
|
|
28
|
+
data: { email: ADMIN.email, password: ADMIN.password },
|
|
29
|
+
});
|
|
30
|
+
const previous = await (await admin.get("/api/site/settings")).json() as { locale?: string };
|
|
34
31
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
try {
|
|
33
|
+
expect((await admin.put("/api/site/settings", { data: { locale: "ko" } })).ok()).toBeTruthy();
|
|
34
|
+
await page.context().addCookies([{ name: "locale", value: "en", url: base }]);
|
|
35
|
+
await ready(page, "/login");
|
|
39
36
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
});
|
|
37
|
+
await expect(page.locator("html")).toHaveAttribute("lang", "en");
|
|
38
|
+
await expect(page.getByRole("button", { name: "Sign in", exact: true })).toBeVisible();
|
|
39
|
+
} finally {
|
|
40
|
+
await admin.put("/api/site/settings", { data: { locale: previous.locale ?? "en" } });
|
|
41
|
+
await admin.dispose();
|
|
42
|
+
}
|
|
47
43
|
});
|
|
48
44
|
|
|
49
45
|
test("managed admin labels react to locale changes", async ({ browser }) => {
|
|
@@ -13,9 +13,12 @@ test("admin can impersonate a user and stop", async ({ page }) => {
|
|
|
13
13
|
await row.getByRole("button").click();
|
|
14
14
|
await page.getByRole("menuitem", { name: "Impersonate" }).click();
|
|
15
15
|
|
|
16
|
-
// Impersonating a normal user leaves the admin-only route group
|
|
17
|
-
//
|
|
18
|
-
|
|
16
|
+
// Impersonating a normal user leaves the admin-only route group. Where the
|
|
17
|
+
// application-owned users page lands is the application's choice; the managed
|
|
18
|
+
// account page always offers the route back to the administrator session.
|
|
19
|
+
await expect(page).not.toHaveURL(/\/admin\/users/);
|
|
20
|
+
await ready(page, "/account");
|
|
21
|
+
await expect(page.getByText(`Impersonating ${USER.email}`)).toBeVisible();
|
|
19
22
|
const stop = page.getByRole("button", { name: "Stop impersonating" });
|
|
20
23
|
await expect(stop).toBeVisible();
|
|
21
24
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
2
|
import { anonState } from "../helpers/accounts";
|
|
3
3
|
import { ready } from "../helpers/hydration";
|
|
4
|
-
import { mailpitReachable,
|
|
4
|
+
import { mailpitReachable, waitForOtp } from "../helpers/mailpit";
|
|
5
5
|
|
|
6
6
|
test.use({ storageState: anonState });
|
|
7
7
|
|
|
@@ -26,10 +26,9 @@ test("login: an emailed one-time code signs a user in", async ({ page, playwrigh
|
|
|
26
26
|
test.skip(!caps?.emailOtp, "email otp disabled");
|
|
27
27
|
test.skip(!(await mailpitReachable()), "mailpit unreachable");
|
|
28
28
|
|
|
29
|
-
const email = `otp-${Date.now()}@example.com`;
|
|
29
|
+
const email = `ui-otp-${Date.now()}@example.com`;
|
|
30
30
|
await api.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "OTP" } });
|
|
31
31
|
await api.dispose();
|
|
32
|
-
await clearMailpit();
|
|
33
32
|
|
|
34
33
|
await ready(page, "/login");
|
|
35
34
|
await page.locator("#email").fill(email);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { expect, test, type Page } from "@playwright/test";
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
-
import { exclusive } from "../helpers/exclusive";
|
|
4
3
|
import { ready } from "../helpers/hydration";
|
|
5
4
|
|
|
6
5
|
// admin storageState (project default) — the admin is the resource owner.
|
|
@@ -14,10 +13,9 @@ const REDIRECT = "https://example.com/callback";
|
|
|
14
13
|
// (with PKCE) that validates the client and hands off to the consent page. The
|
|
15
14
|
// back-channel token exchange is driven by an OIDC client SDK / real RP — see
|
|
16
15
|
// docs/modules.md for the manual round-trip checklist.
|
|
17
|
-
test("OIDC provider: discovery, JWKS, client registration, authorize handoff", async ({ page, baseURL }) => {
|
|
18
|
-
// tests/api/auth.api.spec.ts also toggles the provider; the lock keeps the two apart.
|
|
16
|
+
test("OIDC provider: discovery, JWKS, client registration, authorize handoff", { tag: "@exclusive" }, async ({ page, baseURL }) => {
|
|
19
17
|
test.slow();
|
|
20
|
-
await
|
|
18
|
+
await frontChannel(page, baseURL);
|
|
21
19
|
});
|
|
22
20
|
|
|
23
21
|
async function frontChannel(page: Page, baseURL: string | undefined): Promise<void> {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { USER } from "../helpers/accounts";
|
|
2
3
|
import { ready } from "../helpers/hydration";
|
|
3
4
|
|
|
4
5
|
// admin storageState (project default) — organizations is an admin-only view
|
|
@@ -38,7 +39,9 @@ test("create an org with a non-ascii name, a parent, and a manager", async ({ pa
|
|
|
38
39
|
await page.getByLabel("Name", { exact: true }).fill(child);
|
|
39
40
|
await page.locator("#o-parent").click();
|
|
40
41
|
await page.getByRole("option", { name: parent }).click();
|
|
41
|
-
|
|
42
|
+
// The seeded user, not the first listed one: tests on other workers add and
|
|
43
|
+
// remove users while this one runs.
|
|
44
|
+
await page.locator("label", { hasText: USER.email }).getByRole("checkbox").click();
|
|
42
45
|
await page.getByRole("dialog").getByRole("button", { name: "Create", exact: true }).click();
|
|
43
46
|
await expect(page.getByText("Organization created").first()).toBeVisible();
|
|
44
47
|
// child row shows the parent org name
|
|
@@ -64,7 +67,7 @@ test("organization manage: remove a member, invite one, and cancel the invite",
|
|
|
64
67
|
await page.getByLabel("Name", { exact: true }).fill(name);
|
|
65
68
|
// Seed a specific non-owner member (the admin is always the owner, so pick the
|
|
66
69
|
// seeded user — .first() is order-dependent and could hit the owner).
|
|
67
|
-
await page.locator("label", { hasText:
|
|
70
|
+
await page.locator("label", { hasText: USER.email }).getByRole("checkbox").click();
|
|
68
71
|
await page.getByRole("dialog").getByRole("button", { name: "Create", exact: true }).click();
|
|
69
72
|
await expect(page.getByText("Organization created").first()).toBeVisible();
|
|
70
73
|
|
|
@@ -29,9 +29,14 @@ test("register a passkey and sign in with it", async ({ page, baseURL }) => {
|
|
|
29
29
|
await addBtn.click();
|
|
30
30
|
await expect(page.getByText("Passkey added")).toBeVisible();
|
|
31
31
|
|
|
32
|
-
// Sign out, then sign back in using only the passkey.
|
|
33
|
-
|
|
32
|
+
// Sign out, then sign back in using only the passkey. Check the sign-out: a
|
|
33
|
+
// session left behind would pass the URL check below without the passkey.
|
|
34
|
+
const signOut = await page.context().request.post("/api/auth/sign-out", { headers: origin, data: {} });
|
|
35
|
+
expect(signOut.ok(), await signOut.text()).toBeTruthy();
|
|
36
|
+
expect(await (await page.context().request.get("/api/auth/get-session")).json()).toBeNull();
|
|
34
37
|
await ready(page, "/login");
|
|
35
38
|
await page.getByRole("button", { name: "Use a passkey" }).click();
|
|
36
39
|
await expect(page).toHaveURL(new URL("/", baseURL).toString());
|
|
40
|
+
const session = await (await page.context().request.get("/api/auth/get-session")).json();
|
|
41
|
+
expect(session?.user?.email).toBe(email);
|
|
37
42
|
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { expect } from "@playwright/test";
|
|
2
|
+
import { test } from "../helpers/disposable-users";
|
|
3
|
+
import { ready } from "../helpers/hydration";
|
|
4
|
+
|
|
5
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
6
|
+
const origin = new URL(base).origin;
|
|
7
|
+
// A 1 by 1 pixel PNG.
|
|
8
|
+
const PIXEL_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
|
|
9
|
+
|
|
10
|
+
// SvelteKit answers 403 "Cross-site ... form submissions are forbidden" to a POST,
|
|
11
|
+
// PUT, PATCH, or DELETE that has no content type or one an HTML form can send,
|
|
12
|
+
// unless its Origin header names the origin the web server resolved. The browser
|
|
13
|
+
// sends Origin with every same-origin request below, so FormData uploads and
|
|
14
|
+
// requests without a body reach the API through the /api proxy as they are. When
|
|
15
|
+
// a deployment returns that 403 anyway, the web server resolved another origin,
|
|
16
|
+
// such as http:// behind a TLS proxy whose X-Forwarded-Proto the ingress replaced.
|
|
17
|
+
test("same-origin uploads and requests without a body pass the origin check", async ({
|
|
18
|
+
browser,
|
|
19
|
+
playwright,
|
|
20
|
+
disposableUsers,
|
|
21
|
+
}) => {
|
|
22
|
+
const email = `same-origin-${Date.now()}@example.com`;
|
|
23
|
+
const password = "Same-origin-password-1";
|
|
24
|
+
await disposableUsers.create({ email, password, name: "Same origin" });
|
|
25
|
+
// Start without the project's administrator cookies: with the multi-session
|
|
26
|
+
// plugin, signing out ends every session the browser holds.
|
|
27
|
+
const signIn = await playwright.request.newContext({
|
|
28
|
+
baseURL: base,
|
|
29
|
+
extraHTTPHeaders: { origin },
|
|
30
|
+
storageState: { cookies: [], origins: [] },
|
|
31
|
+
});
|
|
32
|
+
expect((await signIn.post("/api/auth/sign-in/email", { data: { email, password } })).ok()).toBeTruthy();
|
|
33
|
+
const context = await browser.newContext({ storageState: await signIn.storageState() });
|
|
34
|
+
await signIn.dispose();
|
|
35
|
+
const page = await context.newPage();
|
|
36
|
+
try {
|
|
37
|
+
await ready(page, "/account");
|
|
38
|
+
const origins: Record<string, string | undefined> = {};
|
|
39
|
+
const recorded: Promise<void>[] = [];
|
|
40
|
+
page.on("request", (request) => {
|
|
41
|
+
const { pathname } = new URL(request.url());
|
|
42
|
+
if (!pathname.startsWith("/api/")) return;
|
|
43
|
+
recorded.push(request.allHeaders().then((headers) => {
|
|
44
|
+
origins[`${request.method()} ${pathname}`] = headers.origin;
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const results = await page.evaluate(async (png) => {
|
|
49
|
+
const bytes = Uint8Array.from(atob(png), (char) => char.charCodeAt(0));
|
|
50
|
+
const form = new FormData();
|
|
51
|
+
form.set("file", new Blob([bytes], { type: "image/png" }), "pixel.png");
|
|
52
|
+
const answer = async (response: Response): Promise<{ status: number; body: string }> => ({
|
|
53
|
+
status: response.status,
|
|
54
|
+
body: await response.text(),
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
upload: await answer(await fetch("/api/account/profile-image", { method: "POST", body: form })),
|
|
58
|
+
remove: await answer(await fetch("/api/account/profile-image", { method: "DELETE" })),
|
|
59
|
+
urlencoded: await answer(
|
|
60
|
+
await fetch("/api/account/profile-image", {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
63
|
+
body: "file=pixel",
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
signOut: await answer(await fetch("/api/auth/sign-out", { method: "POST" })),
|
|
67
|
+
};
|
|
68
|
+
}, PIXEL_PNG);
|
|
69
|
+
|
|
70
|
+
// The multipart upload and the DELETE without a body reached the API and succeeded.
|
|
71
|
+
expect(results.upload.status, results.upload.body).toBe(201);
|
|
72
|
+
expect(JSON.parse(results.upload.body)).toMatchObject({ image: expect.stringContaining("/api/") });
|
|
73
|
+
expect(results.remove.status, results.remove.body).toBe(200);
|
|
74
|
+
// A form-encoded body is not an image, so the API, not SvelteKit, refuses it.
|
|
75
|
+
expect(results.urlencoded.body).not.toContain("Cross-site");
|
|
76
|
+
expect(JSON.parse(results.urlencoded.body)).toMatchObject({ success: false });
|
|
77
|
+
// A POST without a body or content type signs the user out.
|
|
78
|
+
expect(results.signOut.status, results.signOut.body).toBe(200);
|
|
79
|
+
|
|
80
|
+
await Promise.all(recorded);
|
|
81
|
+
expect(origins).toMatchObject({
|
|
82
|
+
"POST /api/account/profile-image": origin,
|
|
83
|
+
"DELETE /api/account/profile-image": origin,
|
|
84
|
+
"POST /api/auth/sign-out": origin,
|
|
85
|
+
});
|
|
86
|
+
} finally {
|
|
87
|
+
await context.close();
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expect, test } from "../helpers/disposable-users";
|
|
2
|
+
import { USER } from "../helpers/accounts";
|
|
2
3
|
import { ready } from "../helpers/hydration";
|
|
3
4
|
|
|
4
5
|
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
@@ -36,14 +37,14 @@ test("admin sees user sessions across users", async ({ page }) => {
|
|
|
36
37
|
// Search by exact address — robust to pagination as sessions accumulate (the
|
|
37
38
|
// admin account alone can fill the first page). This is the across-users view.
|
|
38
39
|
const searched = page.waitForResponse(
|
|
39
|
-
(response) => isSessionList(response.url()) && new URL(response.url()).searchParams.get("search") ===
|
|
40
|
+
(response) => isSessionList(response.url()) && new URL(response.url()).searchParams.get("search") === USER.email,
|
|
40
41
|
);
|
|
41
|
-
await page.locator("#toolbar-search").fill(
|
|
42
|
+
await page.locator("#toolbar-search").fill(USER.email);
|
|
42
43
|
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
43
44
|
expect((await searched).ok()).toBeTruthy();
|
|
44
|
-
await expect(page.getByRole("main").getByText(
|
|
45
|
+
await expect(page.getByRole("main").getByText(USER.email).first()).toBeVisible();
|
|
45
46
|
// row actions are available
|
|
46
|
-
await page.getByRole("row", { name:
|
|
47
|
+
await page.getByRole("row", { name: USER.email }).first().getByRole("button").click();
|
|
47
48
|
await expect(page.getByRole("menuitem", { name: "Revoke session" })).toBeVisible();
|
|
48
49
|
await expect(page.getByRole("menuitem", { name: "Revoke all for user" })).toBeVisible();
|
|
49
50
|
});
|