Files
Simo 7867bf6b72
CI / check (push) Successful in 3m28s
CI / deploy (push) Successful in 18s
CI / publish-container (push) Successful in 2m47s
test(news): gate deployment on an isolated real browser publication journey
2026-09-13 20:27:04 +02:00

196 lines
6.2 KiB
TypeScript

import { randomBytes } from "node:crypto";
import { readFile } from "node:fs/promises";
import { is, SQL } from "drizzle-orm";
import {
getTableConfig,
MySqlDialect,
type MySqlTable,
} from "drizzle-orm/mysql-core";
import { bcrypt } from "hash-wasm";
import mysql, { type Connection } from "mysql2/promise";
import { splitSqlStatements } from "../../scripts/sql-statements";
import * as schema from "../../src/db/schema";
export const STAFF_ID = 7;
export const STAFF_USERNAME = "NewsEditor";
// Use the actual ORM definitions for the emulator tables this browser journey reads.
// CMS recovery/outbox tables below come from the shipped migrations themselves.
const tables: MySqlTable[] = [
schema.User,
schema.Ban,
schema.WebsiteSetting,
schema.WebsiteLanguages,
schema.AclRole,
schema.AclPermission,
schema.AclModelRole,
schema.AclModelPermission,
schema.WebsiteArticles,
schema.WebsiteArticleComments,
schema.WebsiteArticleReactions,
schema.WebsiteLoginLogs,
schema.AdminAuditLog,
schema.StaffActivities,
schema.WebsiteIpBlacklist,
schema.WebsiteIpWhitelist,
schema.AlertLogs,
schema.ThemeScope,
schema.ThemeScopeValue,
schema.UsersCurrency,
schema.MessengerFriendrequests,
schema.MessengerFriendships,
schema.MessengerOffline,
schema.Rooms,
schema.UsersBadges,
schema.UsersSettings,
schema.UserReferrals,
schema.WebsiteEvent,
schema.WebsiteEventType,
];
const identifier = (name: string) => `\`${name.replaceAll("`", "``")}\``;
export function fixtureStatements(): string[] {
const dialect = new MySqlDialect();
return tables.map((table) => {
const config = getTableConfig(table);
// Fail on new constraints instead of silently reducing the fixture's integrity.
if (
config.foreignKeys.length ||
config.checks.length ||
config.uniqueConstraints.length
)
throw Error(`Fixture requires explicit constraints for ${config.name}`);
const columns = config.columns.map((column) => {
let ddl = `${identifier(column.name)} ${column.getSQLType()}`;
ddl += column.notNull ? " NOT NULL" : " NULL";
if ("autoIncrement" in column && column.autoIncrement)
ddl += " AUTO_INCREMENT";
if (column.primary) ddl += " PRIMARY KEY";
if (column.isUnique) ddl += " UNIQUE";
if (column.default !== undefined) {
if (is(column.default, SQL)) {
const query = dialect.sqlToQuery(column.default);
if (query.params.length)
throw Error(
`Fixture requires parameterized default for ${config.name}.${column.name}`,
);
ddl += ` DEFAULT ${query.sql}`;
} else if (
typeof column.default === "string" ||
typeof column.default === "number" ||
typeof column.default === "boolean" ||
column.default === null
)
ddl += ` DEFAULT ${mysql.escape(column.default)}`;
else
throw Error(
`Fixture requires explicit default for ${config.name}.${column.name}`,
);
} else if (!column.notNull) ddl += " DEFAULT NULL";
return ddl;
});
for (const key of config.primaryKeys)
columns.push(
`PRIMARY KEY (${key.columns.map((column) => identifier(column.name)).join(", ")})`,
);
for (const index of config.indexes) {
if (!index.config.unique) continue;
const names = index.config.columns.map((column) => {
if (is(column, SQL))
throw Error(`Fixture requires expression index for ${config.name}`);
return identifier(column.name);
});
columns.push(
`UNIQUE KEY ${identifier(index.config.name)} (${names.join(", ")})`,
);
}
return `CREATE TABLE ${identifier(config.name)} (${columns.join(",\n")}) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`;
});
}
export async function seedDatabase(
connection: Connection,
staffPassword: string,
) {
for (const statement of fixtureStatements())
await connection.query(statement);
// Rank metadata belongs to the emulator and is queried as raw SQL by authorization.
await connection.query(
"CREATE TABLE permission_ranks (id INT PRIMARY KEY, rank_name VARCHAR(255) NOT NULL) ENGINE=InnoDB",
);
await connection.query(
"INSERT INTO permission_ranks (id, rank_name) VALUES (1, 'Member'), (7, 'Editor'), (9, 'Owner')",
);
for (const migration of [
"0026_article_editor_recovery.sql",
"0031_operations_outbox.sql",
]) {
const contents = await readFile(
new URL(`../../drizzle/migrations/${migration}`, import.meta.url),
"utf8",
);
for (const statement of splitSqlStatements(contents))
await connection.query(statement);
}
const password = await bcrypt({
password: staffPassword,
salt: randomBytes(16),
costFactor: 12,
outputType: "encoded",
});
const ownerPassword = await bcrypt({
password: randomBytes(32).toString("hex"),
salt: randomBytes(16),
costFactor: 12,
outputType: "encoded",
});
await connection.query(
"INSERT INTO users (id, username, password, rank, account_created, ip_register, ip_current, mail, mail_verified) VALUES (?, ?, ?, 7, ?, '127.0.0.1', '127.0.0.1', '[email protected]', '1'), (9, 'FixtureOwner', ?, 9, ?, '127.0.0.1', '127.0.0.1', '[email protected]', '1')",
[
STAFF_ID,
STAFF_USERNAME,
password,
Math.floor(Date.now() / 1000),
ownerPassword,
Math.floor(Date.now() / 1000),
],
);
await connection.query(
"INSERT INTO acl_roles (id, slug, title) VALUES (1, 'news_editor', 'News editor')",
);
await connection.query(
"INSERT INTO acl_model_roles (model_type, model_id, role_id) VALUES ('User', ?, 1)",
[STAFF_ID],
);
for (const [index, slug] of [
"admin.dashboard",
"admin.news.view",
"admin.news.edit",
].entries()) {
await connection.query(
"INSERT INTO acl_permissions (id, slug, title) VALUES (?, ?, ?)",
[index + 1, slug, slug],
);
await connection.query(
"INSERT INTO acl_model_permissions (model_type, model_id, permission_id) VALUES ('Role', 1, ?)",
[index + 1],
);
}
for (const [key, value] of Object.entries({
hotel_name: "News browser fixture",
captcha_provider: "none",
require_email_verification: "1",
force_staff_2fa: "0",
maintenance_enabled: "0",
abuse_guard_enabled: "0",
radio_enabled: "0",
}))
await connection.query(
"INSERT INTO website_settings (`key`, value) VALUES (?, ?)",
[key, value],
);
await connection.query(
"INSERT INTO website_languages (country_code, language) VALUES ('en', 'English')",
);
}