Skip to content
Open
Show file tree
Hide file tree
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
51 changes: 51 additions & 0 deletions packages/ack-pay/src/schemas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import * as v from "valibot"
import { describe, expect, it } from "vitest"

import { paymentRequestSchema as valibotPaymentRequestSchema } from "./schemas/valibot"
import { paymentRequestSchema as zodV3PaymentRequestSchema } from "./schemas/zod/v3"
import { paymentRequestSchema as zodV4PaymentRequestSchema } from "./schemas/zod/v4"

const paymentRequest = {
id: "test-payment-request-id",
paymentOptions: [
{
id: "test-payment-option-id",
amount: 10,
decimals: 2,
currency: "USD",
recipient: "sol:123",
},
],
}

describe("paymentRequestSchema", () => {
it("reports invalid expiresAt strings as schema errors", () => {
const input = {
...paymentRequest,
expiresAt: "invalid-date",
}

expect(v.safeParse(valibotPaymentRequestSchema, input).success).toBe(false)
expect(zodV3PaymentRequestSchema.safeParse(input).success).toBe(false)
expect(zodV4PaymentRequestSchema.safeParse(input).success).toBe(false)
})
Comment on lines +21 to +31

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only exercises the rejection path. Please add a happy-path test asserting that valid inputs are accepted and normalized to an ISO string across all three schemas — the .toISOString() transform is the load-bearing behavior here and is currently only covered indirectly (and only for valibot) by create-signed-payment-request.test.ts. A direct parity test guards against the three implementations drifting.

Suggested cases (a Date object and an equivalent string, both normalizing to the same ISO output):

it("normalizes valid expiresAt inputs to an ISO string", () => {
  const expected = "2024-12-31T23:59:59.000Z"
  for (const expiresAt of [
    new Date("2024-12-31T23:59:59Z"),
    "2024-12-31T23:59:59Z",
  ]) {
    const input = { ...paymentRequest, expiresAt }

    const valibot = v.safeParse(valibotPaymentRequestSchema, input)
    expect(valibot.success && valibot.output.expiresAt).toBe(expected)

    const v3 = zodV3PaymentRequestSchema.safeParse(input)
    expect(v3.success && v3.data.expiresAt).toBe(expected)

    const v4 = zodV4PaymentRequestSchema.safeParse(input)
    expect(v4.success && v4.data.expiresAt).toBe(expected)
  }
})


it("normalizes valid expiresAt inputs to an ISO string", () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Rename the test to follow the required assertive pattern.

Use one of the mandated prefixes (e.g., it("returns ...")) instead of it("normalizes ...") for this case.
As per coding guidelines, test names in **/*.test.ts must use patterns: it("creates..."), it("throws..."), it("requires..."), or it("returns...").

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ack-pay/src/schemas.test.ts` at line 33, Rename the test case
declaration currently written as it("normalizes valid expiresAt inputs to an ISO
string", ...) to use the required assertive prefix, e.g. change the description
to start with "returns" such as it("returns an ISO string for valid expiresAt
inputs", ...) so the test name follows the mandated pattern while keeping the
same test body and intent (locate and update the it(...) call in the test file).

const expected = "2024-12-31T23:59:59.000Z"
for (const expiresAt of [
new Date("2024-12-31T23:59:59Z"),
"2024-12-31T23:59:59Z",
]) {
const input = { ...paymentRequest, expiresAt }

const valibot = v.safeParse(valibotPaymentRequestSchema, input)
expect(valibot.success && valibot.output.expiresAt).toBe(expected)

const v3 = zodV3PaymentRequestSchema.safeParse(input)
expect(v3.success && v3.data.expiresAt).toBe(expected)

const v4 = zodV4PaymentRequestSchema.safeParse(input)
expect(v4.success && v4.data.expiresAt).toBe(expected)
}
})
})
13 changes: 7 additions & 6 deletions packages/ack-pay/src/schemas/valibot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import * as v from "valibot"

const urlOrDidUri = v.union([v.pipe(v.string(), v.url()), didUriSchema])

const timestampSchema = v.pipe(
v.union([v.date(), v.string()]),
v.check((input) => !Number.isNaN(new Date(input).getTime()), "Invalid date"),
v.transform((input) => new Date(input).toISOString()),
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment on lines +7 to +11

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional: Valibot ships an isoTimestamp action that can replace the hand-rolled NaN check here. The canonical pattern from valibot's own docs is v.union([v.date(), v.pipe(v.string(), v.isoTimestamp())]):

const timestampSchema = v.pipe(
  v.union([v.date(), v.pipe(v.string(), v.isoTimestamp())]),
  v.transform((input) => new Date(input).toISOString()),
)

Two upsides: it drops the custom check, and it removes the redundant double new Date(input) construction (currently built once in the check and again in the transform).

Behavioral caveat to decide on first: isoTimestamp only accepts strings that are already in ISO 8601 form, so loosely-formatted-but-parseable strings (e.g. "2024/12/31", "December 31 2024") that the current new Date()-based check accepts would now be rejected. For an expiresAt field that's arguably more correct, but it is a tightening of the accepted input set. If you adopt it, mirror the change on the zod side (z.string().datetime() for v3, z.iso.datetime() for v4) so the three schemas stay in parity — otherwise the permissive new Date() version you have now is fine to keep.


export const paymentOptionSchema = v.object({
id: v.string(),
amount: v.union([v.pipe(v.number(), v.integer(), v.gtValue(0)), v.string()]),
Expand All @@ -19,12 +25,7 @@ export const paymentRequestSchema = v.object({
id: v.string(),
description: v.optional(v.string()),
serviceCallback: v.optional(v.pipe(v.string(), v.url())),
expiresAt: v.optional(
v.pipe(
v.union([v.date(), v.string()]),
v.transform((input) => new Date(input).toISOString()),
),
),
expiresAt: v.optional(timestampSchema),
paymentOptions: v.pipe(
v.tupleWithRest([paymentOptionSchema], paymentOptionSchema),
v.nonEmpty(),
Expand Down
20 changes: 16 additions & 4 deletions packages/ack-pay/src/schemas/zod/v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import { z } from "zod/v3"

const urlOrDidUri = z.union([z.string().url(), didUriSchema])

const timestampSchema = z
.union([z.date(), z.string()])
.transform((val, ctx) => {
const date = new Date(val)
if (Number.isNaN(date.getTime())) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid date",
})
return z.NEVER
}

return date.toISOString()
})

export const paymentOptionSchema = z.object({
id: z.string(),
amount: z.union([z.number().int().positive(), z.string()]),
Expand All @@ -19,10 +34,7 @@ export const paymentRequestSchema = z.object({
id: z.string(),
description: z.string().optional(),
serviceCallback: z.string().url().optional(),
expiresAt: z
.union([z.date(), z.string()])
.transform((val) => new Date(val).toISOString())
.optional(),
expiresAt: timestampSchema.optional(),
paymentOptions: z.array(paymentOptionSchema).nonempty(),
})

Expand Down
21 changes: 17 additions & 4 deletions packages/ack-pay/src/schemas/zod/v4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ import * as z from "zod/v4"

const urlOrDidUri = z.union([z.url(), didUriSchema])

const timestampSchema = z
.union([z.date(), z.string()])
.transform((val, ctx) => {
const date = new Date(val)
if (Number.isNaN(date.getTime())) {
ctx.addIssue({
code: "custom",
message: "Invalid date",
input: val,
})
return z.NEVER
}

return date.toISOString()
})

export const paymentOptionSchema = z.object({
id: z.string(),
amount: z.union([z.number().int().positive(), z.string()]),
Expand All @@ -19,10 +35,7 @@ export const paymentRequestSchema = z.object({
id: z.string(),
description: z.string().optional(),
serviceCallback: z.url().optional(),
expiresAt: z
.union([z.date(), z.string()])
.transform((val) => new Date(val).toISOString())
.optional(),
expiresAt: timestampSchema.optional(),
paymentOptions: z.array(paymentOptionSchema).nonempty(),
})

Expand Down
Loading