/** * SCENARIO: pairing-flow.js * Guardian invite → User accept/reject → Pairing status check → Unpair * * Tujuan: Test business logic pairing — paling complex di WalkGuide karena * melibatkan dua role berbeda, constraint unique, dan state transitions. * Target: p95 pairing latency < 600ms, error rate < 1% */ import { sleep, check } from "k6"; import { DEFAULT_THRESHOLDS, handleSummary, } from "../modules/metrics-helper.js"; import * as api from "../modules/api-client.js"; import { testData } from "../modules/test-data.js"; export const options = { scenarios: { pairing_flow: { executor: "ramping-vus", startVUs: 0, stages: [ { duration: "20s", target: 5 }, { duration: "60s", target: 20 }, { duration: "60s", target: 20 }, { duration: "20s", target: 0 }, ], gracefulRampDown: "20s", }, }, thresholds: { ...DEFAULT_THRESHOLDS, walkguide_pairing_latency_ms: ["p(95)<600", "p(99)<1200"], }, }; // ── Helper: register dan dapat tokens ──────────────────────────────────────── function registerGetTokens(payload) { const res = api.register(payload); if (res.status !== 200) return null; const body = api.parseBody(res); return body && body.data ? body.data : null; } // ── Main VU function ────────────────────────────────────────────────────────── export default function pairingFlow() { const vuId = __VU; const iter = __ITER; const suffix = `${vuId}_${iter}_${Date.now()}`; // ── Step 1: Register Guardian ───────────────────────────────────────────── const gPayload = testData.guardianRegisterPayload(suffix); const gTokens = registerGetTokens(gPayload); if (!gTokens) { console.error(`[pairing-flow] Guardian register failed for VU ${vuId}`); sleep(1); return; } sleep(0.3); // ── Step 2: Register User (tunanetra) ───────────────────────────────────── const uPayload = testData.userRegisterPayload(suffix); const uTokens = registerGetTokens(uPayload); if (!uTokens) { console.error(`[pairing-flow] User register failed for VU ${vuId}`); sleep(1); return; } // uniqueUserId harus ada untuk user baru const uniqueUserId = uTokens.uniqueUserId; if (!uniqueUserId) { console.warn(`[pairing-flow] uniqueUserId missing, VU ${vuId}`); } sleep(0.3); // ── Step 3: Check pairing status (harus NONE/belum paired) ─────────────── const statusRes1 = api.getPairingStatus(gTokens.accessToken); check(statusRes1, { "pairing-status initial: 2xx": (r) => r.status >= 200 && r.status < 300, }); sleep(0.2); // ── Step 4: Guardian invite User ───────────────────────────────────────── const inviteRes = api.inviteUser( gTokens.accessToken, uniqueUserId || testData.fakeUniqueUserId(), ); check(inviteRes, { "pairing-invite: status 2xx": (r) => r.status >= 200 && r.status < 300, "pairing-invite: has data": (r) => { const b = api.parseBody(r); return b && b.data !== undefined; }, }); let pairingId = null; if (inviteRes.status >= 200 && inviteRes.status < 300) { const body = api.parseBody(inviteRes); pairingId = body && body.data ? body.data.pairingId : null; } sleep(0.3); // ── Step 5: User check pending invitation ───────────────────────────────── const statusRes2 = api.getPairingStatus(uTokens.accessToken); check(statusRes2, { "user-pairing-status: 2xx": (r) => r.status >= 200 && r.status < 300, "user-pairing-status: PENDING": (r) => { const b = api.parseBody(r); // Bisa PENDING atau NONE jika invite belum proses return b && b.data !== undefined; }, }); sleep(0.2); // ── Step 6: User accept atau reject (alternating per iteration) ─────────── if (pairingId) { const shouldAccept = iter % 3 !== 0; // 2/3 accept, 1/3 reject const respondRes = api.respondPairing( uTokens.accessToken, pairingId, shouldAccept, ); check(respondRes, { "pairing-respond: status 2xx": (r) => r.status >= 200 && r.status < 300, }); sleep(0.3); // ── Step 7: Confirm status setelah respond ────────────────────────────── const statusRes3 = api.getPairingStatus(gTokens.accessToken); check(statusRes3, { "pairing-confirm: 2xx": (r) => r.status >= 200 && r.status < 300, "pairing-confirm: status matches": (r) => { const b = api.parseBody(r); if (!b || !b.data) return true; // optional const s = b.data.status; return shouldAccept ? s === "ACTIVE" : s === "REJECTED" || s === "NONE"; }, }); sleep(0.3); // ── Step 8: Unpair (cleanup + test unpair endpoint) ──────────────────── if (shouldAccept) { const unpairRes = api.unpair(gTokens.accessToken); check(unpairRes, { "unpair: status 2xx": (r) => r.status >= 200 && r.status < 300, }); } } // ── Step 9: Test bahwa Guardian tidak bisa masuk ke /user endpoint ───────── const forbiddenRes = api.getUserProfile(gTokens.accessToken); check(forbiddenRes, { "RBAC: guardian cannot access /user/profile": (r) => r.status === 403 || r.status === 401, }); // ── Step 10: Test bahwa User tidak bisa akses /guardian endpoint ─────────── const forbiddenRes2 = api.getGuardianDashboard(uTokens.accessToken); check(forbiddenRes2, { "RBAC: user cannot access /guardian/dashboard": (r) => r.status === 403 || r.status === 401, }); sleep(testData.randomFloat(0.5, 1.5, 1)); } export function setup() { const res = api.ping(); if (res.status !== 200) throw new Error(`Backend not reachable: ${res.status}`); console.log("✅ Pairing flow test starting."); } export { handleSummary };