201 lines
7.1 KiB
TypeScript
201 lines
7.1 KiB
TypeScript
import { readFileSync } from 'fs';
|
|
import { resolve } from 'path';
|
|
|
|
const REPO_ROOT = resolve(__dirname, '../../..');
|
|
|
|
/**
|
|
* Reads one repository SQL artifact as normalized lowercase text.
|
|
* @param relativePath - Path relative to the API repository root.
|
|
* @returns SQL text with identifier quotes and repeated whitespace removed.
|
|
*/
|
|
function readNormalizedSql(relativePath: string): string {
|
|
return readFileSync(resolve(REPO_ROOT, relativePath), 'utf8')
|
|
.replace(/`/g, '')
|
|
.replace(/\s+/g, ' ')
|
|
.trim()
|
|
.toLowerCase();
|
|
}
|
|
|
|
/**
|
|
* Extracts one CREATE TABLE body from normalized SQL.
|
|
* @param sql - Normalized SQL text.
|
|
* @param tableName - Table whose declaration is required.
|
|
* @returns CREATE TABLE body used by focused schema assertions.
|
|
*/
|
|
function extractCreateTable(sql: string, tableName: string): string {
|
|
const match = sql.match(
|
|
new RegExp(
|
|
`create table if not exists ${tableName} \\(([\\s\\S]*?)\\) engine=innodb`,
|
|
),
|
|
);
|
|
|
|
expect(match).not.toBeNull();
|
|
return match?.[1] ?? '';
|
|
}
|
|
|
|
/**
|
|
* Asserts the complete DDNS table contract shared by incremental and bootstrap SQL.
|
|
* @param sql - Normalized SQL containing the network DDNS table.
|
|
* @returns Nothing; Jest assertions fail when one schema fragment drifts.
|
|
*/
|
|
function expectDdnsTableContract(sql: string): void {
|
|
const table = extractCreateTable(sql, 'network_ddns_record');
|
|
const requiredColumns = [
|
|
'id bigint not null',
|
|
'name varchar(100) not null',
|
|
'remark text null',
|
|
'record_type varchar(8) not null',
|
|
'source_type varchar(32) not null',
|
|
'port_forward_id bigint null',
|
|
'domain varchar(253) not null',
|
|
'sub_domain varchar(253) not null',
|
|
'active_key varchar(300) null',
|
|
'enabled tinyint(1) not null default 0',
|
|
"sync_status varchar(32) not null default 'disabled'",
|
|
'provider_record_id varchar(32) null',
|
|
'source_address varchar(45) null',
|
|
'applied_address varchar(45) null',
|
|
'retry_count int unsigned not null default 0',
|
|
'next_retry_at datetime(3) null',
|
|
'last_attempt_at datetime(3) null',
|
|
'last_synced_at datetime(3) null',
|
|
'last_error_code varchar(64) null',
|
|
'last_error_message varchar(512) null',
|
|
'is_deleted tinyint(1) not null default 0',
|
|
'create_time datetime(3) not null default current_timestamp(3)',
|
|
'update_time datetime(3) not null default current_timestamp(3) on update current_timestamp(3)',
|
|
];
|
|
|
|
for (const column of requiredColumns) {
|
|
expect(table).toContain(column);
|
|
}
|
|
|
|
expect(table).toContain('primary key');
|
|
expect(table).toContain(
|
|
'unique key uk_network_ddns_record_active_key (active_key)',
|
|
);
|
|
expect(table).toContain(
|
|
'key idx_network_ddns_record_status (is_deleted, enabled, sync_status, next_retry_at)',
|
|
);
|
|
expect(table).toContain(
|
|
'key idx_network_ddns_record_port_forward (port_forward_id)',
|
|
);
|
|
}
|
|
|
|
describe('Network DDNS SQL contract', () => {
|
|
it('keeps the DDNS table aligned in incremental and refactor-v3 bootstrap schemas', () => {
|
|
expectDdnsTableContract(
|
|
readNormalizedSql('sql/network-management-init.sql'),
|
|
);
|
|
expectDdnsTableContract(
|
|
readNormalizedSql('sql/refactor-v3/00-full-schema.sql'),
|
|
);
|
|
});
|
|
|
|
it('adds idempotent Agent IPv6 columns and verifies the schema indexes', () => {
|
|
const incremental = readNormalizedSql('sql/network-management-init.sql');
|
|
const fullSchema = readNormalizedSql('sql/refactor-v3/00-full-schema.sql');
|
|
const verify = readNormalizedSql('sql/refactor-v3/99-verify.sql');
|
|
|
|
for (const sql of [incremental, fullSchema]) {
|
|
const agentState = extractCreateTable(sql, 'network_agent_state');
|
|
expect(agentState).toContain('current_public_ipv6 varchar(45) null');
|
|
expect(agentState).toContain('current_ipv6_observed_at datetime(3) null');
|
|
}
|
|
|
|
expect(incremental).toContain(
|
|
'alter table network_agent_state add column current_public_ipv6 varchar(45) null',
|
|
);
|
|
expect(incremental).toContain(
|
|
'alter table network_agent_state add column current_ipv6_observed_at datetime(3) null',
|
|
);
|
|
expect(incremental).toContain(
|
|
"table_name = 'network_agent_state' and column_name = 'current_public_ipv6'",
|
|
);
|
|
expect(incremental).toContain(
|
|
"table_name = 'network_agent_state' and column_name = 'current_ipv6_observed_at'",
|
|
);
|
|
|
|
expect(verify).toContain(
|
|
"select 'network_ddns_record' as table_name, count(*) as row_count from network_ddns_record",
|
|
);
|
|
expect(verify).toContain(
|
|
"column_name = 'current_public_ipv6' and column_type = 'varchar(45)'",
|
|
);
|
|
expect(verify).toContain(
|
|
"column_name = 'current_ipv6_observed_at' and column_type = 'datetime(3)'",
|
|
);
|
|
expect(verify).toContain(
|
|
"index_name = 'uk_network_ddns_record_active_key'",
|
|
);
|
|
expect(verify).toContain("index_name = 'idx_network_ddns_record_status'");
|
|
expect(verify).toContain(
|
|
"index_name = 'idx_network_ddns_record_port_forward'",
|
|
);
|
|
});
|
|
|
|
it('mirrors stable DDNS permissions and restricts grants to enabled super roles', () => {
|
|
const menu = readNormalizedSql('sql/network-management-menu.sql');
|
|
const seed = readNormalizedSql('sql/refactor-v3/01-seed-core.sql');
|
|
const vben = readNormalizedSql('sql/vben-admin-init.sql');
|
|
const incrementalNonSuperBlock = menu.match(
|
|
/where role\.role_code <> 'super' and menu\.name in \(([\s\S]*?)\);/,
|
|
)?.[1];
|
|
const incrementalSuperBlock = menu.match(
|
|
/join admin_menu menu on menu\.name in \(([\s\S]*?)\) where role\.role_code = 'super'/,
|
|
)?.[1];
|
|
const vbenNonSuperBlock = vben.match(
|
|
/and name not in \(([\s\S]*?)\);/,
|
|
)?.[1];
|
|
const permissions = [
|
|
{
|
|
code: 'system:network:ddns:list',
|
|
id: '2041700000000120222',
|
|
name: 'systemnetworkddnslist',
|
|
},
|
|
{
|
|
code: 'system:network:ddns:create',
|
|
id: '2041700000000120223',
|
|
name: 'systemnetworkddnscreate',
|
|
},
|
|
{
|
|
code: 'system:network:ddns:update',
|
|
id: '2041700000000120224',
|
|
name: 'systemnetworkddnsupdate',
|
|
},
|
|
{
|
|
code: 'system:network:ddns:delete',
|
|
id: '2041700000000120225',
|
|
name: 'systemnetworkddnsdelete',
|
|
},
|
|
{
|
|
code: 'system:network:ddns:retry',
|
|
id: '2041700000000120226',
|
|
name: 'systemnetworkddnsretry',
|
|
},
|
|
];
|
|
|
|
for (const sql of [menu, seed, vben]) {
|
|
for (const permission of permissions) {
|
|
expect(sql).toContain(permission.id);
|
|
expect(sql).toContain(permission.name);
|
|
expect(sql).toContain(permission.code);
|
|
}
|
|
}
|
|
|
|
expect(menu).toContain("where role.role_code <> 'super'");
|
|
expect(menu).toContain("where role.role_code = 'super'");
|
|
expect(menu).toContain('and role.status = 1');
|
|
expect(menu).toContain('and role.is_deleted = 0');
|
|
expect(incrementalNonSuperBlock).toBeDefined();
|
|
expect(incrementalSuperBlock).toBeDefined();
|
|
expect(vbenNonSuperBlock).toBeDefined();
|
|
|
|
for (const permission of permissions) {
|
|
expect(incrementalNonSuperBlock).toContain(permission.name);
|
|
expect(incrementalSuperBlock).toContain(permission.name);
|
|
expect(vbenNonSuperBlock).toContain(permission.name);
|
|
}
|
|
});
|
|
});
|