1
21import http from "k6/http";
22import { check, sleep } from "k6";
23import { Trend, Rate } from "k6/metrics";
24
25export const options = {
26 scenarios: {
27 api_load: {
28 executor: "ramping-arrival-rate",
29 startRate: 10,
30 timeUnit: "1s",
31 preAllocatedVUs: 50,
32 maxVUs: 1_000,
33 stages: [
34 { target: 100, duration: "30s" },
35 { target: 500, duration: "30s" },
36 { target: 1000, duration: "120s" },
37 ],
38 },
39 },
40 thresholds: {
41 http_req_duration: ["p(95)<300", "p(99)<600"],
42 http_req_failed: ["rate<0.001"],
43 auth_setup_failures: ["count<1"],
44 search_ms: ["p(95)<500"],
45 },
46};
47
48
49const searchMs = new Trend("search_ms", true);
50const setupFailures = new Rate("auth_setup_failures");
51
52const API_URL = __ENV.K6_API_URL || "http://localhost:4000";
53const EMAIL = __ENV.K6_EMAIL || "demo@factory.local";
54const PASSWORD = __ENV.K6_PASSWORD || "factory-demo-123";
55
56export function setup() {
57 const unique = `k6-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
58
59
60 let login = http.post(`${API_URL}/api/v1/auth/login`, JSON.stringify({ email: EMAIL, password: PASSWORD }), {
61 headers: { "Content-Type": "application/json" },
62 });
63 if (login.status !== 200) {
64 const reg = http.post(`${API_URL}/api/v1/auth/register`, JSON.stringify({ email: EMAIL, password: PASSWORD, name: "K6 Load" }), {
65 headers: { "Content-Type": "application/json" },
66 });
67 if (reg.status !== 201 && reg.status !== 200) {
68 setupFailures.add(1);
69 throw new Error(`setup: cannot authenticate (login ${login.status}, register ${reg.status})`);
70 }
71 login = http.post(`${API_URL}/api/v1/auth/login`, JSON.stringify({ email: EMAIL, password: PASSWORD }), {
72 headers: { "Content-Type": "application/json" },
73 });
74 }
75 if (login.status !== 200) {
76 setupFailures.add(1);
77 throw new Error(`setup: login failed (${login.status})`);
78 }
79 const body = login.json();
80 const token = body.accessToken || body.access_token || body.token;
81 if (!token) {
82 setupFailures.add(1);
83 throw new Error("setup: no access token in login response");
84 }
85 const user = (body.user && body.user.id) || "unknown-user";
86
87
88 const wsRes = http.post(
89 `${API_URL}/api/v1/workspaces`,
90 JSON.stringify({ name: `K6 WS ${unique}` }),
91 { headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` } },
92 );
93 if (wsRes.status !== 201) throw new Error(`setup: workspace create failed (${wsRes.status})`);
94 const workspaceId = wsRes.json().workspace.id;
95
96 const pageRes = http.post(
97 `${API_URL}/api/v1/workspaces/${workspaceId}/pages`,
98 JSON.stringify({ title: `K6 Load Page ${unique}` }),
99 { headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` } },
100 );
101 if (pageRes.status !== 201) throw new Error(`setup: page create failed (${pageRes.status})`);
102 const pageId = pageRes.json().page.id;
103
104
105 http.post(
106 `${API_URL}/api/v1/pages/${pageId}/blocks:batch`,
107 JSON.stringify({
108 operations: Array.from({ length: 25 }, (_, i) => ({
109 op: "insert",
110 type: "PARAGRAPH",
111 content: { text: `k6 load block ${i}` },
112 afterId: i === 0 ? null : "PREV",
113 })),
114 }),
115 { headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` } },
116 );
117
118 return { token, userId: user, workspaceId, pageId };
119}
120
121export function teardown(data) {
122
123 http.delete(`${API_URL}/api/v1/workspaces/${data.workspaceId}`, {
124 headers: { Authorization: `Bearer ${data.token}` },
125 });
126}
127
128const HEADERS = (data) => ({
129 "Content-Type": "application/json",
130 Authorization: `Bearer ${data.token}`,
131});
132
133export default function (data) {
134 const roll = Math.random();
135 if (roll < 0.4) {
136
137 const res = http.get(`${API_URL}/api/v1/pages/${data.pageId}`, { headers: HEADERS(data) });
138 check(res, { "page: 200": (r) => r.status === 200 });
139 } else if (roll < 0.7) {
140
141 const res = http.get(`${API_URL}/api/v1/pages/${data.pageId}/blocks`, { headers: HEADERS(data) });
142 check(res, { "blocks: 200": (r) => r.status === 200 });
143 } else if (roll < 0.9) {
144
145
146
147 const res = http.post(
148 `${API_URL}/api/v1/pages/${data.pageId}/blocks:batch`,
149 JSON.stringify({
150 operations: [
151 {
152 op: "insert",
153 type: "PARAGRAPH",
154 content: { text: `k6 write ${Date.now()}-${Math.random().toString(36).slice(2)}` },
155 afterId: null,
156 },
157 ],
158 }),
159 { headers: HEADERS(data) },
160 );
161 check(res, { "batch: 2xx": (r) => r.status === 200 || r.status === 201 });
162 } else {
163
164 const started = Date.now();
165 const res = http.get(
166 `${API_URL}/api/v1/search?q=k6%20load%20block&limit=10`,
167 { headers: HEADERS(data) },
168 );
169 searchMs.add(Date.now() - started);
170 check(res, { "search: 200": (r) => r.status === 200 });
171 }
172
173
174 sleep(0.1);
175}
176