Compare commits

...
Author SHA1 Message Date
ishan-karmakar 2700e62935 Remove unnecessary landing page port expose
CI / Test Suite (push) Has been cancelled
CI / Lint & Type Check (push) Has been cancelled
Docker Build and Publish / prepare (push) Has been cancelled
Docker Build and Publish / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Docker Build and Publish / build (linux/arm64, ubuntu-24.04-arm) (push) Has been cancelled
Docker Build and Publish / merge (push) Has been cancelled
Release Please / release-please (push) Has been cancelled
2026-06-06 10:51:50 -05:00
ishan-karmakar 517b93cc95 Update docker-compose for our purposes
CI / Test Suite (push) Waiting to run
CI / Lint & Type Check (push) Waiting to run
Docker Build and Publish / prepare (push) Waiting to run
Docker Build and Publish / build (linux/amd64, ubuntu-latest) (push) Blocked by required conditions
Docker Build and Publish / build (linux/arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Docker Build and Publish / merge (push) Blocked by required conditions
Release Please / release-please (push) Waiting to run
2026-06-06 10:25:13 -05:00
Dries Augustyns 8e3fb9595d tests: prevent race condition by inserting RUNNING execution directly 2026-05-27 18:14:29 +02:00
Dries Augustyns 58be4abc31 tests: prevent race condition by inserting RUNNING execution directly 2026-05-27 18:08:13 +02:00
Dries Augustyns 480f0c1034 tests: refactor request logger tests to use helper functions for async logging verification 2026-05-27 18:01:56 +02:00
3 changed files with 67 additions and 119 deletions
@@ -3,6 +3,20 @@ import type {NextFunction, Request, Response} from 'express';
import {databaseRequestLogger} from '../requestLogger.js'; import {databaseRequestLogger} from '../requestLogger.js';
import {factories, getPrismaClient} from '../../../../../test/helpers'; import {factories, getPrismaClient} from '../../../../../test/helpers';
async function waitForLog(prisma: ReturnType<typeof getPrismaClient>, id: string, timeoutMs = 2000) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const record = await prisma.apiRequest.findUnique({where: {id}});
if (record) return record;
await new Promise(resolve => setTimeout(resolve, 20));
}
return prisma.apiRequest.findUnique({where: {id}});
}
async function waitForNoLog(ms = 200) {
await new Promise(resolve => setTimeout(resolve, ms));
}
describe('Request Logger Middleware', () => { describe('Request Logger Middleware', () => {
const prisma = getPrismaClient(); const prisma = getPrismaClient();
let req: Partial<Request>; let req: Partial<Request>;
@@ -77,13 +91,7 @@ describe('Request Logger Middleware', () => {
const responseBody = {success: true, data: {id: '123'}}; const responseBody = {success: true, data: {id: '123'}};
await res.json!(responseBody); await res.json!(responseBody);
// Wait for async logging to complete const loggedRequest = await waitForLog(prisma, 'test-request-id-123');
await new Promise(resolve => setTimeout(resolve, 100));
// Verify database record was created
const loggedRequest = await prisma.apiRequest.findUnique({
where: {id: 'test-request-id-123'},
});
expect(loggedRequest).toBeDefined(); expect(loggedRequest).toBeDefined();
expect(loggedRequest?.method).toBe('POST'); expect(loggedRequest?.method).toBe('POST');
@@ -109,11 +117,7 @@ describe('Request Logger Middleware', () => {
const responseBody = {success: true}; const responseBody = {success: true};
await res.json!(responseBody); await res.json!(responseBody);
await new Promise(resolve => setTimeout(resolve, 100)); const loggedRequest = await waitForLog(prisma, 'public-request-id');
const loggedRequest = await prisma.apiRequest.findUnique({
where: {id: 'public-request-id'},
});
expect(loggedRequest).toBeDefined(); expect(loggedRequest).toBeDefined();
expect(loggedRequest?.projectId).toBeNull(); expect(loggedRequest?.projectId).toBeNull();
@@ -131,11 +135,7 @@ describe('Request Logger Middleware', () => {
await res.json!({success: true}); await res.json!({success: true});
await new Promise(resolve => setTimeout(resolve, 100)); const loggedRequest = await waitForLog(prisma, 'test-request-id-123');
const loggedRequest = await prisma.apiRequest.findUnique({
where: {id: 'test-request-id-123'},
});
// Allow for timer imprecision (especially in CI environments) // Allow for timer imprecision (especially in CI environments)
expect(loggedRequest?.duration).toBeGreaterThanOrEqual(45); expect(loggedRequest?.duration).toBeGreaterThanOrEqual(45);
@@ -162,11 +162,7 @@ describe('Request Logger Middleware', () => {
await res.json!(errorResponse); await res.json!(errorResponse);
await new Promise(resolve => setTimeout(resolve, 100)); const loggedRequest = await waitForLog(prisma, 'test-request-id-123');
const loggedRequest = await prisma.apiRequest.findUnique({
where: {id: 'test-request-id-123'},
});
expect(loggedRequest).toBeDefined(); expect(loggedRequest).toBeDefined();
expect(loggedRequest?.statusCode).toBe(400); expect(loggedRequest?.statusCode).toBe(400);
@@ -189,11 +185,7 @@ describe('Request Logger Middleware', () => {
await res.json!(errorResponse); await res.json!(errorResponse);
await new Promise(resolve => setTimeout(resolve, 100)); const loggedRequest = await waitForLog(prisma, 'test-request-id-123');
const loggedRequest = await prisma.apiRequest.findUnique({
where: {id: 'test-request-id-123'},
});
expect(loggedRequest?.statusCode).toBe(500); expect(loggedRequest?.statusCode).toBe(500);
expect(loggedRequest?.errorCode).toBe('INTERNAL_SERVER_ERROR'); expect(loggedRequest?.errorCode).toBe('INTERNAL_SERVER_ERROR');
@@ -209,11 +201,7 @@ describe('Request Logger Middleware', () => {
error: {code: 'RESOURCE_NOT_FOUND', message: 'Template not found'}, error: {code: 'RESOURCE_NOT_FOUND', message: 'Template not found'},
}); });
await new Promise(resolve => setTimeout(resolve, 100)); const loggedRequest = await waitForLog(prisma, 'test-request-id-123');
const loggedRequest = await prisma.apiRequest.findUnique({
where: {id: 'test-request-id-123'},
});
expect(loggedRequest?.statusCode).toBe(404); expect(loggedRequest?.statusCode).toBe(404);
expect(loggedRequest?.errorCode).toBe('RESOURCE_NOT_FOUND'); expect(loggedRequest?.errorCode).toBe('RESOURCE_NOT_FOUND');
@@ -306,11 +294,8 @@ describe('Request Logger Middleware', () => {
databaseRequestLogger(req as Request, res as Response, next); databaseRequestLogger(req as Request, res as Response, next);
await res.json!({success: true}); await res.json!({success: true});
await new Promise(resolve => setTimeout(resolve, 100));
const loggedRequest = await prisma.apiRequest.findUnique({ const loggedRequest = await waitForLog(prisma, `log-${path.replace(/\//g, '-')}`);
where: {id: `log-${path.replace(/\//g, '-')}`},
});
expect(loggedRequest).toBeDefined(); expect(loggedRequest).toBeDefined();
expect(loggedRequest?.path).toBe(path); expect(loggedRequest?.path).toBe(path);
@@ -352,17 +337,18 @@ describe('Request Logger Middleware', () => {
await res.json!({success: true}); await res.json!({success: true});
await new Promise(resolve => setTimeout(resolve, 100)); // Should create a record with generated UUID — poll for it
const deadline = Date.now() + 2000;
// Should create a record with generated UUID let allRequests: Awaited<ReturnType<typeof prisma.apiRequest.findMany>> = [];
const allRequests = await prisma.apiRequest.findMany({ while (Date.now() < deadline) {
where: { allRequests = await prisma.apiRequest.findMany({
path: '/v1/send', where: {path: '/v1/send', method: 'POST'},
method: 'POST', orderBy: {createdAt: 'desc'},
}, take: 1,
orderBy: {createdAt: 'desc'}, });
take: 1, if (allRequests.length > 0) break;
}); await new Promise(resolve => setTimeout(resolve, 20));
}
expect(allRequests.length).toBeGreaterThan(0); expect(allRequests.length).toBeGreaterThan(0);
expect(allRequests[0].id).toBeDefined(); expect(allRequests[0].id).toBeDefined();
@@ -418,11 +404,8 @@ describe('Request Logger Middleware', () => {
databaseRequestLogger(reqWithSize as Request, resWithId as Response, next); databaseRequestLogger(reqWithSize as Request, resWithId as Response, next);
await resWithId.json!({success: true}); await resWithId.json!({success: true});
await new Promise(resolve => setTimeout(resolve, 100));
const loggedRequest = await prisma.apiRequest.findUnique({ const loggedRequest = await waitForLog(prisma, 'test-size-5000');
where: {id: 'test-size-5000'},
});
expect(loggedRequest?.requestSize).toBe(5000); expect(loggedRequest?.requestSize).toBe(5000);
}); });
@@ -445,11 +428,8 @@ describe('Request Logger Middleware', () => {
databaseRequestLogger(reqNoSize as Request, resWithId as Response, next); databaseRequestLogger(reqNoSize as Request, resWithId as Response, next);
await resWithId.json!({success: true}); await resWithId.json!({success: true});
await new Promise(resolve => setTimeout(resolve, 100));
const loggedRequest = await prisma.apiRequest.findUnique({ const loggedRequest = await waitForLog(prisma, 'test-no-size');
where: {id: 'test-no-size'},
});
expect(loggedRequest?.requestSize).toBeNull(); expect(loggedRequest?.requestSize).toBeNull();
}); });
@@ -474,11 +454,8 @@ describe('Request Logger Middleware', () => {
}; };
await resLarge.json!(largeResponse); await resLarge.json!(largeResponse);
await new Promise(resolve => setTimeout(resolve, 100));
const loggedRequest = await prisma.apiRequest.findUnique({ const loggedRequest = await waitForLog(prisma, 'test-large-response');
where: {id: 'test-large-response'},
});
const expectedSize = JSON.stringify(largeResponse).length; const expectedSize = JSON.stringify(largeResponse).length;
expect(loggedRequest?.responseSize).toBe(expectedSize); expect(loggedRequest?.responseSize).toBe(expectedSize);
@@ -758,8 +758,20 @@ describe('WorkflowService', () => {
}); });
const contact = await factories.createContact({projectId}); const contact = await factories.createContact({projectId});
// Start first execution (still running) // Insert a RUNNING execution directly to avoid racing with the background
await WorkflowService.startExecution(projectId, workflow.id, contact.id); // step processor that startExecution kicks off (a trigger-only workflow can
// transition to COMPLETED before the second call observes it as RUNNING).
const triggerStep = await prisma.workflowStep.findFirst({
where: {workflowId: workflow.id, type: WorkflowStepType.TRIGGER},
});
await prisma.workflowExecution.create({
data: {
workflowId: workflow.id,
contactId: contact.id,
status: WorkflowExecutionStatus.RUNNING,
currentStepId: triggerStep?.id,
},
});
// Second execution should fail (first still running) // Second execution should fail (first still running)
await expect(WorkflowService.startExecution(projectId, workflow.id, contact.id)).rejects.toThrow( await expect(WorkflowService.startExecution(projectId, workflow.id, contact.id)).rejects.toThrow(
+17 -58
View File
@@ -27,39 +27,6 @@ services:
# Infrastructure Services # Infrastructure Services
# ============================================ # ============================================
postgres:
image: postgres:16-alpine
container_name: plunk-postgres
restart: unless-stopped
environment:
POSTGRES_DB: plunk
POSTGRES_USER: plunk
POSTGRES_PASSWORD: ${DB_PASSWORD:-changeme123}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U plunk" ]
interval: 10s
timeout: 5s
retries: 5
networks:
- plunk
redis:
image: redis:7-alpine
container_name: plunk-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
healthcheck:
test: [ "CMD", "redis-cli", "ping" ]
interval: 10s
timeout: 5s
retries: 5
networks:
- plunk
minio: minio:
image: minio/minio:latest image: minio/minio:latest
container_name: plunk-minio container_name: plunk-minio
@@ -70,10 +37,10 @@ services:
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-plunkminiopass} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-plunkminiopass}
volumes: volumes:
- minio_data:/data - minio_data:/data
ports: # ports:
# Expose Minio API (for S3 operations) and Console (for web UI) # # Expose Minio API (for S3 operations) and Console (for web UI)
- "${MINIO_API_PORT:-9000}:9000" # - "${MINIO_API_PORT:-9000}:9000"
- "${MINIO_CONSOLE_PORT:-9001}:9001" # - "${MINIO_CONSOLE_PORT:-9001}:9001"
healthcheck: healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ] test: [ "CMD", "curl", "-f", "http://localhost:9000/minio/health/live" ]
interval: 30s interval: 30s
@@ -88,13 +55,13 @@ services:
restart: unless-stopped restart: unless-stopped
command: serve command: serve
environment: environment:
- TZ=UTC - TZ=CST
volumes: volumes:
- ntfy_cache:/var/cache/ntfy - ntfy_cache:/var/cache/ntfy
- ntfy_etc:/etc/ntfy - ntfy_etc:/etc/ntfy
ports: # ports:
# Expose ntfy web UI and API # # Expose ntfy web UI and API
- "${NTFY_PORT:-8080}:80" # - "${NTFY_PORT:-8080}:80"
healthcheck: healthcheck:
test: [ "CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1" ] test: [ "CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1" ]
interval: 30s interval: 30s
@@ -118,11 +85,11 @@ services:
NODE_ENV: production NODE_ENV: production
# Database # Database
DATABASE_URL: postgresql://plunk:${DB_PASSWORD:-changeme123}@postgres:5432/plunk DATABASE_URL: ${DATABASE_URL}
DIRECT_DATABASE_URL: postgresql://plunk:${DB_PASSWORD:-changeme123}@postgres:5432/plunk DIRECT_DATABASE_URL: ${DIRECT_DATABASE_URL}
# Redis # Redis
REDIS_URL: redis://redis:6379 REDIS_URL: ${REDIS_URL}
# Security # Security
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
@@ -214,19 +181,15 @@ services:
ports: ports:
# SMTP ports (for email relay) # SMTP ports (for email relay)
- "${PORT_SECURE:-465}:465" # SMTPS (implicit TLS) # - "${PORT_SECURE:-465}:465" # SMTPS (implicit TLS)
- "${PORT_SUBMISSION:-587}:587" # SMTP Submission (STARTTLS) # - "${PORT_SUBMISSION:-587}:587" # SMTP Submission (STARTTLS)
# Optional: Expose individual service ports for debugging # Optional: Expose individual service ports for debugging
# - "8080:8080" # API - "6000:8080" # API
# - "3000:3000" # Web - "6001:3000" # Web
# - "4000:4000" # Landing # - "6002:4000" # Landing
# - "1000:1000" # Wiki # - "6003:1000" # Wiki
depends_on: depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio: minio:
condition: service_healthy condition: service_healthy
ntfy: ntfy:
@@ -244,10 +207,6 @@ services:
- plunk - plunk
volumes: volumes:
postgres_data:
driver: local
redis_data:
driver: local
minio_data: minio_data:
driver: local driver: local
plunk_data: plunk_data: