+12








fcd7c979df
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>
162 lines
6.5 KiB
TypeScript
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);
|