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', () => {