Files
homarr/e2e/mysql-conversion.spec.ts
T
+12 fcd7c979df feat!: release v2 (#6545)
Co-authored-by: Artur <145776561+arturict@users.noreply.github.com>
Co-authored-by: arturict <arturict@users.noreply.github.com>
Co-authored-by: Thomas <thomas@ajnart.fr>
Co-authored-by: Afonso Jorge Ramos <afonsojorgeramos@gmail.com>
Co-authored-by: Danura30082 <danura30082+github@gmail.com>
Co-authored-by: Kushie <122986913+kushiemoon-dev@users.noreply.github.com>
Co-authored-by: ajnart <thomas@ajnart.fr>
Co-authored-by: ajnart <thomas@ajnart.dev>
Co-authored-by: chenjun <chenjun@foxmail.com>
Co-authored-by: Wikibear <wikibear@users.noreply.github.com>
Co-authored-by: Bergasha <bergasha@gmail.com>
Co-authored-by: leafyelin <43306196+leafyelin@users.noreply.github.com>
Co-authored-by: Lucas Araujo <lucas.lra@gmail.com>
Co-authored-by: GreenMonito <86085570+GreenMonito@users.noreply.github.com>
Co-authored-by: Nerdy-Technician <roffo@nerdy-technician.social>
Co-authored-by: Jun <34545896+jcstrong@users.noreply.github.com>
Co-authored-by: Edmond Abraham <54119952+gitedmond@users.noreply.github.com>
Co-authored-by: thrawnx <tight.bulb9384@fastmail.com>
Co-authored-by: Chris <58918656+nordada@users.noreply.github.com>
Co-authored-by: yi111 <153097222+Yi-111-a@users.noreply.github.com>
Co-authored-by: Yanis <naamaneyanis@gmail.com>
Co-authored-by: Wikibear <me@sej.de>
Co-authored-by: alessiottavo <93441886+alessiottavo@users.noreply.github.com>
2026-10-02 21:00:14 +02:00

162 lines
6.5 KiB
TypeScript

import { execFile } from "node:child_process";
import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
import { promisify } from "node:util";
import path from "node:path";
import { chromium, expect as expectBrowser } from "@playwright/test";
import Database from "better-sqlite3";
import { expect, test } from "vitest";
import { decryptSecretWithKey } from "../packages/common/src/encryption";
import { convert } from "../tools/mysql-to-sqlite/src/convert.mjs";
import {
createFixture,
fixtureBoard,
fixtureCiphertext,
fixtureMedia,
fixturePassword,
fixtureSecret,
fixtureUser,
} from "../tools/mysql-to-sqlite/test/fixture.mjs";
import { createHomarrContainer } from "./shared/create-homarr-container";
import { loginAsync } from "./shared/login";
// This deliberately starts from the supported MySQL release, not a freshly seeded v2 database.
test("converted MySQL data boots, authenticates and survives a v2 restart", async () => {
const root = path.join(__dirname, "tmp");
await mkdir(root, { recursive: true });
const appdata = await mkdtemp(path.join(root, "mysql-conversion-"));
const output = path.join(appdata, "db", "db.sqlite");
await mkdir(path.dirname(output));
const fixture = await createFixture();
try {
await convert({ connectionOptions: fixture.connectionOptions, output, homarrStopped: true });
const browser = await chromium.launch();
try {
for (let boot = 0; boot < 2; boot++) {
const container = await createHomarrContainer({ mounts: { "/appdata": appdata } }).start();
const context = await browser.newContext();
try {
const baseUrl = `http://${container.getHost()}:${container.getMappedPort(7575)}`;
const page = await context.newPage();
await loginAsync({
page,
baseUrl,
credentials: { username: fixtureUser, password: fixturePassword },
destination: "/boards/converted-board",
});
await page.locator('[data-testid="board-canvas"][data-board-hydrated="true"]').waitFor({ timeout: 30_000 });
expect(page.url()).toContain("/boards/converted-board");
const clock = page
.locator(
'[data-id="migration-item"] .clock-widget-container, [data-grid-item-id="migration-item"] .clock-widget-container',
)
.filter({ visible: true })
.first();
await expectBrowser(clock).toBeVisible({ timeout: 30_000 });
await expectBrowser(clock.locator("time").filter({ visible: true }).first()).not.toHaveText("--:--", {
timeout: 30_000,
});
const media = await context.request.get(`${baseUrl}/api/user-medias/migration-media`);
expect(media.status()).toBe(200);
expect(media.headers()["content-type"]).toContain("application/octet-stream");
expect(await media.body()).toEqual(fixtureMedia);
} finally {
await context.close();
await container.stop();
}
// Inspect after shutdown so WAL data is settled; the second boot proves persisted state.
const db = new Database(output, { readonly: true });
try {
expect(db.pragma("integrity_check", { simple: true })).toBe("ok");
expect(db.pragma("foreign_key_check")).toEqual([]);
expect(db.prepare("SELECT id, name FROM board WHERE id = ?").get(fixtureBoard)).toEqual({
id: fixtureBoard,
name: "converted-board",
});
const secret = db
.prepare('SELECT value FROM "integrationSecret" WHERE integration_id = ? AND kind = ?')
.get("migration-integration", "apiKey") as { value: `${string}.${string}` };
expect(secret.value).toBe(fixtureCiphertext);
expect(decryptSecretWithKey(secret.value, Buffer.alloc(32))).toBe(fixtureSecret);
expect(
db
.prepare(
"SELECT section_id, x_offset, y_offset, width, height FROM item_layout WHERE item_id = ? AND layout_id = ?",
)
.get("migration-item", "migration-layout"),
).toEqual({ section_id: "migration-section", x_offset: 0, y_offset: 0, width: 3, height: 2 });
expect(
db
.prepare(
"SELECT id, name, url, enabled, auth_type, method, display_type, display_config FROM custom_widget_definition WHERE id = ?",
)
.get("migration-custom"),
).toEqual({
id: "migration-custom",
name: "Legacy weather",
url: "https://example.test/weather",
enabled: 0,
auth_type: "bearer",
method: "GET",
display_type: "singleValue",
display_config: '{"type":"singleValue","jsonPath":"$.temperature"}',
});
expect(
db
.prepare("SELECT value FROM custom_widget_secret WHERE definition_id = ? AND kind = ?")
.get("migration-custom", "apiKey"),
).toEqual({ value: fixtureCiphertext });
} finally {
db.close();
}
}
} finally {
await browser.close();
}
} finally {
await fixture.cleanup();
await rm(appdata, { recursive: true, force: true });
}
}, 180_000);
test("retired MySQL settings fail before creating application data", async () => {
const root = path.join(__dirname, "tmp");
await mkdir(root, { recursive: true });
const image = process.env.HOMARR_E2E_IMAGE?.trim() || "homarr-e2e";
for (const setting of ["DB_DRIVER=mysql2", "DB_DIALECT=mysql", "DB_URL=mysql://unused.invalid/homarr"]) {
const appdata = await mkdtemp(path.join(root, "mysql-rejection-"));
const containerName = path.basename(appdata);
try {
await expect(
promisify(execFile)(
"docker",
[
"run",
"--rm",
"--name",
containerName,
"--network",
"none",
"--mount",
`type=bind,source=${appdata},target=/appdata`,
"-e",
setting,
image,
],
{ timeout: 15_000 },
),
).rejects.toMatchObject({
code: 1,
stdout: expect.stringContaining("MySQL is no longer supported in v2. Use the MySQL-to-SQLite converter"),
});
expect(await readdir(appdata)).toEqual([]);
} finally {
await promisify(execFile)("docker", ["rm", "--force", containerName]).catch(() => undefined);
await rm(appdata, { recursive: true, force: true });
}
}
}, 60_000);