diff --git a/src/docs/asciidoc/api.adoc b/src/docs/asciidoc/api.adoc index e6585a09..d9e2b5da 100644 --- a/src/docs/asciidoc/api.adoc +++ b/src/docs/asciidoc/api.adoc @@ -94,6 +94,10 @@ operation::league-team-controller-test/리그팀을_삭제한다[snippets='http- operation::league-query-controller-test/리그를_하나_조회한다[snippets='http-request,path-parameters,http-response,response-fields'] +=== 리그 삭제 + +operation::league-controller-test/리그를_삭제한다[snippets='http-request,path-parameters,http-response'] + === 리그팀 상세 조회 operation::league-query-controller-test/리그팀을_상세_조회한다[snippets='http-request,path-parameters,http-response,response-fields'] diff --git a/src/main/java/com/sports/server/auth/config/SecurityConfig.java b/src/main/java/com/sports/server/auth/config/SecurityConfig.java index 6a566bfa..b145babf 100644 --- a/src/main/java/com/sports/server/auth/config/SecurityConfig.java +++ b/src/main/java/com/sports/server/auth/config/SecurityConfig.java @@ -42,6 +42,7 @@ SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc .requestMatchers(mvc.pattern("/manager/**"), mvc.pattern(HttpMethod.GET, "/members/info"), mvc.pattern(HttpMethod.POST, "/leagues"), + mvc.pattern(HttpMethod.DELETE, "/leagues/{leagueId}"), mvc.pattern(HttpMethod.POST, "/leagues/*/teams"), mvc.pattern(HttpMethod.POST, "/leagues/{leagueId}/teams"), mvc.pattern(HttpMethod.PUT, "/leagues/{leagueId}/teams/{teamId}"), diff --git a/src/main/java/com/sports/server/command/league/application/LeagueService.java b/src/main/java/com/sports/server/command/league/application/LeagueService.java index c65c26b6..15d1142d 100644 --- a/src/main/java/com/sports/server/command/league/application/LeagueService.java +++ b/src/main/java/com/sports/server/command/league/application/LeagueService.java @@ -1,25 +1,34 @@ package com.sports.server.command.league.application; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - +import com.sports.server.auth.exception.AuthorizationErrorMessages; +import com.sports.server.command.league.domain.League; import com.sports.server.command.league.domain.LeagueRepository; import com.sports.server.command.league.dto.LeagueRequestDto; import com.sports.server.command.member.domain.Member; import com.sports.server.command.organization.domain.Organization; import com.sports.server.common.application.EntityUtils; - +import com.sports.server.common.exception.UnauthorizedException; import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @Transactional @RequiredArgsConstructor public class LeagueService { - private final EntityUtils entityUtils; - private final LeagueRepository leagueRepository; + private final EntityUtils entityUtils; + private final LeagueRepository leagueRepository; + + public void register(final Member manager, final LeagueRequestDto.Register request) { + Organization organization = entityUtils.getEntity(request.organizationId(), Organization.class); + leagueRepository.save(request.toEntity(manager, organization)); + } - public void register(final Member manager, final LeagueRequestDto.Register request) { - Organization organization = entityUtils.getEntity(request.organizationId(), Organization.class); - leagueRepository.save(request.toEntity(manager, organization)); - } + public void delete(final Member manager, final Long leagueId) { + League league = entityUtils.getEntity(leagueId, League.class); + if (!league.isManagedBy(manager)) { + throw new UnauthorizedException(AuthorizationErrorMessages.PERMISSION_DENIED); + } + leagueRepository.delete(league); + } } diff --git a/src/main/java/com/sports/server/command/league/domain/League.java b/src/main/java/com/sports/server/command/league/domain/League.java index 311ee866..96a80bde 100644 --- a/src/main/java/com/sports/server/command/league/domain/League.java +++ b/src/main/java/com/sports/server/command/league/domain/League.java @@ -13,11 +13,13 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.Where; @Entity @Table(name = "leagues") @Where(clause = "is_deleted = 0") +@SQLDelete(sql = "UPDATE leagues SET is_deleted = 1 WHERE id = ?") @NoArgsConstructor(access = AccessLevel.PROTECTED) @Getter public class League extends BaseEntity { @@ -48,25 +50,33 @@ public class League extends BaseEntity { @Column(name = "is_deleted", nullable = false) private boolean isDeleted; - public League( - final Member manager, - final Organization organization, - final String name, - final LocalDateTime startAt, - final LocalDateTime endAt, - final Round maxRound - ) { - this.manager = manager; - this.organization = organization; - this.name = name; - this.startAt = startAt; - this.endAt = endAt; - this.maxRound = maxRound; - this.inProgressRound = maxRound; - this.isDeleted = false; - } + public League( + final Member manager, + final Organization organization, + final String name, + final LocalDateTime startAt, + final LocalDateTime endAt, + final Round maxRound + ) { + this.manager = manager; + this.organization = organization; + this.name = name; + this.startAt = startAt; + this.endAt = endAt; + this.maxRound = maxRound; + this.inProgressRound = maxRound; + this.isDeleted = false; + } - public boolean isManagedBy(Member manager) { - return this.manager.equals(manager); - } + public boolean isManagedBy(Member manager) { + return this.manager.equals(manager); + } + + public void delete() { + this.isDeleted = true; + } + + public String manager() { + return manager.getEmail(); + } } diff --git a/src/main/java/com/sports/server/command/league/domain/LeagueRepository.java b/src/main/java/com/sports/server/command/league/domain/LeagueRepository.java index 692edde2..cf0e858f 100644 --- a/src/main/java/com/sports/server/command/league/domain/LeagueRepository.java +++ b/src/main/java/com/sports/server/command/league/domain/LeagueRepository.java @@ -3,5 +3,7 @@ import org.springframework.data.repository.Repository; public interface LeagueRepository extends Repository { - void save(League league); + void save(League league); + + void delete(League league); } diff --git a/src/main/java/com/sports/server/command/league/presentation/LeagueController.java b/src/main/java/com/sports/server/command/league/presentation/LeagueController.java index 47c7551e..49736b29 100644 --- a/src/main/java/com/sports/server/command/league/presentation/LeagueController.java +++ b/src/main/java/com/sports/server/command/league/presentation/LeagueController.java @@ -1,24 +1,32 @@ package com.sports.server.command.league.presentation; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RestController; - import com.sports.server.command.league.application.LeagueService; import com.sports.server.command.league.dto.LeagueRequestDto; import com.sports.server.command.member.domain.Member; - import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor +@RequestMapping("/leagues") public class LeagueController { - private final LeagueService leagueService; + private final LeagueService leagueService; + + @PostMapping + public ResponseEntity register(@RequestBody final LeagueRequestDto.Register register, Member member) { + leagueService.register(member, register); + return ResponseEntity.ok().build(); + } - @PostMapping("/leagues") - public ResponseEntity register(@RequestBody final LeagueRequestDto.Register register, Member member) { - leagueService.register(member, register); - return ResponseEntity.ok().build(); - } + @DeleteMapping("/{leagueId}") + public ResponseEntity delete(final Member member, @PathVariable final Long leagueId) { + leagueService.delete(member, leagueId); + return ResponseEntity.ok().build(); + } } diff --git a/src/main/resources/static/docs/api.html b/src/main/resources/static/docs/api.html index 0fc65b6a..49b50474 100644 --- a/src/main/resources/static/docs/api.html +++ b/src/main/resources/static/docs/api.html @@ -489,6 +489,7 @@

훕치치 서버 API 문서

  • 리그팀 삭제
  • 리그 상세 조회
  • 리그팀 상세 조회
  • +
  • 리그 삭제
  • 타임라인 API @@ -1585,7 +1586,7 @@

    POST /leagues HTTP/1.1
     Content-Type: application/json
    -Content-Length: 178
    +Content-Length: 179
     Host: www.api.hufstreaming.site
     Cookie: HCC_SES=temp-cookie
     
    @@ -1593,8 +1594,8 @@ 

    @@ -1912,11 +1913,6 @@

    String

    리그의 팀 로고 이미지 URL®

    - -

    [].sizeOfLeagueTeamPlayers

    -

    Number

    -

    리그팀 선수의 인원수

    - @@ -2306,6 +2302,53 @@

    +

    리그 삭제

    +
    +

    HTTP request

    +
    +
    +
    DELETE /leagues/1 HTTP/1.1
    +Content-Type: application/json
    +Host: www.api.hufstreaming.site
    +Cookie: HCC_SES=temp-cookie
    +
    +
    +
    +
    +

    Path parameters

    + + ++++ + + + + + + + + + + + + +
    Table 1. /leagues/{leagueId}
    ParameterDescription

    leagueId

    삭제할 리그의 ID

    +
    +
    +

    HTTP response

    +
    +
    +
    HTTP/1.1 200 OK
    +Vary: Origin
    +Vary: Access-Control-Request-Method
    +Vary: Access-Control-Request-Headers
    +
    +
    +
    + +

    리그팀 상세 조회

    diff --git a/src/test/java/com/sports/server/command/league/acceptance/LeagueAcceptanceTest.java b/src/test/java/com/sports/server/command/league/acceptance/LeagueAcceptanceTest.java index 8ad5c953..91ff86ac 100644 --- a/src/test/java/com/sports/server/command/league/acceptance/LeagueAcceptanceTest.java +++ b/src/test/java/com/sports/server/command/league/acceptance/LeagueAcceptanceTest.java @@ -1,41 +1,58 @@ package com.sports.server.command.league.acceptance; -import static org.assertj.core.api.Assertions.*; - -import java.time.LocalDateTime; - -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.context.jdbc.Sql; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import com.sports.server.command.league.dto.LeagueRequestDto; import com.sports.server.support.AcceptanceTest; - import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; +import java.time.LocalDateTime; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.jdbc.Sql; @Sql("/league-fixture.sql") public class LeagueAcceptanceTest extends AcceptanceTest { - @Test - void 대회를_저장한다() throws Exception { - // given - LeagueRequestDto.Register request = new LeagueRequestDto.Register(1L, "우물정 제기차기 대회", "4강", LocalDateTime.now(), - LocalDateTime.now()); - - configureMockJwtForEmail("john.doe@example.com"); - - // when - ExtractableResponse response = RestAssured.given().log().all() - .cookie(COOKIE_NAME, mockToken) - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(request) - .post("/leagues") - .then().log().all() - .extract(); - - // then - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); - } + @Test + void 대회를_저장한다() { + // given + LeagueRequestDto.Register request = new LeagueRequestDto.Register(1L, "우물정 제기차기 대회", "4강", LocalDateTime.now(), + LocalDateTime.now()); + + configureMockJwtForEmail("john.doe@example.com"); + + // when + ExtractableResponse response = RestAssured.given().log().all() + .cookie(COOKIE_NAME, mockToken) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .post("/leagues") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @Test + void 리그를_삭제한다() { + // given + Long leagueId = 1L; + + configureMockJwtForEmail("john.doe@example.com"); + + // when + ExtractableResponse response = RestAssured.given().log().all() + .cookie(COOKIE_NAME, mockToken) + .pathParam("leagueId", leagueId) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .delete("/leagues/{leagueId}") + .then().log().all() + .extract(); + + // then + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } } diff --git a/src/test/java/com/sports/server/command/league/application/LeagueServiceTest.java b/src/test/java/com/sports/server/command/league/application/LeagueServiceTest.java new file mode 100644 index 00000000..25e2e3a0 --- /dev/null +++ b/src/test/java/com/sports/server/command/league/application/LeagueServiceTest.java @@ -0,0 +1,59 @@ +package com.sports.server.command.league.application; + + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.sports.server.auth.exception.AuthorizationErrorMessages; +import com.sports.server.command.league.domain.League; +import com.sports.server.command.member.domain.Member; +import com.sports.server.common.application.EntityUtils; +import com.sports.server.common.exception.NotFoundException; +import com.sports.server.common.exception.UnauthorizedException; +import com.sports.server.support.ServiceTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.jdbc.Sql; + +@Sql("/league-fixture.sql") +public class LeagueServiceTest extends ServiceTest { + @Autowired + private LeagueService leagueService; + + @Autowired + private EntityUtils entityUtils; + + @Nested + @DisplayName("리그를 삭제할 떄") + class LeagueDeleteTest { + @Test + void 삭제한_이후에는_해당_객체를_찾을_수_없다() { + // given + Long leagueId = 1L; + Member manager = entityUtils.getEntity(1L, Member.class); + + // when + leagueService.delete(manager, leagueId); + + // then + assertThatThrownBy( + () -> entityUtils.getEntity(leagueId, League.class)) + .isInstanceOf(NotFoundException.class); + } + + @Test + void 권한이_없는_멤버는_리그를_삭제할_수_없다() { + // given + Long leagueId = 1L; + Member manager = entityUtils.getEntity(3L, Member.class); + + // when & then + assertThatThrownBy( + () -> leagueService.delete(manager, leagueId)) + .isInstanceOf(UnauthorizedException.class) + .hasMessage(AuthorizationErrorMessages.PERMISSION_DENIED); + + } + } +} diff --git a/src/test/java/com/sports/server/command/league/domain/LeagueTest.java b/src/test/java/com/sports/server/command/league/domain/LeagueTest.java index 2482dcd1..20f7dc0e 100644 --- a/src/test/java/com/sports/server/command/league/domain/LeagueTest.java +++ b/src/test/java/com/sports/server/command/league/domain/LeagueTest.java @@ -1,56 +1,69 @@ package com.sports.server.command.league.domain; -import static com.sports.server.support.fixture.FixtureMonkeyUtils.*; -import static org.assertj.core.api.Assertions.*; +import static com.sports.server.support.fixture.FixtureMonkeyUtils.entityBuilder; +import static com.sports.server.support.fixture.FixtureMonkeyUtils.maxRoundArbitrary; +import static com.sports.server.support.fixture.FixtureMonkeyUtils.nameArbitrary; +import static org.assertj.core.api.Assertions.assertThat; +import com.sports.server.command.member.domain.Member; +import com.sports.server.command.organization.domain.Organization; import java.time.LocalDateTime; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import com.sports.server.command.member.domain.Member; -import com.sports.server.command.organization.domain.Organization; - class LeagueTest { - @Nested - @DisplayName("리그 생성 시") - class CreateLeague { - private Member manager; - private Organization organization; - - @BeforeEach - void setUp() { - manager = entityBuilder(Member.class).sample(); - organization = entityBuilder(Organization.class).sample(); - } - - @Test - void isDeleted가_false로_생성된다() throws Exception { - // given - League sut; - - // when - sut = new League(manager, organization, nameArbitrary().sample(), LocalDateTime.now(), - LocalDateTime.now(), - maxRoundArbitrary().sample()); - - // then - assertThat(sut.isDeleted()).isEqualTo(false); - } - - @Test - void 리그의_현재_라운드와_총_라운드는_같다() throws Exception { - // given - League sut; - - // when - sut = new League(manager, organization, nameArbitrary().sample(), LocalDateTime.now(), LocalDateTime.now(), - maxRoundArbitrary().sample()); - - // then - assertThat(sut.getMaxRound()).isEqualTo(sut.getInProgressRound()); - } - } + @Nested + @DisplayName("리그 생성 시") + class CreateLeague { + private Member manager; + private Organization organization; + + @BeforeEach + void setUp() { + manager = entityBuilder(Member.class).sample(); + organization = entityBuilder(Organization.class).sample(); + } + + @Test + void isDeleted가_false로_생성된다() { + // given + League sut; + + // when + sut = new League(manager, organization, nameArbitrary().sample(), LocalDateTime.now(), + LocalDateTime.now(), + maxRoundArbitrary().sample()); + + // then + assertThat(sut.isDeleted()).isEqualTo(false); + } + + @Test + void 리그의_현재_라운드와_총_라운드는_같다() { + // given + League sut; + + // when + sut = new League(manager, organization, nameArbitrary().sample(), LocalDateTime.now(), LocalDateTime.now(), + maxRoundArbitrary().sample()); + + // then + assertThat(sut.getMaxRound()).isEqualTo(sut.getInProgressRound()); + } + } + + @Nested + @DisplayName("리그 삭제 시") + class DeleteLeague { + private Member manager; + private Organization organization; + + @BeforeEach + void setUp() { + manager = entityBuilder(Member.class).sample(); + organization = entityBuilder(Organization.class).sample(); + } + } } diff --git a/src/test/java/com/sports/server/command/league/presentation/LeagueControllerTest.java b/src/test/java/com/sports/server/command/league/presentation/LeagueControllerTest.java index 695636e1..f90ed589 100644 --- a/src/test/java/com/sports/server/command/league/presentation/LeagueControllerTest.java +++ b/src/test/java/com/sports/server/command/league/presentation/LeagueControllerTest.java @@ -1,54 +1,83 @@ package com.sports.server.command.league.presentation; -import static org.mockito.Mockito.*; -import static org.springframework.restdocs.cookies.CookieDocumentation.*; -import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*; -import static org.springframework.restdocs.payload.PayloadDocumentation.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.springframework.restdocs.cookies.CookieDocumentation.cookieWithName; +import static org.springframework.restdocs.cookies.CookieDocumentation.requestCookies; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.post; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.sports.server.command.league.dto.LeagueRequestDto; +import com.sports.server.command.member.domain.Member; +import com.sports.server.support.DocumentationTest; +import jakarta.servlet.http.Cookie; import java.time.LocalDateTime; - import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.JsonFieldType; import org.springframework.test.web.servlet.ResultActions; -import com.sports.server.command.league.dto.LeagueRequestDto; -import com.sports.server.command.member.domain.Member; -import com.sports.server.support.DocumentationTest; +class LeagueControllerTest extends DocumentationTest { -import jakarta.servlet.http.Cookie; + @Test + void 리그를_생성한다() throws Exception { + // given + LeagueRequestDto.Register request = new LeagueRequestDto.Register(1L, "우물정 제기차기 대회", "4강", LocalDateTime.now(), + LocalDateTime.now()); + Cookie cookie = new Cookie(COOKIE_NAME, "temp-cookie"); -class LeagueControllerTest extends DocumentationTest { + doNothing().when(leagueService).register(any(Member.class), any(LeagueRequestDto.Register.class)); + + // when + ResultActions result = mockMvc.perform(post("/leagues", request) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + .cookie(cookie)); + + // then + result.andExpect(status().isOk()) + .andDo(restDocsHandler.document( + requestFields( + fieldWithPath("organizationId").type(JsonFieldType.NUMBER).description("조직 id"), + fieldWithPath("name").type(JsonFieldType.STRING).description("대회 이름"), + fieldWithPath("maxRound").type(JsonFieldType.STRING).description("대회 진행 라운드 수"), + fieldWithPath("startAt").type(JsonFieldType.STRING).description("대회 시작 시간"), + fieldWithPath("endAt").type(JsonFieldType.STRING).description("대회 종료 시간") + ), + requestCookies( + cookieWithName(COOKIE_NAME).description("로그인을 통해 얻은 토큰") + ) + ) + ); + } + + @Test + void 리그를_삭제한다() throws Exception { + // given + Cookie cookie = new Cookie(COOKIE_NAME, "temp-cookie"); + + doNothing().when(leagueService).delete(any(Member.class), any()); + + // when + ResultActions result = mockMvc.perform(delete("/leagues/{leagueId}", 1L) + .contentType(MediaType.APPLICATION_JSON) + .cookie(cookie) + ); - @Test - void 리그를_생성한다() throws Exception { - // given - LeagueRequestDto.Register request = new LeagueRequestDto.Register(1L, "우물정 제기차기 대회", "4강", LocalDateTime.now(), LocalDateTime.now()); - Cookie cookie = new Cookie(COOKIE_NAME, "temp-cookie"); - - doNothing().when(leagueService).register(any(Member.class), any(LeagueRequestDto.Register.class)); - - // when - ResultActions result = mockMvc.perform(post("/leagues", request) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) - .cookie(cookie)); - - // then - result.andExpect(status().isOk()) - .andDo(restDocsHandler.document( - requestFields( - fieldWithPath("organizationId").type(JsonFieldType.NUMBER).description("조직 id"), - fieldWithPath("name").type(JsonFieldType.STRING).description("대회 이름"), - fieldWithPath("maxRound").type(JsonFieldType.STRING).description("대회 진행 라운드 수"), - fieldWithPath("startAt").type(JsonFieldType.STRING).description("대회 시작 시간"), - fieldWithPath("endAt").type(JsonFieldType.STRING).description("대회 종료 시간") - ), - requestCookies( - cookieWithName(COOKIE_NAME).description("로그인을 통해 얻은 토큰") - ) - ) - ); - } + // then + result.andExpect(status().isOk()) + .andDo(restDocsHandler.document( + pathParameters( + parameterWithName("leagueId").description("삭제할 리그의 ID")), + requestCookies( + cookieWithName(COOKIE_NAME).description("로그인을 통해 얻은 토큰") + ) + ) + ); + } } \ No newline at end of file