import { readFileSync } from 'node:fs'; import path from 'node:path'; import { describe, expect, it } from 'vitest'; const projectRoot = process.cwd(); const compose = readFileSync(path.join(projectRoot, 'compose.yaml'), 'utf8'); const composeExample = readFileSync( path.join(projectRoot, 'compose.example.yaml'), 'utf8' ); const containerfile = readFileSync( path.join(projectRoot, 'Containerfile.release'), 'utf8' ); const environmentExample = readFileSync( path.join(projectRoot, '.env.example'), 'utf8' ); const nginxConfig = readFileSync( path.join(projectRoot, 'deploy', 'nginx.conf'), 'utf8' ); const releaseLock = JSON.parse( readFileSync(path.join(projectRoot, 'release', 'toolbox.lock.json'), 'utf8') ) as { releaseVersion: string }; const immutableCacheControl = 'public, max-age=31536000, immutable'; const immutableUriPatterns = Array.from( nginxConfig.matchAll( new RegExp( `^\\s*"~([^"]+)"\\s+"${immutableCacheControl.replaceAll( ', ', ',\\s+' )}";$`, 'gmu' ) ), (match) => new RegExp(match[1], 'u') ); const isImmutable = (uri: string): boolean => immutableUriPatterns.some((pattern) => pattern.test(uri)); describe('production deployment', () => { it('keeps the release version and checksum pins synchronized', () => { const containerVersion = containerfile.match( /^ARG TOOLBOX_RELEASE_VERSION=(\S+)$/mu )?.[1]; const composeVersion = compose.match( /\$\{TOOLBOX_RELEASE_VERSION:-([^}]+)\}/u )?.[1]; const containerChecksum = containerfile.match( /^ARG TOOLBOX_RELEASE_SHA256=([a-f0-9]{64})$/mu )?.[1]; const composeChecksum = compose.match( /\$\{TOOLBOX_RELEASE_SHA256:-([a-f0-9]{64})\}/u )?.[1]; const environmentVersion = environmentExample.match( /^TOOLBOX_RELEASE_VERSION=(\S+)$/mu )?.[1]; const environmentChecksum = environmentExample.match( /^TOOLBOX_RELEASE_SHA256=([a-f0-9]{64})$/mu )?.[1]; const localImageVersion = composeExample.match( /^\s+image: git\.add-ideas\.de\/zemion\/toolbox:(\S+)$/mu )?.[1]; expect(containerVersion).toBe(releaseLock.releaseVersion); expect(composeVersion).toBe(containerVersion); expect(composeChecksum).toBe(containerChecksum); expect(environmentVersion).toBe(containerVersion); expect(environmentChecksum).toBe(containerChecksum); expect(localImageVersion).toBe(containerVersion); }); it('verifies the release before extraction and retains a rootless final image', () => { expect(containerfile).toContain("--proto-redir '=https' --tlsv1.2"); expect(containerfile.indexOf('sha256sum -c -')).toBeGreaterThan(0); expect(containerfile.indexOf('unzip -q')).toBeGreaterThan( containerfile.indexOf('sha256sum -c -') ); expect(containerfile).toContain( 'COPY --from=release --chown=101:101 /tmp/toolbox/' ); for (const app of ['pdf', 'xslt', 'onenote', 'av', 'regex']) { expect(containerfile).toContain( `test -f /tmp/toolbox/apps/${app}/toolbox-app.json` ); } expect(containerfile).toMatch(/\nUSER 101:101\nEXPOSE 8080\n/u); }); it('uses only the requested external Traefik network and redirects HTTP to HTTPS', () => { expect(compose).toContain('dockerfile: Containerfile.release'); expect(compose).toContain('traefik.enable: "true"'); expect(compose).toContain( 'traefik.docker.network: "${TRAEFIK_NETWORK:-internal}"' ); expect(compose).toContain( 'traefik.http.routers.addideas-toolbox.rule: "Host(`${TOOLBOX_HOST:-toolbox.add-ideas.de}`)"' ); expect(compose).toContain( 'traefik.http.routers.addideas-toolbox.entrypoints: websecure' ); expect(compose).toContain( 'traefik.http.routers.addideas-toolbox.tls.certresolver: "${TRAEFIK_CERT_RESOLVER:-netcup}"' ); expect(compose).toContain( 'traefik.http.routers.addideas-toolbox-http.rule: "Host(`${TOOLBOX_HOST:-toolbox.add-ideas.de}`)"' ); expect(compose).toContain( 'traefik.http.routers.addideas-toolbox-http.entrypoints: web' ); expect(compose).toContain( 'traefik.http.routers.addideas-toolbox-http.priority: 900' ); expect(compose).toContain( 'traefik.http.routers.addideas-toolbox-http.middlewares: addideas-toolbox-https-redirect' ); expect(compose).toContain( 'traefik.http.routers.addideas-toolbox-http.service: addideas-toolbox' ); expect(compose).toContain( 'traefik.http.middlewares.addideas-toolbox-https-redirect.redirectscheme.scheme: https' ); expect(compose).toContain( 'traefik.http.middlewares.addideas-toolbox-https-redirect.redirectscheme.permanent: "true"' ); expect(compose).toContain('external: true'); expect(compose).not.toMatch(/^\s+ports:/mu); }); it('quotes nginx regular expressions that contain brace quantifiers', () => { expect(nginxConfig).toContain( '"~^/(?:.*/)?assets/.*-[A-Za-z0-9_-]{8,}\\.[^/]+$"' ); expect(nginxConfig).not.toMatch(/^\s*~[^\n]*\{\d/mu); }); it('caches only hashed assets and the versioned FFmpeg runtime immutably', () => { expect(immutableUriPatterns).toHaveLength(2); for (const uri of [ '/assets/index-Bp9nK_3a.js', '/apps/av/vendor/ffmpeg/0.12.10/version.json', '/apps/av/vendor/ffmpeg/0.12.10/st/ffmpeg-core.js', '/apps/av/vendor/ffmpeg/0.12.10/st/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.js', '/apps/av/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.worker.js', '/apps/av/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/version.json', '/apps/av/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.js', '/apps/av/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/st/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.js', '/apps/av/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12.10-reviewed-stack5m.1/mt/ffmpeg-core.worker.js', ]) { expect(isImmutable(uri), uri).toBe(true); } for (const uri of [ '/', '/index.html', '/SOURCE.md', '/LICENSE', '/toolbox.catalog.json', '/toolbox.release.json', '/apps/av/toolbox-app.json', '/apps/av/THIRD_PARTY_NOTICES.md', '/apps/av/vendor/ffmpeg/version.json', '/apps/av/vendor/ffmpeg/latest/st/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12/st/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/01.2.3/st/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12.10/st/other.wasm', '/apps/av/vendor/ffmpeg/0.12.10/mt/ffmpeg-core.worker.wasm', '/apps/av/vendor/ffmpeg/0.12.10/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12.10-/st/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12.10-reviewed_/st/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12.10-latest/st/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12.10-SNAPSHOT/st/ffmpeg-core.wasm', '/apps/av/vendor/ffmpeg/0.12.10-reviewed-stack5m.2/st/ffmpeg-core.wasm', ]) { expect(isImmutable(uri), uri).toBe(false); } }); it('serves WebAssembly through an explicit MIME-mapped location', () => { expect(nginxConfig).toMatch( /location ~\* \\\.wasm\$\s*\{\s*types\s*\{\s*application\/wasm wasm;\s*\}\s*try_files \$uri =404;\s*\}/mu ); expect(nginxConfig).not.toMatch( /location ~\* [^\n]*\(\?:[^\n|)]*\bwasm\b[^\n)]*\)/u ); }); it('allows same-origin WebAssembly workers and local blob media previews', () => { expect(nginxConfig).toContain("script-src 'self' 'wasm-unsafe-eval'"); expect(nginxConfig).toContain("worker-src 'self' blob:"); expect(nginxConfig).toContain("media-src 'self' blob:"); expect(nginxConfig).not.toContain("'unsafe-eval'"); }); });