mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 20:56:19 -08:00
chore: tighten validation and runtime logging
* chore: tighten validation and runtime logging * fix(i18n): localize new settings labels
This commit is contained in:
1 parent
d24c77a143
commit
2d5c4fa4f9
52 files changed
+767
-214
No files matched your search
@@ -0,0 +1,13 @@
|
||||
export default {
|
||||
displayName: 'database',
|
||||
preset: '../../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': [
|
||||
'ts-jest',
|
||||
{ tsconfig: '<rootDir>/tsconfig.spec.json' },
|
||||
],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'js'],
|
||||
coverageDirectory: '../../../coverage/libs/shared/database',
|
||||
};
|
||||
@@ -16,6 +16,14 @@
|
||||
"format": ["cjs"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/shared/database/jest.config.ts",
|
||||
"tsConfig": "libs/shared/database/tsconfig.spec.json"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"command": "eslint libs/shared/database/**/*.ts"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { __databaseConnectionTestHooks } from './connection';
|
||||
|
||||
function compactSql(statement: string): string {
|
||||
return statement.replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function createdObjectNames(prefix: string, statements: readonly string[]) {
|
||||
return statements
|
||||
.map(compactSql)
|
||||
.filter((statement) => statement.startsWith(prefix))
|
||||
.map((statement) => {
|
||||
const match = statement.match(
|
||||
/^(?:CREATE\s+(?:UNIQUE\s+)?(?:VIRTUAL\s+)?(?:TABLE|INDEX|TRIGGER)\s+IF\s+NOT\s+EXISTS)\s+([^\s(]+)/i
|
||||
);
|
||||
return match?.[1] ?? null;
|
||||
})
|
||||
.filter((name): name is string => Boolean(name));
|
||||
}
|
||||
|
||||
describe('database schema statements', () => {
|
||||
const {
|
||||
createTableStatements,
|
||||
columnMigrationStatements,
|
||||
indexMigrationStatements,
|
||||
} = __databaseConnectionTestHooks;
|
||||
|
||||
it('defines the core fresh-install tables, indexes, and FTS triggers', () => {
|
||||
const schemaSql = createTableStatements.map(compactSql).join('\n');
|
||||
|
||||
expect(schemaSql).toContain('CREATE TABLE IF NOT EXISTS playlists');
|
||||
expect(schemaSql).toContain('CREATE TABLE IF NOT EXISTS categories');
|
||||
expect(schemaSql).toContain('CREATE TABLE IF NOT EXISTS content');
|
||||
expect(schemaSql).toContain('CREATE TABLE IF NOT EXISTS epg_channels');
|
||||
expect(schemaSql).toContain('CREATE TABLE IF NOT EXISTS epg_programs');
|
||||
expect(schemaSql).toContain(
|
||||
'CREATE VIRTUAL TABLE IF NOT EXISTS epg_programs_fts USING fts5'
|
||||
);
|
||||
expect(schemaSql).toContain(
|
||||
'CREATE TABLE IF NOT EXISTS playback_positions'
|
||||
);
|
||||
expect(schemaSql).toContain('CREATE TABLE IF NOT EXISTS downloads');
|
||||
expect(schemaSql).toContain(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS favorites_content_playlist_unique'
|
||||
);
|
||||
expect(schemaSql).toContain(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS recently_viewed_content_playlist_unique'
|
||||
);
|
||||
expect(schemaSql).toContain(
|
||||
'CREATE TRIGGER IF NOT EXISTS epg_programs_ai'
|
||||
);
|
||||
expect(schemaSql).toContain(
|
||||
'CREATE TRIGGER IF NOT EXISTS epg_programs_ad'
|
||||
);
|
||||
expect(schemaSql).toContain(
|
||||
'CREATE TRIGGER IF NOT EXISTS epg_programs_au'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps legacy column migrations idempotent ALTER TABLE statements', () => {
|
||||
expect(columnMigrationStatements.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
columnMigrationStatements
|
||||
.map(compactSql)
|
||||
.every((statement) => statement.startsWith('ALTER TABLE '))
|
||||
).toBe(true);
|
||||
expect(columnMigrationStatements.map(compactSql)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'ALTER TABLE categories ADD COLUMN hidden INTEGER DEFAULT 0',
|
||||
'ALTER TABLE playlists ADD COLUMN payload TEXT',
|
||||
'ALTER TABLE favorites ADD COLUMN position INTEGER DEFAULT 0',
|
||||
'ALTER TABLE content ADD COLUMN backdrop_url TEXT',
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps legacy index migrations idempotent IF NOT EXISTS statements', () => {
|
||||
expect(indexMigrationStatements.length).toBeGreaterThan(0);
|
||||
expect(
|
||||
indexMigrationStatements
|
||||
.map(compactSql)
|
||||
.every((statement) =>
|
||||
/^CREATE (?:UNIQUE )?INDEX IF NOT EXISTS /i.test(statement)
|
||||
)
|
||||
).toBe(true);
|
||||
expect(indexMigrationStatements.map(compactSql)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS categories_playlist_type_xtream_unique ON categories(playlist_id, type, xtream_id)',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS content_category_type_xtream_unique ON content(category_id, type, xtream_id)',
|
||||
'CREATE INDEX IF NOT EXISTS favorites_playlist_position_idx ON favorites(playlist_id, position, added_at DESC)',
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('does not define duplicate fresh-install schema object names', () => {
|
||||
const objectNames = [
|
||||
...createdObjectNames(
|
||||
'CREATE TABLE IF NOT EXISTS',
|
||||
createTableStatements
|
||||
),
|
||||
...createdObjectNames(
|
||||
'CREATE VIRTUAL TABLE IF NOT EXISTS',
|
||||
createTableStatements
|
||||
),
|
||||
...createdObjectNames(
|
||||
'CREATE INDEX IF NOT EXISTS',
|
||||
createTableStatements
|
||||
),
|
||||
...createdObjectNames(
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS',
|
||||
createTableStatements
|
||||
),
|
||||
...createdObjectNames(
|
||||
'CREATE TRIGGER IF NOT EXISTS',
|
||||
createTableStatements
|
||||
),
|
||||
];
|
||||
|
||||
expect(objectNames).toHaveLength(new Set(objectNames).size);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"esModuleInterop": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node10",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -22,6 +22,9 @@
|
||||
"jestConfig": "libs/shared/interfaces/jest.config.ts",
|
||||
"tsConfig": "libs/shared/interfaces/tsconfig.spec.json"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './lib/channel.interface';
|
||||
export * from './lib/channel.model';
|
||||
export * from './lib/dev-logger.util';
|
||||
export * from './lib/embedded-mpv-session.interface';
|
||||
export * from './lib/epg-channel-metadata.model';
|
||||
export * from './lib/epg-channel-with-programs.interface';
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { createDevLogger } from './dev-logger.util';
|
||||
|
||||
describe('createDevLogger', () => {
|
||||
const globalWithNgDevMode = globalThis as typeof globalThis & {
|
||||
ngDevMode?: boolean;
|
||||
};
|
||||
|
||||
const originalNgDevMode = globalWithNgDevMode.ngDevMode;
|
||||
const originalNodeEnv = process.env['NODE_ENV'];
|
||||
let debugSpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
globalWithNgDevMode.ngDevMode = true;
|
||||
process.env['NODE_ENV'] = 'development';
|
||||
debugSpy = jest.spyOn(console, 'debug').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalWithNgDevMode.ngDevMode = originalNgDevMode;
|
||||
if (originalNodeEnv === undefined) {
|
||||
delete process.env['NODE_ENV'];
|
||||
} else {
|
||||
process.env['NODE_ENV'] = originalNodeEnv;
|
||||
}
|
||||
debugSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('logs scoped debug output outside production and test mode', () => {
|
||||
const debug = createDevLogger('Scope');
|
||||
|
||||
debug('message', { id: 1 });
|
||||
|
||||
expect(debugSpy).toHaveBeenCalledWith('[Scope]', 'message', {
|
||||
id: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('suppresses debug output in Angular production mode', () => {
|
||||
globalWithNgDevMode.ngDevMode = false;
|
||||
const debug = createDevLogger('Scope');
|
||||
|
||||
debug('message');
|
||||
|
||||
expect(debugSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('suppresses debug output during tests', () => {
|
||||
process.env['NODE_ENV'] = 'test';
|
||||
const debug = createDevLogger('Scope');
|
||||
|
||||
debug('message');
|
||||
|
||||
expect(debugSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
export type DevLogFn = (...args: unknown[]) => void;
|
||||
|
||||
function isDevLoggingEnabled(): boolean {
|
||||
const global = globalThis as typeof globalThis & {
|
||||
ngDevMode?: boolean;
|
||||
process?: { env?: { NODE_ENV?: string } };
|
||||
};
|
||||
|
||||
return (
|
||||
global.ngDevMode !== false && global.process?.env?.NODE_ENV !== 'test'
|
||||
);
|
||||
}
|
||||
|
||||
export function createDevLogger(scope: string): DevLogFn {
|
||||
const prefix = `[${scope}]`;
|
||||
|
||||
return (...args: unknown[]) => {
|
||||
if (isDevLoggingEnabled()) {
|
||||
console.debug(prefix, ...args);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,9 @@
|
||||
"options": {
|
||||
"jestConfig": "libs/shared/m3u-utils/jest.config.ts"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user