Skip to content

Commit e0c1cc5

Browse files
authored
Fix null-unsafe payments config validation for partial overrides (#1363)
## Summary - Make the `branchPaymentsSchema` custom validator tolerant of partial override objects - Avoid crashing when `payments.products` or `payments.productLines` are absent during validation - Add regression tests for partial configs plus the existing missing-line and customer-type mismatch cases ## Testing - Added Vitest coverage for partial payments configs and validation failures - Lint passed for the touched schema files - Typecheck passed for `packages/stack-shared` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved validation robustness with stricter type-safety checks for payment-related data configurations. * Enhanced error messages for clearer feedback on validation failures. * **Tests** * Added comprehensive test coverage for edge cases including missing configurations and type mismatches. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent e503587 commit e0c1cc5

1 file changed

Lines changed: 101 additions & 5 deletions

File tree

packages/stack-shared/src/config/schema.ts

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,25 +181,121 @@ export const branchPaymentsSchema = yupObject({
181181
'Product customer type must match its product line customer type',
182182
function(this: yup.TestContext<yup.AnyObject>, value) {
183183
if (!value) return true;
184-
for (const [productId, product] of Object.entries(value.products)) {
185-
if (!product.productLineId) continue;
186-
const productLine = getOrUndefined(value.productLines, product.productLineId);
184+
const products = value.products;
185+
if (!isObjectLike(products)) return true;
186+
187+
const productLines = value.productLines;
188+
for (const [productId, product] of Object.entries(products)) {
189+
if (!isObjectLike(product)) continue;
190+
const productLineId = product.productLineId;
191+
if (typeof productLineId !== "string" || productLineId.length === 0) continue;
192+
const productLine = isObjectLike(productLines) ? getOrUndefined(productLines, productLineId) : undefined;
187193
if (productLine === undefined) {
188194
return this.createError({
189-
message: `Product "${productId}" specifies product line ID "${product.productLineId}", but that product line does not exist`,
195+
message: `Product "${productId}" specifies product line ID "${productLineId}", but that product line does not exist`,
190196
path: `${this.path}.products.${productId}.productLineId`,
191197
});
192198
}
199+
if (!isObjectLike(productLine)) continue;
193200
if (product.customerType !== productLine.customerType) {
194201
return this.createError({
195-
message: `Product "${productId}" has customer type "${product.customerType}" but its product line "${product.productLineId}" has customer type "${productLine.customerType}"`,
202+
message: `Product "${productId}" has customer type "${product.customerType}" but its product line "${productLineId}" has customer type "${productLine.customerType}"`,
196203
path: `${this.path}.products.${productId}.customerType`,
197204
});
198205
}
199206
}
200207
return true;
201208
}
202209
);
210+
import.meta.vitest?.test("branchPaymentsSchema accepts partial payments config without products", async ({ expect }) => {
211+
await expect(branchPaymentsSchema.validate({
212+
blockNewPurchases: true,
213+
}, { abortEarly: false })).resolves.toMatchObject({
214+
blockNewPurchases: true,
215+
});
216+
});
217+
218+
import.meta.vitest?.test("branchPaymentsSchema accepts product lines without products", async ({ expect }) => {
219+
await expect(branchPaymentsSchema.validate({
220+
productLines: {
221+
pro: {
222+
displayName: "Pro",
223+
customerType: "user",
224+
},
225+
},
226+
}, { abortEarly: false })).resolves.toMatchObject({
227+
productLines: {
228+
pro: {
229+
displayName: "Pro",
230+
customerType: "user",
231+
},
232+
},
233+
});
234+
});
235+
236+
import.meta.vitest?.test("branchPaymentsSchema rejects a product that references a missing product line", async ({ expect }) => {
237+
await expect(branchPaymentsSchema.validate({
238+
products: {
239+
pro: {
240+
customerType: "user",
241+
productLineId: "missing-line",
242+
prices: "include-by-default",
243+
},
244+
},
245+
}, { abortEarly: false })).rejects.toThrowErrorMatchingInlineSnapshot(`[ValidationError: Product "pro" specifies product line ID "missing-line", but that product line does not exist]`);
246+
});
247+
248+
import.meta.vitest?.test("branchPaymentsSchema rejects null product entries without throwing a raw TypeError", async ({ expect }) => {
249+
await expect(branchPaymentsSchema.validate({
250+
products: {
251+
pro: null,
252+
},
253+
}, { abortEarly: false })).rejects.toThrowErrorMatchingInlineSnapshot(`[ValidationError: products cannot be null]`);
254+
});
255+
256+
import.meta.vitest?.test("branchPaymentsSchema rejects null product line entries without throwing a raw TypeError", async ({ expect }) => {
257+
await expect(branchPaymentsSchema.validate({
258+
productLines: {
259+
teamLine: null,
260+
},
261+
products: {
262+
pro: {
263+
customerType: "user",
264+
productLineId: "teamLine",
265+
prices: "include-by-default",
266+
},
267+
},
268+
}, { abortEarly: false })).rejects.toThrowErrorMatchingInlineSnapshot(`[ValidationError: productLines cannot be null]`);
269+
});
270+
271+
import.meta.vitest?.test("branchPaymentsSchema rejects a product whose customer type differs from its product line", async ({ expect }) => {
272+
await expect(branchPaymentsSchema.validate({
273+
productLines: {
274+
teamLine: {
275+
customerType: "team",
276+
},
277+
},
278+
products: {
279+
pro: {
280+
customerType: "user",
281+
productLineId: "teamLine",
282+
prices: "include-by-default",
283+
},
284+
},
285+
}, { abortEarly: false })).rejects.toThrowErrorMatchingInlineSnapshot(`[ValidationError: Product "pro" has customer type "user" but its product line "teamLine" has customer type "team"]`);
286+
});
287+
288+
import.meta.vitest?.test("branchPaymentsSchema lets productLineId schema reject empty IDs", async ({ expect }) => {
289+
await expect(branchPaymentsSchema.validate({
290+
products: {
291+
pro: {
292+
customerType: "user",
293+
productLineId: "",
294+
prices: "include-by-default",
295+
},
296+
},
297+
}, { abortEarly: false })).rejects.toThrowErrorMatchingInlineSnapshot(`[ValidationError: productLineId must contain only letters, numbers, underscores, and hyphens, and not start with a hyphen]`);
298+
});
203299

204300
const branchDomain = yupObject({});
205301

0 commit comments

Comments
 (0)