add testcode

This commit is contained in:
ondal 2025-02-14 16:51:26 +09:00
parent d7ca5994b4
commit 2f672a8ea5
40 changed files with 1517 additions and 9 deletions

Binary file not shown.

Binary file not shown.

7
.idea/compiler.xml generated
View File

@ -8,11 +8,16 @@
<processorPath useClasspath="false"> <processorPath useClasspath="false">
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.36/5a30490a6e14977d97d9c73c924c1f1b5311ea95/lombok-1.18.36.jar" /> <entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/org.projectlombok/lombok/1.18.36/5a30490a6e14977d97d9c73c924c1f1b5311ea95/lombok-1.18.36.jar" />
</processorPath> </processorPath>
<module name="lifesub.recommend.test" />
<module name="lifesub.common.main" />
<module name="lifesub.mysub-infra.test" />
<module name="lifesub.mysub-infra.main" /> <module name="lifesub.mysub-infra.main" />
<module name="lifesub.common.test" />
<module name="lifesub.member.test" />
<module name="lifesub.mysub-biz.main" /> <module name="lifesub.mysub-biz.main" />
<module name="lifesub.recommend.main" /> <module name="lifesub.recommend.main" />
<module name="lifesub.common.main" />
<module name="lifesub.member.main" /> <module name="lifesub.member.main" />
<module name="lifesub.mysub-biz.test" />
</profile> </profile>
</annotationProcessing> </annotationProcessing>
<bytecodeTargetLevel target="17" /> <bytecodeTargetLevel target="17" />

View File

@ -18,21 +18,47 @@ subprojects {
repositories { repositories {
mavenCentral() mavenCentral()
} }
dependencies {
dependencies {
// Spring Boot Starters
implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-aop' // AOP:
// Utils
implementation 'com.google.code.gson:gson'
// Lombok // Lombok
compileOnly 'org.projectlombok:lombok' compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok'
// Test // Test Dependencies
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testImplementation 'org.mockito:mockito-core'
testImplementation 'org.mockito:mockito-junit-jupiter'
// Lombok for Tests
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
} }
// Test Configuration
sourceSets {
test {
java {
srcDirs = ['src/test/java']
}
}
}
test { test {
useJUnitPlatform() useJUnitPlatform()
include '**/*Test.class'
testLogging {
events "passed", "skipped", "failed"
}
} }
} }
@ -50,10 +76,15 @@ configure(subprojects.findAll { !it.name.endsWith('-biz') && it.name != 'common'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
// AOP:
implementation 'org.springframework.boot:spring-boot-starter-aop'
// Swagger // Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
// Test Containers
testImplementation 'org.testcontainers:postgresql'
// WebFlux for WebMvc Testing
implementation 'org.springframework.boot:spring-boot-starter-webflux'
} }
} }

View File

@ -0,0 +1,82 @@
!theme mono
title Common Module - Class Diagram
package "com.unicorn.lifesub.common" {
package "dto" {
class ApiResponse<T> {
-status: int
-message: String
-data: T
-timestamp: LocalDateTime
+ApiResponse(status: int, message: String, data: T)
+{static} success(data: T): ApiResponse<T>
+{static} error(errorCode: ErrorCode): ApiResponse<T>
}
class JwtTokenDTO {
-accessToken: String
-refreshToken: String
}
}
package "exception" {
class BusinessException {
-errorCode: ErrorCode
+BusinessException(errorCode: ErrorCode)
+getErrorCode(): ErrorCode
}
class InfraException {
-errorCode: ErrorCode
+InfraException(errorCode: ErrorCode)
+getErrorCode(): ErrorCode
}
enum ErrorCode {
INVALID_INPUT_VALUE(100, "Invalid input value")
INTERNAL_SERVER_ERROR(110, "Internal server error")
MEMBER_NOT_FOUND(200, "Member not found")
INVALID_CREDENTIALS(210, "Invalid credentials")
TOKEN_EXPIRED(220, "Token expired")
SIGNATURE_VERIFICATION_EXCEPTION(230, "서명 검증 실패")
ALGORITHM_MISMATCH_EXCEPTION(240, "알고리즘 불일치")
INVALID_CLAIM_EXCEPTION(250, "유효하지 않은 클레임")
SUBSCRIPTION_NOT_FOUND(300, "Subscription not found")
ALREADY_SUBSCRIBED(310, "Already subscribed to this service")
NO_SPENDING_DATA(400, "No spending data found")
NO_RECOMMENDATION_DATA(410, "추천 구독 카테고리 없음")
UNDIFINED_ERROR(0, "정의되지 않은 에러")
--
-status: int
-message: String
}
}
package "entity" {
abstract class BaseTimeEntity {
-createdAt: LocalDateTime
-updatedAt: LocalDateTime
}
}
package "aop" {
class LoggingAspect {
-gson: Gson
+logMethodStart(joinPoint: JoinPoint): void
+logMethodEnd(joinPoint: JoinPoint, result: Object): void
+logMethodException(joinPoint: JoinPoint, exception: Exception): void
-getArgumentString(args: Object[]): String
-getResultString(result: Object): String
}
}
package "config" {
class JpaConfig {
}
}
}
' Relationships
BusinessException --> ErrorCode
InfraException --> ErrorCode
LoggingAspect ..> ApiResponse : uses

View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta http-equiv="x-ua-compatible" content="IE=edge"/>
<title>Test results - CustomUserDetailsServiceTest</title>
<link href="../css/base-style.css" rel="stylesheet" type="text/css"/>
<link href="../css/style.css" rel="stylesheet" type="text/css"/>
<script src="../js/report.js" type="text/javascript"></script>
</head>
<body>
<div id="content">
<h1>CustomUserDetailsServiceTest</h1>
<div class="breadcrumbs">
<a href="../index.html">all</a> &gt;
<a href="../packages/com.unicorn.lifesub.member.test.unit.config.jwt.html">com.unicorn.lifesub.member.test.unit.config.jwt</a> &gt; CustomUserDetailsServiceTest</div>
<div id="summary">
<table>
<tr>
<td>
<div class="summaryGroup">
<table>
<tr>
<td>
<div class="infoBox" id="tests">
<div class="counter">3</div>
<p>tests</p>
</div>
</td>
<td>
<div class="infoBox" id="failures">
<div class="counter">0</div>
<p>failures</p>
</div>
</td>
<td>
<div class="infoBox" id="ignored">
<div class="counter">0</div>
<p>ignored</p>
</div>
</td>
<td>
<div class="infoBox" id="duration">
<div class="counter">1.064s</div>
<p>duration</p>
</div>
</td>
</tr>
</table>
</div>
</td>
<td>
<div class="infoBox success" id="successRate">
<div class="percent">100%</div>
<p>successful</p>
</div>
</td>
</tr>
</table>
</div>
<div id="tabs">
<ul class="tabLinks">
<li>
<a href="#tab0">Tests</a>
</li>
</ul>
<div id="tab0" class="tab">
<h2>Tests</h2>
<table>
<thead>
<tr>
<th>Test</th>
<th>Method name</th>
<th>Duration</th>
<th>Result</th>
</tr>
</thead>
<tr>
<td class="success">givenExistingUserId_whenLoadUser_thenReturnUserDetails</td>
<td class="success">givenExistingUserId_whenLoadUser_thenReturnUserDetails()</td>
<td class="success">0.008s</td>
<td class="success">passed</td>
</tr>
<tr>
<td class="success">givenNonExistentUserId_whenLoadUser_thenThrowException</td>
<td class="success">givenNonExistentUserId_whenLoadUser_thenThrowException()</td>
<td class="success">0.864s</td>
<td class="success">passed</td>
</tr>
<tr>
<td class="success">givenUserWithRoles_whenLoadUser_thenMapAuthoritiesCorrectly</td>
<td class="success">givenUserWithRoles_whenLoadUser_thenMapAuthoritiesCorrectly()</td>
<td class="success">0.192s</td>
<td class="success">passed</td>
</tr>
</table>
</div>
</div>
<div id="footer">
<p>
<div>
<label class="hidden" id="label-for-line-wrapping-toggle" for="line-wrapping-toggle">Wrap lines
<input id="line-wrapping-toggle" type="checkbox" autocomplete="off"/>
</label>
</div>Generated by
<a href="http://www.gradle.org">Gradle 8.4</a> at 2025. 2. 14. 오후 4:50:22</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,179 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
font-size: 12pt;
}
body, a, a:visited {
color: #303030;
}
#content {
padding-left: 50px;
padding-right: 50px;
padding-top: 30px;
padding-bottom: 30px;
}
#content h1 {
font-size: 160%;
margin-bottom: 10px;
}
#footer {
margin-top: 100px;
font-size: 80%;
white-space: nowrap;
}
#footer, #footer a {
color: #a0a0a0;
}
#line-wrapping-toggle {
vertical-align: middle;
}
#label-for-line-wrapping-toggle {
vertical-align: middle;
}
ul {
margin-left: 0;
}
h1, h2, h3 {
white-space: nowrap;
}
h2 {
font-size: 120%;
}
ul.tabLinks {
padding-left: 0;
padding-top: 10px;
padding-bottom: 10px;
overflow: auto;
min-width: 800px;
width: auto !important;
width: 800px;
}
ul.tabLinks li {
float: left;
height: 100%;
list-style: none;
padding-left: 10px;
padding-right: 10px;
padding-top: 5px;
padding-bottom: 5px;
margin-bottom: 0;
-moz-border-radius: 7px;
border-radius: 7px;
margin-right: 25px;
border: solid 1px #d4d4d4;
background-color: #f0f0f0;
}
ul.tabLinks li:hover {
background-color: #fafafa;
}
ul.tabLinks li.selected {
background-color: #c5f0f5;
border-color: #c5f0f5;
}
ul.tabLinks a {
font-size: 120%;
display: block;
outline: none;
text-decoration: none;
margin: 0;
padding: 0;
}
ul.tabLinks li h2 {
margin: 0;
padding: 0;
}
div.tab {
}
div.selected {
display: block;
}
div.deselected {
display: none;
}
div.tab table {
min-width: 350px;
width: auto !important;
width: 350px;
border-collapse: collapse;
}
div.tab th, div.tab table {
border-bottom: solid #d0d0d0 1px;
}
div.tab th {
text-align: left;
white-space: nowrap;
padding-left: 6em;
}
div.tab th:first-child {
padding-left: 0;
}
div.tab td {
white-space: nowrap;
padding-left: 6em;
padding-top: 5px;
padding-bottom: 5px;
}
div.tab td:first-child {
padding-left: 0;
}
div.tab td.numeric, div.tab th.numeric {
text-align: right;
}
span.code {
display: inline-block;
margin-top: 0em;
margin-bottom: 1em;
}
span.code pre {
font-size: 11pt;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 10px;
padding-right: 10px;
margin: 0;
background-color: #f7f7f7;
border: solid 1px #d0d0d0;
min-width: 700px;
width: auto !important;
width: 700px;
}
span.wrapped pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: break-all;
}
label.hidden {
display: none;
}

View File

@ -0,0 +1,84 @@
#summary {
margin-top: 30px;
margin-bottom: 40px;
}
#summary table {
border-collapse: collapse;
}
#summary td {
vertical-align: top;
}
.breadcrumbs, .breadcrumbs a {
color: #606060;
}
.infoBox {
width: 110px;
padding-top: 15px;
padding-bottom: 15px;
text-align: center;
}
.infoBox p {
margin: 0;
}
.counter, .percent {
font-size: 120%;
font-weight: bold;
margin-bottom: 8px;
}
#duration {
width: 125px;
}
#successRate, .summaryGroup {
border: solid 2px #d0d0d0;
-moz-border-radius: 10px;
border-radius: 10px;
}
#successRate {
width: 140px;
margin-left: 35px;
}
#successRate .percent {
font-size: 180%;
}
.success, .success a {
color: #008000;
}
div.success, #successRate.success {
background-color: #bbd9bb;
border-color: #008000;
}
.failures, .failures a {
color: #b60808;
}
.skipped, .skipped a {
color: #c09853;
}
div.failures, #successRate.failures {
background-color: #ecdada;
border-color: #b60808;
}
ul.linkList {
padding-left: 0;
}
ul.linkList li {
list-style: none;
margin-bottom: 5px;
}

View File

@ -0,0 +1,133 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta http-equiv="x-ua-compatible" content="IE=edge"/>
<title>Test results - Test Summary</title>
<link href="css/base-style.css" rel="stylesheet" type="text/css"/>
<link href="css/style.css" rel="stylesheet" type="text/css"/>
<script src="js/report.js" type="text/javascript"></script>
</head>
<body>
<div id="content">
<h1>Test Summary</h1>
<div id="summary">
<table>
<tr>
<td>
<div class="summaryGroup">
<table>
<tr>
<td>
<div class="infoBox" id="tests">
<div class="counter">3</div>
<p>tests</p>
</div>
</td>
<td>
<div class="infoBox" id="failures">
<div class="counter">0</div>
<p>failures</p>
</div>
</td>
<td>
<div class="infoBox" id="ignored">
<div class="counter">0</div>
<p>ignored</p>
</div>
</td>
<td>
<div class="infoBox" id="duration">
<div class="counter">1.064s</div>
<p>duration</p>
</div>
</td>
</tr>
</table>
</div>
</td>
<td>
<div class="infoBox success" id="successRate">
<div class="percent">100%</div>
<p>successful</p>
</div>
</td>
</tr>
</table>
</div>
<div id="tabs">
<ul class="tabLinks">
<li>
<a href="#tab0">Packages</a>
</li>
<li>
<a href="#tab1">Classes</a>
</li>
</ul>
<div id="tab0" class="tab">
<h2>Packages</h2>
<table>
<thead>
<tr>
<th>Package</th>
<th>Tests</th>
<th>Failures</th>
<th>Ignored</th>
<th>Duration</th>
<th>Success rate</th>
</tr>
</thead>
<tbody>
<tr>
<td class="success">
<a href="packages/com.unicorn.lifesub.member.test.unit.config.jwt.html">com.unicorn.lifesub.member.test.unit.config.jwt</a>
</td>
<td>3</td>
<td>0</td>
<td>0</td>
<td>1.064s</td>
<td class="success">100%</td>
</tr>
</tbody>
</table>
</div>
<div id="tab1" class="tab">
<h2>Classes</h2>
<table>
<thead>
<tr>
<th>Class</th>
<th>Tests</th>
<th>Failures</th>
<th>Ignored</th>
<th>Duration</th>
<th>Success rate</th>
</tr>
</thead>
<tbody>
<tr>
<td class="success">
<a href="classes/com.unicorn.lifesub.member.test.unit.config.jwt.CustomUserDetailsServiceTest.html">com.unicorn.lifesub.member.test.unit.config.jwt.CustomUserDetailsServiceTest</a>
</td>
<td>3</td>
<td>0</td>
<td>0</td>
<td>1.064s</td>
<td class="success">100%</td>
</tr>
</tbody>
</table>
</div>
</div>
<div id="footer">
<p>
<div>
<label class="hidden" id="label-for-line-wrapping-toggle" for="line-wrapping-toggle">Wrap lines
<input id="line-wrapping-toggle" type="checkbox" autocomplete="off"/>
</label>
</div>Generated by
<a href="http://www.gradle.org">Gradle 8.4</a> at 2025. 2. 14. 오후 4:50:22</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,194 @@
(function (window, document) {
"use strict";
var tabs = {};
function changeElementClass(element, classValue) {
if (element.getAttribute("className")) {
element.setAttribute("className", classValue);
} else {
element.setAttribute("class", classValue);
}
}
function getClassAttribute(element) {
if (element.getAttribute("className")) {
return element.getAttribute("className");
} else {
return element.getAttribute("class");
}
}
function addClass(element, classValue) {
changeElementClass(element, getClassAttribute(element) + " " + classValue);
}
function removeClass(element, classValue) {
changeElementClass(element, getClassAttribute(element).replace(classValue, ""));
}
function initTabs() {
var container = document.getElementById("tabs");
tabs.tabs = findTabs(container);
tabs.titles = findTitles(tabs.tabs);
tabs.headers = findHeaders(container);
tabs.select = select;
tabs.deselectAll = deselectAll;
tabs.select(0);
return true;
}
function getCheckBox() {
return document.getElementById("line-wrapping-toggle");
}
function getLabelForCheckBox() {
return document.getElementById("label-for-line-wrapping-toggle");
}
function findCodeBlocks() {
var spans = document.getElementById("tabs").getElementsByTagName("span");
var codeBlocks = [];
for (var i = 0; i < spans.length; ++i) {
if (spans[i].className.indexOf("code") >= 0) {
codeBlocks.push(spans[i]);
}
}
return codeBlocks;
}
function forAllCodeBlocks(operation) {
var codeBlocks = findCodeBlocks();
for (var i = 0; i < codeBlocks.length; ++i) {
operation(codeBlocks[i], "wrapped");
}
}
function toggleLineWrapping() {
var checkBox = getCheckBox();
if (checkBox.checked) {
forAllCodeBlocks(addClass);
} else {
forAllCodeBlocks(removeClass);
}
}
function initControls() {
if (findCodeBlocks().length > 0) {
var checkBox = getCheckBox();
var label = getLabelForCheckBox();
checkBox.onclick = toggleLineWrapping;
checkBox.checked = false;
removeClass(label, "hidden");
}
}
function switchTab() {
var id = this.id.substr(1);
for (var i = 0; i < tabs.tabs.length; i++) {
if (tabs.tabs[i].id === id) {
tabs.select(i);
break;
}
}
return false;
}
function select(i) {
this.deselectAll();
changeElementClass(this.tabs[i], "tab selected");
changeElementClass(this.headers[i], "selected");
while (this.headers[i].firstChild) {
this.headers[i].removeChild(this.headers[i].firstChild);
}
var h2 = document.createElement("H2");
h2.appendChild(document.createTextNode(this.titles[i]));
this.headers[i].appendChild(h2);
}
function deselectAll() {
for (var i = 0; i < this.tabs.length; i++) {
changeElementClass(this.tabs[i], "tab deselected");
changeElementClass(this.headers[i], "deselected");
while (this.headers[i].firstChild) {
this.headers[i].removeChild(this.headers[i].firstChild);
}
var a = document.createElement("A");
a.setAttribute("id", "ltab" + i);
a.setAttribute("href", "#tab" + i);
a.onclick = switchTab;
a.appendChild(document.createTextNode(this.titles[i]));
this.headers[i].appendChild(a);
}
}
function findTabs(container) {
return findChildElements(container, "DIV", "tab");
}
function findHeaders(container) {
var owner = findChildElements(container, "UL", "tabLinks");
return findChildElements(owner[0], "LI", null);
}
function findTitles(tabs) {
var titles = [];
for (var i = 0; i < tabs.length; i++) {
var tab = tabs[i];
var header = findChildElements(tab, "H2", null)[0];
header.parentNode.removeChild(header);
if (header.innerText) {
titles.push(header.innerText);
} else {
titles.push(header.textContent);
}
}
return titles;
}
function findChildElements(container, name, targetClass) {
var elements = [];
var children = container.childNodes;
for (var i = 0; i < children.length; i++) {
var child = children.item(i);
if (child.nodeType === 1 && child.nodeName === name) {
if (targetClass && child.className.indexOf(targetClass) < 0) {
continue;
}
elements.push(child);
}
}
return elements;
}
// Entry point.
window.onload = function() {
initTabs();
initControls();
};
} (window, window.document));

View File

@ -0,0 +1,103 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta http-equiv="x-ua-compatible" content="IE=edge"/>
<title>Test results - Package com.unicorn.lifesub.member.test.unit.config.jwt</title>
<link href="../css/base-style.css" rel="stylesheet" type="text/css"/>
<link href="../css/style.css" rel="stylesheet" type="text/css"/>
<script src="../js/report.js" type="text/javascript"></script>
</head>
<body>
<div id="content">
<h1>Package com.unicorn.lifesub.member.test.unit.config.jwt</h1>
<div class="breadcrumbs">
<a href="../index.html">all</a> &gt; com.unicorn.lifesub.member.test.unit.config.jwt</div>
<div id="summary">
<table>
<tr>
<td>
<div class="summaryGroup">
<table>
<tr>
<td>
<div class="infoBox" id="tests">
<div class="counter">3</div>
<p>tests</p>
</div>
</td>
<td>
<div class="infoBox" id="failures">
<div class="counter">0</div>
<p>failures</p>
</div>
</td>
<td>
<div class="infoBox" id="ignored">
<div class="counter">0</div>
<p>ignored</p>
</div>
</td>
<td>
<div class="infoBox" id="duration">
<div class="counter">1.064s</div>
<p>duration</p>
</div>
</td>
</tr>
</table>
</div>
</td>
<td>
<div class="infoBox success" id="successRate">
<div class="percent">100%</div>
<p>successful</p>
</div>
</td>
</tr>
</table>
</div>
<div id="tabs">
<ul class="tabLinks">
<li>
<a href="#tab0">Classes</a>
</li>
</ul>
<div id="tab0" class="tab">
<h2>Classes</h2>
<table>
<thread>
<tr>
<th>Class</th>
<th>Tests</th>
<th>Failures</th>
<th>Ignored</th>
<th>Duration</th>
<th>Success rate</th>
</tr>
</thread>
<tr>
<td class="success">
<a href="../classes/com.unicorn.lifesub.member.test.unit.config.jwt.CustomUserDetailsServiceTest.html">CustomUserDetailsServiceTest</a>
</td>
<td>3</td>
<td>0</td>
<td>0</td>
<td>1.064s</td>
<td class="success">100%</td>
</tr>
</table>
</div>
</div>
<div id="footer">
<p>
<div>
<label class="hidden" id="label-for-line-wrapping-toggle" for="line-wrapping-toggle">Wrap lines
<input id="line-wrapping-toggle" type="checkbox" autocomplete="off"/>
</label>
</div>Generated by
<a href="http://www.gradle.org">Gradle 8.4</a> at 2025. 2. 14. 오후 4:50:22</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="com.unicorn.lifesub.member.test.unit.config.jwt.CustomUserDetailsServiceTest" tests="3" skipped="0" failures="0" errors="0" timestamp="2025-02-14T07:50:21" hostname="ihaegyeong-ui-MacBookAir.local" time="1.069">
<properties/>
<testcase name="givenNonExistentUserId_whenLoadUser_thenThrowException" classname="com.unicorn.lifesub.member.test.unit.config.jwt.CustomUserDetailsServiceTest" time="0.864"/>
<testcase name="givenUserWithRoles_whenLoadUser_thenMapAuthoritiesCorrectly" classname="com.unicorn.lifesub.member.test.unit.config.jwt.CustomUserDetailsServiceTest" time="0.192"/>
<testcase name="givenExistingUserId_whenLoadUser_thenReturnUserDetails" classname="com.unicorn.lifesub.member.test.unit.config.jwt.CustomUserDetailsServiceTest" time="0.008"/>
<system-out><![CDATA[]]></system-out>
<system-err><![CDATA[]]></system-err>
</testsuite>

Binary file not shown.

Binary file not shown.

View File

@ -2,12 +2,14 @@ package com.unicorn.lifesub.member.dto;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
@Getter @Getter
@Setter
public class LoginRequest { public class LoginRequest {
@NotBlank(message = "사용자 ID는 필수입니다.") @NotBlank(message = "사용자 ID는 필수입니다.")
private String userId; private String userId;
@NotBlank(message = "비밀번호는 필수입니다.") @NotBlank(message = "비밀번호는 필수입니다.")
private String password; private String password;
} }

View File

@ -2,8 +2,10 @@ package com.unicorn.lifesub.member.dto;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
@Getter @Getter
@Setter
public class LogoutRequest { public class LogoutRequest {
@NotBlank(message = "사용자 ID는 필수입니다.") @NotBlank(message = "사용자 ID는 필수입니다.")
private String userId; private String userId;

View File

@ -0,0 +1,122 @@
package com.unicorn.lifesub.member.test.unit.config.jwt;
import com.unicorn.lifesub.member.config.jwt.CustomUserDetailsService;
import com.unicorn.lifesub.member.repository.entity.MemberEntity;
import com.unicorn.lifesub.member.repository.jpa.MemberRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.BDDMockito.given;
/**
* 사용자 상세 정보 서비스 테스트 클래스
* Spring Security의 UserDetailsService 구현체 검증
*/
@ExtendWith(MockitoExtension.class)
class CustomUserDetailsServiceTest {
@InjectMocks
private CustomUserDetailsService userDetailsService;
@Mock
private MemberRepository memberRepository;
// 테스트용 상수 정의
private static final String TEST_USER_ID = "testUser";
private static final String TEST_PASSWORD = "testPassword";
private static final String TEST_USER_NAME = "Test User";
/**
* 사용자 조회 성공 케이스 테스트
* 존재하는 사용자 ID로 조회 UserDetails 객체가 정상적으로 반환되는지 검증
*/
@Test
@DisplayName("givenExistingUserId_whenLoadUser_thenReturnUserDetails")
void givenExistingUserId_whenLoadUser_thenReturnUserDetails() {
// Given
MemberEntity memberEntity = createTestMemberEntity();
given(memberRepository.findByUserId(TEST_USER_ID)).willReturn(Optional.of(memberEntity));
// When
UserDetails userDetails = userDetailsService.loadUserByUsername(TEST_USER_ID);
// Then
assertThat(userDetails).isNotNull();
assertThat(userDetails.getUsername()).isEqualTo(TEST_USER_ID);
assertThat(userDetails.getPassword()).isEqualTo(TEST_PASSWORD);
assertThat(userDetails.getAuthorities()).hasSize(1);
}
/**
* 사용자 조회 실패 케이스 테스트
* 존재하지 않는 사용자 ID로 조회 적절한 예외가 발생하는지 검증
*/
@Test
@DisplayName("givenNonExistentUserId_whenLoadUser_thenThrowException")
void givenNonExistentUserId_whenLoadUser_thenThrowException() {
// Given
String nonExistentUserId = "nonexistent";
given(memberRepository.findByUserId(nonExistentUserId)).willReturn(Optional.empty());
// When & Then
assertThrows(UsernameNotFoundException.class, () ->
userDetailsService.loadUserByUsername(nonExistentUserId));
}
/**
* 사용자 권한 매핑 테스트
* 사용자의 역할이 Spring Security 권한으로 올바르게 매핑되는지 검증
*/
@Test
@DisplayName("givenUserWithRoles_whenLoadUser_thenMapAuthoritiesCorrectly")
void givenUserWithRoles_whenLoadUser_thenMapAuthoritiesCorrectly() {
// Given
MemberEntity memberEntity = createTestMemberEntityWithMultipleRoles();
given(memberRepository.findByUserId(TEST_USER_ID)).willReturn(Optional.of(memberEntity));
// When
UserDetails userDetails = userDetailsService.loadUserByUsername(TEST_USER_ID);
// Then
assertThat(userDetails.getAuthorities()).hasSize(2);
assertThat(userDetails.getAuthorities())
.extracting("authority")
.containsExactlyInAnyOrder("USER", "ADMIN");
}
// 테스트 헬퍼 메서드
private MemberEntity createTestMemberEntity() {
Set<String> roles = new HashSet<>();
roles.add("USER");
return MemberEntity.builder()
.userId(TEST_USER_ID)
.userName(TEST_USER_NAME)
.password(TEST_PASSWORD)
.roles(roles)
.build();
}
private MemberEntity createTestMemberEntityWithMultipleRoles() {
Set<String> roles = new HashSet<>();
roles.add("USER");
roles.add("ADMIN");
return MemberEntity.builder()
.userId(TEST_USER_ID)
.userName(TEST_USER_NAME)
.password(TEST_PASSWORD)
.roles(roles)
.build();
}
}

View File

@ -0,0 +1,148 @@
package com.unicorn.lifesub.member.test.unit.config.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.unicorn.lifesub.common.dto.JwtTokenDTO;
import com.unicorn.lifesub.common.exception.InfraException;
import com.unicorn.lifesub.member.config.jwt.JwtTokenProvider;
import com.unicorn.lifesub.member.repository.entity.MemberEntity;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* JWT 토큰 제공자 테스트 클래스
* 토큰 생성, 검증, 파싱 등의 기능을 검증
*/
class JwtTokenProviderTest {
private JwtTokenProvider jwtTokenProvider;
private static final String SECRET_KEY = "test-secret-key";
private static final long ACCESS_TOKEN_VALIDITY = 3600000;
private static final long REFRESH_TOKEN_VALIDITY = 86400000;
@BeforeEach
void setUp() {
jwtTokenProvider = new JwtTokenProvider(SECRET_KEY, ACCESS_TOKEN_VALIDITY, REFRESH_TOKEN_VALIDITY);
}
/**
* 토큰 생성 테스트
* 유효한 사용자 정보로 JWT 토큰이 정상적으로 생성되는지 검증
*/
@Test
@DisplayName("givenValidMember_whenCreateToken_thenSuccess")
void givenValidMember_whenCreateToken_thenSuccess() {
// Given
MemberEntity member = createTestMemberEntity();
Set<SimpleGrantedAuthority> authorities = Collections.singleton(
new SimpleGrantedAuthority("ROLE_USER"));
// When
JwtTokenDTO tokens = jwtTokenProvider.createToken(member, authorities);
// Then
assertThat(tokens).isNotNull();
assertThat(tokens.getAccessToken()).isNotNull();
assertThat(tokens.getRefreshToken()).isNotNull();
assertThat(jwtTokenProvider.validateToken(tokens.getAccessToken())).isEqualTo(1);
}
/**
* 토큰 검증 테스트
* 유효한 토큰과 유효하지 않은 토큰에 대한 검증이 정상적으로 동작하는지 확인
*/
@Test
@DisplayName("givenToken_whenValidate_thenSuccess")
void givenToken_whenValidate_thenSuccess() {
// Given
String token = createValidToken();
// When & Then
assertThat(jwtTokenProvider.validateToken(token)).isEqualTo(1);
}
/**
* 인증 정보 추출 테스트
* JWT 토큰에서 인증 정보가 정상적으로 추출되는지 검증
*/
@Test
@DisplayName("givenValidToken_whenGetAuthentication_thenSuccess")
void givenValidToken_whenGetAuthentication_thenSuccess() {
// Given
String token = createValidToken();
// When
Authentication authentication = jwtTokenProvider.getAuthentication(token);
// Then
assertThat(authentication).isNotNull();
assertThat(authentication.isAuthenticated()).isTrue();
assertThat(authentication.getAuthorities()).hasSize(1);
}
/**
* 토큰 추출 테스트
* HTTP 요청 헤더에서 토큰이 정상적으로 추출되는지 검증
*/
@Test
@DisplayName("givenRequest_whenResolveToken_thenSuccess")
void givenRequest_whenResolveToken_thenSuccess() {
// Given
HttpServletRequest request = mock(HttpServletRequest.class);
String token = "test-token";
when(request.getHeader("Authorization")).thenReturn("Bearer " + token);
// When
String resolvedToken = jwtTokenProvider.resolveToken(request);
// Then
assertThat(resolvedToken).isEqualTo(token);
}
/**
* 유효하지 않은 토큰 검증 테스트
* 잘못된 형식의 토큰에 대해 적절한 예외가 발생하는지 검증
*/
@Test
@DisplayName("givenInvalidToken_whenValidate_thenThrowException")
void givenInvalidToken_whenValidate_thenThrowException() {
// Given
String invalidToken = "invalid-token";
// When & Then
assertThrows(InfraException.class, () -> jwtTokenProvider.validateToken(invalidToken));
}
// 테스트 헬퍼 메서드
private MemberEntity createTestMemberEntity() {
Set<String> roles = new HashSet<>();
roles.add("USER");
return MemberEntity.builder()
.userId("testUser")
.userName("Test User")
.password("password")
.roles(roles)
.build();
}
private String createValidToken() {
Algorithm algorithm = Algorithm.HMAC512(SECRET_KEY);
return JWT.create()
.withSubject("testUser")
.withClaim("auth", Collections.singletonList("ROLE_USER"))
.sign(algorithm);
}
}

View File

@ -0,0 +1,141 @@
package com.unicorn.lifesub.member.test.unit.domain;
import com.unicorn.lifesub.member.domain.Member;
import com.unicorn.lifesub.member.repository.entity.MemberEntity;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.HashSet;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Member 도메인 객체 테스트 클래스
* 도메인 객체의 생성 엔티티 변환 로직을 검증
*/
class MemberTest {
// 테스트용 상수 정의
private static final String TEST_USER_ID = "testUser";
private static final String TEST_USER_NAME = "Test User";
private static final String TEST_PASSWORD = "testPassword";
/**
* Member 객체 생성 테스트
* Builder 패턴을 사용한 Member 객체 생성이 정상적으로 동작하는지 검증
*/
@Test
@DisplayName("givenMemberInfo_whenBuildMember_thenSuccess")
void givenMemberInfo_whenBuildMember_thenSuccess() {
// Given
Set<String> roles = new HashSet<>();
roles.add("USER");
// When
Member member = Member.builder()
.userId(TEST_USER_ID)
.userName(TEST_USER_NAME)
.password(TEST_PASSWORD)
.roles(roles)
.build();
// Then
assertThat(member).isNotNull();
assertThat(member.getUserId()).isEqualTo(TEST_USER_ID);
assertThat(member.getUserName()).isEqualTo(TEST_USER_NAME);
assertThat(member.getPassword()).isEqualTo(TEST_PASSWORD);
assertThat(member.getRoles()).containsExactly("USER");
}
/**
* Entity에서 Domain 객체로의 변환 테스트
* MemberEntity.toDomain() 메서드가 정상적으로 동작하는지 검증
*/
@Test
@DisplayName("givenMemberEntity_whenConvertToDomain_thenSuccess")
void givenMemberEntity_whenConvertToDomain_thenSuccess() {
// Given
MemberEntity entity = createTestMemberEntity();
// When
Member member = entity.toDomain();
// Then
assertThat(member).isNotNull();
assertThat(member.getUserId()).isEqualTo(TEST_USER_ID);
assertThat(member.getUserName()).isEqualTo(TEST_USER_NAME);
assertThat(member.getPassword()).isEqualTo(TEST_PASSWORD);
assertThat(member.getRoles()).containsExactly("USER");
}
/**
* Domain 객체에서 Entity로의 변환 테스트
* MemberEntity.fromDomain() 메서드가 정상적으로 동작하는지 검증
*/
@Test
@DisplayName("givenMemberDomain_whenConvertToEntity_thenSuccess")
void givenMemberDomain_whenConvertToEntity_thenSuccess() {
// Given
Member member = createTestMember();
// When
MemberEntity entity = MemberEntity.fromDomain(member);
// Then
assertThat(entity).isNotNull();
assertThat(entity.getUserId()).isEqualTo(TEST_USER_ID);
assertThat(entity.getUserName()).isEqualTo(TEST_USER_NAME);
assertThat(entity.getPassword()).isEqualTo(TEST_PASSWORD);
assertThat(entity.getRoles()).containsExactly("USER");
}
/**
* 다중 역할을 가진 Member 객체 생성 테스트
* 여러 역할을 가진 Member 객체가 정상적으로 생성되는지 검증
*/
@Test
@DisplayName("givenMultipleRoles_whenBuildMember_thenSuccess")
void givenMultipleRoles_whenBuildMember_thenSuccess() {
// Given
Set<String> roles = new HashSet<>();
roles.add("USER");
roles.add("ADMIN");
// When
Member member = Member.builder()
.userId(TEST_USER_ID)
.userName(TEST_USER_NAME)
.password(TEST_PASSWORD)
.roles(roles)
.build();
// Then
assertThat(member).isNotNull();
assertThat(member.getRoles()).hasSize(2);
assertThat(member.getRoles()).containsExactlyInAnyOrder("USER", "ADMIN");
}
// 테스트 헬퍼 메서드
private MemberEntity createTestMemberEntity() {
Set<String> roles = new HashSet<>();
roles.add("USER");
return MemberEntity.builder()
.userId(TEST_USER_ID)
.userName(TEST_USER_NAME)
.password(TEST_PASSWORD)
.roles(roles)
.build();
}
private Member createTestMember() {
Set<String> roles = new HashSet<>();
roles.add("USER");
return Member.builder()
.userId(TEST_USER_ID)
.userName(TEST_USER_NAME)
.password(TEST_PASSWORD)
.roles(roles)
.build();
}
}

View File

@ -0,0 +1,163 @@
package com.unicorn.lifesub.member.test.unit.service;
import com.unicorn.lifesub.common.dto.JwtTokenDTO;
import com.unicorn.lifesub.common.exception.BusinessException;
import com.unicorn.lifesub.common.exception.ErrorCode;
import com.unicorn.lifesub.common.exception.InfraException;
import com.unicorn.lifesub.member.config.jwt.JwtTokenProvider;
import com.unicorn.lifesub.member.dto.LoginRequest;
import com.unicorn.lifesub.member.dto.LogoutRequest;
import com.unicorn.lifesub.member.repository.entity.MemberEntity;
import com.unicorn.lifesub.member.repository.jpa.MemberRepository;
import com.unicorn.lifesub.member.service.MemberServiceImpl;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.when;
/**
* 멤버 서비스 테스트 클래스
* 주요 비즈니스 로직인 로그인/로그아웃 기능을 검증
*/
@ExtendWith(MockitoExtension.class)
class MemberServiceImplTest {
@InjectMocks
private MemberServiceImpl memberService;
@Mock
private MemberRepository memberRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private JwtTokenProvider jwtTokenProvider;
// 테스트용 상수 정의
private static final String TEST_USER_ID = "testUser";
private static final String TEST_PASSWORD = "testPassword";
private static final String TEST_USER_NAME = "Test User";
/**
* 로그인 성공 케이스 테스트
* 올바른 사용자 ID와 비밀번호로 로그인 JWT 토큰이 정상적으로 발급되는지 검증
*/
@Test
@DisplayName("givenValidCredentials_whenLogin_thenSuccess")
void givenValidCredentials_whenLogin_thenSuccess() {
// Given
LoginRequest request = new LoginRequest();
request.setUserId(TEST_USER_ID);
request.setPassword(TEST_PASSWORD);
MemberEntity memberEntity = createTestMemberEntity();
JwtTokenDTO expectedToken = createTestJwtTokenDTO();
given(memberRepository.findByUserId(TEST_USER_ID)).willReturn(Optional.of(memberEntity));
given(passwordEncoder.matches(TEST_PASSWORD, memberEntity.getPassword())).willReturn(true);
given(jwtTokenProvider.createToken(any(), any())).willReturn(expectedToken);
// When
JwtTokenDTO result = memberService.login(request);
// Then
assertThat(result).isNotNull();
assertThat(result.getAccessToken()).isEqualTo(expectedToken.getAccessToken());
assertThat(result.getRefreshToken()).isEqualTo(expectedToken.getRefreshToken());
}
/**
* 로그인 실패 케이스 테스트 - 사용자가 존재하지 않는 경우
* 존재하지 않는 사용자 ID로 로그인 시도 적절한 예외가 발생하는지 검증
*/
@Test
@DisplayName("givenNonExistentUser_whenLogin_thenThrowException")
void givenNonExistentUser_whenLogin_thenThrowException() {
// Given
LoginRequest request = new LoginRequest();
request.setUserId("nonexistent");
request.setPassword(TEST_PASSWORD);
when(memberRepository.findByUserId("nonexistent")).thenReturn(Optional.empty());
// When & Then
InfraException exception = assertThrows(InfraException.class,
() -> memberService.login(request));
assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.MEMBER_NOT_FOUND);
}
/**
* 로그인 실패 케이스 테스트 - 잘못된 비밀번호
* 올바른 사용자 ID와 잘못된 비밀번호로 로그인 시도 적절한 예외가 발생하는지 검증
*/
@Test
@DisplayName("givenInvalidPassword_whenLogin_thenThrowException")
void givenInvalidPassword_whenLogin_thenThrowException() {
// Given
LoginRequest request = new LoginRequest();
request.setUserId(TEST_USER_ID);
request.setPassword("wrongPassword");
MemberEntity memberEntity = createTestMemberEntity();
given(memberRepository.findByUserId(TEST_USER_ID)).willReturn(Optional.of(memberEntity));
given(passwordEncoder.matches("wrongPassword", memberEntity.getPassword())).willReturn(false);
// When & Then
BusinessException exception = assertThrows(BusinessException.class,
() -> memberService.login(request));
assertThat(exception.getErrorCode()).isEqualTo(ErrorCode.INVALID_CREDENTIALS);
}
/**
* 로그아웃 테스트
* 로그아웃 요청 정상적으로 처리되는지 검증
*/
@Test
@DisplayName("givenLogoutRequest_whenLogout_thenSuccess")
void givenLogoutRequest_whenLogout_thenSuccess() {
// Given
LogoutRequest request = new LogoutRequest();
request.setUserId(TEST_USER_ID);
// When
var response = memberService.logout(request);
// Then
assertThat(response).isNotNull();
assertThat(response.getMessage()).contains("로그아웃이 완료되었습니다");
}
// 테스트 헬퍼 메서드
private MemberEntity createTestMemberEntity() {
Set<String> roles = new HashSet<>();
roles.add("USER");
return MemberEntity.builder()
.userId(TEST_USER_ID)
.userName(TEST_USER_NAME)
.password(TEST_PASSWORD)
.roles(roles)
.build();
}
private JwtTokenDTO createTestJwtTokenDTO() {
return JwtTokenDTO.builder()
.accessToken("test-access-token")
.refreshToken("test-refresh-token")
.build();
}
}