Files
bun-src/test/js/sql/sql-mysql-datetime-roundtrip.test.ts
2026-08-27 21:09:14 +00:00

83 lines
3.8 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, describeWithContainer, isDockerEnabled } from "harness";
import path from "path";
// A JS Date bound to a MySQL DATETIME/TIMESTAMP and read back must be the same
// instant regardless of the process timezone. Encode breaks the Date's epoch-ms
// into Y/M/D h:m:s via pure-UTC arithmetic, so decode has to treat those
// components as UTC too — if it interprets them as local time, the round-trip
// silently shifts by the machine's UTC offset.
//
// The fixture runs against a real MySQL server (docker-compose in CI, or a
// MYSQL_URL/local instance otherwise) and prints "OK TZ=<tz> offsetMin=<n>"
// only when every Date round-trips to the same instant.
const TIMEZONES = ["Etc/UTC", "America/New_York", "Asia/Tokyo"];
const fixture = path.join(import.meta.dir, "sql-mysql-datetime-tz-fixture.ts");
function runFixture(url: string, TZ: string, caPath = "") {
return Bun.spawnSync([bunExe(), fixture], {
env: { ...bunEnv, MYSQL_URL: url, CA_PATH: caPath, TZ },
stdout: "pipe",
stderr: "pipe",
});
}
function assertRoundTrip(stdout: string, stderr: string, TZ: string) {
// On a round-trip mismatch the fixture writes `FAIL TZ=… offsetMin=…` plus the
// per-row `diffMin` breakdown to stderr, then exits 1. Assert it's empty so a
// CI failure surfaces *which* dates drifted and by how much, not just a bare
// "CONNECTED" vs "OK" mismatch.
expect(stderr).toBe("");
// "OK TZ=<tz>" only prints when all three Dates round-trip to the same instant.
expect(stdout).toContain(`OK TZ=${TZ}`);
// And the child runtime must actually have adopted the injected timezone —
// a non-zero offset for the non-UTC zones — otherwise a silently-unapplied TZ
// would degenerate all three runs into the UTC case and stop exercising the
// local-time decode bug.
expect(stdout).toMatch(TZ === "Etc/UTC" ? /offsetMin=0\b/ : /offsetMin=-?[1-9]/);
}
if (isDockerEnabled()) {
// CI: run against the docker-compose MySQL service.
describeWithContainer("mysql", { image: "mysql_plain" }, container => {
describe.each(TIMEZONES)("TZ=%s", TZ => {
test("DATETIME Date round-trip is the identity", async () => {
await container.ready;
const url = `mysql://root@${container.host}:${container.port}/bun_sql_test`;
const { stdout, stderr, exitCode } = runFixture(url, TZ);
const out = String(stdout);
expect(out).toContain("CONNECTED");
assertRoundTrip(out, String(stderr), TZ);
expect(exitCode).toBe(0);
});
});
});
} else {
// No docker daemon (e.g. local/sandboxed environments). If a MySQL server is
// reachable at MYSQL_URL or the conventional local address, exercise the
// fixture there so the round-trip is still covered without a mock.
const url = process.env.MYSQL_URL || "mysql://[email protected]:3306/bun_sql_test";
describe.each(TIMEZONES)("mysql (local) TZ=%s", TZ => {
test("DATETIME Date round-trip is the identity", () => {
const { stdout, stderr, exitCode } = runFixture(url, TZ);
const out = String(stdout);
// The fixture prints "CONNECTED" once it reaches the server. If it never
// got that far, there's no MySQL to talk to here; the docker-gated branch
// above provides the CI coverage.
if (!out.includes("CONNECTED")) {
if (process.env.MYSQL_URL) {
throw new Error(
`sql-mysql-datetime-roundtrip: MYSQL_URL was provided but fixture never reached CONNECTED\nstdout:\n${out}\nstderr:\n${String(stderr)}`,
);
}
console.warn("sql-mysql-datetime-roundtrip: no MySQL reachable at " + url + "; skipping assertions");
return;
}
assertRoundTrip(out, String(stderr), TZ);
expect(exitCode).toBe(0);
});
});
}