fix(news): preserve recoverable reads and verify publication against real services
This commit is contained in:
1 parent
8abfe352ef
commit
f7b9b55700
33 files changed
+1377
-220
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import type { SQL } from "drizzle-orm";
|
||||
import { MySqlDialect } from "drizzle-orm/mysql-core";
|
||||
import { format } from "mysql2";
|
||||
import { beforeEach, expect, it, vi } from "vitest";
|
||||
|
||||
const state = vi.hoisted(() => ({
|
||||
@@ -9,6 +10,7 @@ const state = vi.hoisted(() => ({
|
||||
status: string;
|
||||
publishAt: Date;
|
||||
}>,
|
||||
queries: [] as Array<{ sql: string; params: unknown[] }>,
|
||||
operations: [] as unknown[][],
|
||||
effects: [] as unknown[][],
|
||||
failOutbox: false,
|
||||
@@ -19,15 +21,20 @@ vi.mock("@/lib/logger", () => ({ logger: { error: vi.fn() } }));
|
||||
vi.mock("./news-cache", () => ({ invalidateNewsCache: state.refresh }));
|
||||
vi.mock("@/lib/db", () => {
|
||||
const dialect = new MySqlDialect();
|
||||
const timestamp = (value: unknown) =>
|
||||
value instanceof Date
|
||||
? value
|
||||
: new Date(`${String(value).replace(" ", "T")}Z`);
|
||||
const tx = {
|
||||
execute: async (query: SQL) => {
|
||||
const { sql, params } = dialect.sqlToQuery(query);
|
||||
state.queries.push({ sql, params });
|
||||
if (sql.startsWith("SELECT"))
|
||||
return [
|
||||
state.articles
|
||||
.filter(
|
||||
(a) =>
|
||||
a.status === "scheduled" && a.publishAt <= (params[0] as Date),
|
||||
a.status === "scheduled" && a.publishAt <= timestamp(params[0]),
|
||||
)
|
||||
.slice(0, 100),
|
||||
];
|
||||
@@ -37,7 +44,7 @@ vi.mock("@/lib/db", () => {
|
||||
state.loseRace ||
|
||||
!article ||
|
||||
article.status !== "scheduled" ||
|
||||
article.publishAt > (params[3] as Date)
|
||||
article.publishAt > timestamp(params[3])
|
||||
)
|
||||
return [{ affectedRows: 0 }];
|
||||
article.status = "published";
|
||||
@@ -83,6 +90,7 @@ beforeEach(() => {
|
||||
publishAt: new Date("2030-01-01T00:00:00Z"),
|
||||
},
|
||||
];
|
||||
state.queries = [];
|
||||
state.operations = [];
|
||||
state.effects = [];
|
||||
state.failOutbox = false;
|
||||
@@ -128,3 +136,25 @@ it("reports committed publication even when immediate cache refresh fails", asyn
|
||||
expect(state.effects).toHaveLength(1);
|
||||
expect(state.refresh).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("uses the same UTC cutoff and publication timestamps regardless of the MySQL client timezone", async () => {
|
||||
const instant = new Date("2030-06-01T08:09:10.123Z");
|
||||
expect(await publishDueArticles(instant)).toBe(1);
|
||||
const statements = state.queries.filter(
|
||||
({ sql }) =>
|
||||
sql.startsWith("SELECT") || sql.startsWith("UPDATE website_articles"),
|
||||
);
|
||||
expect(statements).toHaveLength(2);
|
||||
for (const statement of statements) {
|
||||
expect(format(statement.sql, statement.params, false, "+02:00")).toBe(
|
||||
format(statement.sql, statement.params, false, "-07:00"),
|
||||
);
|
||||
}
|
||||
expect(statements[0].params).toEqual(["2030-06-01 08:09:10.123"]);
|
||||
expect(statements[1].params).toEqual([
|
||||
"2030-06-01 08:09:10.123",
|
||||
"2030-06-01 08:09:10.123",
|
||||
"1",
|
||||
"2030-06-01 08:09:10.123",
|
||||
]);
|
||||
});
|
||||
Reference in new issue
Block a user