diff --git a/.github/workflows/assign-reviewer.yml b/.github/workflows/assign-reviewer.yml index d3ea0164e..722ea9786 100644 --- a/.github/workflows/assign-reviewer.yml +++ b/.github/workflows/assign-reviewer.yml @@ -1,8 +1,11 @@ name: Assign Reviewer By Label on: pull_request: - types: [ opened, edited, labeled, unlabeled ] - + types: + - opened + - edited + - labeled + - unlabeled jobs: assign-reviewer: runs-on: ubuntu-latest diff --git a/.github/workflows/ci-back.yml b/.github/workflows/ci-back.yml index cec9580d9..849fea334 100644 --- a/.github/workflows/ci-back.yml +++ b/.github/workflows/ci-back.yml @@ -4,6 +4,7 @@ on: pull_request: branches: - dev + - main paths: 'backend/**' defaults: diff --git a/.github/workflows/ci-user-aos.yml b/.github/workflows/ci-user-aos.yml index d31f3b75a..2ef6e561b 100644 --- a/.github/workflows/ci-user-aos.yml +++ b/.github/workflows/ci-user-aos.yml @@ -4,10 +4,12 @@ on: push: branches: - dev - paths: ['android/festago/**'] + - main + paths: [ 'android/festago/**' ] pull_request: branches: - dev + - main paths: 'android/festago/**' defaults: @@ -20,35 +22,35 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: 17 - distribution: zulu - cache: gradle - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: add google-services.json - run: echo '${{ secrets.ANDROID_USER_GOOGLE_SERVICES_JSON }}' > ./app/google-services.json - - - name: add local.properties - run: | - echo kakao_native_app_key=\"${{ secrets.ANDROID_USER_KAKAO_NATIVE_APP_KEY }}\" >> ./local.properties - echo kakao_redirection_scheme=\"${{ secrets.ANDROID_USER_KAKAO_REDIRECTION_SCHEME }}\" >> ./local.properties - echo base_url=\"$${{ secrets.ANDROID_USER_BASE_URL }}\" >> ./local.properties - - - name: Build with Gradle - run: ./gradlew build - - - name: Run ktlint - run: ./gradlew ktlintCheck - - - name: Run unit tests - run: ./gradlew testDebugUnitTest - - - name: Build assemble release apk - run: ./gradlew assembleRelease + - uses: actions/checkout@v3 + - name: set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: 17 + distribution: zulu + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: add google-services.json + run: echo '${{ secrets.ANDROID_USER_GOOGLE_SERVICES_JSON }}' > ./app/google-services.json + + - name: add local.properties + run: | + echo kakao_native_app_key=\"${{ secrets.ANDROID_USER_KAKAO_NATIVE_APP_KEY }}\" >> ./local.properties + echo kakao_redirection_scheme=\"${{ secrets.ANDROID_USER_KAKAO_REDIRECTION_SCHEME }}\" >> ./local.properties + echo base_url=\"$${{ secrets.ANDROID_USER_BASE_URL }}\" >> ./local.properties + + - name: Build with Gradle + run: ./gradlew build + + - name: Run ktlint + run: ./gradlew ktlintCheck + + - name: Run unit tests + run: ./gradlew testDebugUnitTest + + - name: Build assemble release apk + run: ./gradlew assembleRelease diff --git a/.github/workflows/closed-issue-notification.yml b/.github/workflows/closed-issue-notification.yml index 37da67fbc..6031248af 100644 --- a/.github/workflows/closed-issue-notification.yml +++ b/.github/workflows/closed-issue-notification.yml @@ -1,39 +1,40 @@ name: Closed Issue Notification on: - issues: - types: [closed] - + issues: + types: + - closed + jobs: - create-issue: + create-issue: name: Send closed issue notification to slack - runs-on: ubuntu-latest - steps: - - name: Send Issue - uses: 8398a7/action-slack@v3 - with: - status: custom - custom_payload: | - { - text: "*이슈가 닫혔습니다!*", - attachments: [{ - fallback: 'fallback', - color: '#7539DE', - title: 'Title', - text: '<${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>', - fields: [{ - title: 'Issue number', - value: '#${{ github.event.issue.number }}', - short: true - }, - { - title: 'Author', - value: '${{ github.event.issue.user.login }}', - short: true - }], - actions: [{ + runs-on: ubuntu-latest + steps: + - name: Send Issue + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + text: "*이슈가 닫혔습니다!*", + attachments: [{ + fallback: 'fallback', + color: '#7539DE', + title: 'Title', + text: '<${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>', + fields: [{ + title: 'Issue number', + value: '#${{ github.event.issue.number }}', + short: true + }, + { + title: 'Author', + value: '${{ github.event.issue.user.login }}', + short: true + }], + actions: [{ + }] }] - }] - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_WEBHOOK_URL }} - if: always() + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_WEBHOOK_URL }} + if: always() diff --git a/.github/workflows/closed-pr-notification.yml b/.github/workflows/closed-pr-notification.yml index eec190437..d92c2d37e 100644 --- a/.github/workflows/closed-pr-notification.yml +++ b/.github/workflows/closed-pr-notification.yml @@ -1,8 +1,11 @@ name: Closed PR Notification on: pull_request: - branches: [ dev ] - types: [ closed ] + branches: + - dev + - main + types: + - closed jobs: create-issue: diff --git a/.github/workflows/opend-issue-notification.yml b/.github/workflows/opend-issue-notification.yml index 4bda56c88..2204395bb 100644 --- a/.github/workflows/opend-issue-notification.yml +++ b/.github/workflows/opend-issue-notification.yml @@ -1,39 +1,40 @@ name: Opend Issue Notification on: - issues: - types: [opened] - + issues: + types: + - opened + jobs: - create-issue: + create-issue: name: Send opend issue notification to slack - runs-on: ubuntu-latest - steps: - - name: Send Issue - uses: 8398a7/action-slack@v3 - with: - status: custom - custom_payload: | - { - text: "*새로운 이슈가 생성되었습니다!*", - attachments: [{ - fallback: 'fallback', - color: '#1F7629', - title: 'Title', - text: '<${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>', - fields: [{ - title: 'Issue number', - value: '#${{ github.event.issue.number }}', - short: true - }, - { - title: 'Author', - value: '${{ github.event.issue.user.login }}', - short: true - }], - actions: [{ + runs-on: ubuntu-latest + steps: + - name: Send Issue + uses: 8398a7/action-slack@v3 + with: + status: custom + custom_payload: | + { + text: "*새로운 이슈가 생성되었습니다!*", + attachments: [{ + fallback: 'fallback', + color: '#1F7629', + title: 'Title', + text: '<${{ github.event.issue.html_url }}|${{ github.event.issue.title }}>', + fields: [{ + title: 'Issue number', + value: '#${{ github.event.issue.number }}', + short: true + }, + { + title: 'Author', + value: '${{ github.event.issue.user.login }}', + short: true + }], + actions: [{ + }] }] - }] - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_WEBHOOK_URL }} - if: always() + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_ISSUE_WEBHOOK_URL }} + if: always() diff --git a/.github/workflows/opened-pr-notification.yml b/.github/workflows/opened-pr-notification.yml index 00ff0314e..1ff6bba58 100644 --- a/.github/workflows/opened-pr-notification.yml +++ b/.github/workflows/opened-pr-notification.yml @@ -1,8 +1,11 @@ name: Opened PR Notification on: pull_request: - branches: [ dev ] - types: [ opened ] + branches: + - dev + - main + types: + - opened jobs: create-issue: diff --git a/backend/src/main/java/com/festago/application/EntryService.java b/backend/src/main/java/com/festago/application/EntryService.java index 3a5522821..5cb5e470a 100644 --- a/backend/src/main/java/com/festago/application/EntryService.java +++ b/backend/src/main/java/com/festago/application/EntryService.java @@ -13,6 +13,7 @@ import com.festago.exception.BadRequestException; import com.festago.exception.ErrorCode; import com.festago.exception.NotFoundException; +import java.time.Clock; import java.time.LocalDateTime; import java.util.Date; import org.springframework.stereotype.Service; @@ -25,12 +26,14 @@ public class EntryService { private final EntryCodeProvider entryCodeProvider; private final EntryCodeExtractor entryCodeExtractor; private final MemberTicketRepository memberTicketRepository; + private final Clock clock; public EntryService(EntryCodeProvider entryCodeProvider, EntryCodeExtractor entryCodeExtractor, - MemberTicketRepository memberTicketRepository) { + MemberTicketRepository memberTicketRepository, Clock clock) { this.entryCodeProvider = entryCodeProvider; this.entryCodeExtractor = entryCodeExtractor; this.memberTicketRepository = memberTicketRepository; + this.clock = clock; } public EntryCodeResponse createEntryCode(Long memberId, Long memberTicketId) { @@ -38,7 +41,7 @@ public EntryCodeResponse createEntryCode(Long memberId, Long memberTicketId) { if (!memberTicket.isOwner(memberId)) { throw new BadRequestException(ErrorCode.NOT_MEMBER_TICKET_OWNER); } - if (!memberTicket.canEntry(LocalDateTime.now())) { + if (!memberTicket.canEntry(LocalDateTime.now(clock))) { throw new BadRequestException(ErrorCode.NOT_ENTRY_TIME); } EntryCodeTime entryCodeTime = EntryCodeTime.create(); diff --git a/backend/src/main/java/com/festago/application/MemberTicketService.java b/backend/src/main/java/com/festago/application/MemberTicketService.java index 6768311c0..050d05f46 100644 --- a/backend/src/main/java/com/festago/application/MemberTicketService.java +++ b/backend/src/main/java/com/festago/application/MemberTicketService.java @@ -12,6 +12,7 @@ import com.festago.exception.BadRequestException; import com.festago.exception.ErrorCode; import com.festago.exception.NotFoundException; +import java.time.Clock; import java.time.Duration; import java.time.LocalDateTime; import java.util.List; @@ -25,11 +26,13 @@ public class MemberTicketService { private final MemberTicketRepository memberTicketRepository; private final MemberRepository memberRepository; + private final Clock clock; public MemberTicketService(MemberTicketRepository memberTicketRepository, - MemberRepository memberRepository) { + MemberRepository memberRepository, Clock clock) { this.memberTicketRepository = memberTicketRepository; this.memberRepository = memberRepository; + this.clock = clock; } @Transactional(readOnly = true) @@ -59,7 +62,7 @@ public MemberTicketsResponse findCurrent(Long memberId, Pageable pageable) { } private List filterCurrentMemberTickets(List memberTickets) { - LocalDateTime currentTime = LocalDateTime.now(); + LocalDateTime currentTime = LocalDateTime.now(clock); return memberTickets.stream() .filter(memberTicket -> memberTicket.isBeforeEntry(currentTime) || memberTicket.canEntry(currentTime)) .sorted(comparing((MemberTicket memberTicket) -> memberTicket.isBeforeEntry(currentTime)) diff --git a/backend/src/main/java/com/festago/auth/config/LoginConfig.java b/backend/src/main/java/com/festago/auth/config/LoginConfig.java index 68a6eff6d..72439a2eb 100644 --- a/backend/src/main/java/com/festago/auth/config/LoginConfig.java +++ b/backend/src/main/java/com/festago/auth/config/LoginConfig.java @@ -38,7 +38,8 @@ public void addInterceptors(InterceptorRegistry registry) { .addPathPatterns("/admin/**", "/js/admin/**") .excludePathPatterns("/admin/login", "/admin/initialize"); registry.addInterceptor(memberAuthInterceptor()) - .addPathPatterns("/member-tickets/**", "/members/**"); + .addPathPatterns("/member-tickets/**", "/members/**", "/auth/**") + .excludePathPatterns("/auth/oauth2"); } @Bean diff --git a/backend/src/main/java/com/festago/auth/domain/AuthPayload.java b/backend/src/main/java/com/festago/auth/domain/AuthPayload.java index 253b559e8..0c8f91b12 100644 --- a/backend/src/main/java/com/festago/auth/domain/AuthPayload.java +++ b/backend/src/main/java/com/festago/auth/domain/AuthPayload.java @@ -9,13 +9,13 @@ public class AuthPayload { private final Role role; public AuthPayload(Long memberId, Role role) { - validate(memberId, role); + validate(role); this.memberId = memberId; this.role = role; } - private void validate(Long memberId, Role role) { - if (memberId == null || role == null) { + private void validate(Role role) { + if (role == null) { throw new InternalServerException(ErrorCode.INVALID_AUTH_TOKEN_PAYLOAD); } } diff --git a/backend/src/test/java/com/festago/application/EntryServiceTest.java b/backend/src/test/java/com/festago/application/EntryServiceTest.java index 87bc9716b..47c55e311 100644 --- a/backend/src/test/java/com/festago/application/EntryServiceTest.java +++ b/backend/src/test/java/com/festago/application/EntryServiceTest.java @@ -25,7 +25,12 @@ import com.festago.support.MemberFixture; import com.festago.support.MemberTicketFixture; import com.festago.support.StageFixture; +import com.festago.support.TimeInstantProvider; +import java.time.Clock; +import java.time.Instant; import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.Optional; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; @@ -34,6 +39,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -50,6 +56,9 @@ class EntryServiceTest { @Mock MemberTicketRepository memberTicketRepository; + @Spy + Clock clock = Clock.systemDefaultZone(); + @InjectMocks EntryService entryService; @@ -59,9 +68,15 @@ class 티켓의_QR_생성_요청 { @Test void 입장_시간_전_요청하면_예외() { // given - LocalDateTime entryTime = LocalDateTime.now().plusMinutes(30); + LocalDateTime entryTime = LocalDateTime.parse("2023-07-30T16:00:00"); + Festival festival = FestivalFixture.festival() + .startDate(entryTime.toLocalDate()) + .endDate(entryTime.toLocalDate()) + .build(); Stage stage = StageFixture.stage() - .startTime(LocalDateTime.now().plusHours(1)) + .festival(festival) + .startTime(entryTime.plusHours(2)) + .ticketOpenTime(entryTime.minusHours(1)) .build(); MemberTicket memberTicket = MemberTicketFixture.memberTicket() .id(1L) @@ -70,9 +85,10 @@ class 티켓의_QR_생성_요청 { .build(); Long memberId = memberTicket.getOwner().getId(); Long memberTicketId = memberTicket.getId(); - given(memberTicketRepository.findById(anyLong())) .willReturn(Optional.of(memberTicket)); + given(clock.instant()) + .willReturn(TimeInstantProvider.from(entryTime.minusHours(1))); // when & then assertThatThrownBy(() -> entryService.createEntryCode(memberId, memberTicketId)) @@ -83,15 +99,15 @@ class 티켓의_QR_생성_요청 { @Test void 입장_시간이_24시간이_넘은_경우_예외() { // given - LocalDateTime stageStartTime = LocalDateTime.now().minusHours(24); - LocalDateTime entryTime = stageStartTime.minusSeconds(10); + LocalDateTime entryTime = LocalDateTime.parse("2023-07-30T16:00:00"); Festival festival = FestivalFixture.festival() - .startDate(stageStartTime.toLocalDate()) - .endDate(stageStartTime.toLocalDate()) + .startDate(entryTime.toLocalDate()) + .endDate(entryTime.toLocalDate()) .build(); Stage stage = StageFixture.stage() .festival(festival) - .startTime(stageStartTime) + .startTime(entryTime.plusHours(2)) + .ticketOpenTime(entryTime.minusHours(1)) .build(); MemberTicket memberTicket = MemberTicketFixture.memberTicket() .id(1L) @@ -100,9 +116,10 @@ class 티켓의_QR_생성_요청 { .build(); Long memberId = memberTicket.getOwner().getId(); Long memberTicketId = memberTicket.getId(); - given(memberTicketRepository.findById(anyLong())) .willReturn(Optional.of(memberTicket)); + given(clock.instant()) + .willReturn(TimeInstantProvider.from((entryTime.plusHours(24)))); // when & then assertThatThrownBy(() -> entryService.createEntryCode(memberId, memberTicketId)) @@ -113,14 +130,12 @@ class 티켓의_QR_생성_요청 { @Test void 자신의_티켓이_아니면_예외() { // given - LocalDateTime entryTime = LocalDateTime.now().minusMinutes(30); Long memberId = 1L; Member other = MemberFixture.member() .id(2L) .build(); MemberTicket otherTicket = MemberTicketFixture.memberTicket() .id(1L) - .entryTime(entryTime) .owner(other) .build(); Long memberTicketId = otherTicket.getId(); @@ -150,8 +165,21 @@ class 티켓의_QR_생성_요청 { @Test void 성공() { // given + LocalDateTime entryTime = LocalDateTime.parse("2023-07-30T16:00:00"); + Instant now = Instant.from(ZonedDateTime.of(entryTime, ZoneId.systemDefault())); + Festival festival = FestivalFixture.festival() + .startDate(entryTime.toLocalDate()) + .endDate(entryTime.toLocalDate()) + .build(); + Stage stage = StageFixture.stage() + .festival(festival) + .startTime(entryTime.plusHours(2)) + .ticketOpenTime(entryTime.minusHours(1)) + .build(); MemberTicket memberTicket = MemberTicketFixture.memberTicket() .id(1L) + .stage(stage) + .entryTime(entryTime) .build(); String code = "3112321312123"; Long memberId = memberTicket.getOwner().getId(); @@ -161,6 +189,8 @@ class 티켓의_QR_생성_요청 { .willReturn(Optional.of(memberTicket)); given(entryCodeProvider.provide(any(), any())) .willReturn(code); + given(clock.instant()) + .willReturn(now); // when EntryCodeResponse entryCode = entryService.createEntryCode(memberId, memberTicketId); diff --git a/backend/src/test/java/com/festago/application/MemberTicketServiceTest.java b/backend/src/test/java/com/festago/application/MemberTicketServiceTest.java index fb111b411..591a820c1 100644 --- a/backend/src/test/java/com/festago/application/MemberTicketServiceTest.java +++ b/backend/src/test/java/com/festago/application/MemberTicketServiceTest.java @@ -19,6 +19,7 @@ import com.festago.support.MemberFixture; import com.festago.support.MemberTicketFixture; import com.festago.support.StageFixture; +import java.time.Clock; import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -29,6 +30,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -44,6 +46,9 @@ class MemberTicketServiceTest { @Mock MemberRepository memberRepository; + @Spy + Clock clock = Clock.systemDefaultZone(); + @InjectMocks MemberTicketService memberTicketService; diff --git a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthExtractorTest.java b/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthExtractorTest.java index d23eeac46..209ce2558 100644 --- a/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthExtractorTest.java +++ b/backend/src/test/java/com/festago/auth/infrastructure/JwtAuthExtractorTest.java @@ -71,21 +71,6 @@ class JwtAuthExtractorTest { .hasMessage("올바르지 않은 로그인 토큰입니다."); } - @Test - void memberId_필드가_없으면_예외() { - // given - String token = Jwts.builder() - .claim(ROLE_ID_KEY, Role.MEMBER) - .setExpiration(new Date(new Date().getTime() + 10000)) - .signWith(KEY, SignatureAlgorithm.HS256) - .compact(); - - // when & then - assertThatThrownBy(() -> jwtAuthExtractor.extract(token)) - .isInstanceOf(InternalServerException.class) - .hasMessage("유효하지 않은 로그인 토큰 payload 입니다."); - } - @Test void role_필드가_없으면_예외() { // given diff --git a/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java b/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java index c7edf6904..738b648fb 100644 --- a/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java +++ b/backend/src/test/java/com/festago/auth/presentation/AuthControllerTest.java @@ -3,16 +3,19 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; import com.festago.auth.application.AuthService; +import com.festago.auth.domain.Role; import com.festago.auth.domain.SocialType; import com.festago.auth.dto.LoginRequest; import com.festago.auth.dto.LoginResponse; import com.festago.support.CustomWebMvcTest; +import com.festago.support.WithMockAuth; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores; import org.junit.jupiter.api.Test; @@ -56,4 +59,29 @@ class AuthControllerTest { assertThat(actual).isEqualTo(expected); } + + @Test + void 로그인을_하지_않고_탈퇴를_하면_예외() throws Exception { + mockMvc.perform(delete("/auth") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + @WithMockAuth(role = Role.ADMIN) + void 멤버_권한이_아닌데_탈퇴하면_예외() throws Exception { + mockMvc.perform(delete("/auth") + .header("Authorization", "Bearer token") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockAuth + void 회원_탈퇴를_한다() throws Exception { + mockMvc.perform(delete("/auth") + .header("Authorization", "Bearer token") + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + } } diff --git a/backend/src/test/java/com/festago/support/TimeInstantProvider.java b/backend/src/test/java/com/festago/support/TimeInstantProvider.java new file mode 100644 index 000000000..3de862d1e --- /dev/null +++ b/backend/src/test/java/com/festago/support/TimeInstantProvider.java @@ -0,0 +1,17 @@ +package com.festago.support; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class TimeInstantProvider { + + public static Instant from(String localDateTime) { + return from(LocalDateTime.parse(localDateTime)); + } + + public static Instant from(LocalDateTime localDateTime) { + return Instant.from(ZonedDateTime.of(localDateTime, ZoneId.systemDefault())); + } +}