import { BASE_URL, fetchStores, fetchUsers, generateRandomBase64String, getHeaders, login, parseIdToString } from "./common.js"; import {check, group, sleep} from 'k6'; import {randomIntBetween, randomItem} from 'https://jslib.k6.io/k6-utils/1.2.0/index.js'; import http from 'k6/http'; export const options = { scenarios: { user_reservation: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '3m', target: 500 }, { duration: '2m', target: 1000 }, { duration: '2m', target: 1500 }, { duration: '3m', target: 1500 }, { duration: '3m', target: 0 }, ] } }, thresholds: { http_req_duration: ['p(95)<3000'], http_req_failed: ['rate<0.15'], }, }; function randomDayBetween(startFrom, endAt) { const random = randomIntBetween(startFrom, endAt) const targetDate = new Date() targetDate.setDate(targetDate.getDate() + random) return targetDate.toISOString().split('T')[0]; } function countForFetchDetails(themeCount) { const random = Math.random() if (random < 0.5) { return Math.min(0, themeCount) } if (random < 0.75) { return Math.min(1, themeCount) } if (random < 0.9) { return Math.min(2, themeCount) } return Math.min(randomIntBetween(3, themeCount), themeCount) } function extractRandomThemeForFetchDetail(themes) { const count = countForFetchDetails(themes.length) const shuffled = [...themes].sort(() => Math.random() - 0.5) return shuffled.slice(0, count).map(t => t.id) } export function setup() { const users = fetchUsers() const stores = fetchStores() console.log(`회원 수 = ${users.length} 조회 완료`) console.log(`매장 수 = ${stores.length} 조회 완료`) return { users, stores } } export default function (data) { const { users, stores } = data; const user = randomItem(users) const accessToken = login(user.account, user.password, 'USER').accessToken let storeId = randomItem(stores).storeId if (!accessToken) { console.log(`로그인 실패: token=${accessToken}`) return } let targetDate let availableScheduleId, selectedThemeId, selectedThemeInfo, reservationId, totalAmount group(`매장=${storeId}, 날짜=${targetDate}의 일정 조회`, function () { let searchTrial = 0 let schedules while (searchTrial < 5) { storeId = randomItem(stores).storeId targetDate = randomDayBetween(1, 7) const params = getHeaders(accessToken, "/stores/${storeId}/schedules?date=${date}") const res = http.get(`${BASE_URL}/stores/${storeId}/schedules?date=${targetDate}`, params) const result = check(res, {'일정 조회 성공': (r) => r.status === 200}) if (result !== true) { continue } schedules = parseIdToString(res).data.schedules if (schedules && schedules.length > 0) { break } searchTrial++ sleep(10) } if (schedules.length <= 0) { console.log(`5회 시도에도 일정 조회 실패`) return; } group(`일부 테마는 상세 조회`, function () { const themesByStoreAndDate = schedules.map(s => s.theme) if (!themesByStoreAndDate && themesByStoreAndDate.length <= 0) { return } const randomThemesForFetchDetail = extractRandomThemeForFetchDetail(themesByStoreAndDate) randomThemesForFetchDetail.forEach(id => { http.get(`${BASE_URL}/themes/${id}`, getHeaders(accessToken, "/themes/${id}")) sleep(10) }) }) const availableSchedules = schedules.filter((s) => s.schedule.status === 'AVAILABLE') if (availableSchedules.length > 0) { const availableSchedule = randomItem(availableSchedules) availableScheduleId = availableSchedule.schedule.id selectedThemeId = availableSchedule.theme.id } }) if (!availableScheduleId) { return; } let isScheduleHeld = false group(`일정 Holding 및 테마 정보 조회 -> 예약 과정중 첫 페이지의 작업 완료`, function () { const holdRes = http.post(`${BASE_URL}/schedules/${availableScheduleId}/hold`, null, getHeaders(accessToken, "/schedules/${id}/hold")) const body = JSON.parse(holdRes.body) if (check(holdRes, {'일정 점유 성공': (r) => r.status === 200})) { const themeInfoRes = http.get(`${BASE_URL}/themes/${selectedThemeId}`, { tag: { name: "/themes/${id}"}}) selectedThemeInfo = parseIdToString(themeInfoRes).data isScheduleHeld = true } else { const errorCode = body.code const errorMessage = body.message console.log(`일정 점유 실패: code=${errorCode}, message=${errorMessage}`) } }) if (!isScheduleHeld || !selectedThemeInfo) { return } let isPendingReservationCreated = false group(`예약 정보 입력 페이지`, function () { let userName, userContact group(`회원 연락처 조회`, function () { const userContactRes = http.get(`${BASE_URL}/users/contact`, getHeaders(accessToken, "/users/contact")) if (!check(userContactRes, {'회원 연락처 조회 성공': (r) => r.status === 200})) { throw new Error("회원 연락처 조회 과정에서 예외 발생") } const responseBody = JSON.parse(userContactRes.body).data userName = responseBody.name userContact = responseBody.phone }) // 20%의 유저는 예약 정보 입력창에서 나감 => 배치의 자동 활성화 테스트 if (Math.random() <= 0.2) { return } sleep(20) group(`예약 정보 입력 및 Pending 예약 생성`, function () { const requirement = `${selectedThemeInfo.name}을 잘부탁드려요!` const participants = randomIntBetween(selectedThemeInfo.minParticipants, selectedThemeInfo.maxParticipants) totalAmount = Math.round(participants * selectedThemeInfo.price) const payload = JSON.stringify({ scheduleId: availableScheduleId, reserverName: userName, reserverContact: userContact, participantCount: participants, requirement: requirement }) const pendingReservationCreateRes = http.post(`${BASE_URL}/reservations/pending`, payload, getHeaders(accessToken, "/reservations/pending")) const responseBody = parseIdToString(pendingReservationCreateRes) if (pendingReservationCreateRes.status !== 200) { const errorCode = responseBody.code const errorMessage = responseBody.message throw new Error(`Pending 예약 중 실패: code=${errorCode}, message=${errorMessage}`) } reservationId = responseBody.data.id isPendingReservationCreated = true }) }) if (!isPendingReservationCreated) { return; } group(`결제 및 예약 확정`, function () { // 20%의 유저는 결제 화면에서 나감 => 배치의 자동 만료 처리 테스트 if (Math.random() <= 0.2) { return } const paymentKey = generateRandomBase64String(64) const orderId = generateRandomBase64String(25) const payload = JSON.stringify({ paymentKey: paymentKey, orderId: orderId, amount: totalAmount, }) let trial = 0 let isConfirmed = false while (trial < 2) { sleep(30) const confirmOrderRes = http.post(`${BASE_URL}/orders/${reservationId}/confirm`, payload, getHeaders(accessToken, "/orders/${reservationId}/confirm")) if (check(confirmOrderRes, {'예약 확정 성공': (r) => r.status === 200})) { isConfirmed = true break } const errorResponse = JSON.parse(confirmOrderRes.body) console.log(`예약 확정 실패: message=${errorResponse.message}`) trial = errorResponse.trial + 1 } // 예약이 확정되었으면 종료, 아니면 임시 확정 if (isConfirmed) { return } sleep(10) const temporalConfirmRes = http.post(`${BASE_URL}/reservations/${reservationId}/confirm`) if (check(temporalConfirmRes, {'임시 예약 확정 성공': (r) => r.status === 200})) { console.log("예약 확정 성공") return } throw new Error('임시 예약 확정 실패') }) }