import http from 'k6/http' import { check, sleep } from 'k6' import { Rate, Trend } from 'k6/metrics' const BASE_URL = (__ENV.NEXUS_BASE_URL || 'http://127.0.0.1:18880').replace(/\/+$/, '') const BEARER_TOKEN = (__ENV.NEXUS_BEARER_TOKEN || '').trim() const API_KEY = (__ENV.NEXUS_API_KEY || '').trim() const IS_SMOKE = (__ENV.NEXUS_K6_SMOKE || '').trim() === '1' const TARGET = new URL(BASE_URL) const IS_LOOPBACK = ['127.0.0.1', 'localhost', '::1', '[::1]'].includes(TARGET.hostname) const INSECURE_TLS = (__ENV.NEXUS_INSECURE_TLS || '').trim() === '1' const ALLOW_REMOTE = (__ENV.NEXUS_K6_ALLOW_REMOTE || '').trim() === '1' const REQUIRE_DONE_CURSOR = (__ENV.NEXUS_K6_REQUIRE_DONE_CURSOR || '').trim() ? (__ENV.NEXUS_K6_REQUIRE_DONE_CURSOR || '').trim() === '1' : !IS_SMOKE if (!BEARER_TOKEN && !API_KEY) { throw new Error( 'Set NEXUS_BEARER_TOKEN or NEXUS_API_KEY. Credentials are read from the environment and are never printed.', ) } if (!['http:', 'https:'].includes(TARGET.protocol)) { throw new Error('NEXUS_BASE_URL must use HTTP or HTTPS.') } if ( TARGET.username || TARGET.password || TARGET.search || TARGET.hash || !['', '/'].includes(TARGET.pathname) ) { throw new Error( 'NEXUS_BASE_URL must be an origin without embedded credentials, path, query, or fragment.', ) } if (!IS_LOOPBACK && !ALLOW_REMOTE) { throw new Error( 'Remote k6 targets are blocked. Set NEXUS_K6_ALLOW_REMOTE=1 only for an isolated non-production environment.', ) } if (!IS_LOOPBACK && TARGET.protocol !== 'https:') { throw new Error( 'Remote k6 targets must use HTTPS because the test sends an authenticated credential.', ) } if (!IS_LOOPBACK && INSECURE_TLS) { throw new Error('NEXUS_INSECURE_TLS is allowed only for loopback diagnostics.') } const boardRequestDuration = new Trend('nexus_board_request_duration', true) const boardInitialDuration = new Trend('nexus_board_initial_duration', true) const boardDoneDuration = new Trend('nexus_board_done_duration', true) const boardFailures = new Rate('nexus_board_failures') const boardContractFailures = new Rate('nexus_board_contract_failures') const boardDatasetFailures = new Rate('nexus_board_dataset_failures') export const options = { scenarios: { task_board: IS_SMOKE ? { executor: 'constant-vus', vus: 1, duration: '10s', gracefulStop: '5s', } : { executor: 'constant-vus', vus: 10, duration: '2m', gracefulStop: '15s', }, }, thresholds: IS_SMOKE ? { nexus_board_failures: ['rate<0.05'], nexus_board_contract_failures: ['rate<0.05'], nexus_board_dataset_failures: ['rate<0.05'], } : { 'http_req_duration{endpoint:task-board,page:initial}': ['p(95)<500'], 'http_req_duration{endpoint:task-board,page:done}': ['p(95)<300'], 'http_req_failed{endpoint:task-board,page:initial}': ['rate<0.01'], 'http_req_failed{endpoint:task-board,page:done}': ['rate<0.01'], nexus_board_initial_duration: ['p(95)<500'], nexus_board_done_duration: ['p(95)<300'], nexus_board_failures: ['rate<0.01'], nexus_board_contract_failures: ['rate<0.01'], nexus_board_dataset_failures: ['rate<0.01'], checks: ['rate>0.99'], }, userAgent: 'nexus-task-board-k6/1.0', insecureSkipTLSVerify: INSECURE_TLS, noConnectionReuse: false, } function authHeaders() { const headers = { Accept: 'application/json', } if (BEARER_TOKEN) headers.Authorization = `Bearer ${BEARER_TOKEN}` if (API_KEY) headers['X-Nexus-Api-Key'] = API_KEY return headers } function readBoardPage(url, page) { const response = http.get(url, { headers: authHeaders(), tags: { endpoint: 'task-board', page, }, timeout: '10s', }) boardRequestDuration.add(response.timings.duration, { page }) if (page === 'initial') boardInitialDuration.add(response.timings.duration) if (page === 'done') boardDoneDuration.add(response.timings.duration) const httpOk = response.status === 200 boardFailures.add(!httpOk, { page }) let body = null if (httpOk) { try { body = response.json() } catch { body = null } } const contractOk = Boolean( body && typeof body.revision === 'string' && Array.isArray(body.offen) && Array.isArray(body.inProgress) && Array.isArray(body.review) && Array.isArray(body.blocked) && Array.isArray(body.done) && typeof body.hasMoreDone === 'boolean', ) boardContractFailures.add(!contractOk, { page }) check(response, { [`${page}: returns 200`]: () => httpOk, [`${page}: preserves the board contract`]: () => contractOk, [`${page}: emits Server-Timing`]: res => Boolean(res.headers['Server-Timing']), }) return contractOk ? body : null } export default function () { const firstPage = readBoardPage( `${BASE_URL}/api/v1/tasks/board?doneLimit=50`, 'initial', ) const hasDoneCursor = Boolean(firstPage?.hasMoreDone && firstPage.nextDoneCursor) const datasetOk = !REQUIRE_DONE_CURSOR || hasDoneCursor boardDatasetFailures.add(!datasetOk) check(firstPage, { 'acceptance dataset exposes a Done continuation cursor': () => datasetOk, }) if (hasDoneCursor) { readBoardPage( `${BASE_URL}/api/v1/tasks/board?doneLimit=50&doneCursor=${encodeURIComponent(firstPage.nextDoneCursor)}`, 'done', ) } sleep(0.75 + Math.random() * 0.5) }