168 lines
6.7 KiB
TypeScript
168 lines
6.7 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
import { test } from "node:test";
|
|
import Database from "better-sqlite3";
|
|
import { migrateDatabase } from "../../lib/db-migrations";
|
|
|
|
const remoteSchema = readFileSync(
|
|
"tests/fixtures/remote-v4-schema.sql",
|
|
"utf8",
|
|
);
|
|
function remoteDb() {
|
|
const db = new Database(":memory:");
|
|
db.pragma("foreign_keys = ON");
|
|
db.exec(remoteSchema);
|
|
return db;
|
|
}
|
|
function snapshot(db: Database.Database) {
|
|
return Object.fromEntries(
|
|
(
|
|
db
|
|
.prepare(
|
|
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
|
|
)
|
|
.all() as { name: string }[]
|
|
)
|
|
.filter(
|
|
({ name }) =>
|
|
![
|
|
"managed_certificates",
|
|
"node_installations",
|
|
"node_agent_counters",
|
|
"relay_hosts",
|
|
"relay_routes",
|
|
].includes(name),
|
|
)
|
|
.map(({ name }) => [name, db.prepare(`SELECT * FROM "${name}"`).all()]),
|
|
);
|
|
}
|
|
function seed(db: Database.Database) {
|
|
db.exec(`INSERT INTO hy2_users (id,auth_id,username,password_hash,notes,created_at,updated_at,subscription_token,auth_secret_encrypted,traffic_limit_bytes,used_tx_bytes,used_rx_bytes)
|
|
VALUES (42,'original-auth','member','test-hash','keep notes','2026-01-01','2026-01-01','test-sub-token','test-encrypted-value',4096,128,256);
|
|
INSERT INTO hy2_nodes (id,slug,name,auth_token,traffic_stats_url,traffic_stats_secret,created_at,updated_at,server_host)
|
|
VALUES (17,'existing-hy2','Existing HY2','test-auth-token','http://stats.example:9999','test-stats-secret','2026-01-01','2026-01-01','existing.example');
|
|
INSERT INTO traffic_ledgers (node_id,user_id,auth_id,tx_bytes,rx_bytes,window_started_at,window_ended_at,created_at) VALUES (17,42,'original-auth',128,256,'2026-01-01','2026-01-01','2026-01-01');
|
|
INSERT INTO user_sessions (id,user_id,session_hash,expires_at,created_at,last_seen_at) VALUES ('session',42,'test-session-hash','2030-01-01','2026-01-01','2026-01-01');
|
|
INSERT INTO app_settings VALUES ('main_proxy_group_member_slugs_json','["legacy-custom","existing-hy2"]','2026-01-01');
|
|
INSERT INTO app_settings VALUES ('proxy_groups_json','[{"id":"group-id","name":"Work","type":"select","memberSlugs":["legacy-custom"]}]','2026-01-01');`);
|
|
}
|
|
|
|
test("fresh database exactly matches the inspected remote v4 column definitions", () => {
|
|
const fresh = new Database(":memory:");
|
|
const remote = remoteDb();
|
|
migrateDatabase(fresh);
|
|
assert.equal(fresh.pragma("user_version", { simple: true }), 7);
|
|
for (const { name } of remote
|
|
.prepare("SELECT name FROM sqlite_master WHERE type='table'")
|
|
.all() as { name: string }[]) {
|
|
assert.deepEqual(
|
|
fresh.pragma(`table_info(${name})`),
|
|
remote.pragma(`table_info(${name})`),
|
|
);
|
|
}
|
|
fresh.close();
|
|
remote.close();
|
|
});
|
|
|
|
test("remote v4: repeated migration preserves every row, ID, secret, quota, session and group", () => {
|
|
const db = remoteDb();
|
|
seed(db);
|
|
const source = `# legacy source stays byte-for-byte\n- name: "AnyTLS #1"\n type: anytls\n password: "test: password"\n server: hk.example.com\n port: 443\n`;
|
|
db.prepare(
|
|
`INSERT INTO hy2_nodes (id,slug,name,auth_token,traffic_stats_url,traffic_stats_secret,created_at,updated_at,node_type,custom_config_yaml,client_name) VALUES (18,'legacy-custom','Legacy','test-token','','test-secret','2026-01-01','2026-01-01','custom',?,'Old fallback')`,
|
|
).run(source);
|
|
const before = snapshot(db);
|
|
migrateDatabase(db);
|
|
migrateDatabase(db);
|
|
assert.deepEqual(snapshot(db), before);
|
|
assert.deepEqual(db.pragma("foreign_key_check"), []);
|
|
db.close();
|
|
});
|
|
|
|
for (const existing of [
|
|
[],
|
|
["node_type"],
|
|
["custom_config_yaml"],
|
|
["node_type", "custom_config_yaml"],
|
|
]) {
|
|
test(`v3 upgrade with existing columns [${existing.join(", ")}] preserves data`, () => {
|
|
const db = remoteDb();
|
|
seed(db);
|
|
for (const field of ["node_type", "custom_config_yaml"])
|
|
if (!existing.includes(field))
|
|
db.exec(`ALTER TABLE hy2_nodes DROP COLUMN ${field}`);
|
|
db.pragma("user_version = 3");
|
|
const before = snapshot(db);
|
|
migrateDatabase(db);
|
|
migrateDatabase(db);
|
|
const after = snapshot(db);
|
|
// New fields have compatible defaults; every pre-existing field is unchanged.
|
|
for (const row of after.hy2_nodes as Record<string, unknown>[]) {
|
|
assert.equal(row.node_type, "hy2");
|
|
assert.equal(row.custom_config_yaml, "");
|
|
for (const field of ["node_type", "custom_config_yaml"])
|
|
if (!existing.includes(field)) delete row[field];
|
|
}
|
|
assert.deepEqual(after, before);
|
|
assert.equal(db.pragma("user_version", { simple: true }), 7);
|
|
db.close();
|
|
});
|
|
}
|
|
|
|
test("repairs interrupted v4 without duplicate-column errors", () => {
|
|
const db = remoteDb();
|
|
seed(db);
|
|
db.exec("ALTER TABLE hy2_nodes DROP COLUMN custom_config_yaml");
|
|
migrateDatabase(db);
|
|
assert.equal(
|
|
(
|
|
db.prepare("SELECT custom_config_yaml FROM hy2_nodes").get() as {
|
|
custom_config_yaml: string;
|
|
}
|
|
).custom_config_yaml,
|
|
"",
|
|
);
|
|
db.close();
|
|
});
|
|
|
|
test("failed migrations roll back all statements and future schemas are not downgraded", () => {
|
|
const db = new Database(":memory:");
|
|
db.exec("CREATE TABLE hy2_users(id INTEGER)");
|
|
assert.throws(() => migrateDatabase(db));
|
|
assert.equal(db.pragma("user_version", { simple: true }), 0);
|
|
assert.deepEqual(
|
|
db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all(),
|
|
[{ name: "hy2_users" }],
|
|
);
|
|
db.pragma("user_version = 8");
|
|
assert.throws(() => migrateDatabase(db), /版本/);
|
|
assert.equal(db.pragma("user_version", { simple: true }), 8);
|
|
db.close();
|
|
});
|
|
|
|
test("v6 upgrade preserves AnyTLS installations, certificates and counters", () => {
|
|
const db = remoteDb();
|
|
seed(db);
|
|
migrateDatabase(db);
|
|
db.exec(
|
|
"DROP TABLE relay_routes; DROP TABLE relay_hosts; PRAGMA user_version=6;",
|
|
);
|
|
db.exec(`INSERT INTO managed_certificates VALUES(1,'wildcard','certificate','encrypted-key','2030-01-01','2026-01-01');
|
|
INSERT INTO node_installations(id,token_hash,token_encrypted,certificate_id,slug,name,server_host,sni,origin,expires_at,created_at,node_id,protocol) VALUES('install','hash','encrypted',1,'anytls','AnyTLS','jp2.example','jp2.example','https://panel.example','2030-01-01','2026-01-01',17,'anytls');
|
|
INSERT INTO node_agent_counters VALUES(17,'boot:1','original-auth',128,256);`);
|
|
const tables = [
|
|
"managed_certificates",
|
|
"node_installations",
|
|
"node_agent_counters",
|
|
];
|
|
const before = tables.map((t) => db.prepare(`SELECT * FROM ${t}`).all());
|
|
migrateDatabase(db);
|
|
migrateDatabase(db);
|
|
assert.deepEqual(
|
|
tables.map((t) => db.prepare(`SELECT * FROM ${t}`).all()),
|
|
before,
|
|
);
|
|
assert.equal(db.pragma("user_version", { simple: true }), 7);
|
|
db.close();
|
|
});
|