fix(deploy): resolve Docker startup and CSRF issues

- Rename TASKPLANER_DATA_DIR to DATA_DIR (avoid adapter-node envPrefix conflict)
- Add TASKPLANER_ORIGIN for CSRF protection in docker-compose.yml
- Add automatic database schema initialization on startup
- Add Playwright E2E tests for Docker deployment verification
- Update .env.example with correct variable names

Fixes container restart loop and 403 errors on form submission.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Richter
2026-02-01 15:54:44 +01:00
parent 89e703daa5
commit 84ad332737
11 changed files with 304 additions and 7 deletions

View File

@@ -0,0 +1,109 @@
import { test, expect } from '@playwright/test';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* Docker deployment tests
*
* These tests verify the application works correctly when deployed in Docker.
* They specifically test CSRF protection which requires TASKPLANER_ORIGIN to be set.
*
* Run against Docker: BASE_URL=http://localhost:3000 npx playwright test
*/
test.describe('Docker Deployment', () => {
test('application loads', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle(/TaskPlaner/i);
});
test('health endpoint returns ok', async ({ request }) => {
const response = await request.get('/health');
expect(response.status()).toBe(200);
expect(await response.text()).toBe('ok');
});
test('can create entry via form submission', async ({ page }) => {
// This test verifies CSRF protection is properly configured
// If TASKPLANER_ORIGIN is not set, form submissions return 403
await page.goto('/');
// Fill in quick capture form (textarea for content)
const contentInput = page.locator('textarea[name="content"]');
await contentInput.fill('Test entry from Playwright');
// Submit the form - button text is "Add"
const addButton = page.locator('button[type="submit"]:has-text("Add")');
await addButton.click();
// Wait for the entry to appear in the list
await expect(page.locator('text=Test entry from Playwright')).toBeVisible({ timeout: 5000 });
});
test('can toggle entry completion', async ({ page }) => {
await page.goto('/');
// Create a task entry first
const contentInput = page.locator('textarea[name="content"]');
await contentInput.fill('Test task for completion');
// Select task type
const typeSelect = page.locator('select[name="type"]');
await typeSelect.selectOption('task');
const addButton = page.locator('button[type="submit"]:has-text("Add")');
await addButton.click();
// Wait for entry to appear
await expect(page.locator('text=Test task for completion')).toBeVisible({ timeout: 5000 });
// Find and click the checkbox to mark complete
const checkbox = page.locator('input[type="checkbox"]').first();
await checkbox.click();
// Verify the action completed (no 403 error)
// The checkbox should now be checked
await expect(checkbox).toBeChecked({ timeout: 5000 });
});
test('data persists across container restart', async ({ page }) => {
// This test verifies Docker volume persistence
const uniqueContent = `Persistence test ${Date.now()}`;
// Create an entry
await page.goto('/');
const contentInput = page.locator('textarea[name="content"]');
await contentInput.fill(uniqueContent);
const addButton = page.locator('button[type="submit"]:has-text("Add")');
await addButton.click();
// Wait for entry to appear
await expect(page.locator(`text=${uniqueContent}`)).toBeVisible({ timeout: 5000 });
// Restart the container
await execAsync('docker compose restart');
// Wait for container to be healthy again
let healthy = false;
for (let i = 0; i < 30; i++) {
try {
const response = await fetch('http://localhost:3000/health');
if (response.ok) {
healthy = true;
break;
}
} catch {
// Container not ready yet
}
await new Promise((r) => setTimeout(r, 1000));
}
expect(healthy).toBe(true);
// Verify entry still exists after restart
await page.goto('/');
await expect(page.locator(`text=${uniqueContent}`)).toBeVisible({ timeout: 5000 });
});
});