Skip to content

Commit 8b2c0fe

Browse files
authored
Merge pull request #948 from Emelie-Dev/test/815-stream-post-negative-cases
Add stream POST negative tests
2 parents c67cfcb + 1abe705 commit 8b2c0fe

7 files changed

Lines changed: 1350 additions & 545 deletions

File tree

backend/Dockerfile

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ COPY package*.json ./
66
RUN npm install
77

88
COPY tsconfig.json ./
9+
COPY prisma.config.ts ./
910
COPY src ./src
1011
COPY prisma ./prisma
1112

@@ -23,10 +24,6 @@ RUN npm install --omit=dev
2324
COPY --from=builder /app/dist ./dist
2425
COPY --from=builder /app/src/generated ./dist/generated
2526
COPY --from=builder /app/prisma ./prisma
26-
# Prisma 7 reads the schema location and datasource url from prisma.config.ts,
27-
# and the schema's `datasource db {}` block has no inline url. The CI health
28-
# check runs `prisma db push` inside this image, so the config must be present
29-
# too (dotenv is a runtime dependency, so the config loads).
3027
COPY prisma.config.ts ./
3128

3229
EXPOSE 3001

backend/src/app.ts

Lines changed: 94 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,33 @@
1-
import express, { type Request, type Response, type NextFunction } from 'express';
2-
import cors from 'cors';
3-
import swaggerUi from 'swagger-ui-express';
4-
import { swaggerSpec } from './config/swagger.js';
5-
import { apiVersionMiddleware, type VersionedRequest } from './middleware/api-version.middleware.js';
6-
import { sandboxMiddleware } from './middleware/sandbox.middleware.js';
7-
import { globalRateLimiter } from './middleware/rate-limiter.middleware.js';
8-
import { requestIdMiddleware } from './middleware/requestId.js';
9-
import v1Routes from './routes/v1/index.js';
10-
11-
import healthRoutes from './routes/health.routes.js';
1+
import express, {
2+
type Request,
3+
type Response,
4+
type NextFunction,
5+
} from "express";
6+
import cors from "cors";
7+
import swaggerUi from "swagger-ui-express";
8+
import { swaggerSpec } from "./config/swagger.js";
9+
import {
10+
apiVersionMiddleware,
11+
type VersionedRequest,
12+
} from "./middleware/api-version.middleware.js";
13+
import { sandboxMiddleware } from "./middleware/sandbox.middleware.js";
14+
import { globalRateLimiter } from "./middleware/rate-limiter.middleware.js";
15+
import { requestIdMiddleware } from "./middleware/requestId.js";
16+
import v1Routes from "./routes/v1/index.js";
17+
18+
import healthRoutes from "./routes/health.routes.js";
1219

1320
const app = express();
14-
const isProduction = process.env.NODE_ENV === 'production';
15-
const rawCors = process.env.CORS_ALLOWED_ORIGINS ?? '';
21+
const isProduction = process.env.NODE_ENV === "production";
22+
const rawCors = process.env.CORS_ALLOWED_ORIGINS ?? "";
1623
const allowedOrigins = rawCors
17-
.split(',')
18-
.map((origin) => origin.trim())
19-
.filter(Boolean);
24+
.split(",")
25+
.map((origin) => origin.trim())
26+
.filter(Boolean);
2027

2128
// Default in development to only localhost:3000 (frontend dev server)
2229
if (!process.env.CORS_ALLOWED_ORIGINS && !isProduction) {
23-
allowedOrigins.push('http://localhost:3000');
30+
allowedOrigins.push("http://localhost:3000");
2431
}
2532

2633
// Apply global rate limiter first
@@ -29,72 +36,88 @@ app.use(globalRateLimiter);
2936
// Request ID tracing
3037
app.use(requestIdMiddleware);
3138

32-
app.disable('x-powered-by');
39+
app.disable("x-powered-by");
3340

3441
// Helmet-equivalent core headers without external dependency.
3542
// Strict CSP applied globally; the /api-docs route overrides it below for Swagger UI.
3643
app.use((req: Request, res: Response, next: NextFunction) => {
37-
res.setHeader('X-Content-Type-Options', 'nosniff');
38-
res.setHeader('X-Frame-Options', 'DENY');
39-
res.setHeader('Referrer-Policy', 'no-referrer');
40-
res.setHeader('X-DNS-Prefetch-Control', 'off');
41-
res.setHeader('X-Download-Options', 'noopen');
42-
res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
43-
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'");
44-
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
45-
res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');
46-
if (process.env.NODE_ENV === 'production') {
47-
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
48-
}
49-
next();
44+
res.setHeader("X-Content-Type-Options", "nosniff");
45+
res.setHeader("X-Frame-Options", "DENY");
46+
res.setHeader("Referrer-Policy", "no-referrer");
47+
res.setHeader("X-DNS-Prefetch-Control", "off");
48+
res.setHeader("X-Download-Options", "noopen");
49+
res.setHeader("X-Permitted-Cross-Domain-Policies", "none");
50+
res.setHeader(
51+
"Content-Security-Policy",
52+
"default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'",
53+
);
54+
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
55+
res.setHeader("Cross-Origin-Resource-Policy", "same-origin");
56+
if (process.env.NODE_ENV === "production") {
57+
res.setHeader(
58+
"Strict-Transport-Security",
59+
"max-age=31536000; includeSubDomains",
60+
);
61+
}
62+
next();
5063
});
5164

52-
app.use(cors({
65+
app.use(
66+
cors({
5367
origin(origin, callback) {
54-
// Allow non-browser clients (no Origin header)
55-
if (!origin) {
56-
callback(null, true);
57-
return;
58-
}
59-
60-
if (allowedOrigins.includes(origin)) {
61-
callback(null, true);
62-
return;
63-
}
64-
65-
// Not allowed
66-
callback(new Error('CORS origin not allowed'));
68+
// Allow non-browser clients (no Origin header)
69+
if (!origin) {
70+
callback(null, true);
71+
return;
72+
}
73+
74+
if (allowedOrigins.includes(origin)) {
75+
callback(null, true);
76+
return;
77+
}
78+
79+
// Not allowed
80+
callback(new Error("CORS origin not allowed"));
6781
},
6882
credentials: true,
69-
}));
83+
}),
84+
);
7085

7186
// Convert CORS errors into 403 responses so callers get a clear status code
7287
app.use((err: unknown, req: Request, res: Response, next: NextFunction) => {
73-
if (err instanceof Error && err.message === 'CORS origin not allowed') {
74-
res.status(403).json({ error: 'CORS origin not allowed' });
75-
return;
76-
}
77-
next(err);
88+
if (err instanceof Error && err.message === "CORS origin not allowed") {
89+
res.status(403).json({ error: "CORS origin not allowed" });
90+
return;
91+
}
92+
next(err);
7893
});
79-
app.use(express.json({ limit: '1mb' }));
94+
app.use(express.json({ limit: "1mb" }));
8095

8196
// Sandbox mode detection (before versioning)
8297
app.use(sandboxMiddleware);
8398

8499
// Swagger UI setup
85100
// Override CSP for /api-docs only: Swagger UI requires inline scripts/styles.
86-
app.use('/api-docs', (req: Request, res: Response, next: NextFunction) => {
87-
res.setHeader('Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'");
101+
app.use(
102+
"/api-docs",
103+
(req: Request, res: Response, next: NextFunction) => {
104+
res.setHeader(
105+
"Content-Security-Policy",
106+
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'; object-src 'none'",
107+
);
88108
next();
89-
}, swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
90-
customCss: '.swagger-ui .topbar { display: none }',
91-
customSiteTitle: 'FlowFi API Documentation',
92-
}));
109+
},
110+
swaggerUi.serve,
111+
swaggerUi.setup(swaggerSpec, {
112+
customCss: ".swagger-ui .topbar { display: none }",
113+
customSiteTitle: "FlowFi API Documentation",
114+
}),
115+
);
93116

94117
// Serve raw OpenAPI spec as JSON
95-
app.get('/api-docs.json', (req: Request, res: Response) => {
96-
res.setHeader('Content-Type', 'application/json');
97-
res.send(swaggerSpec);
118+
app.get("/api-docs.json", (req: Request, res: Response) => {
119+
res.setHeader("Content-Type", "application/json");
120+
res.send(swaggerSpec);
98121
});
99122

100123
// API Versioning
@@ -105,16 +128,16 @@ app.use(apiVersionMiddleware);
105128
// After versioning middleware, /v1/streams becomes /streams, so we mount v1Routes at root
106129
// But only handle requests that had a version prefix (apiVersion is set)
107130
app.use((req: Request, res: Response, next: NextFunction) => {
108-
const versionedReq = req as VersionedRequest;
109-
if (versionedReq.apiVersion) {
110-
// This was a versioned request, route to v1 handlers
111-
return v1Routes(req, res, next);
112-
}
113-
return next(); // Not versioned, continue to deprecated handlers
131+
const versionedReq = req as VersionedRequest;
132+
if (versionedReq.apiVersion) {
133+
// This was a versioned request, route to v1 handlers
134+
return v1Routes(req, res, next);
135+
}
136+
return next(); // Not versioned, continue to deprecated handlers
114137
});
115138

116139
// Health check routes
117-
app.use('/health', healthRoutes);
140+
app.use("/health", healthRoutes);
118141

119142
/**
120143
* @openapi
@@ -128,11 +151,11 @@ app.use('/health', healthRoutes);
128151
* 200:
129152
* description: API is running successfully
130153
*/
131-
app.get('/', (req: Request, res: Response) => {
132-
res.send('FlowFi Backend is running');
154+
app.get("/", (req: Request, res: Response) => {
155+
res.send("FlowFi Backend is running");
133156
});
134157

135-
import { errorHandler } from './middleware/error.middleware.js';
158+
import { errorHandler } from "./middleware/error.middleware.js";
136159

137160
app.use(errorHandler);
138161

0 commit comments

Comments
 (0)