Skip to content

Commit 2b876c1

Browse files
committed
Add end-to-end tests for email login/logout and global settings access control
- Implemented E2E tests for email login and logout functionality, covering both admin and regular user scenarios. - Verified session management and cookie handling during login/logout processes. - Added tests to ensure proper rejection of invalid login attempts. - Created E2E tests for global settings access control, differentiating between admin and regular user permissions. - Included tests for CRUD operations on settings, ensuring proper access control and response validation. - Established global setup and teardown scripts for consistent test environment initialization and cleanup. - Enhanced test context management for sharing state across tests. - Updated TypeScript configuration to include test files and improve module resolution. - Optimized router behavior to reduce unnecessary API calls during navigation. - Improved user feedback in setup success messages.
1 parent 34dc0f0 commit 2b876c1

35 files changed

+10039
-4152
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ fastly-events.log
5656
deploystack.db
5757
services/backend/persistent_data/*
5858

59+
# Test files
60+
services/backend/tests/.test-context.json
61+
5962
._*.ts
6063
._*.vue
6164
._*.md

package-lock.json

Lines changed: 8119 additions & 3914 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@
99
"dev:backend": "cd services/backend && npm run dev",
1010
"build:frontend": "cd services/frontend && npm run lint",
1111
"build:backend": "cd services/backend && npm run lint",
12-
"lint:md": "npx markdownlint-cli2 '**/*.md' '#node_modules' '#**/node_modules/**' '#.github' '#**/CHANGELOG.md'",
12+
"lint:md": "npx markdownlint-cli2 '**/*.md' '#node_modules' '#**/node_modules/**' '#.github' '#**/CHANGELOG.md' '#**/._*'",
1313
"lint:frontend": "cd services/frontend && npm run lint",
1414
"lint:backend": "cd services/backend && npm run lint",
1515
"release:backend": "cd services/backend && npm run release",
16-
"release:frontend": "cd services/frontend && npm run release"
16+
"release:frontend": "cd services/frontend && npm run release",
17+
"test:backend": "cd services/backend && npm run test"
1718
},
1819
"devDependencies": {
1920
"markdownlint-cli2": "^0.18.1"

services/backend/DB.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,31 @@ The request body should be:
4747

4848
Replace the `connectionString` with your actual PostgreSQL connection URI.
4949

50-
**Important:** After the initial database setup via this API, you **must restart the backend server** for the changes to take full effect and for the application to connect to the newly configured database.
50+
**Note:** The database setup is now complete in a single API call. After successful setup, all database-dependent services (global settings, plugins, etc.) are automatically initialized and ready to use immediately. No server restart is required.
51+
52+
#### API Response
53+
54+
The setup endpoint returns a JSON response indicating the success status and whether a restart is required:
55+
56+
**Successful Setup (No Restart Required):**
57+
58+
```json
59+
{
60+
"message": "Database setup successful. All services have been initialized and are ready to use.",
61+
"restart_required": false
62+
}
63+
```
64+
65+
**Successful Setup (Restart Required - Fallback):**
66+
67+
```json
68+
{
69+
"message": "Database setup successful, but some services may require a server restart to function properly.",
70+
"restart_required": true
71+
}
72+
```
73+
74+
In most cases, the setup will complete successfully without requiring a restart. The `restart_required: true` response is a fallback for edge cases where the automatic re-initialization fails.
5175

5276
### Database Configuration File
5377

@@ -181,6 +205,19 @@ You can inspect the SQLite database directly using various tools:
181205
182206
## Troubleshooting
183207
208+
### Database Setup Issues
209+
210+
- **Setup fails with re-initialization error**: If the setup endpoint returns `restart_required: true`, you can manually restart the server to complete the setup process
211+
- **Database already configured**: If you get a 409 error, the database has already been set up. Use the status endpoint to check the current configuration
212+
- **Services not working after setup**: Check the server logs for any initialization errors. In rare cases, a manual restart may be needed
213+
214+
### Migration Issues
215+
184216
- If you get a "table already exists" error, check if you've already applied the migration
185217
- For complex schema changes, you may need to create multiple migrations
186218
- To reset the database, delete the `services/backend/persistent_data/database/deploystack.db` file and restart the server
219+
220+
### Plugin Issues
221+
222+
- **Plugins not working after setup**: Plugins with database extensions should automatically receive database access after setup. Check server logs for plugin re-initialization messages
223+
- **Plugin database tables missing**: Ensure plugins are properly loaded before database setup, or restart the server if tables are missing

services/backend/drizzle/migrations_sqlite/0003_huge_prism.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ ALTER TABLE `authUser` ADD `role_id` text REFERENCES roles(id);
1515

1616
-- Insert default roles
1717
INSERT INTO `roles` (`id`, `name`, `description`, `permissions`, `is_system_role`, `created_at`, `updated_at`) VALUES
18-
('global_admin', 'Global Administrator', 'Full system access with user management capabilities', '["users.list","users.view","users.edit","users.delete","users.create","roles.manage","system.admin"]', 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
19-
('global_user', 'Global User', 'Standard user with basic profile access', '["profile.view","profile.edit"]', 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000);
18+
('global_admin', 'Global Administrator', 'Full system access with user management capabilities', '["users.list","users.view","users.edit","users.delete","users.create","roles.manage","system.admin","settings.view","settings.edit","settings.delete","teams.create","teams.view","teams.edit","teams.delete","teams.manage","team.members.view","team.members.manage"]', 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000),
19+
('global_user', 'Global User', 'Standard user with basic profile access', '["profile.view","profile.edit","teams.create","teams.view","teams.edit","teams.delete","team.members.view"]', 1, strftime('%s', 'now') * 1000, strftime('%s', 'now') * 1000);
2020
--> statement-breakpoint
2121

2222
-- Update existing users to have global_user role (all users since role_id starts as NULL)

services/backend/jest.config.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module.exports = {
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
testMatch: ['**/tests/**/*.test.ts'],
5+
moduleNameMapper: {
6+
'^@src/(.*)$': '<rootDir>/src/$1',
7+
},
8+
globalSetup: '<rootDir>/tests/globalSetup.ts',
9+
globalTeardown: '<rootDir>/tests/globalTeardown.ts',
10+
transform: {
11+
'^.+\\.ts$': ['ts-jest', {
12+
useESM: false,
13+
}],
14+
},
15+
// Run tests sequentially to ensure proper order
16+
maxWorkers: 1,
17+
// Use custom test sequencer to ensure correct order
18+
testSequencer: '<rootDir>/tests/testSequencer.js',
19+
};

services/backend/package.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"lint": "eslint --config eslint.config.ts 'src/**/*.ts' --fix",
99
"db:generate": "drizzle-kit generate",
1010
"db:up": "drizzle-kit up",
11-
"release": "release-it --config=.release-it.js"
11+
"release": "release-it --config=.release-it.js",
12+
"test": "jest"
1213
},
1314
"dependencies": {
1415
"@fastify/cookie": "^11.0.2",
@@ -32,13 +33,24 @@
3233
"@eslint/js": "^9.27.0",
3334
"@release-it/conventional-changelog": "^10.0.1",
3435
"@types/better-sqlite3": "^7.6.13",
36+
"@types/fs-extra": "^11.0.4",
37+
"@types/jest": "^29.5.14",
38+
"@types/supertest": "^6.0.3",
3539
"@typescript-eslint/eslint-plugin": "^8.33.0",
3640
"@typescript-eslint/parser": "^8.33.0",
3741
"drizzle-kit": "^0.31.1",
3842
"eslint": "^9.27.0",
43+
"fs-extra": "^11.3.0",
44+
"jest": "^29.7.0",
3945
"release-it": "^19.0.2",
46+
"supertest": "^7.1.1",
47+
"ts-jest": "^29.3.4",
4048
"ts-node": "^10.9.2",
4149
"typescript": "^5.8.3",
4250
"typescript-eslint": "^8.33.0"
51+
},
52+
"overrides": {
53+
"glob": "^10.0.0",
54+
"inflight": "npm:@isaacs/inflight@^1.0.6"
4355
}
4456
}

services/backend/src/db/config.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ import path from 'node:path';
77
const CONFIG_DIR = path.join(__dirname, '..', '..', 'persistent_data');
88
const CONFIG_FILE_PATH = path.join(CONFIG_DIR, 'db.selection.json');
99

10+
// Helper function to check if we're in test mode
11+
function isTestMode(): boolean {
12+
return process.env.NODE_ENV === 'test';
13+
}
14+
15+
// Helper function for conditional logging
16+
function logInfo(message: string): void {
17+
if (!isTestMode()) {
18+
console.log(message);
19+
}
20+
}
21+
1022
export interface SQLiteConfig {
1123
type: 'sqlite';
1224
dbPath: string; // Relative to services/backend directory
@@ -44,7 +56,7 @@ export async function saveDbConfig(config: DbConfig): Promise<void> {
4456
await fs.mkdir(CONFIG_DIR, { recursive: true }); // Ensure directory exists
4557
const data = JSON.stringify(config, null, 2);
4658
await fs.writeFile(CONFIG_FILE_PATH, data, 'utf-8');
47-
console.log(`[INFO] Database configuration saved to ${CONFIG_FILE_PATH}`);
59+
logInfo(`[INFO] Database configuration saved to ${CONFIG_FILE_PATH}`);
4860
} catch (error) {
4961
console.error('[ERROR] Failed to save database configuration:', error);
5062
throw error; // Re-throw to indicate failure
@@ -58,11 +70,11 @@ export async function saveDbConfig(config: DbConfig): Promise<void> {
5870
export async function deleteDbConfig(): Promise<void> {
5971
try {
6072
await fs.unlink(CONFIG_FILE_PATH);
61-
console.log(`[INFO] Database configuration deleted from ${CONFIG_FILE_PATH}`);
73+
logInfo(`[INFO] Database configuration deleted from ${CONFIG_FILE_PATH}`);
6274
} catch (error) {
6375
// @ts-expect-error - error.code
6476
if (error.code === 'ENOENT') {
65-
console.log('[INFO] Database configuration file not found, nothing to delete.');
77+
logInfo('[INFO] Database configuration file not found, nothing to delete.');
6678
return;
6779
}
6880
console.error('[ERROR] Failed to delete database configuration:', error);

services/backend/src/db/index.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,18 @@ let isDbConfigured = false;
2929

3030
const MIGRATIONS_TABLE_NAME = '__drizzle_migrations';
3131

32+
// Helper function to check if we're in test mode
33+
function isTestMode(): boolean {
34+
return process.env.NODE_ENV === 'test';
35+
}
36+
37+
// Helper function for conditional logging
38+
function logInfo(message: string): void {
39+
if (!isTestMode()) {
40+
console.log(message);
41+
}
42+
}
43+
3244
function getColumnBuilder(type: 'text' | 'integer' | 'timestamp') {
3345
if (type === 'text') return sqliteText;
3446
if (type === 'integer') return sqliteInteger;
@@ -89,11 +101,11 @@ async function applyMigrations() {
89101
try {
90102
await fs.access(migrationsPath);
91103
} catch {
92-
console.log(`[INFO] Migrations directory not found at: ${migrationsPath}, skipping migrations.`);
104+
logInfo(`[INFO] Migrations directory not found at: ${migrationsPath}, skipping migrations.`);
93105
return;
94106
}
95107

96-
console.log(`[INFO] Checking for new migrations in ${migrationsPath}...`);
108+
logInfo(`[INFO] Checking for new migrations in ${migrationsPath}...`);
97109
await ensureMigrationsTable();
98110

99111
let appliedMigrations: { name: string }[] = [];
@@ -108,7 +120,7 @@ async function applyMigrations() {
108120

109121
for (const file of migrationFiles) {
110122
if (!appliedMigrationNames.includes(file)) {
111-
console.log(`[INFO] Applying migration: ${file}`);
123+
logInfo(`[INFO] Applying migration: ${file}`);
112124
const migrationFilePath = path.join(migrationsPath, file);
113125
const sqlContent = await fs.readFile(migrationFilePath, 'utf8');
114126
const statements = sqlContent.split('--> statement-breakpoint');
@@ -122,21 +134,21 @@ async function applyMigrations() {
122134
}
123135
sqliteConn.prepare(`INSERT INTO ${MIGRATIONS_TABLE_NAME} (migration_name) VALUES (?)`).run(file);
124136
sqliteConn.exec('COMMIT');
125-
console.log(`[INFO] Applied migration: ${file}`);
137+
logInfo(`[INFO] Applied migration: ${file}`);
126138
} catch (error) {
127139
const typedError = error as Error;
128140
console.error(`[ERROR] Failed to apply migration ${file}:`, typedError.message, typedError.stack);
129141
throw error;
130142
}
131143
} else {
132-
console.log(`[INFO] Migration already applied: ${file}`);
144+
logInfo(`[INFO] Migration already applied: ${file}`);
133145
}
134146
}
135147
}
136148

137149
export async function initializeDatabase(): Promise<boolean> {
138150
if (isDbInitialized) {
139-
console.log('[INFO] Database already initialized.');
151+
logInfo('[INFO] Database already initialized.');
140152
return true;
141153
}
142154

@@ -170,8 +182,8 @@ export async function initializeDatabase(): Promise<boolean> {
170182
const sqliteConn = new SqliteDriver(absoluteDbPath); // Use constructor
171183
dbConnection = sqliteConn;
172184
dbInstance = drizzleSqliteAdapter(sqliteConn, { schema: dbSchema, logger: false });
173-
console.log(`[INFO] Connected to SQLite database at: ${absoluteDbPath}`);
174-
if (!dbExists) console.log(`[INFO] SQLite database created at: ${absoluteDbPath}`);
185+
logInfo(`[INFO] Connected to SQLite database at: ${absoluteDbPath}`);
186+
if (!dbExists) logInfo(`[INFO] SQLite database created at: ${absoluteDbPath}`);
175187

176188
if (dbInstance) { // Ensure dbInstance is not null
177189
await applyMigrations();
@@ -180,7 +192,7 @@ export async function initializeDatabase(): Promise<boolean> {
180192
}
181193

182194
isDbInitialized = true;
183-
console.log('[INFO] Database initialized successfully.');
195+
logInfo('[INFO] Database initialized successfully.');
184196
return true;
185197
}
186198

@@ -195,7 +207,7 @@ export async function setupNewDatabase(config: DbConfig): Promise<boolean> {
195207
await saveDbConfig(config);
196208
currentDbConfig = config;
197209
isDbConfigured = true;
198-
console.log(`[INFO] Database configuration saved: ${config.type}`);
210+
logInfo(`[INFO] Database configuration saved: ${config.type}`);
199211
}
200212

201213
isDbInitialized = false;
@@ -251,15 +263,15 @@ export function getDbStatus() {
251263
// Function to force schema regeneration (useful for development)
252264
export function regenerateSchema(): void {
253265
if (currentDbConfig) {
254-
console.log('[INFO] Forcing schema regeneration...');
266+
logInfo('[INFO] Forcing schema regeneration...');
255267
dbSchema = generateSchema();
256268

257269
// Recreate the database instance with new schema
258270
if (dbConnection) {
259271
dbInstance = drizzleSqliteAdapter(dbConnection as SqliteDriver.Database, { schema: dbSchema, logger: false });
260272
}
261273

262-
console.log('[INFO] Schema regenerated successfully.');
274+
logInfo('[INFO] Schema regenerated successfully.');
263275
}
264276
}
265277

@@ -286,7 +298,7 @@ export function registerPluginTables(plugins: Plugin[]) {
286298
}
287299

288300
export async function createPluginTables(plugins: Plugin[]) { // db param not used
289-
console.log('[INFO] Attempting to create plugin tables (Note: Better handled by migrations)...');
301+
logInfo('[INFO] Attempting to create plugin tables (Note: Better handled by migrations)...');
290302
if (!currentDbConfig) {
291303
console.error("[ERROR] Cannot create plugin tables: DB config unknown.");
292304
return;
@@ -300,7 +312,7 @@ export async function createPluginTables(plugins: Plugin[]) { // db param not us
300312
for (const [defName] of Object.entries(ext.tableDefinitions)) {
301313
const fullTableName = `${plugin.meta.id}_${defName}`;
302314
if (dbSchema && dbSchema[fullTableName]) {
303-
console.log(`[INFO] Table ${fullTableName} already defined in schema. Creation should be handled by migrations.`);
315+
logInfo(`[INFO] Table ${fullTableName} already defined in schema. Creation should be handled by migrations.`);
304316
} else {
305317
console.warn(`[WARN] Table definition for ${fullTableName} not found in generated schema. Skipping creation.`);
306318
}
@@ -312,7 +324,7 @@ export async function initializePluginDatabases(db: AnyDatabase, plugins: Plugin
312324
for (const plugin of plugins) {
313325
const ext = plugin.databaseExtension as DatabaseExtensionWithTables | undefined; // Cast here
314326
if (ext?.onDatabaseInit) {
315-
console.log(`[INFO] Initializing database for plugin: ${plugin.meta.id}`);
327+
logInfo(`[INFO] Initializing database for plugin: ${plugin.meta.id}`);
316328
await ext.onDatabaseInit(db); // db is AnyDatabase, should be compatible
317329
}
318330
}

services/backend/src/global-settings/index.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import fs from 'fs';
22
import path from 'path';
33
import { GlobalSettingsService } from '../services/globalSettingsService';
4+
import { getDb, getSchema } from '../db';
5+
import { eq } from 'drizzle-orm';
46
import type {
57
GlobalSettingsModule,
68
GlobalSettingDefinition,
@@ -177,13 +179,18 @@ export class GlobalSettingsInitService {
177179
*/
178180
private static async groupExists(groupId: string): Promise<boolean> {
179181
try {
180-
const { getDb, getSchema } = await import('../db');
181-
const { eq } = await import('drizzle-orm');
182182
const db = getDb();
183183
const schema = getSchema();
184+
185+
// Check if database is available
186+
if (!db) {
187+
console.warn(`Database not available during group existence check for: ${groupId}`);
188+
return false;
189+
}
190+
184191
const globalSettingGroupsTable = schema.globalSettingGroups;
185-
186192
if (!globalSettingGroupsTable) {
193+
console.warn(`GlobalSettingGroups table not found in schema for group: ${groupId}`);
187194
return false;
188195
}
189196

@@ -196,7 +203,7 @@ export class GlobalSettingsInitService {
196203

197204
return results.length > 0;
198205
} catch (error) {
199-
console.error(`Error checking if group exists: ${groupId}`, error);
206+
console.warn(`Error checking if group exists: ${groupId}`, error instanceof Error ? error.message : 'Unknown error');
200207
return false;
201208
}
202209
}
@@ -206,11 +213,15 @@ export class GlobalSettingsInitService {
206213
*/
207214
private static async createGroup(group: GlobalSettingGroup): Promise<void> {
208215
try {
209-
const { getDb, getSchema } = await import('../db');
210216
const db = getDb();
211217
const schema = getSchema();
218+
219+
// Check if database is available
220+
if (!db) {
221+
throw new Error(`Database not available during group creation for: ${group.id}`);
222+
}
223+
212224
const globalSettingGroupsTable = schema.globalSettingGroups;
213-
214225
if (!globalSettingGroupsTable) {
215226
throw new Error('GlobalSettingGroups table not found in schema');
216227
}

0 commit comments

Comments
 (0)