diff --git a/Dockerfile b/Dockerfile index 7bf573a..1d859da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,8 @@ FROM openjdk:17-jdk ARG PINPOINT_VERSION ARG AGENT_ID ARG APP_NAME -ENV JAVA_OPTS="-javaagent:/pinpoint-agent/pinpoint-bootstrap-${PINPOINT_VERSION}.jar -Dpinpoint.agentId=${AGENT_ID} -Dpinpoint.applicationName=${APP_NAME}" +ENV JAVA_PINPOINT_OPTS="-javaagent:/pinpoint-agent/pinpoint-bootstrap-${PINPOINT_VERSION}.jar -Dpinpoint.agentId=${AGENT_ID} -Dpinpoint.applicationName=${APP_NAME}" +ENV JAVA_OPTS="${JAVA_PINPOINT_OPTS} -Duser.timezone=Asia/Seoul -Dfile.encoding=UTF-8 -Dsun.jnu.encoding=UTF-8" COPY ./build/libs/*SNAPSHOT.jar app.jar CMD echo 'sleep for initialze hbase' && sleep 30 && java -jar ${JAVA_OPTS} app.jar diff --git a/README.md b/README.md index 6c8a1b2..86bf401 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,17 @@ Numble Challenge - Banking API ### 테스트 -- 테스트 할 API는 [API 문서](https://this-is-spear.github.io/hello-banking-api/src/main/resources/static/docs/index.html)에서 확인 할 수 있습니다. - 서버는 `run.sh` 를 실행하면 됩니다. -- + +> 간혹 pinpoint-hbase 이 정상 실행하기 전에 pinpoint-collector 가 실행되어 apm 이 정상적으로 실행되지 않는 경우가 존재합니다. 이런 경우 collector를 재실행 해주세요. + +- 테스트는 `test.sh` 를 실행하면 됩니다. + ### Development Environment - Back-End : Spring-Boot, Spring-Security, JPA, MySQL, Testcontainers -- Fornt-End : Thymeleaf +- Front-End : Thymeleaf - Cloud : AWS - RDS - Infra : Docker - Document : Rest Docs diff --git a/test.js b/test.js new file mode 100644 index 0000000..3766733 --- /dev/null +++ b/test.js @@ -0,0 +1,303 @@ +import http from 'k6/http'; +import {check, sleep} from 'k6'; +import encoding from 'k6/encoding'; + +const users = []; +const BASE_URL = __ENV.BASE_URL || 'http://localhost'; + +function setup() { + // 사용자 등록 및 정보 저장한다. 이 떄 이름과 이메일은 임의 값을 부여한다. + for (let i = 0; i < 10; i++) { + let user = { + email: `${generateUUID().slice(1, 10)}@test.com`, + name: `User ${generateUUID().slice(1, 10)}`, + password: `password${i}`, + }; + + // 사용자 등록 + let auth = `${user.email}:${user.password}`; + let registerResponse = http.post(`${BASE_URL}/members/register`, JSON.stringify(user), + {headers: {'Content-Type': 'application/json'}}); + check(registerResponse, { + 'registerResponse status is 200': (r) => r.status === 200, + }); + + let meResponse = http.get(`${BASE_URL}/members/me`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(auth)}`, + } + },); + check(meResponse, { + 'meResponse status is 200': (r) => r.status === 200, + }); + // 정보 저장 + user.id = meResponse.json().id; + + // 자신의 계좌 등록 + let accountResponse = http.post(`${BASE_URL}/accounts`, null, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(auth)}`, + 'Idempotency-Key': generateUUID(), + } + }); + + check(accountResponse, { + 'accountResponse status is 200': (r) => r.status === 200, + }); + user.account = accountResponse.json().number; + + users.push(user); + } +} + +export default function () { + if (__ITER === 0) { + setup(); + } + + // 두 명의 사용자를 찾는다. + let randomUsers = getRandomUsers(users, 2); + let fromUser = randomUsers[0]; + let toUser = randomUsers[1]; + + let fromUserAuth = `${fromUser.email}:${fromUser.password}`; + let toUserAuth = `${toUser.email}:${toUser.password}`; + + let fromUserMeResponse = http.get(`${BASE_URL}/members/me`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(fromUserAuth)}`, + } + },); + + check(fromUserMeResponse, { + 'fromUserMeResponse status is 200': (r) => r.status === 200, + }); + + let toUserMeResponse = http.get(`${BASE_URL}/members/me`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(toUserAuth)}`, + } + },); + + check(toUserMeResponse, { + 'toUserMeResponse status is 200': (r) => r.status === 200, + }); + + // 두 명의 사용자가 친구 신청되어 있는지 확인한다. 응답 값은 friendResponses 배열에서 userId를 추출해 확인해야 한다. + let friendResponses = getFriends(fromUserAuth).map(request => request.userId).filter(userId => userId === toUser.id); + + // 친구 신청이 되어 있지 않다면 진행 + if (friendResponses.length === 0) { + // A가 B에게 친구 신청 + let friendRequestResponse = http.post(`${BASE_URL}/members/friends/${toUser.id}`, null, { + headers: { + Authorization: `Basic ${encoding.b64encode(fromUserAuth)}`, + } + }); + + check(friendRequestResponse, { + 'friendResponses status is 200': (r) => r.status === 200, + }); + + // B는 친구 신청을 확인 + let requests = getFriendRequest(toUserAuth).map(request => { + return { + requestId: request.requestId, + userId: request.fromUserId + } + }); + + let requestId; + + if (requests.length === 0) { + return; + } + + requests.forEach(request => { + if (request.userId === fromUser.id) { + requestId = request.requestId; + } + }); + + if (requestId === undefined) { + return; + } + + // B는 A의 친구 신청을 수락 + let approveRequestResponse = http.post(`${BASE_URL}/members/friends/${requestId}/approval`, null, { + headers: { + Authorization: `Basic ${encoding.b64encode(toUserAuth)}`, + } + }); + check(approveRequestResponse, { + 'approveRequestResponse status is 200': (r) => r.status === 200, + }); + } + + // A는 자신의 계좌를 확인하고 임의로 선택 + let accountsResponse = http.get(`${BASE_URL}/accounts`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(fromUserAuth)}`, + } + }); + check(accountsResponse, { + 'accountsResponse status is 200': (r) => r.status === 200, + }); + + let fromAccount = getRandomAccount(accountsResponse.json().map(account => account.number)); + + // A는 선택한 계좌에 돈을 10000원 입금 + let depositResponse = http.post(`${BASE_URL}/accounts/${fromAccount}/deposit`, JSON.stringify({amount: 10000}), { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(fromUserAuth)}`, + 'Idempotency-Key': generateUUID(), + } + }); + + check(depositResponse, { + 'depositResponse status is 200': (r) => r.status === 200, + }); + + // A는 계좌 잔액 확인 + let ABalanceResponse = http.get(`${BASE_URL}/accounts/${fromAccount}/history`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(fromUserAuth)}`, + 'Idempotency-Key': generateUUID(), + } + }); + + check(ABalanceResponse, { + 'ABalanceResponse status is 200': (r) => r.status === 200, + 'ABalanceResponse account amount greater than 10,000': (r) => r.json("balance.amount") >= 10000, + }); + + // 이체 대상 조회 + let transferTargetResponse = http.get(`${BASE_URL}/accounts/transfer/targets`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(fromUserAuth)}`, + } + }); + + check(transferTargetResponse, { + 'transferTargetResponse status is 200': (r) => r.status === 200, + }); + + let transferTargetAccount = getRandomAccount(transferTargetResponse.json("targets").map(request => { + return { + account: request.accountNumber, + email: request.email, + } + }).filter(request => request.email === toUser.email) + .map(request => request.account.number) + ); + + // 10000원 이체 + let transferResponse = http.post(`${BASE_URL}/accounts/${fromAccount}/transfer`, JSON.stringify({ + amount: 10000, + toAccountNumber: transferTargetAccount + }), { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(fromUserAuth)}`, + 'Idempotency-Key': generateUUID() + } + }); + + check(transferResponse, { + 'transferResponse status is 200': (r) => r.status === 200, + }); + + // B는 계좌 잔액 확인 + let BBalanceResponse = http.get(`${BASE_URL}/accounts/${transferTargetAccount}/history`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(toUserAuth)}`, + 'Idempotency-Key': generateUUID() + } + }); + + check(BBalanceResponse, { + 'BBalanceResponse status is 200': (r) => r.status === 200, + 'BBalanceResponse account amount greater than 10,000': (r) => r.json("balance.amount") >= 10000, + }); + + // B는 10000원 출금 + let withdrawResponse = http.post(`${BASE_URL}/accounts/${transferTargetAccount}/withdraw`, JSON.stringify({amount: 10000}), { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(toUserAuth)}`, + 'Idempotency-Key': generateUUID() + } + }); + check(withdrawResponse, { + 'withdrawResponse status is 200': (r) => r.status === 200, + }); + + // 간격을 두고 실행 + sleep(1); +} + + +function getFriends(auth) { + let friendsResponse = http.get(`${BASE_URL}/members/friends`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(auth)}`, + } + }); + check(friendsResponse, { + 'friendsResponse status is 200': (r) => r.status === 200, + }); + + return friendsResponse.json().friendResponses; +} + +function getFriendRequest(auth) { + let request = {}; + let friendRequestResponse = http.get(`${BASE_URL}/members/friends/requests`, { + headers: { + 'Content-Type': 'application/json', + Authorization: `Basic ${encoding.b64encode(auth)}`, + } + }); + check(friendRequestResponse, { + 'friendRequestResponse status is 200': (r) => r.status === 200, + }); + + // 응답 값은 friendResponses 배열에서 user id와 requestId를 추출해 확인해야 한다. + return friendRequestResponse.json().askedFriendResponses; +} + + +// 랜덤으로 n명의 사용자 선택 +function getRandomUsers(users, n) { + let randomUsers = []; + while (randomUsers.length < n) { + let user = users[Math.floor(Math.random() * users.length)]; + if (!randomUsers.includes(user)) { + randomUsers.push(user); + } + } + return randomUsers; +} + +// 랜덤으로 계좌 선택 +function getRandomAccount(accounts) { + return accounts[Math.floor(Math.random() * accounts.length)]; +} + +function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, + v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..e702883 --- /dev/null +++ b/test.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +script="test.js" + +# 사용자에게 가상 사용자 수와 테스트 기간 입력 받기 +read -r -p "Enter the number of virtual users (vuser): " vuser +read -r -p "Enter the test duration (e.g., 30s, 5m): " duration + +# k6 실행 명령어 +base_url="http://localhost" +k6_command="k6 run -e BASE_URL=$base_url -u $vuser -d $duration $script" + +# k6 실행 +echo "Running $script with $vuser virtual users for $duration..." +eval "$k6_command"