fix(ci): repair two pre-existing test failures on dev (#1488)

Both failures are pre-existing on `dev` (confirmed by checking the most
recent dev run
[26434368271](https://github.com/hexclave/stack-auth/actions/runs/26434368271)
— same two annotations, same line numbers). Neither is caused by an open
PR.

## Failure 1 — \`apps/backend/src/lib/redirect-urls.test.tsx:75\`

\`\`\`
AssertionError: expected false to be true
\`\`\`

The \`withHostedHandlerEnv\` helper set/cleared only the
\`STACK_*\`-prefixed env vars. CI's
[e2e-custom-base-port-api-tests.yaml:21](.github/workflows/e2e-custom-base-port-api-tests.yaml#L21)
sets only the \`HEXCLAVE_*\`-prefixed sibling
(\`NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=67\`), and the dual-read shim in
[packages/stack-shared/src/utils/env.tsx#L53-L55](packages/stack-shared/src/utils/env.tsx#L53-L55)
prefers \`HEXCLAVE_*\` over \`STACK_*\`:

\`\`\`ts
const hexclaveName = getHexclaveEnvVarName(name);
let value = (hexclaveName ? process.env[hexclaveName] : undefined) ??
process.env[name];
\`\`\`

So \`getEnvVariable(\"NEXT_PUBLIC_STACK_PORT_PREFIX\", \"81\")\`
returned \`\"67\"\` instead of the test's \`\"92\"\`, the template
resolved to port \`6709\` instead of \`9209\`, and the assertion at line
75 failed.

**Fix:** mirror every \`STACK_*\` key managed by the helper to its
\`HEXCLAVE_*\` sibling. The dual-read then resolves to the
test-controlled value regardless of which key it checks first.

## Failure 2 —
\`apps/backend/prisma/migrations/20260526060000_nullable_oauth_access_token_expires_at/tests/nullable-expires-at.ts:58\`

\`\`\`
PostgresError: null value in column \"updatedAt\" of relation
\"OAuthAccessToken\" violates not-null constraint
\`\`\`

The migration test's raw INSERT omits \`\"updatedAt\"\`. The Prisma
model declares \`updatedAt DateTime @updatedAt\` with no
\`@default(now())\`, so the DB column is \`NOT NULL\` with no default —
Prisma populates it at the ORM layer on insert, but this test bypasses
Prisma via \`postgres.js\`.

**Fix:** add the \`\"updatedAt\"\` column to the INSERT, set to
\`NOW()\`, with a comment noting why raw SQL must set it explicitly.

## Verification

- **Failure 1, before fix:** ran \`NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX=67
pnpm test run apps/backend/src/lib/redirect-urls.test.tsx\` locally →
reproduces the exact line-75 assertion failure from CI.
- **Failure 1, after fix:** same command → 33/33 pass.
- **Failure 2:** local reproduction requires the migration-test postgres
harness; the fix is one column matching how every other raw SQL insert
in this repo handles \`@updatedAt\` fields. CI on this branch will
confirm.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Fixes two failing tests on dev CI by aligning env var handling in
redirect URL tests and by setting the missing updatedAt in a migration
test. Restores green CI with no runtime changes.

- **Bug Fixes**
- Redirect URL tests: `withHostedHandlerEnv` now mirrors `STACK_*`
values to their `HEXCLAVE_*` siblings and restores both, so
`getEnvVariable` reads the test-controlled values even when CI sets only
`HEXCLAVE_*` (e.g. `NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX`).
- Migration test: the raw insert into `OAuthAccessToken` now sets
`"updatedAt" = NOW()` since `Prisma`’s `@updatedAt` isn’t applied when
using `postgres.js` and the column is NOT NULL.

<sup>Written for commit 75c8e4343e.
Summary will update on new commits. <a
href="https://cubic.dev/pr/hexclave/stack-auth/pull/1488?utm_source=github">Review
in cubic</a></sup>

<!-- End of auto-generated description by cubic. -->
This commit is contained in:
BilalG1 2026-05-26 12:59:44 -07:00 committed by Madison
parent 9b30a276c4
commit c7a5c7be1f
2 changed files with 34 additions and 22 deletions

View File

@ -55,6 +55,9 @@ export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof pre
expect(columnRows[0].is_nullable).toBe("YES");
expect(columnRows[0].data_type).toBe("timestamp without time zone");
// `OAuthAccessToken."updatedAt"` is `DateTime @updatedAt` in Prisma with no
// DB-level default — Prisma populates it at the ORM layer on insert. This
// raw SQL bypasses Prisma, so the column has to be set explicitly.
await sql`
INSERT INTO "OAuthAccessToken" (
"id",
@ -62,7 +65,8 @@ export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof pre
"oauthAccountId",
"accessToken",
"scopes",
"expiresAt"
"expiresAt",
"updatedAt"
)
VALUES (
${randomUUID()}::uuid,
@ -70,7 +74,8 @@ export const postMigration = async (sql: Sql, ctx: Awaited<ReturnType<typeof pre
${ctx.oauthAccountId}::uuid,
'github-access-token-without-expiry',
ARRAY['user:email']::text[],
NULL
NULL,
NOW()
)
`;

View File

@ -12,34 +12,41 @@ describe('validateRedirectUrl', () => {
callback: () => T,
): T => {
const processEnv = Reflect.get(process, "env");
const oldHostedHandlerUrlTemplate = Reflect.get(processEnv, "NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE");
const oldHostedHandlerDomainSuffix = Reflect.get(processEnv, "NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX");
const oldStackPortPrefix = Reflect.get(processEnv, "NEXT_PUBLIC_STACK_PORT_PREFIX");
try {
for (const [key, value] of Object.entries({
NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE: values.hostedHandlerUrlTemplate,
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX: values.hostedHandlerDomainSuffix,
NEXT_PUBLIC_STACK_PORT_PREFIX: values.stackPortPrefix,
})) {
// Hexclave rebrand: getEnvVariable() in stack-shared/utils/env.tsx prefers the
// HEXCLAVE_*-prefixed sibling of each STACK_* var. CI sets only the HEXCLAVE_*
// variant (e.g. NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX), so writing only the STACK_*
// key here would be silently overridden. Mirror every STACK_* key to its
// HEXCLAVE_* sibling so both representations resolve to the same value.
const stackKeys = [
"NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE",
"NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX",
"NEXT_PUBLIC_STACK_PORT_PREFIX",
] as const;
const hexclaveOf = (name: string) => name.replace("STACK_", "HEXCLAVE_");
const allKeys = [...stackKeys, ...stackKeys.map(hexclaveOf)];
const oldValues = Object.fromEntries(allKeys.map((k) => [k, Reflect.get(processEnv, k)]));
const newValues: Record<string, string | undefined> = {
NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE: values.hostedHandlerUrlTemplate,
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX: values.hostedHandlerDomainSuffix,
NEXT_PUBLIC_STACK_PORT_PREFIX: values.stackPortPrefix,
};
for (const stackKey of stackKeys) {
newValues[hexclaveOf(stackKey)] = newValues[stackKey];
}
const applyValues = (entries: Record<string, string | undefined>) => {
for (const [key, value] of Object.entries(entries)) {
if (value == null) {
Reflect.deleteProperty(processEnv, key);
} else {
Reflect.set(processEnv, key, value);
}
}
};
try {
applyValues(newValues);
return callback();
} finally {
for (const [key, value] of Object.entries({
NEXT_PUBLIC_STACK_HOSTED_HANDLER_URL_TEMPLATE: oldHostedHandlerUrlTemplate,
NEXT_PUBLIC_STACK_HOSTED_HANDLER_DOMAIN_SUFFIX: oldHostedHandlerDomainSuffix,
NEXT_PUBLIC_STACK_PORT_PREFIX: oldStackPortPrefix,
})) {
if (value == null) {
Reflect.deleteProperty(processEnv, key);
} else {
Reflect.set(processEnv, key, value);
}
}
applyValues(oldValues);
}
};