185 lines
6.5 KiB
JavaScript
185 lines
6.5 KiB
JavaScript
/**
|
|
* 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 };
|