palaryn 0.1.0 → 0.3.2
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 +243 -588
- package/dist/sdk/typescript/src/client.js +2 -2
- package/dist/sdk/typescript/src/client.js.map +1 -1
- package/dist/src/anomaly/detector.d.ts +7 -4
- package/dist/src/anomaly/detector.d.ts.map +1 -1
- package/dist/src/anomaly/detector.js +22 -12
- package/dist/src/anomaly/detector.js.map +1 -1
- package/dist/src/audit/logger.d.ts +10 -0
- package/dist/src/audit/logger.d.ts.map +1 -1
- package/dist/src/audit/logger.js +52 -38
- package/dist/src/audit/logger.js.map +1 -1
- package/dist/src/auth/routes.d.ts.map +1 -1
- package/dist/src/auth/routes.js +35 -0
- package/dist/src/auth/routes.js.map +1 -1
- package/dist/src/budget/manager.d.ts +5 -0
- package/dist/src/budget/manager.d.ts.map +1 -1
- package/dist/src/budget/manager.js +32 -0
- package/dist/src/budget/manager.js.map +1 -1
- package/dist/src/budget/model-pricing.d.ts +20 -0
- package/dist/src/budget/model-pricing.d.ts.map +1 -0
- package/dist/src/budget/model-pricing.js +107 -0
- package/dist/src/budget/model-pricing.js.map +1 -0
- package/dist/src/budget/usage-extractor.d.ts +3 -1
- package/dist/src/budget/usage-extractor.d.ts.map +1 -1
- package/dist/src/budget/usage-extractor.js +47 -3
- package/dist/src/budget/usage-extractor.js.map +1 -1
- package/dist/src/config/defaults.d.ts.map +1 -1
- package/dist/src/config/defaults.js +65 -13
- package/dist/src/config/defaults.js.map +1 -1
- package/dist/src/dlp/tool-patterns.d.ts +7 -0
- package/dist/src/dlp/tool-patterns.d.ts.map +1 -0
- package/dist/src/dlp/tool-patterns.js +34 -0
- package/dist/src/dlp/tool-patterns.js.map +1 -0
- package/dist/src/executor/filesystem-executor.d.ts +28 -0
- package/dist/src/executor/filesystem-executor.d.ts.map +1 -0
- package/dist/src/executor/filesystem-executor.js +192 -0
- package/dist/src/executor/filesystem-executor.js.map +1 -0
- package/dist/src/executor/http-executor.d.ts.map +1 -1
- package/dist/src/executor/http-executor.js +22 -2
- package/dist/src/executor/http-executor.js.map +1 -1
- package/dist/src/executor/index.d.ts +4 -0
- package/dist/src/executor/index.d.ts.map +1 -1
- package/dist/src/executor/index.js +9 -1
- package/dist/src/executor/index.js.map +1 -1
- package/dist/src/executor/shell-executor.d.ts +22 -0
- package/dist/src/executor/shell-executor.d.ts.map +1 -0
- package/dist/src/executor/shell-executor.js +119 -0
- package/dist/src/executor/shell-executor.js.map +1 -0
- package/dist/src/executor/sql-executor.d.ts +29 -0
- package/dist/src/executor/sql-executor.d.ts.map +1 -0
- package/dist/src/executor/sql-executor.js +114 -0
- package/dist/src/executor/sql-executor.js.map +1 -0
- package/dist/src/executor/websocket-executor.d.ts +26 -0
- package/dist/src/executor/websocket-executor.d.ts.map +1 -0
- package/dist/src/executor/websocket-executor.js +205 -0
- package/dist/src/executor/websocket-executor.js.map +1 -0
- package/dist/src/interceptor/index.d.ts +2 -0
- package/dist/src/interceptor/index.d.ts.map +1 -0
- package/dist/src/interceptor/index.js +6 -0
- package/dist/src/interceptor/index.js.map +1 -0
- package/dist/src/interceptor/provider-interceptor.d.ts +36 -0
- package/dist/src/interceptor/provider-interceptor.d.ts.map +1 -0
- package/dist/src/interceptor/provider-interceptor.js +302 -0
- package/dist/src/interceptor/provider-interceptor.js.map +1 -0
- package/dist/src/mcp/auth-verifier.d.ts.map +1 -1
- package/dist/src/mcp/auth-verifier.js +3 -2
- package/dist/src/mcp/auth-verifier.js.map +1 -1
- package/dist/src/mcp/bridge.d.ts +14 -10
- package/dist/src/mcp/bridge.d.ts.map +1 -1
- package/dist/src/mcp/bridge.js +51 -227
- package/dist/src/mcp/bridge.js.map +1 -1
- package/dist/src/mcp/http-transport.d.ts +2 -0
- package/dist/src/mcp/http-transport.d.ts.map +1 -1
- package/dist/src/mcp/http-transport.js +117 -66
- package/dist/src/mcp/http-transport.js.map +1 -1
- package/dist/src/mcp/internal-auth.d.ts +13 -0
- package/dist/src/mcp/internal-auth.d.ts.map +1 -0
- package/dist/src/mcp/internal-auth.js +12 -0
- package/dist/src/mcp/internal-auth.js.map +1 -0
- package/dist/src/mcp/tool-definitions.d.ts +41 -0
- package/dist/src/mcp/tool-definitions.d.ts.map +1 -0
- package/dist/src/mcp/tool-definitions.js +491 -0
- package/dist/src/mcp/tool-definitions.js.map +1 -0
- package/dist/src/middleware/auth.js.map +1 -1
- package/dist/src/middleware/session.js.map +1 -1
- package/dist/src/middleware/validate.d.ts +8 -0
- package/dist/src/middleware/validate.d.ts.map +1 -1
- package/dist/src/middleware/validate.js +45 -0
- package/dist/src/middleware/validate.js.map +1 -1
- package/dist/src/policy/engine.d.ts +4 -0
- package/dist/src/policy/engine.d.ts.map +1 -1
- package/dist/src/policy/engine.js +117 -0
- package/dist/src/policy/engine.js.map +1 -1
- package/dist/src/saas/routes.d.ts.map +1 -1
- package/dist/src/saas/routes.js +355 -22
- package/dist/src/saas/routes.js.map +1 -1
- package/dist/src/server/app.d.ts.map +1 -1
- package/dist/src/server/app.js +24 -3
- package/dist/src/server/app.js.map +1 -1
- package/dist/src/server/gateway.d.ts.map +1 -1
- package/dist/src/server/gateway.js +17 -0
- package/dist/src/server/gateway.js.map +1 -1
- package/dist/src/server/index.d.ts.map +1 -1
- package/dist/src/server/index.js +18 -0
- package/dist/src/server/index.js.map +1 -1
- package/dist/src/storage/interfaces.d.ts +14 -3
- package/dist/src/storage/interfaces.d.ts.map +1 -1
- package/dist/src/storage/memory.d.ts +2 -0
- package/dist/src/storage/memory.d.ts.map +1 -1
- package/dist/src/storage/memory.js +6 -0
- package/dist/src/storage/memory.js.map +1 -1
- package/dist/src/storage/postgres.d.ts +5 -0
- package/dist/src/storage/postgres.d.ts.map +1 -1
- package/dist/src/storage/postgres.js +16 -0
- package/dist/src/storage/postgres.js.map +1 -1
- package/dist/src/storage/redis.d.ts +10 -0
- package/dist/src/storage/redis.d.ts.map +1 -1
- package/dist/src/storage/redis.js +65 -0
- package/dist/src/storage/redis.js.map +1 -1
- package/dist/src/types/budget.d.ts +4 -0
- package/dist/src/types/budget.d.ts.map +1 -1
- package/dist/src/types/config.d.ts +58 -0
- package/dist/src/types/config.d.ts.map +1 -1
- package/dist/src/types/events.d.ts +1 -0
- package/dist/src/types/events.d.ts.map +1 -1
- package/dist/src/types/policy.d.ts +11 -1
- package/dist/src/types/policy.d.ts.map +1 -1
- package/dist/src/types/tool-result.d.ts +11 -0
- package/dist/src/types/tool-result.d.ts.map +1 -1
- package/dist/tests/unit/app-routes.test.d.ts +2 -0
- package/dist/tests/unit/app-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/app-routes.test.js +715 -0
- package/dist/tests/unit/app-routes.test.js.map +1 -0
- package/dist/tests/unit/audit-logger.test.js +105 -0
- package/dist/tests/unit/audit-logger.test.js.map +1 -1
- package/dist/tests/unit/auth-providers.test.d.ts +2 -0
- package/dist/tests/unit/auth-providers.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-providers.test.js +279 -0
- package/dist/tests/unit/auth-providers.test.js.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts +2 -0
- package/dist/tests/unit/auth-routes-extended.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-routes-extended.test.js +993 -0
- package/dist/tests/unit/auth-routes-extended.test.js.map +1 -0
- package/dist/tests/unit/auth-verifier.test.d.ts +2 -0
- package/dist/tests/unit/auth-verifier.test.d.ts.map +1 -0
- package/dist/tests/unit/auth-verifier.test.js +505 -0
- package/dist/tests/unit/auth-verifier.test.js.map +1 -0
- package/dist/tests/unit/billing-routes.test.d.ts +2 -0
- package/dist/tests/unit/billing-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/billing-routes.test.js +432 -0
- package/dist/tests/unit/billing-routes.test.js.map +1 -0
- package/dist/tests/unit/config-defaults.test.d.ts +2 -0
- package/dist/tests/unit/config-defaults.test.d.ts.map +1 -0
- package/dist/tests/unit/config-defaults.test.js +119 -0
- package/dist/tests/unit/config-defaults.test.js.map +1 -0
- package/dist/tests/unit/defaults.test.js +0 -10
- package/dist/tests/unit/defaults.test.js.map +1 -1
- package/dist/tests/unit/filesystem-executor.test.d.ts +2 -0
- package/dist/tests/unit/filesystem-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/filesystem-executor.test.js +280 -0
- package/dist/tests/unit/filesystem-executor.test.js.map +1 -0
- package/dist/tests/unit/gateway-branches.test.d.ts +2 -0
- package/dist/tests/unit/gateway-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/gateway-branches.test.js +1039 -0
- package/dist/tests/unit/gateway-branches.test.js.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts +2 -0
- package/dist/tests/unit/http-executor-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/http-executor-branches.test.js +495 -0
- package/dist/tests/unit/http-executor-branches.test.js.map +1 -0
- package/dist/tests/unit/logger.test.d.ts +2 -0
- package/dist/tests/unit/logger.test.d.ts.map +1 -0
- package/dist/tests/unit/logger.test.js +97 -0
- package/dist/tests/unit/logger.test.js.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts +2 -0
- package/dist/tests/unit/mcp-internal-auth.test.d.ts.map +1 -0
- package/dist/tests/unit/mcp-internal-auth.test.js +445 -0
- package/dist/tests/unit/mcp-internal-auth.test.js.map +1 -0
- package/dist/tests/unit/metrics.test.js +102 -0
- package/dist/tests/unit/metrics.test.js.map +1 -1
- package/dist/tests/unit/model-pricing.test.d.ts +2 -0
- package/dist/tests/unit/model-pricing.test.d.ts.map +1 -0
- package/dist/tests/unit/model-pricing.test.js +87 -0
- package/dist/tests/unit/model-pricing.test.js.map +1 -0
- package/dist/tests/unit/oauth-stores.test.d.ts +2 -0
- package/dist/tests/unit/oauth-stores.test.d.ts.map +1 -0
- package/dist/tests/unit/oauth-stores.test.js +260 -0
- package/dist/tests/unit/oauth-stores.test.js.map +1 -0
- package/dist/tests/unit/policy-engine.test.js +466 -0
- package/dist/tests/unit/policy-engine.test.js.map +1 -1
- package/dist/tests/unit/provider-interceptor.test.d.ts +2 -0
- package/dist/tests/unit/provider-interceptor.test.d.ts.map +1 -0
- package/dist/tests/unit/provider-interceptor.test.js +472 -0
- package/dist/tests/unit/provider-interceptor.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-branches.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-branches.test.js +2165 -0
- package/dist/tests/unit/saas-routes-branches.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-crud.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-crud.test.js +332 -0
- package/dist/tests/unit/saas-routes-crud.test.js.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts +2 -0
- package/dist/tests/unit/saas-routes-data.test.d.ts.map +1 -0
- package/dist/tests/unit/saas-routes-data.test.js +405 -0
- package/dist/tests/unit/saas-routes-data.test.js.map +1 -0
- package/dist/tests/unit/saas-routes.test.js +3 -3
- package/dist/tests/unit/saas-routes.test.js.map +1 -1
- package/dist/tests/unit/shell-executor.test.d.ts +2 -0
- package/dist/tests/unit/shell-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/shell-executor.test.js +145 -0
- package/dist/tests/unit/shell-executor.test.js.map +1 -0
- package/dist/tests/unit/sql-executor.test.d.ts +2 -0
- package/dist/tests/unit/sql-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/sql-executor.test.js +177 -0
- package/dist/tests/unit/sql-executor.test.js.map +1 -0
- package/dist/tests/unit/stream-proxy.test.d.ts +2 -0
- package/dist/tests/unit/stream-proxy.test.d.ts.map +1 -0
- package/dist/tests/unit/stream-proxy.test.js +147 -0
- package/dist/tests/unit/stream-proxy.test.js.map +1 -0
- package/dist/tests/unit/tool-definitions.test.d.ts +2 -0
- package/dist/tests/unit/tool-definitions.test.d.ts.map +1 -0
- package/dist/tests/unit/tool-definitions.test.js +184 -0
- package/dist/tests/unit/tool-definitions.test.js.map +1 -0
- package/dist/tests/unit/usage-extractor.test.js +140 -0
- package/dist/tests/unit/usage-extractor.test.js.map +1 -1
- package/dist/tests/unit/webhook-handler.test.d.ts +2 -0
- package/dist/tests/unit/webhook-handler.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-handler.test.js +453 -0
- package/dist/tests/unit/webhook-handler.test.js.map +1 -0
- package/dist/tests/unit/webhook-routes.test.d.ts +2 -0
- package/dist/tests/unit/webhook-routes.test.d.ts.map +1 -0
- package/dist/tests/unit/webhook-routes.test.js +69 -0
- package/dist/tests/unit/webhook-routes.test.js.map +1 -0
- package/dist/tests/unit/websocket-executor.test.d.ts +2 -0
- package/dist/tests/unit/websocket-executor.test.d.ts.map +1 -0
- package/dist/tests/unit/websocket-executor.test.js +121 -0
- package/dist/tests/unit/websocket-executor.test.js.map +1 -0
- package/package.json +8 -2
- package/policy-packs/demo_fail.yaml +41 -0
- package/policy-packs/full_tools.yaml +136 -0
- package/src/admin/index.ts +1 -0
- package/src/admin/routes.ts +509 -0
- package/src/admin/templates.ts +572 -0
- package/src/anomaly/detector.ts +730 -0
- package/src/anomaly/index.ts +1 -0
- package/src/approval/manager.ts +569 -0
- package/src/approval/webhook.ts +133 -0
- package/src/audit/logger.ts +490 -0
- package/src/auth/index.ts +5 -0
- package/src/auth/password.ts +21 -0
- package/src/auth/pkce.ts +22 -0
- package/src/auth/providers.ts +208 -0
- package/src/auth/routes.ts +561 -0
- package/src/auth/session.ts +84 -0
- package/src/billing/index.ts +6 -0
- package/src/billing/plan-enforcer.ts +135 -0
- package/src/billing/routes.ts +229 -0
- package/src/billing/stripe-client.ts +58 -0
- package/src/billing/webhook-handler.ts +182 -0
- package/src/billing/webhook-routes.ts +28 -0
- package/src/budget/manager.ts +679 -0
- package/src/budget/model-pricing.ts +119 -0
- package/src/budget/usage-extractor.ts +214 -0
- package/src/cli.ts +91 -0
- package/src/config/defaults.ts +261 -0
- package/src/config/validate.ts +88 -0
- package/src/dlp/composite-scanner.ts +213 -0
- package/src/dlp/index.ts +9 -0
- package/src/dlp/interfaces.ts +34 -0
- package/src/dlp/patterns.ts +30 -0
- package/src/dlp/prompt-injection-backend.ts +181 -0
- package/src/dlp/prompt-injection-patterns.ts +302 -0
- package/src/dlp/regex-backend.ts +181 -0
- package/src/dlp/scanner.ts +502 -0
- package/src/dlp/text-normalizer.ts +225 -0
- package/src/dlp/tool-patterns.ts +35 -0
- package/src/dlp/trufflehog-backend.ts +190 -0
- package/src/executor/filesystem-executor.ts +196 -0
- package/src/executor/http-executor.ts +349 -0
- package/src/executor/index.ts +9 -0
- package/src/executor/interfaces.ts +11 -0
- package/src/executor/noop-executor.ts +23 -0
- package/src/executor/registry.ts +64 -0
- package/src/executor/shell-executor.ts +148 -0
- package/src/executor/slack-executor.ts +176 -0
- package/src/executor/sql-executor.ts +146 -0
- package/src/executor/websocket-executor.ts +211 -0
- package/src/index.ts +24 -0
- package/src/interceptor/index.ts +1 -0
- package/src/interceptor/provider-interceptor.ts +315 -0
- package/src/mcp/auth-verifier.ts +152 -0
- package/src/mcp/bridge.ts +703 -0
- package/src/mcp/http-transport.ts +698 -0
- package/src/mcp/index.ts +9 -0
- package/src/mcp/internal-auth.ts +14 -0
- package/src/mcp/oauth-pages.ts +139 -0
- package/src/mcp/oauth-postgres-stores.ts +278 -0
- package/src/mcp/oauth-provider.ts +536 -0
- package/src/mcp/oauth-stores.ts +202 -0
- package/src/mcp/server.ts +55 -0
- package/src/mcp/tool-definitions.ts +562 -0
- package/src/metrics/collector.ts +357 -0
- package/src/metrics/index.ts +1 -0
- package/src/middleware/auth.ts +814 -0
- package/src/middleware/session.ts +85 -0
- package/src/middleware/validate.ts +130 -0
- package/src/policy/engine.ts +815 -0
- package/src/policy/index.ts +2 -0
- package/src/policy/opa-engine.ts +829 -0
- package/src/proxy/forward-proxy.ts +649 -0
- package/src/proxy/index.ts +1 -0
- package/src/ratelimit/limiter.ts +196 -0
- package/src/replay/engine.ts +142 -0
- package/src/replay/index.ts +1 -0
- package/src/saas/index.ts +1 -0
- package/src/saas/routes.ts +2178 -0
- package/src/server/app.ts +985 -0
- package/src/server/errors.ts +49 -0
- package/src/server/gateway.ts +1130 -0
- package/src/server/index.ts +307 -0
- package/src/server/logger.ts +255 -0
- package/src/server/stream-proxy.ts +202 -0
- package/src/storage/file-persistence.ts +315 -0
- package/src/storage/index.ts +4 -0
- package/src/storage/interfaces.ts +287 -0
- package/src/storage/memory.ts +686 -0
- package/src/storage/postgres.ts +1831 -0
- package/src/storage/redis.ts +835 -0
- package/src/tracing/index.ts +1 -0
- package/src/tracing/provider.ts +100 -0
- package/src/trust/calculator.ts +141 -0
- package/src/trust/index.ts +7 -0
- package/src/types/budget.ts +36 -0
- package/src/types/config.ts +278 -0
- package/src/types/events.ts +41 -0
- package/src/types/express.d.ts +14 -0
- package/src/types/index.ts +7 -0
- package/src/types/policy.ts +83 -0
- package/src/types/stripe-config.ts +11 -0
- package/src/types/subscription.ts +59 -0
- package/src/types/tool-call.ts +47 -0
- package/src/types/tool-result.ts +82 -0
- package/src/types/user.ts +125 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const cookie_parser_1 = __importDefault(require("cookie-parser"));
|
|
8
|
+
const supertest_1 = __importDefault(require("supertest"));
|
|
9
|
+
const routes_1 = require("../../src/auth/routes");
|
|
10
|
+
const memory_1 = require("../../src/storage/memory");
|
|
11
|
+
const session_1 = require("../../src/auth/session");
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Mock OAuth providers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
jest.mock('../../src/auth/providers', () => ({
|
|
16
|
+
buildGoogleAuthUrl: jest.fn().mockReturnValue({
|
|
17
|
+
url: 'https://accounts.google.com/o/oauth2/v2/auth?mock=true',
|
|
18
|
+
code_verifier: 'mock-verifier',
|
|
19
|
+
code_challenge: 'mock-challenge',
|
|
20
|
+
}),
|
|
21
|
+
buildGitHubAuthUrl: jest.fn().mockReturnValue('https://github.com/login/oauth/authorize?mock=true'),
|
|
22
|
+
exchangeGoogleCode: jest.fn(),
|
|
23
|
+
getGoogleUserInfo: jest.fn(),
|
|
24
|
+
exchangeGitHubCode: jest.fn(),
|
|
25
|
+
getGitHubUserInfo: jest.fn(),
|
|
26
|
+
}));
|
|
27
|
+
const { exchangeGoogleCode, getGoogleUserInfo, exchangeGitHubCode, getGitHubUserInfo, } = require('../../src/auth/providers');
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
const SESSION_SECRET = 'test-session-secret-32-chars-long!!';
|
|
32
|
+
function makeConfig(overrides) {
|
|
33
|
+
return {
|
|
34
|
+
enabled: true,
|
|
35
|
+
session_secret: SESSION_SECRET,
|
|
36
|
+
session_ttl_seconds: 3600,
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function createDeps(configOverrides) {
|
|
41
|
+
return {
|
|
42
|
+
config: makeConfig(configOverrides),
|
|
43
|
+
userStore: new memory_1.InMemoryUserStore(),
|
|
44
|
+
oauthAccountStore: new memory_1.InMemoryOAuthAccountStore(),
|
|
45
|
+
sessionStore: new memory_1.InMemorySessionStore(),
|
|
46
|
+
workspaceStore: new memory_1.InMemoryWorkspaceStore(),
|
|
47
|
+
workspaceMemberStore: new memory_1.InMemoryWorkspaceMemberStore(),
|
|
48
|
+
userApiKeyStore: new memory_1.InMemoryUserApiKeyStore(),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function createApp(deps) {
|
|
52
|
+
const app = (0, express_1.default)();
|
|
53
|
+
app.use(express_1.default.json());
|
|
54
|
+
app.use((0, cookie_parser_1.default)());
|
|
55
|
+
app.use('/auth', (0, routes_1.createAuthRouter)(deps));
|
|
56
|
+
return app;
|
|
57
|
+
}
|
|
58
|
+
/** Build an app that injects sessionUser/sessionData via middleware (for DELETE routes). */
|
|
59
|
+
function createAuthenticatedApp(deps, user, sessionData) {
|
|
60
|
+
const app = (0, express_1.default)();
|
|
61
|
+
app.use(express_1.default.json());
|
|
62
|
+
app.use((0, cookie_parser_1.default)());
|
|
63
|
+
app.use((req, _res, next) => {
|
|
64
|
+
req.sessionUser = user;
|
|
65
|
+
req.sessionData = sessionData || {
|
|
66
|
+
id: 'sess-test',
|
|
67
|
+
workspace_id: 'ws-1',
|
|
68
|
+
expires_at: new Date(Date.now() + 3600000).toISOString(),
|
|
69
|
+
};
|
|
70
|
+
next();
|
|
71
|
+
});
|
|
72
|
+
app.use('/auth', (0, routes_1.createAuthRouter)(deps));
|
|
73
|
+
return app;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Create a valid encrypted state cookie for the callback tests.
|
|
77
|
+
*/
|
|
78
|
+
function makeStateCookie(overrides) {
|
|
79
|
+
const payload = {
|
|
80
|
+
provider: 'google',
|
|
81
|
+
code_verifier: 'test-verifier',
|
|
82
|
+
nonce: 'test-nonce',
|
|
83
|
+
redirect_uri: 'http://127.0.0.1/auth/google/callback',
|
|
84
|
+
created_at: Date.now(),
|
|
85
|
+
...overrides,
|
|
86
|
+
};
|
|
87
|
+
return (0, session_1.encryptState)(payload, SESSION_SECRET);
|
|
88
|
+
}
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Tests
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
describe('auth routes — extended coverage', () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
jest.clearAllMocks();
|
|
95
|
+
});
|
|
96
|
+
// =========================================================================
|
|
97
|
+
// GET /auth/:provider/authorize
|
|
98
|
+
// =========================================================================
|
|
99
|
+
describe('GET /auth/:provider/authorize', () => {
|
|
100
|
+
it('redirects to Google with state cookie when google is configured', async () => {
|
|
101
|
+
const deps = createDeps({
|
|
102
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
103
|
+
});
|
|
104
|
+
const app = createApp(deps);
|
|
105
|
+
const res = await (0, supertest_1.default)(app).get('/auth/google/authorize');
|
|
106
|
+
expect(res.status).toBe(302);
|
|
107
|
+
expect(res.headers.location).toBe('https://accounts.google.com/o/oauth2/v2/auth?mock=true');
|
|
108
|
+
// State cookie should be set
|
|
109
|
+
const cookies = res.headers['set-cookie'];
|
|
110
|
+
expect(cookies).toBeDefined();
|
|
111
|
+
expect(cookies.some((c) => c.startsWith(`${session_1.STATE_COOKIE_NAME}=`))).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
it('redirects to GitHub with state cookie when github is configured', async () => {
|
|
114
|
+
const deps = createDeps({
|
|
115
|
+
github: { client_id: 'ghid', client_secret: 'ghsecret' },
|
|
116
|
+
});
|
|
117
|
+
const app = createApp(deps);
|
|
118
|
+
const res = await (0, supertest_1.default)(app).get('/auth/github/authorize');
|
|
119
|
+
expect(res.status).toBe(302);
|
|
120
|
+
expect(res.headers.location).toBe('https://github.com/login/oauth/authorize?mock=true');
|
|
121
|
+
const cookies = res.headers['set-cookie'];
|
|
122
|
+
expect(cookies).toBeDefined();
|
|
123
|
+
expect(cookies.some((c) => c.startsWith(`${session_1.STATE_COOKIE_NAME}=`))).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it('returns 400 for unsupported provider', async () => {
|
|
126
|
+
const deps = createDeps({
|
|
127
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
128
|
+
});
|
|
129
|
+
const app = createApp(deps);
|
|
130
|
+
const res = await (0, supertest_1.default)(app).get('/auth/twitter/authorize');
|
|
131
|
+
expect(res.status).toBe(400);
|
|
132
|
+
expect(res.body.error).toContain('Unsupported or unconfigured provider');
|
|
133
|
+
expect(res.body.error).toContain('twitter');
|
|
134
|
+
});
|
|
135
|
+
it('returns 400 when google is requested but not configured', async () => {
|
|
136
|
+
// No google or github in config
|
|
137
|
+
const deps = createDeps();
|
|
138
|
+
const app = createApp(deps);
|
|
139
|
+
const res = await (0, supertest_1.default)(app).get('/auth/google/authorize');
|
|
140
|
+
expect(res.status).toBe(400);
|
|
141
|
+
expect(res.body.error).toContain('Unsupported or unconfigured provider');
|
|
142
|
+
});
|
|
143
|
+
it('returns 400 when github is requested but not configured', async () => {
|
|
144
|
+
const deps = createDeps({
|
|
145
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
146
|
+
// github intentionally omitted
|
|
147
|
+
});
|
|
148
|
+
const app = createApp(deps);
|
|
149
|
+
const res = await (0, supertest_1.default)(app).get('/auth/github/authorize');
|
|
150
|
+
expect(res.status).toBe(400);
|
|
151
|
+
expect(res.body.error).toContain('Unsupported or unconfigured provider');
|
|
152
|
+
});
|
|
153
|
+
it('uses default redirect_uri when not specified in google config', async () => {
|
|
154
|
+
const { buildGoogleAuthUrl } = require('../../src/auth/providers');
|
|
155
|
+
const deps = createDeps({
|
|
156
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
157
|
+
});
|
|
158
|
+
const app = createApp(deps);
|
|
159
|
+
await (0, supertest_1.default)(app).get('/auth/google/authorize');
|
|
160
|
+
// buildGoogleAuthUrl should have been called with a redirect_uri derived from the request host
|
|
161
|
+
expect(buildGoogleAuthUrl).toHaveBeenCalledWith(expect.objectContaining({ client_id: 'gid' }), expect.stringContaining('/auth/google/callback'), expect.any(String), expect.any(String));
|
|
162
|
+
});
|
|
163
|
+
it('uses configured redirect_uri for google when provided', async () => {
|
|
164
|
+
const { buildGoogleAuthUrl } = require('../../src/auth/providers');
|
|
165
|
+
const deps = createDeps({
|
|
166
|
+
google: {
|
|
167
|
+
client_id: 'gid',
|
|
168
|
+
client_secret: 'gsecret',
|
|
169
|
+
redirect_uri: 'https://custom.example.com/auth/google/callback',
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const app = createApp(deps);
|
|
173
|
+
await (0, supertest_1.default)(app).get('/auth/google/authorize');
|
|
174
|
+
expect(buildGoogleAuthUrl).toHaveBeenCalledWith(expect.anything(), 'https://custom.example.com/auth/google/callback', expect.any(String), expect.any(String));
|
|
175
|
+
});
|
|
176
|
+
it('uses configured redirect_uri for github when provided', async () => {
|
|
177
|
+
const { buildGitHubAuthUrl } = require('../../src/auth/providers');
|
|
178
|
+
const deps = createDeps({
|
|
179
|
+
github: {
|
|
180
|
+
client_id: 'ghid',
|
|
181
|
+
client_secret: 'ghsecret',
|
|
182
|
+
redirect_uri: 'https://custom.example.com/auth/github/callback',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const app = createApp(deps);
|
|
186
|
+
await (0, supertest_1.default)(app).get('/auth/github/authorize');
|
|
187
|
+
expect(buildGitHubAuthUrl).toHaveBeenCalledWith(expect.anything(), 'https://custom.example.com/auth/github/callback', expect.any(String));
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// =========================================================================
|
|
191
|
+
// GET /auth/:provider/callback
|
|
192
|
+
// =========================================================================
|
|
193
|
+
describe('GET /auth/:provider/callback', () => {
|
|
194
|
+
it('redirects to /login?error=... when error param is present', async () => {
|
|
195
|
+
const deps = createDeps({
|
|
196
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
197
|
+
});
|
|
198
|
+
const app = createApp(deps);
|
|
199
|
+
const res = await (0, supertest_1.default)(app)
|
|
200
|
+
.get('/auth/google/callback?error=access_denied');
|
|
201
|
+
expect(res.status).toBe(302);
|
|
202
|
+
expect(res.headers.location).toBe('/login?error=access_denied');
|
|
203
|
+
});
|
|
204
|
+
it('redirects to /login?error=missing_params when code is missing', async () => {
|
|
205
|
+
const deps = createDeps({
|
|
206
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
207
|
+
});
|
|
208
|
+
const app = createApp(deps);
|
|
209
|
+
const res = await (0, supertest_1.default)(app)
|
|
210
|
+
.get('/auth/google/callback?state=somestate');
|
|
211
|
+
expect(res.status).toBe(302);
|
|
212
|
+
expect(res.headers.location).toBe('/login?error=missing_params');
|
|
213
|
+
});
|
|
214
|
+
it('redirects to /login?error=missing_params when state is missing', async () => {
|
|
215
|
+
const deps = createDeps({
|
|
216
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
217
|
+
});
|
|
218
|
+
const app = createApp(deps);
|
|
219
|
+
const res = await (0, supertest_1.default)(app)
|
|
220
|
+
.get('/auth/google/callback?code=somecode');
|
|
221
|
+
expect(res.status).toBe(302);
|
|
222
|
+
expect(res.headers.location).toBe('/login?error=missing_params');
|
|
223
|
+
});
|
|
224
|
+
it('redirects to /login?error=invalid_state when state cookie is missing', async () => {
|
|
225
|
+
const deps = createDeps({
|
|
226
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
227
|
+
});
|
|
228
|
+
const app = createApp(deps);
|
|
229
|
+
const res = await (0, supertest_1.default)(app)
|
|
230
|
+
.get('/auth/google/callback?code=abc&state=somestate');
|
|
231
|
+
// No state cookie set
|
|
232
|
+
expect(res.status).toBe(302);
|
|
233
|
+
expect(res.headers.location).toBe('/login?error=invalid_state');
|
|
234
|
+
});
|
|
235
|
+
it('redirects to /login?error=invalid_state when state cookie does not match query param', async () => {
|
|
236
|
+
const deps = createDeps({
|
|
237
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
238
|
+
});
|
|
239
|
+
const app = createApp(deps);
|
|
240
|
+
const stateCookie = makeStateCookie();
|
|
241
|
+
const res = await (0, supertest_1.default)(app)
|
|
242
|
+
.get('/auth/google/callback?code=abc&state=different-state-value')
|
|
243
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
244
|
+
expect(res.status).toBe(302);
|
|
245
|
+
expect(res.headers.location).toBe('/login?error=invalid_state');
|
|
246
|
+
});
|
|
247
|
+
it('redirects to /login?error=invalid_state when state decryption fails', async () => {
|
|
248
|
+
const deps = createDeps({
|
|
249
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
250
|
+
});
|
|
251
|
+
const app = createApp(deps);
|
|
252
|
+
// Use a garbled state that cannot be decrypted
|
|
253
|
+
const garbledState = 'this-is-not-valid-encrypted-state';
|
|
254
|
+
const res = await (0, supertest_1.default)(app)
|
|
255
|
+
.get(`/auth/google/callback?code=abc&state=${garbledState}`)
|
|
256
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${garbledState}`);
|
|
257
|
+
expect(res.status).toBe(302);
|
|
258
|
+
expect(res.headers.location).toBe('/login?error=invalid_state');
|
|
259
|
+
});
|
|
260
|
+
it('redirects to /login?error=state_expired when state is older than 10 minutes', async () => {
|
|
261
|
+
const deps = createDeps({
|
|
262
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
263
|
+
});
|
|
264
|
+
const app = createApp(deps);
|
|
265
|
+
// Created 11 minutes ago
|
|
266
|
+
const stateCookie = makeStateCookie({
|
|
267
|
+
created_at: Date.now() - 11 * 60 * 1000,
|
|
268
|
+
});
|
|
269
|
+
const res = await (0, supertest_1.default)(app)
|
|
270
|
+
.get(`/auth/google/callback?code=abc&state=${encodeURIComponent(stateCookie)}`)
|
|
271
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
272
|
+
expect(res.status).toBe(302);
|
|
273
|
+
expect(res.headers.location).toBe('/login?error=state_expired');
|
|
274
|
+
});
|
|
275
|
+
it('redirects to /login?error=unsupported_provider for unsupported provider in callback', async () => {
|
|
276
|
+
// Config has no twitter provider
|
|
277
|
+
const deps = createDeps();
|
|
278
|
+
const app = createApp(deps);
|
|
279
|
+
const stateCookie = makeStateCookie({ provider: 'twitter' });
|
|
280
|
+
const res = await (0, supertest_1.default)(app)
|
|
281
|
+
.get(`/auth/twitter/callback?code=abc&state=${encodeURIComponent(stateCookie)}`)
|
|
282
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
283
|
+
expect(res.status).toBe(302);
|
|
284
|
+
expect(res.headers.location).toBe('/login?error=unsupported_provider');
|
|
285
|
+
});
|
|
286
|
+
it('redirects to /login?error=auth_failed when token exchange throws', async () => {
|
|
287
|
+
const deps = createDeps({
|
|
288
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
289
|
+
});
|
|
290
|
+
const app = createApp(deps);
|
|
291
|
+
exchangeGoogleCode.mockRejectedValue(new Error('Token exchange failed'));
|
|
292
|
+
const stateCookie = makeStateCookie({ provider: 'google' });
|
|
293
|
+
const res = await (0, supertest_1.default)(app)
|
|
294
|
+
.get(`/auth/google/callback?code=abc&state=${encodeURIComponent(stateCookie)}`)
|
|
295
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
296
|
+
expect(res.status).toBe(302);
|
|
297
|
+
expect(res.headers.location).toBe('/login?error=auth_failed');
|
|
298
|
+
});
|
|
299
|
+
it('redirects to /onboarding for a new user (Google)', async () => {
|
|
300
|
+
const deps = createDeps({
|
|
301
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
302
|
+
});
|
|
303
|
+
const app = createApp(deps);
|
|
304
|
+
exchangeGoogleCode.mockResolvedValue({
|
|
305
|
+
access_token: 'google-access-token',
|
|
306
|
+
refresh_token: 'google-refresh-token',
|
|
307
|
+
});
|
|
308
|
+
getGoogleUserInfo.mockResolvedValue({
|
|
309
|
+
provider: 'google',
|
|
310
|
+
provider_user_id: 'g-123',
|
|
311
|
+
email: 'newuser@gmail.com',
|
|
312
|
+
display_name: 'New User',
|
|
313
|
+
avatar_url: 'https://example.com/avatar.jpg',
|
|
314
|
+
});
|
|
315
|
+
const stateCookie = makeStateCookie({
|
|
316
|
+
provider: 'google',
|
|
317
|
+
code_verifier: 'test-verifier',
|
|
318
|
+
});
|
|
319
|
+
const res = await (0, supertest_1.default)(app)
|
|
320
|
+
.get(`/auth/google/callback?code=valid-code&state=${encodeURIComponent(stateCookie)}`)
|
|
321
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
322
|
+
expect(res.status).toBe(302);
|
|
323
|
+
expect(res.headers.location).toBe('/onboarding');
|
|
324
|
+
// Session cookie should be set
|
|
325
|
+
const cookies = res.headers['set-cookie'];
|
|
326
|
+
expect(cookies.some((c) => c.startsWith(`${session_1.SESSION_COOKIE_NAME}=`))).toBe(true);
|
|
327
|
+
// User should have been created
|
|
328
|
+
const user = deps.userStore.getByEmail('newuser@gmail.com');
|
|
329
|
+
expect(user).toBeDefined();
|
|
330
|
+
expect(user.display_name).toBe('New User');
|
|
331
|
+
expect(user.onboarding_completed).toBe(false);
|
|
332
|
+
});
|
|
333
|
+
it('redirects to /onboarding for a new user (GitHub)', async () => {
|
|
334
|
+
const deps = createDeps({
|
|
335
|
+
github: { client_id: 'ghid', client_secret: 'ghsecret' },
|
|
336
|
+
});
|
|
337
|
+
const app = createApp(deps);
|
|
338
|
+
exchangeGitHubCode.mockResolvedValue({
|
|
339
|
+
access_token: 'gh-access-token',
|
|
340
|
+
});
|
|
341
|
+
getGitHubUserInfo.mockResolvedValue({
|
|
342
|
+
provider: 'github',
|
|
343
|
+
provider_user_id: 'gh-456',
|
|
344
|
+
email: 'dev@github.com',
|
|
345
|
+
display_name: 'GH Dev',
|
|
346
|
+
});
|
|
347
|
+
const stateCookie = makeStateCookie({
|
|
348
|
+
provider: 'github',
|
|
349
|
+
});
|
|
350
|
+
const res = await (0, supertest_1.default)(app)
|
|
351
|
+
.get(`/auth/github/callback?code=valid-code&state=${encodeURIComponent(stateCookie)}`)
|
|
352
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
353
|
+
expect(res.status).toBe(302);
|
|
354
|
+
expect(res.headers.location).toBe('/onboarding');
|
|
355
|
+
// User should have been created
|
|
356
|
+
const user = deps.userStore.getByEmail('dev@github.com');
|
|
357
|
+
expect(user).toBeDefined();
|
|
358
|
+
});
|
|
359
|
+
it('redirects to /dashboard for returning user with onboarding completed', async () => {
|
|
360
|
+
const deps = createDeps({
|
|
361
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
362
|
+
});
|
|
363
|
+
const app = createApp(deps);
|
|
364
|
+
const now = new Date().toISOString();
|
|
365
|
+
const existingUserId = 'u-existing';
|
|
366
|
+
// Seed existing user
|
|
367
|
+
deps.userStore.create({
|
|
368
|
+
id: existingUserId,
|
|
369
|
+
email: 'existing@gmail.com',
|
|
370
|
+
display_name: 'Existing User',
|
|
371
|
+
status: 'active',
|
|
372
|
+
onboarding_completed: true,
|
|
373
|
+
created_at: now,
|
|
374
|
+
updated_at: now,
|
|
375
|
+
});
|
|
376
|
+
// Seed existing OAuth link
|
|
377
|
+
deps.oauthAccountStore.create({
|
|
378
|
+
id: 'oa-1',
|
|
379
|
+
user_id: existingUserId,
|
|
380
|
+
provider: 'google',
|
|
381
|
+
provider_user_id: 'g-existing',
|
|
382
|
+
provider_email: 'existing@gmail.com',
|
|
383
|
+
created_at: now,
|
|
384
|
+
updated_at: now,
|
|
385
|
+
});
|
|
386
|
+
exchangeGoogleCode.mockResolvedValue({
|
|
387
|
+
access_token: 'new-access-token',
|
|
388
|
+
refresh_token: 'new-refresh-token',
|
|
389
|
+
});
|
|
390
|
+
getGoogleUserInfo.mockResolvedValue({
|
|
391
|
+
provider: 'google',
|
|
392
|
+
provider_user_id: 'g-existing',
|
|
393
|
+
email: 'existing@gmail.com',
|
|
394
|
+
display_name: 'Existing User',
|
|
395
|
+
});
|
|
396
|
+
const stateCookie = makeStateCookie({ provider: 'google' });
|
|
397
|
+
const res = await (0, supertest_1.default)(app)
|
|
398
|
+
.get(`/auth/google/callback?code=valid-code&state=${encodeURIComponent(stateCookie)}`)
|
|
399
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
400
|
+
expect(res.status).toBe(302);
|
|
401
|
+
expect(res.headers.location).toBe('/dashboard');
|
|
402
|
+
});
|
|
403
|
+
it('redirects to /onboarding for returning user without onboarding completed', async () => {
|
|
404
|
+
const deps = createDeps({
|
|
405
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
406
|
+
});
|
|
407
|
+
const app = createApp(deps);
|
|
408
|
+
const now = new Date().toISOString();
|
|
409
|
+
const existingUserId = 'u-noonboard';
|
|
410
|
+
// Seed existing user with onboarding NOT completed
|
|
411
|
+
deps.userStore.create({
|
|
412
|
+
id: existingUserId,
|
|
413
|
+
email: 'noonboard@gmail.com',
|
|
414
|
+
display_name: 'Noonboard User',
|
|
415
|
+
status: 'active',
|
|
416
|
+
onboarding_completed: false,
|
|
417
|
+
created_at: now,
|
|
418
|
+
updated_at: now,
|
|
419
|
+
});
|
|
420
|
+
// Seed existing OAuth link
|
|
421
|
+
deps.oauthAccountStore.create({
|
|
422
|
+
id: 'oa-noonboard',
|
|
423
|
+
user_id: existingUserId,
|
|
424
|
+
provider: 'google',
|
|
425
|
+
provider_user_id: 'g-noonboard',
|
|
426
|
+
provider_email: 'noonboard@gmail.com',
|
|
427
|
+
created_at: now,
|
|
428
|
+
updated_at: now,
|
|
429
|
+
});
|
|
430
|
+
exchangeGoogleCode.mockResolvedValue({
|
|
431
|
+
access_token: 'at',
|
|
432
|
+
refresh_token: 'rt',
|
|
433
|
+
});
|
|
434
|
+
getGoogleUserInfo.mockResolvedValue({
|
|
435
|
+
provider: 'google',
|
|
436
|
+
provider_user_id: 'g-noonboard',
|
|
437
|
+
email: 'noonboard@gmail.com',
|
|
438
|
+
display_name: 'Noonboard User',
|
|
439
|
+
});
|
|
440
|
+
const stateCookie = makeStateCookie({ provider: 'google' });
|
|
441
|
+
const res = await (0, supertest_1.default)(app)
|
|
442
|
+
.get(`/auth/google/callback?code=valid-code&state=${encodeURIComponent(stateCookie)}`)
|
|
443
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
444
|
+
expect(res.status).toBe(302);
|
|
445
|
+
expect(res.headers.location).toBe('/onboarding');
|
|
446
|
+
});
|
|
447
|
+
it('redirects to /login?error=account_exists when email exists but OAuth is not linked', async () => {
|
|
448
|
+
const deps = createDeps({
|
|
449
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
450
|
+
});
|
|
451
|
+
const app = createApp(deps);
|
|
452
|
+
const now = new Date().toISOString();
|
|
453
|
+
// Seed a password-based user (no OAuth link)
|
|
454
|
+
deps.userStore.create({
|
|
455
|
+
id: 'u-pw-only',
|
|
456
|
+
email: 'pwonly@gmail.com',
|
|
457
|
+
display_name: 'PW Only',
|
|
458
|
+
password_hash: 'hashed-pw',
|
|
459
|
+
status: 'active',
|
|
460
|
+
onboarding_completed: true,
|
|
461
|
+
created_at: now,
|
|
462
|
+
updated_at: now,
|
|
463
|
+
});
|
|
464
|
+
exchangeGoogleCode.mockResolvedValue({
|
|
465
|
+
access_token: 'at',
|
|
466
|
+
refresh_token: 'rt',
|
|
467
|
+
});
|
|
468
|
+
getGoogleUserInfo.mockResolvedValue({
|
|
469
|
+
provider: 'google',
|
|
470
|
+
provider_user_id: 'g-new-id',
|
|
471
|
+
email: 'pwonly@gmail.com',
|
|
472
|
+
display_name: 'PW Only',
|
|
473
|
+
});
|
|
474
|
+
const stateCookie = makeStateCookie({ provider: 'google' });
|
|
475
|
+
const res = await (0, supertest_1.default)(app)
|
|
476
|
+
.get(`/auth/google/callback?code=valid-code&state=${encodeURIComponent(stateCookie)}`)
|
|
477
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
478
|
+
expect(res.status).toBe(302);
|
|
479
|
+
expect(res.headers.location).toContain('/login?error=account_exists');
|
|
480
|
+
});
|
|
481
|
+
it('creates a session with workspace_id when user has memberships', async () => {
|
|
482
|
+
const deps = createDeps({
|
|
483
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
484
|
+
});
|
|
485
|
+
const app = createApp(deps);
|
|
486
|
+
const now = new Date().toISOString();
|
|
487
|
+
const userId = 'u-withmember';
|
|
488
|
+
// Seed user and OAuth link
|
|
489
|
+
deps.userStore.create({
|
|
490
|
+
id: userId,
|
|
491
|
+
email: 'member@gmail.com',
|
|
492
|
+
display_name: 'Member User',
|
|
493
|
+
status: 'active',
|
|
494
|
+
onboarding_completed: true,
|
|
495
|
+
created_at: now,
|
|
496
|
+
updated_at: now,
|
|
497
|
+
});
|
|
498
|
+
deps.oauthAccountStore.create({
|
|
499
|
+
id: 'oa-member',
|
|
500
|
+
user_id: userId,
|
|
501
|
+
provider: 'google',
|
|
502
|
+
provider_user_id: 'g-member',
|
|
503
|
+
provider_email: 'member@gmail.com',
|
|
504
|
+
created_at: now,
|
|
505
|
+
updated_at: now,
|
|
506
|
+
});
|
|
507
|
+
// Create workspace and membership
|
|
508
|
+
deps.workspaceStore.create({
|
|
509
|
+
id: 'ws-member',
|
|
510
|
+
name: 'Test Workspace',
|
|
511
|
+
slug: 'test-ws',
|
|
512
|
+
owner_user_id: userId,
|
|
513
|
+
plan: 'free',
|
|
514
|
+
settings: {},
|
|
515
|
+
created_at: now,
|
|
516
|
+
updated_at: now,
|
|
517
|
+
});
|
|
518
|
+
deps.workspaceMemberStore.create({
|
|
519
|
+
id: 'wm-1',
|
|
520
|
+
workspace_id: 'ws-member',
|
|
521
|
+
user_id: userId,
|
|
522
|
+
role: 'owner',
|
|
523
|
+
joined_at: now,
|
|
524
|
+
});
|
|
525
|
+
exchangeGoogleCode.mockResolvedValue({ access_token: 'at' });
|
|
526
|
+
getGoogleUserInfo.mockResolvedValue({
|
|
527
|
+
provider: 'google',
|
|
528
|
+
provider_user_id: 'g-member',
|
|
529
|
+
email: 'member@gmail.com',
|
|
530
|
+
display_name: 'Member User',
|
|
531
|
+
});
|
|
532
|
+
const stateCookie = makeStateCookie({ provider: 'google' });
|
|
533
|
+
const res = await (0, supertest_1.default)(app)
|
|
534
|
+
.get(`/auth/google/callback?code=valid-code&state=${encodeURIComponent(stateCookie)}`)
|
|
535
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
536
|
+
expect(res.status).toBe(302);
|
|
537
|
+
expect(res.headers.location).toBe('/dashboard');
|
|
538
|
+
// Verify session was created with workspace_id
|
|
539
|
+
// Extract session id from cookie
|
|
540
|
+
const cookies = res.headers['set-cookie'];
|
|
541
|
+
const sessionCookie = cookies.find((c) => c.startsWith(`${session_1.SESSION_COOKIE_NAME}=`));
|
|
542
|
+
const sessionId = sessionCookie.split('=')[1].split(';')[0];
|
|
543
|
+
const session = deps.sessionStore.getById(sessionId);
|
|
544
|
+
expect(session).toBeDefined();
|
|
545
|
+
expect(session.workspace_id).toBe('ws-member');
|
|
546
|
+
});
|
|
547
|
+
it('redirects to /login?error=auth_failed when getGoogleUserInfo throws', async () => {
|
|
548
|
+
const deps = createDeps({
|
|
549
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
550
|
+
});
|
|
551
|
+
const app = createApp(deps);
|
|
552
|
+
exchangeGoogleCode.mockResolvedValue({
|
|
553
|
+
access_token: 'at',
|
|
554
|
+
});
|
|
555
|
+
getGoogleUserInfo.mockRejectedValue(new Error('Google API error'));
|
|
556
|
+
const stateCookie = makeStateCookie({ provider: 'google' });
|
|
557
|
+
const res = await (0, supertest_1.default)(app)
|
|
558
|
+
.get(`/auth/google/callback?code=valid-code&state=${encodeURIComponent(stateCookie)}`)
|
|
559
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
560
|
+
expect(res.status).toBe(302);
|
|
561
|
+
expect(res.headers.location).toBe('/login?error=auth_failed');
|
|
562
|
+
});
|
|
563
|
+
it('redirects to /login?error=auth_failed when exchangeGitHubCode throws', async () => {
|
|
564
|
+
const deps = createDeps({
|
|
565
|
+
github: { client_id: 'ghid', client_secret: 'ghsecret' },
|
|
566
|
+
});
|
|
567
|
+
const app = createApp(deps);
|
|
568
|
+
exchangeGitHubCode.mockRejectedValue(new Error('GitHub exchange error'));
|
|
569
|
+
const stateCookie = makeStateCookie({ provider: 'github' });
|
|
570
|
+
const res = await (0, supertest_1.default)(app)
|
|
571
|
+
.get(`/auth/github/callback?code=bad-code&state=${encodeURIComponent(stateCookie)}`)
|
|
572
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
573
|
+
expect(res.status).toBe(302);
|
|
574
|
+
expect(res.headers.location).toBe('/login?error=auth_failed');
|
|
575
|
+
});
|
|
576
|
+
it('clears the state cookie after successful validation', async () => {
|
|
577
|
+
const deps = createDeps({
|
|
578
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
579
|
+
});
|
|
580
|
+
const app = createApp(deps);
|
|
581
|
+
exchangeGoogleCode.mockResolvedValue({ access_token: 'at' });
|
|
582
|
+
getGoogleUserInfo.mockResolvedValue({
|
|
583
|
+
provider: 'google',
|
|
584
|
+
provider_user_id: 'g-clear',
|
|
585
|
+
email: 'clearcookie@gmail.com',
|
|
586
|
+
display_name: 'Clear Cookie',
|
|
587
|
+
});
|
|
588
|
+
const stateCookie = makeStateCookie({ provider: 'google' });
|
|
589
|
+
const res = await (0, supertest_1.default)(app)
|
|
590
|
+
.get(`/auth/google/callback?code=valid&state=${encodeURIComponent(stateCookie)}`)
|
|
591
|
+
.set('Cookie', `${session_1.STATE_COOKIE_NAME}=${stateCookie}`);
|
|
592
|
+
expect(res.status).toBe(302);
|
|
593
|
+
// The state cookie should be cleared (expired)
|
|
594
|
+
const cookies = res.headers['set-cookie'];
|
|
595
|
+
const stateCookieCleared = cookies.find((c) => c.startsWith(`${session_1.STATE_COOKIE_NAME}=`));
|
|
596
|
+
if (stateCookieCleared) {
|
|
597
|
+
// Express clearCookie sets expires to the past
|
|
598
|
+
expect(stateCookieCleared).toMatch(/Expires=Thu, 01 Jan 1970/i);
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
it('URL-encodes error messages in redirect', async () => {
|
|
602
|
+
const deps = createDeps({
|
|
603
|
+
google: { client_id: 'gid', client_secret: 'gsecret' },
|
|
604
|
+
});
|
|
605
|
+
const app = createApp(deps);
|
|
606
|
+
const res = await (0, supertest_1.default)(app)
|
|
607
|
+
.get('/auth/google/callback?error=some%20error%20with%20spaces');
|
|
608
|
+
expect(res.status).toBe(302);
|
|
609
|
+
expect(res.headers.location).toBe('/login?error=some%20error%20with%20spaces');
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
// =========================================================================
|
|
613
|
+
// DELETE /auth/users/:id
|
|
614
|
+
// =========================================================================
|
|
615
|
+
describe('DELETE /auth/users/:id', () => {
|
|
616
|
+
it('returns 401 when not authenticated', async () => {
|
|
617
|
+
const deps = createDeps();
|
|
618
|
+
const app = createApp(deps); // No session middleware
|
|
619
|
+
const res = await (0, supertest_1.default)(app).delete('/auth/users/u-123');
|
|
620
|
+
expect(res.status).toBe(401);
|
|
621
|
+
expect(res.body.error).toContain('Not authenticated');
|
|
622
|
+
});
|
|
623
|
+
it('returns 403 when trying to delete someone else\'s account', async () => {
|
|
624
|
+
const deps = createDeps();
|
|
625
|
+
const now = new Date().toISOString();
|
|
626
|
+
const user = {
|
|
627
|
+
id: 'u-self',
|
|
628
|
+
email: 'self@example.com',
|
|
629
|
+
display_name: 'Self',
|
|
630
|
+
status: 'active',
|
|
631
|
+
onboarding_completed: true,
|
|
632
|
+
created_at: now,
|
|
633
|
+
updated_at: now,
|
|
634
|
+
};
|
|
635
|
+
deps.userStore.create(user);
|
|
636
|
+
const app = createAuthenticatedApp(deps, user);
|
|
637
|
+
const res = await (0, supertest_1.default)(app).delete('/auth/users/u-other');
|
|
638
|
+
expect(res.status).toBe(403);
|
|
639
|
+
expect(res.body.error).toContain('Can only delete your own account');
|
|
640
|
+
});
|
|
641
|
+
it('deletes own account, workspace, members, api keys, and OAuth accounts when sole owner', async () => {
|
|
642
|
+
const deps = createDeps();
|
|
643
|
+
const now = new Date().toISOString();
|
|
644
|
+
const userId = 'u-sole-owner';
|
|
645
|
+
// Create user
|
|
646
|
+
const user = {
|
|
647
|
+
id: userId,
|
|
648
|
+
email: 'sole@example.com',
|
|
649
|
+
display_name: 'Sole Owner',
|
|
650
|
+
status: 'active',
|
|
651
|
+
onboarding_completed: true,
|
|
652
|
+
created_at: now,
|
|
653
|
+
updated_at: now,
|
|
654
|
+
};
|
|
655
|
+
deps.userStore.create(user);
|
|
656
|
+
// Create workspace
|
|
657
|
+
deps.workspaceStore.create({
|
|
658
|
+
id: 'ws-owned',
|
|
659
|
+
name: 'Owned Workspace',
|
|
660
|
+
slug: 'owned-ws',
|
|
661
|
+
owner_user_id: userId,
|
|
662
|
+
plan: 'free',
|
|
663
|
+
settings: {},
|
|
664
|
+
created_at: now,
|
|
665
|
+
updated_at: now,
|
|
666
|
+
});
|
|
667
|
+
// Create memberships — user is sole owner
|
|
668
|
+
deps.workspaceMemberStore.create({
|
|
669
|
+
id: 'wm-owner',
|
|
670
|
+
workspace_id: 'ws-owned',
|
|
671
|
+
user_id: userId,
|
|
672
|
+
role: 'owner',
|
|
673
|
+
joined_at: now,
|
|
674
|
+
});
|
|
675
|
+
// Add another member (non-owner) to the workspace
|
|
676
|
+
deps.workspaceMemberStore.create({
|
|
677
|
+
id: 'wm-member',
|
|
678
|
+
workspace_id: 'ws-owned',
|
|
679
|
+
user_id: 'u-other-member',
|
|
680
|
+
role: 'member',
|
|
681
|
+
joined_at: now,
|
|
682
|
+
});
|
|
683
|
+
// Create API keys in the workspace
|
|
684
|
+
deps.userApiKeyStore.create({
|
|
685
|
+
id: 'ak-1',
|
|
686
|
+
key_hash: 'hash1',
|
|
687
|
+
key_prefix: 'pk_1234',
|
|
688
|
+
user_id: userId,
|
|
689
|
+
workspace_id: 'ws-owned',
|
|
690
|
+
name: 'Key 1',
|
|
691
|
+
roles: ['admin'],
|
|
692
|
+
tags: [],
|
|
693
|
+
revoked: false,
|
|
694
|
+
created_at: now,
|
|
695
|
+
});
|
|
696
|
+
// Create user-level API key
|
|
697
|
+
deps.userApiKeyStore.create({
|
|
698
|
+
id: 'ak-user',
|
|
699
|
+
key_hash: 'hash2',
|
|
700
|
+
key_prefix: 'pk_5678',
|
|
701
|
+
user_id: userId,
|
|
702
|
+
workspace_id: 'ws-other',
|
|
703
|
+
name: 'User Key',
|
|
704
|
+
roles: ['member'],
|
|
705
|
+
tags: [],
|
|
706
|
+
revoked: false,
|
|
707
|
+
created_at: now,
|
|
708
|
+
});
|
|
709
|
+
// Create OAuth account
|
|
710
|
+
deps.oauthAccountStore.create({
|
|
711
|
+
id: 'oa-del',
|
|
712
|
+
user_id: userId,
|
|
713
|
+
provider: 'google',
|
|
714
|
+
provider_user_id: 'g-del',
|
|
715
|
+
provider_email: 'sole@example.com',
|
|
716
|
+
created_at: now,
|
|
717
|
+
updated_at: now,
|
|
718
|
+
});
|
|
719
|
+
// Create session
|
|
720
|
+
deps.sessionStore.create({
|
|
721
|
+
id: 'sess-del',
|
|
722
|
+
user_id: userId,
|
|
723
|
+
expires_at: new Date(Date.now() + 3600000).toISOString(),
|
|
724
|
+
last_active_at: now,
|
|
725
|
+
created_at: now,
|
|
726
|
+
});
|
|
727
|
+
const app = createAuthenticatedApp(deps, user);
|
|
728
|
+
const res = await (0, supertest_1.default)(app)
|
|
729
|
+
.delete(`/auth/users/${userId}`)
|
|
730
|
+
.set('Cookie', `${session_1.SESSION_COOKIE_NAME}=sess-del`);
|
|
731
|
+
expect(res.status).toBe(200);
|
|
732
|
+
expect(res.body.status).toBe('ok');
|
|
733
|
+
expect(res.body.deleted_user_id).toBe(userId);
|
|
734
|
+
// Verify: user deleted
|
|
735
|
+
expect(deps.userStore.getById(userId)).toBeUndefined();
|
|
736
|
+
// Verify: workspace deleted
|
|
737
|
+
expect(deps.workspaceStore.getById('ws-owned')).toBeUndefined();
|
|
738
|
+
// Verify: all workspace members deleted (including the non-owner)
|
|
739
|
+
expect(deps.workspaceMemberStore.getById('wm-owner')).toBeUndefined();
|
|
740
|
+
expect(deps.workspaceMemberStore.getById('wm-member')).toBeUndefined();
|
|
741
|
+
// Verify: workspace API keys deleted
|
|
742
|
+
expect(deps.userApiKeyStore.getById('ak-1')).toBeUndefined();
|
|
743
|
+
// Verify: user-level API keys deleted
|
|
744
|
+
expect(deps.userApiKeyStore.getById('ak-user')).toBeUndefined();
|
|
745
|
+
// Verify: OAuth accounts deleted
|
|
746
|
+
expect(deps.oauthAccountStore.getById('oa-del')).toBeUndefined();
|
|
747
|
+
// Verify: session deleted
|
|
748
|
+
expect(deps.sessionStore.getById('sess-del')).toBeUndefined();
|
|
749
|
+
});
|
|
750
|
+
it('removes membership only when other owners exist, keeps workspace', async () => {
|
|
751
|
+
const deps = createDeps();
|
|
752
|
+
const now = new Date().toISOString();
|
|
753
|
+
const userId = 'u-co-owner';
|
|
754
|
+
// Create user
|
|
755
|
+
const user = {
|
|
756
|
+
id: userId,
|
|
757
|
+
email: 'coowner@example.com',
|
|
758
|
+
display_name: 'Co Owner',
|
|
759
|
+
status: 'active',
|
|
760
|
+
onboarding_completed: true,
|
|
761
|
+
created_at: now,
|
|
762
|
+
updated_at: now,
|
|
763
|
+
};
|
|
764
|
+
deps.userStore.create(user);
|
|
765
|
+
// Create workspace
|
|
766
|
+
deps.workspaceStore.create({
|
|
767
|
+
id: 'ws-shared',
|
|
768
|
+
name: 'Shared Workspace',
|
|
769
|
+
slug: 'shared-ws',
|
|
770
|
+
owner_user_id: userId,
|
|
771
|
+
plan: 'free',
|
|
772
|
+
settings: {},
|
|
773
|
+
created_at: now,
|
|
774
|
+
updated_at: now,
|
|
775
|
+
});
|
|
776
|
+
// Two owners — the user being deleted and another owner
|
|
777
|
+
deps.workspaceMemberStore.create({
|
|
778
|
+
id: 'wm-deleting',
|
|
779
|
+
workspace_id: 'ws-shared',
|
|
780
|
+
user_id: userId,
|
|
781
|
+
role: 'owner',
|
|
782
|
+
joined_at: now,
|
|
783
|
+
});
|
|
784
|
+
deps.workspaceMemberStore.create({
|
|
785
|
+
id: 'wm-other-owner',
|
|
786
|
+
workspace_id: 'ws-shared',
|
|
787
|
+
user_id: 'u-other-owner',
|
|
788
|
+
role: 'owner',
|
|
789
|
+
joined_at: now,
|
|
790
|
+
});
|
|
791
|
+
const app = createAuthenticatedApp(deps, user);
|
|
792
|
+
const res = await (0, supertest_1.default)(app)
|
|
793
|
+
.delete(`/auth/users/${userId}`);
|
|
794
|
+
expect(res.status).toBe(200);
|
|
795
|
+
expect(res.body.status).toBe('ok');
|
|
796
|
+
// Workspace should still exist
|
|
797
|
+
expect(deps.workspaceStore.getById('ws-shared')).toBeDefined();
|
|
798
|
+
// The deleting user's membership should be gone
|
|
799
|
+
expect(deps.workspaceMemberStore.getById('wm-deleting')).toBeUndefined();
|
|
800
|
+
// The other owner's membership should still exist
|
|
801
|
+
expect(deps.workspaceMemberStore.getById('wm-other-owner')).toBeDefined();
|
|
802
|
+
// User should be deleted
|
|
803
|
+
expect(deps.userStore.getById(userId)).toBeUndefined();
|
|
804
|
+
});
|
|
805
|
+
it('handles user with no workspaces', async () => {
|
|
806
|
+
const deps = createDeps();
|
|
807
|
+
const now = new Date().toISOString();
|
|
808
|
+
const userId = 'u-no-ws';
|
|
809
|
+
const user = {
|
|
810
|
+
id: userId,
|
|
811
|
+
email: 'nows@example.com',
|
|
812
|
+
display_name: 'No WS',
|
|
813
|
+
status: 'active',
|
|
814
|
+
onboarding_completed: true,
|
|
815
|
+
created_at: now,
|
|
816
|
+
updated_at: now,
|
|
817
|
+
};
|
|
818
|
+
deps.userStore.create(user);
|
|
819
|
+
const app = createAuthenticatedApp(deps, user);
|
|
820
|
+
const res = await (0, supertest_1.default)(app)
|
|
821
|
+
.delete(`/auth/users/${userId}`);
|
|
822
|
+
expect(res.status).toBe(200);
|
|
823
|
+
expect(res.body.status).toBe('ok');
|
|
824
|
+
expect(deps.userStore.getById(userId)).toBeUndefined();
|
|
825
|
+
});
|
|
826
|
+
it('handles user with no OAuth accounts', async () => {
|
|
827
|
+
const deps = createDeps();
|
|
828
|
+
const now = new Date().toISOString();
|
|
829
|
+
const userId = 'u-no-oauth';
|
|
830
|
+
const user = {
|
|
831
|
+
id: userId,
|
|
832
|
+
email: 'nooauth@example.com',
|
|
833
|
+
display_name: 'No OAuth',
|
|
834
|
+
status: 'active',
|
|
835
|
+
onboarding_completed: true,
|
|
836
|
+
created_at: now,
|
|
837
|
+
updated_at: now,
|
|
838
|
+
};
|
|
839
|
+
deps.userStore.create(user);
|
|
840
|
+
const app = createAuthenticatedApp(deps, user);
|
|
841
|
+
const res = await (0, supertest_1.default)(app)
|
|
842
|
+
.delete(`/auth/users/${userId}`);
|
|
843
|
+
expect(res.status).toBe(200);
|
|
844
|
+
expect(res.body.status).toBe('ok');
|
|
845
|
+
expect(deps.userStore.getById(userId)).toBeUndefined();
|
|
846
|
+
});
|
|
847
|
+
it('clears session cookie on deletion', async () => {
|
|
848
|
+
const deps = createDeps();
|
|
849
|
+
const now = new Date().toISOString();
|
|
850
|
+
const userId = 'u-cookie';
|
|
851
|
+
const user = {
|
|
852
|
+
id: userId,
|
|
853
|
+
email: 'cookie@example.com',
|
|
854
|
+
display_name: 'Cookie',
|
|
855
|
+
status: 'active',
|
|
856
|
+
onboarding_completed: true,
|
|
857
|
+
created_at: now,
|
|
858
|
+
updated_at: now,
|
|
859
|
+
};
|
|
860
|
+
deps.userStore.create(user);
|
|
861
|
+
const app = createAuthenticatedApp(deps, user);
|
|
862
|
+
const res = await (0, supertest_1.default)(app)
|
|
863
|
+
.delete(`/auth/users/${userId}`);
|
|
864
|
+
expect(res.status).toBe(200);
|
|
865
|
+
// Session cookie should be cleared
|
|
866
|
+
const cookies = res.headers['set-cookie'];
|
|
867
|
+
if (cookies) {
|
|
868
|
+
const sessionCookie = cookies.find((c) => c.startsWith(`${session_1.SESSION_COOKIE_NAME}=`));
|
|
869
|
+
if (sessionCookie) {
|
|
870
|
+
expect(sessionCookie).toMatch(/Expires=Thu, 01 Jan 1970/i);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
it('works without userApiKeyStore configured (optional dep)', async () => {
|
|
875
|
+
const now = new Date().toISOString();
|
|
876
|
+
const userId = 'u-no-keystore';
|
|
877
|
+
// Create deps without userApiKeyStore
|
|
878
|
+
const deps = {
|
|
879
|
+
config: makeConfig(),
|
|
880
|
+
userStore: new memory_1.InMemoryUserStore(),
|
|
881
|
+
oauthAccountStore: new memory_1.InMemoryOAuthAccountStore(),
|
|
882
|
+
sessionStore: new memory_1.InMemorySessionStore(),
|
|
883
|
+
workspaceStore: new memory_1.InMemoryWorkspaceStore(),
|
|
884
|
+
workspaceMemberStore: new memory_1.InMemoryWorkspaceMemberStore(),
|
|
885
|
+
// userApiKeyStore intentionally omitted
|
|
886
|
+
};
|
|
887
|
+
const user = {
|
|
888
|
+
id: userId,
|
|
889
|
+
email: 'nokeys@example.com',
|
|
890
|
+
display_name: 'No Keys',
|
|
891
|
+
status: 'active',
|
|
892
|
+
onboarding_completed: true,
|
|
893
|
+
created_at: now,
|
|
894
|
+
updated_at: now,
|
|
895
|
+
};
|
|
896
|
+
deps.userStore.create(user);
|
|
897
|
+
// Workspace where user is sole owner
|
|
898
|
+
deps.workspaceStore.create({
|
|
899
|
+
id: 'ws-nokeys',
|
|
900
|
+
name: 'WS No Keys',
|
|
901
|
+
slug: 'ws-nokeys',
|
|
902
|
+
owner_user_id: userId,
|
|
903
|
+
plan: 'free',
|
|
904
|
+
settings: {},
|
|
905
|
+
created_at: now,
|
|
906
|
+
updated_at: now,
|
|
907
|
+
});
|
|
908
|
+
deps.workspaceMemberStore.create({
|
|
909
|
+
id: 'wm-nokeys',
|
|
910
|
+
workspace_id: 'ws-nokeys',
|
|
911
|
+
user_id: userId,
|
|
912
|
+
role: 'owner',
|
|
913
|
+
joined_at: now,
|
|
914
|
+
});
|
|
915
|
+
const app = createAuthenticatedApp(deps, user);
|
|
916
|
+
const res = await (0, supertest_1.default)(app).delete(`/auth/users/${userId}`);
|
|
917
|
+
expect(res.status).toBe(200);
|
|
918
|
+
expect(res.body.status).toBe('ok');
|
|
919
|
+
// Workspace should be deleted since sole owner
|
|
920
|
+
expect(deps.workspaceStore.getById('ws-nokeys')).toBeUndefined();
|
|
921
|
+
expect(deps.userStore.getById(userId)).toBeUndefined();
|
|
922
|
+
});
|
|
923
|
+
it('handles multiple workspaces — sole owner of one, co-owner of another', async () => {
|
|
924
|
+
const deps = createDeps();
|
|
925
|
+
const now = new Date().toISOString();
|
|
926
|
+
const userId = 'u-multi-ws';
|
|
927
|
+
const user = {
|
|
928
|
+
id: userId,
|
|
929
|
+
email: 'multi@example.com',
|
|
930
|
+
display_name: 'Multi WS',
|
|
931
|
+
status: 'active',
|
|
932
|
+
onboarding_completed: true,
|
|
933
|
+
created_at: now,
|
|
934
|
+
updated_at: now,
|
|
935
|
+
};
|
|
936
|
+
deps.userStore.create(user);
|
|
937
|
+
// Workspace 1: sole owner → should be deleted
|
|
938
|
+
deps.workspaceStore.create({
|
|
939
|
+
id: 'ws-sole',
|
|
940
|
+
name: 'Sole WS',
|
|
941
|
+
slug: 'sole-ws',
|
|
942
|
+
owner_user_id: userId,
|
|
943
|
+
plan: 'free',
|
|
944
|
+
settings: {},
|
|
945
|
+
created_at: now,
|
|
946
|
+
updated_at: now,
|
|
947
|
+
});
|
|
948
|
+
deps.workspaceMemberStore.create({
|
|
949
|
+
id: 'wm-sole',
|
|
950
|
+
workspace_id: 'ws-sole',
|
|
951
|
+
user_id: userId,
|
|
952
|
+
role: 'owner',
|
|
953
|
+
joined_at: now,
|
|
954
|
+
});
|
|
955
|
+
// Workspace 2: co-owner → should only remove membership
|
|
956
|
+
deps.workspaceStore.create({
|
|
957
|
+
id: 'ws-co',
|
|
958
|
+
name: 'Co WS',
|
|
959
|
+
slug: 'co-ws',
|
|
960
|
+
owner_user_id: 'u-other',
|
|
961
|
+
plan: 'free',
|
|
962
|
+
settings: {},
|
|
963
|
+
created_at: now,
|
|
964
|
+
updated_at: now,
|
|
965
|
+
});
|
|
966
|
+
deps.workspaceMemberStore.create({
|
|
967
|
+
id: 'wm-co-self',
|
|
968
|
+
workspace_id: 'ws-co',
|
|
969
|
+
user_id: userId,
|
|
970
|
+
role: 'owner',
|
|
971
|
+
joined_at: now,
|
|
972
|
+
});
|
|
973
|
+
deps.workspaceMemberStore.create({
|
|
974
|
+
id: 'wm-co-other',
|
|
975
|
+
workspace_id: 'ws-co',
|
|
976
|
+
user_id: 'u-other',
|
|
977
|
+
role: 'owner',
|
|
978
|
+
joined_at: now,
|
|
979
|
+
});
|
|
980
|
+
const app = createAuthenticatedApp(deps, user);
|
|
981
|
+
const res = await (0, supertest_1.default)(app).delete(`/auth/users/${userId}`);
|
|
982
|
+
expect(res.status).toBe(200);
|
|
983
|
+
// Workspace 1 (sole owner) should be deleted
|
|
984
|
+
expect(deps.workspaceStore.getById('ws-sole')).toBeUndefined();
|
|
985
|
+
expect(deps.workspaceMemberStore.getById('wm-sole')).toBeUndefined();
|
|
986
|
+
// Workspace 2 (co-owner) should still exist
|
|
987
|
+
expect(deps.workspaceStore.getById('ws-co')).toBeDefined();
|
|
988
|
+
expect(deps.workspaceMemberStore.getById('wm-co-self')).toBeUndefined();
|
|
989
|
+
expect(deps.workspaceMemberStore.getById('wm-co-other')).toBeDefined();
|
|
990
|
+
});
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
//# sourceMappingURL=auth-routes-extended.test.js.map
|