diff --git a/package.json b/package.json index 8a5e3dcc11..a632214cac 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "preinstall": "npx only-allow pnpm", "postinstall": "next telemetry disable", "dev": "nodemon -e ts,json,yml --watch server --watch seerr-api.yml --exec 'ts-node -r tsconfig-paths/register --files --project server/tsconfig.json server/index.ts'", - "build:server": "tsc --project server/tsconfig.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && copyfiles -u 2 \"server/i18n/locale/*.json\" dist/i18n && tsc-alias -p server/tsconfig.json", + "build:server": "tsc --project server/tsconfig.build.json && copyfiles -u 2 server/templates/**/*.{html,pug} dist/templates && copyfiles -u 2 \"server/i18n/locale/*.json\" dist/i18n && tsc-alias -p server/tsconfig.build.json", "build:next": "next build", "build": "pnpm build:next && pnpm build:server", "lint": "eslint \"./server/**/*.{ts,tsx}\" \"./src/**/*.{ts,tsx}\" --cache", diff --git a/server/datasource.ts b/server/datasource.ts index 699c0bfeb7..20841b18eb 100644 --- a/server/datasource.ts +++ b/server/datasource.ts @@ -1,3 +1,21 @@ +import { Blocklist } from '@server/entity/Blocklist'; +import DiscoverSlider from '@server/entity/DiscoverSlider'; +import Issue from '@server/entity/Issue'; +import IssueComment from '@server/entity/IssueComment'; +import Media from '@server/entity/Media'; +import { MediaRequest } from '@server/entity/MediaRequest'; +import OverrideRule from '@server/entity/OverrideRule'; +import Season from '@server/entity/Season'; +import SeasonRequest from '@server/entity/SeasonRequest'; +import { Session } from '@server/entity/Session'; +import { User } from '@server/entity/User'; +import { UserPushSubscription } from '@server/entity/UserPushSubscription'; +import { UserSettings } from '@server/entity/UserSettings'; +import { Watchlist } from '@server/entity/Watchlist'; +import { IssueCommentSubscriber } from '@server/subscriber/IssueCommentSubscriber'; +import { IssueSubscriber } from '@server/subscriber/IssueSubscriber'; +import { MediaRequestSubscriber } from '@server/subscriber/MediaRequestSubscriber'; +import { MediaSubscriber } from '@server/subscriber/MediaSubscriber'; import fs from 'fs'; import type { TlsOptions } from 'tls'; import type { DataSourceOptions, EntityTarget, Repository } from 'typeorm'; @@ -5,6 +23,30 @@ import { DataSource } from 'typeorm'; const DB_SSL_PREFIX = 'DB_SSL_'; +const entities = [ + Blocklist, + DiscoverSlider, + Issue, + IssueComment, + Media, + MediaRequest, + OverrideRule, + Season, + SeasonRequest, + Session, + User, + UserPushSubscription, + UserSettings, + Watchlist, +]; + +const subscribers = [ + IssueCommentSubscriber, + IssueSubscriber, + MediaRequestSubscriber, + MediaSubscriber, +]; + function boolFromEnv(envVar: string, defaultVal = false) { if (process.env[envVar]) { return process.env[envVar]?.toLowerCase() === 'true'; @@ -53,9 +95,9 @@ const testConfig: DataSourceOptions = { synchronize: true, dropSchema: true, logging: boolFromEnv('DB_LOG_QUERIES'), - entities: ['server/entity/**/*.ts'], + entities, migrations: ['server/migration/sqlite/**/*.ts'], - subscribers: ['server/subscriber/**/*.ts'], + subscribers, }; const devConfig: DataSourceOptions = { @@ -67,9 +109,9 @@ const devConfig: DataSourceOptions = { migrationsRun: false, logging: boolFromEnv('DB_LOG_QUERIES'), enableWAL: true, - entities: ['server/entity/**/*.ts'], + entities, migrations: ['server/migration/sqlite/**/*.ts'], - subscribers: ['server/subscriber/**/*.ts'], + subscribers, }; const prodConfig: DataSourceOptions = { @@ -81,9 +123,9 @@ const prodConfig: DataSourceOptions = { migrationsRun: false, logging: boolFromEnv('DB_LOG_QUERIES'), enableWAL: true, - entities: ['dist/entity/**/*.js'], + entities, migrations: ['dist/migration/sqlite/**/*.js'], - subscribers: ['dist/subscriber/**/*.js'], + subscribers, }; const postgresDevConfig: DataSourceOptions = { @@ -100,9 +142,9 @@ const postgresDevConfig: DataSourceOptions = { synchronize: false, migrationsRun: true, logging: boolFromEnv('DB_LOG_QUERIES'), - entities: ['server/entity/**/*.ts'], + entities, migrations: ['server/migration/postgres/**/*.ts'], - subscribers: ['server/subscriber/**/*.ts'], + subscribers, }; const postgresProdConfig: DataSourceOptions = { @@ -119,9 +161,9 @@ const postgresProdConfig: DataSourceOptions = { synchronize: false, migrationsRun: false, logging: boolFromEnv('DB_LOG_QUERIES'), - entities: ['dist/entity/**/*.js'], + entities, migrations: ['dist/migration/postgres/**/*.js'], - subscribers: ['dist/subscriber/**/*.js'], + subscribers, }; export const isPgsql = process.env.DB_TYPE === 'postgres'; diff --git a/server/scripts/prepareTestDb.ts b/server/scripts/prepareTestDb.ts index ccde8bb74b..bd917c447d 100644 --- a/server/scripts/prepareTestDb.ts +++ b/server/scripts/prepareTestDb.ts @@ -12,6 +12,7 @@ const prepareDb = async () => { await seedTestDb({ preserveDb: process.env.PRESERVE_DB === 'true', withMigrations: process.env.WITH_MIGRATIONS === 'true', + allowOutsideTest: true, }); }; diff --git a/server/tsconfig.build.json b/server/tsconfig.build.json new file mode 100644 index 0000000000..d472b2c2aa --- /dev/null +++ b/server/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.test.ts"] +} diff --git a/server/utils/seedTestDb.test.ts b/server/utils/seedTestDb.test.ts new file mode 100644 index 0000000000..2176bb9868 --- /dev/null +++ b/server/utils/seedTestDb.test.ts @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import { afterEach, describe, it } from 'node:test'; + +import { resetTestDb, seedTestDb } from '@server/utils/seedTestDb'; + +// NODE_ENV is typed readonly, so it is swapped through the whole env object +function setNodeEnv(value: string | undefined) { + Object.assign(process.env, { NODE_ENV: value }); +} + +describe('test database guard', () => { + const originalNodeEnv = process.env.NODE_ENV; + + afterEach(() => { + setNodeEnv(originalNodeEnv); + }); + + it('refuses to seed when NODE_ENV is not test', async () => { + setNodeEnv('production'); + + await assert.rejects(() => seedTestDb(), /Refusing to seed/); + }); + + it('refuses to reset when NODE_ENV is not test', async () => { + setNodeEnv('production'); + + await assert.rejects(() => resetTestDb(), /Refusing to reset/); + }); +}); diff --git a/server/utils/seedTestDb.ts b/server/utils/seedTestDb.ts index 266169d45c..b91d1abf40 100644 --- a/server/utils/seedTestDb.ts +++ b/server/utils/seedTestDb.ts @@ -8,6 +8,8 @@ export interface SeedDbOptions { preserveDb?: boolean; /** If true, runs migrations instead of synchronizing schema */ withMigrations?: boolean; + /** If true, permits seeding while NODE_ENV is not test */ + allowOutsideTest?: boolean; } // Precomputed bcrypt hash of 'test1234'. We precompute this to avoid @@ -15,6 +17,16 @@ export interface SeedDbOptions { const TEST_USER_PASSWORD_HASH = '$2b$12$Z5V2P5HZgmx4/AnWFMZN1.aD5AM1NucNi.mhNTSQ9oVtmdzu7Le/a'; +function assertTestDatabase(operation: string, allowOutsideTest = false): void { + if (allowOutsideTest || process.env.NODE_ENV === 'test') { + return; + } + + throw new Error( + `Refusing to ${operation} while NODE_ENV is not test: this drops every table and seeds accounts with a known password.` + ); +} + /** * Seeds test users into the database. * Assumes the database schema is already set up. @@ -68,6 +80,8 @@ async function seedTestUsers(): Promise { * Used by both Cypress tests and Vitest unit tests. */ export async function seedTestDb(options: SeedDbOptions = {}): Promise { + assertTestDatabase('seed the test database', options.allowOutsideTest); + const dbConnection = dataSource.isInitialized ? dataSource : await dataSource.initialize(); @@ -91,6 +105,8 @@ export async function seedTestDb(options: SeedDbOptions = {}): Promise { * Assumes DB has been initialized. */ export async function resetTestDb(): Promise { + assertTestDatabase('reset the test database'); + await dataSource.synchronize(true); await seedTestUsers(); } diff --git a/src/components/Settings/SettingsNetwork/index.tsx b/src/components/Settings/SettingsNetwork/index.tsx index dd4fd5fe50..23a5674117 100644 --- a/src/components/Settings/SettingsNetwork/index.tsx +++ b/src/components/Settings/SettingsNetwork/index.tsx @@ -175,7 +175,8 @@ const SettingsNetwork = () => { apiRequestTimeout: Number(values.apiRequestTimeout) * 1000, }); mutate('/api/v1/settings/public'); - mutate('/api/v1/status'); + // the key StatusChecker polls on, so the restart modal shows at once + mutate('/api/v1/status?checkUpdateAvailable=false'); addToast(intl.formatMessage(messages.toastSettingsSuccess), { autoDismiss: true,