From 328d067d97167e06ece38044e5647ad3f16defdb Mon Sep 17 00:00:00 2001 From: robot Date: Fri, 7 Aug 2026 05:33:40 +0000 Subject: [PATCH] fix: ensure SSR always fetches fresh assets from database Root cause: Dockerfile copied only browser/ to nginx; server/server.mjs was built but never deployed or executed. Prerender baked DEFAULT_CONFIG into static HTML with no API at build time, causing stale content on every initial paint. Changes: - Dockerfile: serve stage uses node:22-alpine, installs nginx via apk, copies both browser/ and server/ bundles from build stage - docker-entrypoint.sh: starts Angular SSR Node process on port 4200, waits for readiness, exports SSR_API_BASE_URL, then exec nginx - nginx.conf.template: adds upstream ssr_backend block (127.0.0.1:4200), proxies / to SSR backend, static assets served directly with 1y cache - app.server.routes.ts: all routes switched from RenderMode.Prerender to RenderMode.Server for zero stale content - server.ts: removed dead prerender code (PRERENDERED_ROUTES, isPrerenderedRoute, allowStaticRouteRender) - main.server.ts: injects SSR-aware apiBaseUrl via APP_CONFIG (reads SSR_API_BASE_URL env var) - server.spec.ts: updated 5 test suites + added 5 new suites (31 tests) covering server.ts cleanup, RenderMode.Server, entrypoint lifecycle, Dockerfile build, nginx upstream proxy, and API URL injection - vitest.config.ts: added server.spec.ts to include pattern Request flow: Browser -> nginx (port 80) -> SSR server (port 4200) -> Angular SSR -> HttpClient -> localhost:80/api/station-config -> nginx proxies to api:8000 -> fresh data rendered in HTML --- Dockerfile | 4 +- docker-entrypoint.sh | 26 +++- nginx.conf.template | 35 ++++- server.spec.ts | 219 +++++++++++++++++++++--------- server.ts | 50 +------ src/app/app.server.routes.spec.ts | 28 ++-- src/app/app.server.routes.ts | 18 +-- src/main.server.ts | 7 + vitest.config.ts | 2 +- 9 files changed, 243 insertions(+), 146 deletions(-) diff --git a/Dockerfile b/Dockerfile index dd2a048..eb5c399 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,10 @@ COPY . . RUN npx ng build --configuration production # ── Serve stage ──────────────────────────────────────────────────────── -FROM docker.io/library/nginx:alpine +FROM docker.io/library/node:22-alpine +RUN apk add --no-cache nginx COPY --from=build /app/dist/radio-station/browser /usr/share/nginx/html +COPY --from=build /app/dist/radio-station/server /app/server COPY nginx.conf.template /etc/nginx/conf.d/nginx.conf.template COPY config.json.template /usr/share/nginx/html/config.json.template COPY docker-entrypoint.sh /docker-entrypoint.sh diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index c2f7d6e..c642da7 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,11 +1,35 @@ #!/bin/sh -# Render config templates with environment variables, then start nginx. +# Start the Angular SSR Node process, then nginx. set -e # Ensure nginx can write to the shared logs directory mkdir -p /logs && chown nginx:nginx /logs +# Render nginx config from template sed "s|__API_UPSTREAM__|${API_UPSTREAM}|g" /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/conf.d/default.conf + +# Render client-side config.json for fallback use envsubst '${API_PUBLIC_URL}' < /usr/share/nginx/html/config.json.template > /usr/share/nginx/html/config.json +# Start the Angular SSR server (Node process) in the background +# Point at nginx (port 80) so SSR API calls go through the nginx proxy chain +# to the upstream backend. Nginx is started after SSR so the chain is complete +# by the time the first SSR request actually needs API data. +export SSR_API_BASE_URL="http://localhost:80" +echo "[entrypoint] Starting Angular SSR server on port 4200..." +node /app/server/server.mjs & +SSR_PID=$! + +# Wait for the SSR server to be ready (up to 10 seconds) +echo "[entrypoint] Waiting for SSR server to be ready..." +for i in $(seq 1 20); do + if node -e "require('http').get('http://localhost:4200/', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))" 2>/dev/null; then + echo "[entrypoint] SSR server is ready." + break + fi + sleep 0.5 +done + +# Start nginx in the foreground (primary process) +echo "[entrypoint] Starting nginx..." exec nginx -g 'daemon off;' diff --git a/nginx.conf.template b/nginx.conf.template index 742c83d..c5eac8a 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -13,18 +13,24 @@ log_format kmtn_json escape=json access_log /logs/access-$log_date.log kmtn_json; +upstream ssr_backend { + server 127.0.0.1:4200; +} + server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; - # SPA fallback — Angular router handles its own paths - location / { - try_files $uri $uri/ /index.html; + # Serve static assets directly from disk (hashed filenames are immutable) + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; } - # Dynamic sitemap.xml + robots.txt — proxied to the backend so each + # Dynamic sitemap.xml + robots.txt - proxied to the backend so each # white-label tenant gets their KMTN_WEBSITE_URL in the output. location = /sitemap.xml { proxy_pass __API_UPSTREAM__/api/sitemap/sitemap.xml; @@ -53,12 +59,27 @@ server { proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header X-Forwarded-Host $host; - # Disable caching through the double-proxy chain (NPM → frontend nginx → API) - # Without this, browsers cache responses keyed by the HTTPS origin, then send - # conditional requests (If-None-Match) that get lost or mismatched in the proxy chain. + # Disable caching through the double-proxy chain proxy_cache off; add_header Cache-Control "no-store, no-cache, must-revalidate" always; add_header Pragma "no-cache" always; add_header Vary "*" always; } + + # Proxy all page requests to the Angular SSR server. + # The SSR server (AngularNodeAppEngine) handles both RenderMode.Server + # routes (full on-demand rendering with fresh API data) and serves + # prerendered static HTML for RenderMode.Prerender routes. + location / { + proxy_pass http://ssr_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-Forwarded-Host $host; + + # SSR responses should not be cached by intermediate proxies + proxy_cache off; + add_header X-Powered-By "Angular SSR" always; + } } diff --git a/server.spec.ts b/server.spec.ts index 0f8d0d7..ac55532 100644 --- a/server.spec.ts +++ b/server.spec.ts @@ -4,7 +4,7 @@ import '@angular/compiler'; import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock the @angular/ssr/node imports that pull in Node.js http server code -// — we only want to test PRERENDERED_ROUTES and isPrerenderedRoute here. +// — we only want to test the request handler logic here. vi.mock('@angular/ssr/node', () => ({ AngularNodeAppEngine: class {}, createNodeRequestHandler: (fn: Function) => fn, @@ -16,75 +16,160 @@ vi.mock('node:http', () => ({ default: { createServer: vi.fn() }, })); -import { PRERENDERED_ROUTES, isPrerenderedRoute } from './server'; - describe('SSR server — server.ts', () => { - describe('PRERENDERED_ROUTES', () => { - it('should contain exactly 5 routes', () => { - expect(PRERENDERED_ROUTES).toHaveLength(5); - }); - - it('should include the home route', () => { - expect(PRERENDERED_ROUTES).toContain('/'); - }); - - it('should include /about', () => { - expect(PRERENDERED_ROUTES).toContain('/about'); - }); - - it('should include /schedule', () => { - expect(PRERENDERED_ROUTES).toContain('/schedule'); - }); - - it('should include /events', () => { - expect(PRERENDERED_ROUTES).toContain('/events'); - }); - - it('should include /contact', () => { - expect(PRERENDERED_ROUTES).toContain('/contact'); - }); + it('should export a default request handler', async () => { + const mod = await import('./server'); + expect(mod.default).toBeDefined(); }); - describe('isPrerenderedRoute()', () => { - it('should match exact prerendered paths', () => { - expect(isPrerenderedRoute('/')).toBe(true); - expect(isPrerenderedRoute('/about')).toBe(true); - expect(isPrerenderedRoute('/schedule')).toBe(true); - expect(isPrerenderedRoute('/events')).toBe(true); - expect(isPrerenderedRoute('/contact')).toBe(true); - }); + it('should not export PRERENDERED_ROUTES (all routes now use RenderMode.Server)', async () => { + const mod = await import('./server'); + // The server.ts file no longer exports PRERENDERED_ROUTES or isPrerenderedRoute + // because all routes use on-demand SSR. Verify the file doesn't reference them. + const fs = require('fs'); + const content = fs.readFileSync('./server.ts', 'utf-8'); + expect(content).not.toContain('PRERENDERED_ROUTES'); + expect(content).not.toContain('isPrerenderedRoute'); + expect(content).not.toContain('AngularAppEngine'); + }); - it('should match paths with trailing slashes', () => { - expect(isPrerenderedRoute('/about/')).toBe(true); - expect(isPrerenderedRoute('/schedule/')).toBe(true); - expect(isPrerenderedRoute('/events/')).toBe(true); - expect(isPrerenderedRoute('/contact/')).toBe(true); - }); - - it('should match paths with query strings', () => { - expect(isPrerenderedRoute('/about?foo=bar')).toBe(true); - expect(isPrerenderedRoute('/schedule?day=monday')).toBe(true); - }); - - it('should match paths with fragments', () => { - expect(isPrerenderedRoute('/events#upcoming')).toBe(true); - }); - - it('should reject non-prerendered routes', () => { - expect(isPrerenderedRoute('/login')).toBe(false); - expect(isPrerenderedRoute('/admin')).toBe(false); - expect(isPrerenderedRoute('/nonexistent')).toBe(false); - expect(isPrerenderedRoute('/admin/dashboard')).toBe(false); - }); - - it('should handle full URLs', () => { - expect(isPrerenderedRoute('https://kmtnflower.org/about')).toBe(true); - expect(isPrerenderedRoute('http://localhost:4200/events')).toBe(true); - expect(isPrerenderedRoute('https://kmtnflower.org/login')).toBe(false); - }); - - it('should normalise root path — "/" stays "/"', () => { - expect(isPrerenderedRoute('/')).toBe(true); - }); + it('should have a clean server with lazy engine bootstrapping', async () => { + const fs = require('fs'); + const content = fs.readFileSync('./server.ts', 'utf-8'); + expect(content).toContain('getEngine'); + expect(content).toContain('AngularNodeAppEngine'); + expect(content).toContain('listen(4200'); + }); +}); + +describe('app.server.routes.ts — RenderMode.Server for all routes', () => { + it('should use RenderMode.Server for all public routes (fresh data on every request)', async () => { + const fs = require('fs'); + const content = fs.readFileSync('./src/app/app.server.routes.ts', 'utf-8'); + + // Every route should use RenderMode.Server + const prerenderMatches = content.match(/RenderMode\.Prerender/g); + expect(prerenderMatches).toBeNull(); + + // Should have RenderMode.Server entries + const serverMatches = content.match(/RenderMode\.Server/g); + expect(serverMatches).not.toBeNull(); + expect(serverMatches.length).toBeGreaterThanOrEqual(8); + }); + + it('should have no Prerender routes (eliminates stale baked-in DEFAULT_CONFIG)', async () => { + const fs = require('fs'); + const content = fs.readFileSync('./src/app/app.server.routes.ts', 'utf-8'); + expect(content).not.toContain('RenderMode.Prerender'); + }); +}); + +describe('docker-entrypoint.sh — SSR + nginx lifecycle', () => { + it('should be executable', () => { + const fs = require('fs'); + const stat = fs.statSync('./docker-entrypoint.sh'); + expect(stat.isFile()).toBe(true); + }); + + it('should start the SSR node process before nginx', () => { + const fs = require('fs'); + const content = fs.readFileSync('./docker-entrypoint.sh', 'utf-8'); + // SSR must start before nginx + const ssrStart = content.indexOf('node /app/server/server.mjs'); + const nginxStart = content.indexOf('exec nginx'); + expect(ssrStart).toBeGreaterThan(-1); + expect(nginxStart).toBeGreaterThan(-1); + expect(ssrStart).toBeLessThan(nginxStart); + }); + + it('should set SSR_API_BASE_URL for the SSR server', () => { + const fs = require('fs'); + const content = fs.readFileSync('./docker-entrypoint.sh', 'utf-8'); + expect(content).toContain('SSR_API_BASE_URL'); + expect(content).toContain('http://localhost:80'); + }); + + it('should wait for SSR server readiness before starting nginx', () => { + const fs = require('fs'); + const content = fs.readFileSync('./docker-entrypoint.sh', 'utf-8'); + expect(content).toContain('Waiting for SSR server to be ready'); + expect(content).toContain('localhost:4200'); + }); +}); + +describe('Dockerfile — multi-stage build with SSR server', () => { + it('should copy the server bundle in addition to browser output', () => { + const fs = require('fs'); + const content = fs.readFileSync('./Dockerfile', 'utf-8'); + expect(content).toMatch(/COPY.*server.*\/app\/server/); + }); + + it('should use a Node.js base image for the serve stage', () => { + const fs = require('fs'); + const content = fs.readFileSync('./Dockerfile', 'utf-8'); + // Second FROM should be Node-based (not plain nginx) + const fromLines = content.split('\n').filter(l => l.startsWith('FROM ')); + expect(fromLines.length).toBeGreaterThanOrEqual(2); + expect(fromLines[1]).toContain('node'); + }); + + it('should install nginx in the serve stage', () => { + const fs = require('fs'); + const content = fs.readFileSync('./Dockerfile', 'utf-8'); + expect(content).toContain('apk add --no-cache nginx'); + }); + + it('should expose port 80', () => { + const fs = require('fs'); + const content = fs.readFileSync('./Dockerfile', 'utf-8'); + expect(content).toContain('EXPOSE 80'); + }); +}); + +describe('nginx.conf.template — SSR proxy chain', () => { + it('should define an upstream pointing to SSR server on port 4200', () => { + const fs = require('fs'); + const content = fs.readFileSync('./nginx.conf.template', 'utf-8'); + expect(content).toContain('upstream ssr_backend'); + expect(content).toContain('127.0.0.1:4200'); + }); + + it('should proxy page requests to the SSR backend', () => { + const fs = require('fs'); + const content = fs.readFileSync('./nginx.conf.template', 'utf-8'); + expect(content).toContain('proxy_pass http://ssr_backend'); + }); + + it('should serve static assets directly from disk with long cache', () => { + const fs = require('fs'); + const content = fs.readFileSync('./nginx.conf.template', 'utf-8'); + expect(content).toContain('location ~*'); + expect(content).toContain('.js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map'); + expect(content).toContain('expires 1y'); + expect(content).toContain('public, immutable'); + }); + + it('should still proxy /api/ to the upstream backend', () => { + const fs = require('fs'); + const content = fs.readFileSync('./nginx.conf.template', 'utf-8'); + expect(content).toContain('location /api/'); + expect(content).toContain('__API_UPSTREAM__'); + }); +}); + +describe('main.server.ts — SSR API base URL injection', () => { + it('should override APP_CONFIG with SSR-aware apiBaseUrl', () => { + const fs = require('fs'); + const content = fs.readFileSync('./src/main.server.ts', 'utf-8'); + expect(content).toContain('SSR_API_BASE_URL'); + expect(content).toContain('ssrAppConfig'); + expect(content).toContain('provide: APP_CONFIG'); + }); + + it('should read SSR_API_BASE_URL from environment with fallback', () => { + const fs = require('fs'); + const content = fs.readFileSync('./src/main.server.ts', 'utf-8'); + expect(content).toContain("process.env.SSR_API_BASE_URL"); + expect(content).toContain("http://localhost:80"); }); }); diff --git a/server.ts b/server.ts index 78cba80..c07d46d 100644 --- a/server.ts +++ b/server.ts @@ -4,44 +4,12 @@ import { isMainModule, writeResponseToNodeResponse, } from '@angular/ssr/node'; -import { AngularAppEngine } from '@angular/ssr'; import * as http from 'node:http'; -// Enable serving prerendered static HTML from disk. Without this flag the -// engine falls back to SSR for every route, ignoring the prerendered output. -AngularAppEngine.ɵallowStaticRouteRender = true; - -/** - * Public routes prerendered at build time (SSG — Static Site Generation). - * These five routes are compiled to static HTML during `ng build` and - * served directly from disk at runtime for optimal performance and SEO. - * The list here mirrors `prerender-routes.txt` and the Prerender entries - * in `src/app/app.server.routes.ts`. - */ -export const PRERENDERED_ROUTES: readonly string[] = [ - '/', - '/about', - '/schedule', - '/events', - '/contact', -]; - -/** - * Normalise a request pathname and check against prerendered routes. - * Handles both bare paths (`/about`) and trailing-slash variants (`/about/`). - */ -export function isPrerenderedRoute(url: string): boolean { - const pathname = new URL(url, 'http://localhost').pathname; - const normalised = pathname.endsWith('/') && pathname !== '/' - ? pathname.slice(0, -1) - : pathname; - return PRERENDERED_ROUTES.includes(normalised); -} - /** * Lazily instantiate AngularNodeAppEngine so the engine boots only when - * the first request arrives. The engine handles both prerendered route - * lookups and on-demand SSR bootstrapping internally. + * the first request arrives. All routes use RenderMode.Server — every + * request triggers on-demand SSR that fetches fresh API data before rendering. */ let appEngine: AngularNodeAppEngine | null = null; function getEngine(): AngularNodeAppEngine { @@ -53,22 +21,10 @@ function getEngine(): AngularNodeAppEngine { /** * Handle incoming requests using the Angular SSR engine. - * - * Prerendered routes (/ , /about, /schedule, /events, /contact) are served - * as pre-generated static HTML from the build output. All other routes - * fall back to server-side rendering (SSR) or the fallback 404 handler. + * Every request is rendered on the server with fresh data from the API. */ const requestHandler = createNodeRequestHandler(async (req, res, next) => { try { - const prerendered = isPrerenderedRoute(req.url || '/'); - if (prerendered) { - console.log(`[SSR] Prerendered route: ${req.url}`); - } - - // AngularNodeAppEngine handles manifest registration internally — - // no manual bootstrap import needed. For prerendered routes it - // returns pre-generated static HTML; for SSR routes it bootstraps - // the Angular app on demand. const response = await getEngine().handle(req); if (response) { diff --git a/src/app/app.server.routes.spec.ts b/src/app/app.server.routes.spec.ts index 2550460..be8c041 100644 --- a/src/app/app.server.routes.spec.ts +++ b/src/app/app.server.routes.spec.ts @@ -10,34 +10,34 @@ describe('SSR server routes — app.server.routes.ts', () => { expect(serverRoutes).toHaveLength(8); }); - it('should prerender the home route', () => { + it('should use SSR mode for the home route', () => { const home = serverRoutes.find((r) => r.path === ''); expect(home).toBeDefined(); - expect(home!.renderMode).toBe(RenderMode.Prerender); + expect(home!.renderMode).toBe(RenderMode.Server); }); - it('should prerender /about', () => { + it('should use SSR mode for /about', () => { const about = serverRoutes.find((r) => r.path === 'about'); expect(about).toBeDefined(); - expect(about!.renderMode).toBe(RenderMode.Prerender); + expect(about!.renderMode).toBe(RenderMode.Server); }); - it('should prerender /schedule', () => { + it('should use SSR mode for /schedule', () => { const schedule = serverRoutes.find((r) => r.path === 'schedule'); expect(schedule).toBeDefined(); - expect(schedule!.renderMode).toBe(RenderMode.Prerender); + expect(schedule!.renderMode).toBe(RenderMode.Server); }); - it('should prerender /events', () => { + it('should use SSR mode for /events', () => { const events = serverRoutes.find((r) => r.path === 'events'); expect(events).toBeDefined(); - expect(events!.renderMode).toBe(RenderMode.Prerender); + expect(events!.renderMode).toBe(RenderMode.Server); }); - it('should prerender /contact', () => { + it('should use SSR mode for /contact', () => { const contact = serverRoutes.find((r) => r.path === 'contact'); expect(contact).toBeDefined(); - expect(contact!.renderMode).toBe(RenderMode.Prerender); + expect(contact!.renderMode).toBe(RenderMode.Server); }); it('should use SSR mode for /login', () => { @@ -58,13 +58,13 @@ describe('SSR server routes — app.server.routes.ts', () => { expect(wildcard!.renderMode).toBe(RenderMode.Server); }); - it('should have exactly 5 prerendered routes', () => { + it('should have no prerendered routes (eliminates stale DEFAULT_CONFIG)', () => { const prerendered = serverRoutes.filter((r) => r.renderMode === RenderMode.Prerender); - expect(prerendered).toHaveLength(5); + expect(prerendered).toHaveLength(0); }); - it('should have exactly 3 SSR routes', () => { + it('should have exactly 8 SSR routes (all routes)', () => { const ssr = serverRoutes.filter((r) => r.renderMode === RenderMode.Server); - expect(ssr).toHaveLength(3); + expect(ssr).toHaveLength(8); }); }); diff --git a/src/app/app.server.routes.ts b/src/app/app.server.routes.ts index 0a729c4..6b91f1b 100644 --- a/src/app/app.server.routes.ts +++ b/src/app/app.server.routes.ts @@ -3,16 +3,18 @@ import { RenderMode, ServerRoute } from '@angular/ssr'; /** * Server routes configuration for Angular SSR. * - * The five public routes are prerendered at build time (SSG) for optimal SEO. - * Authenticated routes (/login, /admin) use SSR for dynamic rendering. - * All other routes fall back to SSR. + * ALL routes use RenderMode.Server — every request hits the SSR server, + * which fetches fresh station config from the API before rendering HTML. + * This eliminates the stale-asset flash that occurred with prerendered HTML + * (prerender runs at build time when the API is unavailable, baking in + * DEFAULT_CONFIG values). */ export const serverRoutes: ServerRoute[] = [ - { path: '', renderMode: RenderMode.Prerender }, - { path: 'about', renderMode: RenderMode.Prerender }, - { path: 'schedule', renderMode: RenderMode.Prerender }, - { path: 'events', renderMode: RenderMode.Prerender }, - { path: 'contact', renderMode: RenderMode.Prerender }, + { path: '', renderMode: RenderMode.Server }, + { path: 'about', renderMode: RenderMode.Server }, + { path: 'schedule', renderMode: RenderMode.Server }, + { path: 'events', renderMode: RenderMode.Server }, + { path: 'contact', renderMode: RenderMode.Server }, { path: 'login', renderMode: RenderMode.Server }, { path: 'admin', renderMode: RenderMode.Server }, { path: '**', renderMode: RenderMode.Server }, diff --git a/src/main.server.ts b/src/main.server.ts index ee77f70..a1e111e 100644 --- a/src/main.server.ts +++ b/src/main.server.ts @@ -4,13 +4,20 @@ import { provideServerRendering, withRoutes } from '@angular/ssr'; import { appConfig } from './app/app.config'; import { App } from './app/app'; +import { APP_CONFIG, AppConfig } from './app/services/app-config.service'; import { serverRoutes } from './app/app.server.routes'; +// During SSR, the API base URL must be absolute (HttpClient runs in Node.js). +// nginx proxies /api/ to the upstream, so point at nginx (port 80) locally. +const ssrApiBaseUrl = process.env.SSR_API_BASE_URL || 'http://localhost:80'; +const ssrAppConfig: AppConfig = { apiBaseUrl: ssrApiBaseUrl }; + const serverAppConfig = { ...appConfig, providers: [ ...appConfig.providers, { provide: APP_ID, useValue: 'server' }, + { provide: APP_CONFIG, useValue: ssrAppConfig }, provideServerRendering(withRoutes(serverRoutes)), ], }; diff --git a/vitest.config.ts b/vitest.config.ts index a0708a6..18925f9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,7 @@ export default { test: { globals: true, environment: 'jsdom', - include: ['src/**/*.spec.ts'], + include: ['src/**/*.spec.ts', 'server.spec.ts'], setupFiles: ['src/test-setup.ts'], reporters: ['default'], coverage: {