From 4d5fb43ebc4e024b8c80aef09e32c0aa7e391552 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Thu, 16 Apr 2026 13:06:14 +0200 Subject: [PATCH] Enhance deploy-test.sh with functions for Node.js version management, dependency installation, and public document synchronization. Implement checks for Node.js version requirements and improve error handling for document syncing. Update environment configuration in harheimertc.test.config.cjs to support development and test environments. Modify email recipient logic in contact and email service APIs to prevent notifications in test environments. Add tests to verify behavior in test conditions. --- .gitea/workflows/code-analysis.yml | 33 +++++++++++- deploy-test.sh | 87 ++++++++++++++++++++++++++++-- harheimertc.test.config.cjs | 3 +- server/api/contact.post.js | 6 +++ server/utils/email-service.js | 4 +- tests/auth-endpoints.spec.ts | 29 ++++++++++ tests/public-endpoints.spec.ts | 22 +++++++- 7 files changed, 175 insertions(+), 9 deletions(-) diff --git a/.gitea/workflows/code-analysis.yml b/.gitea/workflows/code-analysis.yml index 5cba5f7..5e69f94 100644 --- a/.gitea/workflows/code-analysis.yml +++ b/.gitea/workflows/code-analysis.yml @@ -3,7 +3,7 @@ name: Code Analysis and Production Deploy on: pull_request: push: - branches: [ main ] + branches: [ main, dev ] jobs: analyze: @@ -119,3 +119,34 @@ jobs: -p "${{ vars.PROD_PORT }}" \ "${{ vars.PROD_USER }}@${{ vars.PROD_HOST }}" \ "bash -lc 'cd /var/www/harheimertc && ./deploy-production.sh'" + + deploy-test: + runs-on: ubuntu-latest + needs: analyze + if: github.event_name == 'push' && github.ref == 'refs/heads/dev' + steps: + - name: Prepare SSH + run: | + set -euo pipefail + + mkdir -p ~/.ssh + printf "%s" "${{ secrets.PROD_SSH_KEY }}" > ~/.ssh/id_ed25519 + chmod 600 ~/.ssh/id_ed25519 + ssh-keyscan -p "${{ vars.PROD_PORT }}" "${{ vars.PROD_HOST }}" >> ~/.ssh/known_hosts + + - name: Test SSH connection + run: | + ssh -i ~/.ssh/id_ed25519 \ + -o StrictHostKeyChecking=no \ + -o BatchMode=yes \ + -p "${{ vars.PROD_PORT }}" \ + "${{ vars.PROD_USER }}@${{ vars.PROD_HOST }}" \ + "echo SSH OK" + + - name: Run test deployment script + run: | + ssh -i ~/.ssh/id_ed25519 \ + -o BatchMode=yes \ + -p "${{ vars.PROD_PORT }}" \ + "${{ vars.PROD_USER }}@${{ vars.PROD_HOST }}" \ + "bash -lc 'cd /var/www/harheimertc.test && ./deploy-test.sh'" diff --git a/deploy-test.sh b/deploy-test.sh index f8649cf..fac56aa 100755 --- a/deploy-test.sh +++ b/deploy-test.sh @@ -67,6 +67,77 @@ has_tracked_files_under() { git ls-files "$prefix" | head -n 1 | grep -q . } +install_dependencies() { + if [ -f "package-lock.json" ]; then + echo " Running: npm ci" + npm ci + else + echo " WARNING: package-lock.json fehlt. Führe npm install aus..." + npm install + fi +} + +use_project_node() { + export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" + if [ -s "$NVM_DIR/nvm.sh" ]; then + # shellcheck disable=SC1090 + . "$NVM_DIR/nvm.sh" + if [ -f ".nvmrc" ]; then + echo " Using Node version from .nvmrc..." + nvm use + fi + fi +} + +ensure_node_version() { + if ! command -v node >/dev/null 2>&1; then + echo "ERROR: Node.js ist nicht im PATH." + exit 1 + fi + + local node_version + node_version="$(node -p 'process.versions.node')" + if ! node -e 'const [major, minor] = process.versions.node.split(".").map(Number); process.exit(major > 22 || (major === 22 && minor >= 12) ? 0 : 1)' >/dev/null 2>&1; then + echo "ERROR: Node.js >= 22.12.0 wird benötigt, aktuell ist $node_version aktiv." + echo "Bitte Node 22 installieren/aktivieren, z.B.:" + echo " nvm install 22" + echo " nvm alias default 22" + exit 1 + fi + + echo " Node.js $node_version" +} + +sync_public_documents_to_build() { + if [ ! -d "public/documents" ]; then + echo " No public/documents directory to sync" + return 0 + fi + + if [ ! -d ".output/public" ]; then + echo "ERROR: .output/public fehlt, kann public/documents nicht synchronisieren." + exit 1 + fi + + mkdir -p ".output/public/documents" + cp -a "public/documents/." ".output/public/documents/" + echo " ✓ public/documents -> .output/public/documents synchronisiert" + + local template_pdf="beitrittserklärung_template.pdf" + if [ -f "public/documents/$template_pdf" ]; then + local source_size output_size + source_size=$(stat -f%z "public/documents/$template_pdf" 2>/dev/null || stat -c%s "public/documents/$template_pdf" 2>/dev/null || echo "0") + output_size=$(stat -f%z ".output/public/documents/$template_pdf" 2>/dev/null || stat -c%s ".output/public/documents/$template_pdf" 2>/dev/null || echo "0") + + if [ "$source_size" != "$output_size" ] || [ "$source_size" = "0" ]; then + echo "ERROR: .output/public/documents/$template_pdf stimmt nicht mit public/documents überein (Source: $source_size, Output: $output_size)." + exit 1 + fi + + echo " ✓ $template_pdf im Build verifiziert ($output_size bytes)" + fi +} + echo "0. Ensuring persistent data directories (recommended)..." # IMPORTANT: Only symlink server/data if it's not tracked by git. if has_tracked_files_under "server/data"; then @@ -143,7 +214,9 @@ git clean -fd \ # Pull latest changes echo " Pulling latest changes..." -if ! git pull --ff-only; then +git fetch origin dev +git checkout dev +if ! git pull --ff-only origin dev; then echo "ERROR: git pull fehlgeschlagen." echo "" echo "Häufige Ursache: SSH-Key für den aktuellen User fehlt." @@ -170,7 +243,9 @@ fi # 3. Install dependencies echo "" echo "3. Installing dependencies..." -npm install +use_project_node +ensure_node_version +install_dependencies # 4. Remove old build (but keep data!) echo "" @@ -204,7 +279,7 @@ fi if [ ! -d "node_modules" ]; then echo "" echo "WARNING: node_modules fehlt. Installiere Dependencies..." - npm install + install_dependencies fi # 5. Build @@ -217,7 +292,7 @@ echo " (This may take a few minutes...)" echo " Checking dependencies..." if [ ! -f "node_modules/.package-lock.json" ] && [ ! -f "package-lock.json" ]; then echo " WARNING: package-lock.json fehlt. Führe npm install aus..." - npm install + install_dependencies fi # Build mit expliziter Fehlerbehandlung und Output-Capture @@ -234,6 +309,10 @@ if [ "$BUILD_EXIT_CODE" -ne 0 ]; then exit 1 fi +echo "" +echo " Synchronizing public documents into build output..." +sync_public_documents_to_build + # Prüfe auf Warnungen im Build-Output, die auf Probleme hinweisen if echo "$BUILD_OUTPUT" | grep -qi "error\|failed\|missing"; then echo "" diff --git a/harheimertc.test.config.cjs b/harheimertc.test.config.cjs index a659fa3..e9dcb0f 100644 --- a/harheimertc.test.config.cjs +++ b/harheimertc.test.config.cjs @@ -10,7 +10,8 @@ try { // Helper function to create env object function createEnv(port) { return { - NODE_ENV: 'production', + NODE_ENV: process.env.NODE_ENV || 'development', + APP_ENV: process.env.APP_ENV || 'test', PORT: port, // Secrets/Config (loaded from .env above, if present) ENCRYPTION_KEY: process.env.ENCRYPTION_KEY, diff --git a/server/api/contact.post.js b/server/api/contact.post.js index ca22cb6..1eb1e3c 100644 --- a/server/api/contact.post.js +++ b/server/api/contact.post.js @@ -24,6 +24,12 @@ async function loadConfig() { } async function collectRecipients(config) { + const isProduction = process.env.NODE_ENV === 'production' && process.env.APP_ENV !== 'test' + + if (!isProduction) { + return ['tsschulz@tsschulz.de'] + } + const recipients = [] // Vorstand diff --git a/server/utils/email-service.js b/server/utils/email-service.js index c72deed..bd68010 100644 --- a/server/utils/email-service.js +++ b/server/utils/email-service.js @@ -41,7 +41,7 @@ async function loadConfig() { * @returns {Array} Email addresses */ function getEmailRecipients(data, config) { - const isProduction = process.env.NODE_ENV === 'production' + const isProduction = process.env.NODE_ENV === 'production' && process.env.APP_ENV !== 'test' if (!isProduction) { return ['tsschulz@tsschulz.de'] @@ -215,4 +215,4 @@ export async function sendRegistrationNotification(data) { console.error('sendRegistrationNotification failed:', error.message || error) throw error } -} \ No newline at end of file +} diff --git a/tests/auth-endpoints.spec.ts b/tests/auth-endpoints.spec.ts index a04989f..28e5574 100644 --- a/tests/auth-endpoints.spec.ts +++ b/tests/auth-endpoints.spec.ts @@ -62,6 +62,11 @@ import resetPasswordHandler from '../server/api/auth/reset-password.post.js' import statusHandler from '../server/api/auth/status.get.js' describe('Auth API Endpoints', () => { + afterEach(() => { + delete process.env.NODE_ENV + delete process.env.APP_ENV + }) + beforeEach(() => { // Setze SMTP-Credentials für Tests process.env.SMTP_USER = 'test@example.com' @@ -171,6 +176,30 @@ describe('Auth API Endpoints', () => { }) expect(nodemailer.default.createTransport).toHaveBeenCalled() }) + + it('benachrichtigt in Testumgebung nicht die Vorstand-Empfänger', async () => { + process.env.NODE_ENV = 'production' + process.env.APP_ENV = 'test' + + const event = createEvent() + mockSuccessReadBody({ + name: 'Max', + email: 'max@example.com', + password: '12345678', + phone: '123', + geburtsdatum: '2000-01-01' + }) + authUtils.readUsers.mockResolvedValue([]) + authUtils.hashPassword.mockResolvedValue('hashed') + authUtils.writeUsers.mockResolvedValue(true) + + await registerHandler(event) + + const transporter = nodemailer.default.createTransport.mock.results[0].value + expect(transporter.sendMail).toHaveBeenNthCalledWith(1, expect.objectContaining({ + to: 'tsschulz@tsschulz.de' + })) + }) }) describe('POST /api/auth/reset-password', () => { diff --git a/tests/public-endpoints.spec.ts b/tests/public-endpoints.spec.ts index ea2fdf9..60403bd 100644 --- a/tests/public-endpoints.spec.ts +++ b/tests/public-endpoints.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { createEvent, mockSuccessReadBody } from './setup' import fsPromises from 'fs/promises' import { promises as fs } from 'fs' @@ -26,6 +26,11 @@ import termineHandler from '../server/api/termine.get.js' import spielplaeneHandler from '../server/api/spielplaene.get.js' describe('Öffentliche API-Endpunkte', () => { + afterEach(() => { + delete process.env.NODE_ENV + delete process.env.APP_ENV + }) + beforeEach(() => { // Setze SMTP-Credentials für Tests process.env.SMTP_USER = 'test@example.com' @@ -58,6 +63,21 @@ describe('Öffentliche API-Endpunkte', () => { expect(response.success).toBe(true) expect(nodemailer.default.createTransport).toHaveBeenCalled() }) + + it('sendet in Testumgebung nicht an Vorstand-Empfänger', async () => { + process.env.NODE_ENV = 'production' + process.env.APP_ENV = 'test' + + const event = createEvent() + mockSuccessReadBody({ name: 'Max', email: 'max@example.com', subject: 'Frage', message: 'Hallo' }) + + await contactHandler(event) + + const transporter = nodemailer.default.createTransport.mock.results[0].value + expect(transporter.sendMail).toHaveBeenCalledWith(expect.objectContaining({ + to: 'tsschulz@tsschulz.de' + })) + }) }) describe('GET /api/galerie', () => {