Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 101 additions & 5 deletions packages/stack-shared/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,25 +181,121 @@ export const branchPaymentsSchema = yupObject({
'Product customer type must match its product line customer type',
function(this: yup.TestContext<yup.AnyObject>, value) {
if (!value) return true;
for (const [productId, product] of Object.entries(value.products)) {
if (!product.productLineId) continue;
const productLine = getOrUndefined(value.productLines, product.productLineId);
const products = value.products;
if (!isObjectLike(products)) return true;

const productLines = value.productLines;
for (const [productId, product] of Object.entries(products)) {
if (!isObjectLike(product)) continue;
const productLineId = product.productLineId;
if (typeof productLineId !== "string" || productLineId.length === 0) continue;
const productLine = isObjectLike(productLines) ? getOrUndefined(productLines, productLineId) : undefined;
if (productLine === undefined) {
return this.createError({
message: `Product "${productId}" specifies product line ID "${product.productLineId}", but that product line does not exist`,
message: `Product "${productId}" specifies product line ID "${productLineId}", but that product line does not exist`,
path: `${this.path}.products.${productId}.productLineId`,
});
}
if (!isObjectLike(productLine)) continue;
if (product.customerType !== productLine.customerType) {
return this.createError({
message: `Product "${productId}" has customer type "${product.customerType}" but its product line "${product.productLineId}" has customer type "${productLine.customerType}"`,
message: `Product "${productId}" has customer type "${product.customerType}" but its product line "${productLineId}" has customer type "${productLine.customerType}"`,
path: `${this.path}.products.${productId}.customerType`,
});
}
}
return true;
}
);
import.meta.vitest?.test("branchPaymentsSchema accepts partial payments config without products", async ({ expect }) => {
await expect(branchPaymentsSchema.validate({
blockNewPurchases: true,
}, { abortEarly: false })).resolves.toMatchObject({
blockNewPurchases: true,
});
});

import.meta.vitest?.test("branchPaymentsSchema accepts product lines without products", async ({ expect }) => {
await expect(branchPaymentsSchema.validate({
productLines: {
pro: {
displayName: "Pro",
customerType: "user",
},
},
}, { abortEarly: false })).resolves.toMatchObject({
productLines: {
pro: {
displayName: "Pro",
customerType: "user",
},
},
});
});

import.meta.vitest?.test("branchPaymentsSchema rejects a product that references a missing product line", async ({ expect }) => {
await expect(branchPaymentsSchema.validate({
products: {
pro: {
customerType: "user",
productLineId: "missing-line",
prices: "include-by-default",
},
},
}, { abortEarly: false })).rejects.toThrowErrorMatchingInlineSnapshot(`[ValidationError: Product "pro" specifies product line ID "missing-line", but that product line does not exist]`);
});

import.meta.vitest?.test("branchPaymentsSchema rejects null product entries without throwing a raw TypeError", async ({ expect }) => {
await expect(branchPaymentsSchema.validate({
products: {
pro: null,
},
}, { abortEarly: false })).rejects.toThrowErrorMatchingInlineSnapshot(`[ValidationError: products cannot be null]`);
});

import.meta.vitest?.test("branchPaymentsSchema rejects null product line entries without throwing a raw TypeError", async ({ expect }) => {
await expect(branchPaymentsSchema.validate({
productLines: {
teamLine: null,
},
products: {
pro: {
customerType: "user",
productLineId: "teamLine",
prices: "include-by-default",
},
},
}, { abortEarly: false })).rejects.toThrowErrorMatchingInlineSnapshot(`[ValidationError: productLines cannot be null]`);
});

import.meta.vitest?.test("branchPaymentsSchema rejects a product whose customer type differs from its product line", async ({ expect }) => {
await expect(branchPaymentsSchema.validate({
productLines: {
teamLine: {
customerType: "team",
},
},
products: {
pro: {
customerType: "user",
productLineId: "teamLine",
prices: "include-by-default",
},
},
}, { abortEarly: false })).rejects.toThrowErrorMatchingInlineSnapshot(`[ValidationError: Product "pro" has customer type "user" but its product line "teamLine" has customer type "team"]`);
});

import.meta.vitest?.test("branchPaymentsSchema lets productLineId schema reject empty IDs", async ({ expect }) => {
await expect(branchPaymentsSchema.validate({
products: {
pro: {
customerType: "user",
productLineId: "",
prices: "include-by-default",
},
},
}, { abortEarly: false })).rejects.toThrowErrorMatchingInlineSnapshot(`[ValidationError: productLineId must contain only letters, numbers, underscores, and hyphens, and not start with a hyphen]`);
});

const branchDomain = yupObject({});

Expand Down
Loading