@cosmicdrift/kumiko-bundled-features 0.319.0 → 0.321.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 +9 -9
- package/src/auth-email-password/__tests__/auth-tenants-rate-limit.integration.test.ts +3 -0
- package/src/auth-email-password/__tests__/post-auth-landing.integration.test.ts +321 -0
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +4 -0
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +4 -0
- package/src/auth-email-password/__tests__/signup-flow.integration.test.ts +21 -1
- package/src/auth-email-password/__tests__/signup-handover.integration.test.ts +10 -0
- package/src/auth-email-password/auth-paths.ts +10 -6
- package/src/auth-email-password/changes.json +12 -0
- package/src/auth-email-password/index.ts +1 -1
- package/src/auth-email-password/web/__tests__/auth-form-logic.test.ts +34 -1
- package/src/auth-email-password/web/__tests__/invite-accept-screen.test.tsx +30 -0
- package/src/auth-email-password/web/__tests__/signup-complete-screen.test.tsx +26 -0
- package/src/auth-email-password/web/auth-client.ts +16 -1
- package/src/auth-email-password/web/auth-form-logic.ts +10 -0
- package/src/auth-email-password/web/invite-accept-screen.tsx +10 -5
- package/src/auth-email-password/web/signup-complete-screen.tsx +11 -7
- package/src/auth-mfa/web/mfa-client.ts +18 -2
- package/src/billing-foundation/__tests__/billing-plans.integration.test.ts +27 -0
- package/src/billing-foundation/__tests__/checkout-core.test.ts +35 -0
- package/src/billing-foundation/__tests__/sync-subscription.integration.test.ts +469 -0
- package/src/billing-foundation/changes.json +14 -0
- package/src/billing-foundation/checkout-core.ts +8 -4
- package/src/billing-foundation/constants.ts +6 -0
- package/src/billing-foundation/feature.ts +37 -2
- package/src/billing-foundation/handlers/process-event.write.ts +112 -106
- package/src/billing-foundation/handlers/switch-plan.write.ts +7 -0
- package/src/billing-foundation/handlers/sync-subscription.write.ts +164 -0
- package/src/billing-foundation/i18n.ts +9 -0
- package/src/billing-foundation/index.ts +1 -0
- package/src/billing-foundation/plan-catalog.ts +22 -5
- package/src/billing-foundation/types.ts +27 -0
- package/src/billing-foundation/web/__tests__/billing-plans-panel.test.tsx +28 -0
- package/src/billing-foundation/web/billing-plans-panel.tsx +8 -1
- package/src/channel-email/__tests__/email-channel.test.ts +92 -0
- package/src/channel-email/__tests__/smtp-transport-pinning.test.ts +42 -0
- package/src/channel-email/changes.json +9 -1
- package/src/channel-email/email-channel.ts +31 -6
- package/src/channel-email/smtp-transport.ts +7 -0
- package/src/delivery/__tests__/delivery.integration.test.ts +101 -26
- package/src/delivery/changes.json +7 -0
- package/src/delivery/feature.ts +1 -1
- package/src/delivery/handlers/unsubscribe-address.write.ts +1 -1
- package/src/delivery/handlers/unsubscribe-user.write.ts +1 -1
- package/src/delivery/index.ts +2 -1
- package/src/delivery/public-names.ts +1 -1
- package/src/delivery/unsubscribe.ts +167 -89
- package/src/file-derivatives/feature.ts +1 -1
- package/src/file-derivatives/handlers/public-variant.query.ts +5 -7
- package/src/foundation-shared/__tests__/mail-host-policy.test.ts +68 -0
- package/src/foundation-shared/index.ts +9 -0
- package/src/foundation-shared/mail-host-policy.ts +70 -0
- package/src/inbound-provider-imap/__tests__/imap-foundation.integration.test.ts +11 -0
- package/src/inbound-provider-imap/__tests__/imap-live.integration.test.ts +14 -1
- package/src/inbound-provider-imap/__tests__/plugin-mocked.test.ts +88 -3
- package/src/inbound-provider-imap/changes.json +14 -1
- package/src/inbound-provider-imap/feature.ts +6 -3
- package/src/inbound-provider-imap/imap-client.ts +60 -2
- package/src/inbound-provider-imap/index.ts +1 -1
- package/src/ledger/__tests__/ledger.integration.test.ts +80 -0
- package/src/ledger/changes.json +9 -1
- package/src/ledger/entity.ts +14 -1
- package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +86 -1
- package/src/mail-transport-smtp/__tests__/feature.test.ts +123 -3
- package/src/mail-transport-smtp/changes.json +14 -1
- package/src/mail-transport-smtp/feature.ts +76 -1
- package/src/mail-transport-smtp/index.ts +6 -1
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +59 -0
- package/src/sessions/__tests__/sessions.integration.test.ts +5 -0
- package/src/shared/password-hashing.test.ts +21 -13
- package/src/step-dispatcher/__tests__/feature.boot.test.ts +9 -2
- package/src/step-dispatcher/__tests__/webhook-runner.test.ts +283 -0
- package/src/step-dispatcher/changes.json +21 -1
- package/src/step-dispatcher/feature.ts +63 -15
- package/src/step-dispatcher/index.ts +11 -2
- package/src/step-dispatcher/webhook-runner.ts +157 -31
- package/src/subscription-stripe/__tests__/plugin-methods.test.ts +117 -0
- package/src/subscription-stripe/changes.json +6 -0
- package/src/subscription-stripe/feature.ts +5 -1
- package/src/subscription-stripe/plugin-methods.ts +53 -0
- package/src/subscription-stripe/verify-webhook.ts +52 -28
- package/src/tenant-handover/__tests__/claim.integration.test.ts +62 -4
- package/src/tenant-handover/changes.json +7 -0
- package/src/tenant-handover/handlers/claim.write.ts +1 -0
- package/src/tenant-handover/move-entity-graph.ts +30 -44
- package/src/user-data-rights/__tests__/anonymous-deletion.integration.test.ts +11 -0
- package/src/user-data-rights/__tests__/download-by-token-ip-bucket.integration.test.ts +170 -0
- package/src/user-data-rights/__tests__/download.integration.test.ts +12 -6
- package/src/user-data-rights/__tests__/extract-audit-meta.test.ts +36 -0
- package/src/user-data-rights/changes.json +6 -0
- package/src/user-data-rights/feature.ts +35 -37
|
@@ -33,6 +33,9 @@ export type LoginResponse = {
|
|
|
33
33
|
readonly tenantId: string;
|
|
34
34
|
readonly roles: readonly string[];
|
|
35
35
|
};
|
|
36
|
+
// Present only when the server's auth.postAuthLanding resolver returned a
|
|
37
|
+
// valid path — see auth-routes.ts. Screens prefer this over loggedInHref.
|
|
38
|
+
readonly landingPath?: string;
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
// Reason codes the login-handler is known to emit today (login-screen.tsx's
|
|
@@ -93,6 +96,14 @@ export type LoginResult =
|
|
|
93
96
|
| { readonly kind: "mfa-setup-required"; readonly preauthSetupToken: string }
|
|
94
97
|
| { readonly kind: "failure"; readonly error: LoginFailure };
|
|
95
98
|
|
|
99
|
+
function toLoginResponse(
|
|
100
|
+
token: string,
|
|
101
|
+
user: LoginResponse["user"],
|
|
102
|
+
landingPath: unknown,
|
|
103
|
+
): LoginResponse {
|
|
104
|
+
return typeof landingPath === "string" ? { token, user, landingPath } : { token, user };
|
|
105
|
+
}
|
|
106
|
+
|
|
96
107
|
// POST /api/auth/login. Success → token + user; MFA-enrolled user → a
|
|
97
108
|
// challenge token the caller completes via auth-mfa's verify screen;
|
|
98
109
|
// unenrolled user blocked by enforcement policy → mfa-setup-required;
|
|
@@ -118,6 +129,7 @@ export async function login(req: LoginRequest): Promise<LoginResult> {
|
|
|
118
129
|
challengeToken?: string;
|
|
119
130
|
mfaSetupRequired?: boolean;
|
|
120
131
|
preauthSetupToken?: string;
|
|
132
|
+
landingPath?: string;
|
|
121
133
|
error?:
|
|
122
134
|
| {
|
|
123
135
|
code?: string;
|
|
@@ -140,7 +152,7 @@ export async function login(req: LoginRequest): Promise<LoginResult> {
|
|
|
140
152
|
return { kind: "failure", error: { reason: "mfa_setup_required" } };
|
|
141
153
|
}
|
|
142
154
|
if (body.token !== undefined && body.user !== undefined) {
|
|
143
|
-
return { kind: "success", data:
|
|
155
|
+
return { kind: "success", data: toLoginResponse(body.token, body.user, body.landingPath) };
|
|
144
156
|
}
|
|
145
157
|
}
|
|
146
158
|
// Der Server schickt error entweder als string ("invalid_body") oder als
|
|
@@ -343,6 +355,9 @@ export type SignupConfirmSuccess = {
|
|
|
343
355
|
// Present only when a bound handover grant existed and its claim
|
|
344
356
|
// succeeded — see signup-confirm.write.ts.
|
|
345
357
|
readonly handover?: { readonly entityType: string; readonly id: string };
|
|
358
|
+
// Present only when the server's auth.postAuthLanding resolver returned a
|
|
359
|
+
// valid path — see auth-routes.ts.
|
|
360
|
+
readonly landingPath?: string;
|
|
346
361
|
};
|
|
347
362
|
|
|
348
363
|
export async function confirmSignup(
|
|
@@ -28,3 +28,13 @@ export function resolveLoggedInHref<Args>(
|
|
|
28
28
|
): string {
|
|
29
29
|
return typeof href === "function" ? href(args) : href;
|
|
30
30
|
}
|
|
31
|
+
|
|
32
|
+
/** The server's `landingPath` (auth.postAuthLanding) wins over the screen's
|
|
33
|
+
* loggedInHref prop — that prop is a deprecated per-app fallback. */
|
|
34
|
+
export function resolvePostAuthHref<Args>(
|
|
35
|
+
landingPath: string | undefined,
|
|
36
|
+
href: string | ((args: Args) => string),
|
|
37
|
+
args: Args,
|
|
38
|
+
): string {
|
|
39
|
+
return landingPath ?? resolveLoggedInHref(href, args);
|
|
40
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
20
20
|
import { type FormEvent, type ReactNode, useContext, useState } from "react";
|
|
21
21
|
import { csrfHeader } from "./auth-client";
|
|
22
|
-
import {
|
|
22
|
+
import { resolvePostAuthHref } from "./auth-form-logic";
|
|
23
23
|
import { AuthCard, useUrlToken } from "./auth-form-primitives";
|
|
24
24
|
import { SessionContext, UNAUTHENTICATED } from "./session";
|
|
25
25
|
|
|
@@ -29,7 +29,9 @@ export type InviteAcceptScreenProps = {
|
|
|
29
29
|
/** Where to redirect on success. Default "/" — multi-tenant apps can pass
|
|
30
30
|
* `(data) => "/${data.tenantId}/"`. Function-form receives the roles this
|
|
31
31
|
* flow grants in the target tenant; branch 1 reports the invitation's
|
|
32
|
-
* role, which for an existing member is not their full role set there.
|
|
32
|
+
* role, which for an existing member is not their full role set there.
|
|
33
|
+
* @deprecated Configure auth.postAuthLanding on the server; its landingPath wins.
|
|
34
|
+
* Kept as fallback. */
|
|
33
35
|
readonly loggedInHref?:
|
|
34
36
|
| string
|
|
35
37
|
| ((args: { tenantId: string; roles: readonly string[] }) => string);
|
|
@@ -46,6 +48,9 @@ type InviteAcceptResponse = {
|
|
|
46
48
|
readonly tenantId: string;
|
|
47
49
|
readonly role?: string;
|
|
48
50
|
readonly user?: { readonly roles?: readonly string[] };
|
|
51
|
+
// Present only when the server's auth.postAuthLanding resolver returned a
|
|
52
|
+
// valid path — see auth-routes.ts.
|
|
53
|
+
readonly landingPath?: string;
|
|
49
54
|
};
|
|
50
55
|
|
|
51
56
|
function grantedRoles(data: InviteAcceptResponse): readonly string[] {
|
|
@@ -92,7 +97,7 @@ export function InviteAcceptScreen({
|
|
|
92
97
|
// @cast-boundary engine-payload — auth route JSON
|
|
93
98
|
const data = (await res.json()) as InviteAcceptResponse;
|
|
94
99
|
window.location.assign(
|
|
95
|
-
|
|
100
|
+
resolvePostAuthHref(data.landingPath, loggedInHref, {
|
|
96
101
|
tenantId: data.tenantId,
|
|
97
102
|
roles: grantedRoles(data),
|
|
98
103
|
}),
|
|
@@ -117,7 +122,7 @@ export function InviteAcceptScreen({
|
|
|
117
122
|
// @cast-boundary engine-payload — auth route JSON
|
|
118
123
|
const data = (await res.json()) as InviteAcceptResponse;
|
|
119
124
|
window.location.assign(
|
|
120
|
-
|
|
125
|
+
resolvePostAuthHref(data.landingPath, loggedInHref, {
|
|
121
126
|
tenantId: data.tenantId,
|
|
122
127
|
roles: grantedRoles(data),
|
|
123
128
|
}),
|
|
@@ -142,7 +147,7 @@ export function InviteAcceptScreen({
|
|
|
142
147
|
// @cast-boundary engine-payload — auth route JSON
|
|
143
148
|
const data = (await res.json()) as InviteAcceptResponse;
|
|
144
149
|
window.location.assign(
|
|
145
|
-
|
|
150
|
+
resolvePostAuthHref(data.landingPath, loggedInHref, {
|
|
146
151
|
tenantId: data.tenantId,
|
|
147
152
|
roles: grantedRoles(data),
|
|
148
153
|
}),
|
|
@@ -10,15 +10,17 @@
|
|
|
10
10
|
// server-side token pass `token` as a prop instead.
|
|
11
11
|
//
|
|
12
12
|
// On success: shows a confirmation (account active, signed in) with a
|
|
13
|
-
// button to
|
|
14
|
-
// server already logged the user in via cookies, but a silent redirect
|
|
15
|
-
// leaves no signal that activation worked.
|
|
16
|
-
//
|
|
13
|
+
// button to the continue target, instead of navigating away immediately —
|
|
14
|
+
// the server already logged the user in via cookies, but a silent redirect
|
|
15
|
+
// leaves no signal that activation worked. Server-side auth.postAuthLanding
|
|
16
|
+
// (landingPath) wins over loggedInHref; loggedInHref is the deprecated
|
|
17
|
+
// per-app fallback. Default pattern is "/" — apps with multi-tenant routing
|
|
18
|
+
// pass `(data) => "/" + data.tenantKey + "/"`.
|
|
17
19
|
|
|
18
20
|
import { usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
|
|
19
21
|
import { type FormEvent, type ReactNode, useState } from "react";
|
|
20
22
|
import { confirmSignup, type SignupConfirmSuccess } from "./auth-client";
|
|
21
|
-
import { passwordPairIssue,
|
|
23
|
+
import { passwordPairIssue, resolvePostAuthHref } from "./auth-form-logic";
|
|
22
24
|
import { AuthCard, useUrlToken } from "./auth-form-primitives";
|
|
23
25
|
|
|
24
26
|
export type SignupCompleteScreenProps = {
|
|
@@ -27,7 +29,9 @@ export type SignupCompleteScreenProps = {
|
|
|
27
29
|
readonly token?: string;
|
|
28
30
|
/** Where to send the user after activation. Function-form receives the tenantKey, the
|
|
29
31
|
* roles granted by this flow in that tenant, and the claimed `handover` entity when
|
|
30
|
-
* signup claimed a try-first grant. Default "/".
|
|
32
|
+
* signup claimed a try-first grant. Default "/".
|
|
33
|
+
* @deprecated Configure auth.postAuthLanding on the server; its landingPath wins.
|
|
34
|
+
* Kept as fallback. */
|
|
31
35
|
readonly loggedInHref?:
|
|
32
36
|
| string
|
|
33
37
|
| ((args: {
|
|
@@ -73,7 +77,7 @@ export function SignupCompleteScreen({
|
|
|
73
77
|
// explicit continue button instead of navigating away silently —
|
|
74
78
|
// the user otherwise gets no signal that activation worked.
|
|
75
79
|
setContinueHref(
|
|
76
|
-
|
|
80
|
+
resolvePostAuthHref(res.data.landingPath, loggedInHref, {
|
|
77
81
|
tenantKey: res.data.tenantKey,
|
|
78
82
|
roles: res.data.user.roles,
|
|
79
83
|
...(res.data.handover !== undefined && { handover: res.data.handover }),
|
|
@@ -32,6 +32,7 @@ export async function verifyMfaChallenge(
|
|
|
32
32
|
isSuccess?: boolean;
|
|
33
33
|
token?: string;
|
|
34
34
|
user?: LoginResponse["user"];
|
|
35
|
+
landingPath?: string;
|
|
35
36
|
error?:
|
|
36
37
|
| {
|
|
37
38
|
code?: string;
|
|
@@ -41,7 +42,14 @@ export async function verifyMfaChallenge(
|
|
|
41
42
|
| string;
|
|
42
43
|
};
|
|
43
44
|
if (body.isSuccess === true && body.token !== undefined && body.user !== undefined) {
|
|
44
|
-
return {
|
|
45
|
+
return {
|
|
46
|
+
kind: "success",
|
|
47
|
+
data: {
|
|
48
|
+
token: body.token,
|
|
49
|
+
user: body.user,
|
|
50
|
+
...(typeof body.landingPath === "string" && { landingPath: body.landingPath }),
|
|
51
|
+
},
|
|
52
|
+
};
|
|
45
53
|
}
|
|
46
54
|
const err = body.error;
|
|
47
55
|
if (typeof err === "string") {
|
|
@@ -156,6 +164,7 @@ export async function confirmMfaSetupPreauth(
|
|
|
156
164
|
isSuccess?: boolean;
|
|
157
165
|
token?: string;
|
|
158
166
|
user?: LoginResponse["user"];
|
|
167
|
+
landingPath?: string;
|
|
159
168
|
error?:
|
|
160
169
|
| {
|
|
161
170
|
code?: string;
|
|
@@ -165,7 +174,14 @@ export async function confirmMfaSetupPreauth(
|
|
|
165
174
|
| string;
|
|
166
175
|
};
|
|
167
176
|
if (body.isSuccess === true && body.token !== undefined && body.user !== undefined) {
|
|
168
|
-
return {
|
|
177
|
+
return {
|
|
178
|
+
kind: "success",
|
|
179
|
+
data: {
|
|
180
|
+
token: body.token,
|
|
181
|
+
user: body.user,
|
|
182
|
+
...(typeof body.landingPath === "string" && { landingPath: body.landingPath }),
|
|
183
|
+
},
|
|
184
|
+
};
|
|
169
185
|
}
|
|
170
186
|
const err = body.error;
|
|
171
187
|
if (typeof err === "string") {
|
|
@@ -232,6 +232,7 @@ async function createSubscription(
|
|
|
232
232
|
tier: string;
|
|
233
233
|
providerSubscriptionId: string;
|
|
234
234
|
providerCustomerId: string;
|
|
235
|
+
cancelAtIso: string | null;
|
|
235
236
|
}> = {},
|
|
236
237
|
onStack: TestStack = stack,
|
|
237
238
|
) {
|
|
@@ -247,6 +248,7 @@ async function createSubscription(
|
|
|
247
248
|
status: overrides.status ?? SubscriptionStatuses.active,
|
|
248
249
|
tier: overrides.tier ?? "starter",
|
|
249
250
|
currentPeriodEndIso: "2026-06-01T00:00:00Z",
|
|
251
|
+
...(overrides.cancelAtIso !== undefined && { cancelAtIso: overrides.cancelAtIso }),
|
|
250
252
|
rawPayload: '{"raw":"payload"}',
|
|
251
253
|
},
|
|
252
254
|
admin,
|
|
@@ -503,6 +505,31 @@ describe("switch-plan", () => {
|
|
|
503
505
|
expect(error.httpStatus).toBe(409);
|
|
504
506
|
});
|
|
505
507
|
|
|
508
|
+
test("switching a subscription that is scheduled to cancel is a conflict, and the query offers unavailable instead of switch", async () => {
|
|
509
|
+
const admin = adminFor(7034);
|
|
510
|
+
await createSubscription(admin.tenantId, {
|
|
511
|
+
tier: "starter",
|
|
512
|
+
providerSubscriptionId: "sub_switch_7034",
|
|
513
|
+
cancelAtIso: "2026-07-01T00:00:00Z",
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const result = (await stack.http.queryOk(
|
|
517
|
+
"billing-foundation:query:billing-plans",
|
|
518
|
+
{},
|
|
519
|
+
admin,
|
|
520
|
+
)) as { plans: Array<{ tier: string; action: string }> };
|
|
521
|
+
expect(result.plans.find((p) => p.tier === "pro")?.action).toBe("unavailable");
|
|
522
|
+
|
|
523
|
+
const error = await stack.http.writeErr(
|
|
524
|
+
SubscriptionFoundationHandlers.switchPlan,
|
|
525
|
+
{ tier: "pro" },
|
|
526
|
+
admin,
|
|
527
|
+
);
|
|
528
|
+
expect(error.httpStatus).toBe(409);
|
|
529
|
+
expect(error.i18nKey).toBe("billing-foundation.errors.cancellationScheduled");
|
|
530
|
+
expect(switchCalls).toHaveLength(0);
|
|
531
|
+
});
|
|
532
|
+
|
|
506
533
|
test("the provider rejecting the switch because two tiers share one Stripe product surfaces as a 422, not a 500", async () => {
|
|
507
534
|
const admin = adminFor(7024);
|
|
508
535
|
await createSubscription(admin.tenantId, {
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
3
|
import { UnconfiguredError, UnprocessableError } from "@cosmicdrift/kumiko-framework/errors";
|
|
3
4
|
// Aliased — an un-aliased `Temporal` would shadow the ambient global
|
|
4
5
|
// `Temporal` TYPE `SubscriptionView.lastChangedAt` resolves against.
|
|
5
6
|
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
6
7
|
import {
|
|
7
8
|
assertRedirectOrigins,
|
|
9
|
+
isBillingEnabled,
|
|
8
10
|
isNonEmptyStringArray,
|
|
9
11
|
isOwnProviderCustomer,
|
|
10
12
|
joinBaseUrl,
|
|
11
13
|
} from "../checkout-core";
|
|
12
14
|
import type { SubscriptionView } from "../get-subscription-for-tenant";
|
|
15
|
+
import type { SubscriptionProviderPlugin } from "../types";
|
|
13
16
|
|
|
14
17
|
function subscriptionView(overrides: Partial<SubscriptionView> = {}): SubscriptionView {
|
|
15
18
|
return {
|
|
@@ -151,3 +154,35 @@ describe("isOwnProviderCustomer", () => {
|
|
|
151
154
|
expect(isOwnProviderCustomer(subscriptionView(), "mock", "cus_own")).toBe(true);
|
|
152
155
|
});
|
|
153
156
|
});
|
|
157
|
+
|
|
158
|
+
function fakeCtxWithProvider(
|
|
159
|
+
entries: ReadonlyArray<{ readonly entityName: string; readonly options: unknown }>,
|
|
160
|
+
): HandlerContext {
|
|
161
|
+
return {
|
|
162
|
+
registry: {
|
|
163
|
+
getExtensionUsages: () => entries,
|
|
164
|
+
},
|
|
165
|
+
} as unknown as HandlerContext; // @cast-boundary test-fixture — only registry is exercised
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
describe("isBillingEnabled", () => {
|
|
169
|
+
test("false when no provider with that name is registered", async () => {
|
|
170
|
+
const ctx = fakeCtxWithProvider([]);
|
|
171
|
+
expect(await isBillingEnabled(ctx, "stripe")).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("false when the registered plugin's own isBillingEnabled reports false", async () => {
|
|
175
|
+
const plugin: SubscriptionProviderPlugin = {
|
|
176
|
+
verifyAndParseWebhook: async () => null,
|
|
177
|
+
isBillingEnabled: async () => false,
|
|
178
|
+
};
|
|
179
|
+
const ctx = fakeCtxWithProvider([{ entityName: "stripe", options: plugin }]);
|
|
180
|
+
expect(await isBillingEnabled(ctx, "stripe")).toBe(false);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("true when the registered plugin has no isBillingEnabled (defaults to enabled)", async () => {
|
|
184
|
+
const plugin: SubscriptionProviderPlugin = { verifyAndParseWebhook: async () => null };
|
|
185
|
+
const ctx = fakeCtxWithProvider([{ entityName: "stripe", options: plugin }]);
|
|
186
|
+
expect(await isBillingEnabled(ctx, "stripe")).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
});
|