174 lines
5.4 KiB
JavaScript
174 lines
5.4 KiB
JavaScript
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)
|
|
}
|