From a83f68e20ed0875248f2d6e22f7075cb4d1b0916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Tue, 28 Jan 2025 21:05:21 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[FIX]=20=ED=85=8C=EC=9D=B4=EB=B8=94=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EA=B0=80=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ParliamentaryTimeBoxRepository.java | 2 +- .../ParliamentaryControllerTest.java | 16 +++--- .../ParliamentaryServiceTest.java | 53 +++++++++---------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java b/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java index c641c242..a2ebb2d9 100644 --- a/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java +++ b/src/main/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepository.java @@ -28,7 +28,7 @@ default ParliamentaryTimeBoxes findTableTimeBoxes(ParliamentaryTable table) { } @Query("DELETE FROM ParliamentaryTimeBox ptb WHERE ptb IN :timeBoxes") - @Modifying(clearAutomatically = true) + @Modifying(clearAutomatically = true, flushAutomatically = true) @Transactional void deleteAll(List timeBoxes); } diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index 2a13970a..67e34e9b 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java @@ -80,14 +80,12 @@ class UpdateTable { void 의회식_토론_테이블을_업데이트한다() { Member bito = memberGenerator.generate("비토"); ParliamentaryTable bitoTable = tableGenerator.generate(bito); - TableInfoCreateRequest renewTableInfo = new TableInfoCreateRequest("비토 테이블", "주제"); - List renewTimeBoxes = List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) - ); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( - renewTableInfo, - renewTimeBoxes + new TableInfoCreateRequest("비토 테이블", "주제"), + List.of( + new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + ) ); ParliamentaryTableResponse response = given() @@ -101,8 +99,8 @@ class UpdateTable { assertAll( () -> assertThat(response.id()).isEqualTo(bitoTable.getId()), - () -> assertThat(response.info().name()).isEqualTo(renewTableInfo.name()), - () -> assertThat(response.table()).hasSize(renewTimeBoxes.size()) + () -> assertThat(response.info().name()).isEqualTo(renewTableRequest.info().name()), + () -> assertThat(response.table()).hasSize(renewTableRequest.table().size()) ); } } diff --git a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java index 89196065..1c7fc208 100644 --- a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java +++ b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java @@ -33,14 +33,12 @@ class Save { @Test void 의회식_토론_테이블을_생성한다() { Member chan = memberGenerator.generate("커찬"); - TableInfoCreateRequest requestTableInfo = new TableInfoCreateRequest("커찬의 테이블", "주제"); - List requestTimeBoxes = List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) - ); ParliamentaryTableCreateRequest chanTableRequest = new ParliamentaryTableCreateRequest( - requestTableInfo, - requestTimeBoxes + new TableInfoCreateRequest("커찬의 테이블", "주제"), + List.of( + new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + ) ); ParliamentaryTableResponse savedTableResponse = parliamentaryService.save(chanTableRequest, chan); @@ -48,8 +46,8 @@ class Save { List foundTimeBoxes = timeBoxRepository.findAllByParliamentaryTable(foundTable.get()); assertAll( - () -> assertThat(foundTable.get().getName()).isEqualTo(requestTableInfo.name()), - () -> assertThat(foundTimeBoxes).hasSize(requestTimeBoxes.size()) + () -> assertThat(foundTable.get().getName()).isEqualTo(chanTableRequest.info().name()), + () -> assertThat(foundTimeBoxes).hasSize(chanTableRequest.table().size()) ); } } @@ -92,23 +90,24 @@ class UpdateTable { void 의회식_토론_테이블을_수정한다() { Member chan = memberGenerator.generate("커찬"); ParliamentaryTable chanTable = tableGenerator.generate(chan); - TableInfoCreateRequest renewTableInfo = new TableInfoCreateRequest("커찬 테이블", "주제"); - List renewTimeBoxes = List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) - ); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( - renewTableInfo, - renewTimeBoxes + new TableInfoCreateRequest("커찬의 테이블", "주제"), + List.of( + new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + ) ); - ParliamentaryTableResponse updatedTable = parliamentaryService.updateTable(renewTableRequest, - chanTable.getId(), chan); + parliamentaryService.updateTable(renewTableRequest, chanTable.getId(), chan); + + Optional updatedTable = parliamentaryTableRepository.findById(chanTable.getId()); + List updatedTimeBoxes = timeBoxRepository.findAllByParliamentaryTable( + updatedTable.get()); assertAll( - () -> assertThat(updatedTable.id()).isEqualTo(chanTable.getId()), - () -> assertThat(updatedTable.info().name()).isEqualTo(renewTableInfo.name()), - () -> assertThat(updatedTable.table()).hasSize(renewTimeBoxes.size()) + () -> assertThat(updatedTable.get().getId()).isEqualTo(chanTable.getId()), + () -> assertThat(updatedTable.get().getName()).isEqualTo(renewTableRequest.info().name()), + () -> assertThat(updatedTimeBoxes).hasSize(renewTableRequest.table().size()) ); } @@ -118,14 +117,12 @@ class UpdateTable { Member coli = memberGenerator.generate("콜리"); ParliamentaryTable chanTable = tableGenerator.generate(chan); long chanTableId = chanTable.getId(); - TableInfoCreateRequest renewTableInfo = new TableInfoCreateRequest("새로운 테이블", "주제"); - List renewTimeBoxes = List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) - ); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( - renewTableInfo, - renewTimeBoxes + new TableInfoCreateRequest("새로운 테이블", "주제"), + List.of( + new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + ) ); assertThatThrownBy(() -> parliamentaryService.updateTable(renewTableRequest, chanTableId, coli)) From ac4800a6679a7f516e575d9c652cf2c94136eea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Thu, 30 Jan 2025 01:40:50 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[FEAT]=20=EB=B0=98=ED=99=98=EA=B0=92?= =?UTF-8?q?=EC=97=90=20=EC=A2=85=EC=86=8C=EB=A6=AC=20flag=EA=B0=92=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../parliamentary/ParliamentaryTable.java | 18 +++++++++++-- .../ParliamentaryTableCreateRequest.java | 2 +- .../request/TableInfoCreateRequest.java | 9 ++++--- .../response/TableInfoResponse.java | 9 +++++-- src/main/resources/application-local.yml | 2 +- .../member/MemberControllerTest.java | 4 +-- .../ParliamentaryControllerTest.java | 4 +-- .../ParliamentaryDocumentTest.java | 25 ++++++++++++----- .../parliamentary/ParliamentaryTableTest.java | 10 +++---- .../ParliamentaryTimeBoxesTest.java | 8 +++--- .../fixture/ParliamentaryTableGenerator.java | 9 ++++++- .../service/member/MemberServiceTest.java | 8 ++++-- .../ParliamentaryServiceTest.java | 27 +++++++------------ 13 files changed, 86 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java index 5304647e..e97dbd0e 100644 --- a/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java +++ b/src/main/java/com/debatetimer/domain/parliamentary/ParliamentaryTable.java @@ -39,15 +39,27 @@ public class ParliamentaryTable { @NotNull private String agenda; - @NotNull private int duration; - public ParliamentaryTable(Member member, String name, String agenda, int duration) { + private boolean warningBell; + + private boolean finishBell; + + public ParliamentaryTable( + Member member, + String name, + String agenda, + int duration, + boolean warningBell, + boolean finishBell + ) { validate(name, duration); this.member = member; this.name = name; this.agenda = agenda; this.duration = duration; + this.warningBell = warningBell; + this.finishBell = finishBell; } private void validate(String name, int duration) { @@ -66,6 +78,8 @@ public void update(ParliamentaryTable renewTable) { this.name = renewTable.getName(); this.agenda = renewTable.getAgenda(); this.duration = renewTable.getDuration(); + this.warningBell = renewTable.isWarningBell(); + this.finishBell = renewTable.isFinishBell(); } public boolean isOwner(long memberId) { diff --git a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java index 28bd6722..5d6b9f9e 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/ParliamentaryTableCreateRequest.java @@ -10,7 +10,7 @@ public record ParliamentaryTableCreateRequest(TableInfoCreateRequest info, List table) { public ParliamentaryTable toTable(Member member) { - return info.toTable(member, sumOfTime()); + return info.toTable(member, sumOfTime(), info.warningBell(), info().finishBell()); } private int sumOfTime() { diff --git a/src/main/java/com/debatetimer/dto/parliamentary/request/TableInfoCreateRequest.java b/src/main/java/com/debatetimer/dto/parliamentary/request/TableInfoCreateRequest.java index dceed3df..390af80d 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/request/TableInfoCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/request/TableInfoCreateRequest.java @@ -10,10 +10,13 @@ public record TableInfoCreateRequest( String name, @NotNull - String agenda + String agenda, + + boolean warningBell, + boolean finishBell ) { - public ParliamentaryTable toTable(Member member, int duration) { - return new ParliamentaryTable(member, name, agenda, duration); + public ParliamentaryTable toTable(Member member, int duration, boolean warningBell, boolean finishBell) { + return new ParliamentaryTable(member, name, agenda, duration, warningBell, finishBell); } } diff --git a/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java b/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java index e1369eb5..615cf99b 100644 --- a/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java +++ b/src/main/java/com/debatetimer/dto/parliamentary/response/TableInfoResponse.java @@ -2,9 +2,14 @@ import com.debatetimer.domain.parliamentary.ParliamentaryTable; -public record TableInfoResponse(String name, String agenda) { +public record TableInfoResponse(String name, String agenda, boolean warningBell, boolean finishBell) { public TableInfoResponse(ParliamentaryTable parliamentaryTable) { - this(parliamentaryTable.getName(), parliamentaryTable.getAgenda()); + this( + parliamentaryTable.getName(), + parliamentaryTable.getAgenda(), + parliamentaryTable.isWarningBell(), + parliamentaryTable.isFinishBell() + ); } } diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 751b8a26..6c0b99ea 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -18,4 +18,4 @@ spring: defer-datasource-initialization: true cors: - origin: * + origin: '*' diff --git a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java index a0251731..406d3c70 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java @@ -38,8 +38,8 @@ class GetTables { @Test void 회원의_전체_토론_시간표를_조회한다() { Member member = memberRepository.save(new Member("커찬")); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", 1800)); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", 1900)); + parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", 1800, false, false)); + parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", 1900, false, false)); TableResponses response = given() .contentType(ContentType.JSON) diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index 67e34e9b..1c8e7040 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java @@ -26,7 +26,7 @@ class Save { void 의회식_테이블을_생성한다() { Member bito = memberGenerator.generate("비토"); ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블", "주제"), + new TableInfoCreateRequest("비토 테이블", "주제", true, true), List.of( new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) @@ -81,7 +81,7 @@ class UpdateTable { Member bito = memberGenerator.generate("비토"); ParliamentaryTable bitoTable = tableGenerator.generate(bito); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블", "주제"), + new TableInfoCreateRequest("비토 테이블", "주제", true, true), List.of( new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java index 2003e00b..4b41a071 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; +import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; import static org.springframework.restdocs.payload.JsonFieldType.OBJECT; import static org.springframework.restdocs.payload.JsonFieldType.STRING; @@ -48,6 +49,8 @@ class Save { fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), fieldWithPath("info.name").type(STRING).description("테이블 이름"), fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), fieldWithPath("table[].stance").type(STRING).description("입장"), fieldWithPath("table[].type").type(STRING).description("발언 유형"), @@ -61,6 +64,8 @@ class Save { fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), fieldWithPath("info.name").type(STRING).description("테이블 이름"), fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), fieldWithPath("table[].stance").type(STRING).description("입장"), fieldWithPath("table[].type").type(STRING).description("발언 유형"), @@ -71,7 +76,7 @@ class Save { @Test void 의회식_테이블_생성_성공() { ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블 1", "토론 주제"), + new TableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), List.of( new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) @@ -79,7 +84,7 @@ class Save { ); ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, - new TableInfoResponse("비토 테이블 1", "토론 주제"), + new TableInfoResponse("비토 테이블 1", "토론 주제", true, true), List.of( new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 3, 1), new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 3, 1) @@ -113,7 +118,7 @@ class Save { @ParameterizedTest void 의회식_테이블_생성_실패(ClientErrorCode errorCode) { ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블 1", "토론 주제"), + new TableInfoCreateRequest("비토 테이블 1", "토론 주제", true, true), List.of( new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) @@ -154,6 +159,8 @@ class GetTable { fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), fieldWithPath("info.name").type(STRING).description("테이블 이름"), fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), fieldWithPath("table[].stance").type(STRING).description("입장"), fieldWithPath("table[].type").type(STRING).description("발언 유형"), @@ -167,7 +174,7 @@ class GetTable { long tableId = 5L; ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, - new TableInfoResponse("비토 테이블 1", "토론 주제"), + new TableInfoResponse("비토 테이블 1", "토론 주제", true, true), List.of( new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 3, 1), new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 3, 1) @@ -225,6 +232,8 @@ class UpdateTable { fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), fieldWithPath("info.name").type(STRING).description("테이블 이름"), fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), fieldWithPath("table[].stance").type(STRING).description("입장"), fieldWithPath("table[].type").type(STRING).description("발언 유형"), @@ -238,6 +247,8 @@ class UpdateTable { fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), fieldWithPath("info.name").type(STRING).description("테이블 이름"), fieldWithPath("info.agenda").type(STRING).description("토론 주제"), + fieldWithPath("info.warningBell").type(BOOLEAN).description("30초 종소리 유무"), + fieldWithPath("info.finishBell").type(BOOLEAN).description("발언 종료 종소리 유무"), fieldWithPath("table").type(ARRAY).description("토론 테이블 구성"), fieldWithPath("table[].stance").type(STRING).description("입장"), fieldWithPath("table[].type").type(STRING).description("발언 유형"), @@ -250,7 +261,7 @@ class UpdateTable { long memberId = 4L; long tableId = 5L; ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블 2", "토론 주제 2"), + new TableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), List.of( new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 300, 1), new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 300, 1) @@ -258,7 +269,7 @@ class UpdateTable { ); ParliamentaryTableResponse response = new ParliamentaryTableResponse( 5L, - new TableInfoResponse("비토 테이블 2", "토론 주제 2"), + new TableInfoResponse("비토 테이블 2", "토론 주제 2", true, true), List.of( new TimeBoxResponse(Stance.PROS, BoxType.OPENING, 300, 1), new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 300, 1) @@ -296,7 +307,7 @@ class UpdateTable { long memberId = 4L; long tableId = 5L; ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("비토 테이블 2", "토론 주제 2"), + new TableInfoCreateRequest("비토 테이블 2", "토론 주제 2", true, true), List.of( new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 300, 1), new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 300, 1) diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java index a3521064..b79cc05b 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java @@ -19,7 +19,7 @@ class Validate { @ValueSource(strings = {"a bc가다9", "가0나 다ab"}) void 테이블_이름은_영문과_한글_숫자_띄어쓰기만_가능하다(String name) { Member member = new Member("member"); - assertThatCode(() -> new ParliamentaryTable(member, name, "agenda", 10)) + assertThatCode(() -> new ParliamentaryTable(member, name, "agenda", 10, true, true)) .doesNotThrowAnyException(); } @@ -27,7 +27,7 @@ class Validate { @ValueSource(ints = {0, ParliamentaryTable.NAME_MAX_LENGTH + 1}) void 테이블_이름은_정해진_길이_이내여야_한다(int length) { Member member = new Member("member"); - assertThatThrownBy(() -> new ParliamentaryTable(member, "f".repeat(length), "agenda", 10)) + assertThatThrownBy(() -> new ParliamentaryTable(member, "f".repeat(length), "agenda", 10, true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); } @@ -36,7 +36,7 @@ class Validate { @ValueSource(strings = {"", "\t", "\n"}) void 테이블_이름은_적어도_한_자_있어야_한다(String name) { Member member = new Member("member"); - assertThatThrownBy(() -> new ParliamentaryTable(member, name, "agenda", 10)) + assertThatThrownBy(() -> new ParliamentaryTable(member, name, "agenda", 10, true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); } @@ -45,7 +45,7 @@ class Validate { @ValueSource(strings = {"abc@", "가나다*", "abc\tde"}) void 허용된_글자_이외의_문자는_불가능하다(String name) { Member member = new Member("member"); - assertThatThrownBy(() -> new ParliamentaryTable(member, name, "agenda", 10)) + assertThatThrownBy(() -> new ParliamentaryTable(member, name, "agenda", 10, true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_FORM.getMessage()); } @@ -54,7 +54,7 @@ class Validate { @ValueSource(ints = {0, -1, -60}) void 테이블_시간은_양수만_가능하다(int duration) { Member member = new Member("member"); - assertThatThrownBy(() -> new ParliamentaryTable(member, "name", "agenda", duration)) + assertThatThrownBy(() -> new ParliamentaryTable(member, "name", "agenda", duration, true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_TIME.getMessage()); } diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java index 9376f493..eabe945d 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java @@ -19,9 +19,11 @@ class SortedBySequence { @Test void 타임박스의_순서에_따라_정렬된다() { Member member = new Member("콜리"); - ParliamentaryTable testTable = new ParliamentaryTable(member, "토론 테이블", "주제", 1800); - ParliamentaryTimeBox firstBox = new ParliamentaryTimeBox(testTable, 1, Stance.PROS, BoxType.OPENING, 300, 1); - ParliamentaryTimeBox secondBox = new ParliamentaryTimeBox(testTable, 2, Stance.PROS, BoxType.OPENING, 300, 1); + ParliamentaryTable testTable = new ParliamentaryTable(member, "토론 테이블", "주제", 1800, true, true); + ParliamentaryTimeBox firstBox = new ParliamentaryTimeBox(testTable, 1, Stance.PROS, BoxType.OPENING, 300, + 1); + ParliamentaryTimeBox secondBox = new ParliamentaryTimeBox(testTable, 2, Stance.PROS, BoxType.OPENING, 300, + 1); List timeBoxes = new ArrayList<>(Arrays.asList(secondBox, firstBox)); ParliamentaryTimeBoxes actual = new ParliamentaryTimeBoxes(timeBoxes); diff --git a/src/test/java/com/debatetimer/fixture/ParliamentaryTableGenerator.java b/src/test/java/com/debatetimer/fixture/ParliamentaryTableGenerator.java index cbb0340f..0f0aba43 100644 --- a/src/test/java/com/debatetimer/fixture/ParliamentaryTableGenerator.java +++ b/src/test/java/com/debatetimer/fixture/ParliamentaryTableGenerator.java @@ -15,7 +15,14 @@ public ParliamentaryTableGenerator(ParliamentaryTableRepository parliamentaryTab } public ParliamentaryTable generate(Member member) { - ParliamentaryTable table = new ParliamentaryTable(member, "토론 테이블", "주제", 1800); + ParliamentaryTable table = new ParliamentaryTable( + member, + "토론 테이블", + "주제", + 1800, + false, + false + ); return parliamentaryTableRepository.save(table); } } diff --git a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java index be13f55e..0282f2e9 100644 --- a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java +++ b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java @@ -52,8 +52,12 @@ class GetTables { @Test void 회원의_전체_토론_시간표를_조회한다() { Member member = memberRepository.save(new Member("커찬")); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", 1800)); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", 1900)); + parliamentaryTableRepository.save(new ParliamentaryTable( + member, "토론 시간표 A", "주제", 1800, true, true + )); + parliamentaryTableRepository.save(new ParliamentaryTable( + member, "토론 시간표 B", "주제", 1900, true, true + )); TableResponses response = memberService.getTables(member.getId()); diff --git a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java index 1c7fc208..3c8345df 100644 --- a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java +++ b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java @@ -34,12 +34,9 @@ class Save { void 의회식_토론_테이블을_생성한다() { Member chan = memberGenerator.generate("커찬"); ParliamentaryTableCreateRequest chanTableRequest = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("커찬의 테이블", "주제"), - List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) - ) - ); + new TableInfoCreateRequest("커찬의 테이블", "주제", true, true), + List.of(new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1))); ParliamentaryTableResponse savedTableResponse = parliamentaryService.save(chanTableRequest, chan); Optional foundTable = parliamentaryTableRepository.findById(savedTableResponse.id()); @@ -91,12 +88,9 @@ class UpdateTable { Member chan = memberGenerator.generate("커찬"); ParliamentaryTable chanTable = tableGenerator.generate(chan); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("커찬의 테이블", "주제"), - List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) - ) - ); + new TableInfoCreateRequest("커찬의 테이블", "주제", true, true), + List.of(new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1))); parliamentaryService.updateTable(renewTableRequest, chanTable.getId(), chan); @@ -118,12 +112,9 @@ class UpdateTable { ParliamentaryTable chanTable = tableGenerator.generate(chan); long chanTableId = chanTable.getId(); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( - new TableInfoCreateRequest("새로운 테이블", "주제"), - List.of( - new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), - new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) - ) - ); + new TableInfoCreateRequest("새로운 테이블", "주제", true, true), + List.of(new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1))); assertThatThrownBy(() -> parliamentaryService.updateTable(renewTableRequest, chanTableId, coli)) .isInstanceOf(DTClientErrorException.class) From 743ea57b8454959bba2c386be8b7c747500b96d8 Mon Sep 17 00:00:00 2001 From: SANGHUN OH <121424793+unifolio0@users.noreply.github.com> Date: Sat, 1 Feb 2025 20:39:42 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[FEAT]=20=EA=B5=AC=EA=B8=80=20OAuth=20?= =?UTF-8?q?=EB=8F=84=EC=9E=85=20(#61)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 + .../com/debatetimer/client/OAuthClient.java | 39 ++++ .../debatetimer/client/OAuthProperties.java | 44 ++++ .../config/AuthMemberArgumentResolver.java | 19 +- .../config/AuthenticationConfig.java | 12 + .../com/debatetimer/config/WebConfig.java | 8 +- .../controller/member/MemberController.java | 44 +++- .../tool/cookie/CookieExtractor.java | 19 ++ .../controller/tool/cookie/CookieManager.java | 30 +++ .../tool/cookie/CookieProvider.java | 24 ++ .../controller/tool/jwt/AuthManager.java | 36 +++ .../tool/jwt/JwtTokenProperties.java | 25 +++ .../controller/tool/jwt/JwtTokenProvider.java | 36 +++ .../controller/tool/jwt/JwtTokenResolver.java | 48 ++++ .../controller/tool/jwt/TokenType.java | 7 + .../com/debatetimer/domain/member/Member.java | 30 +-- .../dto/member/JwtTokenResponse.java | 5 + .../dto/member/MemberCreateRequest.java | 6 +- .../dto/member/MemberCreateResponse.java | 4 +- .../debatetimer/dto/member/MemberInfo.java | 16 ++ .../debatetimer/dto/member/OAuthToken.java | 8 + .../exception/errorcode/ClientErrorCode.java | 9 +- .../repository/member/MemberRepository.java | 7 +- .../debatetimer/service/auth/AuthService.java | 35 +++ .../service/member/MemberService.java | 9 +- src/main/resources/application-dev.yml | 11 + .../controller/BaseControllerTest.java | 19 +- .../controller/BaseDocumentTest.java | 37 ++- .../controller/RestDocumentationRequest.java | 7 + .../controller/RestDocumentationResponse.java | 7 + .../member/MemberControllerTest.java | 76 +++++-- .../controller/member/MemberDocumentTest.java | 210 ++++++++++++++---- .../ParliamentaryControllerTest.java | 21 +- .../ParliamentaryDocumentTest.java | 56 ++--- .../tool/cookie/CookieExtractorTest.java | 48 ++++ .../tool/jwt/JwtTokenResolverTest.java | 84 +++++++ .../debatetimer/domain/member/MemberTest.java | 40 ---- .../parliamentary/ParliamentaryTableTest.java | 12 +- .../ParliamentaryTimeBoxesTest.java | 2 +- .../debatetimer/fixture/CookieGenerator.java | 30 +++ .../debatetimer/fixture/HeaderGenerator.java | 27 +++ .../debatetimer/fixture/JwtTokenFixture.java | 12 + .../debatetimer/fixture/MemberGenerator.java | 4 +- .../debatetimer/fixture/TokenGenerator.java | 40 ++++ .../member/MemberRepositoryTest.java | 58 +++++ .../ParliamentaryTableRepositoryTest.java | 8 +- .../ParliamentaryTimeBoxRepositoryTest.java | 4 +- .../debatetimer/service/BaseServiceTest.java | 8 + .../service/auth/AuthServiceTest.java | 43 ++++ .../service/member/MemberServiceTest.java | 20 +- .../ParliamentaryServiceTest.java | 25 ++- src/test/resources/application.yml | 5 + 52 files changed, 1195 insertions(+), 243 deletions(-) create mode 100644 src/main/java/com/debatetimer/client/OAuthClient.java create mode 100644 src/main/java/com/debatetimer/client/OAuthProperties.java create mode 100644 src/main/java/com/debatetimer/config/AuthenticationConfig.java create mode 100644 src/main/java/com/debatetimer/controller/tool/cookie/CookieExtractor.java create mode 100644 src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java create mode 100644 src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java create mode 100644 src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java create mode 100644 src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProperties.java create mode 100644 src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProvider.java create mode 100644 src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenResolver.java create mode 100644 src/main/java/com/debatetimer/controller/tool/jwt/TokenType.java create mode 100644 src/main/java/com/debatetimer/dto/member/JwtTokenResponse.java create mode 100644 src/main/java/com/debatetimer/dto/member/MemberInfo.java create mode 100644 src/main/java/com/debatetimer/dto/member/OAuthToken.java create mode 100644 src/main/java/com/debatetimer/service/auth/AuthService.java create mode 100644 src/test/java/com/debatetimer/controller/tool/cookie/CookieExtractorTest.java create mode 100644 src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenResolverTest.java delete mode 100644 src/test/java/com/debatetimer/domain/member/MemberTest.java create mode 100644 src/test/java/com/debatetimer/fixture/CookieGenerator.java create mode 100644 src/test/java/com/debatetimer/fixture/HeaderGenerator.java create mode 100644 src/test/java/com/debatetimer/fixture/JwtTokenFixture.java create mode 100644 src/test/java/com/debatetimer/fixture/TokenGenerator.java create mode 100644 src/test/java/com/debatetimer/repository/member/MemberRepositoryTest.java create mode 100644 src/test/java/com/debatetimer/service/auth/AuthServiceTest.java diff --git a/build.gradle b/build.gradle index bdcae96c..88e52ce4 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,10 @@ dependencies { // Excel Export implementation 'org.apache.poi:poi-ooxml:5.2.3' implementation 'org.apache.poi:poi:5.2.3' + + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-gson:0.11.5' } bootJar { diff --git a/src/main/java/com/debatetimer/client/OAuthClient.java b/src/main/java/com/debatetimer/client/OAuthClient.java new file mode 100644 index 00000000..e2fa1607 --- /dev/null +++ b/src/main/java/com/debatetimer/client/OAuthClient.java @@ -0,0 +1,39 @@ +package com.debatetimer.client; + +import com.debatetimer.dto.member.MemberCreateRequest; +import com.debatetimer.dto.member.MemberInfo; +import com.debatetimer.dto.member.OAuthToken; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +@Component +@EnableConfigurationProperties(OAuthProperties.class) +public class OAuthClient { + + private final RestClient restClient; + private final OAuthProperties oauthProperties; + + public OAuthClient(OAuthProperties oauthProperties) { + this.restClient = RestClient.create(); + this.oauthProperties = oauthProperties; + } + + public OAuthToken requestToken(MemberCreateRequest request) { + return restClient.post() + .uri("https://oauth2.googleapis.com/token") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(oauthProperties.createTokenRequestBody(request)) + .retrieve() + .body(OAuthToken.class); + } + + public MemberInfo requestMemberInfo(OAuthToken response) { + return restClient.get() + .uri("https://www.googleapis.com/oauth2/v3/userinfo") + .headers(headers -> headers.setBearerAuth(response.access_token())) + .retrieve() + .body(MemberInfo.class); + } +} diff --git a/src/main/java/com/debatetimer/client/OAuthProperties.java b/src/main/java/com/debatetimer/client/OAuthProperties.java new file mode 100644 index 00000000..22459aee --- /dev/null +++ b/src/main/java/com/debatetimer/client/OAuthProperties.java @@ -0,0 +1,44 @@ +package com.debatetimer.client; + +import com.debatetimer.dto.member.MemberCreateRequest; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +@Getter +@ConfigurationProperties(prefix = "oauth") +public class OAuthProperties { + + private final String clientId; + private final String clientSecret; + private final String redirectUri; + private final String grantType; + + public OAuthProperties( + String clientId, + String clientSecret, + String redirectUri, + String grantType) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.redirectUri = redirectUri; + this.grantType = grantType; + } + + public MultiValueMap createTokenRequestBody(MemberCreateRequest request) { + String code = request.code(); + String decodedVerificationCode = URLDecoder.decode(code, StandardCharsets.UTF_8); + + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("grant_type", grantType); + map.add("client_id", clientId); + map.add("redirect_uri", redirectUri); + map.add("code", decodedVerificationCode); + map.add("client_secret", clientSecret); + + return map; + } +} diff --git a/src/main/java/com/debatetimer/config/AuthMemberArgumentResolver.java b/src/main/java/com/debatetimer/config/AuthMemberArgumentResolver.java index 0a542f60..c3be291e 100644 --- a/src/main/java/com/debatetimer/config/AuthMemberArgumentResolver.java +++ b/src/main/java/com/debatetimer/config/AuthMemberArgumentResolver.java @@ -1,13 +1,14 @@ package com.debatetimer.config; import com.debatetimer.controller.auth.AuthMember; +import com.debatetimer.controller.tool.jwt.AuthManager; import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.custom.DTException; import com.debatetimer.exception.errorcode.ClientErrorCode; -import com.debatetimer.repository.member.MemberRepository; +import com.debatetimer.service.auth.AuthService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -17,7 +18,8 @@ @RequiredArgsConstructor public class AuthMemberArgumentResolver implements HandlerMethodArgumentResolver { - private final MemberRepository memberRepository; + private final AuthManager authManager; + private final AuthService authService; @Override public boolean supportsParameter(MethodParameter parameter) { @@ -31,14 +33,11 @@ public Object resolveArgument( NativeWebRequest webRequest, WebDataBinderFactory binderFactory ) { - try { - long memberId = Long.parseLong(webRequest.getParameter("memberId")); - return memberRepository.getById(memberId); - } catch (DTException | NumberFormatException exception) { - log.warn(exception.getMessage()); + String accessToken = webRequest.getHeader(HttpHeaders.AUTHORIZATION); + if (accessToken == null) { throw new DTClientErrorException(ClientErrorCode.UNAUTHORIZED_MEMBER); } + String email = authManager.resolveAccessToken(accessToken); + return authService.getMember(email); } } - - diff --git a/src/main/java/com/debatetimer/config/AuthenticationConfig.java b/src/main/java/com/debatetimer/config/AuthenticationConfig.java new file mode 100644 index 00000000..3b1073fa --- /dev/null +++ b/src/main/java/com/debatetimer/config/AuthenticationConfig.java @@ -0,0 +1,12 @@ +package com.debatetimer.config; + +import com.debatetimer.client.OAuthProperties; +import com.debatetimer.controller.tool.jwt.JwtTokenProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableConfigurationProperties({OAuthProperties.class, JwtTokenProperties.class}) +public class AuthenticationConfig { + +} diff --git a/src/main/java/com/debatetimer/config/WebConfig.java b/src/main/java/com/debatetimer/config/WebConfig.java index a983c443..0ce8ab1e 100644 --- a/src/main/java/com/debatetimer/config/WebConfig.java +++ b/src/main/java/com/debatetimer/config/WebConfig.java @@ -1,6 +1,7 @@ package com.debatetimer.config; -import com.debatetimer.repository.member.MemberRepository; +import com.debatetimer.controller.tool.jwt.AuthManager; +import com.debatetimer.service.auth.AuthService; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; @@ -11,10 +12,11 @@ @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { - private final MemberRepository memberRepository; + private final AuthManager authManager; + private final AuthService authService; @Override public void addArgumentResolvers(List argumentResolvers) { - argumentResolvers.add(new AuthMemberArgumentResolver(memberRepository)); + argumentResolvers.add(new AuthMemberArgumentResolver(authManager, authService)); } } diff --git a/src/main/java/com/debatetimer/controller/member/MemberController.java b/src/main/java/com/debatetimer/controller/member/MemberController.java index 94b9b393..10e05a28 100644 --- a/src/main/java/com/debatetimer/controller/member/MemberController.java +++ b/src/main/java/com/debatetimer/controller/member/MemberController.java @@ -1,12 +1,21 @@ package com.debatetimer.controller.member; import com.debatetimer.controller.auth.AuthMember; +import com.debatetimer.controller.tool.cookie.CookieManager; +import com.debatetimer.controller.tool.jwt.AuthManager; import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.member.JwtTokenResponse; import com.debatetimer.dto.member.MemberCreateRequest; import com.debatetimer.dto.member.MemberCreateResponse; +import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.TableResponses; +import com.debatetimer.service.auth.AuthService; import com.debatetimer.service.member.MemberService; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; @@ -19,6 +28,9 @@ public class MemberController { private final MemberService memberService; + private final AuthService authService; + private final CookieManager cookieManager; + private final AuthManager authManager; @GetMapping("/api/table") public TableResponses getTables(@AuthMember Member member) { @@ -27,7 +39,35 @@ public TableResponses getTables(@AuthMember Member member) { @PostMapping("/api/member") @ResponseStatus(HttpStatus.CREATED) - public MemberCreateResponse createMember(@RequestBody MemberCreateRequest request) { - return memberService.createMember(request); + public MemberCreateResponse createMember(@RequestBody MemberCreateRequest request, HttpServletResponse response) { + MemberInfo memberInfo = authService.getMemberInfo(request); + MemberCreateResponse memberCreateResponse = memberService.createMember(memberInfo); + JwtTokenResponse jwtTokenResponse = authManager.issueToken(memberInfo); + Cookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(jwtTokenResponse.refreshToken()); + + response.addHeader(HttpHeaders.AUTHORIZATION, jwtTokenResponse.accessToken()); + response.addCookie(refreshTokenCookie); + return memberCreateResponse; + } + + @PostMapping("/api/member/reissue") + public void reissueAccessToken(HttpServletRequest request, HttpServletResponse response) { + String refreshToken = cookieManager.extractRefreshToken(request.getCookies()); + JwtTokenResponse jwtTokenResponse = authManager.reissueToken(refreshToken); + Cookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(jwtTokenResponse.refreshToken()); + + response.addHeader(HttpHeaders.AUTHORIZATION, jwtTokenResponse.accessToken()); + response.addCookie(refreshTokenCookie); + } + + @PostMapping("/api/member/logout") + @ResponseStatus(HttpStatus.NO_CONTENT) + public void logout(@AuthMember Member member, HttpServletRequest request, HttpServletResponse response) { + String refreshToken = cookieManager.extractRefreshToken(request.getCookies()); + String email = authManager.resolveRefreshToken(refreshToken); + authService.logout(member, email); + Cookie deletedRefreshTokenCookie = cookieManager.deleteRefreshTokenCookie(); + + response.addCookie(deletedRefreshTokenCookie); } } diff --git a/src/main/java/com/debatetimer/controller/tool/cookie/CookieExtractor.java b/src/main/java/com/debatetimer/controller/tool/cookie/CookieExtractor.java new file mode 100644 index 00000000..936ea6d8 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/tool/cookie/CookieExtractor.java @@ -0,0 +1,19 @@ +package com.debatetimer.controller.tool.cookie; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import jakarta.servlet.http.Cookie; +import java.util.Arrays; +import org.springframework.stereotype.Component; + +@Component +public class CookieExtractor { + + public String extractCookie(String cookieName, Cookie[] cookies) { + return Arrays.stream(cookies) + .filter(cookie -> cookie.getName().equals(cookieName)) + .findAny() + .map(Cookie::getValue) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.EMPTY_COOKIE)); + } +} diff --git a/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java b/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java new file mode 100644 index 00000000..1a6d4520 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java @@ -0,0 +1,30 @@ +package com.debatetimer.controller.tool.cookie; + +import com.debatetimer.controller.tool.jwt.JwtTokenProperties; +import jakarta.servlet.http.Cookie; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class CookieManager { + + private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; + + private final CookieProvider cookieProvider; + private final CookieExtractor cookieExtractor; + private final JwtTokenProperties jwtTokenProperties; + + public Cookie createRefreshTokenCookie(String token) { + return cookieProvider.createCookie(REFRESH_TOKEN_COOKIE_NAME, token, + jwtTokenProperties.getRefreshTokenExpirationMillis()); + } + + public String extractRefreshToken(Cookie[] cookies) { + return cookieExtractor.extractCookie(REFRESH_TOKEN_COOKIE_NAME, cookies); + } + + public Cookie deleteRefreshTokenCookie() { + return cookieProvider.deleteCookie(REFRESH_TOKEN_COOKIE_NAME); + } +} diff --git a/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java b/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java new file mode 100644 index 00000000..a41f006a --- /dev/null +++ b/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java @@ -0,0 +1,24 @@ +package com.debatetimer.controller.tool.cookie; + +import jakarta.servlet.http.Cookie; +import org.springframework.stereotype.Component; + +@Component +public class CookieProvider { + + private static final String PATH = "/"; + + public Cookie createCookie(String cookieName, String token, long expirationMillis) { + Cookie cookie = new Cookie(cookieName, token); + cookie.setMaxAge((int) (expirationMillis / 1000)); + cookie.setPath(PATH); + return cookie; + } + + public Cookie deleteCookie(String cookieName) { + Cookie cookie = new Cookie(cookieName, ""); + cookie.setMaxAge(0); + cookie.setPath(PATH); + return cookie; + } +} diff --git a/src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java b/src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java new file mode 100644 index 00000000..2cc86a3d --- /dev/null +++ b/src/main/java/com/debatetimer/controller/tool/jwt/AuthManager.java @@ -0,0 +1,36 @@ +package com.debatetimer.controller.tool.jwt; + +import com.debatetimer.dto.member.JwtTokenResponse; +import com.debatetimer.dto.member.MemberInfo; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class AuthManager { + + private final JwtTokenProvider jwtTokenProvider; + private final JwtTokenResolver jwtTokenResolver; + + public JwtTokenResponse issueToken(MemberInfo memberInfo) { + String accessToken = jwtTokenProvider.createAccessToken(memberInfo); + String refreshToken = jwtTokenProvider.createRefreshToken(memberInfo); + return new JwtTokenResponse(accessToken, refreshToken); + } + + public JwtTokenResponse reissueToken(String refreshToken) { + String email = jwtTokenResolver.resolveRefreshToken(refreshToken); + MemberInfo memberInfo = new MemberInfo(email); + String accessToken = jwtTokenProvider.createAccessToken(memberInfo); + String newRefreshToken = jwtTokenProvider.createRefreshToken(memberInfo); + return new JwtTokenResponse(accessToken, newRefreshToken); + } + + public String resolveAccessToken(String accessToken) { + return jwtTokenResolver.resolveAccessToken(accessToken); + } + + public String resolveRefreshToken(String refreshToken) { + return jwtTokenResolver.resolveRefreshToken(refreshToken); + } +} diff --git a/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProperties.java b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProperties.java new file mode 100644 index 00000000..95653df5 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProperties.java @@ -0,0 +1,25 @@ +package com.debatetimer.controller.tool.jwt; + +import io.jsonwebtoken.security.Keys; +import javax.crypto.SecretKey; +import lombok.Getter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +@Getter +@ConfigurationProperties(prefix = "jwt") +public class JwtTokenProperties { + + private final String secretKey; + private final long accessTokenExpirationMillis; + private final long refreshTokenExpirationMillis; + + public JwtTokenProperties(String secretKey, long accessTokenExpirationMillis, long refreshTokenExpirationMillis) { + this.secretKey = secretKey; + this.accessTokenExpirationMillis = accessTokenExpirationMillis; + this.refreshTokenExpirationMillis = refreshTokenExpirationMillis; + } + + public SecretKey getSecretKey() { + return Keys.hmacShaKeyFor(secretKey.getBytes()); + } +} diff --git a/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProvider.java b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProvider.java new file mode 100644 index 00000000..5b903884 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenProvider.java @@ -0,0 +1,36 @@ +package com.debatetimer.controller.tool.jwt; + +import com.debatetimer.dto.member.MemberInfo; +import io.jsonwebtoken.Jwts; +import java.util.Date; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JwtTokenProvider { + + private final JwtTokenProperties jwtTokenProperties; + + public String createAccessToken(MemberInfo memberInfo) { + long accessTokenExpirationMillis = jwtTokenProperties.getAccessTokenExpirationMillis(); + return createToken(memberInfo, accessTokenExpirationMillis, TokenType.ACCESS_TOKEN); + } + + public String createRefreshToken(MemberInfo memberInfo) { + long refreshTokenExpirationMillis = jwtTokenProperties.getRefreshTokenExpirationMillis(); + return createToken(memberInfo, refreshTokenExpirationMillis, TokenType.REFRESH_TOKEN); + } + + private String createToken(MemberInfo memberInfo, long expirationMillis, TokenType tokenType) { + Date now = new Date(); + Date expiredDate = new Date(now.getTime() + expirationMillis); + return Jwts.builder() + .setSubject(memberInfo.email()) + .setIssuedAt(now) + .setExpiration(expiredDate) + .claim("type", tokenType.name()) + .signWith(jwtTokenProperties.getSecretKey()) + .compact(); + } +} diff --git a/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenResolver.java b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenResolver.java new file mode 100644 index 00000000..de5d33f7 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/tool/jwt/JwtTokenResolver.java @@ -0,0 +1,48 @@ +package com.debatetimer.controller.tool.jwt; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class JwtTokenResolver { + + private final JwtTokenProperties jwtTokenProperties; + + public String resolveAccessToken(String accessToken) { + return resolveToken(accessToken, TokenType.ACCESS_TOKEN); + } + + public String resolveRefreshToken(String refreshToken) { + return resolveToken(refreshToken, TokenType.REFRESH_TOKEN); + } + + private String resolveToken(String token, TokenType tokenType) { + try { + Claims claims = Jwts.parserBuilder() + .setSigningKey(jwtTokenProperties.getSecretKey()) + .build() + .parseClaimsJws(token) + .getBody(); + validateTokenType(claims, tokenType); + return claims.getSubject(); + } catch (ExpiredJwtException exception) { + throw new DTClientErrorException(ClientErrorCode.EXPIRED_TOKEN); + } catch (JwtException exception) { + throw new DTClientErrorException(ClientErrorCode.UNAUTHORIZED_MEMBER); + } + } + + private void validateTokenType(Claims claims, TokenType tokenType) { + String extractTokenType = claims.get("type", String.class); + if (!extractTokenType.equals(tokenType.name())) { + throw new DTClientErrorException(ClientErrorCode.UNAUTHORIZED_MEMBER); + } + } +} diff --git a/src/main/java/com/debatetimer/controller/tool/jwt/TokenType.java b/src/main/java/com/debatetimer/controller/tool/jwt/TokenType.java new file mode 100644 index 00000000..1112e4b9 --- /dev/null +++ b/src/main/java/com/debatetimer/controller/tool/jwt/TokenType.java @@ -0,0 +1,7 @@ +package com.debatetimer.controller.tool.jwt; + +public enum TokenType { + + ACCESS_TOKEN, + REFRESH_TOKEN +} diff --git a/src/main/java/com/debatetimer/domain/member/Member.java b/src/main/java/com/debatetimer/domain/member/Member.java index 29b9a3d1..1366b91e 100644 --- a/src/main/java/com/debatetimer/domain/member/Member.java +++ b/src/main/java/com/debatetimer/domain/member/Member.java @@ -1,11 +1,11 @@ package com.debatetimer.domain.member; -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotNull; import lombok.AccessLevel; import lombok.Getter; @@ -16,33 +16,25 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member { - private static final String NICKNAME_REGEX = "^[a-zA-Z가-힣 ]+$"; - public static final int NICKNAME_MAX_LENGTH = 10; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Email + @Column(unique = true) @NotNull - private String nickname; + private String email; - public Member(long id, String nickname) { - validate(nickname); + public Member(long id, String email) { this.id = id; - this.nickname = nickname; + this.email = email; } - public Member(String nickname) { - validate(nickname); - this.nickname = nickname; + public Member(String email) { + this.email = email; } - private void validate(String nickname) { - if (nickname.isEmpty() || nickname.length() > NICKNAME_MAX_LENGTH) { - throw new DTClientErrorException(ClientErrorCode.INVALID_MEMBER_NICKNAME_LENGTH); - } - if (!nickname.matches(NICKNAME_REGEX)) { - throw new DTClientErrorException(ClientErrorCode.INVALID_MEMBER_NICKNAME_FORM); - } + public boolean isSameMember(String email) { + return this.email.equals(email); } } diff --git a/src/main/java/com/debatetimer/dto/member/JwtTokenResponse.java b/src/main/java/com/debatetimer/dto/member/JwtTokenResponse.java new file mode 100644 index 00000000..dd109b43 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/member/JwtTokenResponse.java @@ -0,0 +1,5 @@ +package com.debatetimer.dto.member; + +public record JwtTokenResponse(String accessToken, String refreshToken) { + +} diff --git a/src/main/java/com/debatetimer/dto/member/MemberCreateRequest.java b/src/main/java/com/debatetimer/dto/member/MemberCreateRequest.java index 561246e1..7ba25126 100644 --- a/src/main/java/com/debatetimer/dto/member/MemberCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/member/MemberCreateRequest.java @@ -1,11 +1,7 @@ package com.debatetimer.dto.member; -import com.debatetimer.domain.member.Member; import jakarta.validation.constraints.NotBlank; -public record MemberCreateRequest(@NotBlank String nickname) { +public record MemberCreateRequest(@NotBlank String code) { - public Member toMember() { - return new Member(nickname); - } } diff --git a/src/main/java/com/debatetimer/dto/member/MemberCreateResponse.java b/src/main/java/com/debatetimer/dto/member/MemberCreateResponse.java index 7a4557ef..bde7ca76 100644 --- a/src/main/java/com/debatetimer/dto/member/MemberCreateResponse.java +++ b/src/main/java/com/debatetimer/dto/member/MemberCreateResponse.java @@ -2,9 +2,9 @@ import com.debatetimer.domain.member.Member; -public record MemberCreateResponse(long id, String nickname) { +public record MemberCreateResponse(long id, String email) { public MemberCreateResponse(Member member) { - this(member.getId(), member.getNickname()); + this(member.getId(), member.getEmail()); } } diff --git a/src/main/java/com/debatetimer/dto/member/MemberInfo.java b/src/main/java/com/debatetimer/dto/member/MemberInfo.java new file mode 100644 index 00000000..ecf509b6 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/member/MemberInfo.java @@ -0,0 +1,16 @@ +package com.debatetimer.dto.member; + +import com.debatetimer.domain.member.Member; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record MemberInfo(String email) { + + public MemberInfo(Member member) { + this(member.getEmail()); + } + + public Member toMember() { + return new Member(email); + } +} diff --git a/src/main/java/com/debatetimer/dto/member/OAuthToken.java b/src/main/java/com/debatetimer/dto/member/OAuthToken.java new file mode 100644 index 00000000..51805d45 --- /dev/null +++ b/src/main/java/com/debatetimer/dto/member/OAuthToken.java @@ -0,0 +1,8 @@ +package com.debatetimer.dto.member; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record OAuthToken(String access_token) { + +} diff --git a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java index bdaf2be2..db0ce1bf 100644 --- a/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java +++ b/src/main/java/com/debatetimer/exception/errorcode/ClientErrorCode.java @@ -1,6 +1,5 @@ package com.debatetimer.exception.errorcode; -import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -8,12 +7,6 @@ @Getter public enum ClientErrorCode implements ErrorCode { - INVALID_MEMBER_NICKNAME_LENGTH( - HttpStatus.BAD_REQUEST, - "닉네임은 1자 이상 %d자 이하여야 합니다".formatted(Member.NICKNAME_MAX_LENGTH) - ), - INVALID_MEMBER_NICKNAME_FORM(HttpStatus.BAD_REQUEST, "닉네임은 영문/한글만 가능합니다"), - INVALID_TABLE_NAME_LENGTH( HttpStatus.BAD_REQUEST, "테이블 이름은 1자 이상 %d자 이하여야 합니다".formatted(ParliamentaryTable.NAME_MAX_LENGTH) @@ -39,6 +32,8 @@ public enum ClientErrorCode implements ErrorCode { TABLE_NOT_FOUND(HttpStatus.NOT_FOUND, "토론 테이블을 찾을 수 없습니다."), NOT_TABLE_OWNER(HttpStatus.UNAUTHORIZED, "테이블을 소유한 회원이 아닙니다."), UNAUTHORIZED_MEMBER(HttpStatus.UNAUTHORIZED, "접근 권한이 없습니다"), + EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "토큰 기한이 만료되었습니다"), + EMPTY_COOKIE(HttpStatus.UNAUTHORIZED, "쿠키에 값이 없습니다"), MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "해당 회원이 존재하지 않습니다"), ; diff --git a/src/main/java/com/debatetimer/repository/member/MemberRepository.java b/src/main/java/com/debatetimer/repository/member/MemberRepository.java index de78ccff..e09d370c 100644 --- a/src/main/java/com/debatetimer/repository/member/MemberRepository.java +++ b/src/main/java/com/debatetimer/repository/member/MemberRepository.java @@ -17,5 +17,10 @@ default Member getById(long id) { .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.MEMBER_NOT_FOUND)); } - Optional findByNickname(String nickname); + Optional findByEmail(String email); + + default Member getByEmail(String email) { + return findByEmail(email) + .orElseThrow(() -> new DTClientErrorException(ClientErrorCode.MEMBER_NOT_FOUND)); + } } diff --git a/src/main/java/com/debatetimer/service/auth/AuthService.java b/src/main/java/com/debatetimer/service/auth/AuthService.java new file mode 100644 index 00000000..aec9e8e1 --- /dev/null +++ b/src/main/java/com/debatetimer/service/auth/AuthService.java @@ -0,0 +1,35 @@ +package com.debatetimer.service.auth; + +import com.debatetimer.client.OAuthClient; +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.member.MemberCreateRequest; +import com.debatetimer.dto.member.MemberInfo; +import com.debatetimer.dto.member.OAuthToken; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.repository.member.MemberRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final OAuthClient oauthClient; + private final MemberRepository memberRepository; + + public MemberInfo getMemberInfo(MemberCreateRequest request) { + OAuthToken oauthToken = oauthClient.requestToken(request); + return oauthClient.requestMemberInfo(oauthToken); + } + + public Member getMember(String email) { + return memberRepository.getByEmail(email); + } + + public void logout(Member member, String email) { + if (!member.isSameMember(email)) { + throw new DTClientErrorException(ClientErrorCode.UNAUTHORIZED_MEMBER); + } + } +} diff --git a/src/main/java/com/debatetimer/service/member/MemberService.java b/src/main/java/com/debatetimer/service/member/MemberService.java index 8bfec683..06796d00 100644 --- a/src/main/java/com/debatetimer/service/member/MemberService.java +++ b/src/main/java/com/debatetimer/service/member/MemberService.java @@ -2,8 +2,8 @@ import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; -import com.debatetimer.dto.member.MemberCreateRequest; import com.debatetimer.dto.member.MemberCreateResponse; +import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.TableResponses; import com.debatetimer.repository.member.MemberRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; @@ -27,10 +27,9 @@ public TableResponses getTables(Long memberId) { } @Transactional - public MemberCreateResponse createMember(MemberCreateRequest request) { - // TODO OAuth 로직 들어오면서 수정 예정 - Member member = memberRepository.findByNickname(request.nickname()) - .orElseGet(() -> memberRepository.save(request.toMember())); + public MemberCreateResponse createMember(MemberInfo memberInfo) { + Member member = memberRepository.findByEmail(memberInfo.email()) + .orElseGet(() -> memberRepository.save(memberInfo.toMember())); return new MemberCreateResponse(member); } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 01065082..0b514898 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -17,3 +17,14 @@ spring: cors: origin: ${secret.cors.origin} + +oauth: + client_id: ${secret.oauth.client_id} + client_secret: ${secret.oauth.client_secret} + redirect_uri: ${secret.oauth.redirect_uri} + grant_type: ${secret.oauth.grant_type} + +jwt: + secret_key: ${secret.secret_key} + access_token_expiration_millis: ${secret.access_token_expiration_millis} + refresh_token_expiration_millis: ${secret.refresh_token_expiration_millis} diff --git a/src/test/java/com/debatetimer/controller/BaseControllerTest.java b/src/test/java/com/debatetimer/controller/BaseControllerTest.java index 0885791e..17084b51 100644 --- a/src/test/java/com/debatetimer/controller/BaseControllerTest.java +++ b/src/test/java/com/debatetimer/controller/BaseControllerTest.java @@ -1,9 +1,13 @@ package com.debatetimer.controller; import com.debatetimer.DataBaseCleaner; +import com.debatetimer.client.OAuthClient; +import com.debatetimer.fixture.CookieGenerator; +import com.debatetimer.fixture.HeaderGenerator; import com.debatetimer.fixture.MemberGenerator; import com.debatetimer.fixture.ParliamentaryTableGenerator; import com.debatetimer.fixture.ParliamentaryTimeBoxGenerator; +import com.debatetimer.fixture.TokenGenerator; import com.debatetimer.repository.member.MemberRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; import io.restassured.RestAssured; @@ -16,8 +20,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.restdocs.RestDocumentationContextProvider; -import org.springframework.restdocs.restassured.RestDocumentationFilter; +import org.springframework.test.context.bean.override.mockito.MockitoBean; @ExtendWith(DataBaseCleaner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @@ -38,6 +41,18 @@ public abstract class BaseControllerTest { @Autowired protected ParliamentaryTimeBoxGenerator timeBoxGenerator; + @Autowired + protected HeaderGenerator headerGenerator; + + @Autowired + protected CookieGenerator cookieGenerator; + + @Autowired + protected TokenGenerator tokenGenerator; + + @MockitoBean + protected OAuthClient oAuthClient; + @LocalServerPort private int port; diff --git a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java index 46c879a9..a692a6a4 100644 --- a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java @@ -1,24 +1,31 @@ package com.debatetimer.controller; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.doReturn; import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import com.debatetimer.controller.tool.cookie.CookieManager; +import com.debatetimer.controller.tool.jwt.AuthManager; import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.member.JwtTokenResponse; import com.debatetimer.exception.errorcode.ClientErrorCode; -import com.debatetimer.repository.member.MemberRepository; +import com.debatetimer.service.auth.AuthService; import com.debatetimer.service.member.MemberService; import com.debatetimer.service.parliamentary.ParliamentaryService; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.filter.log.RequestLoggingFilter; import io.restassured.filter.log.ResponseLoggingFilter; +import io.restassured.http.Header; +import io.restassured.http.Headers; import io.restassured.specification.RequestSpecification; +import jakarta.servlet.http.Cookie; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpHeaders; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.restassured.RestAssuredRestDocumentation; @@ -31,22 +38,37 @@ public abstract class BaseDocumentTest { protected static long EXIST_MEMBER_ID = 123L; - protected static Member EXIST_MEMBER = new Member(EXIST_MEMBER_ID, "존재하는 멤버"); + protected static String EXIST_MEMBER_EMAIL = "abcde@gmail.com"; + protected static Member EXIST_MEMBER = new Member(EXIST_MEMBER_ID, EXIST_MEMBER_EMAIL); + protected static String EXIST_MEMBER_ACCESS_TOKEN = "dflskgnkds"; + protected static String EXIST_MEMBER_REFRESH_TOKEN = "dfsfsdgrs"; + protected static JwtTokenResponse EXIST_MEMBER_TOKEN_RESPONSE = new JwtTokenResponse(EXIST_MEMBER_ACCESS_TOKEN, + EXIST_MEMBER_REFRESH_TOKEN); + protected static Headers EXIST_MEMBER_HEADER = new Headers( + new Header(HttpHeaders.AUTHORIZATION, EXIST_MEMBER_ACCESS_TOKEN)); + protected static Cookie EXIST_MEMBER_COOKIE = new Cookie("refreshToken", EXIST_MEMBER_REFRESH_TOKEN); + protected static Cookie DELETE_MEMBER_COOKIE = new Cookie("refreshToken", ""); protected static RestDocumentationResponse ERROR_RESPONSE = new RestDocumentationResponse() .responseBodyField( fieldWithPath("message").type(STRING).description("에러 메시지") ); - @MockitoBean - private MemberRepository memberRepository; - @MockitoBean protected MemberService memberService; @MockitoBean protected ParliamentaryService parliamentaryService; + @MockitoBean + protected AuthService authService; + + @MockitoBean + protected AuthManager authManager; + + @MockitoBean + protected CookieManager cookieManager; + @LocalServerPort private int port; @@ -70,7 +92,8 @@ private void setRestAssured(RestDocumentationContextProvider restDocumentation) } private void setLoginMember() { - when(memberRepository.getById(EXIST_MEMBER_ID)).thenReturn(EXIST_MEMBER); + doReturn(EXIST_MEMBER_EMAIL).when(authManager).resolveAccessToken(EXIST_MEMBER_ACCESS_TOKEN); + doReturn(EXIST_MEMBER).when(authService).getMember(EXIST_MEMBER_EMAIL); } protected RestDocumentationRequest request() { diff --git a/src/test/java/com/debatetimer/controller/RestDocumentationRequest.java b/src/test/java/com/debatetimer/controller/RestDocumentationRequest.java index 6157034f..372422d5 100644 --- a/src/test/java/com/debatetimer/controller/RestDocumentationRequest.java +++ b/src/test/java/com/debatetimer/controller/RestDocumentationRequest.java @@ -1,5 +1,6 @@ package com.debatetimer.controller; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; @@ -8,6 +9,7 @@ import com.epages.restdocs.apispec.ResourceSnippetParametersBuilder; import java.util.LinkedList; import java.util.List; +import org.springframework.restdocs.cookies.CookieDescriptor; import org.springframework.restdocs.headers.HeaderDescriptor; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.request.ParameterDescriptor; @@ -48,6 +50,11 @@ public RestDocumentationRequest requestHeader(HeaderDescriptor... descriptors) { return this; } + public RestDocumentationRequest requestCookie(CookieDescriptor... descriptors) { + snippets.add(requestCookies(descriptors)); + return this; + } + public RestDocumentationRequest requestBodyField(FieldDescriptor... descriptors) { snippets.add(requestFields(descriptors)); return this; diff --git a/src/test/java/com/debatetimer/controller/RestDocumentationResponse.java b/src/test/java/com/debatetimer/controller/RestDocumentationResponse.java index 2e25a24c..1ddfe4d7 100644 --- a/src/test/java/com/debatetimer/controller/RestDocumentationResponse.java +++ b/src/test/java/com/debatetimer/controller/RestDocumentationResponse.java @@ -1,10 +1,12 @@ package com.debatetimer.controller; +import static org.springframework.restdocs.cookies.CookieDocumentation.responseCookies; import static org.springframework.restdocs.headers.HeaderDocumentation.responseHeaders; import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; import java.util.LinkedList; import java.util.List; +import org.springframework.restdocs.cookies.CookieDescriptor; import org.springframework.restdocs.headers.HeaderDescriptor; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.snippet.Snippet; @@ -22,6 +24,11 @@ public RestDocumentationResponse responseHeader(HeaderDescriptor... descriptors) return this; } + public RestDocumentationResponse responseCookie(CookieDescriptor... descriptors) { + snippets.add(responseCookies(descriptors)); + return this; + } + public RestDocumentationResponse responseBodyField(FieldDescriptor... descriptors) { snippets.add(responseFields(descriptors)); return this; diff --git a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java index 406d3c70..b29bc223 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java @@ -1,54 +1,92 @@ package com.debatetimer.controller.member; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; import com.debatetimer.controller.BaseControllerTest; import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; import com.debatetimer.dto.member.MemberCreateRequest; -import com.debatetimer.dto.member.MemberCreateResponse; +import com.debatetimer.dto.member.MemberInfo; +import com.debatetimer.dto.member.OAuthToken; import com.debatetimer.dto.member.TableResponses; import io.restassured.http.ContentType; +import io.restassured.http.Headers; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; class MemberControllerTest extends BaseControllerTest { + @Nested + class GetTables { + + @Test + void 회원의_전체_토론_시간표를_조회한다() { + Member member = memberGenerator.generate("default@gmail.com"); + parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", 1800, false, false)); + parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", 1900, false, false)); + + Headers headers = headerGenerator.generateAccessTokenHeader(member); + + TableResponses response = given() + .contentType(ContentType.JSON) + .headers(headers) + .when().get("/api/table") + .then().statusCode(200) + .extract().as(TableResponses.class); + + assertThat(response.tables()).hasSize(2); + } + } + @Nested class CreateMember { @Test void 회원을_생성한다() { - MemberCreateRequest request = new MemberCreateRequest("커찬"); + MemberCreateRequest request = new MemberCreateRequest("gnkldsnglnksl"); + OAuthToken oAuthToken = new OAuthToken("accessToken"); + MemberInfo memberInfo = new MemberInfo("default@gmail.com"); + doReturn(oAuthToken).when(oAuthClient).requestToken(request); + doReturn(memberInfo).when(oAuthClient).requestMemberInfo(oAuthToken); - MemberCreateResponse response = given() + given() .contentType(ContentType.JSON) .body(request) .when().post("/api/member") - .then().statusCode(201) - .extract().as(MemberCreateResponse.class); - - assertThat(response.nickname()).isEqualTo(request.nickname()); + .then().statusCode(201); } } @Nested - class GetTables { + class ReissueAccessToken { @Test - void 회원의_전체_토론_시간표를_조회한다() { - Member member = memberRepository.save(new Member("커찬")); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", 1800, false, false)); - parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", 1900, false, false)); + void 토큰을_갱신한다() { + Member bito = memberGenerator.generate("bito@gmail.com"); + String refreshToken = tokenGenerator.generateRefreshToken(bito.getEmail()); - TableResponses response = given() - .contentType(ContentType.JSON) - .queryParam("memberId", member.getId()) - .when().get("/api/table") - .then().statusCode(200) - .extract().as(TableResponses.class); + given() + .cookie("refreshToken", refreshToken) + .when().post("/api/member/reissue") + .then().statusCode(200); + } + } - assertThat(response.tables()).hasSize(2); + @Nested + class Logout { + + @Test + void 로그아웃한다() { + Member bito = memberGenerator.generate("bito@gmail.com"); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); + String refreshToken = tokenGenerator.generateRefreshToken(bito.getEmail()); + + given() + .cookie("refreshToken", refreshToken) + .headers(headers) + .when().post("/api/member/logout") + .then().statusCode(204); } } } diff --git a/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java b/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java index a9edd6d2..0e9c9cb5 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java @@ -1,11 +1,14 @@ package com.debatetimer.controller.member; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; import static org.springframework.restdocs.payload.JsonFieldType.STRING; import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; -import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; import com.debatetimer.controller.BaseDocumentTest; import com.debatetimer.controller.RestDocumentationRequest; @@ -13,6 +16,7 @@ import com.debatetimer.controller.Tag; import com.debatetimer.dto.member.MemberCreateRequest; import com.debatetimer.dto.member.MemberCreateResponse; +import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.TableResponse; import com.debatetimer.dto.member.TableResponses; import com.debatetimer.dto.member.TableType; @@ -24,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.http.HttpHeaders; public class MemberDocumentTest extends BaseDocumentTest { @@ -34,25 +39,26 @@ class CreateMember { .tag(Tag.MEMBER_API) .summary("멤버 생성") .requestBodyField( - fieldWithPath("nickname").type(STRING).description("멤버 닉네임") + fieldWithPath("code").type(STRING).description("인가 코드") ); private final RestDocumentationResponse responseDocument = response() .responseBodyField( fieldWithPath("id").type(NUMBER).description("멤버 ID"), - fieldWithPath("nickname").type(STRING).description("멤버 닉네임") + fieldWithPath("email").type(STRING).description("멤버 이메일") ); @Test void 회원_생성_성공() { - MemberCreateRequest request = new MemberCreateRequest("커찬"); - MemberCreateResponse response = new MemberCreateResponse(1L, "커찬"); - when(memberService.createMember(request)).thenReturn(response); + MemberCreateRequest request = new MemberCreateRequest("dfsfgdsg"); + MemberInfo memberInfo = new MemberInfo(EXIST_MEMBER_EMAIL); + MemberCreateResponse response = new MemberCreateResponse(EXIST_MEMBER_ID, EXIST_MEMBER_EMAIL); + doReturn(memberInfo).when(authService).getMemberInfo(request); + doReturn(response).when(memberService).createMember(memberInfo); + doReturn(EXIST_MEMBER_TOKEN_RESPONSE).when(authManager).issueToken(memberInfo); + doReturn(EXIST_MEMBER_COOKIE).when(cookieManager).createRefreshTokenCookie(EXIST_MEMBER_REFRESH_TOKEN); - var document = document("member/create", 201) - .request(requestDocument) - .response(responseDocument) - .build(); + var document = document("member/create", 201).request(requestDocument).response(responseDocument).build(); given(document) .contentType(ContentType.JSON) @@ -60,82 +66,188 @@ class CreateMember { .when().post("/api/member") .then().statusCode(201); } + } + + @Nested + class GetTables { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.MEMBER_API) + .summary("멤버의 토론 시간표 조회") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ); - @EnumSource( - value = ClientErrorCode.class, - names = {"INVALID_MEMBER_NICKNAME_LENGTH", "INVALID_MEMBER_NICKNAME_FORM"} - ) + private final RestDocumentationResponse responseDocument = response().responseBodyField( + fieldWithPath("tables").type(ARRAY).description("멤버의 토론 테이블들"), + fieldWithPath("tables[].id").type(NUMBER).description("토론 테이블 ID (토론 타입 별로 ID를 가짐)"), + fieldWithPath("tables[].name").type(STRING).description("토론 테이블 이름"), + fieldWithPath("tables[].type").type(STRING).description("토론 타입"), + fieldWithPath("tables[].duration").type(NUMBER).description("소요 시간 (초)")); + + @Test + void 테이블_조회_성공() { + TableResponses response = new TableResponses( + List.of(new TableResponse(1L, "토론 테이블 1", TableType.PARLIAMENTARY, 1800), + new TableResponse(2L, "토론 테이블 2", TableType.PARLIAMENTARY, 2000)) + ); + doReturn(response).when(memberService).getTables(EXIST_MEMBER_ID); + + var document = document("member/table", 200) + .request(requestDocument) + .response(responseDocument) + .build(); + + given(document) + .headers(EXIST_MEMBER_HEADER) + .when().get("/api/table") + .then().statusCode(200); + } + + @EnumSource(value = ClientErrorCode.class, names = {"MEMBER_NOT_FOUND"}) @ParameterizedTest - void 회원_생성_실패(ClientErrorCode errorCode) { - MemberCreateRequest request = new MemberCreateRequest("커찬"); - when(memberService.createMember(request)).thenThrow(new DTClientErrorException(errorCode)); + void 테이블_조회_실패(ClientErrorCode errorCode) { + doThrow(new DTClientErrorException(errorCode)).when(memberService).getTables(EXIST_MEMBER_ID); - var document = document("member/create", errorCode) + var document = document("member/table", errorCode) .request(requestDocument) .response(ERROR_RESPONSE) .build(); given(document) .contentType(ContentType.JSON) - .body(request) - .when().post("/api/member") + .headers(EXIST_MEMBER_HEADER) + .when().get("/api/table") .then().statusCode(errorCode.getStatus().value()); } } @Nested - class GetTables { - + class ReissueAccessToken { private final RestDocumentationRequest requestDocument = request() .tag(Tag.MEMBER_API) - .summary("멤버의 토론 시간표 조회") - .queryParameter( - parameterWithName("memberId").description("멤버 ID") + .summary("토큰 갱신") + .requestCookie( + cookieWithName("refreshToken").description("리프레시 토큰") ); private final RestDocumentationResponse responseDocument = response() - .responseBodyField( - fieldWithPath("tables").type(ARRAY).description("멤버의 토론 테이블들"), - fieldWithPath("tables[].id").type(NUMBER).description("토론 테이블 ID (토론 타입 별로 ID를 가짐)"), - fieldWithPath("tables[].name").type(STRING).description("토론 테이블 이름"), - fieldWithPath("tables[].type").type(STRING).description("토론 타입"), - fieldWithPath("tables[].duration").type(NUMBER).description("소요 시간 (초)") + .responseHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .responseCookie( + cookieWithName("refreshToken").description("리프레시 토큰") ); @Test - void 테이블_조회_성공() { - TableResponses response = new TableResponses(List.of( - new TableResponse(1L, "토론 테이블 1", TableType.PARLIAMENTARY, 1800), - new TableResponse(2L, "토론 테이블 2", TableType.PARLIAMENTARY, 2000) - )); - when(memberService.getTables(EXIST_MEMBER_ID)).thenReturn(response); + void 토큰_갱신_성공() { + doReturn(EXIST_MEMBER_TOKEN_RESPONSE).when(authManager).reissueToken(any()); + doReturn(EXIST_MEMBER_COOKIE).when(cookieManager).createRefreshTokenCookie(any()); - var document = document("member/table", 200) + var document = document("member/logout", 204) .request(requestDocument) .response(responseDocument) .build(); given(document) - .contentType(ContentType.JSON) - .queryParam("memberId", EXIST_MEMBER_ID) - .when().get("/api/table") + .headers(EXIST_MEMBER_HEADER) + .cookie("refreshToken") + .when().post("/api/member/reissue") .then().statusCode(200); } - @EnumSource(value = ClientErrorCode.class, names = {"MEMBER_NOT_FOUND"}) + @EnumSource(value = ClientErrorCode.class, names = {"EMPTY_COOKIE"}) @ParameterizedTest - void 테이블_조회_실패(ClientErrorCode errorCode) { - when(memberService.getTables(EXIST_MEMBER_ID)).thenThrow(new DTClientErrorException(errorCode)); + void 토큰_갱신_실패_쿠키_추출(ClientErrorCode errorCode) { + doThrow(new DTClientErrorException(errorCode)).when(cookieManager).extractRefreshToken(any()); - var document = document("member/table", errorCode) + var document = document("member/reissue", errorCode) .request(requestDocument) .response(ERROR_RESPONSE) .build(); given(document) - .contentType(ContentType.JSON) - .queryParam("memberId", EXIST_MEMBER_ID) - .when().get("/api/table") + .cookie("refreshToken") + .when().post("/api/member/reissue") + .then().statusCode(errorCode.getStatus().value()); + } + + @EnumSource(value = ClientErrorCode.class, names = {"EXPIRED_TOKEN", "UNAUTHORIZED_MEMBER"}) + @ParameterizedTest + void 토큰_갱신_실패_토큰_갱신(ClientErrorCode errorCode) { + doThrow(new DTClientErrorException(errorCode)).when(authManager).reissueToken(any()); + + var document = document("member/reissue", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .cookie("refreshToken") + .when().post("/api/member/reissue") + .then().statusCode(errorCode.getStatus().value()); + } + } + + @Nested + class Logout { + + private final RestDocumentationRequest requestDocument = request() + .tag(Tag.MEMBER_API) + .summary("로그아웃") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) + .requestCookie( + cookieWithName("refreshToken").description("리프레시 토큰") + ); + + @Test + void 로그아웃_성공() { + doReturn(DELETE_MEMBER_COOKIE).when(cookieManager).deleteRefreshTokenCookie(); + + var document = document("member/logout", 204) + .request(requestDocument) + .build(); + + given(document) + .headers(EXIST_MEMBER_HEADER) + .cookie("refreshToken") + .when().post("/api/member/logout") + .then().statusCode(204); + } + + @EnumSource(value = ClientErrorCode.class, names = {"EMPTY_COOKIE"}) + @ParameterizedTest + void 로그아웃_실패_쿠키_추출(ClientErrorCode errorCode) { + doThrow(new DTClientErrorException(errorCode)).when(cookieManager).extractRefreshToken(any()); + + var document = document("member/logout", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .headers(EXIST_MEMBER_HEADER) + .cookie("refreshToken") + .when().post("/api/member/logout") + .then().statusCode(errorCode.getStatus().value()); + } + + @EnumSource(value = ClientErrorCode.class, names = {"UNAUTHORIZED_MEMBER", "EXPIRED_TOKEN"}) + @ParameterizedTest + void 로그아웃_실패(ClientErrorCode errorCode) { + doThrow(new DTClientErrorException(errorCode)).when(authService).logout(any(), any()); + + var document = document("member/logout", errorCode) + .request(requestDocument) + .response(ERROR_RESPONSE) + .build(); + + given(document) + .headers(EXIST_MEMBER_HEADER) + .cookie("refreshToken") + .when().post("/api/member/logout") .then().statusCode(errorCode.getStatus().value()); } } diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java index 1c8e7040..fdf092b3 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryControllerTest.java @@ -13,6 +13,7 @@ import com.debatetimer.dto.parliamentary.request.TimeBoxCreateRequest; import com.debatetimer.dto.parliamentary.response.ParliamentaryTableResponse; import io.restassured.http.ContentType; +import io.restassured.http.Headers; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -24,7 +25,7 @@ class Save { @Test void 의회식_테이블을_생성한다() { - Member bito = memberGenerator.generate("비토"); + Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTableCreateRequest request = new ParliamentaryTableCreateRequest( new TableInfoCreateRequest("비토 테이블", "주제", true, true), List.of( @@ -32,10 +33,11 @@ class Save { new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) ) ); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); ParliamentaryTableResponse response = given() .contentType(ContentType.JSON) - .queryParam("memberId", bito.getId()) + .headers(headers) .body(request) .when().post("/api/table/parliamentary") .then().statusCode(201) @@ -53,15 +55,16 @@ class GetTable { @Test void 의회식_테이블을_조회한다() { - Member bito = memberGenerator.generate("비토"); + Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTable bitoTable = tableGenerator.generate(bito); timeBoxGenerator.generate(bitoTable, 1); timeBoxGenerator.generate(bitoTable, 2); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); ParliamentaryTableResponse response = given() .contentType(ContentType.JSON) .pathParam("tableId", bitoTable.getId()) - .queryParam("memberId", bito.getId()) + .headers(headers) .when().get("/api/table/parliamentary/{tableId}") .then().statusCode(200) .extract().as(ParliamentaryTableResponse.class); @@ -78,7 +81,7 @@ class UpdateTable { @Test void 의회식_토론_테이블을_업데이트한다() { - Member bito = memberGenerator.generate("비토"); + Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTable bitoTable = tableGenerator.generate(bito); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( new TableInfoCreateRequest("비토 테이블", "주제", true, true), @@ -87,11 +90,12 @@ class UpdateTable { new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) ) ); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); ParliamentaryTableResponse response = given() .contentType(ContentType.JSON) .pathParam("tableId", bitoTable.getId()) - .queryParam("memberId", bito.getId()) + .headers(headers) .body(renewTableRequest) .when().put("/api/table/parliamentary/{tableId}") .then().statusCode(200) @@ -110,15 +114,16 @@ class DeleteTable { @Test void 의회식_토론_테이블을_삭제한다() { - Member bito = memberGenerator.generate("비토"); + Member bito = memberGenerator.generate("default@gmail.com"); ParliamentaryTable bitoTable = tableGenerator.generate(bito); timeBoxGenerator.generate(bitoTable, 1); timeBoxGenerator.generate(bitoTable, 2); + Headers headers = headerGenerator.generateAccessTokenHeader(bito); given() .contentType(ContentType.JSON) .pathParam("tableId", bitoTable.getId()) - .queryParam("memberId", bito.getId()) + .headers(headers) .when().delete("/api/table/parliamentary/{tableId}") .then().statusCode(204); } diff --git a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java index 4b41a071..be8ea8dd 100644 --- a/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/parliamentary/ParliamentaryDocumentTest.java @@ -3,8 +3,9 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.when; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; import static org.springframework.restdocs.payload.JsonFieldType.ARRAY; import static org.springframework.restdocs.payload.JsonFieldType.BOOLEAN; import static org.springframework.restdocs.payload.JsonFieldType.NUMBER; @@ -33,6 +34,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; +import org.springframework.http.HttpHeaders; public class ParliamentaryDocumentTest extends BaseDocumentTest { @@ -42,8 +44,8 @@ class Save { private final RestDocumentationRequest requestDocument = request() .tag(Tag.PARLIAMENTARY_API) .summary("새로운 의회식 토론 시간표 생성") - .queryParameter( - parameterWithName("memberId").description("멤버 ID") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") ) .requestBodyField( fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), @@ -90,7 +92,7 @@ class Save { new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 3, 1) ) ); - when(parliamentaryService.save(eq(request), any())).thenReturn(response); + doReturn(response).when(parliamentaryService).save(eq(request), any()); var document = document("parliamentary/post", 201) .request(requestDocument) @@ -99,7 +101,7 @@ class Save { given(document) .contentType(ContentType.JSON) - .queryParam("memberId", EXIST_MEMBER_ID) + .headers(EXIST_MEMBER_HEADER) .body(request) .when().post("/api/table/parliamentary") .then().statusCode(201); @@ -124,7 +126,7 @@ class Save { new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) ) ); - when(parliamentaryService.save(eq(request), any())).thenThrow(new DTClientErrorException(errorCode)); + doThrow(new DTClientErrorException(errorCode)).when(parliamentaryService).save(eq(request), any()); var document = document("parliamentary/post", errorCode) .request(requestDocument) @@ -133,7 +135,7 @@ class Save { given(document) .contentType(ContentType.JSON) - .queryParam("memberId", EXIST_MEMBER_ID) + .headers(EXIST_MEMBER_HEADER) .body(request) .when().post("/api/table/parliamentary") .then().statusCode(errorCode.getStatus().value()); @@ -146,11 +148,11 @@ class GetTable { private final RestDocumentationRequest requestDocument = request() .summary("의회식 토론 시간표 조회") .tag(Tag.PARLIAMENTARY_API) + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) .pathParameter( parameterWithName("tableId").description("테이블 ID") - ) - .queryParameter( - parameterWithName("memberId").description("멤버 ID") ); private final RestDocumentationResponse responseDocument = response() @@ -180,7 +182,7 @@ class GetTable { new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 3, 1) ) ); - when(parliamentaryService.findTable(eq(tableId), any())).thenReturn(response); + doReturn(response).when(parliamentaryService).findTable(eq(tableId), any()); var document = document("parliamentary/get", 200) .request(requestDocument) @@ -189,8 +191,8 @@ class GetTable { given(document) .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) .pathParam("tableId", tableId) - .queryParam("memberId", memberId) .when().get("/api/table/parliamentary/{tableId}") .then().statusCode(200); } @@ -200,7 +202,7 @@ class GetTable { void 의회식_테이블_조회_실패(ClientErrorCode errorCode) { long memberId = 4L; long tableId = 5L; - when(parliamentaryService.findTable(eq(tableId), any())).thenThrow(new DTClientErrorException(errorCode)); + doThrow(new DTClientErrorException(errorCode)).when(parliamentaryService).findTable(eq(tableId), any()); var document = document("parliamentary/get", errorCode) .request(requestDocument) @@ -209,8 +211,8 @@ class GetTable { given(document) .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) .pathParam("tableId", tableId) - .queryParam("memberId", memberId) .when().get("/api/table/parliamentary/{tableId}") .then().statusCode(errorCode.getStatus().value()); } @@ -222,12 +224,12 @@ class UpdateTable { private final RestDocumentationRequest requestDocument = request() .tag(Tag.PARLIAMENTARY_API) .summary("의회식 토론 시간표 수정") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) .pathParameter( parameterWithName("tableId").description("테이블 ID") ) - .queryParameter( - parameterWithName("memberId").description("멤버 ID") - ) .requestBodyField( fieldWithPath("info").type(OBJECT).description("토론 테이블 정보"), fieldWithPath("info.name").type(STRING).description("테이블 이름"), @@ -275,7 +277,7 @@ class UpdateTable { new TimeBoxResponse(Stance.CONS, BoxType.OPENING, 300, 1) ) ); - when(parliamentaryService.updateTable(eq(request), eq(tableId), any())).thenReturn(response); + doReturn(response).when(parliamentaryService).updateTable(eq(request), eq(tableId), any()); var document = document("parliamentary/put", 200) .request(requestDocument) @@ -284,7 +286,7 @@ class UpdateTable { given(document) .contentType(ContentType.JSON) - .queryParam("memberId", memberId) + .headers(EXIST_MEMBER_HEADER) .pathParam("tableId", tableId) .body(request) .when().put("/api/table/parliamentary/{tableId}") @@ -313,8 +315,8 @@ class UpdateTable { new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 300, 1) ) ); - when(parliamentaryService.updateTable(eq(request), eq(tableId), any())) - .thenThrow(new DTClientErrorException(errorCode)); + doThrow(new DTClientErrorException(errorCode)).when(parliamentaryService) + .updateTable(eq(request), eq(tableId), any()); var document = document("parliamentary/put", errorCode) .request(requestDocument) @@ -323,8 +325,8 @@ class UpdateTable { given(document) .contentType(ContentType.JSON) + .headers(EXIST_MEMBER_HEADER) .pathParam("tableId", tableId) - .queryParam("memberId", memberId) .body(request) .when().put("/api/table/parliamentary/{tableId}") .then().statusCode(errorCode.getStatus().value()); @@ -337,11 +339,11 @@ class DeleteTable { private final RestDocumentationRequest requestDocument = request() .tag(Tag.PARLIAMENTARY_API) .summary("의회식 토론 시간표 삭제") + .requestHeader( + headerWithName(HttpHeaders.AUTHORIZATION).description("액세스 토큰") + ) .pathParameter( parameterWithName("tableId").description("테이블 ID") - ) - .queryParameter( - parameterWithName("memberId").description("멤버 ID") ); @Test @@ -355,8 +357,8 @@ class DeleteTable { .build(); given(document) + .headers(EXIST_MEMBER_HEADER) .pathParam("tableId", tableId) - .queryParam("memberId", memberId) .when().delete("/api/table/parliamentary/{tableId}") .then().statusCode(204); } @@ -374,8 +376,8 @@ class DeleteTable { .build(); given(document) + .headers(EXIST_MEMBER_HEADER) .pathParam("tableId", tableId) - .queryParam("memberId", memberId) .when().delete("/api/table/parliamentary/{tableId}") .then().statusCode(errorCode.getStatus().value()); } diff --git a/src/test/java/com/debatetimer/controller/tool/cookie/CookieExtractorTest.java b/src/test/java/com/debatetimer/controller/tool/cookie/CookieExtractorTest.java new file mode 100644 index 00000000..84eb34ff --- /dev/null +++ b/src/test/java/com/debatetimer/controller/tool/cookie/CookieExtractorTest.java @@ -0,0 +1,48 @@ +package com.debatetimer.controller.tool.cookie; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.fixture.CookieGenerator; +import jakarta.servlet.http.Cookie; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class CookieExtractorTest { + + private CookieGenerator cookieGenerator; + private CookieExtractor cookieExtractor; + + @BeforeEach + void setUp() { + this.cookieGenerator = new CookieGenerator(); + this.cookieExtractor = new CookieExtractor(); + } + + @Nested + class ExtractCookie { + + @Test + void 쿠키에서_해당하는_키의_값을_추출한다() { + String key = "key"; + String value = "value"; + Cookie[] cookies = cookieGenerator.generateCookie(key, value, 100000); + + assertThat(cookieExtractor.extractCookie(key, cookies)).isEqualTo(value); + } + + @Test + void 쿠키에서_해당하는_값이_없으면_예외를_발생시킨다() { + String key = "key"; + String value = "value"; + Cookie[] cookies = cookieGenerator.generateCookie("token", value, 100000); + + assertThatThrownBy(() -> cookieExtractor.extractCookie(key, cookies)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.EMPTY_COOKIE.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenResolverTest.java b/src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenResolverTest.java new file mode 100644 index 00000000..7e81bae8 --- /dev/null +++ b/src/test/java/com/debatetimer/controller/tool/jwt/JwtTokenResolverTest.java @@ -0,0 +1,84 @@ +package com.debatetimer.controller.tool.jwt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.fixture.JwtTokenFixture; +import com.debatetimer.fixture.TokenGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class JwtTokenResolverTest { + + private TokenGenerator tokenGenerator; + private JwtTokenResolver jwtTokenResolver; + + @BeforeEach + void setUp() { + this.tokenGenerator = new TokenGenerator(); + this.jwtTokenResolver = new JwtTokenResolver(JwtTokenFixture.TEST_TOKEN_PROPERTIES); + } + + @Nested + class ResolveAccessToken { + + @Test + void 액세스_토큰에서_이메일을_가져온다() { + String email = "bito@gmail.com"; + String accessToken = tokenGenerator.generateAccessToken(email); + + assertThat(jwtTokenResolver.resolveAccessToken(accessToken)).isEqualTo(email); + } + + @Test + void 기한이_만료된_토큰이면_예외를_발생시킨다() { + String expiredToken = tokenGenerator.generateExpiredToken(); + + assertThatThrownBy(() -> jwtTokenResolver.resolveAccessToken(expiredToken)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.EXPIRED_TOKEN.getMessage()); + } + + @Test + void 액세스_토큰이_아니면_예외를_발생시킨다() { + String refreshToken = tokenGenerator.generateRefreshToken("bito@gmail.com"); + + assertThatThrownBy(() -> jwtTokenResolver.resolveAccessToken(refreshToken)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.UNAUTHORIZED_MEMBER.getMessage()); + } + } + + @Nested + class ResolveRefreshToken { + + @Test + void 리프레시_토큰에서_이메일을_가져온다() { + String email = "bito@gmail.com"; + String accessToken = tokenGenerator.generateRefreshToken(email); + + assertThat(jwtTokenResolver.resolveRefreshToken(accessToken)).isEqualTo(email); + } + + @Test + void 기한이_만료된_토큰이면_예외를_발생시킨다() { + String expiredToken = tokenGenerator.generateExpiredToken(); + + assertThatThrownBy(() -> jwtTokenResolver.resolveRefreshToken(expiredToken)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.EXPIRED_TOKEN.getMessage()); + } + + @Test + void 리프레시_토큰이_아니면_예외를_발생시킨다() { + String accessToken = tokenGenerator.generateAccessToken("default@gmail.com"); + + assertThatThrownBy(() -> jwtTokenResolver.resolveRefreshToken(accessToken)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.UNAUTHORIZED_MEMBER.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/domain/member/MemberTest.java b/src/test/java/com/debatetimer/domain/member/MemberTest.java deleted file mode 100644 index 0637e45e..00000000 --- a/src/test/java/com/debatetimer/domain/member/MemberTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.debatetimer.domain.member; - -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.debatetimer.exception.custom.DTClientErrorException; -import com.debatetimer.exception.errorcode.ClientErrorCode; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -class MemberTest { - - @Nested - class Validate { - - @ParameterizedTest - @ValueSource(strings = {"a bc가다", "가나 다ab"}) - void 닉네임은_영문과_한글_띄어쓰기만_가능하다(String nickname) { - assertThatCode(() -> new Member(nickname)) - .doesNotThrowAnyException(); - } - - @ParameterizedTest - @ValueSource(ints = {0, Member.NICKNAME_MAX_LENGTH + 1}) - void 닉네임은_정해진_길이_이내여야_한다(int length) { - assertThatThrownBy(() -> new Member("f".repeat(length))) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_MEMBER_NICKNAME_LENGTH.getMessage()); - } - - @ParameterizedTest - @ValueSource(strings = {"abc12", "가나다12"}) - void 닉네임은_영문과_한글만_가능하다(String nickname) { - assertThatThrownBy(() -> new Member(nickname)) - .isInstanceOf(DTClientErrorException.class) - .hasMessage(ClientErrorCode.INVALID_MEMBER_NICKNAME_FORM.getMessage()); - } - } -} diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java index b79cc05b..84bc003d 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTableTest.java @@ -18,7 +18,7 @@ class Validate { @ParameterizedTest @ValueSource(strings = {"a bc가다9", "가0나 다ab"}) void 테이블_이름은_영문과_한글_숫자_띄어쓰기만_가능하다(String name) { - Member member = new Member("member"); + Member member = new Member("default@gmail.com"); assertThatCode(() -> new ParliamentaryTable(member, name, "agenda", 10, true, true)) .doesNotThrowAnyException(); } @@ -26,7 +26,7 @@ class Validate { @ParameterizedTest @ValueSource(ints = {0, ParliamentaryTable.NAME_MAX_LENGTH + 1}) void 테이블_이름은_정해진_길이_이내여야_한다(int length) { - Member member = new Member("member"); + Member member = new Member("default@gmail.com"); assertThatThrownBy(() -> new ParliamentaryTable(member, "f".repeat(length), "agenda", 10, true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); @@ -35,7 +35,7 @@ class Validate { @ParameterizedTest @ValueSource(strings = {"", "\t", "\n"}) void 테이블_이름은_적어도_한_자_있어야_한다(String name) { - Member member = new Member("member"); + Member member = new Member("default@gmail.com"); assertThatThrownBy(() -> new ParliamentaryTable(member, name, "agenda", 10, true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_LENGTH.getMessage()); @@ -44,7 +44,7 @@ class Validate { @ParameterizedTest @ValueSource(strings = {"abc@", "가나다*", "abc\tde"}) void 허용된_글자_이외의_문자는_불가능하다(String name) { - Member member = new Member("member"); + Member member = new Member("default@gmail.com"); assertThatThrownBy(() -> new ParliamentaryTable(member, name, "agenda", 10, true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_NAME_FORM.getMessage()); @@ -53,8 +53,8 @@ class Validate { @ParameterizedTest @ValueSource(ints = {0, -1, -60}) void 테이블_시간은_양수만_가능하다(int duration) { - Member member = new Member("member"); - assertThatThrownBy(() -> new ParliamentaryTable(member, "name", "agenda", duration, true, true)) + Member member = new Member("default@gmail.com"); + assertThatThrownBy(() -> new ParliamentaryTable(member, "nickname", "agenda", duration, true, true)) .isInstanceOf(DTClientErrorException.class) .hasMessage(ClientErrorCode.INVALID_TABLE_TIME.getMessage()); } diff --git a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java index eabe945d..c64fa43b 100644 --- a/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java +++ b/src/test/java/com/debatetimer/domain/parliamentary/ParliamentaryTimeBoxesTest.java @@ -18,7 +18,7 @@ class SortedBySequence { @Test void 타임박스의_순서에_따라_정렬된다() { - Member member = new Member("콜리"); + Member member = new Member("default@gmail.com"); ParliamentaryTable testTable = new ParliamentaryTable(member, "토론 테이블", "주제", 1800, true, true); ParliamentaryTimeBox firstBox = new ParliamentaryTimeBox(testTable, 1, Stance.PROS, BoxType.OPENING, 300, 1); diff --git a/src/test/java/com/debatetimer/fixture/CookieGenerator.java b/src/test/java/com/debatetimer/fixture/CookieGenerator.java new file mode 100644 index 00000000..cb3f3cc2 --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/CookieGenerator.java @@ -0,0 +1,30 @@ +package com.debatetimer.fixture; + +import com.debatetimer.controller.tool.cookie.CookieProvider; +import com.debatetimer.controller.tool.jwt.JwtTokenProvider; +import com.debatetimer.dto.member.MemberInfo; +import jakarta.servlet.http.Cookie; +import org.springframework.stereotype.Component; + +@Component +public class CookieGenerator { + + private final JwtTokenProvider jwtTokenProvider; + private final CookieProvider cookieProvider; + + public CookieGenerator() { + this.jwtTokenProvider = new JwtTokenProvider(JwtTokenFixture.TEST_TOKEN_PROPERTIES); + this.cookieProvider = new CookieProvider(); + } + + public Cookie[] generateRefreshCookie(String email) { + String refreshToken = jwtTokenProvider.createRefreshToken(new MemberInfo(email)); + return generateCookie("refreshToken", refreshToken, 100000); + } + + public Cookie[] generateCookie(String cookieName, String value, long expirationMills) { + Cookie[] cookies = new Cookie[1]; + cookies[0] = cookieProvider.createCookie(cookieName, value, expirationMills); + return cookies; + } +} diff --git a/src/test/java/com/debatetimer/fixture/HeaderGenerator.java b/src/test/java/com/debatetimer/fixture/HeaderGenerator.java new file mode 100644 index 00000000..4e641ae5 --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/HeaderGenerator.java @@ -0,0 +1,27 @@ +package com.debatetimer.fixture; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.dto.member.MemberInfo; +import com.debatetimer.controller.tool.jwt.JwtTokenProvider; +import com.debatetimer.controller.tool.cookie.CookieProvider; +import io.restassured.http.Header; +import io.restassured.http.Headers; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Component; + +@Component +public class HeaderGenerator { + + private final JwtTokenProvider jwtTokenProvider; + private final CookieProvider cookieProvider; + + public HeaderGenerator(JwtTokenProvider jwtTokenProvider, CookieProvider cookieProvider) { + this.jwtTokenProvider = jwtTokenProvider; + this.cookieProvider = cookieProvider; + } + + public Headers generateAccessTokenHeader(Member member) { + String accessToken = jwtTokenProvider.createAccessToken(new MemberInfo(member)); + return new Headers(new Header(HttpHeaders.AUTHORIZATION, accessToken)); + } +} diff --git a/src/test/java/com/debatetimer/fixture/JwtTokenFixture.java b/src/test/java/com/debatetimer/fixture/JwtTokenFixture.java new file mode 100644 index 00000000..0c9eedd0 --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/JwtTokenFixture.java @@ -0,0 +1,12 @@ +package com.debatetimer.fixture; + +import com.debatetimer.controller.tool.jwt.JwtTokenProperties; + +public class JwtTokenFixture { + + public static final JwtTokenProperties TEST_TOKEN_PROPERTIES = new JwtTokenProperties( + "test".repeat(8), + 5000, + 10000 + ); +} diff --git a/src/test/java/com/debatetimer/fixture/MemberGenerator.java b/src/test/java/com/debatetimer/fixture/MemberGenerator.java index 8af700ff..d0aa97bb 100644 --- a/src/test/java/com/debatetimer/fixture/MemberGenerator.java +++ b/src/test/java/com/debatetimer/fixture/MemberGenerator.java @@ -13,8 +13,8 @@ public MemberGenerator(MemberRepository memberRepository) { this.memberRepository = memberRepository; } - public Member generate(String nickName) { - Member member = new Member(nickName); + public Member generate(String email) { + Member member = new Member(email); return memberRepository.save(member); } } diff --git a/src/test/java/com/debatetimer/fixture/TokenGenerator.java b/src/test/java/com/debatetimer/fixture/TokenGenerator.java new file mode 100644 index 00000000..67fd994a --- /dev/null +++ b/src/test/java/com/debatetimer/fixture/TokenGenerator.java @@ -0,0 +1,40 @@ +package com.debatetimer.fixture; + +import com.debatetimer.controller.tool.jwt.JwtTokenProvider; +import com.debatetimer.dto.member.MemberInfo; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.util.Date; +import javax.crypto.SecretKey; +import org.springframework.stereotype.Component; + +@Component +public class TokenGenerator { + + private final JwtTokenProvider jwtTokenProvider; + + public TokenGenerator() { + this.jwtTokenProvider = new JwtTokenProvider(JwtTokenFixture.TEST_TOKEN_PROPERTIES); + } + + public String generateRefreshToken(String email) { + return jwtTokenProvider.createRefreshToken(new MemberInfo(email)); + } + + public String generateAccessToken(String email) { + return jwtTokenProvider.createAccessToken(new MemberInfo(email)); + } + + public String generateExpiredToken() { + Date now = new Date(); + Date expiredDate = new Date(now.getTime() - 1000); + SecretKey secretKey = Keys.hmacShaKeyFor("test".repeat(8).getBytes()); + return Jwts.builder() + .setSubject("") + .setIssuedAt(now) + .setExpiration(expiredDate) + .claim("type", "") + .signWith(secretKey) + .compact(); + } +} diff --git a/src/test/java/com/debatetimer/repository/member/MemberRepositoryTest.java b/src/test/java/com/debatetimer/repository/member/MemberRepositoryTest.java new file mode 100644 index 00000000..47f08000 --- /dev/null +++ b/src/test/java/com/debatetimer/repository/member/MemberRepositoryTest.java @@ -0,0 +1,58 @@ +package com.debatetimer.repository.member; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.repository.BaseRepositoryTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class MemberRepositoryTest extends BaseRepositoryTest { + + @Autowired + private MemberRepository memberRepository; + + @Nested + class GetById { + + @Test + void 아이디에_해당하는_멤버를_반환한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + + assertThat(memberRepository.getById(bito.getId()).getId()).isEqualTo(bito.getId()); + } + + @Test + void 아이디에_해당하는_멤버가_없으면_예외를_발생시킨다() { + Member bito = memberGenerator.generate("default@gmail.com"); + + assertThatThrownBy(() -> memberRepository.getById(bito.getId() + 1)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.MEMBER_NOT_FOUND.getMessage()); + } + } + + @Nested + class GetByNickname { + + @Test + void 닉네임에_해당하는_멤버를_반환한다() { + Member bito = memberGenerator.generate("bito@gmail.com"); + + assertThat(memberRepository.getByEmail(bito.getEmail()).getId()).isEqualTo(bito.getId()); + } + + @Test + void 닉네임에_해당하는_멤버가_없으면_예외를_발생시킨다() { + memberGenerator.generate("default@gmail.com"); + + assertThatThrownBy(() -> memberRepository.getByEmail("notbito@gmail.com")) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.MEMBER_NOT_FOUND.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTableRepositoryTest.java b/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTableRepositoryTest.java index 0ccdb063..246a1212 100644 --- a/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTableRepositoryTest.java +++ b/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTableRepositoryTest.java @@ -23,8 +23,8 @@ class FindAllByMember { @Test void 특정_회원의_테이블만_조회한다() { - Member chan = memberGenerator.generate("커찬"); - Member bito = memberGenerator.generate("비토"); + Member chan = memberGenerator.generate("default@gmail.com"); + Member bito = memberGenerator.generate("default2@gmail.com"); ParliamentaryTable chanTable1 = tableGenerator.generate(chan); ParliamentaryTable chanTable2 = tableGenerator.generate(chan); ParliamentaryTable bitoTable = tableGenerator.generate(bito); @@ -40,10 +40,10 @@ class GetById { @Test void 특정_아이디의_테이블을_조회한다() { - Member chan = memberGenerator.generate("커찬"); + Member chan = memberGenerator.generate("default@gmail.com"); ParliamentaryTable chanTable = tableGenerator.generate(chan); - ParliamentaryTable foundChanTable = tableRepository.getById(chanTable.getId().longValue()); + ParliamentaryTable foundChanTable = tableRepository.getById(chanTable.getId()); assertThat(foundChanTable).usingRecursiveComparison().isEqualTo(chanTable); } diff --git a/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepositoryTest.java b/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepositoryTest.java index 07cac513..17c09566 100644 --- a/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepositoryTest.java +++ b/src/test/java/com/debatetimer/repository/parliamentary/ParliamentaryTimeBoxRepositoryTest.java @@ -21,8 +21,8 @@ class FindAllByParliamentaryTable { @Test void 특정_테이블의_타임박스를_모두_조회한다() { - Member chan = memberGenerator.generate("커찬"); - Member bito = memberGenerator.generate("비토"); + Member chan = memberGenerator.generate("default@gmail.com"); + Member bito = memberGenerator.generate("default2@gmail.com"); ParliamentaryTable chanTable = tableGenerator.generate(chan); ParliamentaryTable bitoTable = tableGenerator.generate(bito); ParliamentaryTimeBox chanBox1 = timeBoxGenerator.generate(chanTable, 1); diff --git a/src/test/java/com/debatetimer/service/BaseServiceTest.java b/src/test/java/com/debatetimer/service/BaseServiceTest.java index e68e262a..4e2a2d65 100644 --- a/src/test/java/com/debatetimer/service/BaseServiceTest.java +++ b/src/test/java/com/debatetimer/service/BaseServiceTest.java @@ -1,9 +1,11 @@ package com.debatetimer.service; import com.debatetimer.DataBaseCleaner; +import com.debatetimer.fixture.CookieGenerator; import com.debatetimer.fixture.MemberGenerator; import com.debatetimer.fixture.ParliamentaryTableGenerator; import com.debatetimer.fixture.ParliamentaryTimeBoxGenerator; +import com.debatetimer.fixture.TokenGenerator; import com.debatetimer.repository.member.MemberRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTableRepository; import com.debatetimer.repository.parliamentary.ParliamentaryTimeBoxRepository; @@ -32,4 +34,10 @@ public abstract class BaseServiceTest { @Autowired protected ParliamentaryTimeBoxGenerator timeBoxGenerator; + + @Autowired + protected TokenGenerator tokenGenerator; + + @Autowired + protected CookieGenerator cookieGenerator; } diff --git a/src/test/java/com/debatetimer/service/auth/AuthServiceTest.java b/src/test/java/com/debatetimer/service/auth/AuthServiceTest.java new file mode 100644 index 00000000..97f545a9 --- /dev/null +++ b/src/test/java/com/debatetimer/service/auth/AuthServiceTest.java @@ -0,0 +1,43 @@ +package com.debatetimer.service.auth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.debatetimer.domain.member.Member; +import com.debatetimer.exception.custom.DTClientErrorException; +import com.debatetimer.exception.errorcode.ClientErrorCode; +import com.debatetimer.service.BaseServiceTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class AuthServiceTest extends BaseServiceTest { + + @Autowired + private AuthService authService; + + @Nested + class GetMember { + + @Test + void 이메일에_해당하는_멤버가_있으면_해당_멤버를_반환한다() { + Member bito = memberGenerator.generate("default@gmail.com"); + + assertThat(authService.getMember(bito.getEmail()).getId()).isEqualTo(bito.getId()); + } + } + + @Nested + class Logout { + + @Test + void 이메일과_멤버의_정보가_다르면_예외를_발생시킨다() { + Member bito = memberGenerator.generate("bito@gmail.com"); + String email = "default@gmail.com"; + + assertThatThrownBy(() -> authService.logout(bito, email)) + .isInstanceOf(DTClientErrorException.class) + .hasMessage(ClientErrorCode.UNAUTHORIZED_MEMBER.getMessage()); + } + } +} diff --git a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java index 0282f2e9..43ea0767 100644 --- a/src/test/java/com/debatetimer/service/member/MemberServiceTest.java +++ b/src/test/java/com/debatetimer/service/member/MemberServiceTest.java @@ -5,8 +5,8 @@ import com.debatetimer.domain.member.Member; import com.debatetimer.domain.parliamentary.ParliamentaryTable; -import com.debatetimer.dto.member.MemberCreateRequest; import com.debatetimer.dto.member.MemberCreateResponse; +import com.debatetimer.dto.member.MemberInfo; import com.debatetimer.dto.member.TableResponses; import com.debatetimer.service.BaseServiceTest; import java.util.Optional; @@ -24,21 +24,21 @@ class CreateMember { @Test void 회원를_생성한다() { - MemberCreateRequest request = new MemberCreateRequest("커찬"); + MemberInfo request = new MemberInfo("default@gmail.com"); MemberCreateResponse actual = memberService.createMember(request); Optional foundMember = memberRepository.findById(actual.id()); assertAll( - () -> assertThat(actual.nickname()).isEqualTo(request.nickname()), + () -> assertThat(actual.email()).isEqualTo(request.email()), () -> assertThat(foundMember).isPresent() ); } @Test void 기존_닉네임을_가진_회원이_있다면_해당_회원을_반환한다() { - Member existedMember = memberGenerator.generate("커찬"); - MemberCreateRequest request = new MemberCreateRequest("커찬"); + Member existedMember = memberGenerator.generate("default@gmail.com"); + MemberInfo request = new MemberInfo("default@gmail.com"); MemberCreateResponse actual = memberService.createMember(request); @@ -51,13 +51,9 @@ class GetTables { @Test void 회원의_전체_토론_시간표를_조회한다() { - Member member = memberRepository.save(new Member("커찬")); - parliamentaryTableRepository.save(new ParliamentaryTable( - member, "토론 시간표 A", "주제", 1800, true, true - )); - parliamentaryTableRepository.save(new ParliamentaryTable( - member, "토론 시간표 B", "주제", 1900, true, true - )); + Member member = memberRepository.save(new Member("default@gmail.com")); + parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 A", "주제", 1800, true, true)); + parliamentaryTableRepository.save(new ParliamentaryTable(member, "토론 시간표 B", "주제", 1900, true, true)); TableResponses response = memberService.getTables(member.getId()); diff --git a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java index 3c8345df..1539ed88 100644 --- a/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java +++ b/src/test/java/com/debatetimer/service/parliamentary/ParliamentaryServiceTest.java @@ -32,7 +32,12 @@ class Save { @Test void 의회식_토론_테이블을_생성한다() { - Member chan = memberGenerator.generate("커찬"); + Member chan = memberGenerator.generate("default@gmail.com"); + TableInfoCreateRequest requestTableInfo = new TableInfoCreateRequest("커찬의 테이블", "주제", true, true); + List requestTimeBoxes = List.of( + new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), + new TimeBoxCreateRequest(Stance.CONS, BoxType.OPENING, 3, 1) + ); ParliamentaryTableCreateRequest chanTableRequest = new ParliamentaryTableCreateRequest( new TableInfoCreateRequest("커찬의 테이블", "주제", true, true), List.of(new TimeBoxCreateRequest(Stance.PROS, BoxType.OPENING, 3, 1), @@ -54,7 +59,7 @@ class FindTable { @Test void 의회식_토론_테이블을_조회한다() { - Member chan = memberGenerator.generate("커찬"); + Member chan = memberGenerator.generate("default@gmail.com"); ParliamentaryTable chanTable = tableGenerator.generate(chan); timeBoxGenerator.generate(chanTable, 1); timeBoxGenerator.generate(chanTable, 2); @@ -69,8 +74,8 @@ class FindTable { @Test void 회원_소유가_아닌_테이블_조회_시_예외를_발생시킨다() { - Member chan = memberGenerator.generate("커찬"); - Member coli = memberGenerator.generate("콜리"); + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); ParliamentaryTable chanTable = tableGenerator.generate(chan); long chanTableId = chanTable.getId(); @@ -85,7 +90,7 @@ class UpdateTable { @Test void 의회식_토론_테이블을_수정한다() { - Member chan = memberGenerator.generate("커찬"); + Member chan = memberGenerator.generate("default@gmail.com"); ParliamentaryTable chanTable = tableGenerator.generate(chan); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( new TableInfoCreateRequest("커찬의 테이블", "주제", true, true), @@ -107,8 +112,8 @@ class UpdateTable { @Test void 회원_소유가_아닌_테이블_수정_시_예외를_발생시킨다() { - Member chan = memberGenerator.generate("커찬"); - Member coli = memberGenerator.generate("콜리"); + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); ParliamentaryTable chanTable = tableGenerator.generate(chan); long chanTableId = chanTable.getId(); ParliamentaryTableCreateRequest renewTableRequest = new ParliamentaryTableCreateRequest( @@ -127,7 +132,7 @@ class DeleteTable { @Test void 의회식_토론_테이블을_삭제한다() { - Member chan = memberGenerator.generate("커찬"); + Member chan = memberGenerator.generate("default@gmail.com"); ParliamentaryTable chanTable = tableGenerator.generate(chan); timeBoxGenerator.generate(chanTable, 1); timeBoxGenerator.generate(chanTable, 2); @@ -145,8 +150,8 @@ class DeleteTable { @Test void 회원_소유가_아닌_테이블_삭제_시_예외를_발생시킨다() { - Member chan = memberGenerator.generate("커찬"); - Member coli = memberGenerator.generate("콜리"); + Member chan = memberGenerator.generate("default@gmail.com"); + Member coli = memberGenerator.generate("default2@gmail.com"); ParliamentaryTable chanTable = tableGenerator.generate(chan); Long chanTableId = chanTable.getId(); diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index e105e48e..1b0ceaf0 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -24,3 +24,8 @@ spring: cors: origin: http://test.debate-timer.com + +jwt: + secret_key: testtesttesttesttesttesttesttest + access_token_expiration_millis: 5000 + refresh_token_expiration_millis: 10000 From a0494617c203aac16b7f2db74e8bed023c731771 Mon Sep 17 00:00:00 2001 From: SANGHUN OH <121424793+unifolio0@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:18:42 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[FIX]=20yml=ED=8C=8C=EC=9D=BC=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 6 +++--- src/main/resources/application-prod.yml | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 0b514898..79925b40 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -25,6 +25,6 @@ oauth: grant_type: ${secret.oauth.grant_type} jwt: - secret_key: ${secret.secret_key} - access_token_expiration_millis: ${secret.access_token_expiration_millis} - refresh_token_expiration_millis: ${secret.refresh_token_expiration_millis} + secret_key: ${secret.jwt.secret_key} + access_token_expiration_millis: ${secret.jwt.access_token_expiration_millis} + refresh_token_expiration_millis: ${secret.jwt.refresh_token_expiration_millis} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 1dce30a9..c23b5bde 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -17,3 +17,15 @@ spring: cors: origin: ${secret.cors.origin} + +oauth: + client_id: ${secret.oauth.client_id} + client_secret: ${secret.oauth.client_secret} + redirect_uri: ${secret.oauth.redirect_uri} + grant_type: ${secret.oauth.grant_type} + +jwt: + secret_key: ${secret.jwt.secret_key} + access_token_expiration_millis: ${secret.jwt.access_token_expiration_millis} + refresh_token_expiration_millis: ${secret.jwt.refresh_token_expiration_millis} + From ace3988c72b215b89a0aa9030ace824e84a2c613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Sun, 2 Feb 2025 18:14:13 +0900 Subject: [PATCH 5/8] =?UTF-8?q?chore:=20db=20=EB=B0=A9=EC=96=B8=20?= =?UTF-8?q?=EC=A7=81=EC=A0=91=20=EC=A7=80=EC=A0=95=20(#86)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 79925b40..5aa0987d 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -11,6 +11,7 @@ spring: properties: hibernate: format_sql: true + dialect: org.hibernate.dialect.MySQLDialect hibernate: ddl-auto: update defer-datasource-initialization: true From 5a26884a81c6f26c3cb1bed7c12c15d6cdc9e767 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B1=B4=EC=9A=B0?= Date: Mon, 3 Feb 2025 16:03:36 +0900 Subject: [PATCH 6/8] =?UTF-8?q?[FIX]=20redirect=5Furl=5Fmismatch=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20(#89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ src/main/java/com/debatetimer/client/OAuthProperties.java | 5 +---- .../java/com/debatetimer/dto/member/MemberCreateRequest.java | 5 ++++- .../debatetimer/controller/member/MemberControllerTest.java | 2 +- .../debatetimer/controller/member/MemberDocumentTest.java | 5 +++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 4e91a957..671e0e9f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ out/ ### Rest Docs /src/main/resources/static/docs/openapi3.yaml + +### application-local.yml +/src/main/resources/application-local.yml diff --git a/src/main/java/com/debatetimer/client/OAuthProperties.java b/src/main/java/com/debatetimer/client/OAuthProperties.java index 22459aee..a747f84a 100644 --- a/src/main/java/com/debatetimer/client/OAuthProperties.java +++ b/src/main/java/com/debatetimer/client/OAuthProperties.java @@ -14,17 +14,14 @@ public class OAuthProperties { private final String clientId; private final String clientSecret; - private final String redirectUri; private final String grantType; public OAuthProperties( String clientId, String clientSecret, - String redirectUri, String grantType) { this.clientId = clientId; this.clientSecret = clientSecret; - this.redirectUri = redirectUri; this.grantType = grantType; } @@ -35,7 +32,7 @@ public MultiValueMap createTokenRequestBody(MemberCreateRequest MultiValueMap map = new LinkedMultiValueMap<>(); map.add("grant_type", grantType); map.add("client_id", clientId); - map.add("redirect_uri", redirectUri); + map.add("redirect_uri", request.redirectUrl()); map.add("code", decodedVerificationCode); map.add("client_secret", clientSecret); diff --git a/src/main/java/com/debatetimer/dto/member/MemberCreateRequest.java b/src/main/java/com/debatetimer/dto/member/MemberCreateRequest.java index 7ba25126..d9923d1a 100644 --- a/src/main/java/com/debatetimer/dto/member/MemberCreateRequest.java +++ b/src/main/java/com/debatetimer/dto/member/MemberCreateRequest.java @@ -2,6 +2,9 @@ import jakarta.validation.constraints.NotBlank; -public record MemberCreateRequest(@NotBlank String code) { +public record MemberCreateRequest( + @NotBlank String code, + @NotBlank String redirectUrl +) { } diff --git a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java index b29bc223..8f463980 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberControllerTest.java @@ -44,7 +44,7 @@ class CreateMember { @Test void 회원을_생성한다() { - MemberCreateRequest request = new MemberCreateRequest("gnkldsnglnksl"); + MemberCreateRequest request = new MemberCreateRequest("gnkldsnglnksl", "http://redirectUrl"); OAuthToken oAuthToken = new OAuthToken("accessToken"); MemberInfo memberInfo = new MemberInfo("default@gmail.com"); doReturn(oAuthToken).when(oAuthClient).requestToken(request); diff --git a/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java b/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java index 0e9c9cb5..c31c89c0 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java @@ -39,7 +39,8 @@ class CreateMember { .tag(Tag.MEMBER_API) .summary("멤버 생성") .requestBodyField( - fieldWithPath("code").type(STRING).description("인가 코드") + fieldWithPath("code").type(STRING).description("인가 코드"), + fieldWithPath("redirectUrl").type(STRING).description("리다이렉트 URL") ); private final RestDocumentationResponse responseDocument = response() @@ -50,7 +51,7 @@ class CreateMember { @Test void 회원_생성_성공() { - MemberCreateRequest request = new MemberCreateRequest("dfsfgdsg"); + MemberCreateRequest request = new MemberCreateRequest("dfsfgdsg", "http://redirectUrl"); MemberInfo memberInfo = new MemberInfo(EXIST_MEMBER_EMAIL); MemberCreateResponse response = new MemberCreateResponse(EXIST_MEMBER_ID, EXIST_MEMBER_EMAIL); doReturn(memberInfo).when(authService).getMemberInfo(request); From 2299ddc86ac5d5a997b93d0beb660d31f3ed3e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=BB=A4=EC=B0=AC?= <44027393+leegwichan@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:06:40 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[FIX]=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=EC=97=90=20=ED=97=A4=EB=8D=94=20=EB=B0=8F=20?= =?UTF-8?q?=EC=BF=A0=ED=82=A4=20=EA=B3=B5=EA=B0=9C=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?fix=20=EA=B8=B0=EB=8A=A5=20=EC=88=98=EC=A0=95=20(#91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/debatetimer/config/CorsConfig.java | 4 ++- .../controller/member/MemberController.java | 14 +++++----- .../controller/tool/cookie/CookieManager.java | 5 ++-- .../tool/cookie/CookieProvider.java | 27 +++++++++++-------- .../controller/BaseDocumentTest.java | 10 +++++++ .../controller/GlobalControllerTest.java | 3 ++- .../controller/member/MemberDocumentTest.java | 6 ++--- .../debatetimer/fixture/CookieGenerator.java | 12 ++++++--- 8 files changed, 53 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/debatetimer/config/CorsConfig.java b/src/main/java/com/debatetimer/config/CorsConfig.java index 29fbf866..a63eb1d1 100644 --- a/src/main/java/com/debatetimer/config/CorsConfig.java +++ b/src/main/java/com/debatetimer/config/CorsConfig.java @@ -2,6 +2,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -28,7 +29,8 @@ public void addCorsMappings(CorsRegistry registry) { HttpMethod.OPTIONS.name() ) .allowCredentials(true) - .allowedHeaders("*"); + .allowedHeaders("*") + .exposedHeaders(HttpHeaders.AUTHORIZATION); } } diff --git a/src/main/java/com/debatetimer/controller/member/MemberController.java b/src/main/java/com/debatetimer/controller/member/MemberController.java index 10e05a28..25d12555 100644 --- a/src/main/java/com/debatetimer/controller/member/MemberController.java +++ b/src/main/java/com/debatetimer/controller/member/MemberController.java @@ -11,12 +11,12 @@ import com.debatetimer.dto.member.TableResponses; import com.debatetimer.service.auth.AuthService; import com.debatetimer.service.member.MemberService; -import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -43,10 +43,10 @@ public MemberCreateResponse createMember(@RequestBody MemberCreateRequest reques MemberInfo memberInfo = authService.getMemberInfo(request); MemberCreateResponse memberCreateResponse = memberService.createMember(memberInfo); JwtTokenResponse jwtTokenResponse = authManager.issueToken(memberInfo); - Cookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(jwtTokenResponse.refreshToken()); + ResponseCookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(jwtTokenResponse.refreshToken()); response.addHeader(HttpHeaders.AUTHORIZATION, jwtTokenResponse.accessToken()); - response.addCookie(refreshTokenCookie); + response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); return memberCreateResponse; } @@ -54,10 +54,10 @@ public MemberCreateResponse createMember(@RequestBody MemberCreateRequest reques public void reissueAccessToken(HttpServletRequest request, HttpServletResponse response) { String refreshToken = cookieManager.extractRefreshToken(request.getCookies()); JwtTokenResponse jwtTokenResponse = authManager.reissueToken(refreshToken); - Cookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(jwtTokenResponse.refreshToken()); + ResponseCookie refreshTokenCookie = cookieManager.createRefreshTokenCookie(jwtTokenResponse.refreshToken()); response.addHeader(HttpHeaders.AUTHORIZATION, jwtTokenResponse.accessToken()); - response.addCookie(refreshTokenCookie); + response.addHeader(HttpHeaders.SET_COOKIE, refreshTokenCookie.toString()); } @PostMapping("/api/member/logout") @@ -66,8 +66,8 @@ public void logout(@AuthMember Member member, HttpServletRequest request, HttpSe String refreshToken = cookieManager.extractRefreshToken(request.getCookies()); String email = authManager.resolveRefreshToken(refreshToken); authService.logout(member, email); - Cookie deletedRefreshTokenCookie = cookieManager.deleteRefreshTokenCookie(); + ResponseCookie deletedRefreshTokenCookie = cookieManager.deleteRefreshTokenCookie(); - response.addCookie(deletedRefreshTokenCookie); + response.addHeader(HttpHeaders.SET_COOKIE, deletedRefreshTokenCookie.toString()); } } diff --git a/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java b/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java index 1a6d4520..c54ee6f8 100644 --- a/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java +++ b/src/main/java/com/debatetimer/controller/tool/cookie/CookieManager.java @@ -3,6 +3,7 @@ import com.debatetimer.controller.tool.jwt.JwtTokenProperties; import jakarta.servlet.http.Cookie; import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Service; @Service @@ -15,7 +16,7 @@ public class CookieManager { private final CookieExtractor cookieExtractor; private final JwtTokenProperties jwtTokenProperties; - public Cookie createRefreshTokenCookie(String token) { + public ResponseCookie createRefreshTokenCookie(String token) { return cookieProvider.createCookie(REFRESH_TOKEN_COOKIE_NAME, token, jwtTokenProperties.getRefreshTokenExpirationMillis()); } @@ -24,7 +25,7 @@ public String extractRefreshToken(Cookie[] cookies) { return cookieExtractor.extractCookie(REFRESH_TOKEN_COOKIE_NAME, cookies); } - public Cookie deleteRefreshTokenCookie() { + public ResponseCookie deleteRefreshTokenCookie() { return cookieProvider.deleteCookie(REFRESH_TOKEN_COOKIE_NAME); } } diff --git a/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java b/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java index a41f006a..6fd4456d 100644 --- a/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java +++ b/src/main/java/com/debatetimer/controller/tool/cookie/CookieProvider.java @@ -1,6 +1,7 @@ package com.debatetimer.controller.tool.cookie; -import jakarta.servlet.http.Cookie; +import java.time.Duration; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @Component @@ -8,17 +9,21 @@ public class CookieProvider { private static final String PATH = "/"; - public Cookie createCookie(String cookieName, String token, long expirationMillis) { - Cookie cookie = new Cookie(cookieName, token); - cookie.setMaxAge((int) (expirationMillis / 1000)); - cookie.setPath(PATH); - return cookie; + public ResponseCookie createCookie(String cookieName, String token, long expirationMillis) { + return ResponseCookie.from(cookieName, token) + .maxAge(Duration.ofMillis(expirationMillis)) + .path(PATH) + .sameSite("None") + .secure(true) + .build(); } - public Cookie deleteCookie(String cookieName) { - Cookie cookie = new Cookie(cookieName, ""); - cookie.setMaxAge(0); - cookie.setPath(PATH); - return cookie; + public ResponseCookie deleteCookie(String cookieName) { + return ResponseCookie.from(cookieName, "") + .maxAge(0) + .path(PATH) + .sameSite("None") + .secure(true) + .build(); } } diff --git a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java index a692a6a4..f7d69304 100644 --- a/src/test/java/com/debatetimer/controller/BaseDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/BaseDocumentTest.java @@ -26,6 +26,7 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; import org.springframework.restdocs.restassured.RestAssuredRestDocumentation; @@ -96,6 +97,15 @@ private void setLoginMember() { doReturn(EXIST_MEMBER).when(authService).getMember(EXIST_MEMBER_EMAIL); } + protected ResponseCookie responseCookie(String token, int maxAge) { + return ResponseCookie.from("refreshToken", token) + .path("/") + .maxAge(maxAge) + .sameSite("None") + .secure(true) + .build(); + } + protected RestDocumentationRequest request() { return new RestDocumentationRequest(); } diff --git a/src/test/java/com/debatetimer/controller/GlobalControllerTest.java b/src/test/java/com/debatetimer/controller/GlobalControllerTest.java index d4347a38..6c9c041f 100644 --- a/src/test/java/com/debatetimer/controller/GlobalControllerTest.java +++ b/src/test/java/com/debatetimer/controller/GlobalControllerTest.java @@ -25,7 +25,8 @@ class CorsConfigTest { .when().options("/") .then().statusCode(200) .headers("Access-Control-Allow-Origin", corsOrigin) - .header("Access-Control-Allow-Methods", containsString(allowedMethod)); + .header("Access-Control-Allow-Methods", containsString(allowedMethod)) + .header("Access-Control-Expose-Headers", containsString("Authorization")); } @Test diff --git a/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java b/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java index c31c89c0..c4504b6f 100644 --- a/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java +++ b/src/test/java/com/debatetimer/controller/member/MemberDocumentTest.java @@ -57,7 +57,7 @@ class CreateMember { doReturn(memberInfo).when(authService).getMemberInfo(request); doReturn(response).when(memberService).createMember(memberInfo); doReturn(EXIST_MEMBER_TOKEN_RESPONSE).when(authManager).issueToken(memberInfo); - doReturn(EXIST_MEMBER_COOKIE).when(cookieManager).createRefreshTokenCookie(EXIST_MEMBER_REFRESH_TOKEN); + doReturn(responseCookie(EXIST_MEMBER_REFRESH_TOKEN, 500)).when(cookieManager).createRefreshTokenCookie(EXIST_MEMBER_REFRESH_TOKEN); var document = document("member/create", 201).request(requestDocument).response(responseDocument).build(); @@ -143,7 +143,7 @@ class ReissueAccessToken { @Test void 토큰_갱신_성공() { doReturn(EXIST_MEMBER_TOKEN_RESPONSE).when(authManager).reissueToken(any()); - doReturn(EXIST_MEMBER_COOKIE).when(cookieManager).createRefreshTokenCookie(any()); + doReturn(responseCookie(EXIST_MEMBER_REFRESH_TOKEN, 500)).when(cookieManager).createRefreshTokenCookie(any()); var document = document("member/logout", 204) .request(requestDocument) @@ -205,7 +205,7 @@ class Logout { @Test void 로그아웃_성공() { - doReturn(DELETE_MEMBER_COOKIE).when(cookieManager).deleteRefreshTokenCookie(); + doReturn(responseCookie(EXIST_MEMBER_REFRESH_TOKEN, 0)).when(cookieManager).deleteRefreshTokenCookie(); var document = document("member/logout", 204) .request(requestDocument) diff --git a/src/test/java/com/debatetimer/fixture/CookieGenerator.java b/src/test/java/com/debatetimer/fixture/CookieGenerator.java index cb3f3cc2..b8999b4e 100644 --- a/src/test/java/com/debatetimer/fixture/CookieGenerator.java +++ b/src/test/java/com/debatetimer/fixture/CookieGenerator.java @@ -4,6 +4,7 @@ import com.debatetimer.controller.tool.jwt.JwtTokenProvider; import com.debatetimer.dto.member.MemberInfo; import jakarta.servlet.http.Cookie; +import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @Component @@ -23,8 +24,13 @@ public Cookie[] generateRefreshCookie(String email) { } public Cookie[] generateCookie(String cookieName, String value, long expirationMills) { - Cookie[] cookies = new Cookie[1]; - cookies[0] = cookieProvider.createCookie(cookieName, value, expirationMills); - return cookies; + ResponseCookie responseCookie = cookieProvider.createCookie(cookieName, value, expirationMills); + + Cookie servletCookie = new Cookie(responseCookie.getName(), responseCookie.getValue()); + servletCookie.setMaxAge((int) (expirationMills / 1000)); + servletCookie.setPath(responseCookie.getPath()); + servletCookie.setSecure(responseCookie.isSecure()); + servletCookie.setHttpOnly(responseCookie.isHttpOnly()); + return new Cookie[]{servletCookie}; } } From acb2affb215416f6c8bc73f864a6f6a1f2990f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=BB=A4=EC=B0=AC?= <44027393+leegwichan@users.noreply.github.com> Date: Wed, 5 Feb 2025 00:01:05 +0900 Subject: [PATCH 8/8] =?UTF-8?q?[DOCS]=20Swagger=20host=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 88e52ce4..031c7a72 100644 --- a/build.gradle +++ b/build.gradle @@ -51,6 +51,7 @@ dependencies { implementation 'org.apache.poi:poi-ooxml:5.2.3' implementation 'org.apache.poi:poi:5.2.3' + // JWT implementation 'io.jsonwebtoken:jjwt-api:0.11.5' implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' implementation 'io.jsonwebtoken:jjwt-gson:0.11.5' @@ -103,7 +104,20 @@ generateSwaggerUI { } openapi3 { - server = "http://localhost:8080" + servers = [ + { + url = "https://api.dev.debate-timer.com" + description = "Dev Server" + }, + { + url = "https://api.prod.debate-timer.com" + description = "Prod Server" + }, + { + url = "http://localhost:8080" + description = "Local Server" + } + ] title = "토론 타이머 API" description = "토론 타이머 API" version = "0.0.1"