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/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..6b09754 --- /dev/null +++ b/k6-tests/data/customers.json @@ -0,0 +1,22 @@ +[ + { "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 new file mode 100644 index 0000000..074c7b4 --- /dev/null +++ b/k6-tests/data/identities.json @@ -0,0 +1,22 @@ +[ + { "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 new file mode 100644 index 0000000..e0f2aa6 --- /dev/null +++ b/k6-tests/data/notifications.json @@ -0,0 +1,122 @@ +[ + { + "orderId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "customerId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "totalAmount": 199.99, + "placedAt": "2026-04-30T08:12:33Z" + }, + { + "orderId": "c3d4e5f6-a7b8-9012-cdef-123456789012", + "customerId": "d4e5f6a7-b8c9-0123-defa-234567890123", + "totalAmount": 59.98, + "placedAt": "2026-04-30T09:05:17Z" + }, + { + "orderId": "e5f6a7b8-c9d0-1234-efab-345678901234", + "customerId": "f6a7b8c9-d0e1-2345-fabc-456789012345", + "totalAmount": 349.00, + "placedAt": "2026-04-30T09:45:02Z" + }, + { + "orderId": "a7b8c9d0-e1f2-3456-abcd-567890123456", + "customerId": "b8c9d0e1-f2a3-4567-bcde-678901234567", + "totalAmount": 74.97, + "placedAt": "2026-04-30T10:22:41Z" + }, + { + "orderId": "c9d0e1f2-a3b4-5678-cdef-789012345678", + "customerId": "d0e1f2a3-b4c5-6789-defa-890123456789", + "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 new file mode 100644 index 0000000..8c54c71 --- /dev/null +++ b/k6-tests/data/orders.json @@ -0,0 +1,22 @@ +[ + { "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 new file mode 100644 index 0000000..075dc20 --- /dev/null +++ b/k6-tests/data/products.json @@ -0,0 +1,22 @@ +[ + { "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/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, + }); +} 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 @@ - - + +