Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2700e62935 | ||
|
|
517b93cc95 | ||
|
|
8e3fb9595d | ||
|
|
58be4abc31 | ||
|
|
480f0c1034 |
@@ -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
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user