@cosmicdrift/kumiko-bundled-features 0.14.0 → 0.16.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/package.json +2 -2
- package/src/__tests__/env-schemas.test.ts +1 -1
- package/src/__tests__/es-ops-e2e.integration.ts +10 -9
- package/src/audit/__tests__/audit.integration.ts +3 -3
- package/src/audit/handlers/list.query.ts +39 -51
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.ts +4 -3
- package/src/auth-email-password/__tests__/account-lockout.integration.ts +4 -3
- package/src/auth-email-password/__tests__/auth-claims.integration.ts +5 -4
- package/src/auth-email-password/__tests__/auth.integration.ts +4 -3
- package/src/auth-email-password/__tests__/confirm-token-flow.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-templates.test.ts +1 -1
- package/src/auth-email-password/__tests__/email-verification.integration.ts +7 -10
- package/src/auth-email-password/__tests__/identity-v3-hash.test.ts +1 -1
- package/src/auth-email-password/__tests__/identity-v3-login.integration.ts +4 -3
- package/src/auth-email-password/__tests__/invite-flow.integration.ts +16 -43
- package/src/auth-email-password/__tests__/multi-roles.integration.ts +6 -9
- package/src/auth-email-password/__tests__/password-reset.integration.ts +8 -7
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.ts +4 -3
- package/src/auth-email-password/__tests__/seed-admin.integration.ts +19 -32
- package/src/auth-email-password/__tests__/session-callbacks.integration.ts +6 -5
- package/src/auth-email-password/__tests__/session-strict-mode.integration.ts +1 -1
- package/src/auth-email-password/__tests__/signed-token.test.ts +1 -1
- package/src/auth-email-password/__tests__/signup-flow.integration.ts +11 -15
- package/src/auth-email-password/handlers/invite-accept-with-login.write.ts +26 -26
- package/src/auth-email-password/handlers/invite-accept.write.ts +24 -21
- package/src/auth-email-password/handlers/invite-create.write.ts +3 -8
- package/src/auth-email-password/handlers/invite-signup-complete.write.ts +20 -17
- package/src/auth-email-password/handlers/signup-confirm.write.ts +3 -7
- package/src/auth-email-password/seeding.ts +1 -1
- package/src/auth-email-password/web/__tests__/auth-gate.test.tsx +1 -2
- package/src/auth-email-password/web/__tests__/forgot-password-screen.test.tsx +10 -19
- package/src/auth-email-password/web/__tests__/login-screen.test.tsx +12 -18
- package/src/auth-email-password/web/__tests__/reset-password-screen.test.tsx +12 -17
- package/src/auth-email-password/web/__tests__/session-roles.test.ts +1 -1
- package/src/auth-email-password/web/__tests__/tenant-switcher.test.tsx +1 -8
- package/src/auth-email-password/web/__tests__/test-utils.tsx +4 -8
- package/src/auth-email-password/web/__tests__/user-menu.test.tsx +2 -8
- package/src/auth-email-password/web/__tests__/verify-email-screen.test.tsx +10 -15
- package/src/billing-foundation/__tests__/billing-foundation.integration.ts +1 -1
- package/src/billing-foundation/__tests__/feature.test.ts +1 -1
- package/src/billing-foundation/__tests__/webhook-handler.test.ts +6 -5
- package/src/billing-foundation/db/queries/subscription-projection.ts +15 -0
- package/src/billing-foundation/get-subscription-for-tenant.ts +2 -6
- package/src/billing-foundation/handlers/create-portal-session.write.ts +2 -2
- package/src/billing-foundation/handlers/list-subscriptions.query.ts +4 -1
- package/src/billing-foundation/projection.ts +32 -13
- package/src/cap-counter/__tests__/cap-counter.integration.ts +1 -1
- package/src/cap-counter/__tests__/enforce-cap.test.ts +37 -32
- package/src/cap-counter/__tests__/with-cap-enforcement.integration.ts +1 -1
- package/src/cap-counter/enforce-cap.ts +14 -20
- package/src/cap-counter/handlers/get-counter.query.ts +7 -13
- package/src/cap-counter/handlers/increment.write.ts +2 -2
- package/src/cap-counter/handlers/mark-soft-warned.write.ts +2 -2
- package/src/channel-in-app/handlers/inbox.query.ts +7 -13
- package/src/channel-in-app/handlers/mark-all-read.write.ts +7 -9
- package/src/channel-in-app/handlers/mark-read.write.ts +8 -14
- package/src/channel-in-app/handlers/unread-count.query.ts +10 -9
- package/src/channel-in-app/in-app-channel.ts +10 -12
- package/src/channel-in-app/tables.ts +1 -1
- package/src/compliance-profiles/__tests__/compliance-profiles.integration.ts +1 -1
- package/src/compliance-profiles/__tests__/seeding.integration.ts +1 -1
- package/src/compliance-profiles/_internal/parse-override.ts +19 -0
- package/src/compliance-profiles/handlers/for-tenant.query.ts +10 -32
- package/src/compliance-profiles/handlers/needs-profile.query.ts +4 -7
- package/src/compliance-profiles/handlers/set-profile.write.ts +5 -7
- package/src/compliance-profiles/resolve-for-tenant.ts +11 -27
- package/src/compliance-profiles/schema/profile-selection.ts +2 -2
- package/src/compliance-profiles/seeding.ts +4 -7
- package/src/config/__tests__/app-overrides.test.ts +1 -1
- package/src/config/__tests__/cascade.integration.ts +1 -1
- package/src/config/__tests__/config.integration.ts +8 -27
- package/src/config/db/queries/resolver.ts +47 -0
- package/src/config/handlers/__tests__/prepare-config-write.test.ts +1 -1
- package/src/config/resolver.ts +14 -62
- package/src/config/table.ts +4 -4
- package/src/config/write-helpers.ts +7 -11
- package/src/custom-fields/__tests__/audit-integration.integration.ts +6 -6
- package/src/custom-fields/__tests__/custom-fields.integration.ts +7 -7
- package/src/custom-fields/__tests__/feature.test.ts +1 -1
- package/src/custom-fields/__tests__/field-access.integration.ts +6 -6
- package/src/custom-fields/__tests__/quota.integration.ts +6 -6
- package/src/custom-fields/__tests__/retention.integration.ts +12 -10
- package/src/custom-fields/__tests__/user-data-rights.integration.ts +27 -17
- package/src/custom-fields/__tests__/wire-for-entity.test.ts +5 -5
- package/src/custom-fields/db/queries/field-access.ts +16 -0
- package/src/custom-fields/db/queries/projection.ts +43 -0
- package/src/custom-fields/db/queries/quota.ts +14 -0
- package/src/custom-fields/db/queries/retention.ts +39 -0
- package/src/custom-fields/db/queries/user-data-rights.ts +54 -0
- package/src/custom-fields/lib/field-access.ts +2 -41
- package/src/custom-fields/lib/quota.ts +2 -25
- package/src/custom-fields/run-retention.ts +19 -21
- package/src/custom-fields/wire-for-entity.ts +30 -23
- package/src/custom-fields/wire-user-data-rights.ts +33 -85
- package/src/data-retention/__tests__/data-retention.integration.ts +1 -1
- package/src/data-retention/__tests__/keep-for.test.ts +1 -1
- package/src/data-retention/__tests__/override-schema.test.ts +1 -1
- package/src/data-retention/__tests__/policy-for.integration.ts +1 -1
- package/src/data-retention/__tests__/resolver.test.ts +1 -1
- package/src/data-retention/handlers/policy-for.query.ts +5 -8
- package/src/data-retention/resolve-for-tenant.ts +6 -8
- package/src/data-retention/schema/tenant-retention-override.ts +2 -2
- package/src/delivery/__tests__/delivery-events.integration.ts +8 -21
- package/src/delivery/__tests__/delivery.integration.ts +100 -190
- package/src/delivery/db/queries/preferences.ts +30 -0
- package/src/delivery/delivery-service.ts +8 -36
- package/src/delivery/feature.ts +10 -2
- package/src/delivery/handlers/log.query.ts +5 -7
- package/src/delivery/handlers/preferences.query.ts +2 -5
- package/src/delivery/tables.ts +26 -1
- package/src/delivery/upsert-preference.ts +8 -14
- package/src/feature-toggles/__tests__/feature-toggles.integration.ts +30 -30
- package/src/feature-toggles/__tests__/registered-system-tenant.test.ts +7 -6
- package/src/feature-toggles/db/queries/toggle-state.ts +25 -0
- package/src/feature-toggles/feature.ts +16 -2
- package/src/feature-toggles/global-feature-state-table.ts +1 -1
- package/src/feature-toggles/handlers/list.query.ts +9 -2
- package/src/feature-toggles/handlers/registered.query.ts +3 -7
- package/src/feature-toggles/handlers/set.write.ts +37 -25
- package/src/feature-toggles/toggle-runtime.ts +3 -6
- package/src/file-foundation/__tests__/feature.test.ts +1 -1
- package/src/file-foundation/__tests__/file-foundation.integration.ts +1 -1
- package/src/file-provider-inmemory/__tests__/feature.test.ts +1 -1
- package/src/file-provider-s3/__tests__/feature.test.ts +1 -1
- package/src/files/__tests__/files.integration.ts +18 -7
- package/src/files/schema/file-ref.ts +1 -1
- package/src/files-provider-s3/__tests__/env-helper.test.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.integration.ts +1 -1
- package/src/files-provider-s3/__tests__/s3-provider.test.ts +1 -1
- package/src/jobs/__tests__/job-system-user.integration.ts +1 -1
- package/src/jobs/__tests__/jobs-events.integration.ts +8 -21
- package/src/jobs/__tests__/jobs-feature.integration.ts +1 -1
- package/src/jobs/feature.ts +26 -15
- package/src/jobs/handlers/detail.query.ts +10 -8
- package/src/jobs/handlers/list.query.ts +9 -21
- package/src/jobs/handlers/retry.write.ts +2 -7
- package/src/jobs/job-run-logger.ts +3 -9
- package/src/jobs/job-run-table.ts +49 -17
- package/src/legal-pages/__tests__/legal-pages.integration.ts +1 -1
- package/src/mail-foundation/__tests__/feature.test.ts +1 -1
- package/src/mail-foundation/__tests__/mail-foundation.integration.ts +1 -1
- package/src/mail-transport-inmemory/__tests__/feature.test.ts +1 -1
- package/src/mail-transport-smtp/__tests__/feature.test.ts +1 -1
- package/src/rate-limiting/__tests__/rate-limiting.integration.ts +1 -1
- package/src/renderer-foundation/__tests__/api.test.ts +2 -2
- package/src/renderer-foundation/__tests__/collect-plugins.integration.ts +1 -1
- package/src/renderer-simple/__tests__/adapter.test.ts +2 -2
- package/src/renderer-simple/__tests__/simple-renderer.test.ts +1 -1
- package/src/secrets/__tests__/require-secrets-context.test.ts +6 -5
- package/src/secrets/__tests__/rotate.integration.ts +6 -9
- package/src/secrets/__tests__/secrets-events.integration.ts +6 -12
- package/src/secrets/__tests__/secrets.integration.ts +6 -11
- package/src/secrets/db/queries/read.ts +16 -0
- package/src/secrets/handlers/list.query.ts +16 -17
- package/src/secrets/handlers/rotate.job.ts +8 -12
- package/src/secrets/secrets-context.ts +9 -21
- package/src/secrets/table.ts +1 -1
- package/src/sessions/__tests__/cleanup.integration.ts +8 -6
- package/src/sessions/__tests__/password-auto-revoke.integration.ts +7 -6
- package/src/sessions/__tests__/sessions.integration.ts +23 -38
- package/src/sessions/__tests__/test-helpers.ts +1 -1
- package/src/sessions/db/queries/cleanup.ts +21 -0
- package/src/sessions/handlers/cleanup.job.ts +6 -29
- package/src/sessions/handlers/list.query.ts +24 -24
- package/src/sessions/handlers/mine.query.ts +24 -23
- package/src/sessions/handlers/revoke-all-for-user.write.ts +7 -11
- package/src/sessions/handlers/revoke-all-others.write.ts +7 -12
- package/src/sessions/handlers/revoke.write.ts +11 -18
- package/src/sessions/schema/user-session.ts +2 -2
- package/src/sessions/session-callbacks.ts +19 -21
- package/src/subscription-mollie/__tests__/feature.test.ts +1 -1
- package/src/subscription-mollie/__tests__/mollie-foundation.integration.ts +1 -1
- package/src/subscription-mollie/__tests__/verify-webhook.test.ts +8 -7
- package/src/subscription-stripe/__tests__/feature.test.ts +1 -1
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +14 -15
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.ts +1 -1
- package/src/subscription-stripe/__tests__/verify-webhook.test.ts +14 -14
- package/src/subscription-stripe/verify-webhook.ts +1 -1
- package/src/template-resolver/__tests__/handlers.integration.ts +1 -1
- package/src/template-resolver/__tests__/template-resolver.integration.ts +3 -2
- package/src/template-resolver/api.ts +7 -13
- package/src/template-resolver/handlers/archive.write.ts +4 -7
- package/src/template-resolver/handlers/find-by-id.query.ts +4 -7
- package/src/template-resolver/handlers/list.query.ts +13 -21
- package/src/template-resolver/handlers/publish.write.ts +4 -7
- package/src/template-resolver/handlers/upsert-system.write.ts +7 -10
- package/src/template-resolver/handlers/upsert-tenant.write.ts +7 -10
- package/src/template-resolver/table.ts +2 -5
- package/src/tenant/__tests__/multi-tenant.integration.ts +1 -1
- package/src/tenant/__tests__/seed-testing.integration.ts +19 -45
- package/src/tenant/__tests__/tenant.integration.ts +1 -1
- package/src/tenant/handlers/active-tenant-ids.query.ts +3 -8
- package/src/tenant/handlers/add-member.write.ts +6 -8
- package/src/tenant/handlers/cancel-invitation.write.ts +5 -7
- package/src/tenant/handlers/invitations.query.ts +5 -10
- package/src/tenant/handlers/me.query.ts +2 -3
- package/src/tenant/handlers/members.query.ts +4 -5
- package/src/tenant/handlers/memberships.query.ts +2 -5
- package/src/tenant/handlers/remove-member.write.ts +6 -8
- package/src/tenant/handlers/resolve-user-ids.query.ts +6 -16
- package/src/tenant/handlers/update-member-roles.write.ts +6 -8
- package/src/tenant/invitation-table.ts +2 -5
- package/src/tenant/membership-table.ts +3 -6
- package/src/tenant/schema/tenant.ts +2 -2
- package/src/tenant/seeding.ts +12 -18
- package/src/text-content/README.md +1 -1
- package/src/text-content/__tests__/text-content.integration.ts +2 -2
- package/src/text-content/api.ts +2 -9
- package/src/text-content/handlers/by-slug.query.ts +6 -9
- package/src/text-content/handlers/by-tenant.query.ts +2 -2
- package/src/text-content/handlers/set.write.ts +7 -9
- package/src/text-content/seeding.ts +6 -9
- package/src/text-content/table.ts +2 -2
- package/src/text-content/web/__tests__/editor-read-only.test.tsx +31 -45
- package/src/text-content/web/__tests__/group-blocks.test.ts +1 -18
- package/src/text-content/web/client-plugin.tsx +11 -23
- package/src/tier-engine/__tests__/auto-default-tier.integration.ts +10 -16
- package/src/tier-engine/__tests__/compose-app.test.ts +1 -1
- package/src/tier-engine/__tests__/drift.test.ts +1 -1
- package/src/tier-engine/__tests__/resolver.integration.ts +6 -6
- package/src/tier-engine/__tests__/tier-engine.integration.ts +1 -1
- package/src/tier-engine/feature.ts +9 -16
- package/src/user/__tests__/seed-testing.integration.ts +10 -22
- package/src/user/__tests__/user-status.test.ts +1 -1
- package/src/user/__tests__/user.integration.ts +6 -5
- package/src/user/handlers/create.write.ts +5 -7
- package/src/user/handlers/find-for-auth.query.ts +5 -7
- package/src/user/schema/user.ts +2 -2
- package/src/user/seeding.ts +2 -3
- package/src/user-data-rights/__tests__/audit-log.integration.ts +24 -12
- package/src/user-data-rights/__tests__/cross-data-matrix.integration.ts +64 -37
- package/src/user-data-rights/__tests__/download.integration.ts +29 -46
- package/src/user-data-rights/__tests__/export-job-idempotency.integration.ts +35 -28
- package/src/user-data-rights/__tests__/export-job-schema.test.ts +2 -2
- package/src/user-data-rights/__tests__/policy-to-strategy.test.ts +1 -1
- package/src/user-data-rights/__tests__/request-cancel-deletion.integration.ts +11 -15
- package/src/user-data-rights/__tests__/request-deletion-callback.integration.ts +10 -12
- package/src/user-data-rights/__tests__/request-export.integration.ts +23 -16
- package/src/user-data-rights/__tests__/restriction-flow.integration.ts +24 -32
- package/src/user-data-rights/__tests__/run-export-jobs.integration.ts +142 -137
- package/src/user-data-rights/__tests__/run-forget-cleanup.integration.ts +46 -28
- package/src/user-data-rights/__tests__/run-user-export.integration.ts +20 -14
- package/src/user-data-rights/__tests__/token-helpers.test.ts +1 -1
- package/src/user-data-rights/__tests__/user-data-rights.integration.ts +1 -1
- package/src/user-data-rights/__tests__/zip-path.test.ts +1 -1
- package/src/user-data-rights/audit-download.ts +3 -3
- package/src/user-data-rights/db/queries/export-jobs.ts +23 -0
- package/src/user-data-rights/db/queries/forget-cleanup.ts +13 -0
- package/src/user-data-rights/handlers/cancel-deletion.write.ts +28 -22
- package/src/user-data-rights/handlers/download-by-job.query.ts +11 -21
- package/src/user-data-rights/handlers/download-by-token.query.ts +20 -35
- package/src/user-data-rights/handlers/export-status.query.ts +19 -33
- package/src/user-data-rights/handlers/lift-restriction.write.ts +7 -12
- package/src/user-data-rights/handlers/list-download-attempts.query.ts +14 -23
- package/src/user-data-rights/handlers/my-audit-log.query.ts +33 -23
- package/src/user-data-rights/handlers/request-deletion.write.ts +15 -15
- package/src/user-data-rights/handlers/request-export.write.ts +7 -11
- package/src/user-data-rights/handlers/restrict-account.write.ts +12 -12
- package/src/user-data-rights/run-export-jobs.ts +20 -60
- package/src/user-data-rights/run-forget-cleanup.ts +19 -33
- package/src/user-data-rights/run-user-export.ts +4 -6
- package/src/user-data-rights/schema/download-attempt.ts +2 -2
- package/src/user-data-rights/schema/download-token.ts +2 -2
- package/src/user-data-rights/schema/export-job.ts +2 -3
- package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.ts +37 -30
- package/src/user-data-rights-defaults/db/queries/user-hook.ts +17 -0
- package/src/user-data-rights-defaults/hooks/file-ref.userdata-hook.ts +12 -27
- package/src/user-data-rights-defaults/hooks/user.userdata-hook.ts +16 -18
- package/CHANGELOG.md +0 -689
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// bei !committed wird der burn released damit ein legitimer Retry
|
|
20
20
|
// nicht durch einen stale Marker geblockt wird (wie reset/verify).
|
|
21
21
|
|
|
22
|
+
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
22
23
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
23
24
|
import {
|
|
24
25
|
defineWriteHandler,
|
|
@@ -32,7 +33,6 @@ import {
|
|
|
32
33
|
} from "@cosmicdrift/kumiko-framework/errors";
|
|
33
34
|
import { generateUniqueName } from "@cosmicdrift/kumiko-framework/random";
|
|
34
35
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
35
|
-
import { eq } from "drizzle-orm";
|
|
36
36
|
import { z } from "zod";
|
|
37
37
|
// kumiko-lint-ignore cross-feature-import signup-confirm reads tenants.key for slug-uniqueness check (TOCTOU + DB-unique-index zusammen)
|
|
38
38
|
import { tenantTable } from "../../tenant/schema/tenant";
|
|
@@ -108,12 +108,8 @@ export function createSignupConfirmHandler() {
|
|
|
108
108
|
|
|
109
109
|
const tenantKey = await generateUniqueName({
|
|
110
110
|
isAvailable: async (slug) => {
|
|
111
|
-
const existing = await dbConn
|
|
112
|
-
|
|
113
|
-
.from(tenantTable)
|
|
114
|
-
.where(eq(tenantTable.key, slug))
|
|
115
|
-
.limit(1);
|
|
116
|
-
return existing.length === 0;
|
|
111
|
+
const existing = await fetchOne<{ id: string }>(dbConn, tenantTable, { key: slug });
|
|
112
|
+
return existing === undefined;
|
|
117
113
|
},
|
|
118
114
|
});
|
|
119
115
|
|
|
@@ -78,7 +78,7 @@ export async function seedUserWithPassword(
|
|
|
78
78
|
* innerhalb dieses Sprints, weil alle existing tests berührt würden.
|
|
79
79
|
*
|
|
80
80
|
* Atomicity: läuft inside einer Drizzle-Tx wenn der Caller das angibt
|
|
81
|
-
* (db.
|
|
81
|
+
* (db.begin(tx => provisionSignupAccount(tx, ...)) — die seed-
|
|
82
82
|
* helpers nehmen DbConnection|DbTx strukturell. Bei pure DbConnection
|
|
83
83
|
* sind die 3 writes nicht atomic; bei Failure zwischen Schritten kann
|
|
84
84
|
* ein orphan-Tenant zurückbleiben (Tenant ohne User → unused row;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { screen } from "@testing-library/react";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
-
import { describe, expect, test } from "vitest";
|
|
5
4
|
import { makeAuthGate } from "../auth-gate";
|
|
6
5
|
import { makeSessionApi, renderWithProviders } from "./test-utils";
|
|
7
6
|
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
4
3
|
import { ForgotPasswordScreen } from "../forgot-password-screen";
|
|
5
4
|
import { renderWithProviders } from "./test-utils";
|
|
6
5
|
|
|
7
6
|
beforeEach(() => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
vi.fn(async () => new Response(null, { status: 200 })),
|
|
12
|
-
);
|
|
13
|
-
});
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
vi.unstubAllGlobals();
|
|
7
|
+
globalThis.fetch = mock(
|
|
8
|
+
async () => new Response(null, { status: 200 }),
|
|
9
|
+
) as unknown as typeof fetch;
|
|
16
10
|
});
|
|
11
|
+
afterEach(() => {});
|
|
17
12
|
|
|
18
13
|
describe("ForgotPasswordScreen", () => {
|
|
19
14
|
test("rendert title + email-input + submit-button (de)", () => {
|
|
@@ -24,8 +19,8 @@ describe("ForgotPasswordScreen", () => {
|
|
|
24
19
|
});
|
|
25
20
|
|
|
26
21
|
test("submit ruft /api/auth/request-password-reset mit der Email", async () => {
|
|
27
|
-
const fetchMock =
|
|
28
|
-
|
|
22
|
+
const fetchMock = mock(async () => new Response(null, { status: 200 }));
|
|
23
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
29
24
|
|
|
30
25
|
renderWithProviders(<ForgotPasswordScreen />);
|
|
31
26
|
fireEvent.change(screen.getByLabelText(/^E-Mail/), {
|
|
@@ -54,15 +49,13 @@ describe("ForgotPasswordScreen", () => {
|
|
|
54
49
|
await waitFor(() => {
|
|
55
50
|
expect(screen.getByText("Mail gesendet")).toBeTruthy();
|
|
56
51
|
});
|
|
57
|
-
// Link zurück zum Login muss im Success-State da sein.
|
|
58
52
|
expect(screen.getByRole("link", { name: /Zurück zum Login/i })).toBeTruthy();
|
|
59
53
|
});
|
|
60
54
|
|
|
61
55
|
test("server 5xx → error-banner statt Success-State", async () => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
56
|
+
globalThis.fetch = mock(
|
|
57
|
+
async () => new Response(null, { status: 500 }),
|
|
58
|
+
) as unknown as typeof fetch;
|
|
66
59
|
|
|
67
60
|
renderWithProviders(<ForgotPasswordScreen />);
|
|
68
61
|
fireEvent.change(screen.getByLabelText(/^E-Mail/), {
|
|
@@ -71,10 +64,8 @@ describe("ForgotPasswordScreen", () => {
|
|
|
71
64
|
fireEvent.click(screen.getByRole("button", { name: "Link anfordern" }));
|
|
72
65
|
|
|
73
66
|
await waitFor(() => {
|
|
74
|
-
// unknownError-Bundle-key → "Etwas ist schief gegangen..."
|
|
75
67
|
expect(screen.getByRole("alert").textContent).toContain("schief");
|
|
76
68
|
});
|
|
77
|
-
// Kein success-state.
|
|
78
69
|
expect(screen.queryByText("Mail gesendet")).toBeNull();
|
|
79
70
|
});
|
|
80
71
|
});
|
|
@@ -1,22 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
3
|
-
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
4
|
-
|
|
5
|
-
// Mock-Bridge für den resend-Flow: LoginScreen ruft requestEmailVerification
|
|
6
|
-
// aus ../auth-client auf, wir stubben das hier um die Server-Antwort pro
|
|
7
|
-
// Test-Case zu kontrollieren. vi.hoisted weil vi.mock() vor allen anderen
|
|
8
|
-
// Statements gehoisted wird und sonst die Variable nicht sehen kann.
|
|
9
|
-
const { requestEmailVerificationMock } = vi.hoisted(() => ({
|
|
10
|
-
requestEmailVerificationMock: vi.fn(),
|
|
11
|
-
}));
|
|
12
|
-
vi.mock("../auth-client", async () => {
|
|
13
|
-
const actual = await vi.importActual<typeof import("../auth-client")>("../auth-client");
|
|
14
|
-
return { ...actual, requestEmailVerification: requestEmailVerificationMock };
|
|
15
|
-
});
|
|
16
3
|
|
|
17
4
|
import { LoginScreen } from "../login-screen";
|
|
18
5
|
import { makeSessionApi, renderWithProviders } from "./test-utils";
|
|
19
6
|
|
|
7
|
+
const requestEmailVerificationMock = mock<() => Promise<unknown>>(() => Promise.resolve());
|
|
8
|
+
const actual_authClient = await import("../auth-client");
|
|
9
|
+
mock.module("../auth-client", () => ({
|
|
10
|
+
...actual_authClient,
|
|
11
|
+
requestEmailVerification: requestEmailVerificationMock,
|
|
12
|
+
}));
|
|
13
|
+
|
|
20
14
|
describe("LoginScreen", () => {
|
|
21
15
|
beforeEach(() => {
|
|
22
16
|
requestEmailVerificationMock.mockReset();
|
|
@@ -54,7 +48,7 @@ describe("LoginScreen", () => {
|
|
|
54
48
|
const session = makeSessionApi({
|
|
55
49
|
status: "unauthenticated",
|
|
56
50
|
user: null,
|
|
57
|
-
login:
|
|
51
|
+
login: mock(async () => ({ ok: false, error: { reason: "invalid_credentials" } })),
|
|
58
52
|
});
|
|
59
53
|
renderWithProviders(<LoginScreen />, { session });
|
|
60
54
|
|
|
@@ -76,7 +70,7 @@ describe("LoginScreen", () => {
|
|
|
76
70
|
const session = makeSessionApi({
|
|
77
71
|
status: "unauthenticated",
|
|
78
72
|
user: null,
|
|
79
|
-
login:
|
|
73
|
+
login: mock(async () => ({
|
|
80
74
|
ok: false,
|
|
81
75
|
error: { reason: "account_locked", retryAfterSeconds: 540 },
|
|
82
76
|
})),
|
|
@@ -129,7 +123,7 @@ describe("LoginScreen", () => {
|
|
|
129
123
|
return makeSessionApi({
|
|
130
124
|
status: "unauthenticated",
|
|
131
125
|
user: null,
|
|
132
|
-
login:
|
|
126
|
+
login: mock(async () => ({ ok: false, error: { reason: "email_not_verified" } })),
|
|
133
127
|
});
|
|
134
128
|
}
|
|
135
129
|
|
|
@@ -211,7 +205,7 @@ describe("LoginScreen", () => {
|
|
|
211
205
|
const session = makeSessionApi({
|
|
212
206
|
status: "unauthenticated",
|
|
213
207
|
user: null,
|
|
214
|
-
login:
|
|
208
|
+
login: mock(async () => ({ ok: false, error: { reason: "invalid_credentials" } })),
|
|
215
209
|
});
|
|
216
210
|
renderWithProviders(<LoginScreen />, { session });
|
|
217
211
|
await loginUntilEmailNotVerified();
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { fireEvent, screen, waitFor } from "@testing-library/react";
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
4
3
|
import { ResetPasswordScreen } from "../reset-password-screen";
|
|
5
4
|
import { renderWithProviders } from "./test-utils";
|
|
6
5
|
|
|
7
6
|
beforeEach(() => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
);
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
vi.unstubAllGlobals();
|
|
7
|
+
globalThis.fetch = mock(
|
|
8
|
+
async () => new Response(null, { status: 200 }),
|
|
9
|
+
) as unknown as typeof fetch;
|
|
15
10
|
});
|
|
11
|
+
afterEach(() => {});
|
|
16
12
|
|
|
17
13
|
describe("ResetPasswordScreen", () => {
|
|
18
14
|
test("ohne Token in URL UND ohne token-Prop → missing-token-Page", () => {
|
|
@@ -29,8 +25,8 @@ describe("ResetPasswordScreen", () => {
|
|
|
29
25
|
});
|
|
30
26
|
|
|
31
27
|
test("Passwort < 8 Zeichen → client-side error, kein fetch-Call", async () => {
|
|
32
|
-
const fetchMock =
|
|
33
|
-
|
|
28
|
+
const fetchMock = mock(async () => new Response(null, { status: 200 }));
|
|
29
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
34
30
|
|
|
35
31
|
renderWithProviders(<ResetPasswordScreen token="abc" />);
|
|
36
32
|
fireEvent.change(screen.getByLabelText(/^Neues Passwort/), { target: { value: "short" } });
|
|
@@ -59,8 +55,8 @@ describe("ResetPasswordScreen", () => {
|
|
|
59
55
|
});
|
|
60
56
|
|
|
61
57
|
test("happy path: gültiges Passwort → fetch-Call + success-State", async () => {
|
|
62
|
-
const fetchMock =
|
|
63
|
-
|
|
58
|
+
const fetchMock = mock(async () => new Response(null, { status: 200 }));
|
|
59
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
64
60
|
|
|
65
61
|
renderWithProviders(<ResetPasswordScreen token="abc-token" />);
|
|
66
62
|
fireEvent.change(screen.getByLabelText(/^Neues Passwort/), {
|
|
@@ -87,10 +83,9 @@ describe("ResetPasswordScreen", () => {
|
|
|
87
83
|
const errBody = JSON.stringify({
|
|
88
84
|
error: { code: "invalid_reset_token", details: { reason: "invalid_reset_token" } },
|
|
89
85
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
);
|
|
86
|
+
globalThis.fetch = mock(
|
|
87
|
+
async () => new Response(errBody, { status: 422 }),
|
|
88
|
+
) as unknown as typeof fetch;
|
|
94
89
|
|
|
95
90
|
renderWithProviders(<ResetPasswordScreen token="bad" />);
|
|
96
91
|
fireEvent.change(screen.getByLabelText(/^Neues Passwort/), {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Sonst sieht client andere roles als server → role-gating divergiert
|
|
4
4
|
// (entweder UI zeigt was nicht erlaubt ist, oder umgekehrt).
|
|
5
5
|
|
|
6
|
-
import { describe, expect, test } from "
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
7
|
import type { CurrentUserProfile, TenantSummary } from "../auth-client";
|
|
8
8
|
import { computeActiveRoles } from "../session";
|
|
9
9
|
|
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { screen } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
|
-
import { describe, expect, test } from "vitest";
|
|
5
4
|
import { TenantSwitcher } from "../tenant-switcher";
|
|
6
5
|
import { makeSessionApi, renderWithProviders } from "./test-utils";
|
|
7
6
|
|
|
8
7
|
// Radix-DropdownMenu reagiert auf pointerdown, nicht auf click — daher
|
|
9
8
|
// userEvent statt fireEvent.
|
|
10
|
-
|
|
11
9
|
describe("TenantSwitcher", () => {
|
|
12
10
|
test("renders nothing when user is null", () => {
|
|
13
11
|
const session = makeSessionApi({ status: "unauthenticated", user: null });
|
|
14
12
|
const { container } = renderWithProviders(<TenantSwitcher />, { session });
|
|
15
13
|
expect(container.firstChild).toBeNull();
|
|
16
14
|
});
|
|
17
|
-
|
|
18
15
|
test("renders nothing when user has only one tenant", () => {
|
|
19
16
|
const session = makeSessionApi({
|
|
20
17
|
tenants: [{ tenantId: "t1", roles: ["Admin"] }],
|
|
@@ -22,7 +19,6 @@ describe("TenantSwitcher", () => {
|
|
|
22
19
|
const { container } = renderWithProviders(<TenantSwitcher />, { session });
|
|
23
20
|
expect(container.firstChild).toBeNull();
|
|
24
21
|
});
|
|
25
|
-
|
|
26
22
|
test("renders trigger when user has multiple tenants", () => {
|
|
27
23
|
const session = makeSessionApi({
|
|
28
24
|
activeTenantId: "tenant-a",
|
|
@@ -35,7 +31,6 @@ describe("TenantSwitcher", () => {
|
|
|
35
31
|
// tenantName-Resolver liefert "Tenant tenant-a" als Trigger-Label
|
|
36
32
|
expect(screen.getByText("Tenant tenant-a")).toBeTruthy();
|
|
37
33
|
});
|
|
38
|
-
|
|
39
34
|
test("opens dropdown showing all memberships with roles", async () => {
|
|
40
35
|
const user = userEvent.setup();
|
|
41
36
|
const session = makeSessionApi({
|
|
@@ -57,7 +52,6 @@ describe("TenantSwitcher", () => {
|
|
|
57
52
|
expect(screen.getByText("Admin")).toBeTruthy();
|
|
58
53
|
expect(screen.getByText("User, Billing")).toBeTruthy();
|
|
59
54
|
});
|
|
60
|
-
|
|
61
55
|
test("clicking a tenant triggers switchTenant", async () => {
|
|
62
56
|
const user = userEvent.setup();
|
|
63
57
|
const session = makeSessionApi({
|
|
@@ -74,7 +68,6 @@ describe("TenantSwitcher", () => {
|
|
|
74
68
|
await user.click(screen.getByText("Tenant tenant-b"));
|
|
75
69
|
expect(session.switchTenant).toHaveBeenCalledWith("tenant-b");
|
|
76
70
|
});
|
|
77
|
-
|
|
78
71
|
test("clicking the active tenant is a no-op (closes menu, no switch call)", async () => {
|
|
79
72
|
const user = userEvent.setup();
|
|
80
73
|
const session = makeSessionApi({
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
1
|
//
|
|
3
2
|
// Shared test setup für die Web-UI-Components. Mountet das Minimum
|
|
4
3
|
// an Provider-Tree den die Components zur Laufzeit voraussetzen
|
|
5
4
|
// (LocaleProvider mit Bundle, SessionContext mit injizierbarem Wert).
|
|
6
5
|
|
|
6
|
+
import { mock } from "bun:test";
|
|
7
7
|
import type { LocaleResolver } from "@cosmicdrift/kumiko-headless";
|
|
8
8
|
import {
|
|
9
9
|
createStaticLocaleResolver,
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
import { defaultPrimitives } from "@cosmicdrift/kumiko-renderer-web";
|
|
14
14
|
import { render as _render, type RenderResult } from "@testing-library/react";
|
|
15
15
|
import type { ReactElement } from "react";
|
|
16
|
-
import { vi } from "vitest";
|
|
17
16
|
import { defaultTranslations } from "../../i18n";
|
|
18
17
|
import type { SessionApi, SessionState } from "../session";
|
|
19
18
|
import { SessionContext } from "../session";
|
|
@@ -23,13 +22,11 @@ import { SessionContext } from "../session";
|
|
|
23
22
|
// einen *anderen* Locale brauchen, übergeben ihren eigenen Resolver
|
|
24
23
|
// über options.resolver.
|
|
25
24
|
const sharedDeResolver = createStaticLocaleResolver({ locale: "de" });
|
|
26
|
-
|
|
27
25
|
export type MakeSessionApiOptions = Partial<SessionState> & {
|
|
28
26
|
readonly login?: SessionApi["login"];
|
|
29
27
|
readonly logout?: SessionApi["logout"];
|
|
30
28
|
readonly switchTenant?: SessionApi["switchTenant"];
|
|
31
29
|
};
|
|
32
|
-
|
|
33
30
|
export function makeSessionApi(overrides: MakeSessionApiOptions = {}): SessionApi {
|
|
34
31
|
const { login, logout, switchTenant, ...stateOverrides } = overrides;
|
|
35
32
|
const base: SessionState = {
|
|
@@ -47,12 +44,11 @@ export function makeSessionApi(overrides: MakeSessionApiOptions = {}): SessionAp
|
|
|
47
44
|
};
|
|
48
45
|
return {
|
|
49
46
|
...base,
|
|
50
|
-
login: login ??
|
|
51
|
-
logout: logout ??
|
|
52
|
-
switchTenant: switchTenant ??
|
|
47
|
+
login: login ?? mock<SessionApi["login"]>(async () => ({ ok: true })),
|
|
48
|
+
logout: logout ?? mock<SessionApi["logout"]>(async () => {}),
|
|
49
|
+
switchTenant: switchTenant ?? mock<SessionApi["switchTenant"]>(async () => {}),
|
|
53
50
|
};
|
|
54
51
|
}
|
|
55
|
-
|
|
56
52
|
export function renderWithProviders(
|
|
57
53
|
ui: ReactElement,
|
|
58
54
|
options: {
|
|
@@ -1,21 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { screen } from "@testing-library/react";
|
|
3
3
|
import userEvent from "@testing-library/user-event";
|
|
4
|
-
import { describe, expect, test } from "vitest";
|
|
5
4
|
import { UserMenu } from "../user-menu";
|
|
6
5
|
import { makeSessionApi, renderWithProviders } from "./test-utils";
|
|
7
6
|
|
|
8
7
|
// Radix-DropdownMenu reagiert auf pointerdown — fireEvent.click greift
|
|
9
8
|
// dort nicht. userEvent simuliert die volle Pointer-Sequenz und Radix
|
|
10
9
|
// öffnet sauber.
|
|
11
|
-
|
|
12
10
|
describe("UserMenu", () => {
|
|
13
11
|
test("renders nothing when user is null", () => {
|
|
14
12
|
const session = makeSessionApi({ status: "unauthenticated", user: null });
|
|
15
13
|
const { container } = renderWithProviders(<UserMenu />, { session });
|
|
16
14
|
expect(container.firstChild).toBeNull();
|
|
17
15
|
});
|
|
18
|
-
|
|
19
16
|
test("shows displayName + initials when authenticated", () => {
|
|
20
17
|
const session = makeSessionApi({
|
|
21
18
|
user: { id: "u1", email: "alice@example.com", displayName: "Alice Wonder", globalRoles: [] },
|
|
@@ -25,7 +22,6 @@ describe("UserMenu", () => {
|
|
|
25
22
|
expect(screen.getByText("AW")).toBeTruthy();
|
|
26
23
|
expect(screen.getByText("Alice Wonder")).toBeTruthy();
|
|
27
24
|
});
|
|
28
|
-
|
|
29
25
|
test("falls back to email-based initials when displayName empty", () => {
|
|
30
26
|
const session = makeSessionApi({
|
|
31
27
|
user: { id: "u1", email: "bob@example.com", displayName: "", globalRoles: [] },
|
|
@@ -34,7 +30,6 @@ describe("UserMenu", () => {
|
|
|
34
30
|
// Trim "" → leerer displayName → fallback auf email → erste 2 Chars
|
|
35
31
|
expect(screen.getByText("BO")).toBeTruthy();
|
|
36
32
|
});
|
|
37
|
-
|
|
38
33
|
test("opens dropdown on click and shows logout button", async () => {
|
|
39
34
|
const user = userEvent.setup();
|
|
40
35
|
const session = makeSessionApi();
|
|
@@ -43,13 +38,12 @@ describe("UserMenu", () => {
|
|
|
43
38
|
expect(screen.getByText("Abmelden")).toBeTruthy();
|
|
44
39
|
expect(screen.getByText("user@example.com")).toBeTruthy();
|
|
45
40
|
});
|
|
46
|
-
|
|
47
41
|
test("logout-click triggers session.logout", async () => {
|
|
48
42
|
const user = userEvent.setup();
|
|
49
43
|
const session = makeSessionApi();
|
|
50
44
|
renderWithProviders(<UserMenu />, { session });
|
|
51
45
|
await user.click(screen.getByRole("button", { name: /Test User/ }));
|
|
52
46
|
await user.click(screen.getByText("Abmelden"));
|
|
53
|
-
expect(session.logout).
|
|
47
|
+
expect(session.logout).toHaveBeenCalledTimes(1);
|
|
54
48
|
});
|
|
55
49
|
});
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
2
|
import { screen, waitFor } from "@testing-library/react";
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
4
3
|
import { VerifyEmailScreen } from "../verify-email-screen";
|
|
5
4
|
import { renderWithProviders } from "./test-utils";
|
|
6
5
|
|
|
7
6
|
beforeEach(() => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
);
|
|
12
|
-
});
|
|
13
|
-
afterEach(() => {
|
|
14
|
-
vi.unstubAllGlobals();
|
|
7
|
+
globalThis.fetch = mock(
|
|
8
|
+
async () => new Response(null, { status: 200 }),
|
|
9
|
+
) as unknown as typeof fetch;
|
|
15
10
|
});
|
|
11
|
+
afterEach(() => {});
|
|
16
12
|
|
|
17
13
|
describe("VerifyEmailScreen", () => {
|
|
18
14
|
test("ohne Token → missing-token-Page", () => {
|
|
@@ -21,8 +17,8 @@ describe("VerifyEmailScreen", () => {
|
|
|
21
17
|
});
|
|
22
18
|
|
|
23
19
|
test("mit Token + 200 → success-state nach auto-submit", async () => {
|
|
24
|
-
const fetchMock =
|
|
25
|
-
|
|
20
|
+
const fetchMock = mock(async () => new Response(null, { status: 200 }));
|
|
21
|
+
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
26
22
|
|
|
27
23
|
renderWithProviders(<VerifyEmailScreen token="t-abc" />);
|
|
28
24
|
|
|
@@ -45,10 +41,9 @@ describe("VerifyEmailScreen", () => {
|
|
|
45
41
|
details: { reason: "invalid_verification_token" },
|
|
46
42
|
},
|
|
47
43
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
);
|
|
44
|
+
globalThis.fetch = mock(
|
|
45
|
+
async () => new Response(errBody, { status: 422 }),
|
|
46
|
+
) as unknown as typeof fetch;
|
|
52
47
|
|
|
53
48
|
renderWithProviders(<VerifyEmailScreen token="bad" />);
|
|
54
49
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// Webhook-Handler-Factory (createSubscriptionWebhookHandler) wird in
|
|
12
12
|
// einem separaten Test mit Hono-mock geprüft.
|
|
13
13
|
|
|
14
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
14
15
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
15
16
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
16
17
|
import { createEventsTable, loadAggregate } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
type TestStack,
|
|
21
22
|
testTenantId,
|
|
22
23
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
23
|
-
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
24
24
|
import { subscriptionAggregateId } from "../aggregate-id";
|
|
25
25
|
import {
|
|
26
26
|
SubscriptionEventTypes,
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// ohne setupTestStack, weil der webhook-handler nur über die deps-
|
|
4
4
|
// injection geht und keinen DB-roundtrip braucht.
|
|
5
5
|
|
|
6
|
+
import { describe, expect, mock, test } from "bun:test";
|
|
6
7
|
import { Hono } from "hono";
|
|
7
|
-
import { describe, expect, test, vi } from "vitest";
|
|
8
8
|
import { SubscriptionEventTypes, SubscriptionStatuses } from "../constants";
|
|
9
9
|
import type { SubscriptionEvent, SubscriptionProviderPlugin } from "../types";
|
|
10
10
|
import { createSubscriptionWebhookHandler, type SubscriptionWebhookDeps } from "../webhook-handler";
|
|
@@ -65,7 +65,7 @@ async function postWebhook(app: Hono, providerName: string, body = '{"id":"evt_t
|
|
|
65
65
|
|
|
66
66
|
describe("webhook-handler — happy path", () => {
|
|
67
67
|
test("verifyAndParseWebhook → SubscriptionEvent → dispatchWrite → 200 processed", async () => {
|
|
68
|
-
const dispatchWrite =
|
|
68
|
+
const dispatchWrite = mock(async () => ({
|
|
69
69
|
isSuccess: true,
|
|
70
70
|
data: { duplicate: false, eventAggregateId: "evt-id" },
|
|
71
71
|
}));
|
|
@@ -77,7 +77,8 @@ describe("webhook-handler — happy path", () => {
|
|
|
77
77
|
expect(body.processed).toBe(true);
|
|
78
78
|
expect(body.duplicate).toBe(false);
|
|
79
79
|
|
|
80
|
-
expect(dispatchWrite).
|
|
80
|
+
expect(dispatchWrite).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(dispatchWrite).toHaveBeenCalledWith(
|
|
81
82
|
expect.objectContaining({
|
|
82
83
|
handlerQn: "billing-foundation:write:process-event",
|
|
83
84
|
tenantId: "tenant-test",
|
|
@@ -92,7 +93,7 @@ describe("webhook-handler — happy path", () => {
|
|
|
92
93
|
});
|
|
93
94
|
|
|
94
95
|
test("plugin returns null (= unbekannter event-type) → 200 ignored, kein dispatch", async () => {
|
|
95
|
-
const dispatchWrite =
|
|
96
|
+
const dispatchWrite = mock();
|
|
96
97
|
const plugin = buildPlugin({ verifyAndParseWebhook: async () => null });
|
|
97
98
|
const app = buildApp(buildDeps({ dispatchWrite, resolveProvider: () => plugin }));
|
|
98
99
|
|
|
@@ -137,7 +138,7 @@ describe("webhook-handler — error paths", () => {
|
|
|
137
138
|
});
|
|
138
139
|
|
|
139
140
|
test("dispatchWrite returns isSuccess: false → 500 mit subscription_webhook_processing_failed", async () => {
|
|
140
|
-
const dispatchWrite =
|
|
141
|
+
const dispatchWrite = mock(async () => ({
|
|
141
142
|
isSuccess: false,
|
|
142
143
|
error: { code: "internal_error", message: "DB unavailable" },
|
|
143
144
|
}));
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
+
import type { DbRunner } from "@cosmicdrift/kumiko-framework/db";
|
|
3
|
+
|
|
4
|
+
export async function upsertSubscriptionProjectionRow(
|
|
5
|
+
tx: DbRunner,
|
|
6
|
+
tableName: string,
|
|
7
|
+
insertCols: Record<string, unknown>,
|
|
8
|
+
setClauses: readonly string[],
|
|
9
|
+
params: readonly unknown[],
|
|
10
|
+
): Promise<void> {
|
|
11
|
+
const insertKeys = Object.keys(insertCols);
|
|
12
|
+
const insertPlaceholders = insertKeys.map((_, i) => `$${i + 1}`);
|
|
13
|
+
const sqlText = `INSERT INTO "${tableName}" (${insertKeys.map((k) => `"${k}"`).join(", ")}) VALUES (${insertPlaceholders.join(", ")}) ON CONFLICT ("id") DO UPDATE SET ${setClauses.join(", ")}`;
|
|
14
|
+
await asRawClient(tx).unsafe(sqlText, params);
|
|
15
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Resolver-helper: liest die current subscription-row für einen Tenant
|
|
2
2
|
// aus der read_subscriptions-projection.
|
|
3
3
|
|
|
4
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
4
5
|
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
-
import { eq } from "drizzle-orm";
|
|
6
6
|
import { subscriptionAggregateId } from "./aggregate-id";
|
|
7
7
|
import { subscriptionsProjectionTable } from "./projection";
|
|
8
8
|
|
|
@@ -22,11 +22,7 @@ export async function getSubscriptionForTenant(
|
|
|
22
22
|
tenantId: string,
|
|
23
23
|
): Promise<SubscriptionView | null> {
|
|
24
24
|
const aggId = subscriptionAggregateId(tenantId);
|
|
25
|
-
const [row] = await ctx.db
|
|
26
|
-
.select()
|
|
27
|
-
.from(subscriptionsProjectionTable)
|
|
28
|
-
.where(eq(subscriptionsProjectionTable["id"], aggId))
|
|
29
|
-
.limit(1);
|
|
25
|
+
const [row] = await selectMany(ctx.db, subscriptionsProjectionTable, { id: aggId }, { limit: 1 });
|
|
30
26
|
if (!row) return null;
|
|
31
27
|
// @cast-boundary db-row — drizzle-row carries column-as-unknown
|
|
32
28
|
return {
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// kann nicht zum Portal eines OTHER Providers, weil der ihn nicht
|
|
10
10
|
// kennt.
|
|
11
11
|
|
|
12
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
12
13
|
import type { WriteHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
13
|
-
import { eq } from "drizzle-orm";
|
|
14
14
|
import { z } from "zod";
|
|
15
15
|
import { subscriptionAggregateId } from "../aggregate-id";
|
|
16
16
|
import { SUBSCRIPTION_PROVIDER_EXTENSION } from "../constants";
|
|
@@ -34,7 +34,7 @@ export const createPortalSessionHandler: WriteHandlerDef = {
|
|
|
34
34
|
// 1. Hol current subscription-row für den Tenant. Aggregate-id ist
|
|
35
35
|
// deterministic per tenant — eine row pro tenant.
|
|
36
36
|
const subAggId = subscriptionAggregateId(tenantId);
|
|
37
|
-
const rows = await ctx.db
|
|
37
|
+
const rows = await selectMany(ctx.db, subTable, { id: subAggId }, { limit: 1 });
|
|
38
38
|
const row = rows[0];
|
|
39
39
|
if (!row) {
|
|
40
40
|
throw new Error(
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// subscription via getSubscriptionForTenant-helper (= ctx.db ist
|
|
4
4
|
// tenant-scoped, gibt automatisch nur die row des Callers zurück).
|
|
5
5
|
|
|
6
|
+
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
6
7
|
import type { QueryHandlerDef } from "@cosmicdrift/kumiko-framework/engine";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
import { subscriptionsProjectionTable } from "../projection";
|
|
@@ -14,7 +15,9 @@ export const listSubscriptionsQuery: QueryHandlerDef = {
|
|
|
14
15
|
schema: listSchema,
|
|
15
16
|
access: { roles: ["SystemAdmin", "TenantAdmin"] },
|
|
16
17
|
handler: async (_query, ctx) => {
|
|
17
|
-
const rows = await ctx.db.
|
|
18
|
+
const rows = await selectMany(ctx.db.raw, subscriptionsProjectionTable, {
|
|
19
|
+
tenantId: ctx.user.tenantId,
|
|
20
|
+
});
|
|
18
21
|
return { rows };
|
|
19
22
|
},
|
|
20
23
|
};
|