aded k6 and script and updated code with better logic and indexing on schema

This commit is contained in:
krizam
2026-10-22 07:50:26 +05:30
parent 4d937b92da
commit 36a7345d26
17 changed files with 1386 additions and 176 deletions
+1
View File
@@ -1,2 +1,3 @@
.env .env
node_modules node_modules
dist
+25
View File
@@ -0,0 +1,25 @@
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY package.json ./
RUN npm install
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM node:22-bookworm-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY package.json ./
RUN npm install --omit=dev
COPY --from=build /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]
+68
View File
@@ -0,0 +1,68 @@
services:
mongo:
image: mongo:7.0.41
container_name: node-ass1-mongo
restart: unless-stopped
command: ["mongod", "--bind_ip_all"]
environment:
MONGO_INITDB_DATABASE: node_ass1
volumes:
- mongo_data:/data/db
networks:
- testnet
mem_limit: 2g
cpus: 1.0
healthcheck:
test:
[
"CMD",
"mongosh",
"--quiet",
"mongodb://127.0.0.1:27017/node_ass1",
"--eval",
"db.runCommand({ ping: 1 }).ok"
]
interval: 5s
timeout: 3s
retries: 15
app:
build:
context: .
dockerfile: Dockerfile
container_name: node-ass1-app
init: true
environment:
NODE_ENV: production
PORT: 3000
MONGO_URI: mongodb://mongo:27017/node_ass1
ports:
- "127.0.0.1:3000:3000"
depends_on:
mongo:
condition: service_healthy
networks:
- testnet
mem_limit: 1g
cpus: 1.5
k6:
image: grafana/k6:latest
container_name: node-ass1-k6
profiles: ["testing"]
working_dir: /scripts
volumes:
- ./k6:/scripts:ro
- ./results:/results
networks:
- testnet
mem_limit: 2g
cpus: 1.0
entrypoint: ["k6"]
networks:
testnet:
driver: bridge
volumes:
mongo_data:
+49
View File
@@ -0,0 +1,49 @@
import http from 'k6/http';
import { check } from 'k6';
import { Counter } from 'k6/metrics';
const seed = JSON.parse(open('./seed-data.json'));
const BASE_URL = __ENV.BASE_URL || 'http://app:3000';
const successfulBookings = new Counter('successful_bookings');
export const options = {
scenarios: {
concurrent_booking: {
executor: 'per-vu-iterations',
vus: 10,
iterations: 1,
maxDuration: '30s',
},
},
thresholds: {
successful_bookings: ['count==1'],
},
};
export default function () {
const session = seed.sessions[0];
const user = seed.users[__VU - 1];
const response = http.post(
`${BASE_URL}/sessions/${session.id}/book`,
JSON.stringify({ userId: user.id }),
{
headers: { 'Content-Type': 'application/json' },
responseCallback: http.expectedStatuses(200, 400),
}
);
if (response.status === 200) {
successfulBookings.add(1);
}
check(response, {
'booking returned expected status': (r) =>
r.status === 200 || r.status === 400,
});
console.log(
`VU ${__VU}: status=${response.status}, session=${session.id}`
);
}
+50
View File
@@ -0,0 +1,50 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://app:3000';
const seed = JSON.parse(open('./seed-data.json'));
export const options = {
stages: [
{ duration: '15s', target: 1 },
{ duration: '20s', target: 3 },
{ duration: '20s', target: 5 },
{ duration: '10s', target: 0 },
],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
checks: ['rate>0.99'],
},
};
export default function () {
const user = seed.users[(__VU + __ITER) % seed.users.length];
const session =
seed.sessions[(__VU + __ITER) % seed.sessions.length];
const requests = [
['users', `${BASE_URL}/users/`],
['teachers', `${BASE_URL}/teachers/`],
[
'available sessions',
`${BASE_URL}/sessions/available?dateTimestamp=${encodeURIComponent(session.dateTimestamp)}`,
],
[
'user session history',
`${BASE_URL}/sessions/${user.id}/sessions`,
],
];
for (const [name, url] of requests) {
const response = http.get(url);
check(response, {
[`${name}: HTTP 200`]: (r) => r.status === 200,
[`${name}: JSON response`]: (r) =>
r.headers['Content-Type']?.includes('application/json'),
});
}
sleep(1);
}
+127
View File
@@ -0,0 +1,127 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter } from 'k6/metrics';
const BASE_URL = __ENV.BASE_URL || 'http://app:3000';
const seed = JSON.parse(open('./seed-data.json'));
const writeFailures = new Counter('write_failures');
export const options = {
stages: [
{ duration: '20s', target: 50 },
{ duration: '20s', target: 100 },
{ duration: '30s', target: 150 },
{ duration: '30s', target: 200 },
{ duration: '30s', target: 250 },
{ duration: '30s', target: 300 },
{ duration: '30s', target: 350 },
{ duration: '30s', target: 400 },
{ duration: '20s', target: 0 },
],
thresholds: {
http_req_failed: ['rate<0.02'],
http_req_duration: ['p(95)<500'],
write_failures: ['count==0'],
},
};
export default function () {
const unique = `${__VU}-${__ITER}-${Date.now()}`;
const startTime = new Date(Date.now() + 60 * 60 * 1000);
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
// 1. Insert a new user
const userResponse = http.post(
`${BASE_URL}/users/`,
JSON.stringify({
fullName: `Load Test ${unique}`,
email: `load-${unique}@example.invalid`,
phone: `9${String(__VU).padStart(1, '0')}${String(__ITER).padStart(8, '0')}`.slice(0, 10),
}),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /users' } }
);
const userOk = check(userResponse, {
'user inserted (201)': (r) => r.status === 201,
});
if (!userOk) {
writeFailures.add(1);
return;
}
const userId = userResponse.json('result._id');
// 2. Insert a session for the existing seeded teacher
const sessionResponse = http.post(
`${BASE_URL}/sessions/`,
JSON.stringify({
teacherId: seed.teacher.id,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
}),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions' } }
);
const sessionOk = check(sessionResponse, {
'session inserted (201)': (r) => r.status === 201,
});
if (!sessionOk) {
writeFailures.add(1);
return;
}
const sessionId = sessionResponse.json('result._id');
// 3. Book the newly created session
const bookingResponse = http.post(
`${BASE_URL}/sessions/${sessionId}/book`,
JSON.stringify({ userId }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions/:id/book' } }
);
const bookingOk = check(bookingResponse, {
'session booked (200)': (r) => r.status === 200,
});
if (!bookingOk) {
writeFailures.add(1);
return;
}
// 4. Complete the booked session
const completeResponse = http.patch(
`${BASE_URL}/sessions/${sessionId}/complete`,
null,
{ tags: { name: 'PATCH /sessions/:id/complete' } }
);
const completeOk = check(completeResponse, {
'session completed (200)': (r) => r.status === 200,
});
if (!completeOk) {
writeFailures.add(1);
return;
}
// 5. Read user session history
const historyResponse = http.get(
`${BASE_URL}/sessions/${userId}/sessions`,
{ tags: { name: 'GET /sessions/:userId/sessions' } }
);
check(historyResponse, {
'user history returned (200)': (r) => r.status === 200,
});
sleep(1);
}
export function handleSummary(data) {
return {
'/results/endpoint-summary.json': JSON.stringify(data, null, 2),
};
}
+128
View File
@@ -0,0 +1,128 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter } from 'k6/metrics';
const BASE_URL = __ENV.BASE_URL || 'http://app:3000';
const seed = JSON.parse(open('./seed-data.json'));
const writeFailures = new Counter('write_failures');
export const options = {
stages: [
{ duration: '20s', target: 50 },
{ duration: '20s', target: 100 },
{ duration: '30s', target: 150 },
{ duration: '30s', target: 200 },
{ duration: '30s', target: 250 },
{ duration: '30s', target: 300 },
{ duration: '30s', target: 350 },
{ duration: '30s', target: 400 },
{ duration: '20s', target: 0 },
],
thresholds: {
http_req_failed: ['rate<0.02'],
http_req_duration: ['p(95)<500'],
write_failures: ['count==0'],
},
};
export default function () {
const unique = `${__VU}-${__ITER}-${Date.now()}`;
const startTime = new Date(Date.now() + 60 * 60 * 1000);
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
// 1. Insert a new user
const userResponse = http.post(
`${BASE_URL}/users/`,
JSON.stringify({
fullName: `Load Test ${unique}`,
email: `load-${unique}@example.invalid`,
phone: `9${String(__VU).padStart(1, '0')}${String(__ITER).padStart(8, '0')}`.slice(0, 10),
}),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /users' } }
);
const userOk = check(userResponse, {
'user inserted (201)': (r) => r.status === 201,
});
if (!userOk) {
writeFailures.add(1);
return;
}
const userId = userResponse.json('result._id');
// 2. Insert a session for the existing seeded teacher
const sessionResponse = http.post(
`${BASE_URL}/sessions/`,
JSON.stringify({
teacherId: seed.teacher.id,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
}),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions' } }
);
const sessionOk = check(sessionResponse, {
'session inserted (201)': (r) => r.status === 201,
});
if (!sessionOk) {
writeFailures.add(1);
return;
}
const sessionId = sessionResponse.json('result._id');
// 3. Book the newly created session
const bookingResponse = http.post(
`${BASE_URL}/sessions/${sessionId}/book`,
JSON.stringify({ userId }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions/:id/book' } }
);
const bookingOk = check(bookingResponse, {
'session booked (200)': (r) => r.status === 200,
});
if (!bookingOk) {
writeFailures.add(1);
return;
}
// 4. Complete the booked session
const completeResponse = http.patch(
`${BASE_URL}/sessions/${sessionId}/complete`,
null,
{ tags: { name: 'PATCH /sessions/:id/complete' } }
);
const completeOk = check(completeResponse, {
'session completed (200)': (r) => r.status === 200,
});
if (!completeOk) {
writeFailures.add(1);
return;
}
// 5. Read user session history
const historyResponse = http.get(
`${BASE_URL}/sessions/${userId}/sessions`,
{ tags: { name: 'GET /sessions/:userId/sessions' } }
);
check(historyResponse, {
'user history returned (200)': (r) => r.status === 200,
});
sleep(1);
}
export function handleSummary(data) {
return {
'/results/endpoint-summary.json': JSON.stringify(data, null, 2),
};
}
@@ -0,0 +1,130 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Counter } from 'k6/metrics';
import { textSummary } from 'https://jslib.k6.io/k6-summary/0.0.1/index.js';
const BASE_URL = __ENV.BASE_URL || 'http://app:3000';
const seed = JSON.parse(open('./seed-data.json'));
const writeFailures = new Counter('write_failures');
export const options = {
stages: [
{ duration: '20s', target: 100 },
{ duration: '20s', target: 200 },
{ duration: '30s', target: 300 },
{ duration: '30s', target: 400 },
{ duration: '30s', target: 450 },
{ duration: '30s', target: 500 },
{ duration: '30s', target: 550 },
{ duration: '30s', target: 600 },
{ duration: '20s', target: 0 },
],
thresholds: {
http_req_failed: ['rate<0.02'],
http_req_duration: ['p(95)<500'],
write_failures: ['count==0'],
},
};
export default function () {
const unique = `${__VU}-${__ITER}-${Date.now()}`;
const startTime = new Date(Date.now() + 60 * 60 * 1000);
const endTime = new Date(startTime.getTime() + 30 * 60 * 1000);
// 1. Insert a new user
const userResponse = http.post(
`${BASE_URL}/users/`,
JSON.stringify({
fullName: `Load Test ${unique}`,
email: `load-${unique}@example.invalid`,
phone: `9${String(__VU).padStart(1, '0')}${String(__ITER).padStart(8, '0')}`.slice(0, 10),
}),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /users' } }
);
const userOk = check(userResponse, {
'user inserted (201)': (r) => r.status === 201,
});
if (!userOk) {
writeFailures.add(1);
return;
}
const userId = userResponse.json('result._id');
// 2. Insert a session for the existing seeded teacher
const sessionResponse = http.post(
`${BASE_URL}/sessions/`,
JSON.stringify({
teacherId: seed.teacher.id,
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
}),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions' } }
);
const sessionOk = check(sessionResponse, {
'session inserted (201)': (r) => r.status === 201,
});
if (!sessionOk) {
writeFailures.add(1);
return;
}
const sessionId = sessionResponse.json('result._id');
// 3. Book the newly created session
const bookingResponse = http.post(
`${BASE_URL}/sessions/${sessionId}/book`,
JSON.stringify({ userId }),
{ headers: { 'Content-Type': 'application/json' }, tags: { name: 'POST /sessions/:id/book' } }
);
const bookingOk = check(bookingResponse, {
'session booked (200)': (r) => r.status === 200,
});
if (!bookingOk) {
writeFailures.add(1);
return;
}
// 4. Complete the booked session
const completeResponse = http.patch(
`${BASE_URL}/sessions/${sessionId}/complete`,
null,
{ tags: { name: 'PATCH /sessions/:id/complete' } }
);
const completeOk = check(completeResponse, {
'session completed (200)': (r) => r.status === 200,
});
if (!completeOk) {
writeFailures.add(1);
return;
}
// 5. Read user session history
const historyResponse = http.get(
`${BASE_URL}/sessions/${userId}/sessions`,
{ tags: { name: 'GET /sessions/:userId/sessions' } }
);
check(historyResponse, {
'user history returned (200)': (r) => r.status === 200,
});
sleep(1);
}
export function handleSummary(data) {
return {
stdout: textSummary(data, { indent: ' ', enableColors: true }),
'/results/endpoint-summary.json': JSON.stringify(data, null, 2),
};
}
+392
View File
@@ -0,0 +1,392 @@
{
"generatedAt": "2026-10-21T18:53:24.974Z",
"runId": "1792608804974",
"teacher": {
"id": "6ad90a253d51b696cdc51357",
"email": "teacher-1792608804974@example.invalid"
},
"users": [
{
"id": "6ad90a253d51b696cdc51358",
"email": "user-1792608804974-1@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51359",
"email": "user-1792608804974-2@example.invalid"
},
{
"id": "6ad90a253d51b696cdc5135a",
"email": "user-1792608804974-3@example.invalid"
},
{
"id": "6ad90a253d51b696cdc5135b",
"email": "user-1792608804974-4@example.invalid"
},
{
"id": "6ad90a253d51b696cdc5135c",
"email": "user-1792608804974-5@example.invalid"
},
{
"id": "6ad90a253d51b696cdc5135d",
"email": "user-1792608804974-6@example.invalid"
},
{
"id": "6ad90a253d51b696cdc5135e",
"email": "user-1792608804974-7@example.invalid"
},
{
"id": "6ad90a253d51b696cdc5135f",
"email": "user-1792608804974-8@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51360",
"email": "user-1792608804974-9@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51361",
"email": "user-1792608804974-10@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51362",
"email": "user-1792608804974-11@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51363",
"email": "user-1792608804974-12@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51364",
"email": "user-1792608804974-13@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51365",
"email": "user-1792608804974-14@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51366",
"email": "user-1792608804974-15@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51367",
"email": "user-1792608804974-16@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51368",
"email": "user-1792608804974-17@example.invalid"
},
{
"id": "6ad90a253d51b696cdc51369",
"email": "user-1792608804974-18@example.invalid"
},
{
"id": "6ad90a253d51b696cdc5136a",
"email": "user-1792608804974-19@example.invalid"
},
{
"id": "6ad90a253d51b696cdc5136b",
"email": "user-1792608804974-20@example.invalid"
}
],
"sessions": [
{
"id": "6ad90a253d51b696cdc5136c",
"startTime": "2026-10-22T18:53:25.235Z",
"endTime": "2026-10-22T19:23:25.235Z",
"dateTimestamp": "2026-10-22T18:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5136d",
"startTime": "2026-10-22T19:53:25.235Z",
"endTime": "2026-10-22T20:23:25.235Z",
"dateTimestamp": "2026-10-22T19:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5136e",
"startTime": "2026-10-22T20:53:25.235Z",
"endTime": "2026-10-22T21:23:25.235Z",
"dateTimestamp": "2026-10-22T20:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5136f",
"startTime": "2026-10-22T21:53:25.235Z",
"endTime": "2026-10-22T22:23:25.235Z",
"dateTimestamp": "2026-10-22T21:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51370",
"startTime": "2026-10-22T22:53:25.235Z",
"endTime": "2026-10-22T23:23:25.235Z",
"dateTimestamp": "2026-10-22T22:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51371",
"startTime": "2026-10-22T23:53:25.235Z",
"endTime": "2026-10-23T00:23:25.235Z",
"dateTimestamp": "2026-10-22T23:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51372",
"startTime": "2026-10-23T00:53:25.235Z",
"endTime": "2026-10-23T01:23:25.235Z",
"dateTimestamp": "2026-10-23T00:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51373",
"startTime": "2026-10-23T01:53:25.235Z",
"endTime": "2026-10-23T02:23:25.235Z",
"dateTimestamp": "2026-10-23T01:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51374",
"startTime": "2026-10-23T02:53:25.235Z",
"endTime": "2026-10-23T03:23:25.235Z",
"dateTimestamp": "2026-10-23T02:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51375",
"startTime": "2026-10-23T03:53:25.235Z",
"endTime": "2026-10-23T04:23:25.235Z",
"dateTimestamp": "2026-10-23T03:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51376",
"startTime": "2026-10-23T04:53:25.235Z",
"endTime": "2026-10-23T05:23:25.235Z",
"dateTimestamp": "2026-10-23T04:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51377",
"startTime": "2026-10-23T05:53:25.235Z",
"endTime": "2026-10-23T06:23:25.235Z",
"dateTimestamp": "2026-10-23T05:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51378",
"startTime": "2026-10-23T06:53:25.235Z",
"endTime": "2026-10-23T07:23:25.235Z",
"dateTimestamp": "2026-10-23T06:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51379",
"startTime": "2026-10-23T07:53:25.235Z",
"endTime": "2026-10-23T08:23:25.235Z",
"dateTimestamp": "2026-10-23T07:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5137a",
"startTime": "2026-10-23T08:53:25.235Z",
"endTime": "2026-10-23T09:23:25.235Z",
"dateTimestamp": "2026-10-23T08:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5137b",
"startTime": "2026-10-23T09:53:25.235Z",
"endTime": "2026-10-23T10:23:25.235Z",
"dateTimestamp": "2026-10-23T09:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5137c",
"startTime": "2026-10-23T10:53:25.235Z",
"endTime": "2026-10-23T11:23:25.235Z",
"dateTimestamp": "2026-10-23T10:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5137d",
"startTime": "2026-10-23T11:53:25.235Z",
"endTime": "2026-10-23T12:23:25.235Z",
"dateTimestamp": "2026-10-23T11:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5137e",
"startTime": "2026-10-23T12:53:25.235Z",
"endTime": "2026-10-23T13:23:25.235Z",
"dateTimestamp": "2026-10-23T12:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5137f",
"startTime": "2026-10-23T13:53:25.235Z",
"endTime": "2026-10-23T14:23:25.235Z",
"dateTimestamp": "2026-10-23T13:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51380",
"startTime": "2026-10-23T14:53:25.235Z",
"endTime": "2026-10-23T15:23:25.235Z",
"dateTimestamp": "2026-10-23T14:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51381",
"startTime": "2026-10-23T15:53:25.235Z",
"endTime": "2026-10-23T16:23:25.235Z",
"dateTimestamp": "2026-10-23T15:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51382",
"startTime": "2026-10-23T16:53:25.235Z",
"endTime": "2026-10-23T17:23:25.235Z",
"dateTimestamp": "2026-10-23T16:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51383",
"startTime": "2026-10-23T17:53:25.235Z",
"endTime": "2026-10-23T18:23:25.235Z",
"dateTimestamp": "2026-10-23T17:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51384",
"startTime": "2026-10-23T18:53:25.235Z",
"endTime": "2026-10-23T19:23:25.235Z",
"dateTimestamp": "2026-10-23T18:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51385",
"startTime": "2026-10-23T19:53:25.235Z",
"endTime": "2026-10-23T20:23:25.235Z",
"dateTimestamp": "2026-10-23T19:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51386",
"startTime": "2026-10-23T20:53:25.235Z",
"endTime": "2026-10-23T21:23:25.235Z",
"dateTimestamp": "2026-10-23T20:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51387",
"startTime": "2026-10-23T21:53:25.235Z",
"endTime": "2026-10-23T22:23:25.235Z",
"dateTimestamp": "2026-10-23T21:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51388",
"startTime": "2026-10-23T22:53:25.235Z",
"endTime": "2026-10-23T23:23:25.235Z",
"dateTimestamp": "2026-10-23T22:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51389",
"startTime": "2026-10-23T23:53:25.235Z",
"endTime": "2026-10-24T00:23:25.235Z",
"dateTimestamp": "2026-10-23T23:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5138a",
"startTime": "2026-10-24T00:53:25.235Z",
"endTime": "2026-10-24T01:23:25.235Z",
"dateTimestamp": "2026-10-24T00:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5138b",
"startTime": "2026-10-24T01:53:25.235Z",
"endTime": "2026-10-24T02:23:25.235Z",
"dateTimestamp": "2026-10-24T01:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5138c",
"startTime": "2026-10-24T02:53:25.235Z",
"endTime": "2026-10-24T03:23:25.235Z",
"dateTimestamp": "2026-10-24T02:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5138d",
"startTime": "2026-10-24T03:53:25.235Z",
"endTime": "2026-10-24T04:23:25.235Z",
"dateTimestamp": "2026-10-24T03:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5138e",
"startTime": "2026-10-24T04:53:25.235Z",
"endTime": "2026-10-24T05:23:25.235Z",
"dateTimestamp": "2026-10-24T04:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5138f",
"startTime": "2026-10-24T05:53:25.235Z",
"endTime": "2026-10-24T06:23:25.235Z",
"dateTimestamp": "2026-10-24T05:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51390",
"startTime": "2026-10-24T06:53:25.235Z",
"endTime": "2026-10-24T07:23:25.235Z",
"dateTimestamp": "2026-10-24T06:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51391",
"startTime": "2026-10-24T07:53:25.235Z",
"endTime": "2026-10-24T08:23:25.235Z",
"dateTimestamp": "2026-10-24T07:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51392",
"startTime": "2026-10-24T08:53:25.235Z",
"endTime": "2026-10-24T09:23:25.235Z",
"dateTimestamp": "2026-10-24T08:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51393",
"startTime": "2026-10-24T09:53:25.235Z",
"endTime": "2026-10-24T10:23:25.235Z",
"dateTimestamp": "2026-10-24T09:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51394",
"startTime": "2026-10-24T10:53:25.235Z",
"endTime": "2026-10-24T11:23:25.235Z",
"dateTimestamp": "2026-10-24T10:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51395",
"startTime": "2026-10-24T11:53:25.235Z",
"endTime": "2026-10-24T12:23:25.235Z",
"dateTimestamp": "2026-10-24T11:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51396",
"startTime": "2026-10-24T12:53:25.235Z",
"endTime": "2026-10-24T13:23:25.235Z",
"dateTimestamp": "2026-10-24T12:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51397",
"startTime": "2026-10-24T13:53:25.235Z",
"endTime": "2026-10-24T14:23:25.235Z",
"dateTimestamp": "2026-10-24T13:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51398",
"startTime": "2026-10-24T14:53:25.235Z",
"endTime": "2026-10-24T15:23:25.235Z",
"dateTimestamp": "2026-10-24T14:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc51399",
"startTime": "2026-10-24T15:53:25.235Z",
"endTime": "2026-10-24T16:23:25.235Z",
"dateTimestamp": "2026-10-24T15:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5139a",
"startTime": "2026-10-24T16:53:25.235Z",
"endTime": "2026-10-24T17:23:25.235Z",
"dateTimestamp": "2026-10-24T16:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5139b",
"startTime": "2026-10-24T17:53:25.235Z",
"endTime": "2026-10-24T18:23:25.235Z",
"dateTimestamp": "2026-10-24T17:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5139c",
"startTime": "2026-10-24T18:53:25.235Z",
"endTime": "2026-10-24T19:23:25.235Z",
"dateTimestamp": "2026-10-24T18:53:25.235Z"
},
{
"id": "6ad90a253d51b696cdc5139d",
"startTime": "2026-10-24T19:53:25.235Z",
"endTime": "2026-10-24T20:23:25.235Z",
"dateTimestamp": "2026-10-24T19:53:25.235Z"
}
]
}
+23
View File
@@ -0,0 +1,23 @@
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 1,
iterations: 10,
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
},
};
export default function () {
const response = http.get('http://app:3000/users/');
check(response, {
'status is 200': (r) => r.status === 200,
'response is JSON': (r) =>
r.headers['Content-Type']?.includes('application/json'),
});
sleep(1);
}
+37
View File
@@ -0,0 +1,37 @@
import http from 'k6/http';
import { check } from 'k6';
const BASE_URL = __ENV.BASE_URL || 'http://app:3000';
export const options = {
stages: [
{ duration: '20s', target: 5 },
{ duration: '20s', target: 10 },
{ duration: '20s', target: 20 },
{ duration: '20s', target: 30 },
{ duration: '20s', target: 40 },
{ duration: '20s', target: 50 },
{ duration: '15s', target: 0 },
],
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<500'],
checks: ['rate>0.99'],
},
};
export default function () {
const endpoints = [
'/users/',
'/teachers/',
'/sessions/available?dateTimestamp=2026-10-25T10%3A00%3A00.000Z',
];
for (const endpoint of endpoints) {
const response = http.get(`${BASE_URL}${endpoint}`);
check(response, {
[`${endpoint}: status 200`]: (r) => r.status === 200,
});
}
}
+126
View File
@@ -0,0 +1,126 @@
import { writeFile } from 'node:fs/promises';
const BASE_URL = process.env.API_BASE_URL || 'http://localhost:3000';
const USER_COUNT = Number(process.env.SEED_USERS || 20);
const SESSION_COUNT = Number(process.env.SEED_SESSIONS || 50);
const runId = `${Date.now()}`;
const generated = {
generatedAt: new Date().toISOString(),
runId,
teacher: null,
users: [],
sessions: [],
};
async function request(path, method = 'GET', body) {
const response = await fetch(`${BASE_URL}${path}`, {
method,
headers: { 'Content-Type': 'application/json' },
...(body ? { body: JSON.stringify(body) } : {}),
});
const text = await response.text();
let data;
try {
data = JSON.parse(text);
} catch {
throw new Error(`${method} ${path}: Invalid JSON response: ${text}`);
}
if (!response.ok) {
throw new Error(
`${method} ${path} failed (${response.status}): ${JSON.stringify(data)}`
);
}
return data;
}
function entityId(data) {
const entity = data.result ?? data.data ?? data;
const id = entity._id ?? entity.id;
if (!id) {
throw new Error(`Could not find created record ID: ${JSON.stringify(data)}`);
}
return id;
}
async function main() {
console.log(`Seeding ${BASE_URL}`);
const teacherResponse = await request('/teachers/', 'POST', {
fullName: `Load Test Teacher ${runId}`,
email: `teacher-${runId}@example.invalid`,
specialization: 'Mathematics',
experience: '5 years',
});
generated.teacher = {
id: entityId(teacherResponse),
email: `teacher-${runId}@example.invalid`,
};
for (let i = 1; i <= USER_COUNT; i++) {
const email = `user-${runId}-${i}@example.invalid`;
const response = await request('/users/', 'POST', {
fullName: `Load Test User ${i}`,
email,
phone: String(9000000000 + i),
});
generated.users.push({
id: entityId(response),
email,
});
if (i % 10 === 0 || i === USER_COUNT) {
console.log(`Created ${i}/${USER_COUNT} users`);
}
}
// Start tomorrow and spread sessions one hour apart.
const firstStart = Date.now() + 24 * 60 * 60 * 1000;
for (let i = 0; i < SESSION_COUNT; i++) {
const start = new Date(firstStart + i * 60 * 60 * 1000);
const end = new Date(start.getTime() + 30 * 60 * 1000);
const response = await request('/sessions/', 'POST', {
teacherId: generated.teacher.id,
startTime: start.toISOString(),
endTime: end.toISOString(),
});
generated.sessions.push({
id: entityId(response),
startTime: start.toISOString(),
endTime: end.toISOString(),
dateTimestamp: start.toISOString(),
});
if ((i + 1) % 10 === 0 || i + 1 === SESSION_COUNT) {
console.log(`Created ${i + 1}/${SESSION_COUNT} sessions`);
}
}
await writeFile(
new URL('../k6/seed-data.json', import.meta.url),
JSON.stringify(generated, null, 2) + '\n'
);
console.log('\nSeeding completed successfully.');
console.log(`Teacher: 1`);
console.log(`Users: ${generated.users.length}`);
console.log(`Sessions: ${generated.sessions.length}`);
console.log('Saved IDs to k6/seed-data.json');
}
main().catch((error) => {
console.error('\nSeeding failed:', error.message);
process.exitCode = 1;
});
+3
View File
@@ -27,5 +27,8 @@ const SessionsSchema = new Schema({
} }
}, { timestamps: true }); }, { timestamps: true });
SessionsSchema.index({ userId: 1 });
SessionsSchema.index({ status: 1, startTime: 1 });
const Sessions = mongoose.model('sessions', SessionsSchema); const Sessions = mongoose.model('sessions', SessionsSchema);
export default Sessions; export default Sessions;
+60 -20
View File
@@ -58,38 +58,78 @@ SessionsRoutes.get('/available', async(req: any, res) => {
// Book Available Sessions // Book Available Sessions
SessionsRoutes.post('/:id/book', async(req: any, res) => { // SessionsRoutes.post('/:id/book', async(req: any, res) => {
//
// const {id} = req.params;
// const {userId} = req.body;
//
// const existingSession = await Sessions.findOne({_id: id});
//
// if(!existingSession) {
// return res.status(400).send({message: 'Session does not exist.'})
// }
// if(existingSession.status !== 'AVAILABLE') {
// return res.status(400).send({message: 'This session is not Available'});
// }
//
// const result = await Sessions.findByIdAndUpdate(
// id,
// { status: "BOOKED", userId: userId },
// { new: true }
// );
//
// // console.log('re-------', result)
//
// if(result?.status === 'BOOKED') {
// return res.status(200).send({message: 'Session Booked', result})
// } else {
// return res.status(400).send({message: 'Error during Booking session.'})
// }
//
//
//
// })
// Book Available Sessions atomically
SessionsRoutes.post('/:id/book', async (req: any, res) => {
const { id } = req.params; const { id } = req.params;
const { userId } = req.body; const { userId } = req.body;
const existingSession = await Sessions.findOne({_id: id}); // Atomically book only if the session is still AVAILABLE
const result = await Sessions.findOneAndUpdate(
if(!existingSession) { { _id: id, status: 'AVAILABLE' },
return res.status(400).send({message: 'Session does not exist.'}) {
$set: {
status: 'BOOKED',
userId: userId
} }
if(existingSession.status !== 'AVAILABLE') { },
return res.status(400).send({message: 'This session is not Available'}); {
new: true,
runValidators: true
} }
const result = await Sessions.findByIdAndUpdate(
id,
{ status: "BOOKED", userId: userId },
{ new: true }
); );
// console.log('re-------', result) if (result) {
return res.status(200).send({
if(result?.status === 'BOOKED') { message: 'Session Booked',
return res.status(200).send({message: 'Session Booked', result}) result
} else { });
return res.status(400).send({message: 'Error during Booking session.'})
} }
// Determine why the booking failed
const existingSession = await Sessions.findById(id);
if (!existingSession) {
return res.status(400).send({
message: 'Session does not exist.'
});
}
}) return res.status(400).send({
message: 'This session is not Available'
});
});
// Update Available Sessions To Complete : User Id also needed to update // Update Available Sessions To Complete : User Id also needed to update
SessionsRoutes.patch('/:id/complete', async (req: any, res) => { SessionsRoutes.patch('/:id/complete', async (req: any, res) => {
+32 -21
View File
@@ -1,34 +1,45 @@
import express from 'express'; import express from 'express';
import Users from './Schema/users.schema.js' import Users from './Schema/users.schema.js';
import mongoose from 'mongoose';
const UsersRoutes = express.Router(); const UsersRoutes = express.Router();
// Get All Users // Get all users
UsersRoutes.get('/', async(req, res) => { UsersRoutes.get('/', async (_req, res, next) => {
try {
const users = await Users.find(); const users = await Users.find();
return res.status(200).send({'messesage': 'success', data: {users}}) return res.status(200).send({
}) message: 'success',
data: { users }
});
} catch (error) {
next(error);
}
});
// Add New User // Add a new user
UsersRoutes.post('/', async(req, res) => { UsersRoutes.post('/', async (req, res, next) => {
try {
const { fullName, email, phone } = req.body; const { fullName, email, phone } = req.body;
//Check for email uniqeness const result = await Users.create({ fullName, email, phone });
const existingUser = await Users.find({email});
console.log('existingUser', existingUser) return res.status(201).send({
if(existingUser.length){ message: 'User added successfully',
return res.status(400).send({message: 'User already exist with this email. Please Use Correct Email Address.'}) result
});
} catch (error: unknown) {
if (
error instanceof mongoose.mongo.MongoServerError &&
error.code === 11000
) {
return res.status(400).send({
message: 'A user already exists with this email.'
});
} }
const newTeacher = new Users({fullName, email, phone }); next(error);
const result = await newTeacher.save();
if(result) {
return res.status(201).send({message: 'Teacher Added Successfully', result})
} }
});
})
export default UsersRoutes; export default UsersRoutes;
+2 -2
View File
@@ -2,8 +2,8 @@
// Visit https://aka.ms/tsconfig to read more about this file // Visit https://aka.ms/tsconfig to read more about this file
"compilerOptions": { "compilerOptions": {
// File Layout // File Layout
// "rootDir": "./src", "rootDir": "./src",
// "outDir": "./dist", "outDir": "./dist",
// Environment Settings // Environment Settings
// See also https://aka.ms/tsconfig/module // See also https://aka.ms/tsconfig/module