From 3d144519e9ac37eb43a709750a605ce5455e7c3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Thu, 18 Jan 2024 13:58:33 +0900 Subject: [PATCH 1/5] =?UTF-8?q?Chore=20:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EA=B5=AC=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 3 +- test.js | 317 +++++++++++++++++++++++++++++++++++++++++++++++++++++ test.sh | 15 +++ 3 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 test.js create mode 100644 test.sh 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/test.js b/test.js new file mode 100644 index 0000000..614d21e --- /dev/null +++ b/test.js @@ -0,0 +1,317 @@ +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, 5)}@test.com`, + name: `User ${generateUUID().slice(1, 5)}`, + 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 acceptRequest = Math.random() > 0.2; + if (acceptRequest) { + 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, + }); + } else { + let rejectRequestResponse = http.post(`${BASE_URL}/members/friends/${requestId}/rejected`, null, { + headers: { + Authorization: `Basic ${encoding.b64encode(toUserAuth)}`, + } + }); + + check(rejectRequestResponse, { + 'rejectRequestResponse status is 200': (r) => r.status === 200, + }); + return; + } + } + + // 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" From 160a663e7987291728c46c42fd3d61742da8f33e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Thu, 18 Jan 2024 13:59:05 +0900 Subject: [PATCH 2/5] =?UTF-8?q?Chore=20:=20README=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6c8a1b2..74f617f 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,13 @@ Numble Challenge - Banking API ### 테스트 -- 테스트 할 API는 [API 문서](https://this-is-spear.github.io/hello-banking-api/src/main/resources/static/docs/index.html)에서 확인 할 수 있습니다. - 서버는 `run.sh` 를 실행하면 됩니다. -- +- 테스트는 `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 From 990b1fa2e89291dc10a3eab666f9317e745f5a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Thu, 18 Jan 2024 22:19:12 +0900 Subject: [PATCH 3/5] =?UTF-8?q?Chore=20:=20=EC=9C=A0=EB=8B=88=ED=81=AC=20?= =?UTF-8?q?=EA=B0=92=20=ED=94=BC=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=83=9D=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.js b/test.js index 614d21e..ec37db9 100644 --- a/test.js +++ b/test.js @@ -9,8 +9,8 @@ function setup() { // 사용자 등록 및 정보 저장한다. 이 떄 이름과 이메일은 임의 값을 부여한다. for (let i = 0; i < 10; i++) { let user = { - email: `${generateUUID().slice(1, 5)}@test.com`, - name: `User ${generateUUID().slice(1, 5)}`, + email: `${generateUUID().slice(1, 10)}@test.com`, + name: `User ${generateUUID().slice(1, 10)}`, password: `password${i}`, }; From b2cad942421e53a07de243b221996c555f62cd12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Thu, 18 Jan 2024 22:20:15 +0900 Subject: [PATCH 4/5] =?UTF-8?q?Chore=20:=20test.js=20=EC=B9=9C=EA=B5=AC=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=88=98=EB=9D=BD=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=EB=A7=8C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test.js | 32 +++++++++----------------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/test.js b/test.js index ec37db9..3766733 100644 --- a/test.js +++ b/test.js @@ -127,29 +127,15 @@ export default function () { return; } - // B는 A의 친구 신청을 수락하거나 거절. 수락하는 확률을 더 높여야 함. - let acceptRequest = Math.random() > 0.2; - if (acceptRequest) { - 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, - }); - } else { - let rejectRequestResponse = http.post(`${BASE_URL}/members/friends/${requestId}/rejected`, null, { - headers: { - Authorization: `Basic ${encoding.b64encode(toUserAuth)}`, - } - }); - - check(rejectRequestResponse, { - 'rejectRequestResponse status is 200': (r) => r.status === 200, - }); - 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는 자신의 계좌를 확인하고 임의로 선택 From d14f363f217ca3a7ab51a8586d77da2918009946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=84=8B=E1=85=B5=E1=84=80=E1=85=A5=E1=86=AB=E1=84=8E?= =?UTF-8?q?=E1=85=A1=E1=86=BC?= Date: Thu, 18 Jan 2024 22:31:17 +0900 Subject: [PATCH 5/5] =?UTF-8?q?Chore=20:=20=EC=8B=A4=ED=8C=A8=ED=95=9C?= =?UTF-8?q?=EB=8A=94=20=EC=83=81=ED=99=A9=20=EB=AC=B8=EC=84=9C=20=EA=B3=B5?= =?UTF-8?q?=EC=9C=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 74f617f..86bf401 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,12 @@ Numble Challenge - Banking API ### 테스트 - 서버는 `run.sh` 를 실행하면 됩니다. + +> 간혹 pinpoint-hbase 이 정상 실행하기 전에 pinpoint-collector 가 실행되어 apm 이 정상적으로 실행되지 않는 경우가 존재합니다. 이런 경우 collector를 재실행 해주세요. + - 테스트는 `test.sh` 를 실행하면 됩니다. + ### Development Environment - Back-End : Spring-Boot, Spring-Security, JPA, MySQL, Testcontainers