From 0fd162e9e98b02ca6abd38949f82e0704840c78a Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Thu, 30 Apr 2026 15:06:37 +0000
Subject: [PATCH 1/2] Add Grafana K6 performance test scripts for all 5
microservices
- Identity, Customer, Order, Product, Notification service tests
- Shared config (config.js) with env vars, stages, thresholds
- Shared utils (utils.js) with auth helpers and check functions
- Test data JSON files for data-driven testing via SharedArray
- Bearer token & Basic auth support with setup() function
- Response validation via check() on status, timing, body
- Logical grouping of requests via group()
- Ramp-up/steady/spike/ramp-down load stages
- Per-group threshold assertions for p95/p99 latency
---
k6-tests/config.js | 57 +++++++++
k6-tests/customer-test.js | 97 +++++++++++++++
k6-tests/data/customers.json | 7 ++
k6-tests/data/identities.json | 7 ++
k6-tests/data/notifications.json | 32 +++++
k6-tests/data/orders.json | 7 ++
k6-tests/data/products.json | 7 ++
k6-tests/identity-test.js | 97 +++++++++++++++
k6-tests/notification-test.js | 203 +++++++++++++++++++++++++++++++
k6-tests/order-test.js | 97 +++++++++++++++
k6-tests/product-test.js | 97 +++++++++++++++
k6-tests/utils.js | 103 ++++++++++++++++
12 files changed, 811 insertions(+)
create mode 100644 k6-tests/config.js
create mode 100644 k6-tests/customer-test.js
create mode 100644 k6-tests/data/customers.json
create mode 100644 k6-tests/data/identities.json
create mode 100644 k6-tests/data/notifications.json
create mode 100644 k6-tests/data/orders.json
create mode 100644 k6-tests/data/products.json
create mode 100644 k6-tests/identity-test.js
create mode 100644 k6-tests/notification-test.js
create mode 100644 k6-tests/order-test.js
create mode 100644 k6-tests/product-test.js
create mode 100644 k6-tests/utils.js
diff --git a/k6-tests/config.js b/k6-tests/config.js
new file mode 100644
index 0000000..00dc4bb
--- /dev/null
+++ b/k6-tests/config.js
@@ -0,0 +1,57 @@
+/**
+ * Shared configuration for all K6 performance test scripts.
+ *
+ * Environment variables (pass via -e flag or __ENV):
+ * BASE_URL – API Gateway root (default http://localhost:5000)
+ * AUTH_USERNAME – Basic-auth username (default "admin")
+ * AUTH_PASSWORD – Basic-auth password (default "password")
+ * AUTH_TOKEN_URL – OAuth / token endpoint for Bearer auth
+ * AUTH_CLIENT_ID – OAuth client id
+ * AUTH_CLIENT_SECRET – OAuth client secret
+ * VUS – override default virtual users
+ * DURATION – override default duration
+ */
+
+// ---------------------------------------------------------------------------
+// Service base URLs – routed through the API Gateway (port 5000) by default.
+// Override with direct service URLs when testing individual services.
+// ---------------------------------------------------------------------------
+export const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
+export const IDENTITY_URL = __ENV.IDENTITY_URL || `${BASE_URL}/api/identity`;
+export const CUSTOMER_URL = __ENV.CUSTOMER_URL || `${BASE_URL}/api/customer`;
+export const ORDER_URL = __ENV.ORDER_URL || `${BASE_URL}/api/order`;
+export const PRODUCT_URL = __ENV.PRODUCT_URL || `${BASE_URL}/api/product`;
+export const NOTIFICATION_URL = __ENV.NOTIFICATION_URL || `${BASE_URL}/api/notification`;
+
+// ---------------------------------------------------------------------------
+// Authentication
+// ---------------------------------------------------------------------------
+export const AUTH_USERNAME = __ENV.AUTH_USERNAME || 'admin';
+export const AUTH_PASSWORD = __ENV.AUTH_PASSWORD || 'password';
+export const AUTH_TOKEN_URL = __ENV.AUTH_TOKEN_URL || `${BASE_URL}/api/identity/token`;
+export const AUTH_CLIENT_ID = __ENV.AUTH_CLIENT_ID || '';
+export const AUTH_CLIENT_SECRET = __ENV.AUTH_CLIENT_SECRET || '';
+
+// ---------------------------------------------------------------------------
+// Default load-test options (can be imported and merged per-script)
+// ---------------------------------------------------------------------------
+export const DEFAULT_STAGES = [
+ { duration: '30s', target: 10 }, // ramp-up
+ { duration: '1m', target: 10 }, // steady state
+ { duration: '30s', target: 20 }, // spike
+ { duration: '1m', target: 20 }, // sustained spike
+ { duration: '30s', target: 0 }, // ramp-down
+];
+
+export const DEFAULT_THRESHOLDS = {
+ http_req_duration: ['p(95)<500', 'p(99)<1000'], // 95th < 500 ms, 99th < 1 s
+ http_req_failed: ['rate<0.05'], // < 5 % error rate
+ checks: ['rate>0.95'], // > 95 % of checks pass
+};
+
+export const DEFAULT_OPTIONS = {
+ stages: DEFAULT_STAGES,
+ thresholds: DEFAULT_THRESHOLDS,
+ noConnectionReuse: false,
+ userAgent: 'K6-PerformanceTest/1.0',
+};
diff --git a/k6-tests/customer-test.js b/k6-tests/customer-test.js
new file mode 100644
index 0000000..f31c170
--- /dev/null
+++ b/k6-tests/customer-test.js
@@ -0,0 +1,97 @@
+/**
+ * K6 Performance Test — Customer Service
+ *
+ * Endpoints tested:
+ * GET /api/customer — List all customers
+ * GET /api/customer/{id} — Get customer by ID
+ *
+ * Run:
+ * k6 run customer-test.js
+ * k6 run -e BASE_URL=http://localhost:5002 customer-test.js
+ */
+import http from 'k6/http';
+import { check, group } from 'k6';
+import { SharedArray } from 'k6/data';
+import { CUSTOMER_URL, DEFAULT_THRESHOLDS, DEFAULT_STAGES } from './config.js';
+import { jsonHeaders, basicAuthHeaders, fetchBearerToken, checkGetResponse } from './utils.js';
+
+// ---------------------------------------------------------------------------
+// Data-driven testing — load customer test data via SharedArray
+// ---------------------------------------------------------------------------
+const customers = new SharedArray('customers', function () {
+ return JSON.parse(open('./data/customers.json'));
+});
+
+// ---------------------------------------------------------------------------
+// Options
+// ---------------------------------------------------------------------------
+export const options = {
+ stages: DEFAULT_STAGES,
+ thresholds: {
+ ...DEFAULT_THRESHOLDS,
+ 'http_req_duration{group:::Customer - Get All}': ['p(95)<400'],
+ 'http_req_duration{group:::Customer - Get By ID}': ['p(95)<300'],
+ },
+ tags: { service: 'customer' },
+};
+
+// ---------------------------------------------------------------------------
+// Setup — authenticate and return a token for use in default function
+// ---------------------------------------------------------------------------
+export function setup() {
+ const token = fetchBearerToken();
+ return { token };
+}
+
+// ---------------------------------------------------------------------------
+// Default (main) function — executed by each VU on every iteration
+// ---------------------------------------------------------------------------
+export default function (data) {
+ const headers = data.token
+ ? jsonHeaders(data.token)
+ : basicAuthHeaders();
+
+ // ---- Group 1: Get All Customers ----
+ group('Customer - Get All', function () {
+ const res = http.get(CUSTOMER_URL, {
+ headers,
+ tags: { name: 'GET /api/customer' },
+ });
+
+ checkGetResponse(res, 'Get All Customers');
+
+ check(res, {
+ 'response is JSON': (r) => {
+ try {
+ JSON.parse(r.body);
+ return true;
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+ });
+
+ // ---- Group 2: Get Customer By ID (data-driven) ----
+ group('Customer - Get By ID', function () {
+ const customer = customers[Math.floor(Math.random() * customers.length)];
+
+ const res = http.get(`${CUSTOMER_URL}/${customer.id}`, {
+ headers,
+ tags: { name: 'GET /api/customer/{id}' },
+ });
+
+ checkGetResponse(res, `Get Customer ${customer.id}`);
+
+ check(res, {
+ 'response contains id field': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body.id !== undefined;
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+ });
+}
diff --git a/k6-tests/data/customers.json b/k6-tests/data/customers.json
new file mode 100644
index 0000000..469789e
--- /dev/null
+++ b/k6-tests/data/customers.json
@@ -0,0 +1,7 @@
+[
+ { "id": 1, "name": "Alice Johnson", "email": "alice@example.com", "phone": "555-0101" },
+ { "id": 2, "name": "Bob Martinez", "email": "bob@example.com", "phone": "555-0102" },
+ { "id": 3, "name": "Carol Lee", "email": "carol@example.com", "phone": "555-0103" },
+ { "id": 4, "name": "David Kim", "email": "david@example.com", "phone": "555-0104" },
+ { "id": 5, "name": "Eva Brown", "email": "eva@example.com", "phone": "555-0105" }
+]
diff --git a/k6-tests/data/identities.json b/k6-tests/data/identities.json
new file mode 100644
index 0000000..2b78d19
--- /dev/null
+++ b/k6-tests/data/identities.json
@@ -0,0 +1,7 @@
+[
+ { "id": 1, "username": "admin", "email": "admin@example.com", "role": "Admin" },
+ { "id": 2, "username": "jdoe", "email": "jdoe@example.com", "role": "User" },
+ { "id": 3, "username": "asmith", "email": "asmith@example.com", "role": "User" },
+ { "id": 4, "username": "mjones", "email": "mjones@example.com", "role": "Manager" },
+ { "id": 5, "username": "bwilson", "email": "bwilson@example.com", "role": "User" }
+]
diff --git a/k6-tests/data/notifications.json b/k6-tests/data/notifications.json
new file mode 100644
index 0000000..b447539
--- /dev/null
+++ b/k6-tests/data/notifications.json
@@ -0,0 +1,32 @@
+[
+ {
+ "orderId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
+ "customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
+ "totalAmount": 59.98,
+ "placedAt": "2026-04-30T10:00:00Z"
+ },
+ {
+ "orderId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
+ "customerId": "d4e5f6a7-b8c9-0123-defa-234567890123",
+ "totalAmount": 199.99,
+ "placedAt": "2026-04-30T11:30:00Z"
+ },
+ {
+ "orderId": "e5f6a7b8-c9d0-1234-efab-345678901234",
+ "customerId": "f6a7b8c9-d0e1-2345-fabc-456789012345",
+ "totalAmount": 124.95,
+ "placedAt": "2026-04-30T12:15:00Z"
+ },
+ {
+ "orderId": "a7b8c9d0-e1f2-3456-abcd-567890123456",
+ "customerId": "b8c9d0e1-f2a3-4567-bcde-678901234567",
+ "totalAmount": 89.97,
+ "placedAt": "2026-04-30T13:45:00Z"
+ },
+ {
+ "orderId": "c9d0e1f2-a3b4-5678-cdef-789012345678",
+ "customerId": "d0e1f2a3-b4c5-6789-defa-890123456789",
+ "totalAmount": 49.99,
+ "placedAt": "2026-04-30T14:00:00Z"
+ }
+]
diff --git a/k6-tests/data/orders.json b/k6-tests/data/orders.json
new file mode 100644
index 0000000..3d16628
--- /dev/null
+++ b/k6-tests/data/orders.json
@@ -0,0 +1,7 @@
+[
+ { "id": 1, "customerId": 1, "productId": 1, "quantity": 2, "totalAmount": 59.98 },
+ { "id": 2, "customerId": 2, "productId": 3, "quantity": 1, "totalAmount": 199.99 },
+ { "id": 3, "customerId": 3, "productId": 2, "quantity": 5, "totalAmount": 124.95 },
+ { "id": 4, "customerId": 4, "productId": 5, "quantity": 3, "totalAmount": 89.97 },
+ { "id": 5, "customerId": 5, "productId": 4, "quantity": 1, "totalAmount": 49.99 }
+]
diff --git a/k6-tests/data/products.json b/k6-tests/data/products.json
new file mode 100644
index 0000000..b6c4dcd
--- /dev/null
+++ b/k6-tests/data/products.json
@@ -0,0 +1,7 @@
+[
+ { "id": 1, "name": "Wireless Mouse", "price": 29.99, "category": "Electronics" },
+ { "id": 2, "name": "USB-C Hub", "price": 24.99, "category": "Electronics" },
+ { "id": 3, "name": "Mechanical Keyboard", "price": 199.99, "category": "Electronics" },
+ { "id": 4, "name": "Desk Lamp", "price": 49.99, "category": "Office" },
+ { "id": 5, "name": "Notebook Pack", "price": 29.99, "category": "Office" }
+]
diff --git a/k6-tests/identity-test.js b/k6-tests/identity-test.js
new file mode 100644
index 0000000..beee9ae
--- /dev/null
+++ b/k6-tests/identity-test.js
@@ -0,0 +1,97 @@
+/**
+ * K6 Performance Test — Identity Service
+ *
+ * Endpoints tested:
+ * GET /api/identity — List all identities
+ * GET /api/identity/{id} — Get identity by ID
+ *
+ * Run:
+ * k6 run identity-test.js
+ * k6 run -e BASE_URL=http://localhost:5001 identity-test.js
+ */
+import http from 'k6/http';
+import { check, group } from 'k6';
+import { SharedArray } from 'k6/data';
+import { IDENTITY_URL, DEFAULT_THRESHOLDS, DEFAULT_STAGES } from './config.js';
+import { jsonHeaders, basicAuthHeaders, fetchBearerToken, checkGetResponse } from './utils.js';
+
+// ---------------------------------------------------------------------------
+// Data-driven testing — load identity test data via SharedArray
+// ---------------------------------------------------------------------------
+const identities = new SharedArray('identities', function () {
+ return JSON.parse(open('./data/identities.json'));
+});
+
+// ---------------------------------------------------------------------------
+// Options
+// ---------------------------------------------------------------------------
+export const options = {
+ stages: DEFAULT_STAGES,
+ thresholds: {
+ ...DEFAULT_THRESHOLDS,
+ 'http_req_duration{group:::Identity - Get All}': ['p(95)<400'],
+ 'http_req_duration{group:::Identity - Get By ID}': ['p(95)<300'],
+ },
+ tags: { service: 'identity' },
+};
+
+// ---------------------------------------------------------------------------
+// Setup — authenticate and return a token for use in default function
+// ---------------------------------------------------------------------------
+export function setup() {
+ const token = fetchBearerToken();
+ return { token };
+}
+
+// ---------------------------------------------------------------------------
+// Default (main) function — executed by each VU on every iteration
+// ---------------------------------------------------------------------------
+export default function (data) {
+ const headers = data.token
+ ? jsonHeaders(data.token)
+ : basicAuthHeaders();
+
+ // ---- Group 1: Get All Identities ----
+ group('Identity - Get All', function () {
+ const res = http.get(IDENTITY_URL, {
+ headers,
+ tags: { name: 'GET /api/identity' },
+ });
+
+ checkGetResponse(res, 'Get All Identities');
+
+ check(res, {
+ 'response is JSON': (r) => {
+ try {
+ JSON.parse(r.body);
+ return true;
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+ });
+
+ // ---- Group 2: Get Identity By ID (data-driven) ----
+ group('Identity - Get By ID', function () {
+ const identity = identities[Math.floor(Math.random() * identities.length)];
+
+ const res = http.get(`${IDENTITY_URL}/${identity.id}`, {
+ headers,
+ tags: { name: 'GET /api/identity/{id}' },
+ });
+
+ checkGetResponse(res, `Get Identity ${identity.id}`);
+
+ check(res, {
+ 'response contains id field': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body.id !== undefined;
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+ });
+}
diff --git a/k6-tests/notification-test.js b/k6-tests/notification-test.js
new file mode 100644
index 0000000..2ca4623
--- /dev/null
+++ b/k6-tests/notification-test.js
@@ -0,0 +1,203 @@
+/**
+ * K6 Performance Test — Notification Service
+ *
+ * Endpoints tested:
+ * GET /api/notification — List all notifications (paginated)
+ * GET /api/notification/{id} — Get notification by ID
+ * GET /api/notification/{id}/preview — Get rendered HTML email preview
+ * POST /api/notification/events/order-placed — Submit an OrderPlacedEvent
+ *
+ * Run:
+ * k6 run notification-test.js
+ * k6 run -e BASE_URL=http://localhost:5005 notification-test.js
+ */
+import http from 'k6/http';
+import { check, group } from 'k6';
+import { SharedArray } from 'k6/data';
+import { NOTIFICATION_URL, DEFAULT_THRESHOLDS, DEFAULT_STAGES } from './config.js';
+import {
+ jsonHeaders,
+ basicAuthHeaders,
+ fetchBearerToken,
+ checkGetResponse,
+ checkPostResponse,
+} from './utils.js';
+
+// ---------------------------------------------------------------------------
+// Data-driven testing — load notification event payloads via SharedArray
+// ---------------------------------------------------------------------------
+const notificationEvents = new SharedArray('notifications', function () {
+ return JSON.parse(open('./data/notifications.json'));
+});
+
+// ---------------------------------------------------------------------------
+// Options
+// ---------------------------------------------------------------------------
+export const options = {
+ stages: DEFAULT_STAGES,
+ thresholds: {
+ ...DEFAULT_THRESHOLDS,
+ 'http_req_duration{group:::Notification - Get All}': ['p(95)<500'],
+ 'http_req_duration{group:::Notification - Get By ID}': ['p(95)<400'],
+ 'http_req_duration{group:::Notification - Preview}': ['p(95)<600'],
+ 'http_req_duration{group:::Notification - Post Order Event}': ['p(95)<1000'],
+ },
+ tags: { service: 'notification' },
+};
+
+// ---------------------------------------------------------------------------
+// Setup — authenticate and seed a notification so GET-by-ID / preview work
+// ---------------------------------------------------------------------------
+export function setup() {
+ const token = fetchBearerToken();
+ const headers = token
+ ? { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: `Bearer ${token}` }
+ : { 'Content-Type': 'application/json', Accept: 'application/json' };
+
+ // Seed one notification via POST so we have a known ID for GET tests
+ const seedPayload = JSON.stringify(notificationEvents[0]);
+ const seedRes = http.post(
+ `${NOTIFICATION_URL}/events/order-placed`,
+ seedPayload,
+ { headers, tags: { name: 'SETUP - seed notification' } },
+ );
+
+ let seededId = null;
+ if (seedRes.status === 201) {
+ try {
+ const body = JSON.parse(seedRes.body);
+ seededId = body.id || null;
+ } catch (_) {
+ // Seed failed — GET-by-ID tests will use fallback
+ }
+ }
+
+ return { token, seededId };
+}
+
+// ---------------------------------------------------------------------------
+// Default (main) function — executed by each VU on every iteration
+// ---------------------------------------------------------------------------
+export default function (data) {
+ const headers = data.token
+ ? jsonHeaders(data.token)
+ : basicAuthHeaders();
+
+ let createdId = null;
+
+ // ---- Group 1: POST — Submit Order-Placed Event ----
+ group('Notification - Post Order Event', function () {
+ const event = notificationEvents[Math.floor(Math.random() * notificationEvents.length)];
+ const payload = JSON.stringify({
+ orderId: event.orderId,
+ customerId: event.customerId,
+ totalAmount: event.totalAmount,
+ placedAt: event.placedAt,
+ });
+
+ const res = http.post(`${NOTIFICATION_URL}/events/order-placed`, payload, {
+ headers,
+ tags: { name: 'POST /api/notification/events/order-placed' },
+ });
+
+ checkPostResponse(res, 'Post OrderPlacedEvent');
+
+ check(res, {
+ 'response contains notification id': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body.id !== undefined && body.id !== null;
+ } catch (_) {
+ return false;
+ }
+ },
+ 'response contains preview URL': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body.previewUrl !== undefined;
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+
+ // Capture the created ID for subsequent GET requests
+ if (res.status === 201) {
+ try {
+ const body = JSON.parse(res.body);
+ createdId = body.id;
+ } catch (_) {
+ // fall through
+ }
+ }
+ });
+
+ // ---- Group 2: GET — List All Notifications (paginated) ----
+ group('Notification - Get All', function () {
+ const page = Math.floor(Math.random() * 3) + 1;
+ const res = http.get(`${NOTIFICATION_URL}?page=${page}&pageSize=20`, {
+ headers,
+ tags: { name: 'GET /api/notification' },
+ });
+
+ checkGetResponse(res, 'Get All Notifications');
+
+ check(res, {
+ 'response is JSON array': (r) => {
+ try {
+ return Array.isArray(JSON.parse(r.body));
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+ });
+
+ // ---- Group 3: GET — Get Notification By ID ----
+ const notificationId = createdId || data.seededId;
+ if (notificationId) {
+ group('Notification - Get By ID', function () {
+ const res = http.get(`${NOTIFICATION_URL}/${notificationId}`, {
+ headers,
+ tags: { name: 'GET /api/notification/{id}' },
+ });
+
+ checkGetResponse(res, `Get Notification ${notificationId}`);
+
+ check(res, {
+ 'response contains orderId': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body.orderId !== undefined;
+ } catch (_) {
+ return false;
+ }
+ },
+ 'response contains status': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body.status !== undefined;
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+ });
+
+ // ---- Group 4: GET — HTML Email Preview ----
+ group('Notification - Preview', function () {
+ const res = http.get(`${NOTIFICATION_URL}/${notificationId}/preview`, {
+ headers: { ...headers, Accept: 'text/html' },
+ tags: { name: 'GET /api/notification/{id}/preview' },
+ });
+
+ check(res, {
+ 'preview — status is 200': (r) => r.status === 200,
+ 'preview — response time < 600ms': (r) => r.timings.duration < 600,
+ 'preview — content-type is HTML': (r) =>
+ r.headers['Content-Type'] && r.headers['Content-Type'].includes('text/html'),
+ 'preview — body contains HTML': (r) => r.body && r.body.includes('<'),
+ });
+ });
+ }
+}
diff --git a/k6-tests/order-test.js b/k6-tests/order-test.js
new file mode 100644
index 0000000..c9f9b33
--- /dev/null
+++ b/k6-tests/order-test.js
@@ -0,0 +1,97 @@
+/**
+ * K6 Performance Test — Order Service
+ *
+ * Endpoints tested:
+ * GET /api/order — List all orders
+ * GET /api/order/{id} — Get order by ID
+ *
+ * Run:
+ * k6 run order-test.js
+ * k6 run -e BASE_URL=http://localhost:5003 order-test.js
+ */
+import http from 'k6/http';
+import { check, group } from 'k6';
+import { SharedArray } from 'k6/data';
+import { ORDER_URL, DEFAULT_THRESHOLDS, DEFAULT_STAGES } from './config.js';
+import { jsonHeaders, basicAuthHeaders, fetchBearerToken, checkGetResponse } from './utils.js';
+
+// ---------------------------------------------------------------------------
+// Data-driven testing — load order test data via SharedArray
+// ---------------------------------------------------------------------------
+const orders = new SharedArray('orders', function () {
+ return JSON.parse(open('./data/orders.json'));
+});
+
+// ---------------------------------------------------------------------------
+// Options
+// ---------------------------------------------------------------------------
+export const options = {
+ stages: DEFAULT_STAGES,
+ thresholds: {
+ ...DEFAULT_THRESHOLDS,
+ 'http_req_duration{group:::Order - Get All}': ['p(95)<400'],
+ 'http_req_duration{group:::Order - Get By ID}': ['p(95)<300'],
+ },
+ tags: { service: 'order' },
+};
+
+// ---------------------------------------------------------------------------
+// Setup — authenticate and return a token for use in default function
+// ---------------------------------------------------------------------------
+export function setup() {
+ const token = fetchBearerToken();
+ return { token };
+}
+
+// ---------------------------------------------------------------------------
+// Default (main) function — executed by each VU on every iteration
+// ---------------------------------------------------------------------------
+export default function (data) {
+ const headers = data.token
+ ? jsonHeaders(data.token)
+ : basicAuthHeaders();
+
+ // ---- Group 1: Get All Orders ----
+ group('Order - Get All', function () {
+ const res = http.get(ORDER_URL, {
+ headers,
+ tags: { name: 'GET /api/order' },
+ });
+
+ checkGetResponse(res, 'Get All Orders');
+
+ check(res, {
+ 'response is JSON': (r) => {
+ try {
+ JSON.parse(r.body);
+ return true;
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+ });
+
+ // ---- Group 2: Get Order By ID (data-driven) ----
+ group('Order - Get By ID', function () {
+ const order = orders[Math.floor(Math.random() * orders.length)];
+
+ const res = http.get(`${ORDER_URL}/${order.id}`, {
+ headers,
+ tags: { name: 'GET /api/order/{id}' },
+ });
+
+ checkGetResponse(res, `Get Order ${order.id}`);
+
+ check(res, {
+ 'response contains id field': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body.id !== undefined;
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+ });
+}
diff --git a/k6-tests/product-test.js b/k6-tests/product-test.js
new file mode 100644
index 0000000..5f882c6
--- /dev/null
+++ b/k6-tests/product-test.js
@@ -0,0 +1,97 @@
+/**
+ * K6 Performance Test — Product Service
+ *
+ * Endpoints tested:
+ * GET /api/product — List all products
+ * GET /api/product/{id} — Get product by ID
+ *
+ * Run:
+ * k6 run product-test.js
+ * k6 run -e BASE_URL=http://localhost:5004 product-test.js
+ */
+import http from 'k6/http';
+import { check, group } from 'k6';
+import { SharedArray } from 'k6/data';
+import { PRODUCT_URL, DEFAULT_THRESHOLDS, DEFAULT_STAGES } from './config.js';
+import { jsonHeaders, basicAuthHeaders, fetchBearerToken, checkGetResponse } from './utils.js';
+
+// ---------------------------------------------------------------------------
+// Data-driven testing — load product test data via SharedArray
+// ---------------------------------------------------------------------------
+const products = new SharedArray('products', function () {
+ return JSON.parse(open('./data/products.json'));
+});
+
+// ---------------------------------------------------------------------------
+// Options
+// ---------------------------------------------------------------------------
+export const options = {
+ stages: DEFAULT_STAGES,
+ thresholds: {
+ ...DEFAULT_THRESHOLDS,
+ 'http_req_duration{group:::Product - Get All}': ['p(95)<400'],
+ 'http_req_duration{group:::Product - Get By ID}': ['p(95)<300'],
+ },
+ tags: { service: 'product' },
+};
+
+// ---------------------------------------------------------------------------
+// Setup — authenticate and return a token for use in default function
+// ---------------------------------------------------------------------------
+export function setup() {
+ const token = fetchBearerToken();
+ return { token };
+}
+
+// ---------------------------------------------------------------------------
+// Default (main) function — executed by each VU on every iteration
+// ---------------------------------------------------------------------------
+export default function (data) {
+ const headers = data.token
+ ? jsonHeaders(data.token)
+ : basicAuthHeaders();
+
+ // ---- Group 1: Get All Products ----
+ group('Product - Get All', function () {
+ const res = http.get(PRODUCT_URL, {
+ headers,
+ tags: { name: 'GET /api/product' },
+ });
+
+ checkGetResponse(res, 'Get All Products');
+
+ check(res, {
+ 'response is JSON': (r) => {
+ try {
+ JSON.parse(r.body);
+ return true;
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+ });
+
+ // ---- Group 2: Get Product By ID (data-driven) ----
+ group('Product - Get By ID', function () {
+ const product = products[Math.floor(Math.random() * products.length)];
+
+ const res = http.get(`${PRODUCT_URL}/${product.id}`, {
+ headers,
+ tags: { name: 'GET /api/product/{id}' },
+ });
+
+ checkGetResponse(res, `Get Product ${product.id}`);
+
+ check(res, {
+ 'response contains id field': (r) => {
+ try {
+ const body = JSON.parse(r.body);
+ return body.id !== undefined;
+ } catch (_) {
+ return false;
+ }
+ },
+ });
+ });
+}
diff --git a/k6-tests/utils.js b/k6-tests/utils.js
new file mode 100644
index 0000000..1039f18
--- /dev/null
+++ b/k6-tests/utils.js
@@ -0,0 +1,103 @@
+/**
+ * Shared utility helpers for K6 test scripts.
+ */
+import http from 'k6/http';
+import { check } from 'k6';
+import encoding from 'k6/encoding';
+import {
+ AUTH_USERNAME,
+ AUTH_PASSWORD,
+ AUTH_TOKEN_URL,
+ AUTH_CLIENT_ID,
+ AUTH_CLIENT_SECRET,
+} from './config.js';
+
+// ---------------------------------------------------------------------------
+// Header builders
+// ---------------------------------------------------------------------------
+
+/**
+ * Returns common JSON request headers with an optional Bearer token.
+ */
+export function jsonHeaders(token) {
+ const headers = { 'Content-Type': 'application/json', Accept: 'application/json' };
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+ return headers;
+}
+
+/**
+ * Returns headers configured for Basic authentication.
+ */
+export function basicAuthHeaders() {
+ const encoded = encoding.b64encode(`${AUTH_USERNAME}:${AUTH_PASSWORD}`);
+ return {
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ Authorization: `Basic ${encoded}`,
+ };
+}
+
+// ---------------------------------------------------------------------------
+// Authentication helpers (used inside setup())
+// ---------------------------------------------------------------------------
+
+/**
+ * Retrieve a Bearer token via client-credentials or password grant.
+ * Returns the token string, or null when the token endpoint is unavailable.
+ */
+export function fetchBearerToken() {
+ const payload = {
+ grant_type: 'client_credentials',
+ client_id: AUTH_CLIENT_ID,
+ client_secret: AUTH_CLIENT_SECRET,
+ username: AUTH_USERNAME,
+ password: AUTH_PASSWORD,
+ };
+
+ const res = http.post(AUTH_TOKEN_URL, JSON.stringify(payload), {
+ headers: { 'Content-Type': 'application/json' },
+ tags: { name: 'auth_token' },
+ });
+
+ const ok = check(res, {
+ 'token request succeeded': (r) => r.status === 200,
+ });
+
+ if (ok) {
+ try {
+ const body = JSON.parse(res.body);
+ return body.access_token || body.token || null;
+ } catch (_) {
+ return null;
+ }
+ }
+ return null;
+}
+
+// ---------------------------------------------------------------------------
+// Common response checks
+// ---------------------------------------------------------------------------
+
+/**
+ * Standard checks for a successful GET response.
+ */
+export function checkGetResponse(res, label) {
+ return check(res, {
+ [`${label} — status is 200`]: (r) => r.status === 200,
+ [`${label} — response time < 500ms`]: (r) => r.timings.duration < 500,
+ [`${label} — body is not empty`]: (r) => r.body && r.body.length > 0,
+ });
+}
+
+/**
+ * Standard checks for a successful POST (201 Created) response.
+ */
+export function checkPostResponse(res, label) {
+ return check(res, {
+ [`${label} — status is 201`]: (r) => r.status === 201,
+ [`${label} — response time < 1000ms`]: (r) => r.timings.duration < 1000,
+ [`${label} — body is not empty`]: (r) => r.body && r.body.length > 0,
+ });
+}
From bd0e00aecc62ada98746d7daf562a20ba769eab4 Mon Sep 17 00:00:00 2001
From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Date: Thu, 30 Apr 2026 15:28:01 +0000
Subject: [PATCH 2/2] Add combined all-services K6 test, expand test data to 20
records each
- all-services-test.js: 10 VUs (2 per service), 25 iterations each, 5 min duration
- Per-VU scenarios using per-vu-iterations executor for precise control
- Expanded data sets to 20 records per service for production-realistic traffic
- Custom metrics (Counter + Trend) per service for granular monitoring
- Pacing with jitter to emulate realistic production request patterns
- Fix Notification.API.csproj Shared project reference paths (../../ -> ../../../)
---
k6-tests/all-services-test.js | 446 ++++++++++++++++++
k6-tests/data/customers.json | 25 +-
k6-tests/data/identities.json | 25 +-
k6-tests/data/notifications.json | 110 ++++-
k6-tests/data/orders.json | 25 +-
k6-tests/data/products.json | 25 +-
.../Notification.API/Notification.API.csproj | 4 +-
7 files changed, 628 insertions(+), 32 deletions(-)
create mode 100644 k6-tests/all-services-test.js
diff --git a/k6-tests/all-services-test.js b/k6-tests/all-services-test.js
new file mode 100644
index 0000000..2c332cd
--- /dev/null
+++ b/k6-tests/all-services-test.js
@@ -0,0 +1,446 @@
+/**
+ * K6 Combined Performance Test — All 5 Microservices
+ *
+ * 10 Virtual Users, 2 per service, each service hit 25 times over 5 minutes.
+ * Uses per-VU scenarios so each pair of VUs targets a specific service.
+ * Different data records are selected per iteration to emulate production traffic patterns.
+ *
+ * Run:
+ * k6 run all-services-test.js
+ * k6 run -e BASE_URL=http://localhost:5000 all-services-test.js
+ */
+import http from 'k6/http';
+import { check, group, sleep } from 'k6';
+import { SharedArray } from 'k6/data';
+import { Counter, Trend } from 'k6/metrics';
+import encoding from 'k6/encoding';
+
+// ---------------------------------------------------------------------------
+// Configuration
+// ---------------------------------------------------------------------------
+const BASE_URL = __ENV.BASE_URL || 'http://localhost:5000';
+const IDENTITY_URL = __ENV.IDENTITY_URL || `${BASE_URL}/api/identity`;
+const CUSTOMER_URL = __ENV.CUSTOMER_URL || `${BASE_URL}/api/customer`;
+const ORDER_URL = __ENV.ORDER_URL || `${BASE_URL}/api/order`;
+const PRODUCT_URL = __ENV.PRODUCT_URL || `${BASE_URL}/api/product`;
+const NOTIFICATION_URL = __ENV.NOTIFICATION_URL || `${BASE_URL}/api/notification`;
+
+const AUTH_USERNAME = __ENV.AUTH_USERNAME || 'admin';
+const AUTH_PASSWORD = __ENV.AUTH_PASSWORD || 'password';
+const AUTH_TOKEN_URL = __ENV.AUTH_TOKEN_URL || `${BASE_URL}/api/identity/token`;
+
+// ---------------------------------------------------------------------------
+// Custom metrics per service
+// ---------------------------------------------------------------------------
+const identityRequests = new Counter('identity_requests');
+const customerRequests = new Counter('customer_requests');
+const orderRequests = new Counter('order_requests');
+const productRequests = new Counter('product_requests');
+const notificationRequests = new Counter('notification_requests');
+
+const identityDuration = new Trend('identity_duration', true);
+const customerDuration = new Trend('customer_duration', true);
+const orderDuration = new Trend('order_duration', true);
+const productDuration = new Trend('product_duration', true);
+const notificationDuration = new Trend('notification_duration', true);
+
+// ---------------------------------------------------------------------------
+// Data-driven testing — 20 records each, cycled with different data per VU
+// ---------------------------------------------------------------------------
+const identities = new SharedArray('identities', function () {
+ return JSON.parse(open('./data/identities.json'));
+});
+
+const customers = new SharedArray('customers', function () {
+ return JSON.parse(open('./data/customers.json'));
+});
+
+const orders = new SharedArray('orders', function () {
+ return JSON.parse(open('./data/orders.json'));
+});
+
+const products = new SharedArray('products', function () {
+ return JSON.parse(open('./data/products.json'));
+});
+
+const notificationEvents = new SharedArray('notifications', function () {
+ return JSON.parse(open('./data/notifications.json'));
+});
+
+// ---------------------------------------------------------------------------
+// Scenarios: 2 VUs per service, 25 iterations each = 50 per service
+// 5 min duration with pacing: sleep ~12s between iterations (300s / 25 = 12s)
+// ---------------------------------------------------------------------------
+export const options = {
+ scenarios: {
+ identity_load: {
+ executor: 'per-vu-iterations',
+ vus: 2,
+ iterations: 25,
+ maxDuration: '5m',
+ exec: 'identityScenario',
+ tags: { service: 'identity' },
+ },
+ customer_load: {
+ executor: 'per-vu-iterations',
+ vus: 2,
+ iterations: 25,
+ maxDuration: '5m',
+ exec: 'customerScenario',
+ tags: { service: 'customer' },
+ },
+ order_load: {
+ executor: 'per-vu-iterations',
+ vus: 2,
+ iterations: 25,
+ maxDuration: '5m',
+ exec: 'orderScenario',
+ tags: { service: 'order' },
+ },
+ product_load: {
+ executor: 'per-vu-iterations',
+ vus: 2,
+ iterations: 25,
+ maxDuration: '5m',
+ exec: 'productScenario',
+ tags: { service: 'product' },
+ },
+ notification_load: {
+ executor: 'per-vu-iterations',
+ vus: 2,
+ iterations: 25,
+ maxDuration: '5m',
+ exec: 'notificationScenario',
+ tags: { service: 'notification' },
+ },
+ },
+ thresholds: {
+ http_req_duration: ['p(95)<500', 'p(99)<1000'],
+ http_req_failed: ['rate<0.05'],
+ checks: ['rate>0.95'],
+ identity_duration: ['p(95)<400'],
+ customer_duration: ['p(95)<400'],
+ order_duration: ['p(95)<400'],
+ product_duration: ['p(95)<400'],
+ notification_duration: ['p(95)<800'],
+ },
+};
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+function authHeaders(token) {
+ const base = { 'Content-Type': 'application/json', Accept: 'application/json' };
+ if (token) {
+ base['Authorization'] = `Bearer ${token}`;
+ } else {
+ const encoded = encoding.b64encode(`${AUTH_USERNAME}:${AUTH_PASSWORD}`);
+ base['Authorization'] = `Basic ${encoded}`;
+ }
+ return base;
+}
+
+function fetchToken() {
+ const res = http.post(AUTH_TOKEN_URL, JSON.stringify({
+ grant_type: 'client_credentials',
+ username: AUTH_USERNAME,
+ password: AUTH_PASSWORD,
+ }), { headers: { 'Content-Type': 'application/json' }, tags: { name: 'auth_token' } });
+
+ if (res.status === 200) {
+ try {
+ const body = JSON.parse(res.body);
+ return body.access_token || body.token || null;
+ } catch (_) { return null; }
+ }
+ return null;
+}
+
+function pick(arr, iteration) {
+ return arr[iteration % arr.length];
+}
+
+// Pacing: spread 25 iterations evenly across ~5 min ≈ 12s between requests
+// Add jitter ±3s to simulate realistic production variance
+function paceRequest() {
+ const base = 10;
+ const jitter = Math.random() * 6 - 3;
+ sleep(Math.max(1, base + jitter));
+}
+
+// ---------------------------------------------------------------------------
+// Setup — fetch auth token once
+// ---------------------------------------------------------------------------
+export function setup() {
+ const token = fetchToken();
+ return { token };
+}
+
+// ---------------------------------------------------------------------------
+// Scenario: Identity Service (VUs 1-2)
+// ---------------------------------------------------------------------------
+export function identityScenario(data) {
+ const headers = authHeaders(data.token);
+ const iter = __ITER;
+ const identity = pick(identities, iter + __VU);
+
+ group('Identity - Get All', function () {
+ const res = http.get(IDENTITY_URL, {
+ headers,
+ tags: { name: 'GET /api/identity' },
+ });
+ identityRequests.add(1);
+ identityDuration.add(res.timings.duration);
+
+ check(res, {
+ 'Identity GetAll — status 200': (r) => r.status === 200,
+ 'Identity GetAll — response time < 500ms': (r) => r.timings.duration < 500,
+ 'Identity GetAll — body is not empty': (r) => r.body && r.body.length > 0,
+ });
+ });
+
+ group('Identity - Get By ID', function () {
+ const res = http.get(`${IDENTITY_URL}/${identity.id}`, {
+ headers,
+ tags: { name: 'GET /api/identity/{id}' },
+ });
+ identityRequests.add(1);
+ identityDuration.add(res.timings.duration);
+
+ check(res, {
+ [`Identity Get #${identity.id} — status 200`]: (r) => r.status === 200,
+ [`Identity Get #${identity.id} — has id field`]: (r) => {
+ try { return JSON.parse(r.body).id !== undefined; } catch (_) { return false; }
+ },
+ [`Identity Get #${identity.id} — response < 300ms`]: (r) => r.timings.duration < 300,
+ });
+ });
+
+ paceRequest();
+}
+
+// ---------------------------------------------------------------------------
+// Scenario: Customer Service (VUs 3-4)
+// ---------------------------------------------------------------------------
+export function customerScenario(data) {
+ const headers = authHeaders(data.token);
+ const iter = __ITER;
+ const customer = pick(customers, iter + __VU);
+
+ group('Customer - Get All', function () {
+ const res = http.get(CUSTOMER_URL, {
+ headers,
+ tags: { name: 'GET /api/customer' },
+ });
+ customerRequests.add(1);
+ customerDuration.add(res.timings.duration);
+
+ check(res, {
+ 'Customer GetAll — status 200': (r) => r.status === 200,
+ 'Customer GetAll — response time < 500ms': (r) => r.timings.duration < 500,
+ 'Customer GetAll — body is not empty': (r) => r.body && r.body.length > 0,
+ });
+ });
+
+ group('Customer - Get By ID', function () {
+ const res = http.get(`${CUSTOMER_URL}/${customer.id}`, {
+ headers,
+ tags: { name: 'GET /api/customer/{id}' },
+ });
+ customerRequests.add(1);
+ customerDuration.add(res.timings.duration);
+
+ check(res, {
+ [`Customer Get #${customer.id} — status 200`]: (r) => r.status === 200,
+ [`Customer Get #${customer.id} — has id field`]: (r) => {
+ try { return JSON.parse(r.body).id !== undefined; } catch (_) { return false; }
+ },
+ [`Customer Get #${customer.id} — response < 300ms`]: (r) => r.timings.duration < 300,
+ });
+ });
+
+ paceRequest();
+}
+
+// ---------------------------------------------------------------------------
+// Scenario: Order Service (VUs 5-6)
+// ---------------------------------------------------------------------------
+export function orderScenario(data) {
+ const headers = authHeaders(data.token);
+ const iter = __ITER;
+ const order = pick(orders, iter + __VU);
+
+ group('Order - Get All', function () {
+ const res = http.get(ORDER_URL, {
+ headers,
+ tags: { name: 'GET /api/order' },
+ });
+ orderRequests.add(1);
+ orderDuration.add(res.timings.duration);
+
+ check(res, {
+ 'Order GetAll — status 200': (r) => r.status === 200,
+ 'Order GetAll — response time < 500ms': (r) => r.timings.duration < 500,
+ 'Order GetAll — body is not empty': (r) => r.body && r.body.length > 0,
+ });
+ });
+
+ group('Order - Get By ID', function () {
+ const res = http.get(`${ORDER_URL}/${order.id}`, {
+ headers,
+ tags: { name: 'GET /api/order/{id}' },
+ });
+ orderRequests.add(1);
+ orderDuration.add(res.timings.duration);
+
+ check(res, {
+ [`Order Get #${order.id} — status 200`]: (r) => r.status === 200,
+ [`Order Get #${order.id} — has id field`]: (r) => {
+ try { return JSON.parse(r.body).id !== undefined; } catch (_) { return false; }
+ },
+ [`Order Get #${order.id} — response < 300ms`]: (r) => r.timings.duration < 300,
+ });
+ });
+
+ paceRequest();
+}
+
+// ---------------------------------------------------------------------------
+// Scenario: Product Service (VUs 7-8)
+// ---------------------------------------------------------------------------
+export function productScenario(data) {
+ const headers = authHeaders(data.token);
+ const iter = __ITER;
+ const product = pick(products, iter + __VU);
+
+ group('Product - Get All', function () {
+ const res = http.get(PRODUCT_URL, {
+ headers,
+ tags: { name: 'GET /api/product' },
+ });
+ productRequests.add(1);
+ productDuration.add(res.timings.duration);
+
+ check(res, {
+ 'Product GetAll — status 200': (r) => r.status === 200,
+ 'Product GetAll — response time < 500ms': (r) => r.timings.duration < 500,
+ 'Product GetAll — body is not empty': (r) => r.body && r.body.length > 0,
+ });
+ });
+
+ group('Product - Get By ID', function () {
+ const res = http.get(`${PRODUCT_URL}/${product.id}`, {
+ headers,
+ tags: { name: 'GET /api/product/{id}' },
+ });
+ productRequests.add(1);
+ productDuration.add(res.timings.duration);
+
+ check(res, {
+ [`Product Get #${product.id} — status 200`]: (r) => r.status === 200,
+ [`Product Get #${product.id} — has id field`]: (r) => {
+ try { return JSON.parse(r.body).id !== undefined; } catch (_) { return false; }
+ },
+ [`Product Get #${product.id} — response < 300ms`]: (r) => r.timings.duration < 300,
+ });
+ });
+
+ paceRequest();
+}
+
+// ---------------------------------------------------------------------------
+// Scenario: Notification Service (VUs 9-10)
+// Includes POST + GET + Preview — mimics order-driven notification flow
+// ---------------------------------------------------------------------------
+export function notificationScenario(data) {
+ const headers = authHeaders(data.token);
+ const iter = __ITER;
+ const event = pick(notificationEvents, iter + __VU);
+
+ let createdId = null;
+
+ // POST: Submit a new order-placed event (creates a notification)
+ group('Notification - Post Order Event', function () {
+ const payload = JSON.stringify({
+ orderId: event.orderId,
+ customerId: event.customerId,
+ totalAmount: event.totalAmount,
+ placedAt: event.placedAt,
+ });
+
+ const res = http.post(`${NOTIFICATION_URL}/events/order-placed`, payload, {
+ headers,
+ tags: { name: 'POST /api/notification/events/order-placed' },
+ });
+ notificationRequests.add(1);
+ notificationDuration.add(res.timings.duration);
+
+ check(res, {
+ 'Notification POST — status 201': (r) => r.status === 201,
+ 'Notification POST — response < 1000ms': (r) => r.timings.duration < 1000,
+ 'Notification POST — has id': (r) => {
+ try { return JSON.parse(r.body).id !== undefined; } catch (_) { return false; }
+ },
+ });
+
+ if (res.status === 201) {
+ try { createdId = JSON.parse(res.body).id; } catch (_) { /* skip */ }
+ }
+ });
+
+ // GET: List all notifications (paginated)
+ group('Notification - Get All', function () {
+ const page = (iter % 3) + 1;
+ const res = http.get(`${NOTIFICATION_URL}?page=${page}&pageSize=10`, {
+ headers,
+ tags: { name: 'GET /api/notification' },
+ });
+ notificationRequests.add(1);
+ notificationDuration.add(res.timings.duration);
+
+ check(res, {
+ 'Notification GetAll — status 200': (r) => r.status === 200,
+ 'Notification GetAll — response < 500ms': (r) => r.timings.duration < 500,
+ 'Notification GetAll — is array': (r) => {
+ try { return Array.isArray(JSON.parse(r.body)); } catch (_) { return false; }
+ },
+ });
+ });
+
+ // GET by ID + Preview (only if we successfully created one)
+ if (createdId) {
+ group('Notification - Get By ID', function () {
+ const res = http.get(`${NOTIFICATION_URL}/${createdId}`, {
+ headers,
+ tags: { name: 'GET /api/notification/{id}' },
+ });
+ notificationRequests.add(1);
+ notificationDuration.add(res.timings.duration);
+
+ check(res, {
+ 'Notification GetByID — status 200': (r) => r.status === 200,
+ 'Notification GetByID — has orderId': (r) => {
+ try { return JSON.parse(r.body).orderId !== undefined; } catch (_) { return false; }
+ },
+ });
+ });
+
+ group('Notification - Preview', function () {
+ const res = http.get(`${NOTIFICATION_URL}/${createdId}/preview`, {
+ headers: { ...headers, Accept: 'text/html' },
+ tags: { name: 'GET /api/notification/{id}/preview' },
+ });
+ notificationRequests.add(1);
+ notificationDuration.add(res.timings.duration);
+
+ check(res, {
+ 'Notification Preview — status 200': (r) => r.status === 200,
+ 'Notification Preview — is HTML': (r) =>
+ r.headers['Content-Type'] && r.headers['Content-Type'].includes('text/html'),
+ 'Notification Preview — body has HTML content': (r) => r.body && r.body.includes('<'),
+ });
+ });
+ }
+
+ paceRequest();
+}
diff --git a/k6-tests/data/customers.json b/k6-tests/data/customers.json
index 469789e..6b09754 100644
--- a/k6-tests/data/customers.json
+++ b/k6-tests/data/customers.json
@@ -1,7 +1,22 @@
[
- { "id": 1, "name": "Alice Johnson", "email": "alice@example.com", "phone": "555-0101" },
- { "id": 2, "name": "Bob Martinez", "email": "bob@example.com", "phone": "555-0102" },
- { "id": 3, "name": "Carol Lee", "email": "carol@example.com", "phone": "555-0103" },
- { "id": 4, "name": "David Kim", "email": "david@example.com", "phone": "555-0104" },
- { "id": 5, "name": "Eva Brown", "email": "eva@example.com", "phone": "555-0105" }
+ { "id": 1, "name": "Alice Johnson", "email": "alice.j@techstart.io", "phone": "555-0101", "tier": "Gold" },
+ { "id": 2, "name": "Bob Martinez", "email": "bob.m@retailhub.com", "phone": "555-0102", "tier": "Silver" },
+ { "id": 3, "name": "Carol Lee", "email": "carol.lee@freshmkt.com", "phone": "555-0103", "tier": "Bronze" },
+ { "id": 4, "name": "David Kim", "email": "david.kim@luxbrand.co", "phone": "555-0104", "tier": "Gold" },
+ { "id": 5, "name": "Eva Brown", "email": "eva.brown@ecogoods.io", "phone": "555-0105", "tier": "Silver" },
+ { "id": 6, "name": "Frank Nguyen", "email": "frank.n@quickship.com", "phone": "555-0106", "tier": "Bronze" },
+ { "id": 7, "name": "Grace Okafor", "email": "grace.o@artisan.shop", "phone": "555-0107", "tier": "Gold" },
+ { "id": 8, "name": "Hector Ruiz", "email": "hector.r@sportzone.com", "phone": "555-0108", "tier": "Silver" },
+ { "id": 9, "name": "Irene Sato", "email": "irene.s@bookworm.co", "phone": "555-0109", "tier": "Bronze" },
+ { "id": 10, "name": "James Patel", "email": "james.p@gadgetworld.io", "phone": "555-0110", "tier": "Gold" },
+ { "id": 11, "name": "Katya Volkov", "email": "katya.v@homegoods.com", "phone": "555-0111", "tier": "Silver" },
+ { "id": 12, "name": "Leo Chen", "email": "leo.c@petcorner.shop", "phone": "555-0112", "tier": "Bronze" },
+ { "id": 13, "name": "Maria Fernandez", "email": "maria.f@stylehub.com", "phone": "555-0113", "tier": "Gold" },
+ { "id": 14, "name": "Nils Eriksson", "email": "nils.e@outdooradv.io", "phone": "555-0114", "tier": "Silver" },
+ { "id": 15, "name": "Olivia Thompson", "email": "olivia.t@gourmet.co", "phone": "555-0115", "tier": "Bronze" },
+ { "id": 16, "name": "Priya Sharma", "email": "priya.s@wellness.io", "phone": "555-0116", "tier": "Gold" },
+ { "id": 17, "name": "Quinn Murphy", "email": "quinn.m@smartliving.com", "phone": "555-0117", "tier": "Silver" },
+ { "id": 18, "name": "Raj Deshmukh", "email": "raj.d@craftbrew.co", "phone": "555-0118", "tier": "Bronze" },
+ { "id": 19, "name": "Sofia Andersson", "email": "sofia.a@greenliving.io", "phone": "555-0119", "tier": "Gold" },
+ { "id": 20, "name": "Tariq Hassan", "email": "tariq.h@fitgear.com", "phone": "555-0120", "tier": "Silver" }
]
diff --git a/k6-tests/data/identities.json b/k6-tests/data/identities.json
index 2b78d19..074c7b4 100644
--- a/k6-tests/data/identities.json
+++ b/k6-tests/data/identities.json
@@ -1,7 +1,22 @@
[
- { "id": 1, "username": "admin", "email": "admin@example.com", "role": "Admin" },
- { "id": 2, "username": "jdoe", "email": "jdoe@example.com", "role": "User" },
- { "id": 3, "username": "asmith", "email": "asmith@example.com", "role": "User" },
- { "id": 4, "username": "mjones", "email": "mjones@example.com", "role": "Manager" },
- { "id": 5, "username": "bwilson", "email": "bwilson@example.com", "role": "User" }
+ { "id": 1, "username": "admin", "email": "admin@globex.com", "role": "Admin" },
+ { "id": 2, "username": "jdoe", "email": "john.doe@acmecorp.com", "role": "User" },
+ { "id": 3, "username": "asmith", "email": "anna.smith@initech.com", "role": "User" },
+ { "id": 4, "username": "mjones", "email": "mike.jones@wayneent.com", "role": "Manager" },
+ { "id": 5, "username": "bwilson", "email": "beth.wilson@stark.io", "role": "User" },
+ { "id": 6, "username": "cgarcia", "email": "carlos.garcia@hooli.com", "role": "User" },
+ { "id": 7, "username": "dtran", "email": "diana.tran@piedpiper.io", "role": "Manager" },
+ { "id": 8, "username": "ewright", "email": "eli.wright@umbrella.co", "role": "User" },
+ { "id": 9, "username": "fmorales", "email": "fiona.morales@oscorp.com","role": "User" },
+ { "id": 10, "username": "gkowalski", "email": "greg.kowalski@lexcorp.io","role": "Admin" },
+ { "id": 11, "username": "hpatel", "email": "hina.patel@cyberdyne.ai", "role": "User" },
+ { "id": 12, "username": "iokoro", "email": "izu.okoro@weyland.com", "role": "User" },
+ { "id": 13, "username": "jpark", "email": "jin.park@aperture.sci", "role": "Manager" },
+ { "id": 14, "username": "klindgren", "email": "karin.lindgren@abstergo.net", "role": "User" },
+ { "id": 15, "username": "lchang", "email": "leon.chang@massiveD.com", "role": "User" },
+ { "id": 16, "username": "mnguyen", "email": "mai.nguyen@vault-tec.io", "role": "User" },
+ { "id": 17, "username": "obanerjee", "email": "om.banerjee@globex.com", "role": "Manager" },
+ { "id": 18, "username": "pjohnson", "email": "paula.johnson@initech.com","role": "User" },
+ { "id": 19, "username": "qali", "email": "qadir.ali@acmecorp.com", "role": "User" },
+ { "id": 20, "username": "rstone", "email": "rachel.stone@wayneent.com","role": "Admin" }
]
diff --git a/k6-tests/data/notifications.json b/k6-tests/data/notifications.json
index b447539..e0f2aa6 100644
--- a/k6-tests/data/notifications.json
+++ b/k6-tests/data/notifications.json
@@ -2,31 +2,121 @@
{
"orderId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
- "totalAmount": 59.98,
- "placedAt": "2026-04-30T10:00:00Z"
+ "totalAmount": 199.99,
+ "placedAt": "2026-04-30T08:12:33Z"
},
{
"orderId": "c3d4e5f6-a7b8-9012-cdef-123456789012",
"customerId": "d4e5f6a7-b8c9-0123-defa-234567890123",
- "totalAmount": 199.99,
- "placedAt": "2026-04-30T11:30:00Z"
+ "totalAmount": 59.98,
+ "placedAt": "2026-04-30T09:05:17Z"
},
{
"orderId": "e5f6a7b8-c9d0-1234-efab-345678901234",
"customerId": "f6a7b8c9-d0e1-2345-fabc-456789012345",
- "totalAmount": 124.95,
- "placedAt": "2026-04-30T12:15:00Z"
+ "totalAmount": 349.00,
+ "placedAt": "2026-04-30T09:45:02Z"
},
{
"orderId": "a7b8c9d0-e1f2-3456-abcd-567890123456",
"customerId": "b8c9d0e1-f2a3-4567-bcde-678901234567",
- "totalAmount": 89.97,
- "placedAt": "2026-04-30T13:45:00Z"
+ "totalAmount": 74.97,
+ "placedAt": "2026-04-30T10:22:41Z"
},
{
"orderId": "c9d0e1f2-a3b4-5678-cdef-789012345678",
"customerId": "d0e1f2a3-b4c5-6789-defa-890123456789",
- "totalAmount": 49.99,
- "placedAt": "2026-04-30T14:00:00Z"
+ "totalAmount": 29.99,
+ "placedAt": "2026-04-30T10:58:09Z"
+ },
+ {
+ "orderId": "11223344-5566-7788-99aa-bbccddeeff00",
+ "customerId": "aabbccdd-eeff-0011-2233-445566778899",
+ "totalAmount": 119.96,
+ "placedAt": "2026-04-30T11:15:55Z"
+ },
+ {
+ "orderId": "22334455-6677-8899-aabb-ccddeeff0011",
+ "customerId": "bbccddee-ff00-1122-3344-556677889900",
+ "totalAmount": 89.99,
+ "placedAt": "2026-04-30T11:33:28Z"
+ },
+ {
+ "orderId": "33445566-7788-99aa-bbcc-ddeeff001122",
+ "customerId": "ccddeeff-0011-2233-4455-667788990011",
+ "totalAmount": 99.98,
+ "placedAt": "2026-04-30T12:01:44Z"
+ },
+ {
+ "orderId": "44556677-8899-aabb-ccdd-eeff00112233",
+ "customerId": "ddeeff00-1122-3344-5566-778899001122",
+ "totalAmount": 599.99,
+ "placedAt": "2026-04-30T12:47:19Z"
+ },
+ {
+ "orderId": "55667788-99aa-bbcc-ddee-ff0011223344",
+ "customerId": "eeff0011-2233-4455-6677-889900112233",
+ "totalAmount": 149.95,
+ "placedAt": "2026-04-30T13:10:07Z"
+ },
+ {
+ "orderId": "66778899-aabb-ccdd-eeff-001122334455",
+ "customerId": "ff001122-3344-5566-7788-990011223344",
+ "totalAmount": 139.98,
+ "placedAt": "2026-04-30T13:35:52Z"
+ },
+ {
+ "orderId": "778899aa-bbcc-ddee-ff00-112233445566",
+ "customerId": "00112233-4455-6677-8899-aabbccddeeff",
+ "totalAmount": 249.99,
+ "placedAt": "2026-04-30T14:02:31Z"
+ },
+ {
+ "orderId": "8899aabb-ccdd-eeff-0011-223344556677",
+ "customerId": "11223344-5566-7788-99aa-bbccddeeff00",
+ "totalAmount": 44.97,
+ "placedAt": "2026-04-30T14:28:16Z"
+ },
+ {
+ "orderId": "99aabbcc-ddee-ff00-1122-334455667788",
+ "customerId": "22334455-6677-8899-aabb-ccddeeff0011",
+ "totalAmount": 179.99,
+ "placedAt": "2026-04-30T14:55:03Z"
+ },
+ {
+ "orderId": "aabbccdd-eeff-0011-2233-445566778899",
+ "customerId": "33445566-7788-99aa-bbcc-ddeeff001122",
+ "totalAmount": 69.98,
+ "placedAt": "2026-04-30T15:11:48Z"
+ },
+ {
+ "orderId": "bbccddee-ff00-1122-3344-556677889900",
+ "customerId": "44556677-8899-aabb-ccdd-eeff00112233",
+ "totalAmount": 449.99,
+ "placedAt": "2026-04-30T15:40:22Z"
+ },
+ {
+ "orderId": "ccddeeff-0011-2233-4455-667788990011",
+ "customerId": "55667788-99aa-bbcc-ddee-ff0011223344",
+ "totalAmount": 53.94,
+ "placedAt": "2026-04-30T16:05:37Z"
+ },
+ {
+ "orderId": "ddeeff00-1122-3344-5566-778899001122",
+ "customerId": "66778899-aabb-ccdd-eeff-001122334455",
+ "totalAmount": 999.99,
+ "placedAt": "2026-04-30T16:33:14Z"
+ },
+ {
+ "orderId": "eeff0011-2233-4455-6677-889900112233",
+ "customerId": "778899aa-bbcc-ddee-ff00-112233445566",
+ "totalAmount": 159.98,
+ "placedAt": "2026-04-30T17:01:59Z"
+ },
+ {
+ "orderId": "ff001122-3344-5566-7788-990011223344",
+ "customerId": "8899aabb-ccdd-eeff-0011-223344556677",
+ "totalAmount": 39.99,
+ "placedAt": "2026-04-30T17:28:46Z"
}
]
diff --git a/k6-tests/data/orders.json b/k6-tests/data/orders.json
index 3d16628..8c54c71 100644
--- a/k6-tests/data/orders.json
+++ b/k6-tests/data/orders.json
@@ -1,7 +1,22 @@
[
- { "id": 1, "customerId": 1, "productId": 1, "quantity": 2, "totalAmount": 59.98 },
- { "id": 2, "customerId": 2, "productId": 3, "quantity": 1, "totalAmount": 199.99 },
- { "id": 3, "customerId": 3, "productId": 2, "quantity": 5, "totalAmount": 124.95 },
- { "id": 4, "customerId": 4, "productId": 5, "quantity": 3, "totalAmount": 89.97 },
- { "id": 5, "customerId": 5, "productId": 4, "quantity": 1, "totalAmount": 49.99 }
+ { "id": 1, "customerId": 1, "productId": 3, "quantity": 1, "totalAmount": 199.99 },
+ { "id": 2, "customerId": 5, "productId": 1, "quantity": 2, "totalAmount": 59.98 },
+ { "id": 3, "customerId": 3, "productId": 7, "quantity": 1, "totalAmount": 349.00 },
+ { "id": 4, "customerId": 10, "productId": 2, "quantity": 3, "totalAmount": 74.97 },
+ { "id": 5, "customerId": 7, "productId": 5, "quantity": 1, "totalAmount": 29.99 },
+ { "id": 6, "customerId": 12, "productId": 9, "quantity": 4, "totalAmount": 119.96 },
+ { "id": 7, "customerId": 2, "productId": 12, "quantity": 1, "totalAmount": 89.99 },
+ { "id": 8, "customerId": 15, "productId": 4, "quantity": 2, "totalAmount": 99.98 },
+ { "id": 9, "customerId": 8, "productId": 15, "quantity": 1, "totalAmount": 599.99 },
+ { "id": 10, "customerId": 19, "productId": 6, "quantity": 5, "totalAmount": 149.95 },
+ { "id": 11, "customerId": 4, "productId": 11, "quantity": 2, "totalAmount": 139.98 },
+ { "id": 12, "customerId": 16, "productId": 8, "quantity": 1, "totalAmount": 249.99 },
+ { "id": 13, "customerId": 6, "productId": 14, "quantity": 3, "totalAmount": 44.97 },
+ { "id": 14, "customerId": 14, "productId": 10, "quantity": 1, "totalAmount": 179.99 },
+ { "id": 15, "customerId": 9, "productId": 13, "quantity": 2, "totalAmount": 69.98 },
+ { "id": 16, "customerId": 20, "productId": 16, "quantity": 1, "totalAmount": 449.99 },
+ { "id": 17, "customerId": 11, "productId": 18, "quantity": 6, "totalAmount": 53.94 },
+ { "id": 18, "customerId": 13, "productId": 20, "quantity": 1, "totalAmount": 999.99 },
+ { "id": 19, "customerId": 17, "productId": 17, "quantity": 2, "totalAmount": 159.98 },
+ { "id": 20, "customerId": 18, "productId": 19, "quantity": 1, "totalAmount": 39.99 }
]
diff --git a/k6-tests/data/products.json b/k6-tests/data/products.json
index b6c4dcd..075dc20 100644
--- a/k6-tests/data/products.json
+++ b/k6-tests/data/products.json
@@ -1,7 +1,22 @@
[
- { "id": 1, "name": "Wireless Mouse", "price": 29.99, "category": "Electronics" },
- { "id": 2, "name": "USB-C Hub", "price": 24.99, "category": "Electronics" },
- { "id": 3, "name": "Mechanical Keyboard", "price": 199.99, "category": "Electronics" },
- { "id": 4, "name": "Desk Lamp", "price": 49.99, "category": "Office" },
- { "id": 5, "name": "Notebook Pack", "price": 29.99, "category": "Office" }
+ { "id": 1, "name": "Wireless Mouse", "price": 29.99, "category": "Electronics", "sku": "ELEC-001" },
+ { "id": 2, "name": "USB-C Hub", "price": 24.99, "category": "Electronics", "sku": "ELEC-002" },
+ { "id": 3, "name": "Mechanical Keyboard", "price": 199.99, "category": "Electronics", "sku": "ELEC-003" },
+ { "id": 4, "name": "Desk Lamp", "price": 49.99, "category": "Office", "sku": "OFFC-001" },
+ { "id": 5, "name": "Notebook Pack", "price": 29.99, "category": "Office", "sku": "OFFC-002" },
+ { "id": 6, "name": "Ergonomic Chair", "price": 349.00, "category": "Furniture", "sku": "FURN-001" },
+ { "id": 7, "name": "Standing Desk", "price": 599.99, "category": "Furniture", "sku": "FURN-002" },
+ { "id": 8, "name": "4K Monitor", "price": 249.99, "category": "Electronics", "sku": "ELEC-004" },
+ { "id": 9, "name": "Noise-Cancel Headphones","price": 29.99, "category": "Audio", "sku": "AUDI-001" },
+ { "id": 10, "name": "Bluetooth Speaker", "price": 179.99, "category": "Audio", "sku": "AUDI-002" },
+ { "id": 11, "name": "Webcam HD", "price": 69.99, "category": "Electronics", "sku": "ELEC-005" },
+ { "id": 12, "name": "External SSD 1TB", "price": 89.99, "category": "Storage", "sku": "STOR-001" },
+ { "id": 13, "name": "Phone Stand", "price": 34.99, "category": "Accessories", "sku": "ACCS-001" },
+ { "id": 14, "name": "Sticky Notes Bulk", "price": 14.99, "category": "Office", "sku": "OFFC-003" },
+ { "id": 15, "name": "Ultrawide Monitor", "price": 599.99, "category": "Electronics", "sku": "ELEC-006" },
+ { "id": 16, "name": "Laptop Backpack", "price": 449.99, "category": "Accessories", "sku": "ACCS-002" },
+ { "id": 17, "name": "Wireless Charger", "price": 79.99, "category": "Electronics", "sku": "ELEC-007" },
+ { "id": 18, "name": "Pens Multipack", "price": 8.99, "category": "Office", "sku": "OFFC-004" },
+ { "id": 19, "name": "Cable Organizer", "price": 39.99, "category": "Accessories", "sku": "ACCS-003" },
+ { "id": 20, "name": "Curved Gaming Monitor", "price": 999.99, "category": "Electronics", "sku": "ELEC-008" }
]
diff --git a/src/Services/Notification/Notification.API/Notification.API.csproj b/src/Services/Notification/Notification.API/Notification.API.csproj
index 25b5fb0..1518ca2 100644
--- a/src/Services/Notification/Notification.API/Notification.API.csproj
+++ b/src/Services/Notification/Notification.API/Notification.API.csproj
@@ -7,8 +7,8 @@
-
-
+
+