feat: add comprehensive test suite and CI pipeline
- Add 402 unit tests with 61.7% code coverage - Add Vitest configuration with coverage reporting - Extract testable utility functions from services - Create test.yml workflow (runs on PR and push to main) - Update docker-build.yml to require tests before building - Add scheduler-utils.ts and server-config.ts for testable code Test files added: - auth.test.ts, medications.test.ts, planner.test.ts - settings.test.ts, doses.test.ts, share.test.ts - database.test.ts, server.test.ts, services.test.ts - env.test.ts, translations.test.ts, integration.test.ts - e2e-routes.test.ts, stock-calculation.test.ts
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Utility functions for server configuration.
|
||||
* Exported separately to allow testing without triggering server start.
|
||||
*/
|
||||
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import type { CookieSerializeOptions } from "@fastify/cookie";
|
||||
|
||||
/**
|
||||
* Parse comma-separated CORS origins string
|
||||
*/
|
||||
export function parseCorsOrigins(originsStr: string): string[] {
|
||||
return originsStr
|
||||
.split(",")
|
||||
.map((o) => o.trim())
|
||||
.filter((o) => o.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build base cookie options for access token
|
||||
*/
|
||||
export function buildBaseCookieOptions(
|
||||
accessTtlMinutes: number,
|
||||
isProduction: boolean
|
||||
): CookieSerializeOptions {
|
||||
return {
|
||||
httpOnly: true,
|
||||
secure: isProduction,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: accessTtlMinutes * 60, // Convert minutes to seconds
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build refresh cookie options (extends base with longer TTL)
|
||||
*/
|
||||
export function buildRefreshCookieOptions(
|
||||
baseCookieOptions: CookieSerializeOptions,
|
||||
refreshTtlDays: number
|
||||
): CookieSerializeOptions {
|
||||
return {
|
||||
...baseCookieOptions,
|
||||
maxAge: refreshTtlDays * 24 * 60 * 60, // Convert days to seconds
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build complete app configuration object
|
||||
*/
|
||||
export interface AppConfigOptions {
|
||||
jwtSecret?: string;
|
||||
refreshSecret?: string;
|
||||
accessTtlMinutes: number;
|
||||
refreshTtlDays: number;
|
||||
isProduction: boolean;
|
||||
}
|
||||
|
||||
export interface AppConfig {
|
||||
accessSecret: string;
|
||||
refreshSecret: string;
|
||||
accessTtl: number;
|
||||
refreshTtl: number;
|
||||
cookieOptions: CookieSerializeOptions;
|
||||
refreshCookieOptions: CookieSerializeOptions;
|
||||
}
|
||||
|
||||
export function buildAppConfig(options: AppConfigOptions): AppConfig {
|
||||
const cookieOptions = buildBaseCookieOptions(
|
||||
options.accessTtlMinutes,
|
||||
options.isProduction
|
||||
);
|
||||
const refreshCookieOptions = buildRefreshCookieOptions(
|
||||
cookieOptions,
|
||||
options.refreshTtlDays
|
||||
);
|
||||
|
||||
return {
|
||||
accessSecret: options.jwtSecret || "",
|
||||
refreshSecret: options.refreshSecret || "",
|
||||
accessTtl: options.accessTtlMinutes,
|
||||
refreshTtl: options.refreshTtlDays,
|
||||
cookieOptions,
|
||||
refreshCookieOptions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure images directory exists
|
||||
*/
|
||||
export function ensureImagesDirectory(cwd?: string): string {
|
||||
const basePath = cwd || process.cwd();
|
||||
const imagesDir = resolve(basePath, "data/images");
|
||||
if (!existsSync(imagesDir)) {
|
||||
mkdirSync(imagesDir, { recursive: true });
|
||||
}
|
||||
return imagesDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWT configuration based on auth enabled status
|
||||
*/
|
||||
export interface JwtConfig {
|
||||
secret: string;
|
||||
cookie: {
|
||||
cookieName: string;
|
||||
signed: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function getJwtConfig(authEnabled: boolean, jwtSecret?: string): JwtConfig {
|
||||
const effectiveSecret =
|
||||
authEnabled && jwtSecret
|
||||
? jwtSecret
|
||||
: "auth-disabled-no-secret-needed";
|
||||
|
||||
return {
|
||||
secret: effectiveSecret,
|
||||
cookie: {
|
||||
cookieName: "access_token",
|
||||
signed: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user