@fuzdev/fuz_app 0.1.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/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/actions/action_bridge.d.ts +65 -0
- package/dist/actions/action_bridge.d.ts.map +1 -0
- package/dist/actions/action_bridge.js +76 -0
- package/dist/actions/action_codegen.d.ts +97 -0
- package/dist/actions/action_codegen.d.ts.map +1 -0
- package/dist/actions/action_codegen.js +280 -0
- package/dist/actions/action_registry.d.ts +35 -0
- package/dist/actions/action_registry.d.ts.map +1 -0
- package/dist/actions/action_registry.js +83 -0
- package/dist/actions/action_spec.d.ts +169 -0
- package/dist/actions/action_spec.d.ts.map +1 -0
- package/dist/actions/action_spec.js +76 -0
- package/dist/auth/account_queries.d.ts +96 -0
- package/dist/auth/account_queries.d.ts.map +1 -0
- package/dist/auth/account_queries.js +172 -0
- package/dist/auth/account_routes.d.ts +86 -0
- package/dist/auth/account_routes.d.ts.map +1 -0
- package/dist/auth/account_routes.js +406 -0
- package/dist/auth/account_schema.d.ts +192 -0
- package/dist/auth/account_schema.d.ts.map +1 -0
- package/dist/auth/account_schema.js +105 -0
- package/dist/auth/admin_routes.d.ts +29 -0
- package/dist/auth/admin_routes.d.ts.map +1 -0
- package/dist/auth/admin_routes.js +193 -0
- package/dist/auth/api_token.d.ts +33 -0
- package/dist/auth/api_token.d.ts.map +1 -0
- package/dist/auth/api_token.js +36 -0
- package/dist/auth/api_token_queries.d.ts +80 -0
- package/dist/auth/api_token_queries.d.ts.map +1 -0
- package/dist/auth/api_token_queries.js +116 -0
- package/dist/auth/app_settings_queries.d.ts +33 -0
- package/dist/auth/app_settings_queries.d.ts.map +1 -0
- package/dist/auth/app_settings_queries.js +51 -0
- package/dist/auth/app_settings_routes.d.ts +27 -0
- package/dist/auth/app_settings_routes.d.ts.map +1 -0
- package/dist/auth/app_settings_routes.js +66 -0
- package/dist/auth/app_settings_schema.d.ts +35 -0
- package/dist/auth/app_settings_schema.d.ts.map +1 -0
- package/dist/auth/app_settings_schema.js +22 -0
- package/dist/auth/audit_log_queries.d.ts +90 -0
- package/dist/auth/audit_log_queries.d.ts.map +1 -0
- package/dist/auth/audit_log_queries.js +205 -0
- package/dist/auth/audit_log_routes.d.ts +33 -0
- package/dist/auth/audit_log_routes.d.ts.map +1 -0
- package/dist/auth/audit_log_routes.js +106 -0
- package/dist/auth/audit_log_schema.d.ts +259 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -0
- package/dist/auth/audit_log_schema.js +123 -0
- package/dist/auth/bearer_auth.d.ts +32 -0
- package/dist/auth/bearer_auth.d.ts.map +1 -0
- package/dist/auth/bearer_auth.js +90 -0
- package/dist/auth/bootstrap_account.d.ts +82 -0
- package/dist/auth/bootstrap_account.d.ts.map +1 -0
- package/dist/auth/bootstrap_account.js +97 -0
- package/dist/auth/bootstrap_routes.d.ts +74 -0
- package/dist/auth/bootstrap_routes.d.ts.map +1 -0
- package/dist/auth/bootstrap_routes.js +154 -0
- package/dist/auth/daemon_token.d.ts +49 -0
- package/dist/auth/daemon_token.d.ts.map +1 -0
- package/dist/auth/daemon_token.js +49 -0
- package/dist/auth/daemon_token_middleware.d.ts +93 -0
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -0
- package/dist/auth/daemon_token_middleware.js +167 -0
- package/dist/auth/ddl.d.ts +27 -0
- package/dist/auth/ddl.d.ts.map +1 -0
- package/dist/auth/ddl.js +111 -0
- package/dist/auth/deps.d.ts +52 -0
- package/dist/auth/deps.d.ts.map +1 -0
- package/dist/auth/deps.js +10 -0
- package/dist/auth/invite_queries.d.ts +68 -0
- package/dist/auth/invite_queries.d.ts.map +1 -0
- package/dist/auth/invite_queries.js +105 -0
- package/dist/auth/invite_routes.d.ts +18 -0
- package/dist/auth/invite_routes.d.ts.map +1 -0
- package/dist/auth/invite_routes.js +129 -0
- package/dist/auth/invite_schema.d.ts +51 -0
- package/dist/auth/invite_schema.d.ts.map +1 -0
- package/dist/auth/invite_schema.js +25 -0
- package/dist/auth/keyring.d.ts +87 -0
- package/dist/auth/keyring.d.ts.map +1 -0
- package/dist/auth/keyring.js +142 -0
- package/dist/auth/middleware.d.ts +40 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +64 -0
- package/dist/auth/migrations.d.ts +42 -0
- package/dist/auth/migrations.d.ts.map +1 -0
- package/dist/auth/migrations.js +79 -0
- package/dist/auth/password.d.ts +39 -0
- package/dist/auth/password.d.ts.map +1 -0
- package/dist/auth/password.js +25 -0
- package/dist/auth/password_argon2.d.ts +43 -0
- package/dist/auth/password_argon2.d.ts.map +1 -0
- package/dist/auth/password_argon2.js +76 -0
- package/dist/auth/permit_queries.d.ts +72 -0
- package/dist/auth/permit_queries.d.ts.map +1 -0
- package/dist/auth/permit_queries.js +116 -0
- package/dist/auth/request_context.d.ts +114 -0
- package/dist/auth/request_context.d.ts.map +1 -0
- package/dist/auth/request_context.js +176 -0
- package/dist/auth/require_keeper.d.ts +20 -0
- package/dist/auth/require_keeper.d.ts.map +1 -0
- package/dist/auth/require_keeper.js +35 -0
- package/dist/auth/role_schema.d.ts +69 -0
- package/dist/auth/role_schema.d.ts.map +1 -0
- package/dist/auth/role_schema.js +70 -0
- package/dist/auth/route_guards.d.ts +21 -0
- package/dist/auth/route_guards.d.ts.map +1 -0
- package/dist/auth/route_guards.js +32 -0
- package/dist/auth/session_cookie.d.ts +158 -0
- package/dist/auth/session_cookie.d.ts.map +1 -0
- package/dist/auth/session_cookie.js +135 -0
- package/dist/auth/session_lifecycle.d.ts +35 -0
- package/dist/auth/session_lifecycle.d.ts.map +1 -0
- package/dist/auth/session_lifecycle.js +27 -0
- package/dist/auth/session_middleware.d.ts +33 -0
- package/dist/auth/session_middleware.d.ts.map +1 -0
- package/dist/auth/session_middleware.js +62 -0
- package/dist/auth/session_queries.d.ts +135 -0
- package/dist/auth/session_queries.d.ts.map +1 -0
- package/dist/auth/session_queries.js +186 -0
- package/dist/auth/signup_routes.d.ts +32 -0
- package/dist/auth/signup_routes.d.ts.map +1 -0
- package/dist/auth/signup_routes.js +150 -0
- package/dist/cli/args.d.ts +48 -0
- package/dist/cli/args.d.ts.map +1 -0
- package/dist/cli/args.js +76 -0
- package/dist/cli/config.d.ts +48 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +77 -0
- package/dist/cli/daemon.d.ts +82 -0
- package/dist/cli/daemon.d.ts.map +1 -0
- package/dist/cli/daemon.js +149 -0
- package/dist/cli/help.d.ts +85 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +138 -0
- package/dist/cli/logger.d.ts +46 -0
- package/dist/cli/logger.d.ts.map +1 -0
- package/dist/cli/logger.js +48 -0
- package/dist/cli/util.d.ts +36 -0
- package/dist/cli/util.d.ts.map +1 -0
- package/dist/cli/util.js +50 -0
- package/dist/crypto.d.ts +13 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +19 -0
- package/dist/db/assert_row.d.ts +18 -0
- package/dist/db/assert_row.d.ts.map +1 -0
- package/dist/db/assert_row.js +24 -0
- package/dist/db/create_db.d.ts +38 -0
- package/dist/db/create_db.d.ts.map +1 -0
- package/dist/db/create_db.js +57 -0
- package/dist/db/db.d.ts +97 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +76 -0
- package/dist/db/db_pg.d.ts +21 -0
- package/dist/db/db_pg.d.ts.map +1 -0
- package/dist/db/db_pg.js +45 -0
- package/dist/db/db_pglite.d.ts +21 -0
- package/dist/db/db_pglite.d.ts.map +1 -0
- package/dist/db/db_pglite.js +28 -0
- package/dist/db/migrate.d.ts +67 -0
- package/dist/db/migrate.d.ts.map +1 -0
- package/dist/db/migrate.js +118 -0
- package/dist/db/pg_error.d.ts +16 -0
- package/dist/db/pg_error.d.ts.map +1 -0
- package/dist/db/pg_error.js +15 -0
- package/dist/db/query_deps.d.ts +14 -0
- package/dist/db/query_deps.d.ts.map +1 -0
- package/dist/db/query_deps.js +9 -0
- package/dist/db/sql_identifier.d.ts +27 -0
- package/dist/db/sql_identifier.d.ts.map +1 -0
- package/dist/db/sql_identifier.js +31 -0
- package/dist/db/status.d.ts +62 -0
- package/dist/db/status.d.ts.map +1 -0
- package/dist/db/status.js +116 -0
- package/dist/dev/setup.d.ts +159 -0
- package/dist/dev/setup.d.ts.map +1 -0
- package/dist/dev/setup.js +265 -0
- package/dist/env/dotenv.d.ts +25 -0
- package/dist/env/dotenv.d.ts.map +1 -0
- package/dist/env/dotenv.js +52 -0
- package/dist/env/load.d.ts +52 -0
- package/dist/env/load.d.ts.map +1 -0
- package/dist/env/load.js +79 -0
- package/dist/env/mask.d.ts +19 -0
- package/dist/env/mask.d.ts.map +1 -0
- package/dist/env/mask.js +26 -0
- package/dist/env/resolve.d.ts +126 -0
- package/dist/env/resolve.d.ts.map +1 -0
- package/dist/env/resolve.js +200 -0
- package/dist/hono_context.d.ts +48 -0
- package/dist/hono_context.d.ts.map +1 -0
- package/dist/hono_context.js +22 -0
- package/dist/http/common_routes.d.ts +52 -0
- package/dist/http/common_routes.d.ts.map +1 -0
- package/dist/http/common_routes.js +65 -0
- package/dist/http/db_routes.d.ts +57 -0
- package/dist/http/db_routes.d.ts.map +1 -0
- package/dist/http/db_routes.js +176 -0
- package/dist/http/error_schemas.d.ts +169 -0
- package/dist/http/error_schemas.d.ts.map +1 -0
- package/dist/http/error_schemas.js +178 -0
- package/dist/http/middleware_spec.d.ts +19 -0
- package/dist/http/middleware_spec.d.ts.map +1 -0
- package/dist/http/middleware_spec.js +9 -0
- package/dist/http/origin.d.ts +57 -0
- package/dist/http/origin.d.ts.map +1 -0
- package/dist/http/origin.js +207 -0
- package/dist/http/proxy.d.ts +112 -0
- package/dist/http/proxy.d.ts.map +1 -0
- package/dist/http/proxy.js +240 -0
- package/dist/http/route_spec.d.ts +197 -0
- package/dist/http/route_spec.d.ts.map +1 -0
- package/dist/http/route_spec.js +243 -0
- package/dist/http/schema_helpers.d.ts +64 -0
- package/dist/http/schema_helpers.d.ts.map +1 -0
- package/dist/http/schema_helpers.js +90 -0
- package/dist/http/surface.d.ts +132 -0
- package/dist/http/surface.d.ts.map +1 -0
- package/dist/http/surface.js +156 -0
- package/dist/http/surface_query.d.ts +77 -0
- package/dist/http/surface_query.d.ts.map +1 -0
- package/dist/http/surface_query.js +86 -0
- package/dist/rate_limiter.d.ts +94 -0
- package/dist/rate_limiter.d.ts.map +1 -0
- package/dist/rate_limiter.js +156 -0
- package/dist/realtime/sse.d.ts +80 -0
- package/dist/realtime/sse.d.ts.map +1 -0
- package/dist/realtime/sse.js +109 -0
- package/dist/realtime/sse_auth_guard.d.ts +93 -0
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -0
- package/dist/realtime/sse_auth_guard.js +111 -0
- package/dist/realtime/subscriber_registry.d.ts +85 -0
- package/dist/realtime/subscriber_registry.d.ts.map +1 -0
- package/dist/realtime/subscriber_registry.js +108 -0
- package/dist/runtime/deno.d.ts +21 -0
- package/dist/runtime/deno.d.ts.map +1 -0
- package/dist/runtime/deno.js +83 -0
- package/dist/runtime/deps.d.ts +113 -0
- package/dist/runtime/deps.d.ts.map +1 -0
- package/dist/runtime/deps.js +10 -0
- package/dist/runtime/fs.d.ts +15 -0
- package/dist/runtime/fs.d.ts.map +1 -0
- package/dist/runtime/fs.js +17 -0
- package/dist/runtime/mock.d.ts +81 -0
- package/dist/runtime/mock.d.ts.map +1 -0
- package/dist/runtime/mock.js +195 -0
- package/dist/runtime/node.d.ts +17 -0
- package/dist/runtime/node.d.ts.map +1 -0
- package/dist/runtime/node.js +117 -0
- package/dist/schema_meta.d.ts +16 -0
- package/dist/schema_meta.d.ts.map +1 -0
- package/dist/schema_meta.js +9 -0
- package/dist/sensitivity.d.ts +15 -0
- package/dist/sensitivity.d.ts.map +1 -0
- package/dist/sensitivity.js +9 -0
- package/dist/server/app_backend.d.ts +74 -0
- package/dist/server/app_backend.d.ts.map +1 -0
- package/dist/server/app_backend.js +39 -0
- package/dist/server/app_server.d.ts +201 -0
- package/dist/server/app_server.d.ts.map +1 -0
- package/dist/server/app_server.js +266 -0
- package/dist/server/env.d.ts +68 -0
- package/dist/server/env.d.ts.map +1 -0
- package/dist/server/env.js +95 -0
- package/dist/server/startup.d.ts +22 -0
- package/dist/server/startup.d.ts.map +1 -0
- package/dist/server/startup.js +48 -0
- package/dist/server/static.d.ts +39 -0
- package/dist/server/static.d.ts.map +1 -0
- package/dist/server/static.js +38 -0
- package/dist/server/validate_nginx.d.ts +34 -0
- package/dist/server/validate_nginx.d.ts.map +1 -0
- package/dist/server/validate_nginx.js +118 -0
- package/dist/testing/CLAUDE.md +3 -0
- package/dist/testing/admin_integration.d.ts +45 -0
- package/dist/testing/admin_integration.d.ts.map +1 -0
- package/dist/testing/admin_integration.js +840 -0
- package/dist/testing/adversarial_404.d.ts +15 -0
- package/dist/testing/adversarial_404.d.ts.map +1 -0
- package/dist/testing/adversarial_404.js +118 -0
- package/dist/testing/adversarial_headers.d.ts +36 -0
- package/dist/testing/adversarial_headers.d.ts.map +1 -0
- package/dist/testing/adversarial_headers.js +128 -0
- package/dist/testing/adversarial_input.d.ts +56 -0
- package/dist/testing/adversarial_input.d.ts.map +1 -0
- package/dist/testing/adversarial_input.js +494 -0
- package/dist/testing/app_server.d.ts +169 -0
- package/dist/testing/app_server.d.ts.map +1 -0
- package/dist/testing/app_server.js +240 -0
- package/dist/testing/assert_dev_env.d.ts +10 -0
- package/dist/testing/assert_dev_env.d.ts.map +1 -0
- package/dist/testing/assert_dev_env.js +13 -0
- package/dist/testing/assertions.d.ts +61 -0
- package/dist/testing/assertions.d.ts.map +1 -0
- package/dist/testing/assertions.js +96 -0
- package/dist/testing/attack_surface.d.ts +63 -0
- package/dist/testing/attack_surface.d.ts.map +1 -0
- package/dist/testing/attack_surface.js +224 -0
- package/dist/testing/audit_completeness.d.ts +29 -0
- package/dist/testing/audit_completeness.d.ts.map +1 -0
- package/dist/testing/audit_completeness.js +410 -0
- package/dist/testing/auth_apps.d.ts +55 -0
- package/dist/testing/auth_apps.d.ts.map +1 -0
- package/dist/testing/auth_apps.js +122 -0
- package/dist/testing/data_exposure.d.ts +62 -0
- package/dist/testing/data_exposure.d.ts.map +1 -0
- package/dist/testing/data_exposure.js +297 -0
- package/dist/testing/db.d.ts +111 -0
- package/dist/testing/db.d.ts.map +1 -0
- package/dist/testing/db.js +258 -0
- package/dist/testing/entities.d.ts +21 -0
- package/dist/testing/entities.d.ts.map +1 -0
- package/dist/testing/entities.js +42 -0
- package/dist/testing/error_coverage.d.ts +78 -0
- package/dist/testing/error_coverage.d.ts.map +1 -0
- package/dist/testing/error_coverage.js +135 -0
- package/dist/testing/integration.d.ts +37 -0
- package/dist/testing/integration.d.ts.map +1 -0
- package/dist/testing/integration.js +1139 -0
- package/dist/testing/integration_helpers.d.ts +107 -0
- package/dist/testing/integration_helpers.d.ts.map +1 -0
- package/dist/testing/integration_helpers.js +246 -0
- package/dist/testing/middleware.d.ts +125 -0
- package/dist/testing/middleware.d.ts.map +1 -0
- package/dist/testing/middleware.js +210 -0
- package/dist/testing/rate_limiting.d.ts +43 -0
- package/dist/testing/rate_limiting.d.ts.map +1 -0
- package/dist/testing/rate_limiting.js +216 -0
- package/dist/testing/round_trip.d.ts +37 -0
- package/dist/testing/round_trip.d.ts.map +1 -0
- package/dist/testing/round_trip.js +128 -0
- package/dist/testing/schema_generators.d.ts +33 -0
- package/dist/testing/schema_generators.d.ts.map +1 -0
- package/dist/testing/schema_generators.js +137 -0
- package/dist/testing/standard.d.ts +49 -0
- package/dist/testing/standard.d.ts.map +1 -0
- package/dist/testing/standard.js +16 -0
- package/dist/testing/stubs.d.ts +96 -0
- package/dist/testing/stubs.d.ts.map +1 -0
- package/dist/testing/stubs.js +192 -0
- package/dist/testing/surface_invariants.d.ts +189 -0
- package/dist/testing/surface_invariants.d.ts.map +1 -0
- package/dist/testing/surface_invariants.js +450 -0
- package/dist/ui/AccountSessions.svelte +75 -0
- package/dist/ui/AccountSessions.svelte.d.ts +19 -0
- package/dist/ui/AccountSessions.svelte.d.ts.map +1 -0
- package/dist/ui/AdminAccounts.svelte +107 -0
- package/dist/ui/AdminAccounts.svelte.d.ts +19 -0
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -0
- package/dist/ui/AdminAuditLog.svelte +144 -0
- package/dist/ui/AdminAuditLog.svelte.d.ts +4 -0
- package/dist/ui/AdminAuditLog.svelte.d.ts.map +1 -0
- package/dist/ui/AdminInvites.svelte +142 -0
- package/dist/ui/AdminInvites.svelte.d.ts +4 -0
- package/dist/ui/AdminInvites.svelte.d.ts.map +1 -0
- package/dist/ui/AdminOverview.svelte +337 -0
- package/dist/ui/AdminOverview.svelte.d.ts +4 -0
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -0
- package/dist/ui/AdminPermitHistory.svelte +61 -0
- package/dist/ui/AdminPermitHistory.svelte.d.ts +19 -0
- package/dist/ui/AdminPermitHistory.svelte.d.ts.map +1 -0
- package/dist/ui/AdminSessions.svelte +85 -0
- package/dist/ui/AdminSessions.svelte.d.ts +19 -0
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -0
- package/dist/ui/AdminSettings.svelte +32 -0
- package/dist/ui/AdminSettings.svelte.d.ts +19 -0
- package/dist/ui/AdminSettings.svelte.d.ts.map +1 -0
- package/dist/ui/AdminSurface.svelte +42 -0
- package/dist/ui/AdminSurface.svelte.d.ts +4 -0
- package/dist/ui/AdminSurface.svelte.d.ts.map +1 -0
- package/dist/ui/AppShell.svelte +93 -0
- package/dist/ui/AppShell.svelte.d.ts +20 -0
- package/dist/ui/AppShell.svelte.d.ts.map +1 -0
- package/dist/ui/BootstrapForm.svelte +105 -0
- package/dist/ui/BootstrapForm.svelte.d.ts +4 -0
- package/dist/ui/BootstrapForm.svelte.d.ts.map +1 -0
- package/dist/ui/ColumnLayout.svelte +46 -0
- package/dist/ui/ColumnLayout.svelte.d.ts +11 -0
- package/dist/ui/ColumnLayout.svelte.d.ts.map +1 -0
- package/dist/ui/ConfirmButton.svelte +125 -0
- package/dist/ui/ConfirmButton.svelte.d.ts +54 -0
- package/dist/ui/ConfirmButton.svelte.d.ts.map +1 -0
- package/dist/ui/Datatable.svelte +185 -0
- package/dist/ui/Datatable.svelte.d.ts +35 -0
- package/dist/ui/Datatable.svelte.d.ts.map +1 -0
- package/dist/ui/LoginForm.svelte +82 -0
- package/dist/ui/LoginForm.svelte.d.ts +8 -0
- package/dist/ui/LoginForm.svelte.d.ts.map +1 -0
- package/dist/ui/LogoutButton.svelte +36 -0
- package/dist/ui/LogoutButton.svelte.d.ts +10 -0
- package/dist/ui/LogoutButton.svelte.d.ts.map +1 -0
- package/dist/ui/MenuLink.svelte +35 -0
- package/dist/ui/MenuLink.svelte.d.ts +12 -0
- package/dist/ui/MenuLink.svelte.d.ts.map +1 -0
- package/dist/ui/OpenSignupToggle.svelte +36 -0
- package/dist/ui/OpenSignupToggle.svelte.d.ts +19 -0
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -0
- package/dist/ui/PopoverButton.svelte +136 -0
- package/dist/ui/PopoverButton.svelte.d.ts +63 -0
- package/dist/ui/PopoverButton.svelte.d.ts.map +1 -0
- package/dist/ui/SignupForm.svelte +117 -0
- package/dist/ui/SignupForm.svelte.d.ts +7 -0
- package/dist/ui/SignupForm.svelte.d.ts.map +1 -0
- package/dist/ui/SurfaceExplorer.svelte +287 -0
- package/dist/ui/SurfaceExplorer.svelte.d.ts +8 -0
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -0
- package/dist/ui/account_sessions_state.svelte.d.ts +15 -0
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -0
- package/dist/ui/account_sessions_state.svelte.js +45 -0
- package/dist/ui/admin_accounts_state.svelte.d.ts +19 -0
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -0
- package/dist/ui/admin_accounts_state.svelte.js +65 -0
- package/dist/ui/admin_invites_state.svelte.d.ts +19 -0
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -0
- package/dist/ui/admin_invites_state.svelte.js +71 -0
- package/dist/ui/admin_sessions_state.svelte.d.ts +18 -0
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -0
- package/dist/ui/admin_sessions_state.svelte.js +62 -0
- package/dist/ui/app_settings_state.svelte.d.ts +14 -0
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -0
- package/dist/ui/app_settings_state.svelte.js +44 -0
- package/dist/ui/audit_log_state.svelte.d.ts +40 -0
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -0
- package/dist/ui/audit_log_state.svelte.js +153 -0
- package/dist/ui/auth_state.svelte.d.ts +85 -0
- package/dist/ui/auth_state.svelte.d.ts.map +1 -0
- package/dist/ui/auth_state.svelte.js +238 -0
- package/dist/ui/datatable.d.ts +25 -0
- package/dist/ui/datatable.d.ts.map +1 -0
- package/dist/ui/datatable.js +9 -0
- package/dist/ui/enter_advance.d.ts +13 -0
- package/dist/ui/enter_advance.d.ts.map +1 -0
- package/dist/ui/enter_advance.js +30 -0
- package/dist/ui/loadable.svelte.d.ts +55 -0
- package/dist/ui/loadable.svelte.d.ts.map +1 -0
- package/dist/ui/loadable.svelte.js +75 -0
- package/dist/ui/popover.svelte.d.ts +137 -0
- package/dist/ui/popover.svelte.d.ts.map +1 -0
- package/dist/ui/popover.svelte.js +288 -0
- package/dist/ui/position_helpers.d.ts +27 -0
- package/dist/ui/position_helpers.d.ts.map +1 -0
- package/dist/ui/position_helpers.js +81 -0
- package/dist/ui/sidebar_state.svelte.d.ts +30 -0
- package/dist/ui/sidebar_state.svelte.d.ts.map +1 -0
- package/dist/ui/sidebar_state.svelte.js +39 -0
- package/dist/ui/table_state.svelte.d.ts +63 -0
- package/dist/ui/table_state.svelte.d.ts.map +1 -0
- package/dist/ui/table_state.svelte.js +117 -0
- package/dist/ui/ui_fetch.d.ts +29 -0
- package/dist/ui/ui_fetch.d.ts.map +1 -0
- package/dist/ui/ui_fetch.js +37 -0
- package/dist/ui/ui_format.d.ts +63 -0
- package/dist/ui/ui_format.d.ts.map +1 -0
- package/dist/ui/ui_format.js +196 -0
- package/package.json +121 -0
|
@@ -0,0 +1,1139 @@
|
|
|
1
|
+
import './assert_dev_env.js';
|
|
2
|
+
/**
|
|
3
|
+
* Standard integration test suite for fuz_app auth routes.
|
|
4
|
+
*
|
|
5
|
+
* `describe_standard_integration_tests` creates a composable test suite that
|
|
6
|
+
* exercises the full middleware stack (origin, session, bearer_auth, request_context)
|
|
7
|
+
* against a real PGlite database. Consumers call it with their route factory and
|
|
8
|
+
* session config — all auth route tests come for free.
|
|
9
|
+
*
|
|
10
|
+
* Tests use `stub_password_deps` (deterministic hashing, no Argon2 overhead).
|
|
11
|
+
* Login handlers call `verify_password(submitted, stored_hash)` which works because
|
|
12
|
+
* both hash and verify use the same stub logic.
|
|
13
|
+
*
|
|
14
|
+
* Rate limiters are disabled by default — tests make many login attempts and would
|
|
15
|
+
* trigger limits otherwise.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import { describe, test, assert, afterAll } from 'vitest';
|
|
20
|
+
import { AUTH_MIGRATION_NS } from '../auth/migrations.js';
|
|
21
|
+
import { create_test_app } from './app_server.js';
|
|
22
|
+
import { create_pglite_factory, create_describe_db, AUTH_INTEGRATION_TRUNCATE_TABLES, } from './db.js';
|
|
23
|
+
import { find_auth_route, assert_response_matches_spec, create_expired_test_cookie, assert_no_error_info_leakage, } from './integration_helpers.js';
|
|
24
|
+
import { RateLimiter } from '../rate_limiter.js';
|
|
25
|
+
import { run_migrations } from '../db/migrate.js';
|
|
26
|
+
import { ErrorCoverageCollector, assert_error_coverage, DEFAULT_INTEGRATION_ERROR_COVERAGE, } from './error_coverage.js';
|
|
27
|
+
/**
|
|
28
|
+
* Build `CreateTestAppOptions` from standard options plus a database.
|
|
29
|
+
*/
|
|
30
|
+
const build_test_app_options = (options, db) => ({
|
|
31
|
+
session_options: options.session_options,
|
|
32
|
+
create_route_specs: options.create_route_specs,
|
|
33
|
+
db,
|
|
34
|
+
app_options: options.app_options,
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Standard integration test suite for fuz_app auth routes.
|
|
38
|
+
*
|
|
39
|
+
* Exercises login/logout, cookie attributes, session security, session
|
|
40
|
+
* revocation, password change (incl. API token revocation), origin
|
|
41
|
+
* verification, bearer auth (incl. browser context rejection on mutations),
|
|
42
|
+
* token revocation, cross-account isolation, expired credential rejection,
|
|
43
|
+
* signup invite edge cases, and response body validation.
|
|
44
|
+
*
|
|
45
|
+
* Each test group asserts that required routes exist, failing with a descriptive
|
|
46
|
+
* message if the consumer's route specs are misconfigured.
|
|
47
|
+
*
|
|
48
|
+
* @param options - session config and route factory
|
|
49
|
+
*/
|
|
50
|
+
export const describe_standard_integration_tests = (options) => {
|
|
51
|
+
const init_schema = async (db) => {
|
|
52
|
+
await run_migrations(db, [AUTH_MIGRATION_NS]);
|
|
53
|
+
};
|
|
54
|
+
const factories = options.db_factories ?? [create_pglite_factory(init_schema)];
|
|
55
|
+
const describe_db = create_describe_db(factories, AUTH_INTEGRATION_TRUNCATE_TABLES);
|
|
56
|
+
describe_db('standard_integration', (get_db) => {
|
|
57
|
+
const { cookie_name } = options.session_options;
|
|
58
|
+
// Error coverage tracking across test groups
|
|
59
|
+
const error_collector = new ErrorCoverageCollector();
|
|
60
|
+
let captured_route_specs = null;
|
|
61
|
+
afterAll(() => {
|
|
62
|
+
if (captured_route_specs) {
|
|
63
|
+
// Scope coverage to auth-related routes that this suite exercises.
|
|
64
|
+
// Consumer-specific routes (tx runs, state, etc.) are not exercised
|
|
65
|
+
// by the standard suite and would dilute the coverage percentage.
|
|
66
|
+
const auth_suffixes = [
|
|
67
|
+
'/login',
|
|
68
|
+
'/logout',
|
|
69
|
+
'/verify',
|
|
70
|
+
'/sessions',
|
|
71
|
+
'/sessions/revoke-all',
|
|
72
|
+
'/tokens',
|
|
73
|
+
'/tokens/create',
|
|
74
|
+
'/password',
|
|
75
|
+
'/signup',
|
|
76
|
+
'/bootstrap',
|
|
77
|
+
];
|
|
78
|
+
const auth_routes = captured_route_specs.filter((s) => (auth_suffixes.some((suffix) => s.path.endsWith(suffix)) ||
|
|
79
|
+
s.path.includes('/sessions/:') ||
|
|
80
|
+
s.path.includes('/tokens/:')) &&
|
|
81
|
+
!(s.auth.type === 'role' && s.auth.role === 'admin'));
|
|
82
|
+
assert_error_coverage(error_collector, auth_routes.length > 0 ? auth_routes : captured_route_specs, {
|
|
83
|
+
min_coverage: DEFAULT_INTEGRATION_ERROR_COVERAGE,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// --- 1. Login/logout lifecycle ---
|
|
88
|
+
describe('login/logout lifecycle', () => {
|
|
89
|
+
test('login with correct credentials returns 200 with Set-Cookie', async () => {
|
|
90
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
91
|
+
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
92
|
+
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
93
|
+
const res = await test_app.app.request(login_route.path, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: {
|
|
96
|
+
host: 'localhost',
|
|
97
|
+
origin: 'http://localhost:5173',
|
|
98
|
+
'content-type': 'application/json',
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
username: test_app.backend.account.username,
|
|
102
|
+
password: 'test-password-123',
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
assert.strictEqual(res.status, 200);
|
|
106
|
+
const body = await res.json();
|
|
107
|
+
assert.strictEqual(body.ok, true);
|
|
108
|
+
const set_cookie = res.headers.get('set-cookie');
|
|
109
|
+
assert.ok(set_cookie, 'Expected Set-Cookie header');
|
|
110
|
+
assert.ok(set_cookie.includes(`${cookie_name}=`), `Expected ${cookie_name} cookie`);
|
|
111
|
+
});
|
|
112
|
+
test('login with wrong password returns 401', async () => {
|
|
113
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
114
|
+
captured_route_specs ??= test_app.route_specs;
|
|
115
|
+
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
116
|
+
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
117
|
+
const res = await test_app.app.request(login_route.path, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: {
|
|
120
|
+
host: 'localhost',
|
|
121
|
+
origin: 'http://localhost:5173',
|
|
122
|
+
'content-type': 'application/json',
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
username: test_app.backend.account.username,
|
|
126
|
+
password: 'wrong-password',
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
assert.strictEqual(res.status, 401);
|
|
130
|
+
error_collector.record(test_app.route_specs, 'POST', login_route.path, 401);
|
|
131
|
+
const body = await res.json();
|
|
132
|
+
assert.strictEqual(body.error, 'invalid_credentials');
|
|
133
|
+
});
|
|
134
|
+
test('login with nonexistent user returns 401', async () => {
|
|
135
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
136
|
+
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
137
|
+
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
138
|
+
const res = await test_app.app.request(login_route.path, {
|
|
139
|
+
method: 'POST',
|
|
140
|
+
headers: {
|
|
141
|
+
host: 'localhost',
|
|
142
|
+
origin: 'http://localhost:5173',
|
|
143
|
+
'content-type': 'application/json',
|
|
144
|
+
},
|
|
145
|
+
body: JSON.stringify({
|
|
146
|
+
username: 'nonexistent_user',
|
|
147
|
+
password: 'test-password-123',
|
|
148
|
+
}),
|
|
149
|
+
});
|
|
150
|
+
assert.strictEqual(res.status, 401);
|
|
151
|
+
error_collector.record(test_app.route_specs, 'POST', login_route.path, 401);
|
|
152
|
+
const body = await res.json();
|
|
153
|
+
assert.strictEqual(body.error, 'invalid_credentials');
|
|
154
|
+
});
|
|
155
|
+
test('login trims whitespace from username', async () => {
|
|
156
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
157
|
+
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
158
|
+
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
159
|
+
const res = await test_app.app.request(login_route.path, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: {
|
|
162
|
+
host: 'localhost',
|
|
163
|
+
origin: 'http://localhost:5173',
|
|
164
|
+
'content-type': 'application/json',
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
username: ` ${test_app.backend.account.username} `,
|
|
168
|
+
password: 'test-password-123',
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
assert.strictEqual(res.status, 200);
|
|
172
|
+
const body = await res.json();
|
|
173
|
+
assert.strictEqual(body.ok, true);
|
|
174
|
+
});
|
|
175
|
+
test('full cycle: login → verify → logout → verify fails', async () => {
|
|
176
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
177
|
+
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
178
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
179
|
+
const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
|
|
180
|
+
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
181
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
182
|
+
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
183
|
+
// Login
|
|
184
|
+
const login_res = await test_app.app.request(login_route.path, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: {
|
|
187
|
+
host: 'localhost',
|
|
188
|
+
origin: 'http://localhost:5173',
|
|
189
|
+
'content-type': 'application/json',
|
|
190
|
+
},
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
username: test_app.backend.account.username,
|
|
193
|
+
password: 'test-password-123',
|
|
194
|
+
}),
|
|
195
|
+
});
|
|
196
|
+
assert.strictEqual(login_res.status, 200);
|
|
197
|
+
// Extract cookie from Set-Cookie
|
|
198
|
+
const set_cookie = login_res.headers.get('set-cookie');
|
|
199
|
+
assert.ok(set_cookie);
|
|
200
|
+
const cookie_match = new RegExp(`${cookie_name}=([^;]+)`).exec(set_cookie);
|
|
201
|
+
assert.ok(cookie_match?.[1]);
|
|
202
|
+
const login_cookie = cookie_match[1];
|
|
203
|
+
const create_headers = () => ({
|
|
204
|
+
host: 'localhost',
|
|
205
|
+
origin: 'http://localhost:5173',
|
|
206
|
+
cookie: `${cookie_name}=${login_cookie}`,
|
|
207
|
+
});
|
|
208
|
+
// Verify works
|
|
209
|
+
const verify_res = await test_app.app.request(verify_route.path, {
|
|
210
|
+
headers: create_headers(),
|
|
211
|
+
});
|
|
212
|
+
assert.strictEqual(verify_res.status, 200);
|
|
213
|
+
// Logout
|
|
214
|
+
const logout_res = await test_app.app.request(logout_route.path, {
|
|
215
|
+
method: 'POST',
|
|
216
|
+
headers: create_headers(),
|
|
217
|
+
});
|
|
218
|
+
assert.strictEqual(logout_res.status, 200);
|
|
219
|
+
const logout_body = await logout_res.json();
|
|
220
|
+
assert.strictEqual(logout_body.ok, true);
|
|
221
|
+
assert.strictEqual(logout_body.username, test_app.backend.account.username, 'Logout response should include the username');
|
|
222
|
+
// Verify fails after logout (session revoked)
|
|
223
|
+
const verify_after = await test_app.app.request(verify_route.path, {
|
|
224
|
+
headers: create_headers(),
|
|
225
|
+
});
|
|
226
|
+
assert.strictEqual(verify_after.status, 401);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
// --- 1b. Login response body identity (account enumeration prevention) ---
|
|
230
|
+
describe('login response body identity', () => {
|
|
231
|
+
test('nonexistent user and wrong password responses are structurally identical', async () => {
|
|
232
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
233
|
+
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
234
|
+
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
235
|
+
const make_login = (username, password) => test_app.app.request(login_route.path, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers: {
|
|
238
|
+
host: 'localhost',
|
|
239
|
+
origin: 'http://localhost:5173',
|
|
240
|
+
'content-type': 'application/json',
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify({ username, password }),
|
|
243
|
+
});
|
|
244
|
+
// wrong password for existing user
|
|
245
|
+
const wrong_pw_res = await make_login(test_app.backend.account.username, 'wrong-password-999');
|
|
246
|
+
assert.strictEqual(wrong_pw_res.status, 401);
|
|
247
|
+
const wrong_pw_body = await wrong_pw_res.json();
|
|
248
|
+
// nonexistent user
|
|
249
|
+
const no_user_res = await make_login('nonexistent_user_xyz', 'any-password');
|
|
250
|
+
assert.strictEqual(no_user_res.status, 401);
|
|
251
|
+
const no_user_body = await no_user_res.json();
|
|
252
|
+
// same keys, same error code, no extra fields
|
|
253
|
+
const wrong_pw_keys = Object.keys(wrong_pw_body).sort();
|
|
254
|
+
const no_user_keys = Object.keys(no_user_body).sort();
|
|
255
|
+
assert.deepStrictEqual(wrong_pw_keys, no_user_keys, 'Response keys must be identical to prevent account enumeration');
|
|
256
|
+
assert.strictEqual(wrong_pw_body.error, no_user_body.error, 'Error codes must be identical');
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
// --- 2. Cookie attributes ---
|
|
260
|
+
describe('cookie attributes', () => {
|
|
261
|
+
test('session cookie has secure attributes', async () => {
|
|
262
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
263
|
+
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
264
|
+
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
265
|
+
const res = await test_app.app.request(login_route.path, {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: {
|
|
268
|
+
host: 'localhost',
|
|
269
|
+
origin: 'http://localhost:5173',
|
|
270
|
+
'content-type': 'application/json',
|
|
271
|
+
},
|
|
272
|
+
body: JSON.stringify({
|
|
273
|
+
username: test_app.backend.account.username,
|
|
274
|
+
password: 'test-password-123',
|
|
275
|
+
}),
|
|
276
|
+
});
|
|
277
|
+
assert.strictEqual(res.status, 200);
|
|
278
|
+
const set_cookie = res.headers.get('set-cookie');
|
|
279
|
+
assert.ok(set_cookie);
|
|
280
|
+
const lower = set_cookie.toLowerCase();
|
|
281
|
+
assert.ok(lower.includes('httponly'), 'Expected HttpOnly');
|
|
282
|
+
assert.ok(lower.includes('samesite=strict'), 'Expected SameSite=Strict');
|
|
283
|
+
assert.ok(lower.includes('secure'), 'Expected Secure');
|
|
284
|
+
assert.ok(lower.includes('path=/'), 'Expected Path=/');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
// --- 3. Session security ---
|
|
288
|
+
describe('session security', () => {
|
|
289
|
+
test('no cookie on protected route returns 401', async () => {
|
|
290
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
291
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
292
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
293
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
294
|
+
headers: { host: 'localhost' },
|
|
295
|
+
});
|
|
296
|
+
assert.strictEqual(res.status, 401);
|
|
297
|
+
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
298
|
+
});
|
|
299
|
+
test('corrupted cookie returns 401', async () => {
|
|
300
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
301
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
302
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
303
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
304
|
+
headers: {
|
|
305
|
+
host: 'localhost',
|
|
306
|
+
cookie: `${cookie_name}=random_garbage_value`,
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
assert.strictEqual(res.status, 401);
|
|
310
|
+
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
311
|
+
});
|
|
312
|
+
test('expired cookie returns 401', async () => {
|
|
313
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
314
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
315
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
316
|
+
const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
|
|
317
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
318
|
+
headers: {
|
|
319
|
+
host: 'localhost',
|
|
320
|
+
cookie: `${cookie_name}=${expired_cookie}`,
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
assert.strictEqual(res.status, 401);
|
|
324
|
+
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
// --- 4. Session revocation ---
|
|
328
|
+
describe('session revocation', () => {
|
|
329
|
+
test('revoke single session by ID invalidates that session', async () => {
|
|
330
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
331
|
+
const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
|
|
332
|
+
const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
|
|
333
|
+
s.path.endsWith('/sessions/:id/revoke') &&
|
|
334
|
+
s.auth.type === 'authenticated');
|
|
335
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
336
|
+
assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
|
|
337
|
+
assert.ok(revoke_route, 'Expected POST /sessions/:id/revoke route — ensure create_route_specs includes account routes');
|
|
338
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
339
|
+
const headers = test_app.create_session_headers();
|
|
340
|
+
// List own sessions to get the session ID
|
|
341
|
+
const list_res = await test_app.app.request(sessions_route.path, { headers });
|
|
342
|
+
assert.strictEqual(list_res.status, 200);
|
|
343
|
+
const list_body = await list_res.json();
|
|
344
|
+
assert.ok(list_body.sessions.length >= 1);
|
|
345
|
+
const session_id = list_body.sessions[0].id;
|
|
346
|
+
// Revoke that session by ID
|
|
347
|
+
const revoke_path = revoke_route.path.replace(':id', session_id);
|
|
348
|
+
const revoke_res = await test_app.app.request(revoke_path, {
|
|
349
|
+
method: 'POST',
|
|
350
|
+
headers,
|
|
351
|
+
});
|
|
352
|
+
assert.strictEqual(revoke_res.status, 200);
|
|
353
|
+
const revoke_body = await revoke_res.json();
|
|
354
|
+
assert.strictEqual(revoke_body.ok, true);
|
|
355
|
+
assert.strictEqual(revoke_body.revoked, true);
|
|
356
|
+
// Session should no longer work
|
|
357
|
+
const after = await test_app.app.request(verify_route.path, { headers });
|
|
358
|
+
assert.strictEqual(after.status, 401);
|
|
359
|
+
});
|
|
360
|
+
test('revoke-all invalidates existing session', async () => {
|
|
361
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
362
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
363
|
+
const revoke_route = find_auth_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
|
|
364
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
365
|
+
assert.ok(revoke_route, 'Expected POST /sessions/revoke-all route — ensure create_route_specs includes account routes');
|
|
366
|
+
const headers = test_app.create_session_headers();
|
|
367
|
+
// Verify works
|
|
368
|
+
const before = await test_app.app.request(verify_route.path, { headers });
|
|
369
|
+
assert.strictEqual(before.status, 200);
|
|
370
|
+
// Revoke all sessions
|
|
371
|
+
const revoke_res = await test_app.app.request(revoke_route.path, {
|
|
372
|
+
method: 'POST',
|
|
373
|
+
headers,
|
|
374
|
+
});
|
|
375
|
+
assert.strictEqual(revoke_res.status, 200);
|
|
376
|
+
// Verify fails after revocation
|
|
377
|
+
const after = await test_app.app.request(verify_route.path, { headers });
|
|
378
|
+
assert.strictEqual(after.status, 401);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
// --- 4b. Password change ---
|
|
382
|
+
describe('password change', () => {
|
|
383
|
+
test('password change invalidates all sessions and allows login with new password', async () => {
|
|
384
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
385
|
+
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
386
|
+
const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
|
|
387
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
388
|
+
assert.ok(login_route, 'Expected POST /login route — ensure create_route_specs includes account routes');
|
|
389
|
+
assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
|
|
390
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
391
|
+
const headers = test_app.create_session_headers({
|
|
392
|
+
'content-type': 'application/json',
|
|
393
|
+
});
|
|
394
|
+
// Change password
|
|
395
|
+
const change_res = await test_app.app.request(password_route.path, {
|
|
396
|
+
method: 'POST',
|
|
397
|
+
headers,
|
|
398
|
+
body: JSON.stringify({
|
|
399
|
+
current_password: 'test-password-123',
|
|
400
|
+
new_password: 'new-password-456',
|
|
401
|
+
}),
|
|
402
|
+
});
|
|
403
|
+
assert.strictEqual(change_res.status, 200);
|
|
404
|
+
const change_body = await change_res.json();
|
|
405
|
+
assert.strictEqual(change_body.ok, true);
|
|
406
|
+
assert.ok(typeof change_body.sessions_revoked === 'number', 'Expected sessions_revoked count');
|
|
407
|
+
assert.ok(change_body.sessions_revoked >= 1, 'Expected at least 1 session revoked');
|
|
408
|
+
// Old session should be invalid
|
|
409
|
+
const verify_after = await test_app.app.request(verify_route.path, {
|
|
410
|
+
headers: test_app.create_session_headers(),
|
|
411
|
+
});
|
|
412
|
+
assert.strictEqual(verify_after.status, 401);
|
|
413
|
+
// Login with new password works
|
|
414
|
+
const login_res = await test_app.app.request(login_route.path, {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: {
|
|
417
|
+
host: 'localhost',
|
|
418
|
+
origin: 'http://localhost:5173',
|
|
419
|
+
'content-type': 'application/json',
|
|
420
|
+
},
|
|
421
|
+
body: JSON.stringify({
|
|
422
|
+
username: test_app.backend.account.username,
|
|
423
|
+
password: 'new-password-456',
|
|
424
|
+
}),
|
|
425
|
+
});
|
|
426
|
+
assert.strictEqual(login_res.status, 200);
|
|
427
|
+
const login_body = await login_res.json();
|
|
428
|
+
assert.strictEqual(login_body.ok, true);
|
|
429
|
+
});
|
|
430
|
+
test('password change with wrong current password returns 401', async () => {
|
|
431
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
432
|
+
const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
|
|
433
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
434
|
+
assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
|
|
435
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
436
|
+
const res = await test_app.app.request(password_route.path, {
|
|
437
|
+
method: 'POST',
|
|
438
|
+
headers: test_app.create_session_headers({
|
|
439
|
+
'content-type': 'application/json',
|
|
440
|
+
}),
|
|
441
|
+
body: JSON.stringify({
|
|
442
|
+
current_password: 'wrong-password-999',
|
|
443
|
+
new_password: 'new-password-456',
|
|
444
|
+
}),
|
|
445
|
+
});
|
|
446
|
+
assert.strictEqual(res.status, 401);
|
|
447
|
+
error_collector.record(test_app.route_specs, 'POST', password_route.path, 401);
|
|
448
|
+
// Session should still be valid (password didn't change)
|
|
449
|
+
const verify_res = await test_app.app.request(verify_route.path, {
|
|
450
|
+
headers: test_app.create_session_headers(),
|
|
451
|
+
});
|
|
452
|
+
assert.strictEqual(verify_res.status, 200);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
// --- 5. Origin verification ---
|
|
456
|
+
describe('origin verification', () => {
|
|
457
|
+
test('evil origin is rejected with 403', async () => {
|
|
458
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
459
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
460
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
461
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
462
|
+
headers: {
|
|
463
|
+
host: 'localhost',
|
|
464
|
+
origin: 'http://evil.com',
|
|
465
|
+
cookie: `${cookie_name}=${test_app.backend.session_cookie}`,
|
|
466
|
+
},
|
|
467
|
+
});
|
|
468
|
+
assert.strictEqual(res.status, 403);
|
|
469
|
+
const body = await res.json();
|
|
470
|
+
assert.strictEqual(body.error, 'forbidden_origin');
|
|
471
|
+
});
|
|
472
|
+
test('valid origin is accepted', async () => {
|
|
473
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
474
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
475
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
476
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
477
|
+
headers: test_app.create_session_headers(),
|
|
478
|
+
});
|
|
479
|
+
assert.strictEqual(res.status, 200);
|
|
480
|
+
});
|
|
481
|
+
test('no origin header is allowed (direct access)', async () => {
|
|
482
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
483
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
484
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
485
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
486
|
+
headers: {
|
|
487
|
+
host: 'localhost',
|
|
488
|
+
cookie: `${cookie_name}=${test_app.backend.session_cookie}`,
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
assert.notStrictEqual(res.status, 403);
|
|
492
|
+
});
|
|
493
|
+
});
|
|
494
|
+
// --- 6. Bearer auth ---
|
|
495
|
+
describe('bearer auth', () => {
|
|
496
|
+
test('valid bearer token authenticates', async () => {
|
|
497
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
498
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
499
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
500
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
501
|
+
headers: test_app.create_bearer_headers(),
|
|
502
|
+
});
|
|
503
|
+
assert.strictEqual(res.status, 200);
|
|
504
|
+
});
|
|
505
|
+
test('invalid bearer token returns 401', async () => {
|
|
506
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
507
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
508
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
509
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
510
|
+
headers: {
|
|
511
|
+
host: 'localhost',
|
|
512
|
+
authorization: 'Bearer secret_fuz_token_invalid',
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
assert.strictEqual(res.status, 401);
|
|
516
|
+
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
517
|
+
});
|
|
518
|
+
test('bearer token with Origin header is rejected', async () => {
|
|
519
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
520
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
521
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
522
|
+
const bearer_headers = test_app.create_bearer_headers();
|
|
523
|
+
// Without Origin — works
|
|
524
|
+
const ok_res = await test_app.app.request(verify_route.path, {
|
|
525
|
+
headers: bearer_headers,
|
|
526
|
+
});
|
|
527
|
+
assert.strictEqual(ok_res.status, 200);
|
|
528
|
+
// With Origin — rejected (browser context)
|
|
529
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
530
|
+
headers: {
|
|
531
|
+
...bearer_headers,
|
|
532
|
+
origin: 'http://localhost:5173',
|
|
533
|
+
},
|
|
534
|
+
});
|
|
535
|
+
assert.strictEqual(res.status, 403);
|
|
536
|
+
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 403);
|
|
537
|
+
const body = await res.json();
|
|
538
|
+
assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
// --- 7. Token revocation ---
|
|
542
|
+
describe('token revocation', () => {
|
|
543
|
+
test('revoked API token returns 401', async () => {
|
|
544
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
545
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
546
|
+
const create_token_route = find_auth_route(test_app.route_specs, '/tokens/create', 'POST');
|
|
547
|
+
const revoke_token_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/tokens/:id/revoke'));
|
|
548
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
549
|
+
assert.ok(create_token_route, 'Expected POST /tokens/create route — ensure create_route_specs includes account routes');
|
|
550
|
+
assert.ok(revoke_token_route, 'Expected POST /tokens/:id/revoke route — ensure create_route_specs includes account routes');
|
|
551
|
+
// Create a new token via the API
|
|
552
|
+
const create_res = await test_app.app.request(create_token_route.path, {
|
|
553
|
+
method: 'POST',
|
|
554
|
+
headers: {
|
|
555
|
+
...test_app.create_session_headers(),
|
|
556
|
+
'content-type': 'application/json',
|
|
557
|
+
},
|
|
558
|
+
body: JSON.stringify({ name: 'test-revoke' }),
|
|
559
|
+
});
|
|
560
|
+
assert.strictEqual(create_res.status, 200);
|
|
561
|
+
const { token, id } = (await create_res.json());
|
|
562
|
+
// Verify token works
|
|
563
|
+
const use_res = await test_app.app.request(verify_route.path, {
|
|
564
|
+
headers: { host: 'localhost', authorization: `Bearer ${token}` },
|
|
565
|
+
});
|
|
566
|
+
assert.strictEqual(use_res.status, 200);
|
|
567
|
+
// Revoke via HTTP
|
|
568
|
+
const revoke_path = revoke_token_route.path.replace(':id', id);
|
|
569
|
+
const revoke_res = await test_app.app.request(revoke_path, {
|
|
570
|
+
method: 'POST',
|
|
571
|
+
headers: test_app.create_session_headers(),
|
|
572
|
+
});
|
|
573
|
+
assert.strictEqual(revoke_res.status, 200);
|
|
574
|
+
// Token should no longer work
|
|
575
|
+
const after_res = await test_app.app.request(verify_route.path, {
|
|
576
|
+
headers: { host: 'localhost', authorization: `Bearer ${token}` },
|
|
577
|
+
});
|
|
578
|
+
assert.strictEqual(after_res.status, 401);
|
|
579
|
+
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
// --- 8. Cross-account isolation ---
|
|
583
|
+
describe('cross-account isolation', () => {
|
|
584
|
+
test('non-admin cannot access admin routes', async () => {
|
|
585
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
586
|
+
// admin routes are optional in the base suite — admin-specific coverage
|
|
587
|
+
// lives in describe_standard_admin_integration_tests
|
|
588
|
+
const admin_route = test_app.route_specs.find((s) => s.auth.type === 'role' && s.auth.role === 'admin');
|
|
589
|
+
if (!admin_route)
|
|
590
|
+
return;
|
|
591
|
+
const res = await test_app.app.request(admin_route.path, {
|
|
592
|
+
method: admin_route.method,
|
|
593
|
+
headers: test_app.create_session_headers(),
|
|
594
|
+
});
|
|
595
|
+
assert.strictEqual(res.status, 403);
|
|
596
|
+
const body = await res.json();
|
|
597
|
+
assert.strictEqual(body.error, 'insufficient_permissions');
|
|
598
|
+
});
|
|
599
|
+
test("user A cannot revoke user B's sessions", async () => {
|
|
600
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
601
|
+
const revoke_all_route = find_auth_route(test_app.route_specs, '/sessions/revoke-all', 'POST');
|
|
602
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
603
|
+
assert.ok(revoke_all_route, 'Expected POST /sessions/revoke-all route — ensure create_route_specs includes account routes');
|
|
604
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
605
|
+
// Create a second account
|
|
606
|
+
const user_b = await test_app.create_account({ username: 'user_b' });
|
|
607
|
+
// User A revokes all their own sessions
|
|
608
|
+
const revoke_res = await test_app.app.request(revoke_all_route.path, {
|
|
609
|
+
method: 'POST',
|
|
610
|
+
headers: test_app.create_session_headers(),
|
|
611
|
+
});
|
|
612
|
+
assert.strictEqual(revoke_res.status, 200);
|
|
613
|
+
// User B's session should still work
|
|
614
|
+
const verify_b = await test_app.app.request(verify_route.path, {
|
|
615
|
+
headers: {
|
|
616
|
+
host: 'localhost',
|
|
617
|
+
cookie: `${cookie_name}=${user_b.session_cookie}`,
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
assert.strictEqual(verify_b.status, 200);
|
|
621
|
+
});
|
|
622
|
+
test("user A cannot revoke user B's session by ID", async () => {
|
|
623
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
624
|
+
const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
|
|
625
|
+
const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
|
|
626
|
+
s.path.endsWith('/sessions/:id/revoke') &&
|
|
627
|
+
s.auth.type === 'authenticated');
|
|
628
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
629
|
+
assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
|
|
630
|
+
assert.ok(revoke_route, 'Expected POST /sessions/:id/revoke route — ensure create_route_specs includes account routes');
|
|
631
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
632
|
+
const user_b = await test_app.create_account({ username: 'user_b' });
|
|
633
|
+
const user_b_headers = {
|
|
634
|
+
host: 'localhost',
|
|
635
|
+
cookie: `${cookie_name}=${user_b.session_cookie}`,
|
|
636
|
+
};
|
|
637
|
+
// Get user B's session ID by listing as user B
|
|
638
|
+
const list_res = await test_app.app.request(sessions_route.path, {
|
|
639
|
+
headers: user_b_headers,
|
|
640
|
+
});
|
|
641
|
+
assert.strictEqual(list_res.status, 200);
|
|
642
|
+
const list_body = await list_res.json();
|
|
643
|
+
assert.ok(list_body.sessions.length >= 1);
|
|
644
|
+
const session_id_b = list_body.sessions[0].id;
|
|
645
|
+
// User A tries to revoke user B's session by ID
|
|
646
|
+
const revoke_path = revoke_route.path.replace(':id', session_id_b);
|
|
647
|
+
const revoke_res = await test_app.app.request(revoke_path, {
|
|
648
|
+
method: 'POST',
|
|
649
|
+
headers: test_app.create_session_headers(),
|
|
650
|
+
});
|
|
651
|
+
assert.strictEqual(revoke_res.status, 200);
|
|
652
|
+
const revoke_body = await revoke_res.json();
|
|
653
|
+
assert.strictEqual(revoke_body.revoked, false, 'Should not revoke another account session');
|
|
654
|
+
// User B's session should still work
|
|
655
|
+
const verify_b = await test_app.app.request(verify_route.path, {
|
|
656
|
+
headers: user_b_headers,
|
|
657
|
+
});
|
|
658
|
+
assert.strictEqual(verify_b.status, 200);
|
|
659
|
+
});
|
|
660
|
+
test("user A cannot revoke user B's token by ID", async () => {
|
|
661
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
662
|
+
const tokens_route = find_auth_route(test_app.route_specs, '/tokens', 'GET');
|
|
663
|
+
const revoke_route = test_app.route_specs.find((s) => s.method === 'POST' &&
|
|
664
|
+
s.path.endsWith('/tokens/:id/revoke') &&
|
|
665
|
+
s.auth.type === 'authenticated');
|
|
666
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
667
|
+
assert.ok(tokens_route, 'Expected GET /tokens route — ensure create_route_specs includes account routes');
|
|
668
|
+
assert.ok(revoke_route, 'Expected POST /tokens/:id/revoke route — ensure create_route_specs includes account routes');
|
|
669
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
670
|
+
const user_b = await test_app.create_account({ username: 'user_b' });
|
|
671
|
+
const user_b_headers = {
|
|
672
|
+
host: 'localhost',
|
|
673
|
+
cookie: `${cookie_name}=${user_b.session_cookie}`,
|
|
674
|
+
};
|
|
675
|
+
// Get user B's token ID by listing as user B
|
|
676
|
+
const list_res = await test_app.app.request(tokens_route.path, {
|
|
677
|
+
headers: user_b_headers,
|
|
678
|
+
});
|
|
679
|
+
assert.strictEqual(list_res.status, 200);
|
|
680
|
+
const list_body = await list_res.json();
|
|
681
|
+
assert.ok(list_body.tokens.length >= 1);
|
|
682
|
+
const token_id_b = list_body.tokens[0].id;
|
|
683
|
+
// User A tries to revoke user B's token by ID
|
|
684
|
+
const revoke_path = revoke_route.path.replace(':id', token_id_b);
|
|
685
|
+
const revoke_res = await test_app.app.request(revoke_path, {
|
|
686
|
+
method: 'POST',
|
|
687
|
+
headers: test_app.create_session_headers(),
|
|
688
|
+
});
|
|
689
|
+
assert.strictEqual(revoke_res.status, 200);
|
|
690
|
+
const revoke_body = await revoke_res.json();
|
|
691
|
+
assert.strictEqual(revoke_body.revoked, false, 'Should not revoke another account token');
|
|
692
|
+
// User B's bearer token should still work
|
|
693
|
+
const verify_b = await test_app.app.request(verify_route.path, {
|
|
694
|
+
headers: { host: 'localhost', authorization: `Bearer ${user_b.api_token}` },
|
|
695
|
+
});
|
|
696
|
+
assert.strictEqual(verify_b.status, 200);
|
|
697
|
+
});
|
|
698
|
+
test("user A's session list does not include user B's sessions", async () => {
|
|
699
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
700
|
+
const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
|
|
701
|
+
assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
|
|
702
|
+
const user_b = await test_app.create_account({ username: 'user_b' });
|
|
703
|
+
// User A lists sessions
|
|
704
|
+
const res = await test_app.app.request(sessions_route.path, {
|
|
705
|
+
headers: test_app.create_session_headers(),
|
|
706
|
+
});
|
|
707
|
+
assert.strictEqual(res.status, 200);
|
|
708
|
+
const body = await res.json();
|
|
709
|
+
// Sessions should only belong to user A's account
|
|
710
|
+
for (const session of body.sessions) {
|
|
711
|
+
assert.strictEqual(session.account_id, test_app.backend.account.id, `Session ${session.id} should belong to user A, not user B (${user_b.account.id})`);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
test("user A's token list does not include user B's tokens", async () => {
|
|
715
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
716
|
+
const tokens_route = find_auth_route(test_app.route_specs, '/tokens', 'GET');
|
|
717
|
+
assert.ok(tokens_route, 'Expected GET /tokens route — ensure create_route_specs includes account routes');
|
|
718
|
+
const user_b = await test_app.create_account({ username: 'user_b' });
|
|
719
|
+
// User A lists tokens
|
|
720
|
+
const res = await test_app.app.request(tokens_route.path, {
|
|
721
|
+
headers: test_app.create_session_headers(),
|
|
722
|
+
});
|
|
723
|
+
assert.strictEqual(res.status, 200);
|
|
724
|
+
const body = await res.json();
|
|
725
|
+
// Tokens should only belong to user A's account
|
|
726
|
+
for (const token of body.tokens) {
|
|
727
|
+
assert.strictEqual(token.account_id, test_app.backend.account.id, `Token ${token.id} should belong to user A, not user B (${user_b.account.id})`);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
// --- 9. Response body validation ---
|
|
732
|
+
describe('response body validation', () => {
|
|
733
|
+
test('401 response matches declared error schema', async () => {
|
|
734
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
735
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
736
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
737
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
738
|
+
headers: { host: 'localhost' },
|
|
739
|
+
});
|
|
740
|
+
assert.strictEqual(res.status, 401);
|
|
741
|
+
// Should not throw — body matches the declared error schema
|
|
742
|
+
await assert_response_matches_spec(test_app.route_specs, 'GET', verify_route.path, res);
|
|
743
|
+
});
|
|
744
|
+
test('GET /verify 200 response matches output schema', async () => {
|
|
745
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
746
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
747
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
748
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
749
|
+
headers: test_app.create_session_headers(),
|
|
750
|
+
});
|
|
751
|
+
assert.strictEqual(res.status, 200);
|
|
752
|
+
await assert_response_matches_spec(test_app.route_specs, 'GET', verify_route.path, res);
|
|
753
|
+
});
|
|
754
|
+
test('GET /sessions 200 response matches output schema', async () => {
|
|
755
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
756
|
+
const sessions_route = find_auth_route(test_app.route_specs, '/sessions', 'GET');
|
|
757
|
+
assert.ok(sessions_route, 'Expected GET /sessions route — ensure create_route_specs includes account routes');
|
|
758
|
+
const res = await test_app.app.request(sessions_route.path, {
|
|
759
|
+
headers: test_app.create_session_headers(),
|
|
760
|
+
});
|
|
761
|
+
assert.strictEqual(res.status, 200);
|
|
762
|
+
await assert_response_matches_spec(test_app.route_specs, 'GET', sessions_route.path, res);
|
|
763
|
+
});
|
|
764
|
+
test('GET /tokens 200 response matches output schema', async () => {
|
|
765
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
766
|
+
const tokens_route = find_auth_route(test_app.route_specs, '/tokens', 'GET');
|
|
767
|
+
assert.ok(tokens_route, 'Expected GET /tokens route — ensure create_route_specs includes account routes');
|
|
768
|
+
const res = await test_app.app.request(tokens_route.path, {
|
|
769
|
+
headers: test_app.create_session_headers(),
|
|
770
|
+
});
|
|
771
|
+
assert.strictEqual(res.status, 200);
|
|
772
|
+
await assert_response_matches_spec(test_app.route_specs, 'GET', tokens_route.path, res);
|
|
773
|
+
});
|
|
774
|
+
test('POST /tokens/create 200 response matches output schema', async () => {
|
|
775
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
776
|
+
const create_token_route = find_auth_route(test_app.route_specs, '/tokens/create', 'POST');
|
|
777
|
+
assert.ok(create_token_route, 'Expected POST /tokens/create route — ensure create_route_specs includes account routes');
|
|
778
|
+
const res = await test_app.app.request(create_token_route.path, {
|
|
779
|
+
method: 'POST',
|
|
780
|
+
headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
|
|
781
|
+
body: JSON.stringify({ name: 'schema-test' }),
|
|
782
|
+
});
|
|
783
|
+
assert.strictEqual(res.status, 200);
|
|
784
|
+
await assert_response_matches_spec(test_app.route_specs, 'POST', create_token_route.path, res);
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
// --- 10b. Rate limiting smoke test (full middleware stack) ---
|
|
788
|
+
describe('rate limiting smoke test', () => {
|
|
789
|
+
test('rate limiter fires in full middleware stack', async () => {
|
|
790
|
+
const test_app = await create_test_app({
|
|
791
|
+
...build_test_app_options(options, get_db()),
|
|
792
|
+
app_options: {
|
|
793
|
+
...options.app_options,
|
|
794
|
+
// tight limiter: 2 attempts / 1 minute
|
|
795
|
+
ip_rate_limiter: new RateLimiter({
|
|
796
|
+
max_attempts: 2,
|
|
797
|
+
window_ms: 60_000,
|
|
798
|
+
cleanup_interval_ms: 0,
|
|
799
|
+
}),
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
const login_route = find_auth_route(test_app.route_specs, '/login', 'POST');
|
|
803
|
+
if (!login_route)
|
|
804
|
+
return; // skip if login route not wired
|
|
805
|
+
const make_bad_login = (ip_header) => {
|
|
806
|
+
const headers = {
|
|
807
|
+
host: 'localhost',
|
|
808
|
+
origin: 'http://localhost:5173',
|
|
809
|
+
'content-type': 'application/json',
|
|
810
|
+
};
|
|
811
|
+
if (ip_header) {
|
|
812
|
+
headers['x-forwarded-for'] = ip_header;
|
|
813
|
+
}
|
|
814
|
+
return test_app.app.request(login_route.path, {
|
|
815
|
+
method: 'POST',
|
|
816
|
+
headers,
|
|
817
|
+
body: JSON.stringify({ username: 'nobody', password: 'wrong' }),
|
|
818
|
+
});
|
|
819
|
+
};
|
|
820
|
+
// exhaust the limiter (2 attempts)
|
|
821
|
+
await make_bad_login();
|
|
822
|
+
await make_bad_login();
|
|
823
|
+
// third attempt should be rate-limited
|
|
824
|
+
const limited_res = await make_bad_login();
|
|
825
|
+
assert.strictEqual(limited_res.status, 429, 'Expected 429 after exceeding rate limit');
|
|
826
|
+
error_collector.record(test_app.route_specs, 'POST', login_route.path, 429);
|
|
827
|
+
const limited_body = await limited_res.json();
|
|
828
|
+
assert.strictEqual(limited_body.error, 'rate_limit_exceeded');
|
|
829
|
+
// Retry-After header present
|
|
830
|
+
const retry_after = limited_res.headers.get('Retry-After');
|
|
831
|
+
assert.ok(retry_after, 'Expected Retry-After header on 429 response');
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
// --- 10c2. Error coverage: unauthenticated access to auth-required routes ---
|
|
835
|
+
describe('error coverage breadth', () => {
|
|
836
|
+
test('exercises 401 on multiple auth-required routes for error coverage', async () => {
|
|
837
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
838
|
+
// Hit several auth-required routes without credentials to broaden
|
|
839
|
+
// error coverage beyond just /verify and /login
|
|
840
|
+
const route_suffixes = ['/sessions', '/tokens', '/sessions/revoke-all', '/tokens/create'];
|
|
841
|
+
for (const suffix of route_suffixes) {
|
|
842
|
+
const route = find_auth_route(test_app.route_specs, suffix, suffix === '/tokens/create' || suffix === '/sessions/revoke-all' ? 'POST' : 'GET');
|
|
843
|
+
if (!route)
|
|
844
|
+
continue;
|
|
845
|
+
// eslint-disable-next-line no-await-in-loop
|
|
846
|
+
const res = await test_app.app.request(route.path, {
|
|
847
|
+
method: route.method,
|
|
848
|
+
headers: { host: 'localhost' },
|
|
849
|
+
});
|
|
850
|
+
if (res.status === 401) {
|
|
851
|
+
error_collector.record(test_app.route_specs, route.method, route.path, 401);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
// Also exercise POST /logout without auth
|
|
855
|
+
const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
|
|
856
|
+
if (logout_route) {
|
|
857
|
+
const res = await test_app.app.request(logout_route.path, {
|
|
858
|
+
method: 'POST',
|
|
859
|
+
headers: { host: 'localhost' },
|
|
860
|
+
});
|
|
861
|
+
if (res.status === 401) {
|
|
862
|
+
error_collector.record(test_app.route_specs, 'POST', logout_route.path, 401);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
// --- 10c. Error response information leakage ---
|
|
868
|
+
describe('error response information leakage', () => {
|
|
869
|
+
test('401 responses contain no leaky fields', async () => {
|
|
870
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
871
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
872
|
+
if (!verify_route)
|
|
873
|
+
return;
|
|
874
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
875
|
+
headers: { host: 'localhost' },
|
|
876
|
+
});
|
|
877
|
+
assert.strictEqual(res.status, 401);
|
|
878
|
+
const body = await res.json();
|
|
879
|
+
assert_no_error_info_leakage(body, `GET ${verify_route.path} 401`);
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
// --- 11. Expired credential rejection ---
|
|
883
|
+
describe('expired credential rejection', () => {
|
|
884
|
+
test('expired session cookie returns 401', async () => {
|
|
885
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
886
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
887
|
+
assert.ok(verify_route, 'Expected GET /verify route — ensure create_route_specs includes account routes');
|
|
888
|
+
const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
|
|
889
|
+
const res = await test_app.app.request(verify_route.path, {
|
|
890
|
+
headers: {
|
|
891
|
+
host: 'localhost',
|
|
892
|
+
cookie: `${cookie_name}=${expired_cookie}`,
|
|
893
|
+
},
|
|
894
|
+
});
|
|
895
|
+
assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected');
|
|
896
|
+
error_collector.record(test_app.route_specs, 'GET', verify_route.path, 401);
|
|
897
|
+
});
|
|
898
|
+
test('expired session cookie returns 401 on mutation route', async () => {
|
|
899
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
900
|
+
const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
|
|
901
|
+
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
902
|
+
const expired_cookie = await create_expired_test_cookie(test_app.backend.keyring, options.session_options);
|
|
903
|
+
const res = await test_app.app.request(logout_route.path, {
|
|
904
|
+
method: 'POST',
|
|
905
|
+
headers: {
|
|
906
|
+
host: 'localhost',
|
|
907
|
+
cookie: `${cookie_name}=${expired_cookie}`,
|
|
908
|
+
},
|
|
909
|
+
});
|
|
910
|
+
assert.strictEqual(res.status, 401, 'Expired session cookie should be rejected on POST');
|
|
911
|
+
error_collector.record(test_app.route_specs, 'POST', logout_route.path, 401);
|
|
912
|
+
});
|
|
913
|
+
});
|
|
914
|
+
// --- 12. Bearer token browser context on mutation routes ---
|
|
915
|
+
describe('bearer token browser context rejection on mutations', () => {
|
|
916
|
+
test('bearer token with Origin header rejected on POST logout', async () => {
|
|
917
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
918
|
+
const logout_route = find_auth_route(test_app.route_specs, '/logout', 'POST');
|
|
919
|
+
assert.ok(logout_route, 'Expected POST /logout route — ensure create_route_specs includes account routes');
|
|
920
|
+
const bearer_headers = test_app.create_bearer_headers({
|
|
921
|
+
'content-type': 'application/json',
|
|
922
|
+
});
|
|
923
|
+
const res = await test_app.app.request(logout_route.path, {
|
|
924
|
+
method: 'POST',
|
|
925
|
+
headers: { ...bearer_headers, origin: 'http://localhost:5173' },
|
|
926
|
+
});
|
|
927
|
+
assert.strictEqual(res.status, 403, 'Bearer with Origin should be rejected on mutation');
|
|
928
|
+
const body = await res.json();
|
|
929
|
+
assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
|
|
930
|
+
error_collector.record(test_app.route_specs, 'POST', logout_route.path, 403);
|
|
931
|
+
});
|
|
932
|
+
test('bearer token with Referer header rejected on POST password', async () => {
|
|
933
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
934
|
+
const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
|
|
935
|
+
assert.ok(password_route, 'Expected POST /password route — ensure create_route_specs includes account routes');
|
|
936
|
+
const bearer_headers = test_app.create_bearer_headers({
|
|
937
|
+
'content-type': 'application/json',
|
|
938
|
+
});
|
|
939
|
+
const res = await test_app.app.request(password_route.path, {
|
|
940
|
+
method: 'POST',
|
|
941
|
+
headers: { ...bearer_headers, referer: 'http://localhost:5173/admin' },
|
|
942
|
+
});
|
|
943
|
+
assert.strictEqual(res.status, 403, 'Bearer with Referer should be rejected on mutation');
|
|
944
|
+
const body = await res.json();
|
|
945
|
+
assert.strictEqual(body.error, 'bearer_token_rejected_in_browser_context');
|
|
946
|
+
error_collector.record(test_app.route_specs, 'POST', password_route.path, 403);
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
// --- 13. Password change revokes API tokens ---
|
|
950
|
+
describe('password change revokes API tokens', () => {
|
|
951
|
+
test('API tokens are invalidated after password change', async () => {
|
|
952
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
953
|
+
const token_create_route = find_auth_route(test_app.route_specs, '/tokens/create', 'POST');
|
|
954
|
+
const password_route = find_auth_route(test_app.route_specs, '/password', 'POST');
|
|
955
|
+
const verify_route = find_auth_route(test_app.route_specs, '/verify', 'GET');
|
|
956
|
+
assert.ok(token_create_route, 'Expected POST /tokens/create route');
|
|
957
|
+
assert.ok(password_route, 'Expected POST /password route');
|
|
958
|
+
assert.ok(verify_route, 'Expected GET /verify route');
|
|
959
|
+
// Create an API token
|
|
960
|
+
const create_res = await test_app.app.request(token_create_route.path, {
|
|
961
|
+
method: 'POST',
|
|
962
|
+
headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
|
|
963
|
+
body: JSON.stringify({ name: 'test-token' }),
|
|
964
|
+
});
|
|
965
|
+
assert.strictEqual(create_res.status, 200);
|
|
966
|
+
const { token: raw_token } = await create_res.json();
|
|
967
|
+
assert.ok(raw_token, 'Expected raw token in create response');
|
|
968
|
+
// Verify bearer token works
|
|
969
|
+
const verify_before = await test_app.app.request(verify_route.path, {
|
|
970
|
+
headers: { host: 'localhost', authorization: `Bearer ${raw_token}` },
|
|
971
|
+
});
|
|
972
|
+
assert.strictEqual(verify_before.status, 200, 'Bearer token should work before password change');
|
|
973
|
+
// Change password
|
|
974
|
+
const change_res = await test_app.app.request(password_route.path, {
|
|
975
|
+
method: 'POST',
|
|
976
|
+
headers: test_app.create_session_headers({ 'content-type': 'application/json' }),
|
|
977
|
+
body: JSON.stringify({
|
|
978
|
+
current_password: 'test-password-123',
|
|
979
|
+
new_password: 'new-password-456',
|
|
980
|
+
}),
|
|
981
|
+
});
|
|
982
|
+
assert.strictEqual(change_res.status, 200);
|
|
983
|
+
const change_body = await change_res.json();
|
|
984
|
+
assert.ok(typeof change_body.tokens_revoked === 'number', 'Expected tokens_revoked count');
|
|
985
|
+
assert.ok(change_body.tokens_revoked >= 1, 'Expected at least 1 token revoked');
|
|
986
|
+
// Bearer token should now be invalid
|
|
987
|
+
const verify_after = await test_app.app.request(verify_route.path, {
|
|
988
|
+
headers: { host: 'localhost', authorization: `Bearer ${raw_token}` },
|
|
989
|
+
});
|
|
990
|
+
assert.strictEqual(verify_after.status, 401, 'Bearer token should be rejected after password change');
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
// --- 14. Signup invite edge cases ---
|
|
994
|
+
describe('signup invite edge cases', () => {
|
|
995
|
+
test('signup with non-matching email cannot claim another email invite', async () => {
|
|
996
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
997
|
+
const signup_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && s.auth.type === 'none');
|
|
998
|
+
if (!signup_route)
|
|
999
|
+
return; // signup is optional
|
|
1000
|
+
const invite_route = test_app.route_specs.find((s) => s.method === 'POST' &&
|
|
1001
|
+
s.path.endsWith('/invites') &&
|
|
1002
|
+
s.auth.type === 'role' &&
|
|
1003
|
+
s.auth.role === 'admin');
|
|
1004
|
+
if (!invite_route)
|
|
1005
|
+
return; // invite routes are optional
|
|
1006
|
+
// Create an admin to manage invites
|
|
1007
|
+
const admin = await test_app.create_account({
|
|
1008
|
+
username: 'invite_edge_admin',
|
|
1009
|
+
roles: ['admin'],
|
|
1010
|
+
});
|
|
1011
|
+
const admin_headers = {
|
|
1012
|
+
host: 'localhost',
|
|
1013
|
+
origin: 'http://localhost:5173',
|
|
1014
|
+
cookie: `${cookie_name}=${admin.session_cookie}`,
|
|
1015
|
+
'content-type': 'application/json',
|
|
1016
|
+
};
|
|
1017
|
+
// Create invite for alice@example.com
|
|
1018
|
+
const invite_res = await test_app.app.request(invite_route.path, {
|
|
1019
|
+
method: 'POST',
|
|
1020
|
+
headers: admin_headers,
|
|
1021
|
+
body: JSON.stringify({ email: 'alice@example.com' }),
|
|
1022
|
+
});
|
|
1023
|
+
assert.strictEqual(invite_res.status, 200);
|
|
1024
|
+
// Try to sign up with a different email — should fail (no matching invite)
|
|
1025
|
+
const signup_res = await test_app.app.request(signup_route.path, {
|
|
1026
|
+
method: 'POST',
|
|
1027
|
+
headers: {
|
|
1028
|
+
host: 'localhost',
|
|
1029
|
+
origin: 'http://localhost:5173',
|
|
1030
|
+
'content-type': 'application/json',
|
|
1031
|
+
},
|
|
1032
|
+
body: JSON.stringify({
|
|
1033
|
+
username: 'eve_attacker',
|
|
1034
|
+
password: 'test-password-123456',
|
|
1035
|
+
email: 'eve@attacker.com',
|
|
1036
|
+
}),
|
|
1037
|
+
});
|
|
1038
|
+
assert.strictEqual(signup_res.status, 403, 'Signup with non-matching email should be rejected');
|
|
1039
|
+
const body = await signup_res.json();
|
|
1040
|
+
assert.strictEqual(body.error, 'no_matching_invite');
|
|
1041
|
+
});
|
|
1042
|
+
});
|
|
1043
|
+
// --- 15. Signup response body identity ---
|
|
1044
|
+
describe('signup response body identity', () => {
|
|
1045
|
+
test('no-invite and conflict failure responses are structurally identical', async () => {
|
|
1046
|
+
const test_app = await create_test_app(build_test_app_options(options, get_db()));
|
|
1047
|
+
// Find signup route (POST ending in /signup, public)
|
|
1048
|
+
const signup_route = test_app.route_specs.find((s) => s.method === 'POST' && s.path.endsWith('/signup') && s.auth.type === 'none');
|
|
1049
|
+
if (!signup_route)
|
|
1050
|
+
return; // signup is optional
|
|
1051
|
+
// Find admin invite creation route (POST ending in /invites, admin-gated)
|
|
1052
|
+
const invite_route = test_app.route_specs.find((s) => s.method === 'POST' &&
|
|
1053
|
+
s.path.endsWith('/invites') &&
|
|
1054
|
+
s.auth.type === 'role' &&
|
|
1055
|
+
s.auth.role === 'admin');
|
|
1056
|
+
if (!invite_route)
|
|
1057
|
+
return; // invite routes are optional
|
|
1058
|
+
// Find admin accounts route to get admin's account ID
|
|
1059
|
+
const accounts_route = test_app.route_specs.find((s) => s.method === 'GET' &&
|
|
1060
|
+
s.path.endsWith('/accounts') &&
|
|
1061
|
+
s.auth.type === 'role' &&
|
|
1062
|
+
s.auth.role === 'admin');
|
|
1063
|
+
if (!accounts_route)
|
|
1064
|
+
return;
|
|
1065
|
+
// We need admin access — create an admin account
|
|
1066
|
+
const admin = await test_app.create_account({
|
|
1067
|
+
username: 'signup_test_admin',
|
|
1068
|
+
roles: ['admin'],
|
|
1069
|
+
});
|
|
1070
|
+
const admin_headers = {
|
|
1071
|
+
host: 'localhost',
|
|
1072
|
+
origin: 'http://localhost:5173',
|
|
1073
|
+
cookie: `${cookie_name}=${admin.session_cookie}`,
|
|
1074
|
+
'content-type': 'application/json',
|
|
1075
|
+
};
|
|
1076
|
+
// Create an invite for a specific test email
|
|
1077
|
+
const test_email = 'signup-test@example.com';
|
|
1078
|
+
const invite_res = await test_app.app.request(invite_route.path, {
|
|
1079
|
+
method: 'POST',
|
|
1080
|
+
headers: admin_headers,
|
|
1081
|
+
body: JSON.stringify({ email: test_email }),
|
|
1082
|
+
});
|
|
1083
|
+
assert.strictEqual(invite_res.status, 200, 'Expected invite creation to succeed');
|
|
1084
|
+
// Attempt 1: signup with a non-matching email (no invite match) → 403
|
|
1085
|
+
const no_match_res = await test_app.app.request(signup_route.path, {
|
|
1086
|
+
method: 'POST',
|
|
1087
|
+
headers: {
|
|
1088
|
+
host: 'localhost',
|
|
1089
|
+
origin: 'http://localhost:5173',
|
|
1090
|
+
'content-type': 'application/json',
|
|
1091
|
+
},
|
|
1092
|
+
body: JSON.stringify({
|
|
1093
|
+
username: 'nomatch_user',
|
|
1094
|
+
password: 'test-password-123456',
|
|
1095
|
+
email: 'wrong-email@example.com',
|
|
1096
|
+
}),
|
|
1097
|
+
});
|
|
1098
|
+
assert.strictEqual(no_match_res.status, 403, 'Expected 403 for non-matching invite');
|
|
1099
|
+
const no_match_body = await no_match_res.json();
|
|
1100
|
+
// For conflict test: create a second account with a known username,
|
|
1101
|
+
// then create an invite for a different email, then try signup with
|
|
1102
|
+
// the invited email but the colliding username
|
|
1103
|
+
const existing_user = await test_app.create_account({ username: 'existing_user' });
|
|
1104
|
+
// Create invite for a different email
|
|
1105
|
+
const conflict_email = 'conflict-test@example.com';
|
|
1106
|
+
const invite2_res = await test_app.app.request(invite_route.path, {
|
|
1107
|
+
method: 'POST',
|
|
1108
|
+
headers: admin_headers,
|
|
1109
|
+
body: JSON.stringify({ email: conflict_email }),
|
|
1110
|
+
});
|
|
1111
|
+
assert.strictEqual(invite2_res.status, 200, 'Expected second invite creation to succeed');
|
|
1112
|
+
// Attempt 2: signup with the invited email but a colliding username → 409
|
|
1113
|
+
const conflict_res = await test_app.app.request(signup_route.path, {
|
|
1114
|
+
method: 'POST',
|
|
1115
|
+
headers: {
|
|
1116
|
+
host: 'localhost',
|
|
1117
|
+
origin: 'http://localhost:5173',
|
|
1118
|
+
'content-type': 'application/json',
|
|
1119
|
+
},
|
|
1120
|
+
body: JSON.stringify({
|
|
1121
|
+
username: existing_user.account.username,
|
|
1122
|
+
password: 'test-password-123456',
|
|
1123
|
+
email: conflict_email,
|
|
1124
|
+
}),
|
|
1125
|
+
});
|
|
1126
|
+
assert.strictEqual(conflict_res.status, 409, 'Expected 409 for username conflict');
|
|
1127
|
+
const conflict_body = await conflict_res.json();
|
|
1128
|
+
// Assert both failure responses have identical Object.keys()
|
|
1129
|
+
const no_match_keys = Object.keys(no_match_body).sort();
|
|
1130
|
+
const conflict_keys = Object.keys(conflict_body).sort();
|
|
1131
|
+
assert.deepStrictEqual(no_match_keys, conflict_keys, 'Response keys must be identical — no extra fields should reveal ' +
|
|
1132
|
+
'whether the failure was "no invite" vs "conflict"');
|
|
1133
|
+
// Assert both use documented generic error codes with no field-level detail
|
|
1134
|
+
assert.strictEqual(no_match_body.error, 'no_matching_invite', 'Expected generic no_matching_invite error code');
|
|
1135
|
+
assert.strictEqual(conflict_body.error, 'signup_conflict', 'Expected generic signup_conflict error code');
|
|
1136
|
+
});
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
};
|