diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml index cbea539a..73a91626 100644 --- a/.github/workflows/backend.yml +++ b/.github/workflows/backend.yml @@ -1,7 +1,7 @@ name: sonar backend on: pull_request: - branches: [main] + branches: [dev] paths: ["backend/**"] types: [opened, synchronize, reopened] defaults: @@ -35,4 +35,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build sonarqube --info + run: ./gradlew build diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index 9677c93d..69f106ea 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -1,7 +1,7 @@ name: sonar frontend on: pull_request: - branches: [main] + branches: [dev] paths: ["frontend/**"] types: [opened, synchronize, reopened] defaults: @@ -13,10 +13,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: SonarCloud Scan - uses: SonarSource/sonarcloud-github-action@master - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + - run: npm ci + - run: npm run build diff --git a/.github/workflows/imagestorage.yml b/.github/workflows/imagestorage.yml new file mode 100644 index 00000000..8944a0f0 --- /dev/null +++ b/.github/workflows/imagestorage.yml @@ -0,0 +1,38 @@ +name: sonar imagestorage +on: + pull_request: + branches: [dev] + paths: ["imagestorage/**"] + types: [opened, synchronize, reopened] +defaults: + run: + working-directory: imagestorage +jobs: + build: + name: sonar imagestorage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages + uses: actions/cache@v1 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: ./gradlew build diff --git a/.gitmodules b/.gitmodules index 2b43688f..d7f23bb2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,8 @@ path = backend/submodule url = https://github.com/gong-check/submodule.git branch = main + +[submodule "frontend/frontend-security"] + path = frontend/frontend-security + url = https://github.com/gong-check/frontend-security.git + branch = main diff --git a/backend/.gitignore b/backend/.gitignore index ad1de709..69c649fd 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -25,7 +25,9 @@ bin/ out/ !**/src/main/**/out/ !**/src/test/**/out/ -application-oauth.yml +application-dev.yml +application-local.yml +application-prod.yml ### NetBeans ### /nbproject/private/ @@ -39,3 +41,9 @@ application-oauth.yml ### Rest docs ### /src/main/resources/static/index.html + +### logging ### +/logs/** + +### upload image ### +/src/test/resources/static/images/** diff --git a/backend/build.gradle b/backend/build.gradle index 36d34021..09b4bb5b 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -30,13 +30,20 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' + // webflux + implementation 'org.springframework.boot:spring-boot-starter-webflux' + // database-driver + runtimeOnly 'mysql:mysql-connector-java' runtimeOnly 'com.h2database:h2' // lombok compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // log + implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:3.3.2' + // jwt implementation 'io.jsonwebtoken:jjwt-api:0.11.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' @@ -55,12 +62,13 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.slack.api:slack-app-backend:1.22.2' implementation 'com.slack.api:slack-api-model:1.22.2' + implementation "com.github.maricn:logback-slack-appender:1.6.1" } processResources.dependsOn('copySecret') task copySecret(type: Copy) { - from '../backend/submodule/application-oauth.yml' + from '../backend/submodule' into 'src/main/resources' } @@ -92,8 +100,16 @@ task createDocument(type: Copy) { into file("src/main/resources/static") } +task displaceDocument(type: Copy) { + dependsOn asciidoctor + + from ("${asciidoctor.outputDir}") + into ("build/resources/main/static") +} + bootJar { dependsOn createDocument + dependsOn displaceDocument } jacocoTestReport { diff --git a/backend/src/docs/asciidoc/guest.adoc b/backend/src/docs/asciidoc/guest.adoc index 7b44a419..6d0c94d6 100644 --- a/backend/src/docs/asciidoc/guest.adoc +++ b/backend/src/docs/asciidoc/guest.adoc @@ -2,4 +2,5 @@ == 게스트 === 공간 입장 -operation::guests/auth/success[snippets='http-request,http-response'] + +operation::guests/auth/success[snippets='http-request,http-response,path-parameters,request-fields,response-fields'] diff --git a/backend/src/docs/asciidoc/hosts.adoc b/backend/src/docs/asciidoc/hosts.adoc new file mode 100644 index 00000000..cd9dd300 --- /dev/null +++ b/backend/src/docs/asciidoc/hosts.adoc @@ -0,0 +1,8 @@ +[[Hosts]] +== 호스트 + +=== 호스트 비밀번호 변경 +operation::hosts/spacePassword_update/success[snippets='http-request,http-response'] + +=== 입장코드 조회 +operation::hosts/entranceCode[snippets='http-request,http-response,response-fields'] diff --git a/backend/src/docs/asciidoc/image-upload.adoc b/backend/src/docs/asciidoc/image-upload.adoc new file mode 100644 index 00000000..e897f3ed --- /dev/null +++ b/backend/src/docs/asciidoc/image-upload.adoc @@ -0,0 +1,3 @@ +[[Image-upload]] +== 이미지 업로드 +operation::image-upload[snippets='http-request,http-response,request-parts,response-fields'] diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index 09eb167f..eee5f468 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -7,7 +7,10 @@ == Gong-Check include::guest.adoc[] +include::hosts.adoc[] include::space.adoc[] include::job.adoc[] include::task.adoc[] +include::runningTask.adoc[] include::submission.adoc[] +include::image-upload.adoc[] diff --git a/backend/src/docs/asciidoc/job.adoc b/backend/src/docs/asciidoc/job.adoc index 7c5a98ba..ffa8ce47 100644 --- a/backend/src/docs/asciidoc/job.adoc +++ b/backend/src/docs/asciidoc/job.adoc @@ -2,4 +2,25 @@ == job === job 목록 조회 -operation::jobs/list[snippets='http-request,http-response'] + +operation::jobs/list[snippets='http-request,http-response,path-parameters,response-fields'] + +=== job의 slack url 조회 + +operation::jobs/slack_url[snippets='http-request,http-response,path-parameters,response-fields'] + +=== job의 slack url 수정 + +operation::jobs/change_slack_url/success[snippets='http-request,http-response,path-parameters,request-fields'] + +=== 새로운 job 생성 + +operation::jobs/create/success[snippets='http-request,http-response,path-parameters,request-fields'] + +=== job 수정 + +operation::jobs/change/success[snippets='http-request,http-response,path-parameters,request-fields'] + +=== job 삭제 + +operation::jobs/delete[snippets='http-request,http-response,path-parameters'] diff --git a/backend/src/docs/asciidoc/runningTask.adoc b/backend/src/docs/asciidoc/runningTask.adoc new file mode 100644 index 00000000..f1b81954 --- /dev/null +++ b/backend/src/docs/asciidoc/runningTask.adoc @@ -0,0 +1,18 @@ +[[RunningTask]] +== RunningTask + +=== RunningTask 정보 조회 + +operation::runningTasks/find/success[snippets='http-request,http-response,path-parameters,response-fields'] + +=== RunningTask 완료 여부 확인 + +operation::runningTasks/active/success[snippets='http-request,http-response,path-parameters,response-fields'] + +=== RunningTask check 상태 변환 + +operation::runningTasks/check/success[snippets='http-request,http-response,path-parameters'] + +=== RunningTask 생성 + +operation::runningTasks/create/success[snippets='http-request,http-response,path-parameters'] diff --git a/backend/src/docs/asciidoc/space.adoc b/backend/src/docs/asciidoc/space.adoc index 473caed9..44b907c4 100644 --- a/backend/src/docs/asciidoc/space.adoc +++ b/backend/src/docs/asciidoc/space.adoc @@ -2,4 +2,21 @@ == space === space 목록 조회 -operation::spaces/list[snippets='http-request,http-response'] + +operation::spaces/list[snippets='http-request,http-response,response-fields'] + +=== 단일 space 조회 + +operation::spaces/find[snippets='http-request,path-parameters,http-response,response-fields'] + +=== 새로운 space 생성 + +operation::spaces/create/success[snippets='http-request,http-response,request-fields'] + +=== space 수정 + +operation::spaces/change/success[snippets='http-request,http-response,path-parameters,request-fields'] + +=== space 삭제 + +operation::spaces/delete[snippets='http-request,http-response,path-parameters'] diff --git a/backend/src/docs/asciidoc/submission.adoc b/backend/src/docs/asciidoc/submission.adoc index d167b4fb..8180807a 100644 --- a/backend/src/docs/asciidoc/submission.adoc +++ b/backend/src/docs/asciidoc/submission.adoc @@ -1,5 +1,10 @@ [[Submission]] == submission -=== 제출 -operation::submissions/submit/success[snippets='http-request,http-response'] +=== submission 목록 조회 + +operation::submissions/list[snippets='http-request,http-response,path-parameters,response-fields'] + +=== 완료된 job tasks의 submission 제출 + +operation::submissions/submit/success[snippets='http-request,http-response,path-parameters,request-fields'] diff --git a/backend/src/docs/asciidoc/task.adoc b/backend/src/docs/asciidoc/task.adoc index b92e6b95..d166c9d0 100644 --- a/backend/src/docs/asciidoc/task.adoc +++ b/backend/src/docs/asciidoc/task.adoc @@ -1,11 +1,6 @@ [[Task]] == task -=== tasks 정보 조회 -operation::tasks/find/success[snippets='http-request,http-response'] -=== task 완료 여부 확인 -operation::tasks/active/success[snippets='http-request,http-response'] -=== task check 상태 변환 -operation::tasks/check/success[snippets='http-request,http-response'] -=== runningtask 생성 -operation::tasks/create/success[snippets='http-request,http-response'] +=== Task 정보 조회 + +operation::tasks/find/success[snippets='http-request,http-response,path-parameters,response-fields'] diff --git a/backend/src/main/java/com/woowacourse/gongcheck/SimpleImageUploader.java b/backend/src/main/java/com/woowacourse/gongcheck/SimpleImageUploader.java deleted file mode 100644 index b5f9c865..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/SimpleImageUploader.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.woowacourse.gongcheck; - -import com.woowacourse.gongcheck.application.ImageUploader; -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; - -@Component -public class SimpleImageUploader implements ImageUploader { - @Override - public String upload(MultipartFile file, String directoryName) { - return "https://user-images.githubusercontent.com/48307960/178979416-449c8a6e-5c8b-4d14-91e6-c19718024206.png"; - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/AlertService.java b/backend/src/main/java/com/woowacourse/gongcheck/application/AlertService.java deleted file mode 100644 index a4c1627a..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/AlertService.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import com.woowacourse.gongcheck.application.response.SubmissionResponse; - -public interface AlertService { - - void sendMessage(final SubmissionResponse submissionResponse); -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/HostService.java b/backend/src/main/java/com/woowacourse/gongcheck/application/HostService.java deleted file mode 100644 index 724090c7..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/HostService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.host.SpacePassword; -import com.woowacourse.gongcheck.presentation.request.SpacePasswordChangeRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional(readOnly = true) -public class HostService { - - private final HostRepository hostRepository; - - public HostService(final HostRepository hostRepository) { - this.hostRepository = hostRepository; - } - - @Transactional - public void changeSpacePassword(final Long hostId, final SpacePasswordChangeRequest request) { - Host host = hostRepository.getById(hostId); - host.changeSpacePassword(new SpacePassword(request.getPassword())); - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/ImageUploader.java b/backend/src/main/java/com/woowacourse/gongcheck/application/ImageUploader.java deleted file mode 100644 index ec5ce21a..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/ImageUploader.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.woowacourse.gongcheck.application; - - -import org.springframework.web.multipart.MultipartFile; - -public interface ImageUploader { - - String upload(MultipartFile file, String directoryName); -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/JobService.java b/backend/src/main/java/com/woowacourse/gongcheck/application/JobService.java deleted file mode 100644 index de6762a7..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/JobService.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import com.woowacourse.gongcheck.application.response.JobsResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.section.Section; -import com.woowacourse.gongcheck.domain.section.SectionRepository; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.space.SpaceRepository; -import com.woowacourse.gongcheck.domain.task.Task; -import com.woowacourse.gongcheck.domain.task.TaskRepository; -import com.woowacourse.gongcheck.presentation.request.JobCreateRequest; -import com.woowacourse.gongcheck.presentation.request.SectionCreateRequest; -import com.woowacourse.gongcheck.presentation.request.TaskCreateRequest; -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional(readOnly = true) -public class JobService { - - private final HostRepository hostRepository; - private final SpaceRepository spaceRepository; - private final JobRepository jobRepository; - private final SectionRepository sectionRepository; - private final TaskRepository taskRepository; - - public JobService(final HostRepository hostRepository, final SpaceRepository spaceRepository, - final JobRepository jobRepository, final SectionRepository sectionRepository, - final TaskRepository taskRepository) { - this.hostRepository = hostRepository; - this.spaceRepository = spaceRepository; - this.jobRepository = jobRepository; - this.sectionRepository = sectionRepository; - this.taskRepository = taskRepository; - } - - public JobsResponse findPage(final Long hostId, final Long spaceId, final Pageable pageable) { - Host host = hostRepository.getById(hostId); - Space space = spaceRepository.getByHostAndId(host, spaceId); - Slice jobs = jobRepository.findBySpaceHostAndSpace(host, space, pageable); - return JobsResponse.from(jobs); - } - - @Transactional - public Long createJob(final Long hostId, final Long spaceId, final JobCreateRequest request) { - Host host = hostRepository.getById(hostId); - Space space = spaceRepository.getByHostAndId(host, spaceId); - - Job job = Job.builder() - .space(space) - .name(request.getName()) - .createdAt(LocalDateTime.now()) - .build(); - jobRepository.save(job); - createSectionsAndTasks(request.getSections(), job); - return job.getId(); - } - - private void createSectionsAndTasks(final List sectionCreateRequests, final Job job) { - sectionCreateRequests.forEach(sectionRequest -> createSectionAndTasks(sectionRequest, job)); - } - - private Section createSectionAndTasks(final SectionCreateRequest sectionCreateRequest, final Job job) { - Section section = Section.builder() - .job(job) - .name(sectionCreateRequest.getName()) - .createdAt(LocalDateTime.now()) - .build(); - sectionRepository.save(section); - createTasks(sectionCreateRequest.getTasks(), section); - return section; - } - - private void createTasks(final List taskCreateRequests, final Section section) { - List tasks = taskCreateRequests - .stream() - .map(taskRequest -> createTask(taskRequest, section)) - .collect(Collectors.toList()); - taskRepository.saveAll(tasks); - } - - private Task createTask(final TaskCreateRequest taskCreateRequest, final Section section) { - return Task.builder() - .section(section) - .name(taskCreateRequest.getName()) - .createdAt(LocalDateTime.now()) - .build(); - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/SlackService.java b/backend/src/main/java/com/woowacourse/gongcheck/application/SlackService.java deleted file mode 100644 index e494f44b..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/SlackService.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import com.slack.api.Slack; -import com.slack.api.webhook.Payload; -import com.woowacourse.gongcheck.application.response.Attachments; -import com.woowacourse.gongcheck.application.response.SubmissionResponse; -import org.springframework.scheduling.annotation.Async; -import org.springframework.stereotype.Service; - -@Service -public class SlackService implements AlertService { - - @Async - @Override - public void sendMessage(final SubmissionResponse submissionResponse) { - try (Slack slack = Slack.getInstance()) { - Payload payload = Payload.builder() - .attachments(Attachments.of(submissionResponse).getAttachments()) - .build(); - slack.send(submissionResponse.getSlackUrl(), payload); - } catch (Exception e) { - throw new RuntimeException(e); - } - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/SpaceService.java b/backend/src/main/java/com/woowacourse/gongcheck/application/SpaceService.java deleted file mode 100644 index 005bea04..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/SpaceService.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import static java.util.stream.Collectors.toList; - -import com.woowacourse.gongcheck.application.response.SpacesResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.section.Section; -import com.woowacourse.gongcheck.domain.section.SectionRepository; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.space.SpaceRepository; -import com.woowacourse.gongcheck.domain.task.RunningTaskRepository; -import com.woowacourse.gongcheck.domain.task.Task; -import com.woowacourse.gongcheck.domain.task.TaskRepository; -import com.woowacourse.gongcheck.exception.BusinessException; -import com.woowacourse.gongcheck.presentation.request.SpaceCreateRequest; -import java.time.LocalDateTime; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -@Service -@Transactional(readOnly = true) -public class SpaceService { - - private final HostRepository hostRepository; - private final SpaceRepository spaceRepository; - private final JobRepository jobRepository; - private final SectionRepository sectionRepository; - private final TaskRepository taskRepository; - private final RunningTaskRepository runningTaskRepository; - private final ImageUploader imageUploader; - - public SpaceService(final HostRepository hostRepository, final SpaceRepository spaceRepository, - final JobRepository jobRepository, final SectionRepository sectionRepository, - final TaskRepository taskRepository, final RunningTaskRepository runningTaskRepository, - final ImageUploader imageUploader) { - this.hostRepository = hostRepository; - this.spaceRepository = spaceRepository; - this.jobRepository = jobRepository; - this.sectionRepository = sectionRepository; - this.taskRepository = taskRepository; - this.runningTaskRepository = runningTaskRepository; - this.imageUploader = imageUploader; - } - - public SpacesResponse findPage(final Long hostId, final Pageable pageable) { - Host host = hostRepository.getById(hostId); - Slice spaces = spaceRepository.findByHost(host, pageable); - return SpacesResponse.from(spaces); - } - - public Long createSpace(final Long hostId, final SpaceCreateRequest request) { - Host host = hostRepository.getById(hostId); - checkDuplicateName(request, host); - - String imageUrl = uploadImageAndGetUrlOrNull(request.getImage()); - - Space space = Space.builder() - .host(host) - .name(request.getName()) - .imageUrl(imageUrl) - .createdAt(LocalDateTime.now()) - .build(); - return spaceRepository.save(space) - .getId(); - } - - public void removeSpace(final Long hostId, final Long spaceId) { - Host host = hostRepository.getById(hostId); - Space space = spaceRepository.getByHostAndId(host, spaceId); - List jobs = jobRepository.findAllBySpace(space); - List
sections = sectionRepository.findAllByJobIn(jobs); - List tasks = taskRepository.findAllBySectionIn(sections); - - runningTaskRepository.deleteAllByIdInBatch(tasks.stream() - .map(Task::getId) - .collect(toList())); - taskRepository.deleteAllInBatch(tasks); - sectionRepository.deleteAllInBatch(sections); - jobRepository.deleteAllInBatch(jobs); - spaceRepository.deleteById(spaceId); - } - - private void checkDuplicateName(final SpaceCreateRequest request, final Host host) { - if (spaceRepository.existsByHostAndName(host, request.getName())) { - throw new BusinessException("이미 존재하는 이름입니다."); - } - } - - private String uploadImageAndGetUrlOrNull(final MultipartFile image) { - if (!Objects.isNull(image)) { - return imageUploader.upload(image, "spaces"); - } - return null; - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/SubmissionService.java b/backend/src/main/java/com/woowacourse/gongcheck/application/SubmissionService.java deleted file mode 100644 index d6240f57..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/SubmissionService.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import com.woowacourse.gongcheck.application.response.SubmissionResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.submission.SubmissionRepository; -import com.woowacourse.gongcheck.domain.task.RunningTaskRepository; -import com.woowacourse.gongcheck.domain.task.RunningTasks; -import com.woowacourse.gongcheck.domain.task.TaskRepository; -import com.woowacourse.gongcheck.domain.task.Tasks; -import com.woowacourse.gongcheck.exception.BusinessException; -import com.woowacourse.gongcheck.presentation.request.SubmissionRequest; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@Transactional(readOnly = true) -public class SubmissionService { - - private final HostRepository hostRepository; - private final JobRepository jobRepository; - private final TaskRepository taskRepository; - private final RunningTaskRepository runningTaskRepository; - private final SubmissionRepository submissionRepository; - - public SubmissionService(final HostRepository hostRepository, final JobRepository jobRepository, - final TaskRepository taskRepository, - final RunningTaskRepository runningTaskRepository, - final SubmissionRepository submissionRepository) { - this.hostRepository = hostRepository; - this.jobRepository = jobRepository; - this.taskRepository = taskRepository; - this.runningTaskRepository = runningTaskRepository; - this.submissionRepository = submissionRepository; - } - - @Transactional - public SubmissionResponse submitJobCompletion(final Long hostId, final Long jobId, final SubmissionRequest request) { - Host host = hostRepository.getById(hostId); - Job job = jobRepository.getBySpaceHostAndId(host, jobId); - saveSubmissionAndClearRunningTasks(request, job); - return SubmissionResponse.of(request.getAuthor(), job); - } - - private void saveSubmissionAndClearRunningTasks(final SubmissionRequest request, final Job job) { - Tasks tasks = new Tasks(taskRepository.findAllBySectionJob(job)); - validateRunning(tasks); - RunningTasks runningTasks = new RunningTasks(runningTaskRepository.findAllById(tasks.getTaskIds())); - runningTasks.validateCompletion(); - submissionRepository.save(job.createSubmission(request.getAuthor())); - runningTaskRepository.deleteAllByIdInBatch(tasks.getTaskIds()); - } - - private void validateRunning(final Tasks tasks) { - if (!runningTaskRepository.existsByTaskIdIn(tasks.getTaskIds())) { - throw new BusinessException("현재 제출할 수 있는 진행중인 작업이 존재하지 않습니다."); - } - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/JobsResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/application/response/JobsResponse.java deleted file mode 100644 index aee369d6..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/JobsResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.woowacourse.gongcheck.application.response; - -import static java.util.stream.Collectors.toList; - -import com.woowacourse.gongcheck.domain.job.Job; -import java.util.List; -import lombok.Getter; -import org.springframework.data.domain.Slice; - -@Getter -public class JobsResponse { - - private List jobs; - private boolean hasNext; - - private JobsResponse() { - } - - private JobsResponse(final List jobs, final boolean hasNext) { - this.jobs = jobs; - this.hasNext = hasNext; - } - - public static JobsResponse from(final Slice jobs) { - return new JobsResponse( - jobs.getContent() - .stream() - .map(JobResponse::from) - .collect(toList()), - jobs.hasNext() - ); - } - - public static JobsResponse of(final List jobs, final boolean hasNext) { - return new JobsResponse( - jobs.stream() - .map(JobResponse::from) - .collect(toList()), - hasNext - ); - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/RunningTaskResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/application/response/RunningTaskResponse.java deleted file mode 100644 index 9b70d6f8..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/RunningTaskResponse.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.woowacourse.gongcheck.application.response; - -import com.woowacourse.gongcheck.domain.task.Task; -import lombok.Getter; - -@Getter -public class RunningTaskResponse { - - private Long id; - private String name; - private boolean checked; - - private RunningTaskResponse() { - } - - private RunningTaskResponse(final Long id, final String name, final Boolean checked) { - this.id = id; - this.name = name; - this.checked = checked; - } - - public static RunningTaskResponse from(final Task task) { - return new RunningTaskResponse(task.getId(), task.getName(), task.getRunningTask().isChecked()); - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/SpacesResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/application/response/SpacesResponse.java deleted file mode 100644 index 10fb1b22..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/SpacesResponse.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.woowacourse.gongcheck.application.response; - -import static java.util.stream.Collectors.toList; - -import com.woowacourse.gongcheck.domain.space.Space; -import java.util.List; -import lombok.Getter; -import org.springframework.data.domain.Slice; - -@Getter -public class SpacesResponse { - - private List spaces; - private boolean hasNext; - - private SpacesResponse() { - } - - private SpacesResponse(final List spaces, final boolean hasNext) { - this.spaces = spaces; - this.hasNext = hasNext; - } - - public static SpacesResponse from(final Slice spaces) { - return new SpacesResponse( - spaces.getContent() - .stream() - .map(SpaceResponse::from) - .collect(toList()), - spaces.hasNext() - ); - } - - public static SpacesResponse of(final List spaces, final boolean hasNext) { - return new SpacesResponse( - spaces.stream() - .map(SpaceResponse::from) - .collect(toList()), - hasNext - ); - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/SubmissionResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/application/response/SubmissionResponse.java deleted file mode 100644 index 5fdfde3d..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/SubmissionResponse.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.woowacourse.gongcheck.application.response; - -import com.woowacourse.gongcheck.domain.job.Job; -import lombok.Getter; - -@Getter -public class SubmissionResponse { - - private String slackUrl; - private String author; - private String spaceName; - private String jobName; - - private SubmissionResponse() { - } - - private SubmissionResponse(final String slackUrl, final String author, final String spaceName, - final String jobName) { - this.slackUrl = slackUrl; - this.author = author; - this.spaceName = spaceName; - this.jobName = jobName; - } - - public static SubmissionResponse of(final String author, final Job job) { - return new SubmissionResponse(job.getSlackUrl(), author, job.getSpace().getName(), job.getName()); - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProvider.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProvider.java new file mode 100644 index 00000000..dd68d412 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProvider.java @@ -0,0 +1,38 @@ +package com.woowacourse.gongcheck.auth.application; + +import com.woowacourse.gongcheck.exception.BusinessException; +import org.springframework.stereotype.Component; + +@Component +public class EntranceCodeProvider { + + private static final int MINIMUM_ID_SIZE = 1; + + private final HashTranslator hashTranslator; + + public EntranceCodeProvider(final HashTranslator hashTranslator) { + this.hashTranslator = hashTranslator; + } + + public String createEntranceCode(final Long id) { + validateIdSize(id); + return hashTranslator.encode(String.valueOf(id)); + } + + public Long parseId(final String entranceCode) { + String result = hashTranslator.decode(entranceCode); + try { + Long id = Long.parseLong(result); + validateIdSize(id); + return id; + } catch (NumberFormatException | BusinessException e) { + throw new BusinessException("유효하지 않은 입장코드입니다."); + } + } + + private void validateIdSize(final Long id) { + if (id < MINIMUM_ID_SIZE) { + throw new BusinessException("유효하지 않은 id입니다."); + } + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/GuestAuthService.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/GuestAuthService.java similarity index 60% rename from backend/src/main/java/com/woowacourse/gongcheck/application/GuestAuthService.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/application/GuestAuthService.java index 1df34b11..29b23376 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/GuestAuthService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/GuestAuthService.java @@ -1,12 +1,12 @@ -package com.woowacourse.gongcheck.application; +package com.woowacourse.gongcheck.auth.application; -import static com.woowacourse.gongcheck.presentation.Authority.GUEST; +import static com.woowacourse.gongcheck.auth.domain.Authority.GUEST; -import com.woowacourse.gongcheck.application.response.GuestTokenResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.host.SpacePassword; -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.auth.application.response.GuestTokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.host.SpacePassword; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,7 +22,7 @@ public GuestAuthService(final HostRepository hostRepository, final JwtTokenProvi this.jwtTokenProvider = jwtTokenProvider; } - public GuestTokenResponse createToken(final long hostId, final GuestEnterRequest request) { + public GuestTokenResponse createToken(final Long hostId, final GuestEnterRequest request) { Host host = hostRepository.getById(hostId); host.checkPassword(new SpacePassword(request.getPassword())); diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/application/HashTranslator.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/HashTranslator.java new file mode 100644 index 00000000..63ac6efa --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/HashTranslator.java @@ -0,0 +1,8 @@ +package com.woowacourse.gongcheck.auth.application; + +public interface HashTranslator { + + String encode(final String input); + + String decode(final String input); +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/HostAuthService.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/HostAuthService.java similarity index 70% rename from backend/src/main/java/com/woowacourse/gongcheck/application/HostAuthService.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/application/HostAuthService.java index c1496ec2..2dbacf32 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/HostAuthService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/HostAuthService.java @@ -1,12 +1,13 @@ -package com.woowacourse.gongcheck.application; +package com.woowacourse.gongcheck.auth.application; -import static com.woowacourse.gongcheck.presentation.Authority.HOST; +import static com.woowacourse.gongcheck.auth.domain.Authority.HOST; -import com.woowacourse.gongcheck.application.response.GithubProfileResponse; -import com.woowacourse.gongcheck.application.response.TokenResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.presentation.request.TokenRequest; +import com.woowacourse.gongcheck.auth.application.response.GithubProfileResponse; +import com.woowacourse.gongcheck.auth.application.response.TokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.TokenRequest; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.infrastructure.oauth.GithubOauthClient; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -27,8 +28,7 @@ public HostAuthService(final JwtTokenProvider jwtTokenProvider, final GithubOaut @Transactional public TokenResponse createToken(final TokenRequest request) { - String accessToken = githubOauthClient.requestAccessToken(request.getCode()); - GithubProfileResponse githubProfileResponse = githubOauthClient.requestGithubProfile(accessToken); + GithubProfileResponse githubProfileResponse = githubOauthClient.requestGithubProfileByCode(request.getCode()); boolean alreadyJoin = hostRepository.existsByGithubId(githubProfileResponse.getGithubId()); Host host = findOrCreateHost(alreadyJoin, githubProfileResponse); String token = jwtTokenProvider.createToken(String.valueOf(host.getId()), HOST); diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/JwtTokenProvider.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/JwtTokenProvider.java similarity index 75% rename from backend/src/main/java/com/woowacourse/gongcheck/application/JwtTokenProvider.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/application/JwtTokenProvider.java index cc0cd31c..a8ba3792 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/JwtTokenProvider.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/JwtTokenProvider.java @@ -1,7 +1,7 @@ -package com.woowacourse.gongcheck.application; +package com.woowacourse.gongcheck.auth.application; +import com.woowacourse.gongcheck.auth.domain.Authority; import com.woowacourse.gongcheck.exception.UnauthorizedException; -import com.woowacourse.gongcheck.presentation.Authority; public interface JwtTokenProvider { String createToken(final String subject, final Authority authority); diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/GithubAccessTokenResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubAccessTokenResponse.java similarity index 85% rename from backend/src/main/java/com/woowacourse/gongcheck/application/response/GithubAccessTokenResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubAccessTokenResponse.java index 8860af70..25682b10 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/GithubAccessTokenResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubAccessTokenResponse.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.application.response; +package com.woowacourse.gongcheck.auth.application.response; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/GithubProfileResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubProfileResponse.java similarity index 83% rename from backend/src/main/java/com/woowacourse/gongcheck/application/response/GithubProfileResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubProfileResponse.java index 822e22cf..3088ef13 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/GithubProfileResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GithubProfileResponse.java @@ -1,8 +1,7 @@ -package com.woowacourse.gongcheck.application.response; +package com.woowacourse.gongcheck.auth.application.response; import com.fasterxml.jackson.annotation.JsonProperty; -import com.woowacourse.gongcheck.domain.host.Host; -import java.time.LocalDateTime; +import com.woowacourse.gongcheck.core.domain.host.Host; import lombok.Getter; @Getter @@ -32,7 +31,6 @@ public Host toHost() { return Host.builder() .githubId(getGithubId()) .imageUrl(imageUrl) - .createdAt(LocalDateTime.now()) .build(); } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/GuestTokenResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GuestTokenResponse.java similarity index 84% rename from backend/src/main/java/com/woowacourse/gongcheck/application/response/GuestTokenResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GuestTokenResponse.java index a5ce1c10..8b8fae06 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/GuestTokenResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/GuestTokenResponse.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.application.response; +package com.woowacourse.gongcheck.auth.application.response; import lombok.Getter; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/TokenResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/TokenResponse.java similarity index 73% rename from backend/src/main/java/com/woowacourse/gongcheck/application/response/TokenResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/TokenResponse.java index 0af9dc4f..495799b0 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/TokenResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/application/response/TokenResponse.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.application.response; +package com.woowacourse.gongcheck.auth.application.response; import lombok.Getter; @@ -11,7 +11,7 @@ public class TokenResponse { private TokenResponse() { } - private TokenResponse(final String token, final boolean alreadyJoin) { + public TokenResponse(final String token, final boolean alreadyJoin) { this.token = token; this.alreadyJoin = alreadyJoin; } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationContext.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/domain/AuthenticationContext.java similarity index 90% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationContext.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/domain/AuthenticationContext.java index eb71aa1b..0c57ce99 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationContext.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/domain/AuthenticationContext.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.presentation; +package com.woowacourse.gongcheck.auth.domain; import lombok.Getter; import org.springframework.stereotype.Component; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/domain/Authority.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/domain/Authority.java new file mode 100644 index 00000000..c3b1d677 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/domain/Authority.java @@ -0,0 +1,9 @@ +package com.woowacourse.gongcheck.auth.domain; + +public enum Authority { + GUEST, HOST; + + public boolean isHost() { + return this == HOST; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationInterceptor.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationInterceptor.java similarity index 80% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationInterceptor.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationInterceptor.java index 3b846cba..88816762 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationInterceptor.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationInterceptor.java @@ -1,8 +1,10 @@ -package com.woowacourse.gongcheck.presentation; +package com.woowacourse.gongcheck.auth.presentation; -import com.woowacourse.gongcheck.application.JwtTokenProvider; +import com.woowacourse.gongcheck.auth.application.JwtTokenProvider; +import com.woowacourse.gongcheck.auth.domain.AuthenticationContext; +import com.woowacourse.gongcheck.auth.domain.Authority; +import com.woowacourse.gongcheck.auth.support.JwtTokenExtractor; import com.woowacourse.gongcheck.exception.UnauthorizedException; -import com.woowacourse.gongcheck.support.AuthorizationTokenExtractor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; @@ -28,7 +30,7 @@ public boolean preHandle(final HttpServletRequest request, final HttpServletResp return true; } - String token = AuthorizationTokenExtractor.extractToken(request) + String token = JwtTokenExtractor.extractToken(request) .orElseThrow(() -> new UnauthorizedException("헤더에 토큰 값이 정상적으로 존재하지 않습니다.")); String subject = jwtTokenProvider.extractSubject(token); diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationPrincipal.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationPrincipal.java similarity index 83% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationPrincipal.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationPrincipal.java index eb9dd253..8e547343 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationPrincipal.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationPrincipal.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.presentation; +package com.woowacourse.gongcheck.auth.presentation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationPrincipalArgumentResolver.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationPrincipalArgumentResolver.java similarity index 91% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationPrincipalArgumentResolver.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationPrincipalArgumentResolver.java index b09e6d17..a41e9aec 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/AuthenticationPrincipalArgumentResolver.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/AuthenticationPrincipalArgumentResolver.java @@ -1,5 +1,6 @@ -package com.woowacourse.gongcheck.presentation; +package com.woowacourse.gongcheck.auth.presentation; +import com.woowacourse.gongcheck.auth.domain.AuthenticationContext; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/GuestAuthController.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/GuestAuthController.java similarity index 57% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/GuestAuthController.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/GuestAuthController.java index 2c3e89bc..26df3759 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/GuestAuthController.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/GuestAuthController.java @@ -1,8 +1,9 @@ -package com.woowacourse.gongcheck.presentation; +package com.woowacourse.gongcheck.auth.presentation; -import com.woowacourse.gongcheck.application.GuestAuthService; -import com.woowacourse.gongcheck.application.response.GuestTokenResponse; -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.auth.application.EntranceCodeProvider; +import com.woowacourse.gongcheck.auth.application.GuestAuthService; +import com.woowacourse.gongcheck.auth.application.response.GuestTokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; import javax.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PathVariable; @@ -16,14 +17,18 @@ public class GuestAuthController { private final GuestAuthService guestAuthService; + private final EntranceCodeProvider entranceCodeProvider; - public GuestAuthController(final GuestAuthService guestAuthService) { + public GuestAuthController(final GuestAuthService guestAuthService, + final EntranceCodeProvider entranceCodeProvider) { this.guestAuthService = guestAuthService; + this.entranceCodeProvider = entranceCodeProvider; } - @PostMapping("/hosts/{hostId}/enter") - public ResponseEntity createGuestToken(@PathVariable final long hostId, + @PostMapping("/hosts/{entranceCode}/enter") + public ResponseEntity createGuestToken(@PathVariable final String entranceCode, @Valid @RequestBody final GuestEnterRequest request) { + Long hostId = entranceCodeProvider.parseId(entranceCode); GuestTokenResponse response = guestAuthService.createToken(hostId, request); return ResponseEntity.ok(response); } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/HostAuthController.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/HostAuthController.java similarity index 73% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/HostAuthController.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/HostAuthController.java index fd825263..5e15794a 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/HostAuthController.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/HostAuthController.java @@ -1,8 +1,8 @@ -package com.woowacourse.gongcheck.presentation; +package com.woowacourse.gongcheck.auth.presentation; -import com.woowacourse.gongcheck.application.HostAuthService; -import com.woowacourse.gongcheck.application.response.TokenResponse; -import com.woowacourse.gongcheck.presentation.request.TokenRequest; +import com.woowacourse.gongcheck.auth.application.HostAuthService; +import com.woowacourse.gongcheck.auth.application.response.TokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.TokenRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostOnly.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostOnly.java new file mode 100644 index 00000000..d7d32718 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostOnly.java @@ -0,0 +1,11 @@ +package com.woowacourse.gongcheck.auth.presentation.aop; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface HostOnly { +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostVerifier.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostVerifier.java new file mode 100644 index 00000000..5133c6c5 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/aop/HostVerifier.java @@ -0,0 +1,27 @@ +package com.woowacourse.gongcheck.auth.presentation.aop; + +import com.woowacourse.gongcheck.auth.domain.AuthenticationContext; +import com.woowacourse.gongcheck.auth.domain.Authority; +import com.woowacourse.gongcheck.exception.UnauthorizedException; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; + +@Aspect +@Component +public class HostVerifier { + + private final AuthenticationContext authenticationContext; + + public HostVerifier(final AuthenticationContext authenticationContext) { + this.authenticationContext = authenticationContext; + } + + @Before("@annotation(com.woowacourse.gongcheck.auth.presentation.aop.HostOnly)") + public void checkHost() { + Authority authority = authenticationContext.getAuthority(); + if (!authority.isHost()) { + throw new UnauthorizedException("호스트만 입장 가능합니다."); + } + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/GithubAccessTokenRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GithubAccessTokenRequest.java similarity index 89% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/request/GithubAccessTokenRequest.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GithubAccessTokenRequest.java index bced3d1d..19849aa7 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/GithubAccessTokenRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GithubAccessTokenRequest.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.presentation.request; +package com.woowacourse.gongcheck.auth.presentation.request; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/GuestEnterRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GuestEnterRequest.java similarity index 82% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/request/GuestEnterRequest.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GuestEnterRequest.java index 05833cb6..46e77da0 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/GuestEnterRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/GuestEnterRequest.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.presentation.request; +package com.woowacourse.gongcheck.auth.presentation.request; import javax.validation.constraints.NotNull; import lombok.Getter; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/TokenRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/TokenRequest.java similarity index 76% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/request/TokenRequest.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/TokenRequest.java index 1dc89316..73ec5e60 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/TokenRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/presentation/request/TokenRequest.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.presentation.request; +package com.woowacourse.gongcheck.auth.presentation.request; import lombok.Getter; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/support/AuthorizationTokenExtractor.java b/backend/src/main/java/com/woowacourse/gongcheck/auth/support/JwtTokenExtractor.java similarity index 87% rename from backend/src/main/java/com/woowacourse/gongcheck/support/AuthorizationTokenExtractor.java rename to backend/src/main/java/com/woowacourse/gongcheck/auth/support/JwtTokenExtractor.java index 9e5b2214..e15f7244 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/support/AuthorizationTokenExtractor.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/auth/support/JwtTokenExtractor.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.support; +package com.woowacourse.gongcheck.auth.support; import java.util.Optional; import java.util.regex.Matcher; @@ -7,13 +7,13 @@ import org.apache.logging.log4j.util.Strings; import org.springframework.http.HttpHeaders; -public class AuthorizationTokenExtractor { +public class JwtTokenExtractor { private static final Pattern JWT_TOKEN_PATTERN = Pattern.compile( "^Bearer \\b([A-Za-z\\d-_]*\\.[A-Za-z\\d-_]*\\.[A-Za-z\\d-_]*)$"); private static final int TOKEN_INDEX = 1; - private AuthorizationTokenExtractor() { + private JwtTokenExtractor() { } public static Optional extractToken(final HttpServletRequest request) { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/AsyncConfig.java b/backend/src/main/java/com/woowacourse/gongcheck/config/AsyncConfig.java similarity index 93% rename from backend/src/main/java/com/woowacourse/gongcheck/application/AsyncConfig.java rename to backend/src/main/java/com/woowacourse/gongcheck/config/AsyncConfig.java index eb90a53a..c05c894b 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/AsyncConfig.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/config/AsyncConfig.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.application; +package com.woowacourse.gongcheck.config; import java.util.concurrent.Executor; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/config/ImageUploadConfig.java b/backend/src/main/java/com/woowacourse/gongcheck/config/ImageUploadConfig.java new file mode 100644 index 00000000..e47ef458 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/config/ImageUploadConfig.java @@ -0,0 +1,23 @@ +package com.woowacourse.gongcheck.config; + +import com.woowacourse.gongcheck.core.application.ImageUploader; +import com.woowacourse.gongcheck.infrastructure.imageuploader.OwnServerImageUploader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class ImageUploadConfig { + + private final WebClient webClient; + + public ImageUploadConfig(@Value("${file.upload-url}") final String imageServerUrl) { + this.webClient = WebClient.create(imageServerUrl); + } + + @Bean + public ImageUploader imageUploader() { + return new OwnServerImageUploader(webClient); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/config/JpaConfig.java b/backend/src/main/java/com/woowacourse/gongcheck/config/JpaConfig.java new file mode 100644 index 00000000..7cecb3aa --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/config/JpaConfig.java @@ -0,0 +1,9 @@ +package com.woowacourse.gongcheck.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfig { +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/config/WebConfig.java b/backend/src/main/java/com/woowacourse/gongcheck/config/WebConfig.java index 1a531912..510400ba 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/config/WebConfig.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/config/WebConfig.java @@ -1,7 +1,7 @@ package com.woowacourse.gongcheck.config; -import com.woowacourse.gongcheck.presentation.AuthenticationInterceptor; -import com.woowacourse.gongcheck.presentation.AuthenticationPrincipalArgumentResolver; +import com.woowacourse.gongcheck.auth.presentation.AuthenticationInterceptor; +import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipalArgumentResolver; import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/AlertService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/AlertService.java new file mode 100644 index 00000000..5283569d --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/AlertService.java @@ -0,0 +1,8 @@ +package com.woowacourse.gongcheck.core.application; + +import com.woowacourse.gongcheck.core.application.response.SubmissionCreatedResponse; + +public interface AlertService { + + void sendMessage(final SubmissionCreatedResponse submissionCreatedResponse); +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/HostService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/HostService.java new file mode 100644 index 00000000..f73b1060 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/HostService.java @@ -0,0 +1,33 @@ +package com.woowacourse.gongcheck.core.application; + +import com.woowacourse.gongcheck.auth.application.EntranceCodeProvider; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.host.SpacePassword; +import com.woowacourse.gongcheck.core.presentation.request.SpacePasswordChangeRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class HostService { + + private final HostRepository hostRepository; + private final EntranceCodeProvider entranceCodeProvider; + + public HostService(final HostRepository hostRepository, final EntranceCodeProvider entranceCodeProvider) { + this.hostRepository = hostRepository; + this.entranceCodeProvider = entranceCodeProvider; + } + + @Transactional + public void changeSpacePassword(final Long hostId, final SpacePasswordChangeRequest request) { + Host host = hostRepository.getById(hostId); + host.changeSpacePassword(new SpacePassword(request.getPassword())); + } + + public String createEntranceCode(final Long hostId) { + Host host = hostRepository.getById(hostId); + return entranceCodeProvider.createEntranceCode(host.getId()); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/ImageUploader.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/ImageUploader.java new file mode 100644 index 00000000..566aaf72 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/ImageUploader.java @@ -0,0 +1,10 @@ +package com.woowacourse.gongcheck.core.application; + + +import com.woowacourse.gongcheck.core.application.response.ImageUrlResponse; +import org.springframework.web.multipart.MultipartFile; + +public interface ImageUploader { + + ImageUrlResponse upload(MultipartFile file, String directoryName); +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/JobService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/JobService.java new file mode 100644 index 00000000..baf49459 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/JobService.java @@ -0,0 +1,150 @@ +package com.woowacourse.gongcheck.core.application; + +import static java.util.stream.Collectors.toList; + +import com.woowacourse.gongcheck.core.application.response.JobsResponse; +import com.woowacourse.gongcheck.core.application.response.SlackUrlResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.section.SectionRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.task.TaskRepository; +import com.woowacourse.gongcheck.core.domain.vo.Description; +import com.woowacourse.gongcheck.core.domain.vo.Name; +import com.woowacourse.gongcheck.core.presentation.request.JobCreateRequest; +import com.woowacourse.gongcheck.core.presentation.request.SectionCreateRequest; +import com.woowacourse.gongcheck.core.presentation.request.SlackUrlChangeRequest; +import com.woowacourse.gongcheck.core.presentation.request.TaskCreateRequest; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class JobService { + + private final HostRepository hostRepository; + private final SpaceRepository spaceRepository; + private final JobRepository jobRepository; + private final SectionRepository sectionRepository; + private final TaskRepository taskRepository; + private final RunningTaskRepository runningTaskRepository; + + public JobService(final HostRepository hostRepository, final SpaceRepository spaceRepository, + final JobRepository jobRepository, final SectionRepository sectionRepository, + final TaskRepository taskRepository, final RunningTaskRepository runningTaskRepository) { + this.hostRepository = hostRepository; + this.spaceRepository = spaceRepository; + this.jobRepository = jobRepository; + this.sectionRepository = sectionRepository; + this.taskRepository = taskRepository; + this.runningTaskRepository = runningTaskRepository; + } + + public JobsResponse findJobs(final Long hostId, final Long spaceId) { + Host host = hostRepository.getById(hostId); + Space space = spaceRepository.getByHostAndId(host, spaceId); + List jobs = jobRepository.findAllBySpaceHostAndSpace(host, space); + return JobsResponse.from(jobs); + } + + @Transactional + public Long createJob(final Long hostId, final Long spaceId, final JobCreateRequest request) { + Host host = hostRepository.getById(hostId); + Space space = spaceRepository.getByHostAndId(host, spaceId); + return saveJob(request, space); + } + + @Transactional + public void removeJob(final Long hostId, final Long jobId) { + Host host = hostRepository.getById(hostId); + Job job = jobRepository.getBySpaceHostAndId(host, jobId); + List
sections = sectionRepository.findAllByJob(job); + List tasks = taskRepository.findAllBySectionIn(sections); + + deleteJob(jobId, sections, tasks); + } + + @Transactional + public Long updateJob(final Long hostId, final Long jobId, final JobCreateRequest request) { + Host host = hostRepository.getById(hostId); + Job job = jobRepository.getBySpaceHostAndId(host, jobId); + Space space = job.getSpace(); + List
sections = sectionRepository.findAllByJob(job); + List tasks = taskRepository.findAllBySectionIn(sections); + + deleteJob(jobId, sections, tasks); + return saveJob(request, space); + } + + public SlackUrlResponse findSlackUrl(final Long hostId, final Long jobId) { + Host host = hostRepository.getById(hostId); + Job job = jobRepository.getBySpaceHostAndId(host, jobId); + return SlackUrlResponse.from(job); + } + + @Transactional + public void changeSlackUrl(final Long hostId, final Long jobId, final SlackUrlChangeRequest request) { + Host host = hostRepository.getById(hostId); + Job job = jobRepository.getBySpaceHostAndId(host, jobId); + job.changeSlackUrl(request.getSlackUrl()); + } + + private Long saveJob(final JobCreateRequest request, final Space space) { + Job job = Job.builder() + .space(space) + .name(new Name(request.getName())) + .build(); + jobRepository.save(job); + createSectionsAndTasks(request.getSections(), job); + return job.getId(); + } + + private void createSectionsAndTasks(final List sectionCreateRequests, final Job job) { + sectionCreateRequests.forEach(sectionRequest -> createSectionAndTasks(sectionRequest, job)); + } + + private Section createSectionAndTasks(final SectionCreateRequest sectionCreateRequest, final Job job) { + Section section = Section.builder() + .job(job) + .name(new Name(sectionCreateRequest.getName())) + .description(new Description(sectionCreateRequest.getDescription())) + .imageUrl(sectionCreateRequest.getImageUrl()) + .build(); + sectionRepository.save(section); + createTasks(sectionCreateRequest.getTasks(), section); + return section; + } + + private void createTasks(final List taskCreateRequests, final Section section) { + List tasks = taskCreateRequests + .stream() + .map(taskRequest -> createTask(taskRequest, section)) + .collect(toList()); + taskRepository.saveAll(tasks); + } + + private Task createTask(final TaskCreateRequest taskCreateRequest, final Section section) { + return Task.builder() + .section(section) + .name(new Name(taskCreateRequest.getName())) + .description(new Description(taskCreateRequest.getDescription())) + .imageUrl(taskCreateRequest.getImageUrl()) + .build(); + } + + private void deleteJob(final Long jobId, final List
sections, final List tasks) { + runningTaskRepository.deleteAllByIdInBatch(tasks.stream() + .map(Task::getId) + .collect(toList())); + taskRepository.deleteAllInBatch(tasks); + sectionRepository.deleteAllInBatch(sections); + jobRepository.deleteById(jobId); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java new file mode 100644 index 00000000..ff0be4e0 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/SpaceService.java @@ -0,0 +1,114 @@ +package com.woowacourse.gongcheck.core.application; + +import static java.util.stream.Collectors.toList; + +import com.woowacourse.gongcheck.core.application.response.SpaceResponse; +import com.woowacourse.gongcheck.core.application.response.SpacesResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.section.SectionRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.task.TaskRepository; +import com.woowacourse.gongcheck.core.domain.vo.Name; +import com.woowacourse.gongcheck.core.presentation.request.SpaceChangeRequest; +import com.woowacourse.gongcheck.core.presentation.request.SpaceCreateRequest; +import com.woowacourse.gongcheck.exception.BusinessException; +import java.util.List; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class SpaceService { + + private final HostRepository hostRepository; + private final SpaceRepository spaceRepository; + private final JobRepository jobRepository; + private final SectionRepository sectionRepository; + private final TaskRepository taskRepository; + private final RunningTaskRepository runningTaskRepository; + + public SpaceService(final HostRepository hostRepository, final SpaceRepository spaceRepository, + final JobRepository jobRepository, final SectionRepository sectionRepository, + final TaskRepository taskRepository, final RunningTaskRepository runningTaskRepository) { + this.hostRepository = hostRepository; + this.spaceRepository = spaceRepository; + this.jobRepository = jobRepository; + this.sectionRepository = sectionRepository; + this.taskRepository = taskRepository; + this.runningTaskRepository = runningTaskRepository; + } + + public SpacesResponse findSpaces(final Long hostId) { + Host host = hostRepository.getById(hostId); + List spaces = spaceRepository.findAllByHost(host); + return SpacesResponse.from(spaces); + } + + @Transactional + public Long createSpace(final Long hostId, final SpaceCreateRequest request) { + Host host = hostRepository.getById(hostId); + Name spaceName = new Name(request.getName()); + checkDuplicateSpaceName(spaceName, host); + + Space space = Space.builder() + .host(host) + .name(spaceName) + .imageUrl(request.getImageUrl()) + .build(); + return spaceRepository.save(space) + .getId(); + } + + public SpaceResponse findSpace(final Long hostId, final Long spaceId) { + Host host = hostRepository.getById(hostId); + Space space = spaceRepository.getByHostAndId(host, spaceId); + return SpaceResponse.from(space); + } + + @Transactional + public void changeSpace(final Long hostId, final Long spaceId, final SpaceChangeRequest request) { + Host host = hostRepository.getById(hostId); + Space space = spaceRepository.getByHostAndId(host, spaceId); + Name changeName = new Name(request.getName()); + checkDuplicateSpaceName(changeName, host, space); + + space.changeName(changeName); + space.changeImageUrl(request.getImageUrl()); + } + + @Transactional + public void removeSpace(final Long hostId, final Long spaceId) { + Host host = hostRepository.getById(hostId); + Space space = spaceRepository.getByHostAndId(host, spaceId); + List jobs = jobRepository.findAllBySpace(space); + List
sections = sectionRepository.findAllByJobIn(jobs); + List tasks = taskRepository.findAllBySectionIn(sections); + + runningTaskRepository.deleteAllByIdInBatch(tasks.stream() + .map(Task::getId) + .collect(toList())); + taskRepository.deleteAllInBatch(tasks); + sectionRepository.deleteAllInBatch(sections); + jobRepository.deleteAllInBatch(jobs); + spaceRepository.deleteById(spaceId); + } + + private void checkDuplicateSpaceName(final Name spaceName, final Host host) { + if (spaceRepository.existsByHostAndName(host, spaceName)) { + throw new BusinessException("이미 존재하는 이름입니다."); + } + } + + private void checkDuplicateSpaceName(final Name spaceName, final Host host, final Space space) { + if (spaceRepository.existsByHostAndNameAndIdNot(host, spaceName, space.getId())) { + throw new BusinessException("이미 존재하는 이름입니다."); + } + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/SubmissionService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/SubmissionService.java new file mode 100644 index 00000000..a001bd18 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/SubmissionService.java @@ -0,0 +1,93 @@ +package com.woowacourse.gongcheck.core.application; + +import com.woowacourse.gongcheck.core.application.response.SubmissionCreatedResponse; +import com.woowacourse.gongcheck.core.application.response.SubmissionsResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import com.woowacourse.gongcheck.core.domain.submission.Submission; +import com.woowacourse.gongcheck.core.domain.submission.SubmissionRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTasks; +import com.woowacourse.gongcheck.core.domain.task.TaskRepository; +import com.woowacourse.gongcheck.core.domain.task.Tasks; +import com.woowacourse.gongcheck.core.presentation.request.SubmissionRequest; +import com.woowacourse.gongcheck.exception.BusinessException; +import java.util.List; +import org.springframework.core.task.TaskRejectedException; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class SubmissionService { + + private final HostRepository hostRepository; + private final JobRepository jobRepository; + private final SpaceRepository spaceRepository; + private final TaskRepository taskRepository; + private final RunningTaskRepository runningTaskRepository; + private final SubmissionRepository submissionRepository; + private final AlertService alertService; + + public SubmissionService(final HostRepository hostRepository, final JobRepository jobRepository, + final SpaceRepository spaceRepository, final TaskRepository taskRepository, + final RunningTaskRepository runningTaskRepository, + final SubmissionRepository submissionRepository, final AlertService alertService) { + this.hostRepository = hostRepository; + this.jobRepository = jobRepository; + this.spaceRepository = spaceRepository; + this.taskRepository = taskRepository; + this.runningTaskRepository = runningTaskRepository; + this.submissionRepository = submissionRepository; + this.alertService = alertService; + } + + @Transactional(isolation = Isolation.SERIALIZABLE) + public void submitJobCompletion(final Long hostId, final Long jobId, + final SubmissionRequest request) { + Host host = hostRepository.getById(hostId); + Job job = jobRepository.getBySpaceHostAndId(host, jobId); + saveSubmissionAndClearRunningTasks(request, job); + if (job.hasUrl()) { + alertMessage(request, job); + } + } + + private void alertMessage(final SubmissionRequest request, final Job job) { + try { + alertService.sendMessage(SubmissionCreatedResponse.of(request.getAuthor(), job)); + } catch (TaskRejectedException e) { + throw new RuntimeException(e); + } + } + + public SubmissionsResponse findPage(final Long hostId, final Long spaceId, final Pageable pageable) { + Host host = hostRepository.getById(hostId); + Space space = spaceRepository.getByHostAndId(host, spaceId); + List jobs = jobRepository.findAllBySpace(space); + Slice submissions = submissionRepository.findAllByJobIn(jobs, pageable); + return SubmissionsResponse.from(submissions); + } + + private void saveSubmissionAndClearRunningTasks(final SubmissionRequest request, final Job job) { + Tasks tasks = new Tasks(taskRepository.findAllBySectionJob(job)); + validateRunning(tasks); + RunningTasks runningTasks = new RunningTasks(runningTaskRepository.findAllById(tasks.getTaskIds())); + runningTasks.validateCompletion(); + submissionRepository.save(job.createSubmission(request.getAuthor())); + runningTaskRepository.deleteAllByIdInBatch(tasks.getTaskIds()); + } + + private void validateRunning(final Tasks tasks) { + if (!runningTaskRepository.existsByTaskIdIn(tasks.getTaskIds())) { + throw new BusinessException("현재 제출할 수 있는 진행중인 작업이 존재하지 않습니다."); + } + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/TaskService.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/TaskService.java similarity index 64% rename from backend/src/main/java/com/woowacourse/gongcheck/application/TaskService.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/application/TaskService.java index 39fdd1d9..e307979c 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/TaskService.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/TaskService.java @@ -1,17 +1,19 @@ -package com.woowacourse.gongcheck.application; +package com.woowacourse.gongcheck.core.application; -import com.woowacourse.gongcheck.application.response.JobActiveResponse; -import com.woowacourse.gongcheck.application.response.RunningTasksResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.task.RunningTask; -import com.woowacourse.gongcheck.domain.task.RunningTaskRepository; -import com.woowacourse.gongcheck.domain.task.Task; -import com.woowacourse.gongcheck.domain.task.TaskRepository; -import com.woowacourse.gongcheck.domain.task.Tasks; +import com.woowacourse.gongcheck.core.application.response.JobActiveResponse; +import com.woowacourse.gongcheck.core.application.response.RunningTasksResponse; +import com.woowacourse.gongcheck.core.application.response.TasksResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTask; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.task.TaskRepository; +import com.woowacourse.gongcheck.core.domain.task.Tasks; import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.NotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -34,7 +36,10 @@ public TaskService(final HostRepository hostRepository, final JobRepository jobR @Transactional public void createNewRunningTasks(final Long hostId, final Long jobId) { - Tasks tasks = createTasksByHostIdAndJobId(hostId, jobId); + Tasks tasks = findTasksByHostIdAndJobId(hostId, jobId); + if (tasks.isEmpty()) { + throw new NotFoundException("작업이 존재하지 않습니다."); + } if (existsAnyRunningTaskIn(tasks)) { throw new BusinessException("현재 진행중인 작업이 존재하여 새로운 작업을 생성할 수 없습니다."); } @@ -42,12 +47,12 @@ public void createNewRunningTasks(final Long hostId, final Long jobId) { } public JobActiveResponse isJobActivated(final Long hostId, final Long jobId) { - Tasks tasks = createTasksByHostIdAndJobId(hostId, jobId); + Tasks tasks = findTasksByHostIdAndJobId(hostId, jobId); return JobActiveResponse.from(existsAnyRunningTaskIn(tasks)); } public RunningTasksResponse findRunningTasks(final Long hostId, final Long jobId) { - Tasks tasks = createTasksByHostIdAndJobId(hostId, jobId); + Tasks tasks = findTasksByHostIdAndJobId(hostId, jobId); return findExistingRunningTasks(tasks); } @@ -61,7 +66,12 @@ public void flipRunningTask(final Long hostId, final Long taskId) { runningTask.flipCheckedStatus(); } - private Tasks createTasksByHostIdAndJobId(final Long hostId, final Long jobId) { + public TasksResponse findTasks(final Long hostId, final Long jobId) { + Tasks tasks = findTasksByHostIdAndJobId(hostId, jobId); + return TasksResponse.from(tasks); + } + + private Tasks findTasksByHostIdAndJobId(final Long hostId, final Long jobId) { Host host = hostRepository.getById(hostId); Job job = jobRepository.getBySpaceHostAndId(host, jobId); return new Tasks(taskRepository.findAllBySectionJob(job)); diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/Attachments.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/Attachments.java similarity index 76% rename from backend/src/main/java/com/woowacourse/gongcheck/application/response/Attachments.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/application/response/Attachments.java index 93d1ecc0..49823592 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/Attachments.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/Attachments.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.application.response; +package com.woowacourse.gongcheck.core.application.response; import com.slack.api.model.Attachment; import com.slack.api.model.Field; @@ -19,15 +19,15 @@ private Attachments(final List attachments) { this.attachments = attachments; } - public static Attachments of(final SubmissionResponse submissionResponse) { + public static Attachments of(final SubmissionCreatedResponse submissionCreatedResponse) { return new Attachments(List.of(Attachment.builder() .fallback("📝 체크리스트가 제출되었습니다.") .color("#99CCFF") .pretext("📝 체크리스트가 제출되었습니다.") .fields(List.of( - Field.builder().value("제출자명 : " + submissionResponse.getAuthor()).build(), - Field.builder().value("공간이름 : " + submissionResponse.getSpaceName()).build(), - Field.builder().value("작업이름 : " + submissionResponse.getJobName()).build())) + Field.builder().value("제출자명 : " + submissionCreatedResponse.getAuthor()).build(), + Field.builder().value("공간이름 : " + submissionCreatedResponse.getSpaceName()).build(), + Field.builder().value("작업이름 : " + submissionCreatedResponse.getJobName()).build())) .footer("제출시간") .ts(String.valueOf(Timestamp.valueOf(LocalDateTime.now()).getTime())) .build())); diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/EntranceCodeResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/EntranceCodeResponse.java new file mode 100644 index 00000000..fd4e2f80 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/EntranceCodeResponse.java @@ -0,0 +1,20 @@ +package com.woowacourse.gongcheck.core.application.response; + +import lombok.Getter; + +@Getter +public class EntranceCodeResponse { + + private String entranceCode; + + private EntranceCodeResponse() { + } + + private EntranceCodeResponse(String entranceCode) { + this.entranceCode = entranceCode; + } + + public static EntranceCodeResponse from(final String entranceCode) { + return new EntranceCodeResponse(entranceCode); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/ImageUrlResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/ImageUrlResponse.java new file mode 100644 index 00000000..f904a5c4 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/ImageUrlResponse.java @@ -0,0 +1,20 @@ +package com.woowacourse.gongcheck.core.application.response; + +import lombok.Getter; + +@Getter +public class ImageUrlResponse { + + private String imageUrl; + + private ImageUrlResponse() { + } + + private ImageUrlResponse(final String imageUrl) { + this.imageUrl = imageUrl; + } + + public static ImageUrlResponse from(final String imageUrl) { + return new ImageUrlResponse(imageUrl); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/JobActiveResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/JobActiveResponse.java similarity index 85% rename from backend/src/main/java/com/woowacourse/gongcheck/application/response/JobActiveResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/application/response/JobActiveResponse.java index 9c7033ec..82b49ae7 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/JobActiveResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/JobActiveResponse.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.application.response; +package com.woowacourse.gongcheck.core.application.response; import lombok.Getter; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/JobResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/JobResponse.java similarity index 63% rename from backend/src/main/java/com/woowacourse/gongcheck/application/response/JobResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/application/response/JobResponse.java index ed5f0f27..e07c5d47 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/JobResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/JobResponse.java @@ -1,6 +1,6 @@ -package com.woowacourse.gongcheck.application.response; +package com.woowacourse.gongcheck.core.application.response; -import com.woowacourse.gongcheck.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.Job; import lombok.Getter; @Getter @@ -18,6 +18,6 @@ private JobResponse(final Long id, final String name) { } public static JobResponse from(final Job job) { - return new JobResponse(job.getId(), job.getName()); + return new JobResponse(job.getId(), job.getName().getValue()); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/JobsResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/JobsResponse.java new file mode 100644 index 00000000..f7fa7223 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/JobsResponse.java @@ -0,0 +1,28 @@ +package com.woowacourse.gongcheck.core.application.response; + +import static java.util.stream.Collectors.toList; + +import com.woowacourse.gongcheck.core.domain.job.Job; +import java.util.List; +import lombok.Getter; + +@Getter +public class JobsResponse { + + private List jobs; + + private JobsResponse() { + } + + private JobsResponse(final List jobs) { + this.jobs = jobs; + } + + public static JobsResponse from(final List jobs) { + return new JobsResponse( + jobs.stream() + .map(JobResponse::from) + .collect(toList()) + ); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/RunningTaskResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/RunningTaskResponse.java new file mode 100644 index 00000000..f9827709 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/RunningTaskResponse.java @@ -0,0 +1,31 @@ +package com.woowacourse.gongcheck.core.application.response; + +import com.woowacourse.gongcheck.core.domain.task.Task; +import lombok.Getter; + +@Getter +public class RunningTaskResponse { + + private Long id; + private String name; + private boolean checked; + private String imageUrl; + private String description; + + private RunningTaskResponse() { + } + + private RunningTaskResponse(final Long id, final String name, final boolean checked, final String imageUrl, + final String description) { + this.id = id; + this.name = name; + this.checked = checked; + this.imageUrl = imageUrl; + this.description = description; + } + + public static RunningTaskResponse from(final Task task) { + return new RunningTaskResponse(task.getId(), task.getName().getValue(), task.getRunningTask().isChecked(), + task.getImageUrl(), task.getDescription().getValue()); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/RunningTasksResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/RunningTasksResponse.java similarity index 65% rename from backend/src/main/java/com/woowacourse/gongcheck/application/response/RunningTasksResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/application/response/RunningTasksResponse.java index aa228047..682d47fd 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/RunningTasksResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/RunningTasksResponse.java @@ -1,6 +1,6 @@ -package com.woowacourse.gongcheck.application.response; +package com.woowacourse.gongcheck.core.application.response; -import com.woowacourse.gongcheck.domain.task.Tasks; +import com.woowacourse.gongcheck.core.domain.task.Tasks; import java.util.List; import lombok.Getter; @@ -12,8 +12,7 @@ public class RunningTasksResponse { private RunningTasksResponse() { } - private RunningTasksResponse( - final List sections) { + private RunningTasksResponse(final List sections) { this.sections = sections; } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/RunningTasksWithSectionResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/RunningTasksWithSectionResponse.java similarity index 59% rename from backend/src/main/java/com/woowacourse/gongcheck/application/response/RunningTasksWithSectionResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/application/response/RunningTasksWithSectionResponse.java index d70ef1fa..14ca576f 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/RunningTasksWithSectionResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/RunningTasksWithSectionResponse.java @@ -1,8 +1,8 @@ -package com.woowacourse.gongcheck.application.response; +package com.woowacourse.gongcheck.core.application.response; -import com.woowacourse.gongcheck.domain.section.Section; -import com.woowacourse.gongcheck.domain.task.Task; -import com.woowacourse.gongcheck.domain.task.Tasks; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.task.Tasks; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -13,15 +13,19 @@ public class RunningTasksWithSectionResponse { private Long id; private String name; + private String imageUrl; + private String description; private List tasks; private RunningTasksWithSectionResponse() { } - private RunningTasksWithSectionResponse(final Long id, final String name, - final List tasks) { + private RunningTasksWithSectionResponse(final Long id, final String name, final String imageUrl, + final String description, final List tasks) { this.id = id; this.name = name; + this.imageUrl = imageUrl; + this.description = description; this.tasks = tasks; } @@ -37,9 +41,9 @@ public static List from(final Tasks tasks) { } private static RunningTasksWithSectionResponse of(final Section section, final List tasks) { - return new RunningTasksWithSectionResponse(section.getId(), section.getName(), - tasks.stream() - .map(RunningTaskResponse::from) - .collect(Collectors.toList())); + return new RunningTasksWithSectionResponse(section.getId(), section.getName().getValue(), + section.getImageUrl(), section.getDescription().getValue(), tasks.stream() + .map(RunningTaskResponse::from) + .collect(Collectors.toList())); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SlackUrlResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SlackUrlResponse.java new file mode 100644 index 00000000..a59814e3 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SlackUrlResponse.java @@ -0,0 +1,21 @@ +package com.woowacourse.gongcheck.core.application.response; + +import com.woowacourse.gongcheck.core.domain.job.Job; +import lombok.Getter; + +@Getter +public class SlackUrlResponse { + + private String slackUrl; + + private SlackUrlResponse() { + } + + public SlackUrlResponse(final String slackUrl) { + this.slackUrl = slackUrl; + } + + public static SlackUrlResponse from(final Job job) { + return new SlackUrlResponse(job.getSlackUrl()); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/response/SpaceResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SpaceResponse.java similarity index 65% rename from backend/src/main/java/com/woowacourse/gongcheck/application/response/SpaceResponse.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SpaceResponse.java index df752e88..f68d0b0f 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/response/SpaceResponse.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SpaceResponse.java @@ -1,6 +1,6 @@ -package com.woowacourse.gongcheck.application.response; +package com.woowacourse.gongcheck.core.application.response; -import com.woowacourse.gongcheck.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.Space; import lombok.Getter; @Getter @@ -20,6 +20,6 @@ private SpaceResponse(final Long id, final String name, final String imageUrl) { } public static SpaceResponse from(final Space space) { - return new SpaceResponse(space.getId(), space.getName(), space.getImageUrl()); + return new SpaceResponse(space.getId(), space.getName().getValue(), space.getImageUrl()); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SpacesResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SpacesResponse.java new file mode 100644 index 00000000..7601f1ae --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SpacesResponse.java @@ -0,0 +1,28 @@ +package com.woowacourse.gongcheck.core.application.response; + +import static java.util.stream.Collectors.toList; + +import com.woowacourse.gongcheck.core.domain.space.Space; +import java.util.List; +import lombok.Getter; + +@Getter +public class SpacesResponse { + + private List spaces; + + private SpacesResponse() { + } + + private SpacesResponse(final List spaces) { + this.spaces = spaces; + } + + public static SpacesResponse from(final List spaces) { + return new SpacesResponse( + spaces.stream() + .map(SpaceResponse::from) + .collect(toList()) + ); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SubmissionCreatedResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SubmissionCreatedResponse.java new file mode 100644 index 00000000..fb3b15a8 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SubmissionCreatedResponse.java @@ -0,0 +1,29 @@ +package com.woowacourse.gongcheck.core.application.response; + +import com.woowacourse.gongcheck.core.domain.job.Job; +import lombok.Getter; + +@Getter +public class SubmissionCreatedResponse { + + private String slackUrl; + private String author; + private String spaceName; + private String jobName; + + private SubmissionCreatedResponse() { + } + + private SubmissionCreatedResponse(final String slackUrl, final String author, final String spaceName, + final String jobName) { + this.slackUrl = slackUrl; + this.author = author; + this.spaceName = spaceName; + this.jobName = jobName; + } + + public static SubmissionCreatedResponse of(final String author, final Job job) { + return new SubmissionCreatedResponse(job.getSlackUrl(), author, job.getSpace().getName().getValue(), + job.getName().getValue()); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SubmissionResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SubmissionResponse.java new file mode 100644 index 00000000..8da46659 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SubmissionResponse.java @@ -0,0 +1,36 @@ +package com.woowacourse.gongcheck.core.application.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.woowacourse.gongcheck.core.domain.submission.Submission; +import java.time.LocalDateTime; +import lombok.Getter; + +@Getter +public class SubmissionResponse { + + private Long submissionId; + private Long jobId; + private String jobName; + private String author; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") + private LocalDateTime createdAt; + + private SubmissionResponse() { + } + + private SubmissionResponse(final Long submissionId, final Long jobId, final String jobName, final String author, + final LocalDateTime createdAt) { + this.submissionId = submissionId; + this.jobId = jobId; + this.jobName = jobName; + this.author = author; + this.createdAt = createdAt; + } + + public static SubmissionResponse from(final Submission submission) { + return new SubmissionResponse(submission.getId(), submission.getJob().getId(), + submission.getJob().getName().getValue(), + submission.getAuthor(), submission.getCreatedAt()); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SubmissionsResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SubmissionsResponse.java new file mode 100644 index 00000000..659c55a8 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/SubmissionsResponse.java @@ -0,0 +1,42 @@ +package com.woowacourse.gongcheck.core.application.response; + +import static java.util.stream.Collectors.toList; + +import com.woowacourse.gongcheck.core.domain.submission.Submission; +import java.util.List; +import lombok.Getter; +import org.springframework.data.domain.Slice; + +@Getter +public class SubmissionsResponse { + + private List submissions; + private boolean hasNext; + + private SubmissionsResponse() { + } + + public SubmissionsResponse(final List submissions, final boolean hasNext) { + this.submissions = submissions; + this.hasNext = hasNext; + } + + public static SubmissionsResponse from(final Slice submissions) { + return new SubmissionsResponse( + submissions.getContent() + .stream() + .map(SubmissionResponse::from) + .collect(toList()), + submissions.hasNext() + ); + } + + public static SubmissionsResponse of(final List submissions, final boolean hasNext) { + return new SubmissionsResponse( + submissions.stream() + .map(SubmissionResponse::from) + .collect(toList()), + hasNext + ); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/TaskResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/TaskResponse.java new file mode 100644 index 00000000..fe71b5bd --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/TaskResponse.java @@ -0,0 +1,28 @@ +package com.woowacourse.gongcheck.core.application.response; + +import com.woowacourse.gongcheck.core.domain.task.Task; +import lombok.Getter; + +@Getter +public class TaskResponse { + + private Long id; + private String name; + private String imageUrl; + private String description; + + private TaskResponse() { + } + + private TaskResponse(final Long id, final String name, final String imageUrl, final String description) { + this.id = id; + this.name = name; + this.imageUrl = imageUrl; + this.description = description; + } + + public static TaskResponse from(final Task task) { + return new TaskResponse(task.getId(), task.getName().getValue(), task.getImageUrl(), + task.getDescription().getValue()); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/TasksResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/TasksResponse.java new file mode 100644 index 00000000..9b61889c --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/TasksResponse.java @@ -0,0 +1,22 @@ +package com.woowacourse.gongcheck.core.application.response; + +import com.woowacourse.gongcheck.core.domain.task.Tasks; +import java.util.List; +import lombok.Getter; + +@Getter +public class TasksResponse { + + private List sections; + + private TasksResponse() { + } + + private TasksResponse(final List sections) { + this.sections = sections; + } + + public static TasksResponse from(final Tasks tasks) { + return new TasksResponse(TasksWithSectionResponse.from(tasks)); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/TasksWithSectionResponse.java b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/TasksWithSectionResponse.java new file mode 100644 index 00000000..405abe42 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/application/response/TasksWithSectionResponse.java @@ -0,0 +1,49 @@ +package com.woowacourse.gongcheck.core.application.response; + +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.task.Tasks; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.Getter; + +@Getter +public class TasksWithSectionResponse { + + private Long id; + private String name; + private String imageUrl; + private String description; + private List tasks; + + private TasksWithSectionResponse() { + } + + private TasksWithSectionResponse(final Long id, final String name, final String imageUrl, final String description, + final List tasks) { + this.id = id; + this.name = name; + this.imageUrl = imageUrl; + this.description = description; + this.tasks = tasks; + } + + public static List from(final Tasks tasks) { + Map> map = tasks.getTasks() + .stream() + .collect(Collectors.groupingBy(Task::getSection)); + + return map.entrySet() + .stream() + .map(entry -> TasksWithSectionResponse.of(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private static TasksWithSectionResponse of(final Section section, final List tasks) { + return new TasksWithSectionResponse(section.getId(), section.getName().getValue(), section.getImageUrl(), + section.getDescription().getValue(), tasks.stream() + .map(TaskResponse::from) + .collect(Collectors.toList())); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/host/Host.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/Host.java similarity index 79% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/host/Host.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/Host.java index c1d9287d..9678207c 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/host/Host.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/Host.java @@ -1,20 +1,26 @@ -package com.woowacourse.gongcheck.domain.host; +package com.woowacourse.gongcheck.core.domain.host; +import com.woowacourse.gongcheck.exception.BusinessException; import com.woowacourse.gongcheck.exception.UnauthorizedException; import java.time.LocalDateTime; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import lombok.Builder; import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Table(name = "host") +@EntityListeners(AuditingEntityListener.class) @Builder @Getter public class Host { @@ -35,9 +41,11 @@ public class Host { @Column(name = "image_url", nullable = false) private String imageUrl; + @CreatedDate @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; + @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; @@ -56,7 +64,7 @@ public Host(final Long id, final SpacePassword spacePassword, final Long githubI public void checkPassword(final SpacePassword spacePassword) { if (!this.spacePassword.equals(spacePassword)) { - throw new UnauthorizedException("공간 비밀번호와 입력하신 비밀번호가 일치하지 않습니다."); + throw new BusinessException("공간 비밀번호와 입력하신 비밀번호가 일치하지 않습니다."); } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/host/HostRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/HostRepository.java similarity index 93% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/host/HostRepository.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/HostRepository.java index da2d91be..d43b853e 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/host/HostRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/HostRepository.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.domain.host; +package com.woowacourse.gongcheck.core.domain.host; import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.Optional; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/host/SpacePassword.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/SpacePassword.java similarity index 95% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/host/SpacePassword.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/SpacePassword.java index fd272b81..132ddb7b 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/host/SpacePassword.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/host/SpacePassword.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.domain.host; +package com.woowacourse.gongcheck.core.domain.host; import com.woowacourse.gongcheck.exception.BusinessException; import java.util.Objects; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFile.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFile.java new file mode 100644 index 00000000..43e7d3b6 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFile.java @@ -0,0 +1,85 @@ +package com.woowacourse.gongcheck.core.domain.image.imageFile; + +import static org.springframework.util.StringUtils.getFilenameExtension; + +import com.woowacourse.gongcheck.exception.BusinessException; +import java.io.IOException; +import java.util.Objects; +import java.util.UUID; +import java.util.regex.Pattern; +import lombok.Getter; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.web.multipart.MultipartFile; + +@Getter +public class ImageFile { + + private static final Pattern IMAGE_FILE_EXTENSION_PATTERN = Pattern.compile("^(png|jpeg|jpg|svg|gif)$"); + + private final String originFileName; + private final String contentType; + private final String extension; + private final byte[] imageBytes; + + public ImageFile(final String originFileName, final String contentType, final String extension, + final byte[] imageBytes) { + this.originFileName = originFileName; + this.contentType = contentType; + this.extension = extension; + this.imageBytes = imageBytes; + } + + + public static ImageFile from(final MultipartFile multipartFile) { + validateNullFile(multipartFile); + validateEmptyFile(multipartFile); + validateNullFileName(multipartFile); + validateImageExtension(multipartFile); + + try { + return new ImageFile(multipartFile.getOriginalFilename(), multipartFile.getContentType(), + getFilenameExtension(multipartFile.getOriginalFilename()), multipartFile.getBytes()); + } catch (IOException exception) { + throw new BusinessException("잘못된 파일입니다."); + } + } + + private static void validateNullFile(final MultipartFile multipartFile) { + if (Objects.isNull(multipartFile)) { + throw new BusinessException("이미지 파일은 null이 들어올 수 없습니다."); + } + } + + private static void validateEmptyFile(final MultipartFile multipartFile) { + if (multipartFile.getSize() == 0) { + throw new BusinessException("이미지 파일은 빈값이 들어올 수 없습니다."); + } + } + + private static void validateNullFileName(final MultipartFile multipartFile) { + if (Objects.requireNonNull(multipartFile.getOriginalFilename()).isEmpty()) { + throw new BusinessException("이미지 파일 이름은 빈값이 들어올 수 없습니다."); + } + } + + private static void validateImageExtension(final MultipartFile multipartFile) { + String fileExtension = getFilenameExtension(multipartFile.getOriginalFilename()); + assert fileExtension != null; + if (!IMAGE_FILE_EXTENSION_PATTERN.matcher(fileExtension).matches()) { + throw new BusinessException("이미지 파일 확장자만 들어올 수 있습니다."); + } + } + + public ByteArrayResource inputStream() { + return new ByteArrayResource(imageBytes) { + @Override + public String getFilename() { + return randomName(); + } + }; + } + + private String randomName() { + return UUID.randomUUID().toString() + "." + extension; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/job/Job.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/Job.java similarity index 66% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/job/Job.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/Job.java index e9de0ec9..7269a98f 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/job/Job.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/Job.java @@ -1,11 +1,14 @@ -package com.woowacourse.gongcheck.domain.job; +package com.woowacourse.gongcheck.core.domain.job; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.submission.Submission; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.submission.Submission; +import com.woowacourse.gongcheck.core.domain.vo.Name; import java.time.LocalDateTime; import java.util.Objects; import javax.persistence.Column; +import javax.persistence.Embedded; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -15,14 +18,17 @@ import javax.persistence.Table; import lombok.Builder; import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Table(name = "job") +@EntityListeners(AuditingEntityListener.class) +@Builder @Getter public class Job { - private static final int NAME_MAX_LENGTH = 20; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -31,23 +37,24 @@ public class Job { @JoinColumn(name = "space_id", nullable = false) private Space space; - @Column(name = "name", length = NAME_MAX_LENGTH, nullable = false) - private String name; + @Embedded + private Name name; @Column(name = "slack_url") private String slackUrl; + @CreatedDate @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; + @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; protected Job() { } - @Builder - public Job(final Long id, final Space space, final String name, final String slackUrl, + public Job(final Long id, final Space space, final Name name, final String slackUrl, final LocalDateTime createdAt, final LocalDateTime updatedAt) { this.id = id; this.space = space; @@ -61,10 +68,17 @@ public Submission createSubmission(final String author) { return Submission.builder() .job(this) .author(author) - .createdAt(LocalDateTime.now()) .build(); } + public void changeSlackUrl(final String slackUrl) { + this.slackUrl = slackUrl; + } + + public boolean hasUrl() { + return Objects.nonNull(slackUrl); + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/job/JobRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/JobRepository.java similarity index 58% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/job/JobRepository.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/JobRepository.java index 4a9d1db3..70586d50 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/job/JobRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/job/JobRepository.java @@ -1,17 +1,15 @@ -package com.woowacourse.gongcheck.domain.job; +package com.woowacourse.gongcheck.core.domain.job; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.space.Space; import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.List; import java.util.Optional; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; public interface JobRepository extends JpaRepository { - Slice findBySpaceHostAndSpace(final Host host, final Space space, final Pageable pageable); + List findAllBySpaceHostAndSpace(final Host host, final Space space); Optional findBySpaceHostAndId(final Host host, final Long id); @@ -21,4 +19,9 @@ default Job getBySpaceHostAndId(final Host host, final Long id) throws NotFoundE return findBySpaceHostAndId(host, id) .orElseThrow(() -> new NotFoundException("존재하지 않는 작업입니다.")); } + + default Job getById(final Long id) throws NotFoundException { + return findById(id) + .orElseThrow(() -> new NotFoundException("존재하지 않는 작업입니다.")); + } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/section/Section.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/section/Section.java similarity index 58% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/section/Section.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/section/Section.java index 1db595b0..3d5aeec2 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/section/Section.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/section/Section.java @@ -1,10 +1,14 @@ -package com.woowacourse.gongcheck.domain.section; +package com.woowacourse.gongcheck.core.domain.section; -import com.woowacourse.gongcheck.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.vo.Description; +import com.woowacourse.gongcheck.core.domain.vo.Name; import java.time.LocalDateTime; import java.util.Objects; import javax.persistence.Column; +import javax.persistence.Embedded; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -14,14 +18,17 @@ import javax.persistence.Table; import lombok.Builder; import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Table(name = "section") +@EntityListeners(AuditingEntityListener.class) +@Builder @Getter public class Section { - private static final int NAME_MAX_LENGTH = 20; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -30,24 +37,33 @@ public class Section { @JoinColumn(name = "job_id", nullable = false) private Job job; - @Column(name = "name", length = NAME_MAX_LENGTH, nullable = false) - private String name; + @Embedded + private Name name; + + @Embedded + private Description description; + + @Column(name = "image_url") + private String imageUrl; + @CreatedDate @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; + @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; protected Section() { } - @Builder - public Section(final Long id, final Job job, final String name, final LocalDateTime createdAt, - final LocalDateTime updatedAt) { + public Section(final Long id, final Job job, final Name name, final Description description, final String imageUrl, + final LocalDateTime createdAt, final LocalDateTime updatedAt) { this.id = id; this.job = job; this.name = name; + this.description = description; + this.imageUrl = imageUrl; this.createdAt = createdAt; this.updatedAt = updatedAt; } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/section/SectionRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/section/SectionRepository.java similarity index 58% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/section/SectionRepository.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/section/SectionRepository.java index ac0150e6..9ad81d08 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/section/SectionRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/section/SectionRepository.java @@ -1,9 +1,11 @@ -package com.woowacourse.gongcheck.domain.section; +package com.woowacourse.gongcheck.core.domain.section; -import com.woowacourse.gongcheck.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.Job; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; public interface SectionRepository extends JpaRepository { List
findAllByJobIn(final List jobs); + + List
findAllByJob(final Job job); } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/space/Space.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/space/Space.java similarity index 62% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/space/Space.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/space/Space.java index 5dfa34b4..7d6973bd 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/space/Space.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/space/Space.java @@ -1,10 +1,13 @@ -package com.woowacourse.gongcheck.domain.space; +package com.woowacourse.gongcheck.core.domain.space; -import com.woowacourse.gongcheck.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.vo.Name; import java.time.LocalDateTime; import java.util.Objects; import javax.persistence.Column; +import javax.persistence.Embedded; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -14,14 +17,17 @@ import javax.persistence.Table; import lombok.Builder; import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Table(name = "space") +@EntityListeners(AuditingEntityListener.class) +@Builder @Getter public class Space { - private static final int NAME_MAX_LENGTH = 20; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -30,25 +36,25 @@ public class Space { @JoinColumn(name = "host_id", nullable = false) private Host host; - @Column(name = "name", length = NAME_MAX_LENGTH, nullable = false) - private String name; + @Embedded + private Name name; @Column(name = "img_url") private String imageUrl; + @CreatedDate @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; + @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; protected Space() { } - @Builder - public Space(final Long id, final Host host, final String name, final String imageUrl, - final LocalDateTime createdAt, final LocalDateTime updatedAt) { - checkNameLength(name); + public Space(final Long id, final Host host, final Name name, final String imageUrl, final LocalDateTime createdAt, + final LocalDateTime updatedAt) { this.id = id; this.host = host; this.name = name; @@ -57,14 +63,12 @@ public Space(final Long id, final Host host, final String name, final String ima this.updatedAt = updatedAt; } - private void checkNameLength(final String name) { - if (name.isBlank()) { - throw new IllegalArgumentException("공간의 이름은 공백일 수 없습니다."); - } + public void changeName(final Name name) { + this.name = name; + } - if (name.length() > NAME_MAX_LENGTH) { - throw new IllegalArgumentException("공간의 이름은 " + NAME_MAX_LENGTH + "자 이하여야합니다."); - } + public void changeImageUrl(final String imageUrl) { + this.imageUrl = imageUrl; } @Override diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/space/SpaceRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepository.java similarity index 57% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/space/SpaceRepository.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepository.java index a9dcc192..19bbc9c9 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/space/SpaceRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepository.java @@ -1,19 +1,21 @@ -package com.woowacourse.gongcheck.domain.space; +package com.woowacourse.gongcheck.core.domain.space; -import com.woowacourse.gongcheck.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.vo.Name; import com.woowacourse.gongcheck.exception.NotFoundException; +import java.util.List; import java.util.Optional; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; public interface SpaceRepository extends JpaRepository { - Slice findByHost(final Host host, final Pageable pageable); + List findAllByHost(final Host host); Optional findByHostAndId(final Host host, final Long id); - boolean existsByHostAndName(final Host host, final String name); + boolean existsByHostAndName(final Host host, final Name name); + + boolean existsByHostAndNameAndIdNot(final Host host, final Name name, final Long id); default Space getByHostAndId(final Host host, final Long id) throws NotFoundException { return findByHostAndId(host, id) diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/submission/Submission.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/submission/Submission.java similarity index 63% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/submission/Submission.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/submission/Submission.java index 0c387e8d..1ff6f870 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/submission/Submission.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/submission/Submission.java @@ -1,10 +1,12 @@ -package com.woowacourse.gongcheck.domain.submission; +package com.woowacourse.gongcheck.core.domain.submission; -import com.woowacourse.gongcheck.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.exception.BusinessException; import java.time.LocalDateTime; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -14,13 +16,17 @@ import javax.persistence.Table; import lombok.Builder; import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Table(name = "submission") +@EntityListeners(AuditingEntityListener.class) +@Builder @Getter public class Submission { - private static final int AUTHOR_MAX_LENGTH = 20; + private static final int AUTHOR_MAX_LENGTH = 10; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -33,20 +39,31 @@ public class Submission { @Column(name = "author", length = AUTHOR_MAX_LENGTH, nullable = false) private String author; + @CreatedDate @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; protected Submission() { } - @Builder public Submission(final Long id, final Job job, final String author, final LocalDateTime createdAt) { + validateAuthorLength(author); this.id = id; this.job = job; this.author = author; this.createdAt = createdAt; } + private void validateAuthorLength(final String author) { + if (author.isBlank()) { + throw new BusinessException("제출자 이름은 공백일 수 없습니다."); + } + + if (author.length() > AUTHOR_MAX_LENGTH) { + throw new BusinessException("제출자 이름은 " + AUTHOR_MAX_LENGTH + "자 이하여야 합니다."); + } + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionRepository.java new file mode 100644 index 00000000..a18e8869 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionRepository.java @@ -0,0 +1,15 @@ +package com.woowacourse.gongcheck.core.domain.submission; + +import com.woowacourse.gongcheck.core.domain.job.Job; +import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; + +public interface SubmissionRepository extends JpaRepository { + + @EntityGraph(attributePaths = "job") + Slice findAllByJobIn(@Param("jobs") final List jobs, final Pageable pageable); +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/RunningTask.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTask.java similarity index 84% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/task/RunningTask.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTask.java index b8af2f41..546acb77 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/RunningTask.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTask.java @@ -1,9 +1,10 @@ -package com.woowacourse.gongcheck.domain.task; +package com.woowacourse.gongcheck.core.domain.task; import java.time.LocalDateTime; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; @@ -13,9 +14,13 @@ import lombok.Builder; import lombok.Getter; import org.hibernate.annotations.ColumnDefault; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Table(name = "running_task") +@EntityListeners(AuditingEntityListener.class) +@Builder @Getter public class RunningTask { @@ -32,13 +37,13 @@ public class RunningTask { @ColumnDefault("false") private boolean isChecked; + @CreatedDate @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; protected RunningTask() { } - @Builder public RunningTask(final Long taskId, final Task task, final boolean isChecked, final LocalDateTime createdAt) { this.taskId = taskId; this.task = task; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/RunningTaskRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskRepository.java similarity index 88% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/task/RunningTaskRepository.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskRepository.java index 598fe793..2c1cb14e 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/RunningTaskRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskRepository.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.domain.task; +package com.woowacourse.gongcheck.core.domain.task; import java.util.List; import java.util.Optional; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/RunningTasks.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTasks.java similarity index 92% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/task/RunningTasks.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTasks.java index 01434052..2ae44958 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/RunningTasks.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/RunningTasks.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.domain.task; +package com.woowacourse.gongcheck.core.domain.task; import com.woowacourse.gongcheck.exception.BusinessException; import java.util.List; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/Task.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/Task.java similarity index 64% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/task/Task.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/Task.java index d5d89418..3b524376 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/Task.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/Task.java @@ -1,10 +1,14 @@ -package com.woowacourse.gongcheck.domain.task; +package com.woowacourse.gongcheck.core.domain.task; -import com.woowacourse.gongcheck.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.vo.Description; +import com.woowacourse.gongcheck.core.domain.vo.Name; import java.time.LocalDateTime; import java.util.Objects; import javax.persistence.Column; +import javax.persistence.Embedded; import javax.persistence.Entity; +import javax.persistence.EntityListeners; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; @@ -15,14 +19,17 @@ import javax.persistence.Table; import lombok.Builder; import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Entity @Table(name = "task") +@EntityListeners(AuditingEntityListener.class) +@Builder @Getter public class Task { - private static final int NAME_MAX_LENGTH = 50; - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @@ -34,25 +41,35 @@ public class Task { @OneToOne(mappedBy = "task", fetch = FetchType.LAZY) private RunningTask runningTask; - @Column(name = "name", length = NAME_MAX_LENGTH, nullable = false) - private String name; + @Embedded + private Name name; + + @Embedded + private Description description; + + @Column(name = "image_url") + private String imageUrl; + @CreatedDate @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; + @LastModifiedDate @Column(name = "updated_at") private LocalDateTime updatedAt; protected Task() { } - @Builder - public Task(final Long id, final Section section, final RunningTask runningTask, final String name, - final LocalDateTime createdAt, final LocalDateTime updatedAt) { + public Task(final Long id, final Section section, final RunningTask runningTask, final Name name, + final Description description, final String imageUrl, final LocalDateTime createdAt, + final LocalDateTime updatedAt) { this.id = id; this.section = section; this.runningTask = runningTask; this.name = name; + this.description = description; + this.imageUrl = imageUrl; this.createdAt = createdAt; this.updatedAt = updatedAt; } @@ -61,7 +78,6 @@ public RunningTask createRunningTask() { return RunningTask.builder() .taskId(id) .isChecked(false) - .createdAt(LocalDateTime.now()) .build(); } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/TaskRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/TaskRepository.java similarity index 76% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/task/TaskRepository.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/TaskRepository.java index 610fa915..8d9dd488 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/TaskRepository.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/TaskRepository.java @@ -1,8 +1,8 @@ -package com.woowacourse.gongcheck.domain.task; +package com.woowacourse.gongcheck.core.domain.task; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.section.Section; import com.woowacourse.gongcheck.exception.NotFoundException; import java.util.List; import java.util.Optional; @@ -19,6 +19,8 @@ public interface TaskRepository extends JpaRepository { List findAllBySectionIn(final List
sections); + void deleteAllBySectionIn(final List
sections); + default Task getBySectionJobSpaceHostAndId(final Host host, final Long id) throws NotFoundException { return findBySectionJobSpaceHostAndId(host, id) .orElseThrow(() -> new NotFoundException("존재하지 않는 작업입니다.")); diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/Tasks.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/Tasks.java similarity index 88% rename from backend/src/main/java/com/woowacourse/gongcheck/domain/task/Tasks.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/Tasks.java index e63bbbdb..b3ce2886 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/task/Tasks.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/task/Tasks.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.domain.task; +package com.woowacourse.gongcheck.core.domain.task; import java.util.List; import java.util.Objects; @@ -26,6 +26,10 @@ public List getTaskIds() { .collect(Collectors.toList()); } + public boolean isEmpty() { + return tasks.isEmpty(); + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Description.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Description.java new file mode 100644 index 00000000..612f36e4 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Description.java @@ -0,0 +1,48 @@ +package com.woowacourse.gongcheck.core.domain.vo; + +import com.woowacourse.gongcheck.exception.BusinessException; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import lombok.Getter; + +@Embeddable +@Getter +public class Description { + + private static final int DESCRIPTION_MAX_LENGTH = 128; + + @Column(name = "description", length = DESCRIPTION_MAX_LENGTH) + private String value; + + protected Description() { + } + + public Description(final String value) { + validateLength(value); + this.value = value; + } + + private void validateLength(final String value) { + if (value.length() > DESCRIPTION_MAX_LENGTH) { + throw new BusinessException("설명은 " + DESCRIPTION_MAX_LENGTH + "자 이하여야 합니다."); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Description that = (Description) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Name.java b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Name.java new file mode 100644 index 00000000..e0c1c8ef --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/domain/vo/Name.java @@ -0,0 +1,52 @@ +package com.woowacourse.gongcheck.core.domain.vo; + +import com.woowacourse.gongcheck.exception.BusinessException; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import lombok.Getter; + +@Embeddable +@Getter +public class Name { + + private static final int NAME_MAX_LENGTH = 10; + + @Column(name = "name", length = NAME_MAX_LENGTH, nullable = false) + private String value; + + protected Name() { + } + + public Name(final String value) { + checkNameLength(value); + this.value = value; + } + + private void checkNameLength(final String name) { + if (name.isBlank()) { + throw new BusinessException("이름은 공백일 수 없습니다."); + } + + if (name.length() > NAME_MAX_LENGTH) { + throw new BusinessException("이름은 " + NAME_MAX_LENGTH + "자 이하여야합니다."); + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Name name = (Name) o; + return Objects.equals(value, name.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/HostController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/HostController.java similarity index 52% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/HostController.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/presentation/HostController.java index 97084de6..d92038a4 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/HostController.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/HostController.java @@ -1,9 +1,13 @@ -package com.woowacourse.gongcheck.presentation; +package com.woowacourse.gongcheck.core.presentation; -import com.woowacourse.gongcheck.application.HostService; -import com.woowacourse.gongcheck.presentation.request.SpacePasswordChangeRequest; +import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipal; +import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.core.application.HostService; +import com.woowacourse.gongcheck.core.application.response.EntranceCodeResponse; +import com.woowacourse.gongcheck.core.presentation.request.SpacePasswordChangeRequest; import javax.validation.Valid; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -20,9 +24,17 @@ public HostController(final HostService hostService) { } @PatchMapping("/spacePassword") + @HostOnly public ResponseEntity changeSpacePassword(@AuthenticationPrincipal final Long hostId, @Valid @RequestBody final SpacePasswordChangeRequest request) { hostService.changeSpacePassword(hostId, request); return ResponseEntity.noContent().build(); } + + @GetMapping("/hosts/entranceCode") + @HostOnly + public ResponseEntity showEntranceCode(@AuthenticationPrincipal final Long hostId) { + String entranceCode = hostService.createEntranceCode(hostId); + return ResponseEntity.ok(EntranceCodeResponse.from(entranceCode)); + } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/ImageUploadController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/ImageUploadController.java new file mode 100644 index 00000000..26995871 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/ImageUploadController.java @@ -0,0 +1,31 @@ +package com.woowacourse.gongcheck.core.presentation; + +import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.core.application.ImageUploader; +import com.woowacourse.gongcheck.core.application.response.ImageUrlResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/api") +@Slf4j +public class ImageUploadController { + + private final ImageUploader imageUploader; + + public ImageUploadController(final ImageUploader imageUploader) { + this.imageUploader = imageUploader; + } + + @PostMapping(value = "/imageUpload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @HostOnly + public ResponseEntity uploadImage(@RequestPart final MultipartFile image) { + return ResponseEntity.ok(imageUploader.upload(image, "")); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/JobController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/JobController.java new file mode 100644 index 00000000..29aa7c98 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/JobController.java @@ -0,0 +1,80 @@ +package com.woowacourse.gongcheck.core.presentation; + +import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipal; +import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.core.application.JobService; +import com.woowacourse.gongcheck.core.application.response.JobsResponse; +import com.woowacourse.gongcheck.core.application.response.SlackUrlResponse; +import com.woowacourse.gongcheck.core.presentation.request.JobCreateRequest; +import com.woowacourse.gongcheck.core.presentation.request.SlackUrlChangeRequest; +import java.net.URI; +import javax.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +public class JobController { + + private final JobService jobService; + + public JobController(final JobService jobService) { + this.jobService = jobService; + } + + @GetMapping("/spaces/{spaceId}/jobs") + public ResponseEntity showJobs(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long spaceId) { + JobsResponse response = jobService.findJobs(hostId, spaceId); + return ResponseEntity.ok(response); + } + + @PostMapping("/spaces/{spaceId}/jobs") + @HostOnly + public ResponseEntity createJob(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long spaceId, + @Valid @RequestBody final JobCreateRequest request) { + Long savedJobId = jobService.createJob(hostId, spaceId, request); + return ResponseEntity.created(URI.create("/api/spaces/" + savedJobId + "/jobs")).build(); + } + + @PutMapping("/jobs/{jobId}") + @HostOnly + public ResponseEntity updateJob(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long jobId, + @Valid @RequestBody final JobCreateRequest request) { + jobService.updateJob(hostId, jobId, request); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/jobs/{jobId}") + @HostOnly + public ResponseEntity removeJob(@AuthenticationPrincipal final Long hostId, @PathVariable final Long jobId) { + jobService.removeJob(hostId, jobId); + return ResponseEntity.noContent().build(); + } + + @GetMapping("/jobs/{jobId}/slack") + @HostOnly + public ResponseEntity findSlackUrl(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long jobId) { + SlackUrlResponse response = jobService.findSlackUrl(hostId, jobId); + return ResponseEntity.ok(response); + } + + @PutMapping("/jobs/{jobId}/slack") + @HostOnly + public ResponseEntity changeSlackUrl(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long jobId, + @Valid @RequestBody final SlackUrlChangeRequest request) { + jobService.changeSlackUrl(hostId, jobId, request); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SpaceController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SpaceController.java new file mode 100644 index 00000000..6256d392 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SpaceController.java @@ -0,0 +1,72 @@ +package com.woowacourse.gongcheck.core.presentation; + +import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipal; +import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.core.application.SpaceService; +import com.woowacourse.gongcheck.core.application.response.SpaceResponse; +import com.woowacourse.gongcheck.core.application.response.SpacesResponse; +import com.woowacourse.gongcheck.core.presentation.request.SpaceChangeRequest; +import com.woowacourse.gongcheck.core.presentation.request.SpaceCreateRequest; +import java.net.URI; +import javax.validation.Valid; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/api") +public class SpaceController { + + private final SpaceService spaceService; + + public SpaceController(final SpaceService spaceService) { + this.spaceService = spaceService; + } + + @GetMapping("/spaces") + public ResponseEntity showSpaces(@AuthenticationPrincipal final Long hostId) { + SpacesResponse response = spaceService.findSpaces(hostId); + return ResponseEntity.ok(response); + } + + @PostMapping("/spaces") + @HostOnly + public ResponseEntity createSpace(@AuthenticationPrincipal final Long hostId, + @Valid @RequestBody final SpaceCreateRequest request) { + Long spaceId = spaceService.createSpace(hostId, request); + return ResponseEntity.created(URI.create("/api/spaces/" + spaceId)).build(); + } + + @GetMapping("/spaces/{spaceId}") + public ResponseEntity showSpace(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long spaceId) { + SpaceResponse response = spaceService.findSpace(hostId, spaceId); + return ResponseEntity.ok(response); + } + + @PutMapping("/spaces/{spaceId}") + public ResponseEntity changeSpace(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long spaceId, + @Valid @RequestBody final SpaceChangeRequest request) { + spaceService.changeSpace(hostId, spaceId, request); + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/spaces/{spaceId}") + @HostOnly + public ResponseEntity removeSpace(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long spaceId) { + spaceService.removeSpace(hostId, spaceId); + return ResponseEntity.noContent().build(); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SubmissionController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SubmissionController.java new file mode 100644 index 00000000..91ea12d6 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/SubmissionController.java @@ -0,0 +1,44 @@ +package com.woowacourse.gongcheck.core.presentation; + +import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipal; +import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.core.application.SubmissionService; +import com.woowacourse.gongcheck.core.application.response.SubmissionsResponse; +import com.woowacourse.gongcheck.core.presentation.request.SubmissionRequest; +import javax.validation.Valid; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +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 +@RequestMapping("/api") +public class SubmissionController { + + private final SubmissionService submissionService; + + public SubmissionController(final SubmissionService submissionService) { + this.submissionService = submissionService; + } + + @PostMapping("/jobs/{jobId}/complete") + public ResponseEntity submitJobCompletion(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long jobId, + @Valid @RequestBody final SubmissionRequest request) { + submissionService.submitJobCompletion(hostId, jobId, request); + return ResponseEntity.ok().build(); + } + + @GetMapping("/spaces/{spaceId}/submissions") + @HostOnly + public ResponseEntity showSubmissions(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long spaceId, + final Pageable pageable) { + SubmissionsResponse response = submissionService.findPage(hostId, spaceId, pageable); + return ResponseEntity.ok(response); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/TaskController.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/TaskController.java similarity index 68% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/TaskController.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/presentation/TaskController.java index 111d5a13..a0b13ed2 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/TaskController.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/TaskController.java @@ -1,8 +1,11 @@ -package com.woowacourse.gongcheck.presentation; - -import com.woowacourse.gongcheck.application.TaskService; -import com.woowacourse.gongcheck.application.response.JobActiveResponse; -import com.woowacourse.gongcheck.application.response.RunningTasksResponse; +package com.woowacourse.gongcheck.core.presentation; + +import com.woowacourse.gongcheck.auth.presentation.AuthenticationPrincipal; +import com.woowacourse.gongcheck.auth.presentation.aop.HostOnly; +import com.woowacourse.gongcheck.core.application.TaskService; +import com.woowacourse.gongcheck.core.application.response.JobActiveResponse; +import com.woowacourse.gongcheck.core.application.response.RunningTasksResponse; +import com.woowacourse.gongcheck.core.application.response.TasksResponse; import java.net.URI; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -21,7 +24,7 @@ public TaskController(final TaskService taskService) { this.taskService = taskService; } - @PostMapping("/jobs/{jobId}/tasks/new") + @PostMapping("/jobs/{jobId}/runningTasks/new") public ResponseEntity createNewTasks(@AuthenticationPrincipal final Long hostId, @PathVariable final Long jobId) { taskService.createNewRunningTasks(hostId, jobId); @@ -35,7 +38,7 @@ public ResponseEntity isJobActive(@AuthenticationPrincipal fi return ResponseEntity.ok(response); } - @GetMapping("/jobs/{jobId}/tasks") + @GetMapping("/jobs/{jobId}/runningTasks") public ResponseEntity showRunningTasks(@AuthenticationPrincipal final Long hostId, @PathVariable final Long jobId) { RunningTasksResponse response = taskService.findRunningTasks(hostId, jobId); @@ -48,4 +51,12 @@ public ResponseEntity flipRunningTask(@AuthenticationPrincipal final Long taskService.flipRunningTask(hostId, taskId); return ResponseEntity.ok().build(); } + + @GetMapping("/jobs/{jobId}/tasks") + @HostOnly + public ResponseEntity showTasks(@AuthenticationPrincipal final Long hostId, + @PathVariable final Long jobId) { + TasksResponse response = taskService.findTasks(hostId, jobId); + return ResponseEntity.ok(response); + } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/JobCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/JobCreateRequest.java similarity index 61% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/request/JobCreateRequest.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/JobCreateRequest.java index 9b019747..ca65589f 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/JobCreateRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/JobCreateRequest.java @@ -1,16 +1,14 @@ -package com.woowacourse.gongcheck.presentation.request; +package com.woowacourse.gongcheck.core.presentation.request; import java.util.List; import javax.validation.Valid; import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; import lombok.Getter; @Getter public class JobCreateRequest { - @Size(min = 1, max = 20, message = "작업 이름은 한글자 이상 20자 이하여야 합니다.") - @NotNull(message = "작업의 이름은 null 일 수 없습니다.") + @NotNull(message = "이름은 null일 수 없습니다.") private String name; @Valid diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SectionCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SectionCreateRequest.java new file mode 100644 index 00000000..43f3506a --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SectionCreateRequest.java @@ -0,0 +1,31 @@ +package com.woowacourse.gongcheck.core.presentation.request; + +import java.util.List; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class SectionCreateRequest { + + @NotNull(message = "이름은 null일 수 없습니다.") + private String name; + + private String description; + + private String imageUrl; + + @Valid + private List tasks; + + private SectionCreateRequest() { + } + + public SectionCreateRequest(final String name, final String description, final String imageUrl, + final List tasks) { + this.name = name; + this.description = description; + this.imageUrl = imageUrl; + this.tasks = tasks; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SlackUrlChangeRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SlackUrlChangeRequest.java new file mode 100644 index 00000000..6a35144d --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SlackUrlChangeRequest.java @@ -0,0 +1,18 @@ +package com.woowacourse.gongcheck.core.presentation.request; + +import javax.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class SlackUrlChangeRequest { + + @NotNull(message = "Slack URL은 null일 수 없습니다.") + private String slackUrl; + + private SlackUrlChangeRequest() { + } + + public SlackUrlChangeRequest(final String slackUrl) { + this.slackUrl = slackUrl; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceChangeRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceChangeRequest.java new file mode 100644 index 00000000..14fa336c --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceChangeRequest.java @@ -0,0 +1,21 @@ +package com.woowacourse.gongcheck.core.presentation.request; + +import javax.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class SpaceChangeRequest { + + @NotNull(message = "이름은 null일 수 없습니다.") + private String name; + + private String imageUrl; + + private SpaceChangeRequest() { + } + + public SpaceChangeRequest(final String name, final String imageUrl) { + this.name = name; + this.imageUrl = imageUrl; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceCreateRequest.java new file mode 100644 index 00000000..4595b361 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpaceCreateRequest.java @@ -0,0 +1,21 @@ +package com.woowacourse.gongcheck.core.presentation.request; + +import javax.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class SpaceCreateRequest { + + @NotNull(message = "이름은 null일 수 없습니다.") + private String name; + + private String imageUrl; + + private SpaceCreateRequest() { + } + + public SpaceCreateRequest(final String name, final String imageUrl) { + this.name = name; + this.imageUrl = imageUrl; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SpacePasswordChangeRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpacePasswordChangeRequest.java similarity index 70% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SpacePasswordChangeRequest.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpacePasswordChangeRequest.java index eb30dc13..5798f035 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SpacePasswordChangeRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SpacePasswordChangeRequest.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.presentation.request; +package com.woowacourse.gongcheck.core.presentation.request; import javax.validation.constraints.NotNull; import lombok.Getter; @@ -6,7 +6,7 @@ @Getter public class SpacePasswordChangeRequest { - @NotNull + @NotNull(message = "비밀번호는 null일 수 없습니다.") private String password; private SpacePasswordChangeRequest() { diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SubmissionRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SubmissionRequest.java similarity index 63% rename from backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SubmissionRequest.java rename to backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SubmissionRequest.java index 50c5c7f0..3656763e 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SubmissionRequest.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/SubmissionRequest.java @@ -1,13 +1,11 @@ -package com.woowacourse.gongcheck.presentation.request; +package com.woowacourse.gongcheck.core.presentation.request; import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; import lombok.Getter; @Getter public class SubmissionRequest { - @Size(min = 1, max = 20, message = "제출자 이름의 길이가 올바르지 않습니다.") @NotNull(message = "제출자 이름은 null 일 수 없습니다.") private String author; diff --git a/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/TaskCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/TaskCreateRequest.java new file mode 100644 index 00000000..bc112e42 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/core/presentation/request/TaskCreateRequest.java @@ -0,0 +1,24 @@ +package com.woowacourse.gongcheck.core.presentation.request; + +import javax.validation.constraints.NotNull; +import lombok.Getter; + +@Getter +public class TaskCreateRequest { + + @NotNull(message = "이름은 null일 수 없습니다.") + private String name; + + private String description; + + private String imageUrl; + + private TaskCreateRequest() { + } + + public TaskCreateRequest(final String name, final String description, final String imageUrl) { + this.name = name; + this.description = description; + this.imageUrl = imageUrl; + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/domain/submission/SubmissionRepository.java b/backend/src/main/java/com/woowacourse/gongcheck/domain/submission/SubmissionRepository.java deleted file mode 100644 index 8c2d391b..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/domain/submission/SubmissionRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.woowacourse.gongcheck.domain.submission; - -import org.springframework.data.jpa.repository.JpaRepository; - -public interface SubmissionRepository extends JpaRepository { -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java index c5af9f1f..0f21be8b 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/ControllerAdvice.java @@ -1,5 +1,8 @@ package com.woowacourse.gongcheck.exception; +import java.io.PrintWriter; +import java.io.StringWriter; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -7,30 +10,49 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice +@Slf4j public class ControllerAdvice { @ExceptionHandler(UnauthorizedException.class) public ResponseEntity handleUnauthorized(final RuntimeException e) { + log.info(e.getMessage()); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.from(e)); } @ExceptionHandler(NotFoundException.class) public ResponseEntity handleNotFound(final RuntimeException e) { + log.warn(e.getMessage()); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(e)); } @ExceptionHandler(BusinessException.class) public ResponseEntity handleBusiness(final RuntimeException e) { + log.info(e.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.from(e)); } @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleMethodArgumentNotValid(final MethodArgumentNotValidException e) { + log.warn(e.getMessage()); return ResponseEntity.badRequest().body(ErrorResponse.from(e)); } - @ExceptionHandler(RuntimeException.class) - public ResponseEntity handleInternalServerError(RuntimeException e) { + @ExceptionHandler(InfrastructureException.class) + public ResponseEntity handleInfrastructureException(final InfrastructureException e) { return ResponseEntity.internalServerError().body(ErrorResponse.from(e)); } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleInternalServerError(final Exception e) { + log.error("Stack Trace : {}", extractStackTrace(e)); + return ResponseEntity.internalServerError().body(ErrorResponse.from("서버 에러가 발생했습니다.")); + } + + private String extractStackTrace(final Exception e) { + StringWriter stringWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(stringWriter); + + e.printStackTrace(printWriter); + return stringWriter.toString(); + } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/exception/InfrastructureException.java b/backend/src/main/java/com/woowacourse/gongcheck/exception/InfrastructureException.java new file mode 100644 index 00000000..db7b7a90 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/exception/InfrastructureException.java @@ -0,0 +1,8 @@ +package com.woowacourse.gongcheck.exception; + +public class InfrastructureException extends RuntimeException { + + public InfrastructureException(final String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/alert/SlackService.java b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/alert/SlackService.java new file mode 100644 index 00000000..a8a28eb8 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/alert/SlackService.java @@ -0,0 +1,30 @@ +package com.woowacourse.gongcheck.infrastructure.alert; + +import com.slack.api.Slack; +import com.slack.api.webhook.Payload; +import com.woowacourse.gongcheck.core.application.AlertService; +import com.woowacourse.gongcheck.core.application.response.Attachments; +import com.woowacourse.gongcheck.core.application.response.SubmissionCreatedResponse; +import com.woowacourse.gongcheck.exception.InfrastructureException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class SlackService implements AlertService { + + @Async + @Override + public void sendMessage(final SubmissionCreatedResponse submissionCreatedResponse) { + try (Slack slack = Slack.getInstance()) { + Payload payload = Payload.builder() + .attachments(Attachments.of(submissionCreatedResponse).getAttachments()) + .build(); + slack.send(submissionCreatedResponse.getSlackUrl(), payload); + } catch (Exception e) { + log.error(e.getMessage()); + throw new InfrastructureException("메시지 전송에 실패했습니다."); + } + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/hash/AES256.java b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/hash/AES256.java new file mode 100644 index 00000000..06ef8587 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/hash/AES256.java @@ -0,0 +1,72 @@ +package com.woowacourse.gongcheck.infrastructure.hash; + +import com.woowacourse.gongcheck.auth.application.HashTranslator; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import lombok.extern.slf4j.Slf4j; +import org.apache.tomcat.util.codec.binary.Base64; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class AES256 implements HashTranslator { + + private static final int SECRET_KEY_SIZE = 32; + private static final int INITIALIZATION_VECTOR_SIZE = 16; + private static final String ALGORITHM = "AES"; + private static final String CIPHER_MODE = "CBC"; + private static final String PADDING = "PKCS5Padding"; + private static final String TRANSFORMATION = String.format("%s/%s/%s", ALGORITHM, CIPHER_MODE, PADDING); + + private final SecretKeySpec secretKeySpec; + private final IvParameterSpec ivParamSpec; + + public AES256(@Value("${security.hash.secret-key}") final String secretKey) { + if (secretKey.length() < SECRET_KEY_SIZE) { + throw new IllegalArgumentException( + "Minimum key size is " + SECRET_KEY_SIZE + ", current key size :" + secretKey.length()); + } + secretKeySpec = new SecretKeySpec(secretKey.substring(0, SECRET_KEY_SIZE).getBytes(), ALGORITHM); + ivParamSpec = new IvParameterSpec(secretKey.substring(0, INITIALIZATION_VECTOR_SIZE).getBytes()); + } + + @Override + public String encode(String input) { + try { + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParamSpec); + byte[] encrypted = cipher.doFinal(input.getBytes(StandardCharsets.UTF_8)); + return Base64.encodeBase64URLSafeString(encrypted); + } catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | + NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | NullPointerException e) { + // TODO: 2022/08/02 Infrastructure 예외로 변경 필요 + log.error("encoding error, input = {}, message = {}", input, e.getMessage()); + throw new IllegalArgumentException(e); + } + } + + @Override + public String decode(String input) { + try { + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParamSpec); + byte[] decodedBytes = Base64.decodeBase64URLSafe(input); + byte[] decrypted = cipher.doFinal(decodedBytes); + return new String(decrypted, StandardCharsets.UTF_8); + } catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException | + NoSuchAlgorithmException | BadPaddingException | InvalidKeyException | NullPointerException e) { + // TODO: 2022/08/02 Infrastructure 예외로 변경 필요 + log.error("decoding error, input = {}, message = {}", input, e.getMessage()); + throw new IllegalArgumentException(e); + } + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/imageuploader/OwnServerImageUploader.java b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/imageuploader/OwnServerImageUploader.java new file mode 100644 index 00000000..4ddc2e57 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/imageuploader/OwnServerImageUploader.java @@ -0,0 +1,38 @@ +package com.woowacourse.gongcheck.infrastructure.imageuploader; + +import com.woowacourse.gongcheck.core.application.ImageUploader; +import com.woowacourse.gongcheck.core.application.response.ImageUrlResponse; +import com.woowacourse.gongcheck.core.domain.image.imageFile.ImageFile; +import org.springframework.http.MediaType; +import org.springframework.http.client.MultipartBodyBuilder; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +public class OwnServerImageUploader implements ImageUploader { + + private final WebClient webClient; + + public OwnServerImageUploader(final WebClient webClient) { + this.webClient = webClient; + } + + @Override + public ImageUrlResponse upload(final MultipartFile image, final String directoryName) { + ImageFile imageFile = ImageFile.from(image); + return ImageUrlResponse.from(postToImageServer(imageFile)); + } + + private String postToImageServer(final ImageFile imageFile) { + MultipartBodyBuilder builder = new MultipartBodyBuilder(); + builder.part("file", imageFile.inputStream()); + + return webClient.post() + .uri("/api/image-upload") + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(BodyInserters.fromMultipartData(builder.build())) + .retrieve() + .bodyToMono(String.class) + .block(); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/imageuploader/SimpleImageUploader.java b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/imageuploader/SimpleImageUploader.java new file mode 100644 index 00000000..2f1a9a01 --- /dev/null +++ b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/imageuploader/SimpleImageUploader.java @@ -0,0 +1,12 @@ +package com.woowacourse.gongcheck.infrastructure.imageuploader; + +import com.woowacourse.gongcheck.core.application.ImageUploader; +import com.woowacourse.gongcheck.core.application.response.ImageUrlResponse; +import org.springframework.web.multipart.MultipartFile; + +public class SimpleImageUploader implements ImageUploader { + @Override + public ImageUrlResponse upload(MultipartFile file, String directoryName) { + return ImageUrlResponse.from("https://user-images.githubusercontent.com/48307960/178979416-449c8a6e-5c8b-4d14-91e6-c19718024206.png"); + } +} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/JjwtTokenProvider.java b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProvider.java similarity index 87% rename from backend/src/main/java/com/woowacourse/gongcheck/application/JjwtTokenProvider.java rename to backend/src/main/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProvider.java index 06d3e558..05a2efa0 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/JjwtTokenProvider.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProvider.java @@ -1,7 +1,9 @@ -package com.woowacourse.gongcheck.application; +package com.woowacourse.gongcheck.infrastructure.jwt; +import com.woowacourse.gongcheck.auth.application.JwtTokenProvider; +import com.woowacourse.gongcheck.auth.domain.Authority; +import com.woowacourse.gongcheck.exception.InfrastructureException; import com.woowacourse.gongcheck.exception.UnauthorizedException; -import com.woowacourse.gongcheck.presentation.Authority; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.JwtException; @@ -63,7 +65,7 @@ private Claims extractBody(final String token) { } catch (ExpiredJwtException e) { throw new UnauthorizedException("만료된 토큰입니다."); } catch (JwtException e) { - throw new UnauthorizedException("올바르지 않은 토큰입니다."); + throw new InfrastructureException("올바르지 않은 토큰입니다."); } } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/application/GithubOauthClient.java b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClient.java similarity index 74% rename from backend/src/main/java/com/woowacourse/gongcheck/application/GithubOauthClient.java rename to backend/src/main/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClient.java index 20c714e4..b4c8797b 100644 --- a/backend/src/main/java/com/woowacourse/gongcheck/application/GithubOauthClient.java +++ b/backend/src/main/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClient.java @@ -1,11 +1,12 @@ -package com.woowacourse.gongcheck.application; +package com.woowacourse.gongcheck.infrastructure.oauth; -import com.woowacourse.gongcheck.application.response.GithubAccessTokenResponse; -import com.woowacourse.gongcheck.application.response.GithubProfileResponse; -import com.woowacourse.gongcheck.exception.NotFoundException; +import com.woowacourse.gongcheck.auth.application.response.GithubAccessTokenResponse; +import com.woowacourse.gongcheck.auth.application.response.GithubProfileResponse; +import com.woowacourse.gongcheck.auth.presentation.request.GithubAccessTokenRequest; +import com.woowacourse.gongcheck.exception.InfrastructureException; import com.woowacourse.gongcheck.exception.UnauthorizedException; -import com.woowacourse.gongcheck.presentation.request.GithubAccessTokenRequest; import java.util.Objects; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -16,6 +17,7 @@ import org.springframework.web.client.RestTemplate; @Component +@Slf4j public class GithubOauthClient { private static final String BEARER_TYPE = "Bearer "; @@ -38,7 +40,11 @@ public GithubOauthClient(@Value("${github.client.id}") final String clientId, this.restTemplate = restTemplate; } - public String requestAccessToken(final String code) { + public GithubProfileResponse requestGithubProfileByCode(final String code) { + return requestGithubProfile(requestAccessToken(code)); + } + + private String requestAccessToken(final String code) { GithubAccessTokenRequest githubAccessTokenRequest = new GithubAccessTokenRequest(code, clientId, clientSecret); HttpHeaders headers = new HttpHeaders(); @@ -48,12 +54,12 @@ public String requestAccessToken(final String code) { GithubAccessTokenResponse githubAccessTokenResponse = exchangeRestTemplateBody(tokenUrl, HttpMethod.POST, httpEntity, GithubAccessTokenResponse.class); if (Objects.isNull(githubAccessTokenResponse)) { - throw new UnauthorizedException("잘못된 요청입니다."); + throw new InfrastructureException("잘못된 요청입니다."); } return githubAccessTokenResponse.getAccessToken(); } - public GithubProfileResponse requestGithubProfile(final String accessToken) { + private GithubProfileResponse requestGithubProfile(final String accessToken) { HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.AUTHORIZATION, BEARER_TYPE + accessToken); @@ -68,7 +74,8 @@ private T exchangeRestTemplateBody(final String url, final HttpMethod httpMe .exchange(url, httpMethod, httpEntity, exchangeType) .getBody(); } catch (HttpClientErrorException | NullPointerException e) { - throw new NotFoundException("해당 사용자의 프로필을 요청할 수 없습니다."); + log.error(e.getMessage()); + throw new InfrastructureException("해당 사용자의 프로필을 요청할 수 없습니다."); } } } diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/Authority.java b/backend/src/main/java/com/woowacourse/gongcheck/presentation/Authority.java deleted file mode 100644 index 960fb9ae..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/Authority.java +++ /dev/null @@ -1,6 +0,0 @@ -package com.woowacourse.gongcheck.presentation; - -public enum Authority { - GUEST, HOST; -} - diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/JobController.java b/backend/src/main/java/com/woowacourse/gongcheck/presentation/JobController.java deleted file mode 100644 index b356ecb7..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/JobController.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.woowacourse.gongcheck.presentation; - -import com.woowacourse.gongcheck.application.JobService; -import com.woowacourse.gongcheck.application.response.JobsResponse; -import com.woowacourse.gongcheck.presentation.request.JobCreateRequest; -import java.net.URI; -import javax.validation.Valid; -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -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 -@RequestMapping("/api") -public class JobController { - - private final JobService jobService; - - public JobController(final JobService jobService) { - this.jobService = jobService; - } - - @GetMapping("/spaces/{spaceId}/jobs") - public ResponseEntity showJobs(@AuthenticationPrincipal final Long hostId, - @PathVariable final Long spaceId, - final Pageable pageable) { - JobsResponse response = jobService.findPage(hostId, spaceId, pageable); - return ResponseEntity.ok(response); - } - - @PostMapping("/spaces/{spaceId}/jobs") - public ResponseEntity createJob(@AuthenticationPrincipal final Long hostId, - @PathVariable final Long spaceId, - @Valid @RequestBody final JobCreateRequest request) { - Long savedJobId = jobService.createJob(hostId, spaceId, request); - return ResponseEntity.created(URI.create("/api/spaces/" + savedJobId + "/jobs")).build(); - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/SpaceController.java b/backend/src/main/java/com/woowacourse/gongcheck/presentation/SpaceController.java deleted file mode 100644 index ab94061e..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/SpaceController.java +++ /dev/null @@ -1,48 +0,0 @@ -package com.woowacourse.gongcheck.presentation; - -import com.woowacourse.gongcheck.application.SpaceService; -import com.woowacourse.gongcheck.application.response.SpacesResponse; -import com.woowacourse.gongcheck.presentation.request.SpaceCreateRequest; -import java.net.URI; -import javax.validation.Valid; -import org.springframework.data.domain.Pageable; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ModelAttribute; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("/api") -public class SpaceController { - - private final SpaceService spaceService; - - public SpaceController(final SpaceService spaceService) { - this.spaceService = spaceService; - } - - @GetMapping("/spaces") - public ResponseEntity showSpaces(@AuthenticationPrincipal final Long hostId, - final Pageable pageable) { - SpacesResponse response = spaceService.findPage(hostId, pageable); - return ResponseEntity.ok(response); - } - - @PostMapping("/spaces") - public ResponseEntity createSpace(@AuthenticationPrincipal final Long hostId, - @Valid @ModelAttribute final SpaceCreateRequest request) { - Long spaceId = spaceService.createSpace(hostId, request); - return ResponseEntity.created(URI.create("/api/spaces/" + spaceId)).build(); - } - - @DeleteMapping("/spaces/{spaceId}") - public ResponseEntity removeSpace(@AuthenticationPrincipal final Long hostId, - @PathVariable final Long spaceId) { - spaceService.removeSpace(hostId, spaceId); - return ResponseEntity.noContent().build(); - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/SubmissionController.java b/backend/src/main/java/com/woowacourse/gongcheck/presentation/SubmissionController.java deleted file mode 100644 index ed15f570..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/SubmissionController.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.woowacourse.gongcheck.presentation; - -import com.woowacourse.gongcheck.application.AlertService; -import com.woowacourse.gongcheck.application.SubmissionService; -import com.woowacourse.gongcheck.application.response.SubmissionResponse; -import com.woowacourse.gongcheck.presentation.request.SubmissionRequest; -import javax.validation.Valid; -import org.springframework.core.task.TaskRejectedException; -import org.springframework.http.ResponseEntity; -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 -@RequestMapping("/api") -public class SubmissionController { - - private final SubmissionService submissionService; - private final AlertService alertService; - - public SubmissionController(final SubmissionService submissionService, final AlertService alertService) { - this.submissionService = submissionService; - this.alertService = alertService; - } - - @PostMapping("/jobs/{jobId}/complete") - public ResponseEntity submitJobCompletion(@AuthenticationPrincipal final Long hostId, - @PathVariable final Long jobId, - @Valid @RequestBody final SubmissionRequest request) { - SubmissionResponse submissionResponse = submissionService.submitJobCompletion(hostId, jobId, request); - try { - alertService.sendMessage(submissionResponse); - } catch (TaskRejectedException e) { - throw new RuntimeException(e); - } - return ResponseEntity.ok().build(); - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SectionCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SectionCreateRequest.java deleted file mode 100644 index cb0f57b7..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SectionCreateRequest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.woowacourse.gongcheck.presentation.request; - -import java.util.List; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; -import lombok.Getter; - -@Getter -public class SectionCreateRequest { - - @Size(min = 1, max = 20, message = "Section 이름은 한글자 이상 20자 이하여야 합니다.") - @NotNull(message = "작업의 이름은 null 일 수 없습니다.") - private String name; - - @Valid - private List tasks; - - private SectionCreateRequest() { - } - - public SectionCreateRequest(final String name, final List tasks) { - this.name = name; - this.tasks = tasks; - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SpaceCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SpaceCreateRequest.java deleted file mode 100644 index d587f937..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/SpaceCreateRequest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.woowacourse.gongcheck.presentation.request; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; -import lombok.Getter; -import org.springframework.web.multipart.MultipartFile; - -@Getter -public class SpaceCreateRequest { - - @NotNull - @Size(min = 1, max = 20, message = "공간의 이름은 한글자 이상 20자 이하여야 합니다.") - private String name; - private MultipartFile image; - - private SpaceCreateRequest() { - } - - public SpaceCreateRequest(final String name, final MultipartFile image) { - this.name = name; - this.image = image; - } -} diff --git a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/TaskCreateRequest.java b/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/TaskCreateRequest.java deleted file mode 100644 index d2b48495..00000000 --- a/backend/src/main/java/com/woowacourse/gongcheck/presentation/request/TaskCreateRequest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.woowacourse.gongcheck.presentation.request; - -import javax.validation.constraints.NotNull; -import javax.validation.constraints.Size; -import lombok.Getter; - -@Getter -public class TaskCreateRequest { - - @Size(min = 1, max = 50, message = "Task 이름은 한글자 이상 50자 이하여야 합니다.") - @NotNull(message = "작업의 이름은 null 일 수 없습니다.") - private String name; - - private TaskCreateRequest() { - } - - public TaskCreateRequest(final String name) { - this.name = name; - } -} diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml index cda584e1..14243861 100644 --- a/backend/src/main/resources/application-test.yml +++ b/backend/src/main/resources/application-test.yml @@ -1,3 +1,35 @@ +spring: + sql: + init: + schema-locations: classpath:schema_local.sql + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:gong-check-test;MODE=MYSQL; + username: sa + password: + h2: + console: + enabled: true + jpa: + hibernate: + ddl-auto: validate + open-in-view: false + properties: + hibernate: + show_sql: false + format_sql: true + output: + ansi: + enabled: always + +security: + jwt: + token: + secret-key: Z29uZy1jaGVjay1nb25nLWNoZWNrLWdvbmctY2hlY2stZ29uZy1jaGVjay1nb25nLWNoZWNrLWdvbmctY2hlY2stZ29uZy1jaGVjay1nb25nLWNoZWNrCg== + expire-time: 3600000 + hash: + secret-key: 12345678901234567890123456789012 + github: client: id: client_id @@ -5,3 +37,9 @@ github: url: token: https://github.com/login/oauth/access_token profile: https://api.github.com/user + +file: + upload-url: http://localhost:7070 + +server: + port: 7070 diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 1783877b..7e152230 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -1,31 +1,10 @@ spring: - datasource: - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:gong-check;MODE=MYSQL; - username: sa - password: - h2: - console: - enabled: true jpa: hibernate: ddl-auto: validate - properties: - hibernate: - show_sql: true - format_sql: true open-in-view: false - sql: - init: - schema-locations: classpath:schema.sql - data-locations: classpath:dummy_data.sql + output: + ansi: + enabled: always profiles: - include: oauth -security: - jwt: - token: - secret-key: Z29uZy1jaGVjay1nb25nLWNoZWNrLWdvbmctY2hlY2stZ29uZy1jaGVjay1nb25nLWNoZWNrLWdvbmctY2hlY2stZ29uZy1jaGVjay1nb25nLWNoZWNrCg== - expire-time: 3600000 - -server: - port: 8080 + default: local diff --git a/backend/src/main/resources/dummy_data.sql b/backend/src/main/resources/dummy_data.sql deleted file mode 100644 index fa253cbe..00000000 --- a/backend/src/main/resources/dummy_data.sql +++ /dev/null @@ -1,46 +0,0 @@ -INSERT INTO host (space_password, github_id, image_url, created_at) -VALUES ('1234', 1, 'test.com', current_timestamp()); - -INSERT INTO space (host_id, name, img_url, created_at) -VALUES (1, '잠실', 'https://velog.velcdn.com/images/cks3066/post/258f92c1-32be-4acb-be30-1eb64635c013/image.jpg', - current_timestamp()); -INSERT INTO space (host_id, name, img_url, created_at) -VALUES (1, '선릉', 'https://velog.velcdn.com/images/cks3066/post/28a9d0e5-d585-42e4-bc9e-458e439e2f4f/image.jpg', - current_timestamp()); - -INSERT INTO job (space_id, name, created_at) -VALUES (1, '청소', current_timestamp()); -INSERT INTO job (space_id, name, created_at) -VALUES (1, '마감', current_timestamp()); -INSERT INTO job (space_id, name, created_at) -VALUES (2, '청소', current_timestamp()); -INSERT INTO job (space_id, name, created_at) -VALUES (2, '마감', current_timestamp()); - -INSERT INTO section (job_id, name, created_at) -VALUES (1, '트랙룸', current_timestamp()); -INSERT INTO section (job_id, name, created_at) -VALUES (1, '굿샷 강의장', current_timestamp()); -INSERT INTO section (job_id, name, created_at) -VALUES (1, '톱오브스윙방', current_timestamp()); - -INSERT INTO task (section_id, name, created_at) -VALUES (1, '칠판 닦기', current_timestamp()); -INSERT INTO task (section_id, name, created_at) -VALUES (1, '빈백 털기', current_timestamp()); -INSERT INTO task (section_id, name, created_at) -VALUES (1, '책상 닦기', current_timestamp()); - -INSERT INTO task (section_id, name, created_at) -VALUES (2, '칠판 닦기', current_timestamp()); -INSERT INTO task (section_id, name, created_at) -VALUES (2, '책상 닦기', current_timestamp()); -INSERT INTO task (section_id, name, created_at) -VALUES (2, '의자 닦기', current_timestamp()); - -INSERT INTO task (section_id, name, created_at) -VALUES (3, '칠판 닦기', current_timestamp()); -INSERT INTO task (section_id, name, created_at) -VALUES (3, '책상 닦기', current_timestamp()); -INSERT INTO task (section_id, name, created_at) -VALUES (3, '의자 닦기', current_timestamp()); diff --git a/backend/src/main/resources/logback-access.xml b/backend/src/main/resources/logback-access.xml new file mode 100644 index 00000000..c2769226 --- /dev/null +++ b/backend/src/main/resources/logback-access.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/backend/src/main/resources/logback-spring.xml b/backend/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..bfaf37c9 --- /dev/null +++ b/backend/src/main/resources/logback-spring.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/logback/console/console-access-appender.xml b/backend/src/main/resources/logback/console/console-access-appender.xml new file mode 100644 index 00000000..ed3baa74 --- /dev/null +++ b/backend/src/main/resources/logback/console/console-access-appender.xml @@ -0,0 +1,10 @@ + + + + + + ${ACCESS_LOG_PATTERN} + ${UTF8_LOG_CHARSET} + + + diff --git a/backend/src/main/resources/logback/console/console-appender.xml b/backend/src/main/resources/logback/console/console-appender.xml new file mode 100644 index 00000000..f9516bea --- /dev/null +++ b/backend/src/main/resources/logback/console/console-appender.xml @@ -0,0 +1,24 @@ + + + + + + ${CONSOLE_LOG_PATTERN} + ${CONSOLE_LOG_CHARSET} + + + + + + ${CONSOLE_DB_LOG_PATTERN} + ${CONSOLE_LOG_CHARSET} + + + + + + ${CONSOLE_QUERY_LOG_PATTERN} + ${CONSOLE_LOG_CHARSET} + + + diff --git a/backend/src/main/resources/logback/file/file-access-appender.xml b/backend/src/main/resources/logback/file/file-access-appender.xml new file mode 100644 index 00000000..a38cd875 --- /dev/null +++ b/backend/src/main/resources/logback/file/file-access-appender.xml @@ -0,0 +1,19 @@ + + + + + + ${home}access.log + + ${home}access-%d{yyyy-MM-dd}-%i.log + + 15MB + + 7 + + + ${ACCESS_LOG_PATTERN} + ${UTF8_LOG_CHARSET} + + + diff --git a/backend/src/main/resources/logback/file/file-debug-appender.xml b/backend/src/main/resources/logback/file/file-debug-appender.xml new file mode 100644 index 00000000..2062ad6d --- /dev/null +++ b/backend/src/main/resources/logback/file/file-debug-appender.xml @@ -0,0 +1,24 @@ + + + + + + ${home}debug.log + + DEBUG + ACCEPT + DENY + + + ${home}debug-%d{yyyy-MM-dd}-%i.log + + 32MB + + 7 + + + ${FILE_LOG_PATTERN} + ${UTF8_LOG_CHARSET} + + + diff --git a/backend/src/main/resources/logback/file/file-error-appender.xml b/backend/src/main/resources/logback/file/file-error-appender.xml new file mode 100644 index 00000000..ab7f004c --- /dev/null +++ b/backend/src/main/resources/logback/file/file-error-appender.xml @@ -0,0 +1,24 @@ + + + + + + ${home}error.log + + ERROR + ACCEPT + DENY + + + ${home}error-%d{yyyy-MM-dd}-%i.log + + 32MB + + 30 + + + ${FILE_LOG_PATTERN} + ${UTF8_LOG_CHARSET} + + + diff --git a/backend/src/main/resources/logback/file/file-info-appender.xml b/backend/src/main/resources/logback/file/file-info-appender.xml new file mode 100644 index 00000000..1df19c53 --- /dev/null +++ b/backend/src/main/resources/logback/file/file-info-appender.xml @@ -0,0 +1,24 @@ + + + + + + ${home}info.log + + INFO + ACCEPT + DENY + + + ${home}info-%d{yyyy-MM-dd}-%i.log + + 16MB + + 7 + + + ${FILE_LOG_PATTERN} + ${UTF8_LOG_CHARSET} + + + diff --git a/backend/src/main/resources/logback/file/file-warn-appender.xml b/backend/src/main/resources/logback/file/file-warn-appender.xml new file mode 100644 index 00000000..afcb0314 --- /dev/null +++ b/backend/src/main/resources/logback/file/file-warn-appender.xml @@ -0,0 +1,24 @@ + + + + + + ${home}warn.log + + WARN + ACCEPT + DENY + + + ${home}warn-%d{yyyy-MM-dd}-%i.log + + 16MB + + 7 + + + ${FILE_LOG_PATTERN} + ${UTF8_LOG_CHARSET} + + + diff --git a/backend/src/main/resources/logback/properties/default-properties.xml b/backend/src/main/resources/logback/properties/default-properties.xml new file mode 100644 index 00000000..d36659cd --- /dev/null +++ b/backend/src/main/resources/logback/properties/default-properties.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/backend/src/main/resources/logback/slack/slack-appender.xml b/backend/src/main/resources/logback/slack/slack-appender.xml new file mode 100644 index 00000000..0e77d1d9 --- /dev/null +++ b/backend/src/main/resources/logback/slack/slack-appender.xml @@ -0,0 +1,18 @@ + + + + + + ${webhook} + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level --- [%thread] %logger.%M{35} : %msg %n + + + + + + + WARN + + + diff --git a/backend/src/main/resources/schema.sql b/backend/src/main/resources/schema_local.sql similarity index 56% rename from backend/src/main/resources/schema.sql rename to backend/src/main/resources/schema_local.sql index 7733c0c8..6a20a934 100644 --- a/backend/src/main/resources/schema.sql +++ b/backend/src/main/resources/schema_local.sql @@ -1,3 +1,11 @@ +DROP TABLE host IF EXISTS; +DROP TABLE space IF EXISTS; +DROP TABLE job IF EXISTS; +DROP TABLE running_task IF EXISTS; +DROP TABLE task IF EXISTS; +DROP TABLE section IF EXISTS; +DROP TABLE submission IF EXISTS; + CREATE TABLE host ( id BIGINT NOT NULL AUTO_INCREMENT, @@ -5,7 +13,7 @@ CREATE TABLE host github_id BIGINT NOT NULL UNIQUE, image_url VARCHAR NOT NULL, created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, PRIMARY KEY (id) ); @@ -13,7 +21,7 @@ CREATE TABLE space ( id BIGINT NOT NULL AUTO_INCREMENT, host_id BIGINT NOT NULL, - name VARCHAR(20) NOT NULL, + name VARCHAR(10) NOT NULL, img_url VARCHAR NULL, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NULL, @@ -24,30 +32,34 @@ CREATE TABLE job ( id BIGINT NOT NULL AUTO_INCREMENT, space_id BIGINT NOT NULL, - name VARCHAR(20) NOT NULL, - slack_url VARCHAR NULL, + name VARCHAR(10) NOT NULL, + slack_url VARCHAR NULL, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NULL, PRIMARY KEY (id) ); -CREATE TABLE task +CREATE TABLE section ( - id BIGINT NOT NULL AUTO_INCREMENT, - section_id BIGINT NOT NULL, - name VARCHAR(50) NOT NULL, - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NULL, + id BIGINT NOT NULL AUTO_INCREMENT, + job_id BIGINT NOT NULL, + name VARCHAR(10) NOT NULL, + description VARCHAR(128) NULL, + image_url VARCHAR NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NULL, PRIMARY KEY (id) ); -CREATE TABLE section +CREATE TABLE task ( - id BIGINT NOT NULL AUTO_INCREMENT, - job_id BIGINT NOT NULL, - name VARCHAR(20) NOT NULL, - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NULL, + id BIGINT NOT NULL AUTO_INCREMENT, + section_id BIGINT NOT NULL, + name VARCHAR(10) NOT NULL, + description VARCHAR(128) NULL, + image_url VARCHAR NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NULL, PRIMARY KEY (id) ); @@ -64,7 +76,7 @@ CREATE TABLE submission ( id BIGINT NOT NULL AUTO_INCREMENT, job_id BIGINT NOT NULL, - author VARCHAR(20) NOT NULL, + author VARCHAR(10) NOT NULL, created_at TIMESTAMP NOT NULL, PRIMARY KEY (id) ); diff --git a/backend/src/test/java/com/woowacourse/gongcheck/FakeImageFactory.java b/backend/src/test/java/com/woowacourse/gongcheck/FakeImageFactory.java new file mode 100644 index 00000000..ed6209ac --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/FakeImageFactory.java @@ -0,0 +1,21 @@ +package com.woowacourse.gongcheck; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import org.jetbrains.annotations.NotNull; + +public class FakeImageFactory { + + @NotNull + public static File createFakeImage() throws IOException { + File fakeImage = File.createTempFile("temp", ".jpg"); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(fakeImage))) { + writer.write("1234"); + } catch (IOException e) { + e.printStackTrace(); + } + return fakeImage; + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AcceptanceTest.java index ae118c1e..2560676b 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AcceptanceTest.java @@ -1,26 +1,72 @@ package com.woowacourse.gongcheck.acceptance; -import com.woowacourse.gongcheck.application.AlertService; +import static io.restassured.RestAssured.UNDEFINED_PORT; + +import com.woowacourse.gongcheck.auth.application.EntranceCodeProvider; +import com.woowacourse.gongcheck.auth.application.response.GuestTokenResponse; +import com.woowacourse.gongcheck.auth.application.response.TokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.core.application.AlertService; +import com.woowacourse.gongcheck.core.application.ImageUploader; import io.restassured.RestAssured; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.web.server.LocalServerPort; -import org.springframework.test.annotation.DirtiesContext; +import org.springframework.http.MediaType; @SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT) -@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) class AcceptanceTest { @MockBean private AlertService alertService; + @MockBean + protected ImageUploader imageUploader; + + @Autowired + private EntranceCodeProvider entranceCodeProvider; + + @Autowired + private DatabaseInitializer databaseInitializer; + @LocalServerPort private int port; @BeforeEach - void setPort() { - RestAssured.port = port; + void setUp() { + if (RestAssured.port == UNDEFINED_PORT) { + RestAssured.port = port; + } + databaseInitializer.initTable(); + } + + @AfterEach + void clean() { + databaseInitializer.truncateTables(); + } + public String 토큰을_요청한다(final GuestEnterRequest guestEnterRequest) { + String entranceCode = entranceCodeProvider.createEntranceCode(1L); + return RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(guestEnterRequest) + .when().post("/api/hosts/{entranceCode}/enter/", entranceCode) + .then().log().all() + .extract() + .as(GuestTokenResponse.class) + .getToken(); + } + + public TokenResponse Host_토큰을_요청한다() { + return RestAssured + .given().log().all() + .when().post("/fake/login") + .then().log().all() + .extract() + .as(TokenResponse.class); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AuthSupport.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AuthSupport.java deleted file mode 100644 index 1f935b6a..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/AuthSupport.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.woowacourse.gongcheck.acceptance; - -import com.woowacourse.gongcheck.application.response.GuestTokenResponse; -import com.woowacourse.gongcheck.application.response.TokenResponse; -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; -import com.woowacourse.gongcheck.presentation.request.TokenRequest; -import io.restassured.RestAssured; -import org.springframework.http.MediaType; - -public class AuthSupport { - - public static String 토큰을_요청한다(final GuestEnterRequest guestEnterRequest) { - return RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(guestEnterRequest) - .when().post("/api/hosts/1/enter") - .then().log().all() - .extract() - .as(GuestTokenResponse.class) - .getToken(); - } - - public static TokenResponse Host_토큰을_요청한다(final TokenRequest tokenRequest) { - return RestAssured - .given().log().all() - .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(tokenRequest) - .when().post("/fake/login") - .then().log().all() - .extract() - .as(TokenResponse.class); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/DatabaseInitializer.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/DatabaseInitializer.java new file mode 100644 index 00000000..93eba75f --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/DatabaseInitializer.java @@ -0,0 +1,80 @@ +package com.woowacourse.gongcheck.acceptance; + +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.PostConstruct; +import javax.persistence.EntityManager; +import javax.sql.DataSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class DatabaseInitializer { + + private static final String TRUNCATE_QUERY = "TRUNCATE TABLE %s"; + private static final String ALTER_COLUMN_QUERY = "ALTER TABLE %s ALTER COLUMN id RESTART WITH 1"; + + @Autowired + private EntityManager entityManager; + + @Autowired + private DataSource dataSource; + + private final List tableNames = new ArrayList<>(); + + @PostConstruct + public void afterPropertiesSet() { + try { + DatabaseMetaData metaData = dataSource.getConnection().getMetaData(); + ResultSet tables = metaData.getTables(null, null, null, new String[]{"TABLE"}); + while (tables.next()) { + String tableName = tables.getString("TABLE_NAME"); + tableNames.add(tableName); + } + } catch (Exception e) { + throw new RuntimeException("테이블 이름을 불러올 수 없습니다."); + } + } + + @Transactional + public void truncateTables() { + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate(); + for (String tableName : tableNames) { + truncateTable(tableName); + } + entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate(); + } + + @Transactional + public void initTable() { + entityManager.createNativeQuery("INSERT INTO host (id, space_password, github_id, image_url, created_at)\n" + + "VALUES (1, '1234', 2, 'test.com', current_timestamp())").executeUpdate(); + entityManager.createNativeQuery("INSERT INTO space (host_id, name, created_at)\n" + + "VALUES (1, '잠실', current_timestamp())").executeUpdate(); + entityManager.createNativeQuery("INSERT INTO job (space_id, name, slack_url, created_at)\n" + + "VALUES (1, '청소', 'http://slackurl.com', current_timestamp())").executeUpdate(); + entityManager.createNativeQuery("INSERT INTO section (job_id, name, created_at, image_url, description)\n" + + "VALUES (1, '트랙룸', current_timestamp(), 'image_url', '설명')").executeUpdate(); + entityManager.createNativeQuery("INSERT INTO task (section_id, name, created_at, image_url, description)\n" + + "VALUES (1, '책상 청소', current_timestamp(), 'image_url', '설명')").executeUpdate(); + entityManager.createNativeQuery("INSERT INTO task (section_id, name, created_at, image_url, description)\n" + + "VALUES (1, '빈백 털기', current_timestamp(), 'image_url', '설명')").executeUpdate(); + entityManager.createNativeQuery("INSERT INTO section (job_id, name, created_at, image_url, description)\n" + + "VALUES (1, '굿샷 강의장', current_timestamp(), 'image_url', '설명')").executeUpdate(); + entityManager.createNativeQuery("INSERT INTO task (section_id, name, created_at, image_url, description)\n" + + "VALUES (2, '책상 청소', current_timestamp(), 'image_url', '설명')").executeUpdate(); + entityManager.createNativeQuery("INSERT INTO task (section_id, name, created_at, image_url, description)\n" + + "VALUES (2, '의자 청소', current_timestamp(), 'image_url', '설명')").executeUpdate(); + } + + private void truncateTable(final String tableName) { + entityManager.createNativeQuery(String.format(TRUNCATE_QUERY, tableName)).executeUpdate(); + if (tableName.equals("RUNNING_TASK")) { + return; + } + entityManager.createNativeQuery(String.format(ALTER_COLUMN_QUERY, tableName)).executeUpdate(); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/FakeHostAuthController.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/FakeHostAuthController.java index 69273976..943dd07a 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/FakeHostAuthController.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/FakeHostAuthController.java @@ -6,11 +6,11 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.woowacourse.gongcheck.application.HostAuthService; -import com.woowacourse.gongcheck.application.response.GithubAccessTokenResponse; -import com.woowacourse.gongcheck.application.response.GithubProfileResponse; -import com.woowacourse.gongcheck.application.response.TokenResponse; -import com.woowacourse.gongcheck.presentation.request.TokenRequest; +import com.woowacourse.gongcheck.auth.application.HostAuthService; +import com.woowacourse.gongcheck.auth.application.response.GithubAccessTokenResponse; +import com.woowacourse.gongcheck.auth.application.response.GithubProfileResponse; +import com.woowacourse.gongcheck.auth.application.response.TokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.TokenRequest; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; @@ -43,7 +43,28 @@ public ResponseEntity login() throws JsonProcessingException { .body(objectMapper.writeValueAsString(new GithubAccessTokenResponse("token")))); GithubProfileResponse githubProfileResponse = new GithubProfileResponse("nickname", "loginName", - "1234", "test.com"); + "2", "test.com"); + mockRestServiceServer.expect(requestTo("https://api.github.com/user")) + .andExpect(method(HttpMethod.GET)) + .andRespond(withStatus(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(githubProfileResponse))); + + TokenResponse result = hostAuthService.createToken(new TokenRequest("code")); + return ResponseEntity.ok(result); + } + + @PostMapping("/fake/signup") + public ResponseEntity signup() throws JsonProcessingException { + MockRestServiceServer mockRestServiceServer = MockRestServiceServer.createServer(restTemplate); + mockRestServiceServer.expect(requestTo("https://github.com/login/oauth/access_token")) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(new GithubAccessTokenResponse("token")))); + + GithubProfileResponse githubProfileResponse = new GithubProfileResponse("nickname", "loginName", + "3", "test.com"); mockRestServiceServer.expect(requestTo("https://api.github.com/user")) .andExpect(method(HttpMethod.GET)) .andRespond(withStatus(HttpStatus.OK) diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/GuestAuthAcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/GuestAuthAcceptanceTest.java index 41e843dd..0de1a71c 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/GuestAuthAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/GuestAuthAcceptanceTest.java @@ -3,25 +3,31 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -import com.woowacourse.gongcheck.application.response.GuestTokenResponse; -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.auth.application.EntranceCodeProvider; +import com.woowacourse.gongcheck.auth.application.response.GuestTokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; class GuestAuthAcceptanceTest extends AcceptanceTest { + @Autowired + private EntranceCodeProvider entranceCodeProvider; + @Test - void 올바른_공간_비밀번호를_입력하면_토큰을_반환한다() { + void 올바른_Space_비밀번호를_입력하면_토큰을_반환한다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); + String entranceCode = entranceCodeProvider.createEntranceCode(1L); ExtractableResponse response = RestAssured.given().log().all() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(guestEnterRequest) - .when().post("/api/hosts/1/enter") + .when().post("/api/hosts/{entranceCode}/enter/", entranceCode) .then().log().all() .extract(); diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/HostAcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/HostAcceptanceTest.java index f6edb0a9..00421b7c 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/HostAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/HostAcceptanceTest.java @@ -1,24 +1,21 @@ package com.woowacourse.gongcheck.acceptance; -import static com.woowacourse.gongcheck.acceptance.AuthSupport.토큰을_요청한다; +import static org.assertj.core.api.Assertions.assertThat; -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; -import com.woowacourse.gongcheck.presentation.request.SpacePasswordChangeRequest; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.core.presentation.request.SpacePasswordChangeRequest; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; class HostAcceptanceTest extends AcceptanceTest { - + @Test - void 공간_비밀번호를_변경한다() { - // 호스트 로그인 구현 전까지 토큰 입력용으로 사용 - GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); - String token = 토큰을_요청한다(guestEnterRequest); + void Host_토큰으로_Space_비밀번호를_변경한다() { + String token = Host_토큰을_요청한다().getToken(); ExtractableResponse response = RestAssured .given().log().all() @@ -29,6 +26,35 @@ class HostAcceptanceTest extends AcceptanceTest { .then().log().all() .extract(); - Assertions.assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + } + + @Test + void Guest_토큰으로_Space_비밀번호를_변경_요청_시_예외가_발생한다() { + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); + ExtractableResponse response = RestAssured + .given().log().all() + .body(new SpacePasswordChangeRequest("4567")) + .auth().oauth2(token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .when().patch("/api/spacePassword") + .then().log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + } + + @Test + void Host_토큰으로_호스트_아이디를_조회한다() { + String token = Host_토큰을_요청한다().getToken(); + + ExtractableResponse response = RestAssured + .given().log().all() + .auth().oauth2(token) + .when().get("/api/hosts/entranceCode") + .then().log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/HostAuthAcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/HostAuthAcceptanceTest.java index fb15957b..09ffc940 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/HostAuthAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/HostAuthAcceptanceTest.java @@ -1,11 +1,10 @@ package com.woowacourse.gongcheck.acceptance; -import static com.woowacourse.gongcheck.acceptance.AuthSupport.Host_토큰을_요청한다; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -import com.woowacourse.gongcheck.application.response.TokenResponse; -import com.woowacourse.gongcheck.presentation.request.TokenRequest; +import com.woowacourse.gongcheck.auth.application.response.TokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.TokenRequest; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; @@ -14,6 +13,7 @@ import org.springframework.http.MediaType; class HostAuthAcceptanceTest extends AcceptanceTest { + @Test void 첫_로그인한_Host이면_회원가입하고_토큰을_발급한다() { TokenRequest tokenRequest = new TokenRequest("code"); @@ -30,7 +30,7 @@ class HostAuthAcceptanceTest extends AcceptanceTest { assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), () -> assertThat(tokenResponse.getToken()).isNotNull(), - () -> assertThat(tokenResponse.isAlreadyJoin()).isFalse() + () -> assertThat(tokenResponse.isAlreadyJoin()).isTrue() ); } @@ -38,12 +38,11 @@ class HostAuthAcceptanceTest extends AcceptanceTest { void 첫_로그인한_Host가_아니면_토큰을_발급한다() { TokenRequest tokenRequest = new TokenRequest("code"); - Host_토큰을_요청한다(tokenRequest); ExtractableResponse response = RestAssured .given().log().all() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(tokenRequest) - .when().post("/fake/login") + .when().post("/fake/signup") .then().log().all() .extract(); @@ -51,7 +50,7 @@ class HostAuthAcceptanceTest extends AcceptanceTest { assertAll( () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), () -> assertThat(tokenResponse.getToken()).isNotNull(), - () -> assertThat(tokenResponse.isAlreadyJoin()).isTrue() + () -> assertThat(tokenResponse.isAlreadyJoin()).isFalse() ); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/ImageUploadAcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/ImageUploadAcceptanceTest.java new file mode 100644 index 00000000..04e1ac36 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/ImageUploadAcceptanceTest.java @@ -0,0 +1,52 @@ +package com.woowacourse.gongcheck.acceptance; + + +import static com.woowacourse.gongcheck.FakeImageFactory.createFakeImage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.core.application.response.ImageUrlResponse; +import io.restassured.RestAssured; +import java.io.File; +import java.io.IOException; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +class ImageUploadAcceptanceTest extends AcceptanceTest { + + @Test + void Host_토큰으로_이미지를_업로드한다() throws IOException { + File fakeImage = createFakeImage(); + String token = Host_토큰을_요청한다().getToken(); + + when(imageUploader.upload(any(), anyString())) + .thenReturn(ImageUrlResponse.from("https://localhost/images/cjnaskdcnljaskd.jpg")); + + RestAssured + .given().log().all() + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .multiPart("image", fakeImage, "image/jpg") + .auth().oauth2(token) + .when().post("/api/imageUpload") + .then().log().all() + .statusCode(HttpStatus.OK.value()); + } + + @Test + void Guest_토큰으로_이미지_업로드_시_예외가_발생한다() throws IOException { + File fakeImage = createFakeImage(); + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); + + RestAssured + .given().log().all() + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .multiPart("image", fakeImage) + .auth().oauth2(token) + .when().post("/api/imageUpload") + .then().log().all() + .statusCode(HttpStatus.UNAUTHORIZED.value()); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/JobAcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/JobAcceptanceTest.java index 77bbf99c..f8ac97cd 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/JobAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/JobAcceptanceTest.java @@ -1,127 +1,222 @@ package com.woowacourse.gongcheck.acceptance; -import static com.woowacourse.gongcheck.acceptance.AuthSupport.토큰을_요청한다; -import static org.assertj.core.api.Assertions.assertThat; - -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; -import com.woowacourse.gongcheck.presentation.request.JobCreateRequest; -import com.woowacourse.gongcheck.presentation.request.SectionCreateRequest; -import com.woowacourse.gongcheck.presentation.request.TaskCreateRequest; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.core.presentation.request.JobCreateRequest; +import com.woowacourse.gongcheck.core.presentation.request.SectionCreateRequest; +import com.woowacourse.gongcheck.core.presentation.request.SlackUrlChangeRequest; +import com.woowacourse.gongcheck.core.presentation.request.TaskCreateRequest; import io.restassured.RestAssured; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; import java.util.List; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullSource; -import org.junit.jupiter.params.provider.ValueSource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; class JobAcceptanceTest extends AcceptanceTest { @Test - void Job을_조회한다() { - GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); - String token = 토큰을_요청한다(guestEnterRequest); + void Host_토큰으로_Job을_조회한다() { + String token = Host_토큰을_요청한다().getToken(); - ExtractableResponse response = RestAssured + RestAssured .given().log().all() .auth().oauth2(token) - .when().get("/api/spaces/1/jobs?page=0&size=5") + .when().get("/api/spaces/1/jobs") .then().log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + .statusCode(HttpStatus.OK.value()); } @Test - void Job을_생성한다() { - GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); - String token = 토큰을_요청한다(guestEnterRequest); - - List tasks = List.of(new TaskCreateRequest("책상 닦기"), new TaskCreateRequest("칠판 닦기")); - List sections = List.of(new SectionCreateRequest("대강의실", tasks)); + void Host_토큰으로_Job을_생성한다() { + String token = Host_토큰을_요청한다().getToken(); + + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List + .of(new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); JobCreateRequest request = new JobCreateRequest("청소", sections); - ExtractableResponse response = RestAssured + RestAssured .given().log().all() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(request) .auth().oauth2(token) .when().post("/api/spaces/1/jobs") .then().log().all() - .extract(); + .statusCode(HttpStatus.CREATED.value()); + } - assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + @Test + void Host_토큰으로_Job을_수정한다() { + String token = Host_토큰을_요청한다().getToken(); + + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List + .of(new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + JobCreateRequest request = new JobCreateRequest("청소", sections); + + RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .auth().oauth2(token) + .when().put("/api/jobs/1") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); } - @ParameterizedTest - @NullSource - @ValueSource(strings = {"", "Job 이름이 20글자 이상일 경우 예외"}) - void Job의_이름이_1글자_미만_20글자_초과하거나_null일_경우_예외가_발생한다(final String input) { - GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); - String token = 토큰을_요청한다(guestEnterRequest); + @Test + void Host_토큰으로_존재하지_않는_Job을_수정할_경우_예외가_발생한다() { + String token = Host_토큰을_요청한다().getToken(); + + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List + .of(new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + JobCreateRequest request = new JobCreateRequest("청소", sections); + + RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .auth().oauth2(token) + .when().put("/api/jobs/0") + .then().log().all() + .statusCode(HttpStatus.NOT_FOUND.value()); + } - List tasks = List.of(new TaskCreateRequest("책상 닦기"), new TaskCreateRequest("칠판 닦기")); - List sections = List.of(new SectionCreateRequest("대강의실", tasks)); - JobCreateRequest wrongRequest = new JobCreateRequest(input, sections); + @Test + void Host_토큰으로_Job을_삭제한다() { + String token = Host_토큰을_요청한다().getToken(); - ExtractableResponse response = RestAssured + RestAssured .given().log().all() .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(wrongRequest) .auth().oauth2(token) - .when().post("/api/spaces/1/jobs") + .when().delete("/api/jobs/1") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + void Host_토큰으로_Job의_Slack_Url을_조회한다() { + String token = Host_토큰을_요청한다().getToken(); + + RestAssured + .given().log().all() + .auth().oauth2(token) + .when().get("/api/jobs/1/slack") .then().log().all() - .extract(); + .statusCode(HttpStatus.OK.value()); + } - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + @Test + void Host_토큰으로_Job의_Slack_Url을_수정한다() { + String token = Host_토큰을_요청한다().getToken(); + + RestAssured + .given().log().all() + .auth().oauth2(token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new SlackUrlChangeRequest("https://newslackurl.com")) + .when().put("/api/jobs/1/slack") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); } - @ParameterizedTest - @NullSource - @ValueSource(strings = {"", "Section의 name이 20자 초과"}) - void Section의_이름이_1글자_미만_20글자_초과하거나_null일_경우_예외가_발생한다(String input) { - GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); - String token = 토큰을_요청한다(guestEnterRequest); + @Test + void Guest_토큰으로_Job을_조회한다() { + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); - List tasks = List.of(new TaskCreateRequest("책상 닦기"), new TaskCreateRequest("칠판 닦기")); - List sections = List.of(new SectionCreateRequest(input, tasks)); - JobCreateRequest wrongRequest = new JobCreateRequest("청소", sections); + RestAssured + .given().log().all() + .auth().oauth2(token) + .when().get("/api/spaces/1/jobs") + .then().log().all() + .statusCode(HttpStatus.OK.value()); + } + + @Test + void Guest_토큰으로_Job을_생성_시_예외가_발생한다() { + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); + + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List + .of(new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + JobCreateRequest request = new JobCreateRequest("청소", sections); - ExtractableResponse response = RestAssured + RestAssured .given().log().all() .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(wrongRequest) + .body(request) .auth().oauth2(token) .when().post("/api/spaces/1/jobs") .then().log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + .statusCode(HttpStatus.UNAUTHORIZED.value()); } - @ParameterizedTest - @NullSource - @ValueSource(strings = {"", "Task의 이름이 1글자 미만 50글자 초과일 경우, Status Code 404를 반환한다"}) - void Task의_이름이_1글자_미만_50글자_초과하거나_null일_경우_예외가_발생한다(String input) { - GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); - String token = 토큰을_요청한다(guestEnterRequest); + @Test + void Guest_토큰으로_Job을_수정_시_예외가_발생한다() { + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); + + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List + .of(new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + JobCreateRequest request = new JobCreateRequest("청소", sections); + + RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .auth().oauth2(token) + .when().put("/api/jobs/1") + .then().log().all() + .statusCode(HttpStatus.UNAUTHORIZED.value()); + } - List tasks = List.of(new TaskCreateRequest(input), new TaskCreateRequest("칠판 닦기")); - List sections = List.of(new SectionCreateRequest("대강의실", tasks)); - JobCreateRequest wrongRequest = new JobCreateRequest("청소", sections); + @Test + void Guest_토큰으로_Job을_삭제_시_예외가_발생한다() { + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); - ExtractableResponse response = RestAssured + RestAssured .given().log().all() .contentType(MediaType.APPLICATION_JSON_VALUE) - .body(wrongRequest) .auth().oauth2(token) - .when().post("/api/spaces/1/jobs") + .when().delete("/api/jobs/1") + .then().log().all() + .statusCode(HttpStatus.UNAUTHORIZED.value()); + } + + @Test + void Guest_토큰으로_Job의_Slack_Url을_조회_시_예외가_발생한다() { + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); + + RestAssured + .given().log().all() + .auth().oauth2(token) + .when().get("/api/jobs/1/slack") .then().log().all() - .extract(); + .statusCode(HttpStatus.UNAUTHORIZED.value()); + } + + @Test + void Guest_토큰으로_Job의_Slack_Url을_수정_시_예외가_발생한다() { + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + RestAssured + .given().log().all() + .auth().oauth2(token) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(new SlackUrlChangeRequest("https://newslackurl.com")) + .when().put("/api/jobs/1/slack") + .then().log().all() + .statusCode(HttpStatus.UNAUTHORIZED.value()); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/SpaceAcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/SpaceAcceptanceTest.java index 81bd650f..3578a662 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/SpaceAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/SpaceAcceptanceTest.java @@ -1,12 +1,9 @@ package com.woowacourse.gongcheck.acceptance; -import static com.woowacourse.gongcheck.acceptance.AuthSupport.토큰을_요청한다; -import static org.assertj.core.api.Assertions.assertThat; - -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.core.presentation.request.SpaceChangeRequest; +import com.woowacourse.gongcheck.core.presentation.request.SpaceCreateRequest; import io.restassured.RestAssured; -import io.restassured.response.ExtractableResponse; -import io.restassured.response.Response; import java.io.File; import java.io.IOException; import org.junit.jupiter.api.Test; @@ -16,84 +13,130 @@ class SpaceAcceptanceTest extends AcceptanceTest { @Test - void 공간을_조회한다() { - GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); - String token = 토큰을_요청한다(guestEnterRequest); + void Host_토큰으로_Space를_조회한다() { + String token = Host_토큰을_요청한다().getToken(); - ExtractableResponse response = RestAssured + RestAssured .given().log().all() .auth().oauth2(token) - .when().get("/api/spaces?page=0&size=5") + .when().get("/api/spaces") .then().log().all() - .extract(); + .statusCode(HttpStatus.OK.value()); + } - assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + @Test + void Host_토큰으로_Space를_생성한다() { + SpaceCreateRequest request = new SpaceCreateRequest("잠실 캠퍼스", "https://image.gongcheck.shop/123sdf5"); + String token = Host_토큰을_요청한다().getToken(); + + RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .auth().oauth2(token) + .when().post("/api/spaces") + .then().log().all() + .statusCode(HttpStatus.CREATED.value()); } @Test - void 공간을_생성한다() throws IOException { - File fakeImage = File.createTempFile("temp", ".jpg"); + void Host_토큰으로_한_Host가_이미_존재하는_이름의_Space를_생성하면_에러_응답을_반환한다() { + SpaceCreateRequest request = new SpaceCreateRequest("잠실 캠퍼스", "https://image.gongcheck.shop/123sdf5"); + String token = Host_토큰을_요청한다().getToken(); - // 호스트 로그인 구현 전까지 토큰 입력용으로 사용 - GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); - String token = 토큰을_요청한다(guestEnterRequest); + RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .auth().oauth2(token) + .when().post("/api/spaces") + .then().log().all(); - ExtractableResponse response = RestAssured + RestAssured .given().log().all() - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .multiPart("name", "잠실 캠퍼스") - .multiPart("image", fakeImage) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) .auth().oauth2(token) .when().post("/api/spaces") .then().log().all() - .extract(); + .statusCode(HttpStatus.BAD_REQUEST.value()); + } + + @Test + void Host_토큰으로_Space를_수정한다() { + SpaceChangeRequest request = new SpaceChangeRequest("잠실 캠퍼스", "https://image.gongcheck.shop/123sdf5"); + String token = Host_토큰을_요청한다().getToken(); - assertThat(response.statusCode()).isEqualTo(HttpStatus.CREATED.value()); + RestAssured + .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .auth().oauth2(token) + .when().put("/api/spaces/1") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); } @Test - void 한_호스트가_이미_존재하는_이름의_공간을_생성하면_에러_응답을_반환한다() throws IOException { - File fakeImage = File.createTempFile("temp", ".jpg"); + void Guest_토큰으로_단일_Space를_조회한다() { + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); - // 호스트 로그인 구현 전까지 토큰 입력용으로 사용 - GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); - String token = 토큰을_요청한다(guestEnterRequest); RestAssured .given().log().all() - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .multiPart("name", "잠실 캠퍼스") - .multiPart("image", fakeImage) .auth().oauth2(token) - .when().post("/api/spaces") + .when().get("/api/spaces/1") .then().log().all() - .extract(); + .statusCode(HttpStatus.OK.value()); + } - ExtractableResponse response = RestAssured + @Test + void Host_토큰으로_Space를_삭제한다() { + String token = Host_토큰을_요청한다().getToken(); + + RestAssured .given().log().all() - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .multiPart("name", "잠실 캠퍼스") - .multiPart("image", fakeImage) .auth().oauth2(token) - .when().post("/api/spaces") + .when().delete("/api/spaces/1") .then().log().all() - .extract(); + .statusCode(HttpStatus.NO_CONTENT.value()); + } - assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + @Test + void Guest_토큰으로_Space를_조회한다() { + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); + + RestAssured + .given().log().all() + .auth().oauth2(token) + .when().get("/api/spaces") + .then().log().all() + .statusCode(HttpStatus.OK.value()); } @Test - void 공간을_삭제한다() { - // 호스트 로그인 구현 전까지 토큰 입력용으로 사용 - GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); - String token = 토큰을_요청한다(guestEnterRequest); + void Guest_토큰으로_Space를_생성_시_예외가_발생한다() { + SpaceCreateRequest request = new SpaceCreateRequest("잠실 캠퍼스", "https://image.gongcheck.shop/123sdf5"); + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); - ExtractableResponse response = RestAssured + RestAssured .given().log().all() + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) .auth().oauth2(token) - .when().delete("/api/spaces/1") + .when().post("/api/spaces") .then().log().all() - .extract(); + .statusCode(HttpStatus.UNAUTHORIZED.value()); + } + + @Test + void Guest_토큰으로_Space를_삭제_시_예외가_발생한다() { + String token = 토큰을_요청한다(new GuestEnterRequest("1234")); - assertThat(response.statusCode()).isEqualTo(HttpStatus.NO_CONTENT.value()); + RestAssured + .given().log().all() + .auth().oauth2(token) + .when().delete("/api/spaces/1") + .then().log().all() + .statusCode(HttpStatus.UNAUTHORIZED.value()); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/SubmissionAcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/SubmissionAcceptanceTest.java index bc21e065..d2cdfeed 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/SubmissionAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/SubmissionAcceptanceTest.java @@ -1,10 +1,9 @@ package com.woowacourse.gongcheck.acceptance; -import static com.woowacourse.gongcheck.acceptance.AuthSupport.토큰을_요청한다; import static org.assertj.core.api.Assertions.assertThat; -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; -import com.woowacourse.gongcheck.presentation.request.SubmissionRequest; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.core.presentation.request.SubmissionRequest; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; @@ -15,14 +14,14 @@ class SubmissionAcceptanceTest extends AcceptanceTest { @Test - void 현재_진행중인_작업이_모두_완료된_상태로_제출한다() { + void RunningTask가_모두_체크된_상테로_Submission을_생성한다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_요청한다(guestEnterRequest); - 새로운_진행작업을_생성한다(token); - 체크박스를_체크한다(token, 1L); - 체크박스를_체크한다(token, 2L); - 체크박스를_체크한다(token, 3L); - 체크박스를_체크한다(token, 4L); + RunningTask를_생성한다(token); + 체크상태를_변경한다(token, 1L); + 체크상태를_변경한다(token, 2L); + 체크상태를_변경한다(token, 3L); + 체크상태를_변경한다(token, 4L); SubmissionRequest submissionRequest = new SubmissionRequest("제출자"); ExtractableResponse response = RestAssured @@ -38,11 +37,11 @@ class SubmissionAcceptanceTest extends AcceptanceTest { } @Test - void 현재_진행중인_작업을_미완료_상태로_제출을_시도할_경우_실패한다() { + void RunningTask가_모두_체크되지_않은_상태로_Submission을_생성할_수_없다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_요청한다(guestEnterRequest); - 새로운_진행작업을_생성한다(token); - 체크박스를_체크한다(token, 1L); + RunningTask를_생성한다(token); + 체크상태를_변경한다(token, 1L); SubmissionRequest submissionRequest = new SubmissionRequest("제출자"); ExtractableResponse response = RestAssured @@ -57,15 +56,15 @@ class SubmissionAcceptanceTest extends AcceptanceTest { assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } - private void 새로운_진행작업을_생성한다(final String token) { + private void RunningTask를_생성한다(final String token) { RestAssured .given().log().all() .auth().oauth2(token) - .when().post("/api/jobs/1/tasks/new") + .when().post("/api/jobs/1/runningTasks/new") .then().log().all(); } - private void 체크박스를_체크한다(final String token, final Long taskId) { + private void 체크상태를_변경한다(final String token, final Long taskId) { RestAssured .given().log().all() .auth().oauth2(token) diff --git a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/TaskAcceptanceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/TaskAcceptanceTest.java index 9cbb70f0..157bab1e 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/acceptance/TaskAcceptanceTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/acceptance/TaskAcceptanceTest.java @@ -1,11 +1,11 @@ package com.woowacourse.gongcheck.acceptance; -import static com.woowacourse.gongcheck.acceptance.AuthSupport.토큰을_요청한다; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; -import com.woowacourse.gongcheck.application.response.RunningTasksResponse; -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.core.application.response.RunningTasksResponse; +import com.woowacourse.gongcheck.core.application.response.TasksResponse; import io.restassured.RestAssured; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; @@ -13,16 +13,16 @@ import org.springframework.http.HttpStatus; class TaskAcceptanceTest extends AcceptanceTest { - + @Test - void 새로운_진행작업을_생성한다() { + void RunningTask를_생성한다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_요청한다(guestEnterRequest); ExtractableResponse response = RestAssured .given().log().all() .auth().oauth2(token) - .when().post("/api/jobs/1/tasks/new") + .when().post("/api/jobs/1/runningTasks/new") .then().log().all() .extract(); @@ -30,21 +30,21 @@ class TaskAcceptanceTest extends AcceptanceTest { } @Test - void 이미_존재하는_진행작업이_있는데_생성하는_경우_실패한다() { + void 이미_RunningTask가_존재하는_경우_생성에_실패한다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_요청한다(guestEnterRequest); RestAssured .given().log().all() .auth().oauth2(token) - .when().post("/api/jobs/1/tasks/new") + .when().post("/api/jobs/1/runningTasks/new") .then().log().all() .extract(); ExtractableResponse response = RestAssured .given().log().all() .auth().oauth2(token) - .when().post("/api/jobs/1/tasks/new") + .when().post("/api/jobs/1/runningTasks/new") .then().log().all() .extract(); @@ -52,20 +52,20 @@ class TaskAcceptanceTest extends AcceptanceTest { } @Test - void 진행중인_작업을_조회한다() { + void RunningTask를_조회한다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_요청한다(guestEnterRequest); RestAssured .given().log().all() .auth().oauth2(token) - .when().post("/api/jobs/1/tasks/new") + .when().post("/api/jobs/1/runningTasks/new") .then().log().all() .extract(); ExtractableResponse response = RestAssured .given().log().all() .auth().oauth2(token) - .when().get("/api/jobs/1/tasks") + .when().get("/api/jobs/1/runningTasks") .then().log().all() .extract(); RunningTasksResponse runningTasksResponse = response.as(RunningTasksResponse.class); @@ -77,14 +77,14 @@ class TaskAcceptanceTest extends AcceptanceTest { } @Test - void 진행_작업을_생성하고_작업의_진행여부를_확인한다() { + void RunningTask를_생성하고_존재_여부를_확인한다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_요청한다(guestEnterRequest); RestAssured .given().log().all() .auth().oauth2(token) - .when().post("/api/jobs/1/tasks/new") + .when().post("/api/jobs/1/runningTasks/new") .then().log().all() .extract(); @@ -99,7 +99,7 @@ class TaskAcceptanceTest extends AcceptanceTest { } @Test - void 진행_작업이_없는_경우에_작업의_진행여부를_확인한다() { + void RunningTask의_존재여부를_확인한다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_요청한다(guestEnterRequest); @@ -114,14 +114,14 @@ class TaskAcceptanceTest extends AcceptanceTest { } @Test - void 진행중인_작업이_존재하지_않는데_조회할_경우_예외가_발생한다() { + void 존재하지_않는_RunningTask를_조회하려는_경우_예외가_발생한다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_요청한다(guestEnterRequest); ExtractableResponse response = RestAssured .given().log().all() .auth().oauth2(token) - .when().get("/api/jobs/1/tasks") + .when().get("/api/jobs/1/runningTasks") .then().log().all() .extract(); @@ -129,13 +129,13 @@ class TaskAcceptanceTest extends AcceptanceTest { } @Test - void 진행중인_작업의_체크_상태를_변환한다() { + void RunningTask의_체크상태를_변경한다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_요청한다(guestEnterRequest); RestAssured .given().log().all() .auth().oauth2(token) - .when().post("/api/jobs/1/tasks/new") + .when().post("/api/jobs/1/runningTasks/new") .then().log().all() .extract(); @@ -150,7 +150,7 @@ class TaskAcceptanceTest extends AcceptanceTest { } @Test - void 진행중이지_않은_작업을_체크시도할_경우_예외가_발생한다() { + void 존재하지_않는_RunningTask의_체크상태를_변경하려는_경우_예외가_발생한다() { GuestEnterRequest guestEnterRequest = new GuestEnterRequest("1234"); String token = 토큰을_요청한다(guestEnterRequest); @@ -163,4 +163,22 @@ class TaskAcceptanceTest extends AcceptanceTest { assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } + + @Test + void Task를_조회한다() { + String token = Host_토큰을_요청한다().getToken(); + + ExtractableResponse response = RestAssured + .given().log().all() + .auth().oauth2(token) + .when().get("/api/jobs/1/tasks") + .then().log().all() + .extract(); + TasksResponse taskResponse = response.as(TasksResponse.class); + + assertAll( + () -> assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()), + () -> assertThat(taskResponse.getSections()).hasSize(2) + ); + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/application/GithubOauthClientTest.java b/backend/src/test/java/com/woowacourse/gongcheck/application/GithubOauthClientTest.java deleted file mode 100644 index c92a57f9..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/application/GithubOauthClientTest.java +++ /dev/null @@ -1,118 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.woowacourse.gongcheck.application.response.GithubAccessTokenResponse; -import com.woowacourse.gongcheck.application.response.GithubProfileResponse; -import com.woowacourse.gongcheck.exception.NotFoundException; -import com.woowacourse.gongcheck.exception.UnauthorizedException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.test.web.client.MockRestServiceServer; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.client.RestTemplate; - -@SpringBootTest -@Transactional -class GithubOauthClientTest { - - private MockRestServiceServer mockRestServiceServer; - - @Autowired - private ObjectMapper objectMapper; - - @Autowired - private RestTemplate restTemplate; - - @Autowired - private GithubOauthClient githubOauthClient; - - @BeforeEach - void setUp() { - mockRestServiceServer = MockRestServiceServer.createServer(restTemplate); - } - - @Test - void code를_받아_access_token을_반환한다() throws JsonProcessingException { - GithubAccessTokenResponse token = new GithubAccessTokenResponse("access_token"); - mockRestServiceServer.expect(requestTo("https://github.com/login/oauth/access_token")) - .andExpect(method(HttpMethod.POST)) - .andRespond(withStatus(HttpStatus.OK) - .contentType(MediaType.APPLICATION_JSON) - .body(objectMapper.writeValueAsString(token))); - - String result = githubOauthClient.requestAccessToken("code"); - mockRestServiceServer.verify(); - - assertThat(token.getAccessToken()).isEqualTo(result); - } - - @Test - void 깃허브_access_Token_요청에_실패하면_예외가_발생한다() { - mockRestServiceServer.expect(requestTo("https://github.com/login/oauth/access_token")) - .andExpect(method(HttpMethod.POST)) - .andRespond(withStatus(HttpStatus.NOT_FOUND) - .contentType(MediaType.APPLICATION_JSON)); - - assertThatThrownBy(() -> githubOauthClient.requestAccessToken("code")) - .isInstanceOf(NotFoundException.class) - .hasMessage("해당 사용자의 프로필을 요청할 수 없습니다."); - mockRestServiceServer.verify(); - } - - @Test - void 올바르지_않은_code이면_예외가_발생한다() throws JsonProcessingException { - mockRestServiceServer.expect(requestTo("https://github.com/login/oauth/access_token")) - .andExpect(method(HttpMethod.POST)) - .andRespond(withStatus(HttpStatus.OK) - .contentType(MediaType.APPLICATION_JSON) - .body(objectMapper.writeValueAsString(null))); - - assertThatThrownBy(() -> githubOauthClient.requestAccessToken("code")) - .isInstanceOf(UnauthorizedException.class) - .hasMessage("잘못된 요청입니다."); - mockRestServiceServer.verify(); - } - - @Test - void 깃허브_프로필을_요청한다() throws JsonProcessingException { - GithubProfileResponse githubProfileResponse = new GithubProfileResponse("nickname", "loginName", "1", - "test.com"); - mockRestServiceServer.expect(requestTo("https://api.github.com/user")) - .andExpect(method(HttpMethod.GET)) - .andRespond(withStatus(HttpStatus.OK) - .contentType(MediaType.APPLICATION_JSON) - .body(objectMapper.writeValueAsString(githubProfileResponse))); - - GithubProfileResponse result = githubOauthClient.requestGithubProfile("access_token"); - mockRestServiceServer.verify(); - - assertThat(githubProfileResponse).usingRecursiveComparison() - .isEqualTo(result); - } - - @Test - void 깃허브_프로필_요청에_실패하면_예외가_발생한다() throws JsonProcessingException { - mockRestServiceServer.expect(requestTo("https://api.github.com/user")) - .andExpect(method(HttpMethod.GET)) - .andRespond(withStatus(HttpStatus.NOT_FOUND) - .contentType(MediaType.APPLICATION_JSON)); - - assertThatThrownBy(() -> githubOauthClient - .requestGithubProfile("access_token")) - .isInstanceOf(NotFoundException.class) - .hasMessage("해당 사용자의 프로필을 요청할 수 없습니다."); - mockRestServiceServer.verify(); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/application/GuestAuthServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/application/GuestAuthServiceTest.java deleted file mode 100644 index 770c90f7..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/application/GuestAuthServiceTest.java +++ /dev/null @@ -1,56 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.woowacourse.gongcheck.application.response.GuestTokenResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.exception.NotFoundException; -import com.woowacourse.gongcheck.exception.UnauthorizedException; -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -@SpringBootTest -@Transactional -class GuestAuthServiceTest { - - @Autowired - private GuestAuthService guestAuthService; - - @Autowired - private HostRepository hostRepository; - - @Nested - class 토큰_발급_시 { - - @Test - void 해당하는_호스트가_존재하지_않으면_예외가_발생한다() { - assertThatThrownBy(() -> guestAuthService.createToken(0L, new GuestEnterRequest("1234"))) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - - @Test - void 비밀번호가_틀리면_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("0123", 1234L)); - - assertThatThrownBy(() -> guestAuthService.createToken(host.getId(), new GuestEnterRequest("1234"))) - .isInstanceOf(UnauthorizedException.class) - .hasMessage("공간 비밀번호와 입력하신 비밀번호가 일치하지 않습니다."); - } - - @Test - void 정상적으로_토큰을_발행한다() { - Host host = hostRepository.save(Host_생성("0123", 1234L)); - GuestTokenResponse token = guestAuthService.createToken(host.getId(), new GuestEnterRequest("0123")); - - assertThat(token.getToken()).isNotNull(); - } - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/application/HostAuthServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/application/HostAuthServiceTest.java deleted file mode 100644 index cbcbcc07..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/application/HostAuthServiceTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - -import com.woowacourse.gongcheck.application.response.GithubProfileResponse; -import com.woowacourse.gongcheck.application.response.TokenResponse; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.presentation.request.TokenRequest; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.transaction.annotation.Transactional; - -@SpringBootTest -@Transactional -class HostAuthServiceTest { - - @Autowired - private HostAuthService hostAuthService; - - @MockBean - private GithubOauthClient githubOauthClient; - - @Autowired - private HostRepository hostRepository; - - @MockBean - private JwtTokenProvider jwtTokenProvider; - - @Nested - class code를_받아_token을_반환한다 { - - @Test - void 존재하지_않는_Host이면_alreadyJoin이_false이다() { - GithubProfileResponse response = new GithubProfileResponse("nickname", "loginName", "1234", - "test.com"); - when(githubOauthClient.requestGithubProfile(any())).thenReturn(response); - when(jwtTokenProvider.createToken(any(), any())).thenReturn("jwt.token.here"); - - TokenResponse result = hostAuthService.createToken(new TokenRequest("code")); - - assertThat(result) - .extracting("token", "alreadyJoin") - .containsExactly("jwt.token.here", false); - } - - @Test - void 이미_존재하는_Host이면_alreadyJoin이_true이다() { - hostRepository.save(Host_생성("1234", 1234L)); - GithubProfileResponse response = new GithubProfileResponse("nickname", "loginName", "1234", - "test.com"); - when(githubOauthClient.requestGithubProfile(any())).thenReturn(response); - when(jwtTokenProvider.createToken(any(), any())).thenReturn("jwt.token.here"); - - TokenResponse result = hostAuthService.createToken(new TokenRequest("code")); - - assertThat(result) - .extracting("token", "alreadyJoin") - .containsExactly("jwt.token.here", true); - } - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/application/HostServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/application/HostServiceTest.java deleted file mode 100644 index 18a6a3a8..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/application/HostServiceTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; - -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.host.SpacePassword; -import com.woowacourse.gongcheck.exception.NotFoundException; -import com.woowacourse.gongcheck.presentation.request.SpacePasswordChangeRequest; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -@SpringBootTest -@Transactional -class HostServiceTest { - - @Autowired - private HostService hostService; - - @Autowired - private HostRepository hostRepository; - - @Test - void SpacePassword를_변경한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - - String changedPassword = "4567"; - hostService.changeSpacePassword(host.getId(), new SpacePasswordChangeRequest(changedPassword)); - - Assertions.assertThat(hostRepository.getById(host.getId()).getSpacePassword()) - .isEqualTo(new SpacePassword(changedPassword)); - } - - @Test - void 존재하지_않는_Host의_SpacePassword를_변경하려는_경우_예외가_발생한다() { - SpacePasswordChangeRequest request = new SpacePasswordChangeRequest("1234"); - Assertions.assertThatThrownBy(() -> hostService.changeSpacePassword(0L, request)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/application/JobServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/application/JobServiceTest.java deleted file mode 100644 index 4ab99fc8..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/application/JobServiceTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.woowacourse.gongcheck.application.response.JobsResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.space.SpaceRepository; -import com.woowacourse.gongcheck.exception.NotFoundException; -import com.woowacourse.gongcheck.presentation.request.JobCreateRequest; -import com.woowacourse.gongcheck.presentation.request.SectionCreateRequest; -import com.woowacourse.gongcheck.presentation.request.TaskCreateRequest; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.domain.PageRequest; -import org.springframework.transaction.annotation.Transactional; - -@SpringBootTest -@Transactional -class JobServiceTest { - - @Autowired - private JobService jobService; - - @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Test - void Job_목록을_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job1 = Job_생성(space, "오픈"); - Job job2 = Job_생성(space, "청소"); - Job job3 = Job_생성(space, "마감"); - jobRepository.saveAll(List.of(job1, job2, job3)); - - JobsResponse result = jobService.findPage(host.getId(), space.getId(), PageRequest.of(0, 2)); - - assertAll( - () -> assertThat(result.getJobs()).hasSize(2), - () -> assertThat(result.isHasNext()).isTrue() - ); - } - - @Test - void 존재하지_않는_Host로_Job_목록을_조회할_경우_예외를_던진다() { - assertThatThrownBy(() -> jobService.findPage(0L, 1L, PageRequest.of(0, 1))) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - - @Test - void 존재하지_않는_Space의_Job_목록을_조회할_경우_예외를_던진다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - - assertThatThrownBy(() -> jobService.findPage(host.getId(), 0L, PageRequest.of(0, 1))) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 공간입니다."); - } - - @Test - void 다른_Host의_Space의_Job_목록을_조회할_경우_예외를_던진다() { - Host host1 = hostRepository.save(Host_생성("1234", 1234L)); - Host host2 = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host2, "잠실")); - - assertThatThrownBy(() -> jobService.findPage(host1.getId(), space.getId(), PageRequest.of(0, 1))) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 공간입니다."); - } - - @Test - void Job과_Section들과_Task들을_한_번에_생성한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - List tasks = List.of(new TaskCreateRequest("책상 닦기"), new TaskCreateRequest("칠판 닦기")); - List sections = List.of(new SectionCreateRequest("대강의실", tasks)); - JobCreateRequest jobCreateRequest = new JobCreateRequest("청소", sections); - - Long savedJobId = jobService.createJob(host.getId(), space.getId(), jobCreateRequest); - - assertThat(savedJobId).isNotNull(); - } - - @Test - void Host가_존재하지_않는데_Job_생성_시_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - - List tasks = List.of(new TaskCreateRequest("책상 닦기"), new TaskCreateRequest("칠판 닦기")); - List sections = List.of(new SectionCreateRequest("대강의실", tasks)); - JobCreateRequest jobCreateRequest = new JobCreateRequest("청소", sections); - - assertThatThrownBy(() -> jobService.createJob(0L, space.getId(), jobCreateRequest)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - - @Test - void Space가_존재하지_않는데_Job_생성_시_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - - List tasks = List.of(new TaskCreateRequest("책상 닦기"), new TaskCreateRequest("칠판 닦기")); - List sections = List.of(new SectionCreateRequest("대강의실", tasks)); - JobCreateRequest jobCreateRequest = new JobCreateRequest("청소", sections); - - assertThatThrownBy(() -> jobService.createJob(host.getId(), 0L, jobCreateRequest)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 공간입니다."); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/application/SpaceServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/application/SpaceServiceTest.java deleted file mode 100644 index 390a670d..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/application/SpaceServiceTest.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.woowacourse.gongcheck.application.response.SpacesResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.section.Section; -import com.woowacourse.gongcheck.domain.section.SectionRepository; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.space.SpaceRepository; -import com.woowacourse.gongcheck.domain.task.RunningTask; -import com.woowacourse.gongcheck.domain.task.RunningTaskRepository; -import com.woowacourse.gongcheck.domain.task.Task; -import com.woowacourse.gongcheck.domain.task.TaskRepository; -import com.woowacourse.gongcheck.exception.BusinessException; -import com.woowacourse.gongcheck.exception.NotFoundException; -import com.woowacourse.gongcheck.presentation.request.SpaceCreateRequest; -import java.util.List; -import javax.persistence.EntityManager; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.data.domain.PageRequest; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.transaction.annotation.Transactional; - -@SpringBootTest -@Transactional -class SpaceServiceTest { - - @Autowired - EntityManager entityManager; - - @Autowired - private SpaceService spaceService; - - @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TaskRepository taskRepository; - - @Autowired - private RunningTaskRepository runningTaskRepository; - - @Nested - class 공간_조회 { - - @Test - void 공간을_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space1 = Space_생성(host, "잠실"); - Space space2 = Space_생성(host, "선릉"); - Space space3 = Space_생성(host, "양평같은방"); - spaceRepository.saveAll(List.of(space1, space2, space3)); - - SpacesResponse result = spaceService.findPage(host.getId(), PageRequest.of(0, 2)); - - assertAll( - () -> assertThat(result.getSpaces()).hasSize(2), - () -> assertThat(result.isHasNext()).isTrue() - ); - } - - @Test - void 존재하지_않는_호스트로_공간을_조회할_경우_예외를_던진다() { - assertThatThrownBy(() -> spaceService.findPage(0L, PageRequest.of(0, 1))) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - } - - @Nested - class 공간_생성 { - - @Test - void 공간을_생성한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - SpaceCreateRequest spaceCreateRequest = new SpaceCreateRequest("잠실 캠퍼스", - new MockMultipartFile("잠실 캠퍼스 사진", new byte[]{})); - Long spaceId = spaceService.createSpace(host.getId(), spaceCreateRequest); - - assertThat(spaceId).isNotNull(); - } - - @Test - void 이미_존재하는_공간_이름을_입력할_경우_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - String spaceName = "잠실 캠퍼스"; - Space space = Space_생성(host, spaceName); - spaceRepository.save(space); - - SpaceCreateRequest request = new SpaceCreateRequest(spaceName, - new MockMultipartFile("잠실 캠퍼스 사진", new byte[]{})); - - assertThatThrownBy(() -> spaceService.createSpace(host.getId(), request)) - .isInstanceOf(BusinessException.class) - .hasMessage("이미 존재하는 이름입니다."); - } - - @Test - void 존재하지_않는_호스트로_생성하려는_경우_예외가_발생한다() { - SpaceCreateRequest request = new SpaceCreateRequest("잠실 캠퍼스", - new MockMultipartFile("잠실 캠퍼스 사진", new byte[]{})); - - assertThatThrownBy(() -> spaceService.createSpace(0L, request)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - } - - @Test - void Space를_삭제하면_관련된_Job_Section_Task_RunningTask를_함께_삭제한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section = sectionRepository.save(Section_생성(job, "대강의실")); - Task task = taskRepository.save(Task_생성(section, "책상 닦기")); - RunningTask runningTask = runningTaskRepository.save(RunningTask_생성(task.getId(), false)); - - spaceService.removeSpace(host.getId(), space.getId()); - - entityManager.flush(); - entityManager.clear(); - assertAll( - () -> assertThat(spaceRepository.findById(space.getId())).isEmpty(), - () -> assertThat(jobRepository.findById(job.getId())).isEmpty(), - () -> assertThat(sectionRepository.findById(section.getId())).isEmpty(), - () -> assertThat(taskRepository.findById(task.getId())).isEmpty(), - () -> assertThat(runningTaskRepository.findById(runningTask.getTaskId())).isEmpty() - ); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/application/SubmissionServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/application/SubmissionServiceTest.java deleted file mode 100644 index 85e26cfd..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/application/SubmissionServiceTest.java +++ /dev/null @@ -1,151 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.woowacourse.gongcheck.application.response.SubmissionResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.section.Section; -import com.woowacourse.gongcheck.domain.section.SectionRepository; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.space.SpaceRepository; -import com.woowacourse.gongcheck.domain.submission.SubmissionRepository; -import com.woowacourse.gongcheck.domain.task.RunningTask; -import com.woowacourse.gongcheck.domain.task.RunningTaskRepository; -import com.woowacourse.gongcheck.domain.task.Task; -import com.woowacourse.gongcheck.domain.task.TaskRepository; -import com.woowacourse.gongcheck.exception.BusinessException; -import com.woowacourse.gongcheck.exception.NotFoundException; -import com.woowacourse.gongcheck.presentation.request.SubmissionRequest; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -@SpringBootTest -@Transactional -class SubmissionServiceTest { - - @Autowired - private SubmissionService submissionService; - - @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TaskRepository taskRepository; - - @Autowired - private RunningTaskRepository runningTaskRepository; - - @Autowired - private SubmissionRepository submissionRepository; - - @Nested - class 제출_시도_시 { - - private final SubmissionRequest request = new SubmissionRequest("제출자"); - - private Host hostWithoutTasks; - private Host hostWithTasks; - private Space space; - private Job job; - private Section section; - private Task task1; - private Task task2; - - @BeforeEach - void setUp() { - hostWithoutTasks = hostRepository.save(Host_생성("1234", 1234L)); - hostWithTasks = hostRepository.save(Host_생성("1234", 2345L)); - space = spaceRepository.save(Space_생성(hostWithTasks, "잠실")); - job = jobRepository.save(Job_생성(space, "청소")); - section = sectionRepository.save(Section_생성(job, "트랙룸")); - task1 = Task_생성(section, "책상 청소"); - task2 = Task_생성(section, "의자 넣기"); - taskRepository.saveAll(List.of(task1, task2)); - } - - @Test - void 호스트가_존재하지_않는_경우_예외가_발생한다() { - assertThatThrownBy(() -> submissionService.submitJobCompletion(0L, 1L, request)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - - @Test - void 작업이_존재하지_않는_경우_예외가_발생한다() { - assertThatThrownBy(() -> submissionService.submitJobCompletion(hostWithTasks.getId(), 0L, request)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 다른_호스트의_작업으로_제출을_시도할_경우_예외가_발생한다() { - assertThatThrownBy( - () -> submissionService.submitJobCompletion(hostWithoutTasks.getId(), job.getId(), request)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 현재_진행중인_작업이_없는데_제출을_시도할_경우_예외가_발생한다() { - assertThatThrownBy(() -> submissionService.submitJobCompletion(hostWithTasks.getId(), job.getId(), request)) - .isInstanceOf(BusinessException.class) - .hasMessage("현재 제출할 수 있는 진행중인 작업이 존재하지 않습니다."); - } - - @Test - void 현재_진행중인_작업을_미완료_상태로_제출을_시도할_경우_예외가_발생한다() { - checkAllRunningTasks(false); - - assertThatThrownBy(() -> submissionService.submitJobCompletion(hostWithTasks.getId(), job.getId(), request)) - .isInstanceOf(BusinessException.class) - .hasMessage("모든 작업이 완료되지않아 제출이 불가합니다."); - } - - @Test - void 현재_진행중인_작업이_모두_완료된_상태로_제출한다() { - checkAllRunningTasks(true); - - SubmissionResponse submissionResponse = submissionService.submitJobCompletion(hostWithTasks.getId(), - job.getId(), request); - - assertAll( - () -> assertThat(submissionRepository.findAll().size()).isOne(), - () -> assertThat(runningTaskRepository.findAll().size()).isZero(), - () -> assertThat(submissionResponse.getAuthor()).isEqualTo(request.getAuthor()), - () -> assertThat(submissionResponse.getSpaceName()).isEqualTo(space.getName()), - () -> assertThat(submissionResponse.getJobName()).isEqualTo(job.getName()) - ); - } - - private void checkAllRunningTasks(final boolean isChecked) { - RunningTask runningTask1 = RunningTask_생성(task1.getId(), true); - RunningTask runningTask2 = RunningTask_생성(task2.getId(), isChecked); - runningTaskRepository.saveAll(List.of(runningTask1, runningTask2)); - } - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/application/TaskServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/application/TaskServiceTest.java deleted file mode 100644 index 766ee758..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/application/TaskServiceTest.java +++ /dev/null @@ -1,335 +0,0 @@ -package com.woowacourse.gongcheck.application; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.woowacourse.gongcheck.application.response.JobActiveResponse; -import com.woowacourse.gongcheck.application.response.RunningTasksResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.section.Section; -import com.woowacourse.gongcheck.domain.section.SectionRepository; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.space.SpaceRepository; -import com.woowacourse.gongcheck.domain.task.RunningTask; -import com.woowacourse.gongcheck.domain.task.RunningTaskRepository; -import com.woowacourse.gongcheck.domain.task.Task; -import com.woowacourse.gongcheck.domain.task.TaskRepository; -import com.woowacourse.gongcheck.exception.BusinessException; -import com.woowacourse.gongcheck.exception.NotFoundException; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.persistence.EntityManager; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.transaction.annotation.Transactional; - -@SpringBootTest -@Transactional -class TaskServiceTest { - - @Autowired - private TaskService taskService; - - @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TaskRepository taskRepository; - - @Autowired - private RunningTaskRepository runningTaskRepository; - - @Autowired - private EntityManager entityManager; - - @Test - void 존재하지_않는_호스트로_새로운_작업을_진행하려하는_경우_예외가_발생한다() { - assertThatThrownBy(() -> taskService.createNewRunningTasks(0L, 1L)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - - @Test - void 존재하지_않는_작업의_새로운_작업을_진행하려는_경우_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - - assertThatThrownBy(() -> taskService.createNewRunningTasks(host.getId(), 0L)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 다른_호스트의_작업의_새로운_작업을_진행하려는_경우_예외가_발생한다() { - Host host1 = hostRepository.save(Host_생성("1234", 1234L)); - Host host2 = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host2, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - - assertThatThrownBy(() -> taskService.createNewRunningTasks(host1.getId(), job.getId())) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 이미_진행중인_작업이_존재하는데_새로운_작업을_진행하려는_경우_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section = sectionRepository.save(Section_생성(job, "트랙룸")); - Task task1 = Task_생성(section, "책상 청소"); - Task task2 = Task_생성(section, "의자 넣기"); - taskRepository.saveAll(List.of(task1, task2)); - runningTaskRepository.saveAll( - List.of(RunningTask_생성(task1.getId(), true), - RunningTask_생성(task2.getId(), true))); - - assertThatThrownBy(() -> taskService.createNewRunningTasks(host.getId(), job.getId())) - .isInstanceOf(BusinessException.class) - .hasMessage("현재 진행중인 작업이 존재하여 새로운 작업을 생성할 수 없습니다."); - } - - @Test - void 정상적으로_새로운_진행_작업을_생성한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section = sectionRepository.save(Section_생성(job, "트랙룸")); - Task task1 = Task_생성(section, "책상 청소"); - Task task2 = Task_생성(section, "의자 넣기"); - taskRepository.saveAll(List.of(task1, task2)); - - taskService.createNewRunningTasks(host.getId(), job.getId()); - List result = runningTaskRepository.findAllById(Stream.of(task1, task2) - .map(Task::getId) - .collect(Collectors.toList())); - - assertThat(result).hasSize(2); - } - - @Nested - class 작업의_진행_여부는 { - private Host host; - private Space space; - private Job job; - private Section section; - - @BeforeEach - void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "잠실")); - job = jobRepository.save(Job_생성(space, "청소")); - section = sectionRepository.save(Section_생성(job, "트랙룸")); - taskRepository.saveAll(List.of(Task_생성(section, "책상 청소"), Task_생성(section, "의자 넣기"))); - } - - @Test - void 존재하지_않는_호스트로_확인하려는_경우_예외가_발생한다() { - assertThatThrownBy(() -> taskService.isJobActivated(0L, 1L)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - - @Test - void 존재하지_않는_작업으로_확인하려는_경우_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 2345L)); - - assertThatThrownBy(() -> taskService.isJobActivated(host.getId(), 0L)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 다른_호스트의_작업으로_확인하려는_경우_예외가_발생한다() { - Host host1 = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host1, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - - assertThatThrownBy(() -> taskService.isJobActivated(host.getId(), job.getId())) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 진행_작업이_존재하는_경우_참을_반환한다() { - taskService.createNewRunningTasks(host.getId(), job.getId()); - - JobActiveResponse result = taskService.isJobActivated(host.getId(), job.getId()); - - assertThat(result.isActive()).isTrue(); - } - - @Test - void 진행_작업이_존재하지_않는_경우_거짓을_반환한다() { - JobActiveResponse result = taskService.isJobActivated(host.getId(), job.getId()); - - assertThat(result.isActive()).isFalse(); - } - } - - @Nested - class 진행_작업_조회 { - - private Host host; - private Space space; - private Job job; - private Section section; - private Task task1, task2; - - @BeforeEach - void init() { - host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "잠실")); - job = jobRepository.save(Job_생성(space, "청소")); - section = sectionRepository.save(Section_생성(job, "트랙룸")); - task1 = Task_생성(section, "책상 청소"); - task2 = Task_생성(section, "의자 넣기"); - } - - @Test - void 존재하지_않는_호스트로_진행중인_작업을_조회하려하는_경우_예외가_발생한다() { - assertThatThrownBy(() -> taskService.findRunningTasks(0L, 1L)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - - @Test - void 존재하지_않는_작업의_진행중인_작업을_조회하려는_경우_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 2345L)); - - assertThatThrownBy(() -> taskService.findRunningTasks(host.getId(), 0L)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 다른_호스트의_작업의_진행중인_작업을_조회하려는_경우_예외가_발생한다() { - Host differentHost = hostRepository.save(Host_생성("1234", 2345L)); - taskRepository.saveAll(List.of(task1, task2)); - RunningTask runningTask1 = RunningTask_생성(task1.getId(), false); - RunningTask runningTask2 = RunningTask_생성(task2.getId(), false); - runningTaskRepository.saveAll(List.of(runningTask1, runningTask2)); - entityManager.flush(); - entityManager.clear(); - - assertThatThrownBy(() -> taskService.findRunningTasks(differentHost.getId(), job.getId())) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 진행중인_작업이_없는데_진행중인_작업을_조회하려는_경우_예외가_발생한다() { - taskRepository.saveAll(List.of(task1, task2)); - - assertThatThrownBy(() -> taskService.findRunningTasks(host.getId(), job.getId())) - .isInstanceOf(BusinessException.class) - .hasMessage("현재 진행중인 작업이 존재하지 않아 조회할 수 없습니다"); - } - - @Test - void 정상적으로_진행중인_작업을_조회한다() { - taskRepository.saveAll(List.of(task1, task2)); - RunningTask runningTask1 = RunningTask_생성(task1.getId(), false); - RunningTask runningTask2 = RunningTask_생성(task2.getId(), false); - runningTaskRepository.saveAll(List.of(runningTask1, runningTask2)); - entityManager.flush(); - entityManager.clear(); - - RunningTasksResponse result = taskService.findRunningTasks(host.getId(), job.getId()); - assertThat(result.getSections()).hasSize(1); - } - } - - @Test - void 진행_작업_체크_시_진행_작업이_존재하지_않을_경우_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section = sectionRepository.save(Section_생성(job, "트랙룸")); - Task task = Task_생성(section, "책상 청소"); - taskRepository.save(task); - - assertThatThrownBy(() -> taskService.flipRunningTask(host.getId(), task.getId())) - .isInstanceOf(BusinessException.class) - .hasMessage("현재 진행 중인 작업이 아닙니다."); - } - - @Test - void 진행_작업_체크_시_입력한_host의_진행_작업이_아닐_경우_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section = sectionRepository.save(Section_생성(job, "트랙룸")); - Task task = Task_생성(section, "책상 청소"); - - Host differentHost = hostRepository.save(Host_생성("1234", 2345L)); - Space differentSpace = spaceRepository.save(Space_생성(differentHost, "선릉")); - Job differentJob = jobRepository.save(Job_생성(differentSpace, "청소")); - Section differentSection = sectionRepository.save(Section_생성(differentJob, "트랙룸")); - Task differentTask = Task_생성(differentSection, "책상 청소"); - - taskRepository.save(task); - taskRepository.save(differentTask); - runningTaskRepository.save(RunningTask_생성(task.getId(), false)); - runningTaskRepository.save(RunningTask_생성(differentTask.getId(), false)); - - assertThatThrownBy(() -> taskService.flipRunningTask(differentHost.getId(), task.getId())) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 진행_작업_체크_시_Host가_존재하지_않는_경우_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section = sectionRepository.save(Section_생성(job, "트랙룸")); - Task task = Task_생성(section, "책상 청소"); - taskRepository.save(task); - runningTaskRepository.save(RunningTask_생성(task.getId(), false)); - - assertThatThrownBy(() -> taskService.flipRunningTask(0L, task.getId())) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - - @ParameterizedTest - @CsvSource(value = {"false:true", "true:false"}, delimiter = ':') - void 진행_작업의_상태를_변경한다(final boolean input, final boolean expected) { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section = sectionRepository.save(Section_생성(job, "트랙룸")); - Task task = Task_생성(section, "책상 청소"); - taskRepository.save(task); - runningTaskRepository.save(RunningTask_생성(task.getId(), input)); - - taskService.flipRunningTask(host.getId(), task.getId()); - - RunningTask runningTask = runningTaskRepository.findByTaskId(task.getId()).get(); - assertThat(runningTask.isChecked()).isEqualTo(expected); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProviderTest.java b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProviderTest.java new file mode 100644 index 00000000..5e995f0d --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/EntranceCodeProviderTest.java @@ -0,0 +1,116 @@ +package com.woowacourse.gongcheck.auth.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.gongcheck.exception.BusinessException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@DisplayName("EntranceCodeProvider 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class EntranceCodeProviderTest { + + @Autowired + private EntranceCodeProvider entranceCodeProvider; + + @Autowired + private HashTranslator hashTranslator; + + @Nested + class createEntranceCode_메서드는 { + + @Nested + class 입력받은_id가_양수가_아닌_경우 { + + @ParameterizedTest + @ValueSource(longs = {-1L, 0L}) + void 예외를_발생시킨다(final Long hostId) { + assertThatThrownBy(() -> entranceCodeProvider.createEntranceCode(hostId)) + .isInstanceOf(BusinessException.class) + .hasMessage("유효하지 않은 id입니다."); + } + } + + @Nested + class id를_입력받는_경우 { + + private static final long ID = 1L; + + @Test + void 입장코드를_반환한다() { + String entranceCode = entranceCodeProvider.createEntranceCode(ID); + assertThat(entranceCode).isNotNull(); + } + } + } + + @Nested + class parseId_메서드는 { + + @Nested + class 입력받은_입장코드가_id로_변환될_수_없는_경우 { + + private String invalidEntranceCode; + + @BeforeEach + void setUp() { + invalidEntranceCode = hashTranslator.encode("INVALID"); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> entranceCodeProvider.parseId(invalidEntranceCode)) + .isInstanceOf(BusinessException.class) + .hasMessage("유효하지 않은 입장코드입니다."); + } + } + + @Nested + class 변환한_id가_유효하지_않은_경우 { + + private String invalidEntranceCode; + + @BeforeEach + void setUp() { + invalidEntranceCode = hashTranslator.encode("-1"); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> entranceCodeProvider.parseId(invalidEntranceCode)) + .isInstanceOf(BusinessException.class) + .hasMessage("유효하지 않은 입장코드입니다."); + } + } + + @Nested + class 유효한_입장코드를_입력받은_경우 { + + private static final long expected = 1L; + + private String entranceCode; + + @BeforeEach + void setUp() { + entranceCode = hashTranslator.encode(String.valueOf(expected)); + } + + @Test + void id를_반환한다() { + Long actual = entranceCodeProvider.parseId(entranceCode); + + assertThat(actual).isEqualTo(expected); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/auth/application/GuestAuthServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/GuestAuthServiceTest.java new file mode 100644 index 00000000..e0502b8f --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/GuestAuthServiceTest.java @@ -0,0 +1,103 @@ +package com.woowacourse.gongcheck.auth.application; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.gongcheck.auth.application.response.GuestTokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.NotFoundException; +import com.woowacourse.gongcheck.exception.UnauthorizedException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@DisplayName("GuestAuthService 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class GuestAuthServiceTest { + + @Autowired + private GuestAuthService guestAuthService; + + @Autowired + private HostRepository hostRepository; + + @Nested + class createToken_메소드는 { + + @Nested + class 존재하는_Host의_id와_정확한_password를_입력하는_경우 { + + private static final String CORRECT_PASSWORD = "1234"; + private Long hostId; + private GuestEnterRequest guestEnterRequest; + + @BeforeEach + void setUp() { + hostId = hostRepository.save(Host_생성(CORRECT_PASSWORD, 1234L)) + .getId(); + guestEnterRequest = new GuestEnterRequest(CORRECT_PASSWORD); + } + + @Test + void Space_사용을_위한_token을_발행한다() { + GuestTokenResponse token = guestAuthService.createToken(hostId, guestEnterRequest); + + assertThat(token.getToken()).isNotNull(); + } + } + + @Nested + class 존재하지_않는_Host의_id를_받는_경우 { + + private static final String CORRECT_PASSWORD = "1234"; + private static final long NON_EXIST_ID = 0L; + private GuestEnterRequest guestEnterRequest; + + @BeforeEach + void setUp() { + guestEnterRequest = new GuestEnterRequest(CORRECT_PASSWORD); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> guestAuthService.createToken(NON_EXIST_ID, guestEnterRequest)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 잘못된_password를_입력하는_경우 { + + private static final String CORRECT_PASSWORD = "1234"; + private static final String ERROR_PASSWORD = "4567"; + private Long hostId; + private GuestEnterRequest errorGuestEnterRequest; + + @BeforeEach + void setUp() { + hostId = hostRepository.save(Host_생성(CORRECT_PASSWORD, 1234L)) + .getId(); + errorGuestEnterRequest = new GuestEnterRequest(ERROR_PASSWORD); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> guestAuthService.createToken(hostId, errorGuestEnterRequest)) + .isInstanceOf(BusinessException.class) + .hasMessage("공간 비밀번호와 입력하신 비밀번호가 일치하지 않습니다."); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/auth/application/HostAuthServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/HostAuthServiceTest.java new file mode 100644 index 00000000..8c46bf42 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/auth/application/HostAuthServiceTest.java @@ -0,0 +1,127 @@ +package com.woowacourse.gongcheck.auth.application; + +import static com.woowacourse.gongcheck.auth.domain.Authority.HOST; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import com.woowacourse.gongcheck.auth.application.response.GithubProfileResponse; +import com.woowacourse.gongcheck.auth.application.response.TokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.TokenRequest; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.exception.UnauthorizedException; +import com.woowacourse.gongcheck.infrastructure.oauth.GithubOauthClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@DisplayName("HostAuthService 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HostAuthServiceTest { + + @Autowired + private HostAuthService hostAuthService; + + @MockBean + private GithubOauthClient githubOauthClient; + + @Autowired + private HostRepository hostRepository; + + @MockBean + private JwtTokenProvider jwtTokenProvider; + + @Nested + class createToken_메소드는 { + + private static final String GITHUB_NICKNAME = "jinyoungchoi95"; + private static final String GITHUB_LOGIN_NAME = "jinyoungchoi95"; + private static final String GITHUB_ID = "1234567"; + private static final String GITHUB_IMAGE_URL = "https://github.com"; + + @Nested + class 새로_가입한_Host의_oauth_토큰_코드가_입력될_경우 { + + private static final String OAUTH_CODE = "1234"; + private static final String JWT_ACCESS_TOKEN = "jwt.token.here"; + private TokenRequest tokenRequest; + + @BeforeEach + void setUp() { + when(githubOauthClient.requestGithubProfileByCode(OAUTH_CODE)) + .thenReturn(new GithubProfileResponse(GITHUB_NICKNAME, GITHUB_LOGIN_NAME, GITHUB_ID, + GITHUB_IMAGE_URL)); + when(jwtTokenProvider.createToken(any(), eq(HOST))).thenReturn(JWT_ACCESS_TOKEN); + tokenRequest = new TokenRequest(OAUTH_CODE); + } + + @Test + void 유저를_가입시키고_토큰과_기존유저가_아님을_반환한다() { + TokenResponse actual = hostAuthService.createToken(tokenRequest); + + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(new TokenResponse(JWT_ACCESS_TOKEN, false)); + } + } + + @Nested + class 기존에_가입한_Host의_oauth_토큰_코드가_입력될_경우 { + + private static final String OAUTH_CODE = "1234"; + private static final String JWT_ACCESS_TOKEN = "jwt.token.here"; + private TokenRequest tokenRequest; + + @BeforeEach + void setUp() { + when(githubOauthClient.requestGithubProfileByCode(OAUTH_CODE)) + .thenReturn(new GithubProfileResponse(GITHUB_NICKNAME, GITHUB_LOGIN_NAME, GITHUB_ID, + GITHUB_IMAGE_URL)); + when(jwtTokenProvider.createToken(any(), eq(HOST))).thenReturn(JWT_ACCESS_TOKEN); + hostRepository.save(Host_생성("1234", Long.parseLong(GITHUB_ID))); + tokenRequest = new TokenRequest(OAUTH_CODE); + } + + @Test + void 기존의_유저에_대한_토큰과_기존유저임을_반환한다() { + TokenResponse actual = hostAuthService.createToken(tokenRequest); + + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(new TokenResponse(JWT_ACCESS_TOKEN, true)); + } + } + + @Nested + class 잘못된_oauth_토큰_코드가_입력될_경우 { + + private static final String ERROR_OAUTH_CODE = "1234"; + private TokenRequest tokenRequest; + + @BeforeEach + void setUp() { + when(githubOauthClient.requestGithubProfileByCode(ERROR_OAUTH_CODE)) + .thenThrow(UnauthorizedException.class); + tokenRequest = new TokenRequest(ERROR_OAUTH_CODE); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> hostAuthService.createToken(tokenRequest)) + .isInstanceOf(UnauthorizedException.class); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/support/AuthorizationTokenExtractorTest.java b/backend/src/test/java/com/woowacourse/gongcheck/auth/support/JwtTokenExtractorTest.java similarity index 77% rename from backend/src/test/java/com/woowacourse/gongcheck/support/AuthorizationTokenExtractorTest.java rename to backend/src/test/java/com/woowacourse/gongcheck/auth/support/JwtTokenExtractorTest.java index 28d69a29..61a14ad4 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/support/AuthorizationTokenExtractorTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/auth/support/JwtTokenExtractorTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.support; +package com.woowacourse.gongcheck.auth.support; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -14,26 +14,26 @@ import org.springframework.http.HttpHeaders; @ExtendWith(MockitoExtension.class) -class AuthorizationTokenExtractorTest { +class JwtTokenExtractorTest { private final HttpServletRequest httpRequest = Mockito.mock(HttpServletRequest.class); @Test void Authorization_헤더가_비어있으면_빈_값을_반환한다() { when(httpRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(null); - assertThat(AuthorizationTokenExtractor.extractToken(httpRequest)).isEmpty(); + assertThat(JwtTokenExtractor.extractToken(httpRequest)).isEmpty(); } @ParameterizedTest @ValueSource(strings = {"", " ", "jwt.token.here", "Bearer ", "Digest jwt.token.here"}) void 토큰이_지정된_형식이_아닌_경우_빈_값을_반환한다(final String header) { when(httpRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(header); - assertThat(AuthorizationTokenExtractor.extractToken(httpRequest)).isEmpty(); + assertThat(JwtTokenExtractor.extractToken(httpRequest)).isEmpty(); } @Test void 토큰을_정상적으로_추출한다() { when(httpRequest.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer jwt.token.here"); - assertThat(AuthorizationTokenExtractor.extractToken(httpRequest)).isEqualTo(Optional.of("jwt.token.here")); + assertThat(JwtTokenExtractor.extractToken(httpRequest)).isEqualTo(Optional.of("jwt.token.here")); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java new file mode 100644 index 00000000..36c307b5 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/HostServiceTest.java @@ -0,0 +1,128 @@ +package com.woowacourse.gongcheck.core.application; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.gongcheck.auth.application.EntranceCodeProvider; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.presentation.request.SpacePasswordChangeRequest; +import com.woowacourse.gongcheck.exception.NotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@DisplayName("HostService 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HostServiceTest { + + @Autowired + private HostService hostService; + + @Autowired + private HostRepository hostRepository; + + @Autowired + private EntranceCodeProvider entranceCodeProvider; + + @Nested + class changeSpacePassword_메소드는 { + + @Nested + class 존재하는_Host의_id와_수정할_패스워드를_받는_경우 { + + private static final String ORIGIN_PASSWORD = "1234"; + private static final String CHANGING_PASSWORD = "4567"; + private static final long GITHUB_ID = 1234L; + + private SpacePasswordChangeRequest spacePasswordChangeRequest; + private Long hostId; + + @BeforeEach + void setUp() { + spacePasswordChangeRequest = new SpacePasswordChangeRequest(CHANGING_PASSWORD); + hostId = hostRepository.save(Host_생성(ORIGIN_PASSWORD, GITHUB_ID)) + .getId(); + } + + @Test + void 패스워드를_수정한다() { + hostService.changeSpacePassword(hostId, spacePasswordChangeRequest); + Host actual = hostRepository.getById(hostId); + + assertAll( + () -> assertThat(actual.getSpacePassword().getValue()).isEqualTo(CHANGING_PASSWORD), + () -> assertThat(actual.getGithubId()).isEqualTo(GITHUB_ID), + () -> assertThat(actual.getId()).isEqualTo(hostId) + ); + } + } + + @Nested + class 존재하지_않는_Host의_id를_받는_경우 { + + private static final String CHANGING_PASSWORD = "4567"; + + private SpacePasswordChangeRequest spacePasswordChangeRequest; + private Long hostId; + + @BeforeEach + void setUp() { + spacePasswordChangeRequest = new SpacePasswordChangeRequest(CHANGING_PASSWORD); + hostId = 0L; + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> hostService.changeSpacePassword(hostId, spacePasswordChangeRequest)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + } + + @Nested + class createEntranceCode_메소드는 { + + @Nested + class 존재하는_Host의_id를_받는_경우 { + + private Long hostId; + private String expected; + + @BeforeEach + void setUp() { + hostId = hostRepository.save(Host_생성("1234", 1111L)) + .getId(); + expected = entranceCodeProvider.createEntranceCode(hostId); + } + + @Test + void 입장코드를_반환한다() { + String actual = hostService.createEntranceCode(hostId); + assertThat(actual).isEqualTo(expected); + } + } + + @Nested + class 존재하지_않는_Host의_id를_받는_경우 { + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> hostService.createEntranceCode(0L)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/JobServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/JobServiceTest.java new file mode 100644 index 00000000..3abbd0e0 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/JobServiceTest.java @@ -0,0 +1,735 @@ +package com.woowacourse.gongcheck.core.application; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.gongcheck.core.application.response.JobResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.section.SectionRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTask; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.task.TaskRepository; +import com.woowacourse.gongcheck.core.presentation.request.JobCreateRequest; +import com.woowacourse.gongcheck.core.presentation.request.SectionCreateRequest; +import com.woowacourse.gongcheck.core.presentation.request.SlackUrlChangeRequest; +import com.woowacourse.gongcheck.core.presentation.request.TaskCreateRequest; +import com.woowacourse.gongcheck.exception.NotFoundException; +import java.util.List; +import java.util.stream.Collectors; +import javax.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@DisplayName("JobService 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class JobServiceTest { + + @Autowired + private EntityManager entityManager; + + @Autowired + private JobService jobService; + + @Autowired + private HostRepository hostRepository; + + @Autowired + private SpaceRepository spaceRepository; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TaskRepository taskRepository; + + @Autowired + private RunningTaskRepository runningTaskRepository; + + @Nested + class findJobs_메소드는 { + + @Nested + class Job_목록이_존재하는_경우 { + + private Host host; + private Space space; + private List jobNames; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실")); + List jobs = jobRepository.saveAll( + List.of(Job_생성(space, "오픈"), Job_생성(space, "청소"), Job_생성(space, "마감"))); + jobNames = jobs.stream() + .map(job -> job.getName().getValue()) + .collect(Collectors.toList()); + } + + @Test + void Job_목록을_조회한다() { + List result = jobService.findJobs(host.getId(), space.getId()).getJobs(); + + assertAll( + () -> assertThat(result) + .extracting(JobResponse::getName) + .containsAll(jobNames), + () -> assertThat(result).hasSize(jobNames.size()) + ); + } + } + + @Nested + class 존재하지_않는_Host로_조회할_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + private Host host; + private Space space; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실")); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> jobService.findJobs(NON_EXIST_HOST_ID, space.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 존재하지_않는_Space의_Job_목록을_조회할_경우 { + + private static final long NON_EXIST_SPACE_ID = 0L; + + private long hostId; + + @BeforeEach + void setUp() { + hostId = hostRepository.save(Host_생성("1234", 1234L)) + .getId(); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> jobService.findJobs(hostId, NON_EXIST_SPACE_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + + @Nested + class 다른_Host의_Space의_Job_목록을_조회할_경우 { + + private Host host; + private Space space; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Host otherHost = hostRepository.save(Host_생성("1234", 2345L)); + space = spaceRepository.save(Space_생성(otherHost, "잠실")); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> jobService.findJobs(host.getId(), space.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + } + + @Nested + class createJob_메소드는 { + + @Nested + class Job_Section들과_Task들을_입력_받는_경우 { + + private Host host; + private Space space; + private JobCreateRequest request; + private List taskCreateRequestNames; + private List sectionCreateRequestNames; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실")); + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List.of( + new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + sectionCreateRequestNames = sections.stream() + .map(SectionCreateRequest::getName) + .collect(Collectors.toList()); + taskCreateRequestNames = tasks.stream() + .map(TaskCreateRequest::getName) + .collect(Collectors.toList()); + request = new JobCreateRequest("청소", sections); + } + + @Test + void 한_번에_생성한다() { + long savedJobId = jobService.createJob(host.getId(), space.getId(), request); + + Job savedJob = jobRepository.getById(savedJobId); + List
savedSections = sectionRepository.findAllByJob(savedJob); + List savedTasks = taskRepository.findAllBySectionIn(savedSections); + + assertAll( + () -> assertThat(savedJob.getId()).isEqualTo(savedJobId), + () -> assertThat(savedTasks) + .extracting(task -> task.getName().getValue()) + .containsAll(taskCreateRequestNames), + () -> assertThat(savedSections) + .extracting(section -> section.getName().getValue()) + .containsAll(sectionCreateRequestNames) + ); + } + } + + @Nested + class 존재하지_않는_Host의_id를_입력받은_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + private Space space; + private JobCreateRequest request; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실")); + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List.of( + new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + request = new JobCreateRequest("청소", sections); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.createJob(NON_EXIST_HOST_ID, space.getId(), request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 존재하지_않는_Space의_id를_입력받은_경우 { + + private static final long NON_EXIST_SPACE_ID = 0L; + + private Host host; + private JobCreateRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List.of( + new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + request = new JobCreateRequest("청소", sections); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.createJob(host.getId(), NON_EXIST_SPACE_ID, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + + @Nested + class 다른_host의_space_id를_입력받은_경우 { + + private Host host; + private Space otherSpace; + private JobCreateRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + spaceRepository.save(Space_생성(host, "잠실")); + Host otherHost = hostRepository.save(Host_생성("5678", 5678L)); + otherSpace = spaceRepository.save(Space_생성(otherHost, "잠실")); + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List.of( + new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + request = new JobCreateRequest("청소", sections); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.createJob(host.getId(), otherSpace.getId(), request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + } + + + @Nested + class updateJob_메소드는 { + + @Nested + class 기존의_Job이_존재하는_경우 { + + private Host host; + private Job originJob; + private Section originSection; + private Task originTask; + private JobCreateRequest request; + private List requestSectionNames; + List requestTaskNames; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + originJob = jobRepository.save(Job_생성(space, "마감")); + originSection = sectionRepository.save(Section_생성(originJob, "소강의실")); + originTask = taskRepository.save(Task_생성(originSection, "불 끄기")); + + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List.of( + new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + request = new JobCreateRequest("청소", sections); + List requestSections = request.getSections(); + requestSectionNames = requestSections.stream() + .map(SectionCreateRequest::getName) + .collect(Collectors.toList()); + requestTaskNames = requestSections.get(0) + .getTasks() + .stream() + .map(TaskCreateRequest::getName) + .collect(Collectors.toList()); + } + + @Test + void 기존에_존재하던_Job을_삭제한_후_새로운_Job을_생성한다() { + long updateJobId = jobService.updateJob(host.getId(), originJob.getId(), request); + + Job updateJob = jobRepository.findBySpaceHostAndId(host, updateJobId) + .get(); + List
updateSections = sectionRepository.findAllByJob(updateJob); + List updateTasks = taskRepository.findAllBySectionIn(updateSections); + + assertAll( + () -> assertThat(updateJob.getName().getValue()).isEqualTo(request.getName()), + () -> assertThat(updateSections).doesNotContain(originSection), + () -> assertThat(updateTasks).doesNotContain(originTask), + () -> assertThat(updateSections) + .extracting(section -> section.getName().getValue()) + .containsAll(requestSectionNames), + () -> assertThat(updateTasks) + .extracting(task -> task.getName().getValue()) + .containsAll(requestTaskNames) + ); + } + } + + @Nested + class 존재하지_않는_Host의_id를_입력받은_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + private JobCreateRequest request; + private long savedJobId; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List.of( + new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + request = new JobCreateRequest("청소", sections); + savedJobId = jobService.createJob(host.getId(), space.getId(), request); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.updateJob(NON_EXIST_HOST_ID, savedJobId, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 다른_host의_job_id를_입력받은_경우 { + + private JobCreateRequest request; + private long savedJobId; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List.of( + new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + request = new JobCreateRequest("청소", sections); + savedJobId = jobService.createJob(host.getId(), space.getId(), request); + } + + @Test + void 예외가_발생한다() { + Host anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + assertThatThrownBy(() -> jobService.updateJob(anotherHost.getId(), savedJobId, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 존재하지_않는_job_id를_입력받을_경우 { + + private static final long NON_EXIST_JOB_ID = 0L; + + private Host host; + private JobCreateRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + List tasks = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List.of( + new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks)); + request = new JobCreateRequest("청소", sections); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.updateJob(host.getId(), NON_EXIST_JOB_ID, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + } + + @Nested + class removeJob_메소드는 { + + @Nested + class 존재하지_않는_Host의_id를_입력받은_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + private Job job; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.removeJob(NON_EXIST_HOST_ID, job.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class Job이_존재하지_않는_경우 { + + private static final long NON_EXIST_JOB_ID = 0L; + + private Host host; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.removeJob(host.getId(), NON_EXIST_JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 다른_Host의_Job_제거할_경우 { + + private Job job; + private Host otherHost; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + otherHost = hostRepository.save(Host_생성("1234", 2345L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.removeJob(otherHost.getId(), job.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 존재하는_Job이_있는_경우 { + + private Host host; + private Job job; + private Section section; + private Task task; + private RunningTask runningTask; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); + job = jobRepository.save(Job_생성(space, "청소")); + section = sectionRepository.save(Section_생성(job, "대강의실")); + task = taskRepository.save(Task_생성(section, "책상 닦기")); + runningTask = runningTaskRepository.save(RunningTask_생성(task.getId(), false)); + } + + @Test + void Job과_관련된_Section_Task_RunningTask를_함께_삭제한다() { + jobService.removeJob(host.getId(), job.getId()); + entityManager.flush(); + entityManager.clear(); + + assertAll( + () -> assertThat(jobRepository.findById(job.getId())).isEmpty(), + () -> assertThat(sectionRepository.findById(section.getId())).isEmpty(), + () -> assertThat(taskRepository.findById(task.getId())).isEmpty(), + () -> assertThat(runningTaskRepository.findById(runningTask.getTaskId())).isEmpty() + ); + } + } + + @Nested + class findSlackUrl_메소드는 { + + @Nested + class 존재하지_않는_Host의_id를_입력받은_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + private static final long NON_EXIST_JOB_ID = 0L; + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.findSlackUrl(NON_EXIST_HOST_ID, NON_EXIST_JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class Job이_존재하지_않는_경우 { + + private static final long NON_EXIST_JOB_ID = 0L; + + private Host host; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.findSlackUrl(host.getId(), NON_EXIST_JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + } + + @Nested + class Job이_존재하는데_다른_Host의_Job의_Slack_Url_조회하는_경우 { + + private Host myHost; + private Job otherJob; + + @BeforeEach + void setUp() { + myHost = hostRepository.save(Host_생성("1234", 1234L)); + Host otherHost = hostRepository.save(Host_생성("1234", 2456L)); + Space otherSpace = spaceRepository.save(Space_생성(otherHost, "잠실")); + otherJob = jobRepository.save(Job_생성(otherSpace, "톱오브스윙방")); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.findSlackUrl(myHost.getId(), otherJob.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class Job이_존재하는_경우 { + + private Host host; + private Job job; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "톱오브스윙방", "http://slackurl.com")); + } + + @Test + void Slack_Url을_조회한다() { + assertThat(jobService.findSlackUrl(host.getId(), job.getId()).getSlackUrl()).isEqualTo( + "http://slackurl.com"); + } + } + } + + + @Nested + class changeSlackUrl_메소드는 { + + @Nested + class 존재하지_않는_Host를_입력_받는_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + private static final long JOB_ID = 1L; + + private SlackUrlChangeRequest request; + + @BeforeEach + void setUp() { + request = new SlackUrlChangeRequest("https://newslackurl.com"); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.changeSlackUrl(NON_EXIST_HOST_ID, JOB_ID, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 존재하지_않는_Job을_입력_받는_경우 { + + private static final long NON_EXIST_JOB_ID = 0L; + + private SlackUrlChangeRequest request; + private Host host; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + request = new SlackUrlChangeRequest("https://newslackurl.com"); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.changeSlackUrl(host.getId(), NON_EXIST_JOB_ID, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 다른_Host의_JobId를_입력_받는_경우 { + + private Host host; + private Job otherJob; + private SlackUrlChangeRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Host otherHost = hostRepository.save(Host_생성("1234", 2456L)); + Space otherSpace = spaceRepository.save(Space_생성(otherHost, "잠실")); + otherJob = jobRepository.save(Job_생성(otherSpace, "톱오브스윙방")); + request = new SlackUrlChangeRequest("https://newslackurl.com"); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobService.changeSlackUrl(host.getId(), otherJob.getId(), request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 존재하는_Job을_입력_받는_경우 { + + private static final String SLACK_URL = "http://slackurl.com"; + private static final String NEW_SLACK_URL = "https://newslackurl.com"; + + private Host host; + private Job job; + private SlackUrlChangeRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "톱오브스윙방", SLACK_URL)); + request = new SlackUrlChangeRequest(NEW_SLACK_URL); + } + + @Test + void Slack_Url을_수정한다() { + jobService.changeSlackUrl(host.getId(), job.getId(), request); + + assertThat(job.getSlackUrl()).isEqualTo(NEW_SLACK_URL); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java new file mode 100644 index 00000000..083574a2 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/SpaceServiceTest.java @@ -0,0 +1,382 @@ +package com.woowacourse.gongcheck.core.application; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.gongcheck.core.application.response.SpaceResponse; +import com.woowacourse.gongcheck.core.application.response.SpacesResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.section.SectionRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTask; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.task.TaskRepository; +import com.woowacourse.gongcheck.core.presentation.request.SpaceCreateRequest; +import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.NotFoundException; +import java.util.List; +import javax.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@DisplayName("SpaceService 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SpaceServiceTest { + + @Autowired + EntityManager entityManager; + + @Autowired + private SpaceService spaceService; + + @Autowired + private HostRepository hostRepository; + + @Autowired + private SpaceRepository spaceRepository; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TaskRepository taskRepository; + + @Autowired + private RunningTaskRepository runningTaskRepository; + + @Nested + class findSpaces_메서드는 { + + @Nested + class 존재하지_않는_Host_id를_입력받는_경우 { + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.findSpaces(0L)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 존재하는_Host의_id를_입력받은_경우 { + + private Host host; + private SpacesResponse expected; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + + Space space_1 = Space_생성(host, "잠실 캠퍼스"); + Space space_2 = Space_생성(host, "선릉 캠퍼스"); + Space space_3 = Space_생성(host, "양평같은방"); + List spaces = spaceRepository.saveAll(List.of(space_1, space_2, space_3)); + + expected = SpacesResponse.from(spaces); + } + + @Test + void 해당_Host가_소유한_Space를_응답으로_반환한다() { + SpacesResponse actual = spaceService.findSpaces(host.getId()); + + assertThat(actual.getSpaces()) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(expected.getSpaces()); + } + } + } + + @Nested + class createSpace_메서드는 { + + @Nested + class Host가_입력받은_Space_이름과_같은_Space를_이미_가지고_있는_경우 { + + private Host host; + private SpaceCreateRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); + request = new SpaceCreateRequest(space.getName().getValue(), "https://image.gongcheck.shop/123sdf5"); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.createSpace(host.getId(), request)) + .isInstanceOf(BusinessException.class) + .hasMessage("이미 존재하는 이름입니다."); + } + } + + @Nested + class 존재하지_않는_Host_id를_입력받은_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + private SpaceCreateRequest request; + + @BeforeEach + void setUp() { + request = new SpaceCreateRequest("이것은 유일한 Space이름", "https://image.gongcheck.shop/123sdf5"); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.createSpace(NON_EXIST_HOST_ID, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 입력받은_Host가_존재하는_경우 { + + private static final String SPACE_NAME = "잠실 캠퍼스"; + private static final String SPACE_IMAGE_URL = "https://image.gongcheck.shop/123sdf5"; + + private Host host; + private SpaceCreateRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + request = new SpaceCreateRequest(SPACE_NAME, SPACE_IMAGE_URL); + } + + @Test + void Space를_생성한다() { + Long spaceId = spaceService.createSpace(host.getId(), request); + Space actual = spaceRepository.getById(spaceId); + + assertAll( + () -> assertThat(actual.getName().getValue()).isEqualTo(SPACE_NAME), + () -> assertThat(actual.getImageUrl()).isEqualTo(SPACE_IMAGE_URL) + ); + } + } + } + + @Nested + class findSpace_메서드는 { + + @Nested + class Space_목록이_존재하는_경우 { + + private static final String SPACE_NAME = "잠실 캠퍼스"; + + private Host host; + private Space space; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 2345L)); + space = spaceRepository.save(Space_생성(host, SPACE_NAME)); + } + + @Test + void Job_목록을_조회한다() { + SpaceResponse actual = spaceService.findSpace(host.getId(), space.getId()); + + assertAll( + () -> assertThat(actual.getId()).isEqualTo(space.getId()), + () -> assertThat(actual.getName()).isEqualTo(SPACE_NAME) + ); + } + } + + @Nested + class 입력받은_Host가_입력받은_Space를_가지고_있지_않은_경우 { + + private Space space; + private Host anotherHost; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); + anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.findSpace(anotherHost.getId(), space.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + + @Nested + class 존재하지_않는_Host_id를_입력받은_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + private Space space; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.findSpace(NON_EXIST_HOST_ID, space.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 존재하지_않는_Space_id를_입력받은_경우 { + + private Host host; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.findSpace(host.getId(), 0L)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + + @Nested + class 입력받은_Host가_입력받은_Space를_소유하고_있는_경우 { + + private static final String SPACE_NAME = "잠실 캠퍼스"; + + private Host host; + private Space space; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, SPACE_NAME)); + } + + @Test + void Space_응답을_반환한다() { + SpaceResponse actual = spaceService.findSpace(host.getId(), space.getId()); + + assertAll( + () -> assertThat(actual.getId()).isEqualTo(space.getId()), + () -> assertThat(actual.getName()).isEqualTo(SPACE_NAME) + ); + } + } + } + + @Nested + class removeSpace_메서드는 { + + @Nested + class 입력받은_Host_id가_존재하지_않는_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + private Space space; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.removeSpace(NON_EXIST_HOST_ID, space.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 입력받은_Host가_입력받은_Space를_소유하고_있지_않은_경우 { + + private Host anotherHost; + private Space space; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + anotherHost = hostRepository.save(Host_생성("1234", 4567L)); + space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> spaceService.removeSpace(anotherHost.getId(), space.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + + @Nested + class 입력받은_Space_id가_존재하면 { + + private Host host; + private Space space; + private Job job; + private Section section; + private Task task; + private RunningTask runningTask; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); + job = jobRepository.save(Job_생성(space, "청소")); + section = sectionRepository.save(Section_생성(job, "대강의실")); + task = taskRepository.save(Task_생성(section, "책상 닦기")); + runningTask = runningTaskRepository.save(RunningTask_생성(task.getId(), false)); + } + + @Test + void 해당_Space_및_관련된_Job_Section_Task_RunningTask를_삭제한다() { + spaceService.removeSpace(host.getId(), space.getId()); + + entityManager.flush(); + entityManager.clear(); + assertAll( + () -> assertThat(spaceRepository.findById(space.getId())).isEmpty(), + () -> assertThat(jobRepository.findById(job.getId())).isEmpty(), + () -> assertThat(sectionRepository.findById(section.getId())).isEmpty(), + () -> assertThat(taskRepository.findById(task.getId())).isEmpty(), + () -> assertThat(runningTaskRepository.findById(runningTask.getTaskId())).isEmpty() + ); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/SubmissionServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/SubmissionServiceTest.java new file mode 100644 index 00000000..ed6ecc28 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/SubmissionServiceTest.java @@ -0,0 +1,346 @@ +package com.woowacourse.gongcheck.core.application; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Submission_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.gongcheck.core.application.response.SubmissionResponse; +import com.woowacourse.gongcheck.core.application.response.SubmissionsResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.section.SectionRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import com.woowacourse.gongcheck.core.domain.submission.Submission; +import com.woowacourse.gongcheck.core.domain.submission.SubmissionRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.task.TaskRepository; +import com.woowacourse.gongcheck.core.presentation.request.SubmissionRequest; +import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.NotFoundException; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@DisplayName("SubmissionService 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SubmissionServiceTest { + + @Autowired + private SubmissionService submissionService; + + @Autowired + private HostRepository hostRepository; + + @Autowired + private SpaceRepository spaceRepository; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TaskRepository taskRepository; + + @Autowired + private RunningTaskRepository runningTaskRepository; + + @Autowired + private SubmissionRepository submissionRepository; + + @Nested + class submitJobCompletion_메소드는 { + + @Nested + class 입력받은_Host가_존재하지_않는_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + private Long jobId; + private SubmissionRequest request; + + @BeforeEach + void setUp() { + jobId = 1L; + request = new SubmissionRequest("제출자"); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> submissionService.submitJobCompletion(NON_EXIST_HOST_ID, jobId, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 입력받은_Job이_존재하지_않는_경우 { + + private static final long NON_EXIST_JOB_ID = 0L; + + private Host host; + private SubmissionRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + request = new SubmissionRequest("제출자"); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> submissionService.submitJobCompletion(host.getId(), NON_EXIST_JOB_ID, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 다른_Host의_Job을_입력받는_경우 { + + private Host anotherHost; + private Job job; + private SubmissionRequest request; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + request = new SubmissionRequest("제출자"); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy( + () -> submissionService.submitJobCompletion(anotherHost.getId(), job.getId(), request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class RunningTask가_존재하지_않는_경우 { + + private Host host; + private Job job; + private SubmissionRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + request = new SubmissionRequest("제출자"); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> submissionService.submitJobCompletion(host.getId(), job.getId(), request)) + .isInstanceOf(BusinessException.class) + .hasMessage("현재 제출할 수 있는 진행중인 작업이 존재하지 않습니다."); + } + } + + @Nested + class 모든_RunningTask가_체크상태가_아닌_경우 { + + private Host host; + private Job job; + private SubmissionRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + Task task_1 = taskRepository.save(Task_생성(section, "책상 청소")); + Task task_2 = taskRepository.save(Task_생성(section, "의자 넣기")); + runningTaskRepository.save(RunningTask_생성(task_1.getId(), false)); + runningTaskRepository.save(RunningTask_생성(task_2.getId(), false)); + request = new SubmissionRequest("제출자"); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> submissionService.submitJobCompletion(host.getId(), job.getId(), request)) + .isInstanceOf(BusinessException.class) + .hasMessage("모든 작업이 완료되지않아 제출이 불가합니다."); + } + } + + @Nested + class 모든_RunningTask가_체크_상태인_경우 { + + private Host host; + private Space space; + private Job job; + private SubmissionRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + Task task_1 = taskRepository.save(Task_생성(section, "책상 청소")); + Task task_2 = taskRepository.save(Task_생성(section, "의자 넣기")); + runningTaskRepository.save(RunningTask_생성(task_1.getId(), true)); + runningTaskRepository.save(RunningTask_생성(task_2.getId(), true)); + request = new SubmissionRequest("제출자"); + } + + @Test + void Submission을_생성한다() { + submissionService.submitJobCompletion(host.getId(), job.getId(), request); + List submissions = submissionRepository.findAll(); + int runningTaskSize = runningTaskRepository.findAll() + .size(); + + assertAll( + () -> assertThat(submissions).hasSize(1), + () -> assertThat(submissions.get(0).getAuthor()).isEqualTo(request.getAuthor()), + () -> assertThat(runningTaskSize).isZero() + ); + } + } + } + + @Nested + class findPage_메소드는 { + + @Nested + class 입력받은_Host가_존재하지_않는_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + + private Long spaceId; + private PageRequest request; + + @BeforeEach + void setUp() { + spaceId = 1L; + request = PageRequest.of(0, 2); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> submissionService.findPage(NON_EXIST_HOST_ID, spaceId, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 입력받은_Space가_존재하지_않는_경우 { + + private static final long NON_EXIST_SPACE_ID = 0L; + + private Host host; + private PageRequest request; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + request = PageRequest.of(0, 2); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> submissionService.findPage(host.getId(), NON_EXIST_SPACE_ID, request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + + @Nested + class 다른_Host의_Space를_입력받는_경우 { + + private Host anotherHost; + private Space space; + private PageRequest request; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + space = spaceRepository.save(Space_생성(host, "잠실")); + request = PageRequest.of(0, 2); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy( + () -> submissionService.findPage(anotherHost.getId(), space.getId(), request)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + + @Nested + class 올바른_Host의_Space를_입력받는_경우 { + + private static final String SUBMISSION_AUTHOR_1 = "어썸오"; + private static final String SUBMISSION_AUTHOR_2 = "어썸오"; + + private Host host; + private Space space; + private PageRequest request; + private Job job; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + request = PageRequest.of(0, 2); + submissionRepository.save(Submission_생성(job, SUBMISSION_AUTHOR_1)); + submissionRepository.save(Submission_생성(job, SUBMISSION_AUTHOR_2)); + submissionRepository.save(Submission_생성(job, SUBMISSION_AUTHOR_2)); + } + + @Test + void Submission을_조회한다() { + SubmissionsResponse actual = submissionService.findPage(host.getId(), space.getId(), request); + + assertAll( + () -> assertThat(actual.getSubmissions()) + .extracting(SubmissionResponse::getJobId) + .containsExactly(job.getId(), job.getId()), + () -> assertThat(actual.getSubmissions()) + .extracting(SubmissionResponse::getJobName) + .containsExactly(job.getName().getValue(), job.getName().getValue()), + () -> assertThat(actual.getSubmissions()) + .extracting(SubmissionResponse::getAuthor) + .containsExactly(SUBMISSION_AUTHOR_1, SUBMISSION_AUTHOR_2), + () -> assertThat(actual.isHasNext()).isTrue() + ); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/application/TaskServiceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/application/TaskServiceTest.java new file mode 100644 index 00000000..152f4b30 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/application/TaskServiceTest.java @@ -0,0 +1,708 @@ +package com.woowacourse.gongcheck.core.application; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; +import static java.util.stream.Collectors.toList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.gongcheck.core.application.response.JobActiveResponse; +import com.woowacourse.gongcheck.core.application.response.RunningTaskResponse; +import com.woowacourse.gongcheck.core.application.response.RunningTasksWithSectionResponse; +import com.woowacourse.gongcheck.core.application.response.TaskResponse; +import com.woowacourse.gongcheck.core.application.response.TasksResponse; +import com.woowacourse.gongcheck.core.application.response.TasksWithSectionResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.section.SectionRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import com.woowacourse.gongcheck.core.domain.task.RunningTask; +import com.woowacourse.gongcheck.core.domain.task.RunningTaskRepository; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.task.TaskRepository; +import com.woowacourse.gongcheck.exception.BusinessException; +import com.woowacourse.gongcheck.exception.NotFoundException; +import java.util.List; +import javax.persistence.EntityManager; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@DisplayName("TaskService 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class TaskServiceTest { + + @Autowired + private TaskService taskService; + + @Autowired + private HostRepository hostRepository; + + @Autowired + private SpaceRepository spaceRepository; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TaskRepository taskRepository; + + @Autowired + private RunningTaskRepository runningTaskRepository; + + @Autowired + private EntityManager entityManager; + + @Nested + class createNewRunningTasks_메소드는 { + + @Nested + class 존재하는_Host와_Job을_입력받는_경우 { + + private Host host; + private Job job; + private List taskIds; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + List tasks = taskRepository.saveAll( + List.of(Task_생성(section, "책상 청소"), Task_생성(section, "의자 넣기"))); + taskIds = tasks.stream() + .map(Task::getId) + .collect(toList()); + } + + @Test + void Job이_가진_Task에_해당하는_RunningTask를_생성한다() { + taskService.createNewRunningTasks(host.getId(), job.getId()); + List actual = runningTaskRepository.findAllById(taskIds); + + assertAll( + () -> assertThat(actual) + .extracting(RunningTask::isChecked) + .containsExactly(false, false), + () -> assertThat(actual) + .extracting(RunningTask::getTaskId) + .containsAll(taskIds) + ); + } + } + + @Nested + class Task가_존재하지_않는_경우 { + + private Host host; + private Job job; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + sectionRepository.save(Section_생성(job, "트랙룸")); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.createNewRunningTasks(host.getId(), job.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("작업이 존재하지 않습니다."); + } + } + + @Nested + class 존재하지_않는_Host로_RunningTask를_생성할_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + private static final long JOB_ID = 1L; + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.createNewRunningTasks(NON_EXIST_HOST_ID, JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 존재하지_않는_Task로_RunningTask를_생성할_경우 { + + private static final long NON_EXIST_JOB_ID = 0L; + + private Long hostId; + + @BeforeEach + void setUp() { + hostId = hostRepository.save(Host_생성("1234", 1234567L)) + .getId(); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.createNewRunningTasks(hostId, NON_EXIST_JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 다른_Host의_Task로_RunningTask를_생성할_경우 { + + private Host anotherHost; + private Job job; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskRepository.saveAll(List.of(Task_생성(section, "책상 청소"), Task_생성(section, "의자 넣기"))); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.createNewRunningTasks(anotherHost.getId(), job.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class RunningTask가_이미_존재할_때_새로운_RunningTask를_생성할_경우 { + + private Host host; + private Job job; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + List tasks = taskRepository.saveAll( + List.of(Task_생성(section, "책상 청소"), Task_생성(section, "의자 넣기"))); + runningTaskRepository.saveAll(tasks.stream() + .map(Task::getId) + .map(id -> RunningTask_생성(id, true)) + .collect(toList())); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.createNewRunningTasks(host.getId(), job.getId())) + .isInstanceOf(BusinessException.class) + .hasMessage("현재 진행중인 작업이 존재하여 새로운 작업을 생성할 수 없습니다."); + } + } + } + + @Nested + class isJobActivated_메소드는 { + + @Nested + class 존재하는_Host와_Job의_RunningTask가_존재하는_경우 { + + private Host host; + private Job job; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + List tasks = taskRepository.saveAll(List.of( + Task_생성(section, "책상 청소"), Task_생성(section, "의자 넣기"))); + runningTaskRepository.saveAll(tasks.stream() + .map(Task::getId) + .map(id -> RunningTask_생성(id, true)) + .collect(toList())); + } + + @Test + void True를_반환한다() { + JobActiveResponse actual = taskService.isJobActivated(host.getId(), job.getId()); + + assertThat(actual.isActive()).isTrue(); + } + } + + @Nested + class 존재하는_Host와_Job의_RunningTask가_존재하지_않는_경우 { + + private Host host; + private Job job; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskRepository.saveAll(List.of(Task_생성(section, "책상 청소"), Task_생성(section, "의자 넣기"))); + } + + @Test + void False을_반환한다() { + JobActiveResponse actual = taskService.isJobActivated(host.getId(), job.getId()); + + assertThat(actual.isActive()).isFalse(); + } + } + + @Nested + class 존재하지_않는_Host로_확인하려는_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + private static final long JOB_ID = 1L; + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.isJobActivated(NON_EXIST_HOST_ID, JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 존재하지_않는_Task로_확인하려는_경우 { + + private static final long NON_EXIST_JOB_ID = 1L; + + private Long hostId; + + @BeforeEach + void setUp() { + hostId = hostRepository.save(Host_생성("1234", 1111L)) + .getId(); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.isJobActivated(hostId, NON_EXIST_JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 다른_Host의_Task로_확인하려는_경우 { + + private Host anotherHost; + private Job job; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskRepository.saveAll(List.of(Task_생성(section, "책상 청소"), Task_생성(section, "의자 넣기"))); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.isJobActivated(anotherHost.getId(), job.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + } + + @Nested + class findRunningTasks_메소드는 { + + @Nested + class 존재하는_Host와_RunningTasks가_생성된_Job을_입력받은_경우 { + + private static final String SECTION_NAME = "트랙룸"; + private static final String TASK_NAME_1 = "책상 청소"; + private static final String TASK_NAME_2 = "의자 넣기"; + private static final int TASK_INDEX = 0; + + private Host host; + private Job job; + private Section section; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, SECTION_NAME)); + List tasks = taskRepository.saveAll(List.of( + Task_생성(section, TASK_NAME_1), Task_생성(section, TASK_NAME_2))); + runningTaskRepository.saveAll(tasks.stream() + .map(task -> RunningTask_생성(task.getId(), false)) + .collect(toList())); + entityManager.flush(); + entityManager.clear(); + } + + @Test + void 정상적으로_RunningTasks를_조회한다() { + List actual = taskService.findRunningTasks(host.getId(), job.getId()) + .getSections(); + List actualTasks = actual.get(TASK_INDEX).getTasks(); + + assertAll( + () -> assertThat(actual) + .extracting(RunningTasksWithSectionResponse::getName) + .containsExactly(SECTION_NAME), + () -> assertThat(actual).hasSize(1), + () -> assertThat(actualTasks) + .extracting(RunningTaskResponse::getName) + .containsExactly(TASK_NAME_1, TASK_NAME_2) + ); + } + } + + @Nested + class 존재하지_않는_Host를_입력받은_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + private static final long JOB_ID = 1L; + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.findRunningTasks(NON_EXIST_HOST_ID, JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 존재하지_않는_Job을_입력받은_경우 { + + private static final long NON_EXIST_JOB_ID = 0L; + + private Long hostId; + + @BeforeEach + void setUp() { + hostId = hostRepository.save(Host_생성("1234", 1111L)) + .getId(); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.findRunningTasks(hostId, NON_EXIST_JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 다른_Host의_Task의_RunningTask를_조회하면 { + + private Host anotherHost; + private Job job; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + List tasks = taskRepository.saveAll(List.of( + Task_생성(section, "책상 청소"), Task_생성(section, "의자 넣기"))); + runningTaskRepository.saveAll(tasks.stream() + .map(task -> RunningTask_생성(task.getId(), false)) + .collect(toList())); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.findRunningTasks(anotherHost.getId(), job.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class RunningTasks가_생성되지_않은_Job을_입력받은_경우 { + + private Host host; + private Job job; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskRepository.saveAll(List.of(Task_생성(section, "책상 청소"), Task_생성(section, "의자 넣기"))); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.findRunningTasks(host.getId(), job.getId())) + .isInstanceOf(BusinessException.class) + .hasMessage("현재 진행중인 작업이 존재하지 않아 조회할 수 없습니다"); + } + } + } + + @Nested + class flipRunningTask_메소드는 { + + @Nested + class 존재하는_Host와_체크되지_않은_RunningTask를_가진_Task를_입력받은_경우 { + + private Host host; + private Task task; + private RunningTask runningTask; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + task = taskRepository.save(Task_생성(section, "책상 청소")); + runningTask = runningTaskRepository.save(RunningTask_생성(task.getId(), false)); + } + + @Test + void 체크상태를_True로_변경한다() { + taskService.flipRunningTask(host.getId(), task.getId()); + + assertThat(runningTask.isChecked()).isTrue(); + } + } + + @Nested + class 존재하는_Host와_체크된_RunningTask를_가진_Task를_입력받은_경우 { + + private Host host; + private Task task; + private RunningTask runningTask; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + task = taskRepository.save(Task_생성(section, "책상 청소")); + runningTask = runningTaskRepository.save(RunningTask_생성(task.getId(), true)); + } + + @Test + void 체크상태를_False로_변경한다() { + taskService.flipRunningTask(host.getId(), task.getId()); + + assertThat(runningTask.isChecked()).isFalse(); + } + } + + @Nested + class 존재하지_않는_Host를_입력받은_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + private static final long TASK_ID = 1L; + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.flipRunningTask(NON_EXIST_HOST_ID, TASK_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 존재하지_않는_Task를_입력받은_경우 { + + private static final long NON_EXIST_TASK_ID = 0L; + + private Long hostId; + + @BeforeEach + void setUp() { + hostId = hostRepository.save(Host_생성("1234", 1111L)) + .getId(); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.flipRunningTask(hostId, NON_EXIST_TASK_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 다른_Host의_Task를_입력받은_경우 { + + private Host anotherHost; + private Long taskId; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskId = taskRepository.save(Task_생성(section, "책상 청소")) + .getId(); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.flipRunningTask(anotherHost.getId(), taskId)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 진행중이지_않은_Task를_입력받은_경우 { + + private Host host; + private Long taskId; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskId = taskRepository.save(Task_생성(section, "책상 청소")) + .getId(); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.flipRunningTask(host.getId(), taskId)) + .isInstanceOf(BusinessException.class) + .hasMessage("현재 진행 중인 작업이 아닙니다."); + } + } + } + + @Nested + class findTasks_메소드는 { + + @Nested + class 존재하는_Host와_Job을_입력하는_경우 { + + private static final String SECTION_NAME = "트랙룸"; + private static final String TASK_NAME_1 = "책상 청소"; + private static final String TASK_NAME_2 = "의자 넣기"; + + private Host host; + private Job job; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, SECTION_NAME)); + taskRepository.saveAll(List.of(Task_생성(section, TASK_NAME_1), Task_생성(section, TASK_NAME_2))); + } + + @Test + void 조회에_성공한다() { + TasksResponse actual = taskService.findTasks(host.getId(), job.getId()); + TasksWithSectionResponse actualTaskWithSectionResponses = actual.getSections().get(0); + List actualTaskResponses = actualTaskWithSectionResponses.getTasks(); + + assertAll( + () -> assertThat(actualTaskWithSectionResponses.getName()).isEqualTo(SECTION_NAME), + () -> assertThat(actualTaskResponses).extracting(TaskResponse::getName) + .containsExactly(TASK_NAME_1, TASK_NAME_2), + () -> assertThat(actual.getSections()).hasSize(1) + ); + } + } + + @Nested + class 존재하지_않는_Host를_입력하는_경우 { + + private static final long NON_EXIST_HOST_ID = 0L; + private static final long JOB_ID = 1L; + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.findTasks(NON_EXIST_HOST_ID, JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + + @Nested + class 존재하지_않는_Job을_입력하는_경우 { + + private static final long NON_EXIST_JOB_ID = 0L; + + private long hostId; + + @BeforeEach + void setUp() { + hostId = hostRepository.save(Host_생성("1234", 1111L)) + .getId(); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.findTasks(hostId, NON_EXIST_JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 다른_Host의_Job을_입력하는_경우 { + + private Host anotherHost; + private Job job; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskRepository.saveAll(List.of(Task_생성(section, "책상 청소"), Task_생성(section, "의자 넣기"))); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskService.findTasks(anotherHost.getId(), job.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostRepositoryTest.java new file mode 100644 index 00000000..ceefff8e --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostRepositoryTest.java @@ -0,0 +1,154 @@ +package com.woowacourse.gongcheck.core.domain.host; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.gongcheck.config.JpaConfig; +import com.woowacourse.gongcheck.exception.NotFoundException; +import java.time.LocalDateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(JpaConfig.class) +@DisplayName("HostRepository 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HostRepositoryTest { + + @Autowired + private HostRepository hostRepository; + + @Nested + class save_메소드는 { + + @Nested + class 저장할_Host를_받는_경우 { + + private Host host; + + @BeforeEach + void setUp() { + host = Host_생성("1234", 1234567L); + } + + @Test + void 생성시간이_같이_저장된다() { + LocalDateTime nowLocalDateTime = LocalDateTime.now(); + Host actual = hostRepository.save(host); + + assertThat(actual.getCreatedAt()).isAfter(nowLocalDateTime); + } + } + } + + @Nested + class getById_메소드는 { + + @Nested + class 존재하는_Host의_id를_입력받는_경우 { + + private Long hostId; + + @BeforeEach + void setUp() { + hostId = hostRepository.save(Host_생성("1234", 1234567L)) + .getId(); + } + + @Test + void Host를_반환한다() { + Host actual = hostRepository.getById(hostId); + assertThat(actual.getId()).isEqualTo(hostId); + } + } + + @Nested + class 존재하지않는_Host의_id를_입력받는_경우 { + + private static final long NON_EXIST_ID = 0L; + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> hostRepository.getById(NON_EXIST_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + } + + @Nested + class getByGithubId_메소드는 { + + @Nested + class 존재하는_Host의_githubId를_입력받는_경우 { + + private static final long EXIST_GITHUB_ID = 1234567L; + private Host host; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", EXIST_GITHUB_ID)); + } + + @Test + void Host를_반환한다() { + Host actual = hostRepository.getByGithubId(EXIST_GITHUB_ID); + assertThat(actual).isEqualTo(host); + } + } + + @Nested + class 존재하지않는_Host의_githubId를_입력받는_경우 { + + private static final long NON_EXIST_ID = 987654L; + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> hostRepository.getByGithubId(NON_EXIST_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 호스트입니다."); + } + } + } + + @Nested + class existsByGithubId_메소드는 { + + @Nested + class 존재하는_Host의_githubId를_입력받는_경우 { + + private static final long EXIST_GITHUB_ID = 1234567L; + + @BeforeEach + void setUp() { + hostRepository.save(Host_생성("1234", EXIST_GITHUB_ID)); + } + + @Test + void True를_반환한다() { + boolean actual = hostRepository.existsByGithubId(EXIST_GITHUB_ID); + assertThat(actual).isTrue(); + } + } + + @Nested + class 존재하지않는_Host의_githubId를_입력받는_경우 { + + private static final long NON_EXIST_GITHUB_ID = 0L; + + @Test + void False를_반환한다() { + boolean actual = hostRepository.existsByGithubId(NON_EXIST_GITHUB_ID); + assertThat(actual).isFalse(); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/host/HostTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostTest.java similarity index 90% rename from backend/src/test/java/com/woowacourse/gongcheck/domain/host/HostTest.java rename to backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostTest.java index 2937468b..a1777563 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/host/HostTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/HostTest.java @@ -1,11 +1,11 @@ -package com.woowacourse.gongcheck.domain.host; +package com.woowacourse.gongcheck.core.domain.host; import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import com.woowacourse.gongcheck.exception.UnauthorizedException; +import com.woowacourse.gongcheck.exception.BusinessException; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -29,7 +29,7 @@ class 비밀번호를_검사할_때 { SpacePassword spacePassword = new SpacePassword("1234"); assertThatThrownBy(() -> host.checkPassword(spacePassword)) - .isInstanceOf(UnauthorizedException.class) + .isInstanceOf(BusinessException.class) .hasMessage("공간 비밀번호와 입력하신 비밀번호가 일치하지 않습니다."); } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/host/SpacePasswordTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/SpacePasswordTest.java similarity index 96% rename from backend/src/test/java/com/woowacourse/gongcheck/domain/host/SpacePasswordTest.java rename to backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/SpacePasswordTest.java index 14114536..53bdc882 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/host/SpacePasswordTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/host/SpacePasswordTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.domain.host; +package com.woowacourse.gongcheck.core.domain.host; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFileTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFileTest.java new file mode 100644 index 00000000..5d4451e5 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/image/imageFile/ImageFileTest.java @@ -0,0 +1,112 @@ +package com.woowacourse.gongcheck.core.domain.image.imageFile; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.woowacourse.gongcheck.exception.BusinessException; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +class ImageFileTest { + + @Nested + class from_메소드는 { + + @Nested + class null인_multipartFile이_입력된_경우 { + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> ImageFile.from(null)) + .isInstanceOf(BusinessException.class) + .hasMessage("이미지 파일은 null이 들어올 수 없습니다."); + } + } + + @Nested + class 파일이_비어있는_multipartFile이_입력된_경우 { + + private MultipartFile emptyFile; + + @BeforeEach + void setUp() { + emptyFile = new MockMultipartFile("images", + "jamsil.jpg", + "images/jpg", + new byte[]{}); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> ImageFile.from(emptyFile)) + .isInstanceOf(BusinessException.class) + .hasMessage("이미지 파일은 빈값이 들어올 수 없습니다."); + } + } + + @Nested + class 파일이름이_비어있는_multipartFile이_입력된_경우 { + + private MultipartFile nullNameFile; + + @BeforeEach + void setUp() { + nullNameFile = new MockMultipartFile("images", + null, + "images/jpg", + "123".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> ImageFile.from(nullNameFile)) + .isInstanceOf(BusinessException.class) + .hasMessage("이미지 파일 이름은 빈값이 들어올 수 없습니다."); + } + } + + @Nested + class 이미지_확장자가_아닌_파일이_입력된_경우 { + + private MultipartFile textFile; + + @BeforeEach + void setUp() { + textFile = new MockMultipartFile("images", + "jamsil.text", + "images/jpg", + "123".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> ImageFile.from(textFile)) + .isInstanceOf(BusinessException.class) + .hasMessage("이미지 파일 확장자만 들어올 수 있습니다."); + } + } + + @Nested + class 정상적인_multipartFile이_입력된_경우 { + + private MultipartFile multipartFile; + + @BeforeEach + void setUp() { + multipartFile = new MockMultipartFile("images", + "jamsil.jpg", + "images/jpg", + "123".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void imageFile이_반환된다() { + assertDoesNotThrow(() -> ImageFile.from(multipartFile)); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobRepositoryTest.java new file mode 100644 index 00000000..7a5603a0 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobRepositoryTest.java @@ -0,0 +1,207 @@ +package com.woowacourse.gongcheck.core.domain.job; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.gongcheck.config.JpaConfig; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import com.woowacourse.gongcheck.core.domain.vo.Name; +import com.woowacourse.gongcheck.exception.NotFoundException; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(JpaConfig.class) +@DisplayName("JobRepositoryTest 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class JobRepositoryTest { + + @Autowired + private HostRepository hostRepository; + + @Autowired + private SpaceRepository spaceRepository; + + @Autowired + private JobRepository jobRepository; + + @Nested + class save_메소드는 { + + @Nested + class Job_저장할_경우 { + + private LocalDateTime nowLocalDateTime; + private Job job; + private Space space; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실")); + + nowLocalDateTime = LocalDateTime.now(); + } + + @Test + void 생성시간이_저장된다() { + job = jobRepository.save(Job.builder() + .space(space) + .name(new Name("청소")) + .build()); + assertThat(job.getCreatedAt()).isAfter(nowLocalDateTime); + } + } + } + + @Nested + class findAllBySpaceHostAndSpace_메소드는 { + + @Nested + class Host와_Space를_입력받은_경우 { + + private Host host; + private Space space; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실")); + Job job1 = Job_생성(space, "오픈"); + Job job2 = Job_생성(space, "청소"); + Job job3 = Job_생성(space, "마감"); + jobRepository.saveAll(List.of(job1, job2, job3)); + } + + @Test + void 연관된_Job_목록을_조회한다() { + List result = jobRepository.findAllBySpaceHostAndSpace(host, space); + + assertThat(result).hasSize(3); + } + } + } + + @Nested + class getBySpaceHostAndId_메소드는 { + + @Nested + class Host와_JobId를_입력받는_경우 { + + private Host host; + private Job job; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + } + + @Test + void 해당되는_Job을_조회한다() { + Job result = jobRepository.getBySpaceHostAndId(host, job.getId()); + + assertThat(result).isEqualTo(job); + } + } + + @Nested + class 입력받은_Host와_JobId가_연관되지_않은_경우 { + + private Host otherHost; + private Job job; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + otherHost = hostRepository.save(Host_생성("1234", 2345L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobRepository.getBySpaceHostAndId(otherHost, job.getId())) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + } + + @Nested + class findAllBySpace_메소드는 { + + @Nested + class Space를_입력_받는_경우 { + + private Space space; + private List jobs; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); + jobs = jobRepository.saveAll(List.of(Job_생성(space, "청소"), Job_생성(space, "마감"))); + } + + @Test + void 연관된_모든_Job_목록을_조회한다() { + List result = jobRepository.findAllBySpace(space); + + assertThat(result).containsExactlyElementsOf(jobs); + } + } + } + + @Nested + class getById_메소드는 { + + @Nested + class jobId를_입력_받는_경우 { + + private Job job; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + } + + @Test + void Job을_조회한다() { + Job savedJob = jobRepository.getById(job.getId()); + + assertThat(savedJob).isNotNull(); + } + } + + @Nested + class 존재하지_않는_jobId를_입력_받는_경우 { + private final static long NON_EXIST_JOB_ID = 0L; + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> jobRepository.getById(NON_EXIST_JOB_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobTest.java new file mode 100644 index 00000000..edafef6a --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/job/JobTest.java @@ -0,0 +1,39 @@ +package com.woowacourse.gongcheck.core.domain.job; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +class JobTest { + + @Test + void SlackUrl을_수정한다() { + Job job = Job.builder().slackUrl("https://slackurl.com").build(); + + job.changeSlackUrl("https://newslackurl.com"); + + assertThat(job.getSlackUrl()).isEqualTo("https://newslackurl.com"); + } + + @Nested + class url이_존재하는지_확인한다 { + + @Test + void exist() { + Job job = Job.builder() + .slackUrl("https://slackurl.com") + .build(); + + assertThat(job.hasUrl()).isTrue(); + } + + @Test + void nonExist() { + Job job = Job.builder() + .build(); + + assertThat(job.hasUrl()).isFalse(); + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/section/SectionRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/section/SectionRepositoryTest.java new file mode 100644 index 00000000..74ea0029 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/section/SectionRepositoryTest.java @@ -0,0 +1,82 @@ +package com.woowacourse.gongcheck.core.domain.section; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; +import static org.assertj.core.api.Assertions.assertThat; + +import com.woowacourse.gongcheck.config.JpaConfig; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import com.woowacourse.gongcheck.core.domain.vo.Name; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(JpaConfig.class) +class SectionRepositoryTest { + + @Autowired + private HostRepository hostRepository; + + @Autowired + private SpaceRepository spaceRepository; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Test + void Section_저장_시_생성시간이_저장된다() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + + LocalDateTime nowLocalDateTime = LocalDateTime.now(); + Section section = sectionRepository.save(Section.builder() + .job(job) + .name(new Name("트랙룸")) + .build()); + assertThat(section.getCreatedAt()).isAfter(nowLocalDateTime); + } + + @Test + void 입력된_Job_목록에_해당하는_모든_Section을_조회한다() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job1 = jobRepository.save(Job_생성(space, "청소")); + Job job2 = jobRepository.save(Job_생성(space, "마감")); + Section section1 = sectionRepository.save(Section_생성(job1, "트랙룸")); + Section section2 = sectionRepository.save(Section_생성(job1, "굿샷 강의장")); + Section section3 = sectionRepository.save(Section_생성(job2, "트랙룸")); + Section section4 = sectionRepository.save(Section_생성(job2, "굿샷 강의장")); + + List
result = sectionRepository.findAllByJobIn(List.of(job1, job2)); + + assertThat(result).containsExactly(section1, section2, section3, section4); + } + + @Test + void Job에_해당하는_모든_Section을_조회한다() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section1 = sectionRepository.save(Section_생성(job, "트랙룸")); + Section section2 = sectionRepository.save(Section_생성(job, "굿샷 강의장")); + + List
result = sectionRepository.findAllByJob(job); + + assertThat(result).containsExactly(section1, section2); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/section/SectionTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/section/SectionTest.java new file mode 100644 index 00000000..07d25e27 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/section/SectionTest.java @@ -0,0 +1,14 @@ +package com.woowacourse.gongcheck.core.domain.section; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +class SectionTest { + + @Test + void description과_imageUrl이_null이_들어오는_경우_정상적으로_생성한다() { + assertDoesNotThrow(() -> Section.builder() + .build()); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepositoryTest.java new file mode 100644 index 00000000..61c1f13b --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/space/SpaceRepositoryTest.java @@ -0,0 +1,201 @@ +package com.woowacourse.gongcheck.core.domain.space; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.gongcheck.config.JpaConfig; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.vo.Name; +import com.woowacourse.gongcheck.exception.NotFoundException; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(JpaConfig.class) +@DisplayName("SpaceRepository 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SpaceRepositoryTest { + + @Autowired + private HostRepository hostRepository; + + @Autowired + private SpaceRepository spaceRepository; + + + @Nested + class save_메서드는 { + + @Nested + class 입력받은_Space를_저장할_때 { + + private Space space; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + space = Space_생성(host, "잠실"); + } + + @Test + void 생성_시간도_함께_저장한다() { + LocalDateTime timeThatBeforeSave = LocalDateTime.now(); + Space savedSpace = spaceRepository.save(space); + assertThat(savedSpace.getCreatedAt()).isAfter(timeThatBeforeSave); + } + } + } + + @Nested + class findAllByHost_메서드는 { + + @Nested + class 입력받은_Host가_Space를_가지고_있을_때 { + + private Host host; + private List expected; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + expected = spaceRepository.saveAll(List.of( + Space_생성(host, "잠실 캠퍼스"), + Space_생성(host, "선릉 캠퍼스"), + Space_생성(host, "양평같은방")) + ); + } + + @Test + void 가지고_있는_Space를_모두_조회한다() { + List actual = spaceRepository.findAllByHost(host); + + assertThat(actual).isEqualTo(expected); + } + } + } + + @Nested + class getByHostAndId_메서드는 { + + @Nested + class 다른_Host가_소유한_Space_id를_입력받은_경우 { + + private Long spaceId; + private Host anotherHost; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + spaceId = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")).getId(); + anotherHost = hostRepository.save(Host_생성("4567", 4567L)); + + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> + spaceRepository.getByHostAndId(anotherHost, spaceId)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + + @Nested + class 존재하지_않는_Space_id를_입력받은_경우 { + + private static final long NON_EXIST_SPACE_ID = 0L; + + private Host host; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> + spaceRepository.getByHostAndId(host, NON_EXIST_SPACE_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 공간입니다."); + } + } + + @Nested + class 입력받은_Host가_입력받은_Space_id를_가지고_있는_경우 { + + private Space space; + private Long spaceId; + private Host host; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); + spaceId = space.getId(); + } + + @Test + void Space를_반환한다() { + Space actual = spaceRepository.getByHostAndId(host, spaceId); + assertThat(actual).isEqualTo(space); + } + } + } + + @Nested + class existsByHostAndName_메서드는 { + @Nested + class 입력받은_이름과_같은_이름을_가진_Space를_가지고_있는_경우 { + + private static final String EXIST_SPACE_NAME = "잠실 캠퍼스"; + + private Host host; + private Name name; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + spaceRepository.save(Space_생성(host, EXIST_SPACE_NAME)); + name = new Name(EXIST_SPACE_NAME); + } + + @Test + void 참을_반환한다() { + boolean actual = spaceRepository.existsByHostAndName(host, name); + assertThat(actual).isTrue(); + } + } + + @Nested + class 입력받은_이름과_같은_이름을_가진_Space를_가지고_있지_않은_경우 { + + private Host host; + private Name name; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + name = new Name("없는Space이름"); + } + + @Test + void 거짓을_반환한다() { + boolean actual = spaceRepository.existsByHostAndName(host, name); + assertThat(actual).isFalse(); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionRepositoryTest.java new file mode 100644 index 00000000..8d4dc155 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionRepositoryTest.java @@ -0,0 +1,111 @@ +package com.woowacourse.gongcheck.core.domain.submission; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Submission_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.woowacourse.gongcheck.config.JpaConfig; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; + +@DataJpaTest +@Import(JpaConfig.class) +@DisplayName("SubmissionRepository 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SubmissionRepositoryTest { + + @Autowired + private HostRepository hostRepository; + + @Autowired + private SpaceRepository spaceRepository; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private SubmissionRepository submissionRepository; + + @Nested + class save_메소드는 { + + @Nested + class 입력받은_Submission을_저장하는_경우 { + + private Job job; + private LocalDateTime expected; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + expected = LocalDateTime.now(); + } + + @Test + void 생성시간을_저장한다() { + Submission actual = submissionRepository.save(Submission.builder() + .job(job) + .author("어썸오") + .build()); + assertThat(actual.getCreatedAt()).isAfter(expected); + } + } + } + + @Nested + class findAllByJobIn_메소드는 { + + @Nested + class Job들의_리스트를_입력받는_경우 { + + private static final String SUBMISSION_AUTHOR = "어썸오"; + + private List jobs; + private PageRequest request; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job_1 = jobRepository.save(Job_생성(space, "청소")); + Job job_2 = jobRepository.save(Job_생성(space, "마감")); + jobs = jobRepository.saveAll(List.of(job_1, job_2)); + submissionRepository.saveAll(List.of(Submission_생성(job_1, SUBMISSION_AUTHOR), Submission_생성(job_1, SUBMISSION_AUTHOR), + Submission_생성(job_2, SUBMISSION_AUTHOR), Submission_생성(job_2, SUBMISSION_AUTHOR))); + request = PageRequest.of(0, 2); + } + + @Test + void 입력받은_Job들의_Submission들을_반환한다() { + Slice actual = submissionRepository.findAllByJobIn(jobs, request); + + assertAll( + () -> assertThat(actual.getContent()).hasSize(2), + () -> assertThat(actual.hasNext()).isTrue() + ); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionTest.java new file mode 100644 index 00000000..08e52063 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/submission/SubmissionTest.java @@ -0,0 +1,18 @@ +package com.woowacourse.gongcheck.core.domain.submission; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.gongcheck.exception.BusinessException; +import org.junit.jupiter.api.Test; + +class SubmissionTest { + + @Test + void author의_이름은_10자_이하여야_한다() { + assertThatThrownBy(() -> Submission.builder() + .author("12345678901") + .build()) + .isInstanceOf(BusinessException.class) + .hasMessage("제출자 이름은 10자 이하여야 합니다."); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskRepositoryTest.java new file mode 100644 index 00000000..c3591b4c --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskRepositoryTest.java @@ -0,0 +1,191 @@ +package com.woowacourse.gongcheck.core.domain.task; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; +import static org.assertj.core.api.Assertions.assertThat; + +import com.woowacourse.gongcheck.config.JpaConfig; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.section.SectionRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(JpaConfig.class) +@DisplayName("RunningTaskRepository 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class RunningTaskRepositoryTest { + + @Autowired + private HostRepository hostRepository; + + @Autowired + private SpaceRepository spaceRepository; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TaskRepository taskRepository; + + @Autowired + private RunningTaskRepository runningTaskRepository; + + @Nested + class save_메소드는 { + + @Nested + class 입력받은_RunningTask를_저장할_때 { + + private RunningTask runningTask; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + Task task = taskRepository.save(Task_생성(section, "책상 청소")); + runningTask = RunningTask_생성(task.getId(), false); + } + + @Test + void RunningTask를_저장한다() { + LocalDateTime timeThatBeforeSave = LocalDateTime.now(); + RunningTask actual = runningTaskRepository.save(runningTask); + + assertThat(actual.getCreatedAt()).isAfter(timeThatBeforeSave); + } + } + } + + @Nested + class existsByTaskIdIn_메소드는 { + + @Nested + class RunningTask가_존재하는_TaskIds를_입력받는_경우 { + + private List taskIds; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + Task task = Task_생성(section, "책상 청소"); + taskIds = taskRepository.saveAll(List.of(task)) + .stream() + .map(Task::getId) + .collect(Collectors.toList()); + runningTaskRepository.save(RunningTask_생성(task.getId(), true)); + } + + @Test + void True를_반환한다() { + boolean actual = runningTaskRepository.existsByTaskIdIn(taskIds); + assertThat(actual).isTrue(); + } + } + + @Nested + class RunningTask가_존재하지_않는_TaskIds를_입력받는_경우 { + + private List taskIds; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskIds = taskRepository.saveAll(List.of(Task_생성(section, "책상 청소"))) + .stream() + .map(Task::getId) + .collect(Collectors.toList()); + } + + @Test + void False를_반환한다() { + boolean result = runningTaskRepository.existsByTaskIdIn(taskIds); + assertThat(result).isFalse(); + } + } + } + + @Nested + class findByTaskId_메소드는 { + + @Nested + class 존재하는_RunningTask의_taskId를_입력받은_경우 { + + private Long taskId; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskId = taskRepository.save(Task_생성(section, "책상 청소")) + .getId(); + runningTaskRepository.save(RunningTask_생성(taskId, false)); + } + + @Test + void RunningTask를_반환한다() { + Optional actual = runningTaskRepository.findByTaskId(taskId); + + assertThat(actual).isNotEmpty(); + } + + } + + @Nested + class 존재하지_않는_RunningTask의_taskId를_입력받은_경우 { + + private Long taskId; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskId = taskRepository.save(Task_생성(section, "책상 청소")) + .getId(); + } + + @Test + void 빈_값이_반환된다() { + Optional result = runningTaskRepository.findByTaskId(taskId); + + assertThat(result).isEmpty(); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/task/RunningTaskTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskTest.java similarity index 76% rename from backend/src/test/java/com/woowacourse/gongcheck/domain/task/RunningTaskTest.java rename to backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskTest.java index d6b8d3e6..9df1ba1f 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/task/RunningTaskTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTaskTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.domain.task; +package com.woowacourse.gongcheck.core.domain.task; import static org.assertj.core.api.Assertions.assertThat; @@ -9,7 +9,7 @@ class RunningTaskTest { @ParameterizedTest @CsvSource(value = {"false:true", "true:false"}, delimiter = ':') - void RunningTask의_체크_상태를_변경한다(final boolean isChecked, final boolean expected) { + void RunningTask의_체크상태를_변경한다(final boolean isChecked, final boolean expected) { RunningTask runningTask = RunningTask.builder() .isChecked(isChecked) .build(); diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/task/RunningTasksTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTasksTest.java similarity index 84% rename from backend/src/test/java/com/woowacourse/gongcheck/domain/task/RunningTasksTest.java rename to backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTasksTest.java index 846cccc3..831cb361 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/task/RunningTasksTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/RunningTasksTest.java @@ -1,4 +1,4 @@ -package com.woowacourse.gongcheck.domain.task; +package com.woowacourse.gongcheck.core.domain.task; import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -11,7 +11,7 @@ class RunningTasksTest { @Test - void 모든_테스크가_완료되었는지_확인한다() { + void 모든_RunningTask가_체크상태인지_확인한다() { RunningTask runningTask1 = RunningTask_생성(1L, true); RunningTask runningTask2 = RunningTask_생성(2L, true); RunningTasks runningTasks = new RunningTasks(List.of(runningTask1, runningTask2)); @@ -20,7 +20,7 @@ class RunningTasksTest { } @Test - void 테스크가_하나라도_미완료상태인지_확인한다() { + void 하나의_RunningTask라도_체크상태가_아니면_예외가_발생한다() { RunningTask runningTask1 = RunningTask_생성(1L, true); RunningTask runningTask2 = RunningTask_생성(2L, false); RunningTasks runningTasks = new RunningTasks(List.of(runningTask1, runningTask2)); diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TaskRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TaskRepositoryTest.java new file mode 100644 index 00000000..6facd3ed --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TaskRepositoryTest.java @@ -0,0 +1,269 @@ +package com.woowacourse.gongcheck.core.domain.task; + +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.gongcheck.config.JpaConfig; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.HostRepository; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.job.JobRepository; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.section.SectionRepository; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.space.SpaceRepository; +import com.woowacourse.gongcheck.core.domain.vo.Name; +import com.woowacourse.gongcheck.exception.NotFoundException; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; + +@DataJpaTest +@Import(JpaConfig.class) +@DisplayName("TaskRepository 클래스") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class TaskRepositoryTest { + + @Autowired + private HostRepository hostRepository; + + @Autowired + private SpaceRepository spaceRepository; + + @Autowired + private JobRepository jobRepository; + + @Autowired + private SectionRepository sectionRepository; + + @Autowired + private TaskRepository taskRepository; + + @Nested + class save_메소드는 { + + @Nested + class 입력받은_Task를_저장할_떄 { + + private Task task; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + task = Task_생성(section, "책상 청소"); + } + + @Test + void 생성_시간도_함께_저장한다() { + LocalDateTime timeThatBeforeSave = LocalDateTime.now(); + Task actual = taskRepository.save(task); + assertThat(actual.getCreatedAt()).isAfter(timeThatBeforeSave); + } + } + + @Nested + class 입력받은_Task의_Description이_null인_경우 { + + private Section section; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + section = sectionRepository.save(Section_생성(job, "트랙룸")); + } + + @Test + void 정상적으로_Task를_저장한다() { + Task task = Task.builder() + .name(new Name("책상 닦기")) + .section(section) + .build(); + assertThatCode(() -> taskRepository.save(task)) + .doesNotThrowAnyException(); + } + } + } + + @Nested + class findAllBySectionJob_메소드는 { + + @Nested + class 입력받은_Job이_Task를_가지고_있는_경우 { + + private Job job; + private List expected; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + job = jobRepository.save(Job_생성(space, "청소")); + Section section1 = sectionRepository.save(Section_생성(job, "트랙룸")); + Section section2 = sectionRepository.save(Section_생성(job, "굿샷 강의장")); + expected = List.of( + Task_생성(section1, "책상 청소"), Task_생성(section1, "빈백 정리"), + Task_생성(section2, "책상 청소"), Task_생성(section2, "의자 넣기") + ); + taskRepository.saveAll(expected); + } + + @Test + void 가지고_있는_모든_Task를_반환한다() { + List actual = taskRepository.findAllBySectionJob(job); + + assertThat(actual).hasSize(expected.size()); + } + } + } + + @Nested + class getBySectionJobSpaceHostAndId_메소드는 { + + @Nested + class 존재하는_Host와_TaskId를_받으면 { + + private Host host; + private Task expected; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section1 = sectionRepository.save(Section_생성(job, "트랙룸")); + expected = taskRepository.save(Task_생성(section1, "책상 청소")); + } + + @Test + void Task를_반환한다() { + Task actual = taskRepository.getBySectionJobSpaceHostAndId(host, expected.getId()); + + assertThat(actual).isEqualTo(expected); + } + } + + @Nested + class 존재하지_않는_TaskId를_받으면 { + + private static final long NON_EXIST_TASK_ID = 0L; + + private Host host; + + @BeforeEach + void setUp() { + host = hostRepository.save(Host_생성("1234", 1234L)); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskRepository.getBySectionJobSpaceHostAndId(host, NON_EXIST_TASK_ID)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + + @Nested + class 해당_Host의_Task가_아닌_TaskId를_받으면 { + + private Host anotherHost; + private Long taskId; + + @BeforeEach + void setUp() { + anotherHost = hostRepository.save(Host_생성("1234", 2345L)); + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + Section section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskId = taskRepository.save(Task_생성(section, "책상 청소")) + .getId(); + } + + @Test + void 예외가_발생한다() { + assertThatThrownBy(() -> taskRepository.getBySectionJobSpaceHostAndId(anotherHost, taskId)) + .isInstanceOf(NotFoundException.class) + .hasMessage("존재하지 않는 작업입니다."); + } + } + } + + @Nested + class findAllBySectionIn_메소드는 { + + @Nested + class Section_목록을_받으면 { + + private Section section1; + private Section section2; + private List expected; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + section1 = sectionRepository.save(Section_생성(job, "트랙룸")); + section2 = sectionRepository.save(Section_생성(job, "트랙룸")); + expected = List.of( + Task_생성(section1, "책상 청소"), Task_생성(section1, "빈백 정리"), + Task_생성(section2, "책상 청소"), Task_생성(section2, "의자 넣기")); + taskRepository.saveAll(expected); + } + + @Test + void Section에_해당하는_모든_Task를_조회한다() { + List actual = taskRepository.findAllBySectionIn(List.of(section1, section2)); + + assertThat(actual).hasSize(expected.size()); + } + } + } + + @Nested + class deleteAllBySectionIn_메소드는 { + + @Nested + class Section_목록을_받으면 { + + private Section section; + private Long taskId; + + @BeforeEach + void setUp() { + Host host = hostRepository.save(Host_생성("1234", 1234L)); + Space space = spaceRepository.save(Space_생성(host, "잠실")); + Job job = jobRepository.save(Job_생성(space, "청소")); + section = sectionRepository.save(Section_생성(job, "트랙룸")); + taskId = taskRepository.save(Task_생성(section, "책상 청소")) + .getId(); + } + + @Test + void Section에_해당하는_모든_Task를_삭제한다() { + taskRepository.deleteAllBySectionIn(List.of(section)); + + assertThat(taskRepository.findById(taskId)).isEmpty(); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TaskTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TaskTest.java new file mode 100644 index 00000000..99eef7e7 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TaskTest.java @@ -0,0 +1,14 @@ +package com.woowacourse.gongcheck.core.domain.task; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.junit.jupiter.api.Test; + +class TaskTest { + + @Test + void description과_imageUrl이_null이_들어오는_경우_정상적으로_생성한다() { + assertDoesNotThrow(() -> Task.builder() + .build()); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TasksTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TasksTest.java new file mode 100644 index 00000000..65fbb6fd --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/task/TasksTest.java @@ -0,0 +1,25 @@ +package com.woowacourse.gongcheck.core.domain.task; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class TasksTest { + + @Test + void Tasks_가_비어있다면_true를_반환한다() { + Tasks tasks = new Tasks(List.of()); + + assertThat(tasks.isEmpty()).isTrue(); + } + + @Test + void Tasks_가_비어있다면_false를_반환한다() { + Task task = Task.builder() + .build(); + Tasks tasks = new Tasks(List.of(task)); + + assertThat(tasks.isEmpty()).isFalse(); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/DescriptionTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/DescriptionTest.java new file mode 100644 index 00000000..06f62067 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/DescriptionTest.java @@ -0,0 +1,38 @@ +package com.woowacourse.gongcheck.core.domain.vo; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.gongcheck.exception.BusinessException; +import org.junit.jupiter.api.Test; + +class DescriptionTest { + + @Test + void 설명은_128자를_초과할_수_없다() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 128; i++) { + sb.append(i); + } + String value = sb.toString(); + assertThatThrownBy(() -> new Description(value)) + .isInstanceOf(BusinessException.class) + .hasMessage("설명은 128자 이하여야 합니다."); + } + + @Test + void 설명값이_같으면_같은_객체이다() { + Description description1 = new Description("이것은 설명"); + Description description2 = new Description("이것은 설명"); + + assertThat(description1).isEqualTo(description2); + } + + @Test + void 설명값이_다르면_다른_객체이다() { + Description description1 = new Description("이것은 설명"); + Description description2 = new Description("이것은 다른 설명"); + + assertThat(description1).isNotEqualTo(description2); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/NameTest.java b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/NameTest.java new file mode 100644 index 00000000..c536e6f9 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/core/domain/vo/NameTest.java @@ -0,0 +1,40 @@ +package com.woowacourse.gongcheck.core.domain.vo; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.woowacourse.gongcheck.exception.BusinessException; +import org.junit.jupiter.api.Test; + +class NameTest { + + @Test + void 이름은_10자_이하여야_한다() { + assertThatThrownBy(() -> new Name("12345678901")) + .isInstanceOf(BusinessException.class) + .hasMessage("이름은 10자 이하여야합니다."); + } + + @Test + void 이름은_빈_값일_수_없다() { + assertThatThrownBy(() -> new Name("")) + .isInstanceOf(BusinessException.class) + .hasMessage("이름은 공백일 수 없습니다."); + } + + @Test + void 이름_값이_같으면_같은_객체이다() { + Name name1 = new Name("awesome"); + Name name2 = new Name("awesome"); + + assertThat(name1).isEqualTo(name2); + } + + @Test + void 이름_값이_다르면_다른_객체이다() { + Name name1 = new Name("awesome"); + Name name2 = new Name("notAwesome"); + + assertThat(name1).isNotEqualTo(name2); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/DocumentationTest.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/DocumentationTest.java index ee792652..00776280 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/DocumentationTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/DocumentationTest.java @@ -3,24 +3,26 @@ import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.operation.preprocess.Preprocessors.prettyPrint; -import com.woowacourse.gongcheck.application.AlertService; -import com.woowacourse.gongcheck.application.GuestAuthService; -import com.woowacourse.gongcheck.application.HostAuthService; -import com.woowacourse.gongcheck.application.HostService; -import com.woowacourse.gongcheck.application.ImageUploader; -import com.woowacourse.gongcheck.application.JjwtTokenProvider; -import com.woowacourse.gongcheck.application.JobService; -import com.woowacourse.gongcheck.application.SpaceService; -import com.woowacourse.gongcheck.application.SubmissionService; -import com.woowacourse.gongcheck.application.TaskService; -import com.woowacourse.gongcheck.presentation.AuthenticationContext; -import com.woowacourse.gongcheck.presentation.GuestAuthController; -import com.woowacourse.gongcheck.presentation.HostAuthController; -import com.woowacourse.gongcheck.presentation.HostController; -import com.woowacourse.gongcheck.presentation.JobController; -import com.woowacourse.gongcheck.presentation.SpaceController; -import com.woowacourse.gongcheck.presentation.SubmissionController; -import com.woowacourse.gongcheck.presentation.TaskController; +import com.woowacourse.gongcheck.auth.application.EntranceCodeProvider; +import com.woowacourse.gongcheck.auth.application.GuestAuthService; +import com.woowacourse.gongcheck.auth.application.HostAuthService; +import com.woowacourse.gongcheck.auth.domain.AuthenticationContext; +import com.woowacourse.gongcheck.auth.presentation.GuestAuthController; +import com.woowacourse.gongcheck.auth.presentation.HostAuthController; +import com.woowacourse.gongcheck.core.application.AlertService; +import com.woowacourse.gongcheck.core.application.HostService; +import com.woowacourse.gongcheck.core.application.ImageUploader; +import com.woowacourse.gongcheck.core.application.JobService; +import com.woowacourse.gongcheck.core.application.SpaceService; +import com.woowacourse.gongcheck.core.application.SubmissionService; +import com.woowacourse.gongcheck.core.application.TaskService; +import com.woowacourse.gongcheck.core.presentation.HostController; +import com.woowacourse.gongcheck.core.presentation.ImageUploadController; +import com.woowacourse.gongcheck.core.presentation.JobController; +import com.woowacourse.gongcheck.core.presentation.SpaceController; +import com.woowacourse.gongcheck.core.presentation.SubmissionController; +import com.woowacourse.gongcheck.core.presentation.TaskController; +import com.woowacourse.gongcheck.infrastructure.jwt.JjwtTokenProvider; import io.restassured.module.mockmvc.RestAssuredMockMvc; import io.restassured.module.mockmvc.specification.MockMvcRequestSpecification; import org.junit.jupiter.api.BeforeEach; @@ -39,7 +41,8 @@ JobController.class, TaskController.class, SubmissionController.class, - HostController.class + HostController.class, + ImageUploadController.class }) @ExtendWith(RestDocumentationExtension.class) class DocumentationTest { @@ -48,7 +51,7 @@ class DocumentationTest { @MockBean protected HostAuthService hostAuthService; - + @MockBean protected GuestAuthService guestAuthService; @@ -73,6 +76,9 @@ class DocumentationTest { @MockBean protected JjwtTokenProvider jwtTokenProvider; + @MockBean + protected EntranceCodeProvider entranceCodeProvider; + @MockBean protected AuthenticationContext authenticationContext; diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/GuestAuthDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/GuestAuthDocumentation.java index 14ddf823..59e9be75 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/GuestAuthDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/GuestAuthDocumentation.java @@ -7,20 +7,28 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import com.woowacourse.gongcheck.application.response.GuestTokenResponse; +import com.woowacourse.gongcheck.auth.application.response.GuestTokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.GuestEnterRequest; import com.woowacourse.gongcheck.exception.BusinessException; import com.woowacourse.gongcheck.exception.ErrorResponse; -import com.woowacourse.gongcheck.presentation.request.GuestEnterRequest; import io.restassured.module.mockmvc.response.MockMvcResponse; import io.restassured.response.ExtractableResponse; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; class GuestAuthDocumentation extends DocumentationTest { + private static final String ENTRANCE_CODE = "random_entrance_code"; + @Nested class 게스트_토큰을_요청한다 { @@ -32,9 +40,17 @@ class 게스트_토큰을_요청한다 { docsGiven .contentType(MediaType.APPLICATION_JSON_VALUE) .body(guestEnterRequest) - .when().post("/api/hosts/1/enter") + .when().post("/api/hosts/{entranceCode}/enter", ENTRANCE_CODE) .then().log().all() - .apply(document("guests/auth/success")) + .apply(document("guests/auth/success", + pathParameters( + parameterWithName("entranceCode").description("호스트가 제공하는 입장코드")), + requestFields( + fieldWithPath("password").type(JsonFieldType.STRING).description("공간 비밀번호")), + responseFields( + fieldWithPath("token").type(JsonFieldType.STRING).description("Access Token") + ) + )) .statusCode(HttpStatus.OK.value()); } @@ -47,7 +63,7 @@ class 게스트_토큰을_요청한다 { ExtractableResponse response = docsGiven .contentType(MediaType.APPLICATION_JSON_VALUE) .body(guestEnterRequest) - .when().post("/api/hosts/1/enter") + .when().post("/api/hosts/{entranceCode}/enter", ENTRANCE_CODE) .then().log().all() .apply(document("guests/auth/fail/length")) .extract(); @@ -68,7 +84,7 @@ class 게스트_토큰을_요청한다 { ExtractableResponse response = docsGiven .contentType(MediaType.APPLICATION_JSON_VALUE) .body(guestEnterRequest) - .when().post("/api/hosts/1/enter") + .when().post("/api/hosts/{entranceCode}/enter", ENTRANCE_CODE) .then().log().all() .apply(document("guests/auth/fail/pattern")) .extract(); diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostAuthDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostAuthDocumentation.java index d771bd15..76ab2288 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostAuthDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostAuthDocumentation.java @@ -3,13 +3,17 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; -import com.woowacourse.gongcheck.application.response.TokenResponse; -import com.woowacourse.gongcheck.presentation.request.TokenRequest; +import com.woowacourse.gongcheck.auth.application.response.TokenResponse; +import com.woowacourse.gongcheck.auth.presentation.request.TokenRequest; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; public class HostAuthDocumentation extends DocumentationTest { @@ -27,7 +31,14 @@ class 호스트_토큰을_요청한다 { .body(tokenRequest) .when().post("api/login") .then().log().all() - .apply(document("hosts/auth/success")) + .apply(document("hosts/auth/success", + requestFields( + fieldWithPath("code").type(JsonFieldType.STRING).description("Authorization Code")), + responseFields( + fieldWithPath("token").type(JsonFieldType.STRING).description("Access Token"), + fieldWithPath("alreadyJoin").type(JsonFieldType.BOOLEAN).description("가입 여부") + ) + )) .statusCode(HttpStatus.OK.value()); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostDocumentation.java index 312768df..dcb95bca 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/HostDocumentation.java @@ -6,13 +6,16 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import com.woowacourse.gongcheck.core.presentation.request.SpacePasswordChangeRequest; import com.woowacourse.gongcheck.exception.BusinessException; -import com.woowacourse.gongcheck.presentation.request.SpacePasswordChangeRequest; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; class HostDocumentation extends DocumentationTest { @@ -30,7 +33,7 @@ class 비밀번호_변경 { .body(new SpacePasswordChangeRequest("1234")) .when().patch("/api/spacePassword") .then().log().all() - .apply(document("host/update")) + .apply(document("hosts/spacePassword_update/success")) .statusCode(HttpStatus.NO_CONTENT.value()); } @@ -47,7 +50,7 @@ class 비밀번호_변경 { .body(new SpacePasswordChangeRequest("12345")) .when().patch("/api/spacePassword") .then().log().all() - .apply(document("host/update")) + .apply(document("hosts/spacePassword_update/fail/password_length")) .statusCode(HttpStatus.BAD_REQUEST.value()); } @@ -64,8 +67,25 @@ class 비밀번호_변경 { .body(new SpacePasswordChangeRequest("가나다라")) .when().patch("/api/spacePassword") .then().log().all() - .apply(document("host/update")) + .apply(document("hosts/spacePassword_update/fail/password_pattern")) .statusCode(HttpStatus.BAD_REQUEST.value()); } } + + @Test + void 호스트_입장코드를_조회한다() { + when(hostService.createEntranceCode(anyLong())).thenReturn("random_entrance_code"); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + docsGiven + .header("Authorization", "Bearer jwt.token.here") + .when().get("/api/hosts/entranceCode") + .then().log().all() + .apply(document("hosts/entranceCode", + responseFields( + fieldWithPath("entranceCode").type(JsonFieldType.STRING).description("입장코드") + ) + )) + .statusCode(HttpStatus.OK.value()); + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/ImageUploadDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/ImageUploadDocumentation.java new file mode 100644 index 00000000..05e44329 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/ImageUploadDocumentation.java @@ -0,0 +1,47 @@ +package com.woowacourse.gongcheck.documentation; + +import static com.woowacourse.gongcheck.FakeImageFactory.createFakeImage; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.partWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParts; + +import com.woowacourse.gongcheck.core.application.response.ImageUrlResponse; +import java.io.File; +import java.io.IOException; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; + +class ImageUploadDocumentation extends DocumentationTest { + + @Test + void 이미지를_업로드한다() throws IOException { + File fakeImage = createFakeImage(); + when(imageUploader.upload(any(), anyString())) + .thenReturn(ImageUrlResponse.from("https://image.gongcheck.com/12sdf124sx.jpg")); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + docsGiven + .header(AUTHORIZATION, "Bearer jwt.token.here") + .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .multiPart("image", fakeImage, "image/jpg") + .when().post("/api/imageUpload") + .then().log().all() + .apply(document("image-upload", + requestParts(partWithName("image") + .description("The version of the image")), + responseFields( + fieldWithPath("imageUrl").type(JsonFieldType.STRING).description("저장된 Image Url") + ) + )) + .statusCode(HttpStatus.OK.value()); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/JobDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/JobDocumentation.java index cb2adaaf..f0d4e10e 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/JobDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/JobDocumentation.java @@ -6,22 +6,37 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import com.woowacourse.gongcheck.application.response.JobsResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.presentation.request.JobCreateRequest; -import com.woowacourse.gongcheck.presentation.request.SectionCreateRequest; -import com.woowacourse.gongcheck.presentation.request.TaskCreateRequest; +import com.woowacourse.gongcheck.core.application.response.JobsResponse; +import com.woowacourse.gongcheck.core.application.response.SlackUrlResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.presentation.request.JobCreateRequest; +import com.woowacourse.gongcheck.core.presentation.request.SectionCreateRequest; +import com.woowacourse.gongcheck.core.presentation.request.SlackUrlChangeRequest; +import com.woowacourse.gongcheck.core.presentation.request.TaskCreateRequest; +import com.woowacourse.gongcheck.exception.BusinessException; import io.restassured.module.mockmvc.response.MockMvcResponse; import io.restassured.response.ExtractableResponse; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; class JobDocumentation extends DocumentationTest { @@ -32,31 +47,42 @@ class Job을_조회한다 { void Job_조회에_성공한다() { Host host = Host_생성("1234", 1234L); Space space = Space_생성(host, "잠실"); - when(jobService.findPage(anyLong(), anyLong(), any())).thenReturn( - JobsResponse.of(List.of( - Job_아이디_지정_생성(1L, space, "청소"), - Job_아이디_지정_생성(2L, space, "마감")), - true) + when(jobService.findJobs(anyLong(), anyLong())).thenReturn( + JobsResponse.from(List.of( + Job_아이디_지정_생성(1L, space, "청소"), + Job_아이디_지정_생성(2L, space, "마감")) + ) ); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven .header("Authorization", "Bearer jwt.token.here") - .queryParam("page", 0) - .queryParam("size", 2) - .when().get("/api/spaces/1/jobs") + .when().get("/api/spaces/{spaceId}/jobs", 1) .then().log().all() - .apply(document("jobs/list")) + .apply(document("jobs/list", + pathParameters( + parameterWithName("spaceId").description("Job 목록을 조회할 Space Id")), + responseFields( + fieldWithPath("jobs.[].id").type(JsonFieldType.NUMBER).description("Job id"), + fieldWithPath("jobs.[].name").type(JsonFieldType.STRING).description("Job 이름") + + ) + )) .statusCode(HttpStatus.OK.value()); } } @Nested class Job을_생성_시 { - List tasks1 = List.of(new TaskCreateRequest("책상 닦기"), new TaskCreateRequest("칠판 닦기")); - List tasks2 = List.of(new TaskCreateRequest("책상 닦기"), new TaskCreateRequest("칠판 닦기")); - List sections = List.of(new SectionCreateRequest("대강의실", tasks1), - new SectionCreateRequest("소강의실", tasks2)); + List tasks1 = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List tasks2 = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List + .of(new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks1), + new SectionCreateRequest("소강의실", " 소강의실 설명", "https://image.gongcheck.shop/sogang123", tasks2)); @Test void Job을_생성한다() { @@ -67,16 +93,38 @@ class Job을_생성_시 { .header("Authorization", "Bearer jwt.token.here") .contentType(MediaType.APPLICATION_JSON_VALUE) .body(request) - .when().post("/api/spaces/1/jobs") + .when().post("/api/spaces/{spaceId}/jobs", 1) .then().log().all() - .apply(document("jobs/list")) + .apply(document("jobs/create/success", + pathParameters( + parameterWithName("spaceId").description("Job을 생성할 Space Id")), + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("Job 이름"), + fieldWithPath("sections.[].name").type(JsonFieldType.STRING) + .description("Section 이름"), + fieldWithPath("sections.[].description").type(JsonFieldType.STRING) + .description("Section 설명"), + fieldWithPath("sections.[].imageUrl").type(JsonFieldType.STRING) + .description("Section Image Url"), + fieldWithPath("sections.[].tasks.[].name").type(JsonFieldType.STRING) + .description("Task 이름"), + fieldWithPath("sections.[].tasks.[].description").type(JsonFieldType.STRING) + .description("Task 설명"), + fieldWithPath("sections.[].tasks.[].imageUrl").type(JsonFieldType.STRING) + .description("Task Image Url") + ) + )) .statusCode(HttpStatus.CREATED.value()); } @Test void Job의_이름_길이가_올바르지_않을_경우_예외가_발생한다() { + doThrow(BusinessException.class) + .when(jobService) + .createJob(anyLong(), anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); - JobCreateRequest wrongRequest = new JobCreateRequest("작업의 이름이 20글자 초과한다면 예외", sections); + + JobCreateRequest wrongRequest = new JobCreateRequest("10자초과의이름은안돼", sections); ExtractableResponse response = docsGiven .header("Authorization", "Bearer jwt.token.here") @@ -84,7 +132,7 @@ class Job을_생성_시 { .body(wrongRequest) .when().post("/api/spaces/1/jobs") .then().log().all() - .apply(document("jobs/list")) + .apply(document("jobs/create/fail/job_name_length")) .extract(); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); @@ -92,8 +140,13 @@ class Job을_생성_시 { @Test void Section_이름_길이가_올바르지_않을_경우_예외가_발생한다() { + doThrow(BusinessException.class) + .when(jobService) + .createJob(anyLong(), anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); - List sections = List.of(new SectionCreateRequest("Section의 name이 20자 초과", tasks1)); + + List sections = List.of(new SectionCreateRequest("10자초과의이름은안돼", "대강의실 설명", + "https://image.gongcheck.shop/degang123", tasks1)); JobCreateRequest wrongRequest = new JobCreateRequest("청소", sections); ExtractableResponse response = docsGiven @@ -102,7 +155,7 @@ class Job을_생성_시 { .body(wrongRequest) .when().post("/api/spaces/1/jobs") .then().log().all() - .apply(document("jobs/list")) + .apply(document("jobs/create/fail/section_name_length")) .extract(); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); @@ -110,10 +163,16 @@ class Job을_생성_시 { @Test void Task_이름_길이가_올바르지_않을_경우_예외가_발생한다() { + doThrow(BusinessException.class) + .when(jobService) + .createJob(anyLong(), anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); - List tasks1 = List.of( - new TaskCreateRequest("Task의 이름이 1글자 미만 50글자 초과일 경우, Status Code 404를 반환한다")); - List sections = List.of(new SectionCreateRequest("대강의실", tasks1)); + + List tasks1 = List + .of(new TaskCreateRequest("10자초과의이름은안돼", "책상 닦기 설명", + "https://image.gongcheck.shop/checksang123")); + List sections = List + .of(new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks1)); JobCreateRequest wrongRequest = new JobCreateRequest("청소", sections); ExtractableResponse response = docsGiven @@ -122,10 +181,220 @@ class Job을_생성_시 { .body(wrongRequest) .when().post("/api/spaces/1/jobs") .then().log().all() - .apply(document("jobs/list")) + .apply(document("jobs/create/fail/task_name_length")) .extract(); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } } + + @Nested + class Job을_수정_시 { + List tasks1 = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List tasks2 = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List + .of(new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks1), + new SectionCreateRequest("소강의실", "소강의실 설명", "https://image.gongcheck.shop/sogang123", tasks2)); + + @Test + void 성공적으로_수정한다() { + List tasks1 = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List tasks2 = List + .of(new TaskCreateRequest("책상 닦기", "책상 닦기 설명", "https://image.gongcheck.shop/checksang123"), + new TaskCreateRequest("칠판 닦기", "칠판 닦기 설명", "https://image.gongcheck.shop/chilpan123")); + List sections = List + .of(new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks1), + new SectionCreateRequest("소강의실", "소강의실 설명", "https://image.gongcheck.shop/sogang123", + tasks2)); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + JobCreateRequest request = new JobCreateRequest("청소", sections); + + docsGiven + .header("Authorization", "Bearer jwt.token.here") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when().put("/api/jobs/{jobId}", 1) + .then().log().all() + .apply(document("jobs/change/success", + pathParameters( + parameterWithName("jobId").description("수정할 Job Id")), + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING).description("Job 이름"), + fieldWithPath("sections.[].name").type(JsonFieldType.STRING) + .description("Section 이름"), + fieldWithPath("sections.[].description").type(JsonFieldType.STRING) + .description("Section 설명"), + fieldWithPath("sections.[].imageUrl").type(JsonFieldType.STRING) + .description("Section Image Url"), + fieldWithPath("sections.[].tasks.[].name").type(JsonFieldType.STRING) + .description("Task 이름"), + fieldWithPath("sections.[].tasks.[].description").type(JsonFieldType.STRING) + .description("Task 설명"), + fieldWithPath("sections.[].tasks.[].imageUrl").type(JsonFieldType.STRING) + .description("Task Image Url") + ) + )) + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", "10자초과의이름은안돼"}) + void Job_이름이_1글자_미만_10글자_초과_nul_인_경우_예외가_발생한다(final String input) { + doThrow(BusinessException.class) + .when(jobService) + .updateJob(anyLong(), anyLong(), any()); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + JobCreateRequest wrongRequest = new JobCreateRequest(input, sections); + + ExtractableResponse response = docsGiven + .header("Authorization", "Bearer jwt.token.here") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(wrongRequest) + .when().put("/api/jobs/{jobId}", 1) + .then().log().all() + .apply(document("jobs/change/fail/job_name_length")) + .extract(); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", "10자초과의이름은안돼"}) + void Section_이름이_1글자_미만_10글자_초과_null_일_경우_예외가_발생한다(final String input) { + doThrow(BusinessException.class) + .when(jobService) + .updateJob(anyLong(), anyLong(), any()); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + List sections = List + .of(new SectionCreateRequest(input, "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks1)); + JobCreateRequest wrongRequest = new JobCreateRequest("청소", sections); + + ExtractableResponse response = docsGiven + .header("Authorization", "Bearer jwt.token.here") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(wrongRequest) + .when().put("/api/jobs/{jobId}", 1) + .then().log().all() + .apply(document("jobs/change/fail/section_name_length")) + .extract(); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", "10자초과의이름은안돼"}) + void Task_이름이_1글자_미만_10글자_초과하거나_null일_경우_예외가_발생한다(final String input) { + doThrow(BusinessException.class) + .when(jobService) + .updateJob(anyLong(), anyLong(), any()); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + List tasks1 = List.of( + new TaskCreateRequest(input, "책상 닦기 설명", "https://image.gongcheck.shop/checksang123")); + List sections = List + .of(new SectionCreateRequest("대강의실", "대강의실 설명", "https://image.gongcheck.shop/degang123", tasks1)); + JobCreateRequest wrongRequest = new JobCreateRequest("청소", sections); + + ExtractableResponse response = docsGiven + .header("Authorization", "Bearer jwt.token.here") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(wrongRequest) + .when().put("/api/jobs/{jobId}", 1) + .then().log().all() + .apply(document("jobs/change/fail/task_name_length")) + .extract(); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); + } + } + + @Test + void Job을_삭제한다() { + doNothing().when(jobService).removeJob(anyLong(), anyLong()); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + docsGiven + .header(AUTHORIZATION, "Bearer jwt.token.here") + .when().delete("/api/jobs/{jobId}", 1) + .then().log().all() + .apply(document("jobs/delete", + pathParameters( + parameterWithName("jobId").description("삭제할 Job Id")) + )) + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + void Job의_Slack_Url을_조회한다() { + when(jobService.findSlackUrl(anyLong(), anyLong())).thenReturn(new SlackUrlResponse("http://slackurl.com")); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + ExtractableResponse response = docsGiven + .header("Authorization", "Bearer jwt.token.here") + .when().get("/api/jobs/{jobId}/slack", 1) + .then().log().all() + .apply(document("jobs/slack_url", + pathParameters( + parameterWithName("jobId").description("Slack Url을 조회할 Job Id")), + responseFields( + fieldWithPath("slackUrl").type(JsonFieldType.STRING).description("Slack Url") + ))) + .extract(); + + assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value()); + } + + @Nested + class SlackUrl_수정_시 { + + @Test + void 정상적으로_수정한다() { + doNothing().when(jobService).changeSlackUrl(anyLong(), anyLong(), any()); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + SlackUrlChangeRequest request = new SlackUrlChangeRequest("https://newslackurl.com"); + + docsGiven + .header("Authorization", "Bearer jwt.token.here") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when().put("/api/jobs/{jobId}/slack", 1) + .then().log().all() + .apply(document("jobs/change_slack_url/success", + pathParameters( + parameterWithName("jobId").description("Slack Url을 수정할 Job Id")), + requestFields( + fieldWithPath("slackUrl").type(JsonFieldType.STRING).description("수정할 Slack Url") + ) + )) + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + void null이_전달될_경우_예외가_발생한다() { + doNothing().when(jobService).changeSlackUrl(anyLong(), anyLong(), any()); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + SlackUrlChangeRequest wrongRequest = new SlackUrlChangeRequest(null); + + docsGiven + .header("Authorization", "Bearer jwt.token.here") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(wrongRequest) + .when().put("/api/jobs/1/slack") + .then().log().all() + .apply(document("jobs/change_slack_url/fail")) + .statusCode(HttpStatus.BAD_REQUEST.value()); + } + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/SpaceDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/SpaceDocumentation.java index 143ecc31..8c6c2493 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/SpaceDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/SpaceDocumentation.java @@ -5,107 +5,213 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import com.woowacourse.gongcheck.application.response.SpacesResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import java.io.File; -import java.io.IOException; +import com.woowacourse.gongcheck.core.application.response.SpaceResponse; +import com.woowacourse.gongcheck.core.application.response.SpacesResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.presentation.request.SpaceChangeRequest; +import com.woowacourse.gongcheck.core.presentation.request.SpaceCreateRequest; +import com.woowacourse.gongcheck.exception.BusinessException; import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; class SpaceDocumentation extends DocumentationTest { @Nested - class 공간을_조회한다 { + class Space를_조회한다 { @Test - void 공간_조회에_성공한다() { + void Space_조회에_성공한다() { Host host = Host_생성("1234", 1234L); - when(spaceService.findPage(anyLong(), any())).thenReturn( - SpacesResponse.of(List.of( + when(spaceService.findSpaces(anyLong())).thenReturn( + SpacesResponse.from(List.of( Space_아이디_지정_생성(1L, host, "잠실"), - Space_아이디_지정_생성(2L, host, "선릉")), - true) + Space_아이디_지정_생성(2L, host, "선릉")) + ) ); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven .header(AUTHORIZATION, "Bearer jwt.token.here") - .queryParam("page", 0) - .queryParam("size", 2) .when().get("/api/spaces") .then().log().all() - .apply(document("spaces/list")) + .apply(document("spaces/list", + responseFields( + fieldWithPath("spaces.[].id").type(JsonFieldType.NUMBER) + .description("Space Id"), + fieldWithPath("spaces.[].name").type(JsonFieldType.STRING) + .description("Space 이름"), + fieldWithPath("spaces.[].imageUrl").type(JsonFieldType.STRING) + .description("Space Image Url") + ))) .statusCode(HttpStatus.OK.value()); } } @Nested - class 공간을_생성한다 { + class Space를_생성한다 { @Test - void 공간_생성에_성공한다() throws IOException { - File fakeImage = File.createTempFile("temp", ".jpg"); + void Space_생성에_성공한다() { when(spaceService.createSpace(anyLong(), any())).thenReturn(1L); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + SpaceCreateRequest request = new SpaceCreateRequest("잠실 캠퍼스", "https://image.gongcheck.shop/123sdf5"); + docsGiven .header(AUTHORIZATION, "Bearer jwt.token.here") - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .param("name", "잠실 캠퍼스") - .multiPart("image", fakeImage) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) .when().post("/api/spaces") .then().log().all() - .apply(document("spaces/create")) + .apply(document("spaces/create/success", + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING) + .description("Space 이름"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING) + .description("Space Image Url") + ) + )) .statusCode(HttpStatus.CREATED.value()); } @Test - void 공간_이름이_null_인_경우_생성에_실패한다() { - when(spaceService.createSpace(anyLong(), any())).thenReturn(1L); + void Space_이름이_null_인_경우_생성에_실패한다() { when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + SpaceCreateRequest request = new SpaceCreateRequest(null, "https://image.gongcheck.shop/123sdf5"); + docsGiven .header(AUTHORIZATION, "Bearer jwt.token.here") - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) .when().post("/api/spaces") .then().log().all() - .apply(document("spaces/create")) + .apply(document("spaces/create/fail/name_null")) .statusCode(HttpStatus.BAD_REQUEST.value()); } @Test - void 공간_이름이_빈_값_인_경우_생성에_실패한다() { - when(spaceService.createSpace(anyLong(), any())).thenReturn(1L); + void Space_이름이_빈_값_인_경우_생성에_실패한다() { + doThrow(BusinessException.class) + .when(spaceService) + .createSpace(anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + SpaceCreateRequest request = new SpaceCreateRequest("", "https://image.gongcheck.shop/123sdf5"); + docsGiven .header(AUTHORIZATION, "Bearer jwt.token.here") - .contentType(MediaType.MULTIPART_FORM_DATA_VALUE) - .param("name", "") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) .when().post("/api/spaces") .then().log().all() - .apply(document("spaces/create")) + .apply(document("spaces/create/fail/name_blank")) + .statusCode(HttpStatus.BAD_REQUEST.value()); + } + } + + @Nested + class 단일_Space를_조회한다 { + + @Test + void 조회에_성공한다() { + Host host = Host_생성("1234", 1234L); + when(spaceService.findSpace(anyLong(), anyLong())) + .thenReturn(SpaceResponse.from(Space_아이디_지정_생성(1L, host, "잠실 캠퍼스"))); + when(authenticationContext.getPrincipal()) + .thenReturn(String.valueOf(anyLong())); + + docsGiven + .header(AUTHORIZATION, "Bearer jwt.token.here") + .when().get("/api/spaces/{spaceId}", 1) + .then().log().all() + .apply(document("spaces/find", + pathParameters( + parameterWithName("spaceId").description("조회할 Space Id")), + responseFields( + fieldWithPath("id").type(JsonFieldType.NUMBER) + .description("Space Id"), + fieldWithPath("name").type(JsonFieldType.STRING) + .description("Space 이름"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING) + .description("Space Image Url") + ) + )) + .statusCode(HttpStatus.OK.value()); + } + } + + @Nested + class Space를_수정한다 { + + @Test + void Space_수정에_성공한다() { + SpaceChangeRequest request = new SpaceChangeRequest("잠실 캠퍼스", "https://image.gongcheck.shop/123sdf5"); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + docsGiven + .header(AUTHORIZATION, "Bearer jwt.token.here") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when().put("/api/spaces/{spaceId}", 1) + .then().log().all() + .apply(document("spaces/change/success", + pathParameters( + parameterWithName("spaceId").description("수정할 Space Id")), + requestFields( + fieldWithPath("name").type(JsonFieldType.STRING) + .description("Space 이름"), + fieldWithPath("imageUrl").type(JsonFieldType.STRING) + .description("Space Image Url") + ) + )) + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + void Space_이름이_null_인_경우_수정에_실패한다() { + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + SpaceChangeRequest request = new SpaceChangeRequest(null, "https://image.gongcheck.shop/123sdf5"); + + docsGiven + .header(AUTHORIZATION, "Bearer jwt.token.here") + .contentType(MediaType.APPLICATION_JSON_VALUE) + .body(request) + .when().put("/api/spaces/1") + .then().log().all() + .apply(document("spaces/change/fail/name_null")) .statusCode(HttpStatus.BAD_REQUEST.value()); } } @Test - void 공간을_삭제한다() { + void Space를_삭제한다() { doNothing().when(spaceService).removeSpace(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven .header(AUTHORIZATION, "Bearer jwt.token.here") - .when().delete("/api/spaces/1") + .when().delete("/api/spaces/{spaceId}", 1) .then().log().all() - .apply(document("spaces/delete")) + .apply(document("spaces/delete", + pathParameters( + parameterWithName("spaceId").description("삭제할 Space Id")) + )) .statusCode(HttpStatus.NO_CONTENT.value()); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/SubmissionDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/SubmissionDocumentation.java index 03291340..abf82e7c 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/SubmissionDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/SubmissionDocumentation.java @@ -3,6 +3,7 @@ import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_아이디_지정_생성; import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_아이디_지정_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Submission_아이디_지정_생성; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.ArgumentMatchers.any; @@ -10,36 +11,45 @@ import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; - -import com.woowacourse.gongcheck.application.response.SubmissionResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.space.Space; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.requestFields; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; + +import com.woowacourse.gongcheck.core.application.response.SubmissionCreatedResponse; +import com.woowacourse.gongcheck.core.application.response.SubmissionsResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.submission.Submission; +import com.woowacourse.gongcheck.core.presentation.request.SubmissionRequest; import com.woowacourse.gongcheck.exception.BusinessException; import com.woowacourse.gongcheck.exception.ErrorResponse; -import com.woowacourse.gongcheck.presentation.request.SubmissionRequest; import io.restassured.module.mockmvc.response.MockMvcResponse; import io.restassured.response.ExtractableResponse; +import java.util.List; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; class SubmissionDocumentation extends DocumentationTest { @Nested - class 작업을_제출한다 { + class Submission을_생성한다 { @Test - void 현재_진행중인_작업이_모두_완료된_상태로_제출하면_제출에_성공한다() { + void RunningTaks가_모두_체크상태이면_성공한다() { Host host = Host_생성("1234", 1234L); Space space = Space_아이디_지정_생성(1L, host, "잠실"); Job job = Job_아이디_지정_생성(1L, space, "청소"); - SubmissionResponse response = SubmissionResponse.of("author", job); - when(submissionService.submitJobCompletion(anyLong(), anyLong(), any())).thenReturn(response); - doNothing().when(alertService).sendMessage(response); + SubmissionCreatedResponse response = SubmissionCreatedResponse.of("author", job); + doNothing().when(submissionService).submitJobCompletion(anyLong(), anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); SubmissionRequest submissionRequest = new SubmissionRequest("제출자"); @@ -47,14 +57,24 @@ class 작업을_제출한다 { .header("Authorization", "Bearer jwt.token.here") .contentType(MediaType.APPLICATION_JSON_VALUE) .body(submissionRequest) - .when().post("/api/jobs/1/complete") + .when().post("/api/jobs/{jobId}/complete", 1) .then().log().all() - .apply(document("submissions/submit/success")) + .apply(document("submissions/submit/success", + pathParameters( + parameterWithName("jobId").description("Submission을 제출할 Job Id")), + requestFields( + fieldWithPath("author").type(JsonFieldType.STRING) + .description("제출자") + ) + )) .statusCode(HttpStatus.OK.value()); } @Test - void 제출자_이름의_길이가_올바르지_않을_경우_예외가_발생한다() { + void author_길이가_올바르지_않은_경우_예외가_발생한다() { + doThrow(new BusinessException("제출자 이름의 길이가 올바르지 않습니다.")) + .when(submissionService) + .submitJobCompletion(anyLong(), anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); SubmissionRequest submissionRequest = new SubmissionRequest("123456789123456789123"); @@ -75,7 +95,7 @@ class 작업을_제출한다 { } @Test - void 제출자_이름이_null_일_경우_예외가_발생한다() { + void author가_null_일_경우_예외가_발생한다() { when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); SubmissionRequest submissionRequest = new SubmissionRequest(null); @@ -96,7 +116,7 @@ class 작업을_제출한다 { } @Test - void 현재_진행중인_작업이_없는데_제출을_시도할_경우_예외가_발생한다() { + void RunningTask가_존재하지_않으면_예외가_발생한다() { doThrow(new BusinessException("현재 제출할 수 있는 진행중인 작업이 존재하지 않습니다.")) .when(submissionService) .submitJobCompletion(anyLong(), anyLong(), any()); @@ -120,7 +140,7 @@ class 작업을_제출한다 { } @Test - void 현재_진행중인_작업을_미완료_상태로_제출을_시도할_경우_예외가_발생한다() { + void 모든_RunningTask가_체크상태가_아니면_예외가_발생한다() { doThrow(new BusinessException("모든 작업이 완료되지않아 제출이 불가합니다.")) .when(submissionService) .submitJobCompletion(anyLong(), anyLong(), any()); @@ -143,4 +163,47 @@ class 작업을_제출한다 { ); } } + + @Nested + class Submission_목록_조회 { + + @Test + void Submission_목록_조회에_성공한다() { + Host host = Host_생성("1234", 1234L); + Space space = Space_아이디_지정_생성(1L, host, "잠실"); + Job job1 = Job_아이디_지정_생성(1L, space, "청소"); + Job job2 = Job_아이디_지정_생성(2L, space, "마감"); + Submission submission1 = Submission_아이디_지정_생성(1L, job1); + Submission submission2 = Submission_아이디_지정_생성(2L, job2); + SubmissionsResponse response = SubmissionsResponse.of(List.of(submission1, submission2), true); + + when(submissionService.findPage(anyLong(), anyLong(), any())).thenReturn(response); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + docsGiven + .header(AUTHORIZATION, "Bearer jwt.token.here") + .queryParam("page", 0) + .queryParam("size", 2) + .when().get("/api/spaces/{spaceId}/submissions", 1) + .then().log().all() + .apply(document("submissions/list", + pathParameters( + parameterWithName("spaceId").description("Submission 목록을 조회할 Space Id")), + responseFields( + fieldWithPath("submissions.[].submissionId").type(JsonFieldType.NUMBER) + .description("Submission Id"), + fieldWithPath("submissions.[].jobId").type(JsonFieldType.NUMBER) + .description("Job Id"), + fieldWithPath("submissions.[].jobName").type(JsonFieldType.STRING) + .description("Job 이름"), + fieldWithPath("submissions.[].author").type(JsonFieldType.STRING) + .description("제출자"), + fieldWithPath("submissions.[].createdAt").type(JsonFieldType.STRING) + .description("제출 날짜"), + fieldWithPath("hasNext").type(JsonFieldType.BOOLEAN) + .description("다음 페이지 존재 여부") + ))) + .statusCode(HttpStatus.OK.value()); + } + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/documentation/TaskDocumentation.java b/backend/src/test/java/com/woowacourse/gongcheck/documentation/TaskDocumentation.java index 4178f9d5..ee73fba2 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/documentation/TaskDocumentation.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/documentation/TaskDocumentation.java @@ -7,6 +7,7 @@ import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_아이디_지정_생성; import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; +import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_아이디_지정_생성; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -14,16 +15,21 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; -import com.woowacourse.gongcheck.application.response.JobActiveResponse; -import com.woowacourse.gongcheck.application.response.RunningTasksResponse; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.section.Section; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.task.RunningTask; -import com.woowacourse.gongcheck.domain.task.Task; -import com.woowacourse.gongcheck.domain.task.Tasks; +import com.woowacourse.gongcheck.core.application.response.JobActiveResponse; +import com.woowacourse.gongcheck.core.application.response.RunningTasksResponse; +import com.woowacourse.gongcheck.core.application.response.TasksResponse; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.task.RunningTask; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.task.Tasks; import com.woowacourse.gongcheck.exception.BusinessException; import com.woowacourse.gongcheck.exception.NotFoundException; import io.restassured.module.mockmvc.response.MockMvcResponse; @@ -33,27 +39,31 @@ import org.junit.jupiter.api.Test; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; +import org.springframework.restdocs.payload.JsonFieldType; class TaskDocumentation extends DocumentationTest { @Nested - class 진행_작업을_생성한다 { + class RunningTask를_생성한다 { @Test - void 새_진행_작업_생성에_성공한다() { + void RunningTask_생성에_성공한다() { doNothing().when(taskService).createNewRunningTasks(anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven .header("Authorization", "Bearer jwt.token.here") - .when().post("/api/jobs/1/tasks/new") + .when().post("/api/jobs/{jobId}/runningTasks/new", 1) .then().log().all() - .apply(document("tasks/create/success")) + .apply(document("runningTasks/create/success", + pathParameters( + parameterWithName("jobId").description("RunningTask를 생성할 Job Id")) + )) .statusCode(HttpStatus.CREATED.value()); } @Test - void 이미_존재하는_진행작업이_있는데_생성하는_경우_예외가_발생한다() { + void 이미_RunningTask가_존재하는데_새로운_RunningTask를_생성하려는_경우_예외가_발생한다() { doThrow(new BusinessException("현재 진행중인 작업이 존재하여 새로운 작업을 생성할 수 없습니다.")).when(taskService) .createNewRunningTasks(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -61,9 +71,9 @@ class 진행_작업을_생성한다 { ExtractableResponse response = docsGiven .header("Authorization", "Bearer jwt.token.here") .contentType(MediaType.APPLICATION_JSON_VALUE) - .when().post("/api/jobs/1/tasks/new") + .when().post("/api/jobs/1/runningTasks/new") .then().log().all() - .apply(document("tasks/create/fail/active")) + .apply(document("runningTasks/create/fail/active")) .extract(); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); @@ -71,27 +81,34 @@ class 진행_작업을_생성한다 { } @Nested - class 작업의_진행_여부를_확인한다 { + class RunningTask_생성_여부를_확인한다 { @Test - void 작업_진행_여부_확인() { + void RunningTask_생성_여부_확인() { when(taskService.isJobActivated(anyLong(), any())).thenReturn(JobActiveResponse.from(true)); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven .header("Authorization", "Bearer jwt.token.here") - .when().get("/api/jobs/1/active") + .when().get("/api/jobs/{jobId}/active", 1) .then().log().all() - .apply(document("tasks/active/success")) + .apply(document("runningTasks/active/success", + pathParameters( + parameterWithName("jobId").description("RunningTask 생성 여부를 확인할 Job Id")), + responseFields( + fieldWithPath("active").type(JsonFieldType.BOOLEAN) + .description("RunningTask 진행 여부") + ) + )) .statusCode(HttpStatus.OK.value()); } } @Nested - class 진행중인_작업을_조회한다 { + class Running_Task를_조회한다 { @Test - void 진행중인_작업이_있으면_조회에_성공한다() { + void RunningTask가_존재하면_성공적으로_조회한다() { Host host = Host_생성("1234", 1234L); Space space = Space_생성(host, "잠실"); Job job = Job_생성(space, "청소"); @@ -108,14 +125,37 @@ class 진행중인_작업을_조회한다 { docsGiven .header("Authorization", "Bearer jwt.token.here") - .when().get("/api/jobs/1/tasks") + .when().get("/api/jobs/{jobId}/runningTasks", 1) .then().log().all() - .apply(document("tasks/find/success")) + .apply(document("runningTasks/find/success", + pathParameters( + parameterWithName("jobId").description("해당 RunningTask를 조회할 Job Id")), + responseFields( + fieldWithPath("sections.[].id").type(JsonFieldType.NUMBER) + .description("Section Id"), + fieldWithPath("sections.[].name").type(JsonFieldType.STRING) + .description("Section 이름"), + fieldWithPath("sections.[].imageUrl").type(JsonFieldType.STRING) + .description("Section Image Url"), + fieldWithPath("sections.[].description").type(JsonFieldType.STRING) + .description("Section 설명"), + fieldWithPath("sections.[].tasks.[].id").type(JsonFieldType.NUMBER) + .description("Task Id"), + fieldWithPath("sections.[].tasks.[].name").type(JsonFieldType.STRING) + .description("Task 이름"), + fieldWithPath("sections.[].tasks.[].imageUrl").type(JsonFieldType.STRING) + .description("Task Image Url"), + fieldWithPath("sections.[].tasks.[].description").type(JsonFieldType.STRING) + .description("Task 설명"), + fieldWithPath("sections.[].tasks.[].checked").type(JsonFieldType.BOOLEAN) + .description("완료 여부") + ) + )) .statusCode(HttpStatus.OK.value()); } @Test - void 존재하는_진행작업이_없는데_조회하는_경우_예외가_발생한다() { + void RunningTask가_존재하지_않는_상태에서_조회하려는_경우_예외가_발생한다() { doThrow(new BusinessException("현재 진행중인 작업이 존재하지 않아 조회할 수 없습니다")).when(taskService) .findRunningTasks(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -123,9 +163,9 @@ class 진행중인_작업을_조회한다 { ExtractableResponse response = docsGiven .header("Authorization", "Bearer jwt.token.here") .contentType(MediaType.APPLICATION_JSON_VALUE) - .when().get("/api/jobs/1/tasks") + .when().get("/api/jobs/1/runningTasks") .then().log().all() - .apply(document("tasks/find/fail/active")) + .apply(document("runningTasks/find/fail/active")) .extract(); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); @@ -133,23 +173,26 @@ class 진행중인_작업을_조회한다 { } @Nested - class 단일_작업을_체크한다 { + class RunningTask의_체크상태를_변경한다 { @Test - void 진행중인_단일_작업이라면_체크에_성공한다() { + void 체크상태_변경에_성공한다() { doNothing().when(taskService).flipRunningTask(anyLong(), any()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); docsGiven .header("Authorization", "Bearer jwt.token.here") - .when().post("/api/tasks/1/flip") + .when().post("/api/tasks/{taskId}/flip", 1) .then().log().all() - .apply(document("tasks/check/success")) + .apply(document("runningTasks/check/success", + pathParameters( + parameterWithName("taskId").description("체크 상태를 변경할 RunningTask Id")) + )) .statusCode(HttpStatus.OK.value()); } @Test - void 진행중인_작업이_아니라면_예외가_발생한다() { + void 존재하지_않는_RunningTask의_체크상태를_변경하려는_경우_예외가_발생한다() { doThrow(new BusinessException("현재 진행 중인 작업이 아닙니다.")).when(taskService) .flipRunningTask(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -159,14 +202,14 @@ class 단일_작업을_체크한다 { .contentType(MediaType.APPLICATION_JSON_VALUE) .when().post("/api/tasks/1/flip") .then().log().all() - .apply(document("tasks/check/fail/active")) + .apply(document("runningTasks/check/fail/active")) .extract(); assertThat(response.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()); } @Test - void 진행중인_작업과_hostId가_일치하지_않는_경우_예외가_발생한다() { + void RunningTask의_아이디와_Host_아이디가_연관되지_않는_경우_예외가_발생한다() { doThrow(new NotFoundException("진행중인 작업이 존재하지 않습니다.")).when(taskService) .flipRunningTask(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -176,14 +219,14 @@ class 단일_작업을_체크한다 { .contentType(MediaType.APPLICATION_JSON_VALUE) .when().post("/api/tasks/1/flip") .then().log().all() - .apply(document("tasks/check/fail/match")) + .apply(document("runningTasks/check/fail/match")) .extract(); assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } @Test - void 호스트가_존재하지_않는_경우_예외가_발생한다() { + void RunningTask와_연관된_Host가_존재하지_않는_경우_예외가_발생한다() { doThrow(new NotFoundException("진행중인 작업이 존재하지 않습니다.")).when(taskService) .flipRunningTask(anyLong(), anyLong()); when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); @@ -193,10 +236,53 @@ class 단일_작업을_체크한다 { .contentType(MediaType.APPLICATION_JSON_VALUE) .when().post("/api/tasks/1/flip") .then().log().all() - .apply(document("tasks/check/fail/notfound")) + .apply(document("runningTasks/check/fail/notfound")) .extract(); assertThat(response.statusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); } } + + @Test + void Task를_조회한다() { + Host host = Host_생성("1234", 1234L); + Space space = Space_생성(host, "잠실"); + Job job = Job_생성(space, "청소"); + Section section1 = Section_아이디_지정_생성(1L, job, "트랙룸"); + Section section2 = Section_아이디_지정_생성(2L, job, "굿샷강의장"); + Task task1 = Task_아이디_지정_생성(1L, section1, "책상 청소"); + Task task2 = Task_아이디_지정_생성(2L, section2, "책상 청소"); + when(taskService.findTasks(anyLong(), any())).thenReturn( + TasksResponse.from(new Tasks(List.of(task1, task2))) + ); + when(authenticationContext.getPrincipal()).thenReturn(String.valueOf(anyLong())); + + docsGiven + .header("Authorization", "Bearer jwt.token.here") + .when().get("/api/jobs/{jobId}/tasks", 1) + .then().log().all() + .apply(document("tasks/find/success", + pathParameters( + parameterWithName("jobId").description("해당 Task를 조회할 Job Id")), + responseFields( + fieldWithPath("sections.[].id").type(JsonFieldType.NUMBER) + .description("Section Id"), + fieldWithPath("sections.[].name").type(JsonFieldType.STRING) + .description("Section 이름"), + fieldWithPath("sections.[].imageUrl").type(JsonFieldType.STRING) + .description("Section Image Url"), + fieldWithPath("sections.[].description").type(JsonFieldType.STRING) + .description("Section 설명"), + fieldWithPath("sections.[].tasks.[].id").type(JsonFieldType.NUMBER) + .description("Task Id"), + fieldWithPath("sections.[].tasks.[].name").type(JsonFieldType.STRING) + .description("Task 이름"), + fieldWithPath("sections.[].tasks.[].imageUrl").type(JsonFieldType.STRING) + .description("Task Image Url"), + fieldWithPath("sections.[].tasks.[].description").type(JsonFieldType.STRING) + .description("Task 설명") + ) + )) + .statusCode(HttpStatus.OK.value()); + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/host/HostRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/domain/host/HostRepositoryTest.java deleted file mode 100644 index b6863d0f..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/host/HostRepositoryTest.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.woowacourse.gongcheck.domain.host; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.woowacourse.gongcheck.exception.NotFoundException; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; - -@DataJpaTest -class HostRepositoryTest { - - @Autowired - private HostRepository hostRepository; - - @Test - void 아이디로_호스트를_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - - Host result = hostRepository.getById(host.getId()); - - assertThat(result).isEqualTo(host); - } - - @Test - void 입력받은_아이디와_일치하는_호스트가_없으면_예외를_던진다() { - assertThatThrownBy(() -> hostRepository.getById(0L)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - - @Test - void 깃허브_아이디로_호스트를_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - - Host result = hostRepository.getByGithubId(1234L); - - assertThat(result).isEqualTo(host); - } - - @Test - void 깃허브_아이디로_조회시_해당하는_호스트가_없으면_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - - assertThatThrownBy(() -> hostRepository.getByGithubId(1235L)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 호스트입니다."); - } - - @ParameterizedTest - @CsvSource(value = {"1234, true", "1235, false"}) - void 깃허브_아이디로_호스트가_존재하는지_확인한다(final Long githubId, final boolean actual) { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - - boolean result = hostRepository.existsByGithubId(githubId); - - assertThat(result).isEqualTo(actual); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/job/JobRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/domain/job/JobRepositoryTest.java deleted file mode 100644 index f8163e8f..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/job/JobRepositoryTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.woowacourse.gongcheck.domain.job; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.space.SpaceRepository; -import com.woowacourse.gongcheck.exception.NotFoundException; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; - -@DataJpaTest -class JobRepositoryTest { - - @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Test - void 멤버와_Space으로_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job1 = Job_생성(space, "오픈"); - Job job2 = Job_생성(space, "청소"); - Job job3 = Job_생성(space, "마감"); - jobRepository.saveAll(List.of(job1, job2, job3)); - - Slice result = jobRepository.findBySpaceHostAndSpace(host, space, PageRequest.of(0, 2)); - - assertAll( - () -> assertThat(result.getSize()).isEqualTo(2), - () -> assertThat(result.hasNext()).isTrue() - ); - } - - @Test - void 멤버와_아이디로_작업을_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - - Job result = jobRepository.getBySpaceHostAndId(host, job.getId()); - - assertThat(result).isEqualTo(job); - } - - @Test - void 다른_Host의_Job을_조회할_경우_예외가_발생한다() { - Host host1 = hostRepository.save(Host_생성("1234", 1234L)); - Host host2 = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host2, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - - assertThatThrownBy(() -> jobRepository.getBySpaceHostAndId(host1, job.getId())) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 입력된_Space에_등록된_모든_Job을_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실 캠퍼스")); - Job job1 = jobRepository.save(Job_생성(space, "청소")); - Job job2 = jobRepository.save(Job_생성(space, "마감")); - - List result = jobRepository.findAllBySpace(space); - - assertThat(result).containsExactly(job1, job2); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/section/SectionRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/domain/section/SectionRepositoryTest.java deleted file mode 100644 index 19cbf20c..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/section/SectionRepositoryTest.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.woowacourse.gongcheck.domain.section; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; -import static org.assertj.core.api.Assertions.assertThat; - -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.space.SpaceRepository; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; - -@DataJpaTest -class SectionRepositoryTest { - - @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Test - void 입력된_Job_목록에_해당하는_모든_Section을_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job1 = jobRepository.save(Job_생성(space, "청소")); - Job job2 = jobRepository.save(Job_생성(space, "마감")); - Section section1 = sectionRepository.save(Section_생성(job1, "트랙룸")); - Section section2 = sectionRepository.save(Section_생성(job1, "굿샷 강의장")); - Section section3 = sectionRepository.save(Section_생성(job2, "트랙룸")); - Section section4 = sectionRepository.save(Section_생성(job2, "굿샷 강의장")); - - List
result = sectionRepository.findAllByJobIn(List.of(job1, job2)); - - assertThat(result).containsExactly(section1, section2, section3, section4); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/space/SpaceRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/domain/space/SpaceRepositoryTest.java deleted file mode 100644 index 6282c347..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/space/SpaceRepositoryTest.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.woowacourse.gongcheck.domain.space; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; - -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.exception.NotFoundException; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; - -@DataJpaTest -class SpaceRepositoryTest { - - @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Test - void 멤버아이디로_공간을_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space1 = Space_생성(host, "잠실"); - Space space2 = Space_생성(host, "선릉"); - Space space3 = Space_생성(host, "양평같은방"); - spaceRepository.saveAll(List.of(space1, space2, space3)); - - Slice result = spaceRepository.findByHost(host, PageRequest.of(0, 2)); - - assertAll( - () -> assertThat(result.getSize()).isEqualTo(2), - () -> assertThat(result.hasNext()).isTrue() - ); - } - - @Test - void 멤버와_아이디로_공간을_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - - Space result = spaceRepository.getByHostAndId(host, space.getId()); - - assertThat(result).isEqualTo(space); - } - - @Test - void 다른_호스트의_공간을_조회할_경우_예외가_발생한다() { - Host host1 = hostRepository.save(Host_생성("1234", 1234L)); - Host host2 = hostRepository.save(Host_생성("1234", 2345L)); - Space space = Space_생성(host2, "잠실"); - spaceRepository.save(space); - - assertThatThrownBy(() -> - spaceRepository.getByHostAndId(host1, space.getId())) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 공간입니다."); - } - - @Test - void 호스트와_공간_이름을_입력_받아_이미_존재하는_공간_이름이면_참을_반환한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = Space_생성(host, "잠실"); - spaceRepository.save(space); - - boolean result = spaceRepository.existsByHostAndName(host, space.getName()); - - assertThat(result).isTrue(); - } - - @Test - void 호스트와_공간_이름을_입력_받아_이미_존재하는_공간_이름이_아니면_거짓을_반환한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - - boolean result = spaceRepository.existsByHostAndName(host, "잠실"); - - assertThat(result).isFalse(); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/space/SpaceTest.java b/backend/src/test/java/com/woowacourse/gongcheck/domain/space/SpaceTest.java deleted file mode 100644 index 2f747c54..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/space/SpaceTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.woowacourse.gongcheck.domain.space; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.woowacourse.gongcheck.domain.host.Host; -import java.time.LocalDateTime; -import org.junit.jupiter.api.Test; - -class SpaceTest { - - @Test - void Space_이름은_20자_이하여야_한다() { - Host host = Host_생성("1234", 1234L); - - assertThatThrownBy( - () -> Space.builder() - .host(host) - .name("123456789123467891234") - .createdAt(LocalDateTime.now()) - .build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("공간의 이름은 20자 이하여야합니다."); - } - - @Test - void Space_이름은_빈_값일_수_없다() { - Host host = Host_생성("1234", 1234L); - - assertThatThrownBy( - () -> Space.builder() - .host(host) - .name("") - .createdAt(LocalDateTime.now()) - .build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("공간의 이름은 공백일 수 없습니다."); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/task/RunningTaskRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/domain/task/RunningTaskRepositoryTest.java deleted file mode 100644 index bc26be1d..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/task/RunningTaskRepositoryTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.woowacourse.gongcheck.domain.task; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.RunningTask_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; -import static org.assertj.core.api.Assertions.assertThat; - -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.section.Section; -import com.woowacourse.gongcheck.domain.section.SectionRepository; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.space.SpaceRepository; -import java.util.List; -import java.util.Optional; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; - -@DataJpaTest -class RunningTaskRepositoryTest { - - @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TaskRepository taskRepository; - - @Autowired - private RunningTaskRepository runningTaskRepository; - - @Nested - class 테스크가_존재한다 { - - private Host host; - private Space space; - private Job job; - private Section section; - private Task task; - - @BeforeEach - void setUp() { - host = hostRepository.save(Host_생성("1234", 1234L)); - space = spaceRepository.save(Space_생성(host, "잠실")); - job = jobRepository.save(Job_생성(space, "청소")); - section = sectionRepository.save(Section_생성(job, "트랙룸")); - task = taskRepository.save(Task_생성(section, "책상 청소")); - } - - @Test - void 진행중인_테스크가_존재하는_경우_True를_반환한다() { - runningTaskRepository.save(RunningTask_생성(task.getId(), false)); - boolean result = runningTaskRepository.existsByTaskIdIn(List.of(task.getId())); - - assertThat(result).isTrue(); - } - - @Test - void 진행중인_테스크가_존재하지_않는_경우_False를_반환한다() { - boolean result = runningTaskRepository.existsByTaskIdIn(List.of(task.getId())); - - assertThat(result).isFalse(); - } - - @Test - void 진행중인_테스크가_존재하지_않는_경우_빈_값이_조회된다() { - Optional result = runningTaskRepository.findByTaskId(task.getId()); - - assertThat(result).isEmpty(); - } - - @Test - void 진행중인_테스크가_존재하는_경우_테스크를_조회한다() { - runningTaskRepository.save(RunningTask_생성(task.getId(), false)); - Optional result = runningTaskRepository.findByTaskId(task.getId()); - - assertThat(result).isNotEmpty(); - } - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/domain/task/TaskRepositoryTest.java b/backend/src/test/java/com/woowacourse/gongcheck/domain/task/TaskRepositoryTest.java deleted file mode 100644 index 0680097a..00000000 --- a/backend/src/test/java/com/woowacourse/gongcheck/domain/task/TaskRepositoryTest.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.woowacourse.gongcheck.domain.task; - -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Host_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Job_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Section_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Space_생성; -import static com.woowacourse.gongcheck.fixture.FixtureFactory.Task_생성; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.HostRepository; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.job.JobRepository; -import com.woowacourse.gongcheck.domain.section.Section; -import com.woowacourse.gongcheck.domain.section.SectionRepository; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.space.SpaceRepository; -import com.woowacourse.gongcheck.exception.NotFoundException; -import java.util.List; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; - -@DataJpaTest -class TaskRepositoryTest { - - @Autowired - private HostRepository hostRepository; - - @Autowired - private SpaceRepository spaceRepository; - - @Autowired - private JobRepository jobRepository; - - @Autowired - private SectionRepository sectionRepository; - - @Autowired - private TaskRepository taskRepository; - - @Test - void Job이_가진_모든_Task를_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section1 = sectionRepository.save(Section_생성(job, "트랙룸")); - Section section2 = sectionRepository.save(Section_생성(job, "굿샷 강의장")); - taskRepository.saveAll(List.of(Task_생성(section1, "책상 청소"), Task_생성(section1, "빈백 정리"))); - taskRepository.saveAll(List.of(Task_생성(section2, "책상 청소"), Task_생성(section2, "의자 넣기"))); - - List result = taskRepository.findAllBySectionJob(job); - - assertThat(result).hasSize(4); - } - - @Test - void Host와_TaskId를_입력_받아_Task를_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section1 = sectionRepository.save(Section_생성(job, "트랙룸")); - Task task = taskRepository.save(Task_생성(section1, "책상 청소")); - - Task result = taskRepository.getBySectionJobSpaceHostAndId(host, task.getId()); - - assertThat(result).isEqualTo(task); - } - - @Test - void 입력받은_TaskId에_해당하는_Task가_없는_경우_예외가_발생한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - - assertThatThrownBy(() -> taskRepository.getBySectionJobSpaceHostAndId(host, 0L)) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 입력받은_Host에_해당하는_Task가_없는_경우_예외가_발생한다() { - Host host1 = hostRepository.save(Host_생성("1234", 1234L)); - Host host2 = hostRepository.save(Host_생성("1234", 2345L)); - Space space = spaceRepository.save(Space_생성(host1, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section1 = sectionRepository.save(Section_생성(job, "트랙룸")); - Task task = taskRepository.save(Task_생성(section1, "책상 청소")); - - assertThatThrownBy(() -> taskRepository.getBySectionJobSpaceHostAndId(host2, task.getId())) - .isInstanceOf(NotFoundException.class) - .hasMessage("존재하지 않는 작업입니다."); - } - - @Test - void 입력받은_Section_목록에_해당하는_모든_Task를_조회한다() { - Host host = hostRepository.save(Host_생성("1234", 1234L)); - Space space = spaceRepository.save(Space_생성(host, "잠실")); - Job job = jobRepository.save(Job_생성(space, "청소")); - Section section1 = sectionRepository.save(Section_생성(job, "트랙룸")); - Section section2 = sectionRepository.save(Section_생성(job, "트랙룸")); - Task task1 = taskRepository.save(Task_생성(section1, "책상 청소")); - Task task2 = taskRepository.save(Task_생성(section1, "빈백 정리")); - Task task3 = taskRepository.save(Task_생성(section2, "책상 청소")); - Task task4 = taskRepository.save(Task_생성(section2, "빈백 정리")); - - List result = taskRepository.findAllBySectionIn(List.of(section1, section2)); - - assertThat(result).containsExactly(task1, task2, task3, task4); - } -} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/fixture/FixtureFactory.java b/backend/src/test/java/com/woowacourse/gongcheck/fixture/FixtureFactory.java index af753398..cfd88afc 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/fixture/FixtureFactory.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/fixture/FixtureFactory.java @@ -1,12 +1,15 @@ package com.woowacourse.gongcheck.fixture; -import com.woowacourse.gongcheck.domain.host.Host; -import com.woowacourse.gongcheck.domain.host.SpacePassword; -import com.woowacourse.gongcheck.domain.job.Job; -import com.woowacourse.gongcheck.domain.section.Section; -import com.woowacourse.gongcheck.domain.space.Space; -import com.woowacourse.gongcheck.domain.task.RunningTask; -import com.woowacourse.gongcheck.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.host.Host; +import com.woowacourse.gongcheck.core.domain.host.SpacePassword; +import com.woowacourse.gongcheck.core.domain.job.Job; +import com.woowacourse.gongcheck.core.domain.section.Section; +import com.woowacourse.gongcheck.core.domain.space.Space; +import com.woowacourse.gongcheck.core.domain.submission.Submission; +import com.woowacourse.gongcheck.core.domain.task.RunningTask; +import com.woowacourse.gongcheck.core.domain.task.Task; +import com.woowacourse.gongcheck.core.domain.vo.Description; +import com.woowacourse.gongcheck.core.domain.vo.Name; import java.time.LocalDateTime; public class FixtureFactory { @@ -22,7 +25,7 @@ public class FixtureFactory { public static Space Space_생성(final Host host, final String name) { return Space.builder() .host(host) - .name(name) + .name(new Name(name)) .createdAt(LocalDateTime.now()) .build(); } @@ -31,7 +34,8 @@ public class FixtureFactory { return Space.builder() .id(id) .host(host) - .name(name) + .name(new Name(name)) + .imageUrl("image.url") .createdAt(LocalDateTime.now()) .build(); } @@ -39,7 +43,16 @@ public class FixtureFactory { public static Job Job_생성(final Space space, final String name) { return Job.builder() .space(space) - .name(name) + .name(new Name(name)) + .createdAt(LocalDateTime.now()) + .build(); + } + + public static Job Job_생성(final Space space, final String name, final String slackUrl) { + return Job.builder() + .space(space) + .name(new Name(name)) + .slackUrl(slackUrl) .createdAt(LocalDateTime.now()) .build(); } @@ -48,7 +61,7 @@ public class FixtureFactory { return Job.builder() .id(id) .space(space) - .name(name) + .name(new Name(name)) .createdAt(LocalDateTime.now()) .build(); } @@ -56,7 +69,9 @@ public class FixtureFactory { public static Section Section_생성(final Job job, final String name) { return Section.builder() .job(job) - .name(name) + .name(new Name(name)) + .description(new Description("설명")) + .imageUrl("image.url") .createdAt(LocalDateTime.now()) .build(); } @@ -65,7 +80,9 @@ public class FixtureFactory { return Section.builder() .id(id) .job(job) - .name(name) + .name(new Name(name)) + .description(new Description("설명")) + .imageUrl("image.url") .createdAt(LocalDateTime.now()) .build(); } @@ -73,7 +90,20 @@ public class FixtureFactory { public static Task Task_생성(final Section section, final String name) { return Task.builder() .section(section) - .name(name) + .name(new Name(name)) + .description(new Description("설명")) + .imageUrl("image.url") + .createdAt(LocalDateTime.now()) + .build(); + } + + public static Task Task_아이디_지정_생성(final Long id, final Section section, final String name) { + return Task.builder() + .id(id) + .section(section) + .name(new Name(name)) + .description(new Description("설명")) + .imageUrl("image.url") .createdAt(LocalDateTime.now()) .build(); } @@ -84,7 +114,9 @@ public class FixtureFactory { .id(id) .section(section) .runningTask(runningTask) - .name(name) + .name(new Name(name)) + .description(new Description("설명")) + .imageUrl("image.url") .createdAt(LocalDateTime.now()) .build(); } @@ -96,4 +128,21 @@ public class FixtureFactory { .createdAt(LocalDateTime.now()) .build(); } + + public static Submission Submission_생성(final Job job, final String author) { + return Submission.builder() + .job(job) + .author(author) + .createdAt(LocalDateTime.now()) + .build(); + } + + public static Submission Submission_아이디_지정_생성(final Long id, final Job job) { + return Submission.builder() + .id(id) + .job(job) + .author("어썸오") + .createdAt(LocalDateTime.now()) + .build(); + } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/hash/AES256Test.java b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/hash/AES256Test.java new file mode 100644 index 00000000..41b26f9f --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/hash/AES256Test.java @@ -0,0 +1,40 @@ +package com.woowacourse.gongcheck.infrastructure.hash; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.Test; + +class AES256Test { + + private final String key = "01234567890123456789012345678901"; + private AES256 aes256 = new AES256(key); + + @Test + void 문자열을_인코딩한다() { + String actual = aes256.encode("1"); + assertThat(actual).isNotNull(); + } + + @Test + void 인코딩_시_null을_입력받는_경우_예외를_발생시킨다() { + assertThatThrownBy(() -> aes256.encode(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 디코딩_시_null을_입력받는_경우_예외를_발생시킨다() { + assertThatThrownBy(() -> aes256.decode(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void 디코딩한_문자열은_인코딩한_문자열과_같다() { + String expected = "1"; + String encode = aes256.encode(expected); + + String actual = aes256.decode(encode); + + assertThat(actual).isEqualTo(expected); + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/imageuploader/OwnServerImageUploaderTest.java b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/imageuploader/OwnServerImageUploaderTest.java new file mode 100644 index 00000000..fa905497 --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/imageuploader/OwnServerImageUploaderTest.java @@ -0,0 +1,63 @@ +package com.woowacourse.gongcheck.infrastructure.imageuploader; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +import com.woowacourse.gongcheck.core.application.response.ImageUrlResponse; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.reactive.function.client.WebClient; + +@ExtendWith(MockitoExtension.class) +class OwnServerImageUploaderTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private WebClient webClient; + + @InjectMocks + private OwnServerImageUploader imageUploader; + + @Nested + class storeImage_메소드는 { + + @Nested + class 저장하고자하는_이미지_파일이_입력된_경우 { + + private MultipartFile image; + + @BeforeEach + void setUp() { + image = new MockMultipartFile("images", + "jamsil.jpg", + "images/jpg", + "123".getBytes(StandardCharsets.UTF_8)); + when(webClient.post() + .uri("/api/image-upload") + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(any()) + .retrieve() + .bodyToMono(String.class) + .block()) + .thenReturn("https://localhost:8080/images/ajsicjnasdioc.jpg"); + } + + @Test + void 이미지를_저장하고_이미지_경로를_반환한다() { + ImageUrlResponse actual = imageUploader.upload(image, ""); + + assertThat(actual.getImageUrl()).isNotNull(); + } + } + } +} diff --git a/backend/src/test/java/com/woowacourse/gongcheck/application/JjwtTokenProviderTest.java b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProviderTest.java similarity index 89% rename from backend/src/test/java/com/woowacourse/gongcheck/application/JjwtTokenProviderTest.java rename to backend/src/test/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProviderTest.java index 5ee6710f..bfd83082 100644 --- a/backend/src/test/java/com/woowacourse/gongcheck/application/JjwtTokenProviderTest.java +++ b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/jwt/JjwtTokenProviderTest.java @@ -1,9 +1,10 @@ -package com.woowacourse.gongcheck.application; +package com.woowacourse.gongcheck.infrastructure.jwt; -import static com.woowacourse.gongcheck.presentation.Authority.GUEST; +import static com.woowacourse.gongcheck.auth.domain.Authority.GUEST; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import com.woowacourse.gongcheck.exception.InfrastructureException; import com.woowacourse.gongcheck.exception.UnauthorizedException; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; @@ -42,7 +43,7 @@ class JjwtTokenProviderTest { String invalidToken = "invalidToken"; assertThatThrownBy(() -> tokenProvider.extractSubject(invalidToken)) - .isInstanceOf(UnauthorizedException.class) + .isInstanceOf(InfrastructureException.class) .hasMessage("올바르지 않은 토큰입니다."); } @@ -73,7 +74,7 @@ class JjwtTokenProviderTest { String invalidToken = "invalidToken"; assertThatThrownBy(() -> tokenProvider.extractAuthority(invalidToken)) - .isInstanceOf(UnauthorizedException.class) + .isInstanceOf(InfrastructureException.class) .hasMessage("올바르지 않은 토큰입니다."); } } diff --git a/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClientTest.java b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClientTest.java new file mode 100644 index 00000000..5f77c50b --- /dev/null +++ b/backend/src/test/java/com/woowacourse/gongcheck/infrastructure/oauth/GithubOauthClientTest.java @@ -0,0 +1,152 @@ +package com.woowacourse.gongcheck.infrastructure.oauth; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.woowacourse.gongcheck.auth.application.response.GithubAccessTokenResponse; +import com.woowacourse.gongcheck.auth.application.response.GithubProfileResponse; +import com.woowacourse.gongcheck.exception.InfrastructureException; +import com.woowacourse.gongcheck.exception.NotFoundException; +import com.woowacourse.gongcheck.exception.UnauthorizedException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestTemplate; + +@SpringBootTest +@Transactional +class GithubOauthClientTest { + + private MockRestServiceServer mockRestServiceServer; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private RestTemplate restTemplate; + + @Autowired + private GithubOauthClient githubOauthClient; + + @BeforeEach + void setUp() { + mockRestServiceServer = MockRestServiceServer.createServer(restTemplate); + } + + @Nested + class requestGithubProfileByCode_메소드는 { + + @Nested + class Github에서_access_token_반환이_불가능한_경우 { + + @BeforeEach + void setUp() { + mockRestServiceServer.expect(requestTo("https://github.com/login/oauth/access_token")) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.NOT_FOUND) + .contentType(MediaType.APPLICATION_JSON)); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> githubOauthClient.requestGithubProfileByCode("code")) + .isInstanceOf(InfrastructureException.class) + .hasMessage("해당 사용자의 프로필을 요청할 수 없습니다."); + mockRestServiceServer.verify(); + } + } + + @Nested + class 권한이_없는_code을_입력하여_Github_access_Token를_요청할_경우 { + + @BeforeEach + void setUp() throws JsonProcessingException { + mockRestServiceServer.expect(requestTo("https://github.com/login/oauth/access_token")) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(null))); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> githubOauthClient.requestGithubProfileByCode("code")) + .isInstanceOf(InfrastructureException.class) + .hasMessage("잘못된 요청입니다."); + mockRestServiceServer.verify(); + } + } + + @Nested + class Github에서_프로필_반환이_불가능한_경우 { + + @BeforeEach + void setUp() throws JsonProcessingException { + GithubAccessTokenResponse token = new GithubAccessTokenResponse("access_token"); + mockRestServiceServer.expect(requestTo("https://github.com/login/oauth/access_token")) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(token))); + + mockRestServiceServer.expect(requestTo("https://api.github.com/user")) + .andExpect(method(HttpMethod.GET)) + .andRespond(withStatus(HttpStatus.NOT_FOUND) + .contentType(MediaType.APPLICATION_JSON)); + } + + @Test + void 예외를_발생시킨다() { + assertThatThrownBy(() -> githubOauthClient.requestGithubProfileByCode("code")) + .isInstanceOf(InfrastructureException.class) + .hasMessage("해당 사용자의 프로필을 요청할 수 없습니다."); + mockRestServiceServer.verify(); + } + } + + @Nested + class 권한이_있는_code로_Github에_프로필접근이_가능한_경우 { + + private GithubProfileResponse githubProfileResponse; + + @BeforeEach + void setUp() throws JsonProcessingException { + GithubAccessTokenResponse token = new GithubAccessTokenResponse("access_token"); + mockRestServiceServer.expect(requestTo("https://github.com/login/oauth/access_token")) + .andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(token))); + + githubProfileResponse = new GithubProfileResponse("nickname", "loginName", "1", "test.com"); + mockRestServiceServer.expect(requestTo("https://api.github.com/user")) + .andExpect(method(HttpMethod.GET)) + .andRespond(withStatus(HttpStatus.OK) + .contentType(MediaType.APPLICATION_JSON) + .body(objectMapper.writeValueAsString(githubProfileResponse))); + } + + @Test + void Github_프로필정보를_반환한다() { + GithubProfileResponse actual = githubOauthClient.requestGithubProfileByCode("access_token"); + + assertThat(actual) + .usingRecursiveComparison() + .isEqualTo(githubProfileResponse); + mockRestServiceServer.verify(); + } + } + } +} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 7f41e765..f85f71da 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -1,30 +1,3 @@ spring: - sql: - init: - schema-locations: classpath:schema.sql - datasource: - driver-class-name: org.h2.Driver - url: jdbc:h2:mem:gong-check-test;MODE=MYSQL; - username: sa - password: - h2: - console: - enabled: true - jpa: - hibernate: - ddl-auto: validate - properties: - hibernate: - show_sql: true - format_sql: true - open-in-view: false profiles: include: test -security: - jwt: - token: - secret-key: Z29uZy1jaGVjay1nb25nLWNoZWNrLWdvbmctY2hlY2stZ29uZy1jaGVjay1nb25nLWNoZWNrLWdvbmctY2hlY2stZ29uZy1jaGVjay1nb25nLWNoZWNrCg== - expire-time: 3600000 - -server: - port: 7070 diff --git a/backend/src/test/resources/data.sql b/backend/src/test/resources/data.sql deleted file mode 100644 index fba9bc2b..00000000 --- a/backend/src/test/resources/data.sql +++ /dev/null @@ -1,18 +0,0 @@ -INSERT INTO host (space_password, github_id, image_url, created_at) -VALUES ('1234', 2, 'test.com', current_timestamp()); -INSERT INTO space (host_id, name, created_at) -VALUES (1, '잠실', current_timestamp()); -INSERT INTO job (space_id, name, created_at) -VALUES (1, '청소', current_timestamp()); -INSERT INTO section (job_id, name, created_at) -VALUES (1, '트랙룸', current_timestamp()); -INSERT INTO task (section_id, name, created_at) -VALUES (1, '책상 청소', current_timestamp()); -INSERT INTO task (section_id, name, created_at) -VALUES (1, '빈백 털기', current_timestamp()); -INSERT INTO section (job_id, name, created_at) -VALUES (1, '굿샷 강의장', current_timestamp()); -INSERT INTO task (section_id, name, created_at) -VALUES (2, '책상 청소', current_timestamp()); -INSERT INTO task (section_id, name, created_at) -VALUES (2, '의자 청소', current_timestamp()); diff --git a/backend/src/test/resources/schema.sql b/backend/src/test/resources/schema.sql index 17de6290..6a20a934 100644 --- a/backend/src/test/resources/schema.sql +++ b/backend/src/test/resources/schema.sql @@ -13,7 +13,7 @@ CREATE TABLE host github_id BIGINT NOT NULL UNIQUE, image_url VARCHAR NOT NULL, created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NULL, + updated_at TIMESTAMP NULL, PRIMARY KEY (id) ); @@ -21,7 +21,7 @@ CREATE TABLE space ( id BIGINT NOT NULL AUTO_INCREMENT, host_id BIGINT NOT NULL, - name VARCHAR(20) NOT NULL, + name VARCHAR(10) NOT NULL, img_url VARCHAR NULL, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NULL, @@ -32,30 +32,34 @@ CREATE TABLE job ( id BIGINT NOT NULL AUTO_INCREMENT, space_id BIGINT NOT NULL, - name VARCHAR(20) NOT NULL, + name VARCHAR(10) NOT NULL, slack_url VARCHAR NULL, created_at TIMESTAMP NOT NULL, updated_at TIMESTAMP NULL, PRIMARY KEY (id) ); -CREATE TABLE task +CREATE TABLE section ( - id BIGINT NOT NULL AUTO_INCREMENT, - section_id BIGINT NOT NULL, - name VARCHAR(50) NOT NULL, - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NULL, + id BIGINT NOT NULL AUTO_INCREMENT, + job_id BIGINT NOT NULL, + name VARCHAR(10) NOT NULL, + description VARCHAR(128) NULL, + image_url VARCHAR NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NULL, PRIMARY KEY (id) ); -CREATE TABLE section +CREATE TABLE task ( - id BIGINT NOT NULL AUTO_INCREMENT, - job_id BIGINT NOT NULL, - name VARCHAR(20) NOT NULL, - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NULL, + id BIGINT NOT NULL AUTO_INCREMENT, + section_id BIGINT NOT NULL, + name VARCHAR(10) NOT NULL, + description VARCHAR(128) NULL, + image_url VARCHAR NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NULL, PRIMARY KEY (id) ); @@ -72,7 +76,7 @@ CREATE TABLE submission ( id BIGINT NOT NULL AUTO_INCREMENT, job_id BIGINT NOT NULL, - author VARCHAR(20) NOT NULL, + author VARCHAR(10) NOT NULL, created_at TIMESTAMP NOT NULL, PRIMARY KEY (id) ); diff --git a/backend/submodule b/backend/submodule index 8b7abd33..86818e0a 160000 --- a/backend/submodule +++ b/backend/submodule @@ -1 +1 @@ -Subproject commit 8b7abd33ee7ba5016f3db9433adee82ea6ac129c +Subproject commit 86818e0aa73864ada829ecd1e88a1fa7f0807da0 diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 2d83fbcc..c62b5c73 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -3,9 +3,15 @@ "browser": true, "es2021": true }, - "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended"], + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:prettier/recommended", + "plugin:cypress/recommended" + ], "parser": "@typescript-eslint/parser", - "plugins": ["react", "@typescript-eslint"], + "plugins": ["react", "@typescript-eslint", "prettier"], "rules": { "react/react-in-jsx-scope": "off", "react/prop-types": "off" diff --git a/frontend/.gitignore b/frontend/.gitignore index 1a82b59c..ce31cbb3 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -4,5 +4,9 @@ logs **/*.backup.* **/*.back.* dist +.env + +cypress/videos +cypress/screenshots node_modules diff --git a/frontend/.prettierrc.json b/frontend/.prettierrc.json index 7731546d..0f9af619 100644 --- a/frontend/.prettierrc.json +++ b/frontend/.prettierrc.json @@ -20,7 +20,7 @@ "^(@/types)(.*|$)", "^(@/constants/)(.*|$)", "^(@/assets/)(.*|$)", - "@/ModalPortal", + "^(@/portals/)(.*|$)", "^(@/styles)(.*|$)", "^(./styles)(.*|$)", "^(./config)(.*|$)" diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts new file mode 100644 index 00000000..66933adb --- /dev/null +++ b/frontend/cypress.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, + env: { + 'cypress-react-selector': { + root: '#root', + modal: '#modal', + toast: '#toast', + }, + }, +}); diff --git a/frontend/cypress/e2e/guest.cy.ts b/frontend/cypress/e2e/guest.cy.ts new file mode 100644 index 00000000..f829783d --- /dev/null +++ b/frontend/cypress/e2e/guest.cy.ts @@ -0,0 +1,155 @@ +const WRONG_PASSWORD = '0000'; +const CORRECT_PASSWORD = '1234'; + +const PAGE = { + PASSWORD: 'http://localhost:3000/enter/1/pwd', + SPACE_LIST: 'http://localhost:3000/enter/1/spaces', + JOB_LIST: 'http://localhost:3000/enter/1/spaces/1', + TASK_LIST: 'http://localhost:3000/enter/1/spaces/1/1', +}; + +describe('사용자, - 비밀번호 입력 페이지', () => { + beforeEach(() => { + cy.visit(PAGE.PASSWORD); + cy.spaceEnter(); + cy.getSpaces(); + }); + + it('사용자가 잘못된 비밀번호를 입력하면, API 호출이 실패하고 토스트바로 안내해준다.', () => { + cy.get('input').type(WRONG_PASSWORD); + + cy.get('button').click(); + + cy.get('@spaceEnter').then(({ response }: any) => { + cy.get('#toast > div').should('be.contain', '비밀번호를 확인해주세요.'); + expect(response.statusCode).to.equal(401); + }); + }); + + it('사용자가 올바른 비밀번호를 입력하면, API 호출이 성공하고 공간 선택 페이지로 이동한다.', () => { + cy.get('input').type(CORRECT_PASSWORD); + + cy.get('button').click(); + + cy.get('@spaceEnter').then(({ response }: any) => { + expect(response.statusCode).to.equal(200); + }); + }); +}); + +describe('사용자, - 공간 선택 페이지', () => { + beforeEach(() => { + cy.setToken(); + cy.visit(PAGE.SPACE_LIST); + cy.spaceEnter(); + cy.getSpaces(); + cy.getJobs(); + cy.getSpaceInfo(); + }); + + it('사용자가 공간을 클릭하면, 공간의 업무 선택 페이지로 이동한다.', () => { + cy.get('[class$=-spaceCard]').first().click(); + + cy.url().should('eq', PAGE.JOB_LIST); + }); +}); + +describe('사용자 - 업무 선택 페이지', () => { + beforeEach(() => { + cy.setToken(); + cy.visit(PAGE.JOB_LIST); + cy.getJobs(); + cy.getSpaceInfo(); + cy.getRunningTasks(); + }); + + it('사용자가 작업이 진행중이지 않은 업무를 클릭하면, confirm 창이 노출된다.', () => { + cy.getRunningTaskActive_false(1); + cy.postNewRunningTasks(1); + cy.get('[class$=-jobCard]').first().click(); + + cy.on('window:confirm', text => { + expect(text).to.contains('진행중인 체크리스트가 없습니다. 새롭게 생성하시겠습니까?'); + return false; + }); + }); + + it('사용자가 작업이 진행중인 업무를 클릭하면, 해당 체크리스트 페이지로 이동한다.', () => { + cy.getRunningTaskActive_true(1); + cy.postNewRunningTasks(1); + cy.get('[class$=-jobCard]').first().click(); + + cy.url().should('eq', PAGE.TASK_LIST); + }); + + it('사용자가 새로운 체크리스트 생성시, 업무 목록이 존재하지 않으면 토스트바로 안내해준다.', () => { + cy.getRunningTaskActive_false(2); + cy.postNewRunningTasks(2); + cy.get('[class$=-jobCard]').last().click(); + + cy.on('window:confirm', text => { + expect(text).to.contains('진행중인 체크리스트가 없습니다. 새롭게 생성하시겠습니까?'); + return true; + }); + + cy.get('@postNewRunningTasks').then(({ response }: any) => { + cy.get('#toast > div').should('be.contain', '작업이 존재하지 않습니다.'); + expect(response.statusCode).to.equal(401); + }); + }); +}); + +describe('사용자 - 체크리스트 체크 페이지', () => { + beforeEach(() => { + cy.setToken(); + cy.visit(PAGE.TASK_LIST); + cy.getSpaceInfo(); + cy.getRunningTasks(); + cy.postJobComplete(); + }); + + it('사용자가 체크 박스를 클릭하면, 해당 사항이 체크표시된다.', () => { + cy.postCheckTask(1); + cy.get('label').first().click(); + + cy.get('label').first().should('have.css', 'background-color', 'rgb(126, 217, 87)'); + }); + + it('사용자가 모든 체크 박스를 클릭하면, 제출 버튼이 활성화된다.', () => { + cy.postCheckTask(1); + cy.get('label').first().click(); + cy.postCheckTask(2); + cy.get('label').last().click(); + + cy.get('button').should('not.be.disabled'); + }); + + it('사용자가 제출 버튼을 클릭하면, 제출 모달이 노출된다.', () => { + cy.postCheckTask(1); + cy.get('label').first().click(); + cy.postCheckTask(2); + cy.get('label').last().click(); + + cy.get('button').click(); + + cy.get('#modal > div').should('be.visible'); + }); + + it('사용자가 제출 모달에서 제출자를 입력하고 버튼을 누르면, 제출 alert가 발생한다.', () => { + cy.getSpaces(); + + cy.postCheckTask(1); + cy.get('label').first().click(); + cy.postCheckTask(2); + cy.get('label').last().click(); + + cy.get('button').click(); + + cy.get('#modal input').type('coke'); + cy.get('#modal button').click(); + + cy.on('window:alert', text => { + expect(text).to.include('제출 되었습니다.'); + }); + }); +}); diff --git a/frontend/cypress/fixtures/example.json b/frontend/cypress/fixtures/example.json new file mode 100644 index 00000000..02e42543 --- /dev/null +++ b/frontend/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} diff --git a/frontend/cypress/support/commands.ts b/frontend/cypress/support/commands.ts new file mode 100644 index 00000000..9d33cbc0 --- /dev/null +++ b/frontend/cypress/support/commands.ts @@ -0,0 +1,176 @@ +/// +// *********************************************** + +const SECTIONS = [ + { + id: 1, + name: '트랙룸', + tasks: [ + { + id: 1, + name: '책상 청소', + checked: false, + }, + ], + }, + { + id: 2, + name: '굿샷강의장', + tasks: [ + { + id: 2, + name: '의자 청소', + checked: false, + }, + ], + }, +]; + +Cypress.Commands.add('setToken', () => { + localStorage.setItem('token', 'json_web_token'); +}); + +// mockAPIs +Cypress.Commands.addAll({ + spaceEnter() { + const PASSWORD = '1234'; + + cy.intercept('POST', 'http://localhost:8080/api/hosts/1/enter', (request: any) => { + request.reply( + request.body.password === PASSWORD + ? { + statusCode: 200, + } + : { + statusCode: 401, + } + ); + }).as('spaceEnter'); + }, + + getSpaces() { + cy.intercept('GET', 'http://localhost:8080/api/spaces', (request: any) => { + request.reply({ + statusCode: 200, + body: { + spaces: [ + { + id: 1, + name: '잠실 캠퍼스', + imageUrl: 'https://velog.velcdn.com/images/cks3066/post/258f92c1-32be-4acb-be30-1eb64635c013/image.jpg', + }, + { + id: 2, + name: '선릉 캠퍼스', + imageUrl: 'https://velog.velcdn.com/images/cks3066/post/28a9d0e5-d585-42e4-bc9e-458e439e2f4f/image.jpg', + }, + ], + }, + }); + }).as('getSpaces'); + }, + + getJobs() { + cy.intercept('GET', 'http://localhost:8080/api/spaces/1/jobs', (request: any) => { + request.reply({ + statusCode: 200, + body: { + jobs: [ + { + id: 1, + name: '청소', + }, + { + id: 2, + name: '마감', + }, + ], + }, + }); + }).as('getJobs'); + }, + + getSpaceInfo() { + cy.intercept('GET', 'http://localhost:8080/api/spaces/1', (request: any) => { + request.reply({ + statusCode: 200, + body: { + id: 1, + name: '잠실 캠퍼스', + imageUrl: 'https://velog.velcdn.com/images/cks3066/post/258f92c1-32be-4acb-be30-1eb64635c013/image.jpg', + }, + }); + }).as('getSpaceInfo'); + }, + + getRunningTaskActive_true(jobId: number) { + cy.intercept('GET', `http://localhost:8080/api/jobs/${jobId}/active`, (request: any) => { + request.reply({ + statusCode: 200, + body: { + active: true, + }, + }); + }).as('getRunningTaskActive_true'); + }, + + getRunningTaskActive_false(jobId: number) { + cy.intercept('GET', `http://localhost:8080/api/jobs/${jobId}/active`, (request: any) => { + request.reply({ + statusCode: 200, + body: { + active: false, + }, + }); + }).as('getRunningTaskActive_false'); + }, + + postNewRunningTasks(jobId: number) { + cy.intercept('POST', `http://localhost:8080/api/jobs/${jobId}/runningTasks/new`, (request: any) => { + request.reply( + jobId === 1 + ? { + statusCode: 200, + } + : { + statusCode: 401, + body: { + message: '작업이 존재하지 않습니다.', + }, + } + ); + }).as('postNewRunningTasks'); + }, + + getRunningTasks() { + cy.intercept('GET', 'http://localhost:8080/api/jobs/1/runningTasks', (request: any) => { + request.reply({ + statusCode: 200, + body: { sections: SECTIONS }, + }); + }).as('getRunningTasks'); + }, + + postCheckTask(taskId: number) { + cy.intercept('POST', `http://localhost:8080/api/tasks/${taskId}/flip`, (request: any) => { + SECTIONS.forEach(section => + section.tasks.forEach(task => { + if (task.id === taskId) { + task.checked = true; + } + }) + ); + request.reply({ + statusCode: 200, + }); + }).as('postCheckTask'); + }, + + postJobComplete() { + cy.intercept('POST', `http://localhost:8080/api/jobs/1/complete`, (request: any) => { + request.reply({ + statusCode: 200, + }); + }).as('postJobComplete'); + }, +}); diff --git a/frontend/cypress/support/e2e.ts b/frontend/cypress/support/e2e.ts new file mode 100644 index 00000000..b93ad2a8 --- /dev/null +++ b/frontend/cypress/support/e2e.ts @@ -0,0 +1,19 @@ +// *********************************************************** +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/frontend/cypress/support/index.d.ts b/frontend/cypress/support/index.d.ts new file mode 100644 index 00000000..263d7ae2 --- /dev/null +++ b/frontend/cypress/support/index.d.ts @@ -0,0 +1,15 @@ +declare namespace Cypress { + interface Chainable { + setToken(): void; + spaceEnter(): void; + getSpaces(): void; + getJobs(): void; + getSpaceInfo(): void; + getRunningTaskActive_true(jobId: number): void; + getRunningTaskActive_false(jobId: number): void; + postNewRunningTasks(jobId: number): void; + getRunningTasks(): void; + postCheckTask(taskId: number): void; + postJobComplete(): void; + } +} diff --git a/frontend/cypress/videos/guest.cy.ts.mp4 b/frontend/cypress/videos/guest.cy.ts.mp4 new file mode 100644 index 00000000..b63bb163 Binary files /dev/null and b/frontend/cypress/videos/guest.cy.ts.mp4 differ diff --git a/frontend/frontend-security b/frontend/frontend-security new file mode 160000 index 00000000..b7d26630 --- /dev/null +++ b/frontend/frontend-security @@ -0,0 +1 @@ +Subproject commit b7d266308a50ad392d9dbf591eccbdb6a0a8d0b8 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index c42bab7c..ed247c2b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,9 +13,9 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", - "react-icons": "^4.4.0", "react-query": "^3.39.1", "react-router-dom": "^6.3.0", + "react-transition-group": "^4.4.2", "recoil": "^0.7.4" }, "devDependencies": { @@ -32,6 +32,7 @@ "@types/node": "^18.0.0", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", + "@types/react-transition-group": "^4.4.5", "@typescript-eslint/eslint-plugin": "^5.30.3", "@typescript-eslint/parser": "^5.30.3", "@webpack-cli/generators": "^2.5.0", @@ -39,11 +40,18 @@ "babel-loader": "^8.2.5", "babel-plugin-module-resolver": "^4.1.0", "clean-webpack-plugin": "^4.0.0", + "cross-env": "^7.0.3", + "cypress": "^10.3.1", + "cypress-react-selector": "^3.0.0", + "dotenv-webpack": "^8.0.0", "eslint": "^8.19.0", + "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-react": "^7.30.1", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", + "nanoid": "^4.0.0", "prettier": "^2.7.1", + "react-icons": "^4.4.0", "ts-loader": "^9.3.1", "typescript": "^4.7.4", "webpack": "^5.73.0", @@ -1789,6 +1797,87 @@ "node": ">=6.9.0" } }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cypress/request": { + "version": "2.88.10", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz", + "integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==", + "dev": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@cypress/request/node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/@cypress/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "dependencies": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + } + }, + "node_modules/@cypress/xvfb/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -2825,6 +2914,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -2856,6 +2954,18 @@ "@types/node": "*" } }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "node_modules/@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, "node_modules/@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -2884,6 +2994,16 @@ "@types/node": "*" } }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "5.30.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.3.tgz", @@ -3472,6 +3592,15 @@ "ajv": "^6.9.1" } }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -3539,6 +3668,26 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -3682,6 +3831,24 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -3691,6 +3858,15 @@ "node": ">=0.10.0" } }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", @@ -3709,6 +3885,15 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", @@ -3721,6 +3906,21 @@ "node": ">= 4.5.0" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, "node_modules/axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -4886,6 +5086,15 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -4962,6 +5171,18 @@ "readable-stream": "^3.4.0" } }, + "node_modules/blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "node_modules/body-parser": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", @@ -5116,6 +5337,15 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5186,6 +5416,15 @@ "node": ">=0.10.0" } }, + "node_modules/cachedir": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", + "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -5249,6 +5488,12 @@ "node": ">=0.10.0" } }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -5269,6 +5514,15 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -5314,6 +5568,12 @@ "node": ">=6.0" } }, + "node_modules/ci-info": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", + "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "dev": true + }, "node_modules/class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -5484,6 +5744,37 @@ "node": ">= 0.2.0" } }, + "node_modules/cli-table3": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, + "node_modules/cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "dependencies": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -5653,6 +5944,15 @@ "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", "dev": true }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -5869,6 +6169,24 @@ "node": ">=0.10.0" } }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -5914,68 +6232,325 @@ "node_modules/csstype": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", - "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", - "dev": true + "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, - "node_modules/dargs": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/dargs/-/dargs-6.1.0.tgz", - "integrity": "sha512-5dVBvpBLBnPwSsYXqfybFyehMmC/EenKEcf23AhCTgTf48JFBbmJKqoZBsERDnjL0FyiVTYWdFsRfTLHxLyKdQ==", + "node_modules/cypress": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.3.1.tgz", + "integrity": "sha512-As9HrExjAgpgjCnbiQCuPdw5sWKx5HUJcK2EOKziu642akwufr/GUeqL5UnCPYXTyyibvEdWT/pSC2qnGW/e5w==", "dev": true, + "hasInstallScript": true, + "dependencies": { + "@cypress/request": "^2.88.10", + "@cypress/xvfb": "^1.2.4", + "@types/node": "^14.14.31", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.6.0", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^5.1.0", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "eventemitter2": "^6.4.3", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.0", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.6", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.3.2", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "bin": { + "cypress": "bin/cypress" + }, "engines": { - "node": ">=6" + "node": ">=12.0.0" } }, - "node_modules/dateformat": { - "version": "4.6.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", - "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "node_modules/cypress-react-selector": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cypress-react-selector/-/cypress-react-selector-3.0.0.tgz", + "integrity": "sha512-AQCgwbcMDkIdYcf6knvLxqzBnejahIbJPHqUhARi8k+QbM8sgUBDds98PaHJVMdPiX2J8RJjXHmUMPD8VerPSw==", "dev": true, - "engines": { - "node": "*" + "dependencies": { + "resq": "1.10.2" } }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/cypress/node_modules/@types/node": { + "version": "14.18.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.23.tgz", + "integrity": "sha512-MhbCWN18R4GhO8ewQWAFK4TGQdBpXWByukz7cWyJmXhvRuCIaM/oWytGPqVmDzgEnnaIc9ss6HbU5mUi+vyZPA==", + "dev": true + }, + "node_modules/cypress/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "dependencies": { - "ms": "2.1.2" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=6.0" + "node": ">=8" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/debuglog": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", - "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "node_modules/cypress/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, "engines": { - "node": "*" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "node_modules/cypress/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "engines": { + "node_modules/cypress/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cypress/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/cypress/node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cypress/node_modules/execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/cypress/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cypress/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/cypress/node_modules/human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true, + "engines": { + "node": ">=8.12.0" + } + }, + "node_modules/cypress/node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cypress/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/cypress/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/dargs": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-6.1.0.tgz", + "integrity": "sha512-5dVBvpBLBnPwSsYXqfybFyehMmC/EenKEcf23AhCTgTf48JFBbmJKqoZBsERDnjL0FyiVTYWdFsRfTLHxLyKdQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/dayjs": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz", + "integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { "node": ">=4.0.0" } }, @@ -6258,6 +6833,15 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -6323,6 +6907,39 @@ "tslib": "^2.0.3" } }, + "node_modules/dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/dotenv-defaults": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz", + "integrity": "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==", + "dev": true, + "dependencies": { + "dotenv": "^8.2.0" + } + }, + "node_modules/dotenv-webpack": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.0.0.tgz", + "integrity": "sha512-vsWj11yWbIxLUPcQDbifCGW1+Mp03XfApFHJTC+/Ag9g3D/AnxoaVZcp76LpuBmReRwIJ+YO1fVdhmpzh+LL1A==", + "dev": true, + "dependencies": { + "dotenv-defaults": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "webpack": "^4 || ^5" + } + }, "node_modules/download-stats": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/download-stats/-/download-stats-0.3.4.tgz", @@ -6344,6 +6961,16 @@ "integrity": "sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==", "dev": true }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/editions": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/editions/-/editions-2.3.1.tgz", @@ -6439,7 +7066,6 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, - "optional": true, "dependencies": { "once": "^1.4.0" } @@ -6457,6 +7083,18 @@ "node": ">=10.13.0" } }, + "node_modules/enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -6665,6 +7303,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-plugin-cypress": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz", + "integrity": "sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA==", + "dev": true, + "dependencies": { + "globals": "^11.12.0" + }, + "peerDependencies": { + "eslint": ">= 3.2.1" + } + }, "node_modules/eslint-plugin-react": { "version": "7.30.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.1.tgz", @@ -7037,6 +7687,12 @@ "node": ">= 0.6" } }, + "node_modules/eventemitter2": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.6.tgz", + "integrity": "sha512-OHqo4wbHX5VbvlbB6o6eDwhYmiTjrpWACjF8Pmof/GTD6rdBNdZFNck3xlhqOiQFGCOoq3uzHvA0cQpFHIGVAQ==", + "dev": true + }, "node_modules/eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -7075,6 +7731,18 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -7364,6 +8032,12 @@ } ] }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, "node_modules/extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -7443,6 +8117,50 @@ "node": ">=0.10.0" } }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -7504,6 +8222,15 @@ "node": ">=0.8.0" } }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -7816,6 +8543,15 @@ "node": ">=0.10.0" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -7859,6 +8595,21 @@ "node": ">= 0.6" } }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -8021,6 +8772,24 @@ "node": ">=0.10.0" } }, + "node_modules/getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "dependencies": { + "async": "^3.2.0" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/gh-got": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/gh-got/-/gh-got-5.0.0.tgz", @@ -8139,6 +8908,21 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "node_modules/global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "dev": true, + "dependencies": { + "ini": "2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -8644,6 +9428,20 @@ } } }, + "node_modules/http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -8810,6 +9608,15 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/inquirer": { "version": "8.2.4", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", @@ -9029,6 +9836,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "dependencies": { + "ci-info": "^3.2.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, "node_modules/is-core-module": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", @@ -9174,6 +9993,31 @@ "node": ">=0.10.0" } }, + "node_modules/is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "dependencies": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -9402,6 +10246,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -9486,6 +10336,12 @@ "node": ">=0.10.0" } }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, "node_modules/istextorbinary": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.6.0.tgz", @@ -9682,6 +10538,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -9700,6 +10562,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -9721,6 +10589,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", @@ -9733,6 +10607,18 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -9759,6 +10645,21 @@ "node": "*" } }, + "node_modules/jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.1.tgz", @@ -9793,6 +10694,15 @@ "node": ">=0.10.0" } }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, "node_modules/lazy-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", @@ -9825,6 +10735,33 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "node_modules/listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "dependencies": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "enquirer": ">= 2.3.0 < 3" + }, + "peerDependenciesMeta": { + "enquirer": { + "optional": true + } + } + }, "node_modules/load-yaml-file": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", @@ -9911,6 +10848,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -9997,6 +10940,88 @@ "node": ">=8" } }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-update/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10507,6 +11532,18 @@ "big-integer": "^1.6.16" } }, + "node_modules/nanoid": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz", + "integrity": "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, "node_modules/nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -11154,7 +12191,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11589,6 +12625,12 @@ "node": ">=0.10.0" } }, + "node_modules/ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, "node_modules/output-file-sync": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", @@ -11962,6 +13004,18 @@ "node": ">=8" } }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -12301,7 +13355,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -12330,12 +13383,23 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, - "optional": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -12494,6 +13558,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.4.0.tgz", "integrity": "sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==", + "dev": true, "peerDependencies": { "react": "*" } @@ -12501,8 +13566,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-query": { "version": "3.39.1", @@ -12553,6 +13617,21 @@ "react-dom": ">=16.8" } }, + "node_modules/react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/read-chunk": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.2.0.tgz", @@ -12952,6 +14031,15 @@ "node": ">= 0.10" } }, + "node_modules/request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "dependencies": { + "throttleit": "^1.0.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -13018,6 +14106,21 @@ "deprecated": "https://github.com/lydell/resolve-url#deprecated", "dev": true }, + "node_modules/resq": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.10.2.tgz", + "integrity": "sha512-HmgVS3j+FLrEDBTDYysPdPVF9/hioDMJ/otOiQDKqk77YfZeeLOj0qi34yObumcud1gBpk+wpBTEg4kMicD++A==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^2.0.1" + } + }, + "node_modules/resq/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -13059,6 +14162,12 @@ "node": ">=0.10.0" } }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -13505,6 +14614,53 @@ "node": ">=8" } }, + "node_modules/slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -13871,6 +15027,31 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -14280,6 +15461,12 @@ "url": "https://bevry.me/fund" } }, + "node_modules/throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==", + "dev": true + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -14392,6 +15579,19 @@ "node": ">=0.6" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -14545,6 +15745,24 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "dev": true }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -14692,6 +15910,15 @@ "imurmurhash": "^0.1.4" } }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unload": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", @@ -14927,6 +16154,26 @@ "node": ">= 0.8" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + }, "node_modules/vinyl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", @@ -15542,6 +16789,16 @@ "node": ">= 6" } }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "node_modules/yeoman-environment": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-3.9.1.tgz", @@ -18077,6 +19334,79 @@ "to-fast-properties": "^2.0.0" } }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "optional": true + }, + "@cypress/request": { + "version": "2.88.10", + "resolved": "https://registry.npmjs.org/@cypress/request/-/request-2.88.10.tgz", + "integrity": "sha512-Zp7F+R93N0yZyG34GutyTNr+okam7s/Fzc1+i3kcqOP8vk6OuajuE9qZJ6Rs+10/1JFtXFYMdyarnU1rZuJesg==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "http-signature": "~1.3.6", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^8.3.2" + }, + "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true + } + } + }, + "@cypress/xvfb": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@cypress/xvfb/-/xvfb-1.2.4.tgz", + "integrity": "sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==", + "dev": true, + "requires": { + "debug": "^3.1.0", + "lodash.once": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, "@discoveryjs/json-ext": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", @@ -18958,6 +20288,15 @@ "@types/react": "*" } }, + "@types/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", @@ -18989,6 +20328,18 @@ "@types/node": "*" } }, + "@types/sinonjs__fake-timers": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", + "integrity": "sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==", + "dev": true + }, + "@types/sizzle": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.3.tgz", + "integrity": "sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==", + "dev": true + }, "@types/sockjs": { "version": "0.3.33", "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", @@ -19017,6 +20368,16 @@ "@types/node": "*" } }, + "@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, "@typescript-eslint/eslint-plugin": { "version": "5.30.3", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.3.tgz", @@ -19448,6 +20809,12 @@ "dev": true, "requires": {} }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true + }, "ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -19494,6 +20861,12 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true }, + "arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true + }, "are-we-there-yet": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", @@ -19598,12 +20971,33 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "dev": true }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", "dev": true }, + "astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true + }, "async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", @@ -19622,12 +21016,30 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true + }, "atob": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "dev": true + }, "axios": { "version": "0.27.2", "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", @@ -20607,6 +22019,15 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, "big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -20665,6 +22086,18 @@ "readable-stream": "^3.4.0" } }, + "blob-util": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/blob-util/-/blob-util-2.0.2.tgz", + "integrity": "sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==", + "dev": true + }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, "body-parser": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", @@ -20781,6 +22214,12 @@ "ieee754": "^1.1.13" } }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -20842,6 +22281,12 @@ "unset-value": "^1.0.0" } }, + "cachedir": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.3.0.tgz", + "integrity": "sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw==", + "dev": true + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -20886,6 +22331,12 @@ "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw==", "dev": true }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -20903,6 +22354,12 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==", + "dev": true + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -20931,6 +22388,12 @@ "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true }, + "ci-info": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz", + "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", + "dev": true + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -21059,6 +22522,26 @@ "colors": "1.0.3" } }, + "cli-table3": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.2.tgz", + "integrity": "sha512-QyavHCaIC80cMivimWu4aWHilIpiDpfm3hGmqAmXVL1UsnbLuBSMd21hTX6VY4ZSDSM73ESLeF8TOYId3rBTbw==", + "dev": true, + "requires": { + "@colors/colors": "1.5.0", + "string-width": "^4.2.0" + } + }, + "cli-truncate": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz", + "integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==", + "dev": true, + "requires": { + "slice-ansi": "^3.0.0", + "string-width": "^4.2.0" + } + }, "cli-width": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", @@ -21203,6 +22686,12 @@ "integrity": "sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==", "dev": true }, + "common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -21375,6 +22864,15 @@ "capture-stack-trace": "^1.0.0" } }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -21408,8 +22906,190 @@ "csstype": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.0.tgz", - "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==", - "dev": true + "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" + }, + "cypress": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-10.3.1.tgz", + "integrity": "sha512-As9HrExjAgpgjCnbiQCuPdw5sWKx5HUJcK2EOKziu642akwufr/GUeqL5UnCPYXTyyibvEdWT/pSC2qnGW/e5w==", + "dev": true, + "requires": { + "@cypress/request": "^2.88.10", + "@cypress/xvfb": "^1.2.4", + "@types/node": "^14.14.31", + "@types/sinonjs__fake-timers": "8.1.1", + "@types/sizzle": "^2.3.2", + "arch": "^2.2.0", + "blob-util": "^2.0.2", + "bluebird": "^3.7.2", + "buffer": "^5.6.0", + "cachedir": "^2.3.0", + "chalk": "^4.1.0", + "check-more-types": "^2.24.0", + "cli-cursor": "^3.1.0", + "cli-table3": "~0.6.1", + "commander": "^5.1.0", + "common-tags": "^1.8.0", + "dayjs": "^1.10.4", + "debug": "^4.3.2", + "enquirer": "^2.3.6", + "eventemitter2": "^6.4.3", + "execa": "4.1.0", + "executable": "^4.1.1", + "extract-zip": "2.0.1", + "figures": "^3.2.0", + "fs-extra": "^9.1.0", + "getos": "^3.2.1", + "is-ci": "^3.0.0", + "is-installed-globally": "~0.4.0", + "lazy-ass": "^1.6.0", + "listr2": "^3.8.3", + "lodash": "^4.17.21", + "log-symbols": "^4.0.0", + "minimist": "^1.2.6", + "ospath": "^1.2.2", + "pretty-bytes": "^5.6.0", + "proxy-from-env": "1.0.0", + "request-progress": "^3.0.0", + "semver": "^7.3.2", + "supports-color": "^8.1.1", + "tmp": "~0.2.1", + "untildify": "^4.0.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "@types/node": { + "version": "14.18.23", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.23.tgz", + "integrity": "sha512-MhbCWN18R4GhO8ewQWAFK4TGQdBpXWByukz7cWyJmXhvRuCIaM/oWytGPqVmDzgEnnaIc9ss6HbU5mUi+vyZPA==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + }, + "execa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + } + } + }, + "cypress-react-selector": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cypress-react-selector/-/cypress-react-selector-3.0.0.tgz", + "integrity": "sha512-AQCgwbcMDkIdYcf6knvLxqzBnejahIbJPHqUhARi8k+QbM8sgUBDds98PaHJVMdPiX2J8RJjXHmUMPD8VerPSw==", + "dev": true, + "requires": { + "resq": "1.10.2" + } }, "dargs": { "version": "6.1.0", @@ -21417,12 +23097,27 @@ "integrity": "sha512-5dVBvpBLBnPwSsYXqfybFyehMmC/EenKEcf23AhCTgTf48JFBbmJKqoZBsERDnjL0FyiVTYWdFsRfTLHxLyKdQ==", "dev": true }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, "dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "dev": true }, + "dayjs": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.4.tgz", + "integrity": "sha512-Zj/lPM5hOvQ1Bf7uAvewDaUcsJoI6JmNqmHhHl3nyumwe0XHwt8sWdOVAPACJzCebL8gQCi+K49w7iKWnGwX9g==", + "dev": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -21671,6 +23366,15 @@ "utila": "~0.4" } }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -21718,6 +23422,30 @@ "tslib": "^2.0.3" } }, + "dotenv": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", + "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", + "dev": true + }, + "dotenv-defaults": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-2.0.2.tgz", + "integrity": "sha512-iOIzovWfsUHU91L5i8bJce3NYK5JXeAwH50Jh6+ARUdLiiGlYWfGw6UkzsYqaXZH/hjE/eCd/PlfM/qqyK0AMg==", + "dev": true, + "requires": { + "dotenv": "^8.2.0" + } + }, + "dotenv-webpack": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-8.0.0.tgz", + "integrity": "sha512-vsWj11yWbIxLUPcQDbifCGW1+Mp03XfApFHJTC+/Ag9g3D/AnxoaVZcp76LpuBmReRwIJ+YO1fVdhmpzh+LL1A==", + "dev": true, + "requires": { + "dotenv-defaults": "^2.0.2" + } + }, "download-stats": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/download-stats/-/download-stats-0.3.4.tgz", @@ -21736,6 +23464,16 @@ "integrity": "sha512-CEj8FwwNA4cVH2uFCoHUrmojhYh1vmCdOaneKJXwkeY1i9jnlslVo9dx+hQ5Hl9GnH/Bwy/IjxAyOePyPKYnzA==", "dev": true }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "editions": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/editions/-/editions-2.3.1.tgz", @@ -21812,7 +23550,6 @@ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, - "optional": true, "requires": { "once": "^1.4.0" } @@ -21827,6 +23564,15 @@ "tapable": "^2.2.0" } }, + "enquirer": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", + "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "dev": true, + "requires": { + "ansi-colors": "^4.1.1" + } + }, "entities": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", @@ -22102,6 +23848,15 @@ } } }, + "eslint-plugin-cypress": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz", + "integrity": "sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA==", + "dev": true, + "requires": { + "globals": "^11.12.0" + } + }, "eslint-plugin-react": { "version": "7.30.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.30.1.tgz", @@ -22254,6 +24009,12 @@ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true }, + "eventemitter2": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.6.tgz", + "integrity": "sha512-OHqo4wbHX5VbvlbB6o6eDwhYmiTjrpWACjF8Pmof/GTD6rdBNdZFNck3xlhqOiQFGCOoq3uzHvA0cQpFHIGVAQ==", + "dev": true + }, "eventemitter3": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", @@ -22283,6 +24044,15 @@ "strip-final-newline": "^2.0.0" } }, + "executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "requires": { + "pify": "^2.2.0" + } + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -22520,6 +24290,12 @@ } } }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, "extend-shallow": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", @@ -22572,17 +24348,46 @@ "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, "requires": { - "is-extendable": "^0.1.0" + "is-extendable": "^0.1.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true + } + } + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true } } }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -22638,6 +24443,15 @@ "websocket-driver": ">=0.5.1" } }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -22883,6 +24697,12 @@ "for-in": "^1.0.1" } }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true + }, "form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -22914,6 +24734,18 @@ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true }, + "fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "requires": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -23033,6 +24865,24 @@ "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", "dev": true }, + "getos": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", + "integrity": "sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==", + "dev": true, + "requires": { + "async": "^3.2.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, "gh-got": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/gh-got/-/gh-got-5.0.0.tgz", @@ -23128,6 +24978,15 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "global-dirs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz", + "integrity": "sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA==", + "dev": true, + "requires": { + "ini": "2.0.0" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -23524,6 +25383,17 @@ "micromatch": "^4.0.2" } }, + "http-signature": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.3.6.tgz", + "integrity": "sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw==", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^2.0.2", + "sshpk": "^1.14.1" + } + }, "https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -23639,6 +25509,12 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true + }, "inquirer": { "version": "8.2.4", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", @@ -23806,6 +25682,15 @@ "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", "dev": true }, + "is-ci": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", + "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", + "dev": true, + "requires": { + "ci-info": "^3.2.0" + } + }, "is-core-module": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", @@ -23903,6 +25788,24 @@ "is-extglob": "^2.1.1" } }, + "is-installed-globally": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", + "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", + "dev": true, + "requires": { + "global-dirs": "^3.0.0", + "is-path-inside": "^3.0.2" + }, + "dependencies": { + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + } + } + }, "is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -24053,6 +25956,12 @@ "has-symbols": "^1.0.2" } }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true + }, "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -24113,6 +26022,12 @@ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true + }, "istextorbinary": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/istextorbinary/-/istextorbinary-2.6.0.tgz", @@ -24255,6 +26170,12 @@ "esprima": "^4.0.0" } }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -24267,6 +26188,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -24285,12 +26212,28 @@ "integrity": "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw==", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", "dev": true }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -24308,6 +26251,18 @@ "through": ">=2.2.7 <3" } }, + "jsprim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", + "integrity": "sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, "jsx-ast-utils": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.1.tgz", @@ -24336,6 +26291,12 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==", + "dev": true + }, "lazy-cache": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", @@ -24362,6 +26323,22 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, + "listr2": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-3.14.0.tgz", + "integrity": "sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g==", + "dev": true, + "requires": { + "cli-truncate": "^2.1.0", + "colorette": "^2.0.16", + "log-update": "^4.0.0", + "p-map": "^4.0.0", + "rfdc": "^1.3.0", + "rxjs": "^7.5.1", + "through": "^2.3.8", + "wrap-ansi": "^7.0.0" + } + }, "load-yaml-file": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz", @@ -24432,6 +26409,12 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -24493,6 +26476,66 @@ } } }, + "log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "requires": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -24887,6 +26930,12 @@ "big-integer": "^1.6.16" } }, + "nanoid": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.0.tgz", + "integrity": "sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==", + "dev": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -25404,8 +27453,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" }, "object-copy": { "version": "0.1.0", @@ -25728,6 +27776,12 @@ "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "dev": true }, + "ospath": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz", + "integrity": "sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==", + "dev": true + }, "output-file-sync": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-1.1.2.tgz", @@ -26020,6 +28074,18 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -26263,7 +28329,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -26288,12 +28353,23 @@ } } }, + "proxy-from-env": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.0.0.tgz", + "integrity": "sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==", + "dev": true + }, + "psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", + "dev": true + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, - "optional": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -26405,13 +28481,13 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.4.0.tgz", "integrity": "sha512-fSbvHeVYo/B5/L4VhB7sBA1i2tS8MkT0Hb9t2H1AVPkwGfVHLJCqyr2Py9dKMxsyM63Eng1GkdZfbWj+Fmv8Rg==", + "dev": true, "requires": {} }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-query": { "version": "3.39.1", @@ -26440,6 +28516,17 @@ "react-router": "6.3.0" } }, + "react-transition-group": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.2.tgz", + "integrity": "sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==", + "requires": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + } + }, "read-chunk": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-3.2.0.tgz", @@ -26749,6 +28836,15 @@ "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", "dev": true }, + "request-progress": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", + "integrity": "sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==", + "dev": true, + "requires": { + "throttleit": "^1.0.0" + } + }, "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -26799,6 +28895,23 @@ "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", "dev": true }, + "resq": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/resq/-/resq-1.10.2.tgz", + "integrity": "sha512-HmgVS3j+FLrEDBTDYysPdPVF9/hioDMJ/otOiQDKqk77YfZeeLOj0qi34yObumcud1gBpk+wpBTEg4kMicD++A==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1" + }, + "dependencies": { + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", + "dev": true + } + } + }, "restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -26827,6 +28940,12 @@ "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -27187,6 +29306,43 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "slice-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz", + "integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, "smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -27496,6 +29652,23 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, "ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", @@ -27788,6 +29961,12 @@ "integrity": "sha512-MeqZRHLuaGamUXGuVn2ivtU3LA3mLCCIO5kUGoohTCoGmCBg/+8yPhWVX9WSl9telvVd8erftjFk9Fwb2dD6rw==", "dev": true }, + "throttleit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", + "integrity": "sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==", + "dev": true + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -27878,6 +30057,16 @@ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "dev": true }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, "tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -27992,6 +30181,21 @@ } } }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -28101,6 +30305,12 @@ "imurmurhash": "^0.1.4" } }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, "unload": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/unload/-/unload-2.2.0.tgz", @@ -28278,6 +30488,25 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true + } + } + }, "vinyl": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", @@ -28725,6 +30954,16 @@ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, "yeoman-environment": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/yeoman-environment/-/yeoman-environment-3.9.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 54c4324e..d9c0cc8b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,8 +3,11 @@ "version": "1.0.0", "description": "Gong Check Project", "scripts": { - "start": "webpack serve", - "build": "webpack --mode=production --node-env=production", + "start": "cross-env NODE_ENV=development webpack serve", + "build": "cross-env NODE_ENV=production webpack", + "build-local": "cross-env NODE_ENV=development webpack", + "build-staging": "cross-env NODE_ENV=staging webpack", + "build-release": "cross-env NODE_ENV=production webpack", "prettier": "npx prettier --write 'src/**/*.{tsx,ts,css,html,json}'" }, "repository": { @@ -20,10 +23,10 @@ "react-error-boundary": "^3.1.4", "react-query": "^3.39.1", "react-router-dom": "^6.3.0", + "react-transition-group": "^4.4.2", "recoil": "^0.7.4" }, "devDependencies": { - "react-icons": "^4.4.0", "@babel/core": "^7.18.6", "@babel/preset-env": "^7.18.6", "@babel/preset-react": "^7.18.6", @@ -37,6 +40,7 @@ "@types/node": "^18.0.0", "@types/react": "^18.0.14", "@types/react-dom": "^18.0.5", + "@types/react-transition-group": "^4.4.5", "@typescript-eslint/eslint-plugin": "^5.30.3", "@typescript-eslint/parser": "^5.30.3", "@webpack-cli/generators": "^2.5.0", @@ -44,11 +48,18 @@ "babel-loader": "^8.2.5", "babel-plugin-module-resolver": "^4.1.0", "clean-webpack-plugin": "^4.0.0", + "cross-env": "^7.0.3", + "cypress": "^10.3.1", + "cypress-react-selector": "^3.0.0", + "dotenv-webpack": "^8.0.0", "eslint": "^8.19.0", + "eslint-plugin-cypress": "^2.12.1", "eslint-plugin-react": "^7.30.1", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", + "nanoid": "^4.0.0", "prettier": "^2.7.1", + "react-icons": "^4.4.0", "ts-loader": "^9.3.1", "typescript": "^4.7.4", "webpack": "^5.73.0", diff --git a/frontend/public/index.html b/frontend/public/index.html index 110d12dd..bee50722 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -4,10 +4,12 @@ + Gong Check
+
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e8ea2bef..a31da716 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,13 @@ import routes from './Routes'; -import Transitions from '@/transitions'; +import ToastBar from './components/common/ToastBar'; +import Transitions from '@/Transitions'; import { useRoutes } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; import useTransitionSelect from '@/hooks/useTransitionSelect'; import { isShowModalState, modalComponentState } from '@/recoil/modal'; +import { isShowToastState } from '@/recoil/toast'; const App = () => { const content = useRoutes(routes, location); @@ -13,6 +15,8 @@ const App = () => { const isShowModal = useRecoilValue(isShowModalState); const modalComponent = useRecoilValue(modalComponentState); + const isShowToast = useRecoilValue(isShowToastState); + const transition = useTransitionSelect(); return ( @@ -21,6 +25,7 @@ const App = () => { {content} {isShowModal && modalComponent} + {isShowToast && } ); }; diff --git a/frontend/src/errorBoundary/ErrorUserToken.tsx b/frontend/src/ErrorBoundary/ErrorHostToken.tsx similarity index 72% rename from frontend/src/errorBoundary/ErrorUserToken.tsx rename to frontend/src/ErrorBoundary/ErrorHostToken.tsx index 597ced0c..45eee6c9 100644 --- a/frontend/src/errorBoundary/ErrorUserToken.tsx +++ b/frontend/src/ErrorBoundary/ErrorHostToken.tsx @@ -1,18 +1,17 @@ import { AxiosError } from 'axios'; import { ErrorBoundary } from 'react-error-boundary'; import { QueryErrorResetBoundary } from 'react-query'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; const EXPIRED_TOKEN_TEXT = '만료된 토큰입니다.'; const NOT_TOKEN_TEXT = '헤더에 토큰 값이 정상적으로 존재하지 않습니다.'; -interface ErrorUserTokenProps { +interface ErrorHostTokenProps { children: React.ReactNode; } -const ErrorUserToken: React.FC = ({ children }) => { +const ErrorHostToken: React.FC = ({ children }) => { const navigate = useNavigate(); - const { hostId } = useParams(); return ( @@ -22,8 +21,8 @@ const ErrorUserToken: React.FC = ({ children }) => { const message = err.response?.data.message; if (message === EXPIRED_TOKEN_TEXT || message === NOT_TOKEN_TEXT) { - localStorage.removeItem('user'); - navigate(`/enter/${hostId}/pwd`); + localStorage.removeItem('token'); + navigate(`/host`); } return <>; @@ -35,4 +34,4 @@ const ErrorUserToken: React.FC = ({ children }) => { ); }; -export default ErrorUserToken; +export default ErrorHostToken; diff --git a/frontend/src/ErrorBoundary/ErrorUserTask.tsx b/frontend/src/ErrorBoundary/ErrorUserTask.tsx new file mode 100644 index 00000000..6b307d19 --- /dev/null +++ b/frontend/src/ErrorBoundary/ErrorUserTask.tsx @@ -0,0 +1,61 @@ +import { AxiosError } from 'axios'; +import { useEffect, useState } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { QueryErrorResetBoundary } from 'react-query'; +import { useNavigate, useParams } from 'react-router-dom'; + +import useToast from '@/hooks/useToast'; + +// 사용자가 체크리스트 페이지에 접속 중일 때, 관리자가 작업을 수정 할 경우 발생 +const NOT_TASK_TEXT = '존재하지 않는 작업입니다.'; + +// 사용자들이 체크리스트 페이지에 접속 중일 때, 다른 사용자가 체크리시트를 제출 한 경우 발생 +const NOT_JOB_TEXT = '작업이 존재하지 않습니다.'; +const NOT_JOB_WORK_TEXT = '현재 진행중인 작업이 존재하지 않아 조회할 수 없습니다'; + +interface ErrorUserTaskProps { + children: React.ReactNode; +} + +const ErrorUserTask: React.FC = ({ children }) => { + const navigate = useNavigate(); + const { hostId } = useParams(); + const [message, setMessage] = useState(''); + + const { openToast } = useToast(); + + const isTaskListPage = location.pathname.split('/').length === 6; + + useEffect(() => { + if (message === NOT_TASK_TEXT) { + openToast('ERROR', `관리자가 체크리스트를 수정했습니다.`); + navigate(`/enter/${hostId}/spaces`); + } + + if (message === NOT_JOB_TEXT || message === NOT_JOB_WORK_TEXT) { + if (isTaskListPage) { + openToast('ERROR', `다른 사용자가 체크리스트를 제출 했습니다.`); + navigate(`/enter/${hostId}/spaces`); + } + } + }, [message]); + + return ( + + { + const err = error as AxiosError<{ message: string }>; + const message = err.response?.data.message; + + setMessage(message); + + return <>; + }} + > + {children} + + + ); +}; + +export default ErrorUserTask; diff --git a/frontend/src/ErrorBoundary/ErrorUserToken.tsx b/frontend/src/ErrorBoundary/ErrorUserToken.tsx new file mode 100644 index 00000000..6cc3dab6 --- /dev/null +++ b/frontend/src/ErrorBoundary/ErrorUserToken.tsx @@ -0,0 +1,46 @@ +import { AxiosError } from 'axios'; +import { useEffect, useState } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { QueryErrorResetBoundary } from 'react-query'; +import { useNavigate, useParams } from 'react-router-dom'; + +import useToast from '@/hooks/useToast'; + +const EXPIRED_TOKEN_TEXT = '만료된 토큰입니다.'; +const NOT_TOKEN_TEXT = '헤더에 토큰 값이 정상적으로 존재하지 않습니다.'; + +interface ErrorUserTokenProps { + children: React.ReactNode; +} + +const ErrorUserToken: React.FC = ({ children }) => { + const navigate = useNavigate(); + const { hostId } = useParams(); + const [message, setMessage] = useState(''); + + useEffect(() => { + if (message === EXPIRED_TOKEN_TEXT || message === NOT_TOKEN_TEXT) { + localStorage.removeItem('token'); + navigate(`/enter/${hostId}/pwd`); + } + }, [message]); + + return ( + + { + const err = error as AxiosError<{ message: string }>; + const message = err.response?.data.message; + + setMessage(message); + + return <>; + }} + > + {children} + + + ); +}; + +export default ErrorUserToken; diff --git a/frontend/src/Routes.tsx b/frontend/src/Routes.tsx index 785ed076..df17965e 100644 --- a/frontend/src/Routes.tsx +++ b/frontend/src/Routes.tsx @@ -5,17 +5,22 @@ import HostLayout from '@/layouts/HostLayout'; import ManageLayout from '@/layouts/ManageLayout'; import UserLayout from '@/layouts/UserLayout'; +// user const SpaceListPage = lazy(() => import('@/pages/user/SpaceList')); const JobListPage = lazy(() => import('@/pages/user/JobList')); const TaskListPage = lazy(() => import('@/pages/user/TaskList')); const PasswordPage = lazy(() => import('@/pages/user/Password')); +// host const HomePage = lazy(() => import('@/pages/host/Home')); -const LoginPage = lazy(() => import('@/pages/host/Login')); const AuthCallBackPage = lazy(() => import('@/pages/host/AuthCallBack')); const DashBoardPage = lazy(() => import('@/pages/host/DashBoard')); const SpaceCreatePage = lazy(() => import('@/pages/host/SpaceCreate')); +const SpaceUpdatePage = lazy(() => import('@/pages/host/SpaceUpdate')); const SpaceRecordPage = lazy(() => import('@/pages/host/SpaceRecord')); +const JobCreatePage = lazy(() => import('@/pages/host/JobCreate')); +const JobUpdatePage = lazy(() => import('@/pages/host/JobUpdate')); +const PasswordUpdatePage = lazy(() => import('@/pages/host/PasswordUpdate')); const routes = [ { @@ -52,10 +57,6 @@ const routes = [ path: '', element: , }, - { - path: 'login', - element: , - }, { path: 'authCallback', element: , @@ -65,23 +66,33 @@ const routes = [ element: , children: [ { - path: '', - element: , + path: 'passwordUpdate', + element: , }, { path: 'spaceCreate', element: , }, { - path: 'spaceRecord', + path: ':spaceId', + element: , + }, + { + path: ':spaceId/spaceUpdate', + element: , + }, + { + path: ':spaceId/spaceRecord', element: , }, - // 공간 생성, 수정 - // 공간 정보 수정 페이지 - // 공간 사용 내역 보기 페이지 - // 체크리스트 생성 페이지 - // 체크리스트 수정 페이지 - // 회원 정보 수정 페이지 + { + path: ':spaceId/jobCreate', + element: , + }, + { + path: ':spaceId/jobUpdate/:jobId', + element: , + }, ], }, ], diff --git a/frontend/src/transitions/index.tsx b/frontend/src/Transitions.tsx similarity index 100% rename from frontend/src/transitions/index.tsx rename to frontend/src/Transitions.tsx diff --git a/frontend/src/apis/config.ts b/frontend/src/apis/config.ts index b7fc2773..402f4252 100644 --- a/frontend/src/apis/config.ts +++ b/frontend/src/apis/config.ts @@ -1,7 +1,6 @@ import axios from 'axios'; -const DEV_URL = 'http://localhost:8080'; -const API_URL = DEV_URL; +const API_URL = process.env.REACT_APP_API_URL!; export const axiosInstance = axios.create({ baseURL: API_URL, @@ -13,7 +12,7 @@ export const axiosInstanceToken = axios.create({ axiosInstanceToken.interceptors.request.use( config => { - const accessToken = localStorage.getItem('user'); + const accessToken = localStorage.getItem('token'); if (accessToken) { config.headers = { diff --git a/frontend/src/apis/githubAuth.ts b/frontend/src/apis/githubAuth.ts index ee8be145..856a2189 100644 --- a/frontend/src/apis/githubAuth.ts +++ b/frontend/src/apis/githubAuth.ts @@ -1,14 +1,13 @@ -import axios, { AxiosResponse } from 'axios'; +import { AxiosResponse } from 'axios'; -const getToken = async (code: any) => { - const { - data, - }: AxiosResponse<{ - token: string; - existHost: number; - }> = await axios({ +import { ApiHostTokenData } from '@/types/apis'; + +import { axiosInstance } from './config'; + +const getToken = async (code: string | null) => { + const { data }: AxiosResponse = await axiosInstance({ method: 'POST', - url: `http://192.168.6.158:8080/api/login`, + url: '/api/login', data: { code, }, @@ -17,6 +16,6 @@ const getToken = async (code: any) => { return data; }; -const githubAuth = { getToken }; +const apiAuth = { getToken }; -export default githubAuth; +export default apiAuth; diff --git a/frontend/src/apis/host.ts b/frontend/src/apis/host.ts new file mode 100644 index 00000000..5b0d1bf7 --- /dev/null +++ b/frontend/src/apis/host.ts @@ -0,0 +1,18 @@ +import { AxiosResponse } from 'axios'; + +import { ApiEntranceCodedData } from '@/types/apis'; + +import { axiosInstanceToken } from './config'; + +const getEntranceCode = async () => { + const { data }: AxiosResponse = await axiosInstanceToken({ + method: 'GET', + url: `/api/hosts/entranceCode`, + }); + + return data; +}; + +const apiHost = { getEntranceCode }; + +export default apiHost; diff --git a/frontend/src/apis/image.ts b/frontend/src/apis/image.ts new file mode 100644 index 00000000..7bc1a697 --- /dev/null +++ b/frontend/src/apis/image.ts @@ -0,0 +1,16 @@ +import { axiosInstanceToken } from './config'; + +const postImageUpload = async (formData: any) => { + const { data } = await axiosInstanceToken({ + method: 'POST', + url: `/api/imageUpload`, + headers: { 'Content-Type': 'multipart/form-data' }, + data: formData, + }); + + return data; +}; + +const apiImage = { postImageUpload }; + +export default apiImage; diff --git a/frontend/src/apis/index.ts b/frontend/src/apis/index.ts index bd6dee09..39166c6a 100644 --- a/frontend/src/apis/index.ts +++ b/frontend/src/apis/index.ts @@ -1,81 +1,21 @@ -import { AxiosResponse } from 'axios'; - -import { ApiSpacesData, ApiJobData, ApiTaskData, ApiJobActiveData, ApiTokenData } from '@/types/apis'; - -import { axiosInstance, axiosInstanceToken } from './config'; - -const postPassword = async ({ hostId, password }: any) => { - const { data }: AxiosResponse = await axiosInstance({ - method: 'POST', - url: `api/hosts/${hostId}/enter`, - data: { - password, - }, - }); - - return data; -}; - -const getSpaces = async () => { - const { data }: AxiosResponse = await axiosInstanceToken({ - method: 'GET', - url: `api/spaces`, - }); - - return data; -}; - -const getJobs = async ({ spaceId }: any) => { - const { data }: AxiosResponse = await axiosInstanceToken({ - method: 'GET', - url: `/api/spaces/${spaceId}/jobs`, - }); - - return data; +import apiAuth from './githubAuth'; +import apiImage from './image'; +import apisJob from './job'; +import apisPassword from './password'; +import apisSlack from './slack'; +import apisSpace from './space'; +import apisSubmission from './submission'; +import apisTask from './task'; + +const apis = { + ...apisJob, + ...apisSpace, + ...apisSubmission, + ...apisPassword, + ...apisSlack, + ...apisTask, + ...apiAuth, + ...apiImage, }; -const getJobActive = async ({ jobId }: any) => { - const { data }: AxiosResponse = await axiosInstanceToken({ - method: 'GET', - url: `/api/jobs/${jobId}/active`, - }); - - return data; -}; - -const postNewTasks = async ({ jobId }: any) => { - return axiosInstanceToken({ - method: 'POST', - url: `/api/jobs/${jobId}/tasks/new`, - }); -}; - -const getTasks = async ({ jobId }: any) => { - const { data }: AxiosResponse = await axiosInstanceToken({ - method: 'GET', - url: `/api/jobs/${jobId}/tasks`, - }); - - return data; -}; - -const postCheckTask = ({ taskId }: any) => { - return axiosInstanceToken({ - method: 'POST', - url: `/api/tasks/${taskId}/flip`, - }); -}; - -const postJobComplete = ({ jobId, author }: any) => { - return axiosInstanceToken({ - method: 'POST', - url: `/api/jobs/${jobId}/complete`, - data: { - author, - }, - }); -}; - -const apis = { postPassword, getSpaces, getJobs, getJobActive, postNewTasks, getTasks, postCheckTask, postJobComplete }; - export default apis; diff --git a/frontend/src/apis/job.ts b/frontend/src/apis/job.ts new file mode 100644 index 00000000..daff6591 --- /dev/null +++ b/frontend/src/apis/job.ts @@ -0,0 +1,61 @@ +import { AxiosResponse } from 'axios'; + +import { ID, SectionType } from '@/types'; +import { ApiJobActiveData, ApiJobData } from '@/types/apis'; + +import { axiosInstanceToken } from './config'; + +const getJobs = async (spaceId: ID | undefined) => { + const { data }: AxiosResponse = await axiosInstanceToken({ + method: 'GET', + url: `/api/spaces/${spaceId}/jobs`, + }); + + return data; +}; + +const getJobActive = async (jobId: ID) => { + const { data }: AxiosResponse = await axiosInstanceToken({ + method: 'GET', + url: `/api/jobs/${jobId}/active`, + }); + + return data; +}; + +// job 생성 +// Location : /api/jobs/{jobId} +const postNewJob = (spaceId: ID | undefined, name: string, sections: SectionType[]) => { + return axiosInstanceToken({ + method: 'POST', + url: `/api/spaces/${spaceId}/jobs`, + data: { + name, + sections, + }, + }); +}; + +// job 수정 +const putJob = (jobId: ID | undefined, name: string, sections: SectionType[]) => { + return axiosInstanceToken({ + method: 'PUT', + url: `/api/jobs/${jobId}`, + data: { + name, + sections, + }, + }); +}; + +// job 삭제 +const deleteJob = (jobId: ID) => { + return axiosInstanceToken({ + method: 'DELETE', + url: `/api/jobs/${jobId}`, + }); +}; + +const apiJobs = { getJobs, getJobActive, postNewJob, putJob, deleteJob }; + +export default apiJobs; diff --git a/frontend/src/apis/password.ts b/frontend/src/apis/password.ts new file mode 100644 index 00000000..5dc5b9cf --- /dev/null +++ b/frontend/src/apis/password.ts @@ -0,0 +1,33 @@ +import { AxiosResponse } from 'axios'; + +import { ApiTokenData } from '@/types/apis'; + +import { axiosInstance, axiosInstanceToken } from './config'; + +const postPassword = async ({ hostId, password }: any) => { + const { data }: AxiosResponse = await axiosInstance({ + method: 'POST', + url: `api/hosts/${hostId}/enter`, + data: { + password, + }, + }); + + return data; +}; + +// /api/spacePassword +// space password 수정 +const patchSpacePassword = (password: number | string) => { + return axiosInstanceToken({ + method: 'PATCH', + url: `/api/spacePassword`, + data: { + password, + }, + }); +}; + +const apiPassword = { postPassword, patchSpacePassword }; + +export default apiPassword; diff --git a/frontend/src/apis/slack.ts b/frontend/src/apis/slack.ts new file mode 100644 index 00000000..d2327fac --- /dev/null +++ b/frontend/src/apis/slack.ts @@ -0,0 +1,32 @@ +import { AxiosResponse } from 'axios'; + +import { ID } from '@/types'; + +import { axiosInstanceToken } from './config'; + +type ApiSlackUrlData = { + slackUrl: string; +}; + +// slack URL 조회 +const getSlackUrl = async (jobId: ID) => { + const { data }: AxiosResponse = await axiosInstanceToken({ + method: 'GET', + url: `/api/jobs/${jobId}/slack`, + }); + + return data; +}; + +// slack URL 수정 +const putSlackUrl = (jobId: ID, slackUrl: string) => { + return axiosInstanceToken({ + method: 'PUT', + url: `/api/jobs/${jobId}/slack`, + data: { slackUrl }, + }); +}; + +const apiSlack = { getSlackUrl, putSlackUrl }; + +export default apiSlack; diff --git a/frontend/src/apis/space.ts b/frontend/src/apis/space.ts new file mode 100644 index 00000000..3734522a --- /dev/null +++ b/frontend/src/apis/space.ts @@ -0,0 +1,55 @@ +import { AxiosResponse } from 'axios'; + +import { ID } from '@/types'; +import { ApiSpacesData, ApiSpaceData } from '@/types/apis'; + +import { axiosInstanceToken } from './config'; + +const getSpaces = async () => { + const { data }: AxiosResponse = await axiosInstanceToken({ + method: 'GET', + url: `/api/spaces`, + }); + + return data; +}; + +// space 생성 +const postNewSpace = (name: string, imageUrl: string | undefined) => { + return axiosInstanceToken({ + method: 'POST', + url: `/api/spaces`, + data: { name, imageUrl }, + }); +}; + +// space 삭제 +const deleteSpace = (spaceId: string | undefined) => { + return axiosInstanceToken({ + method: 'DELETE', + url: `/api/spaces/${spaceId}`, + }); +}; + +// space 단건 조회 +const getSpace = async (spaceId: ID | undefined) => { + const { data }: AxiosResponse = await axiosInstanceToken({ + method: 'GET', + url: `/api/spaces/${spaceId}`, + }); + + return data; +}; + +// space 수정 +const putSpace = (spaceId: ID | undefined, name: string, imageUrl: string | undefined) => { + return axiosInstanceToken({ + method: 'PUT', + url: `/api/spaces/${spaceId}`, + data: { name, imageUrl }, + }); +}; + +const apiSpace = { getSpaces, postNewSpace, deleteSpace, getSpace, putSpace }; + +export default apiSpace; diff --git a/frontend/src/apis/submission.ts b/frontend/src/apis/submission.ts new file mode 100644 index 00000000..d4439b71 --- /dev/null +++ b/frontend/src/apis/submission.ts @@ -0,0 +1,29 @@ +import { AxiosResponse } from 'axios'; + +import { ApiSubmissionData } from '@/types/apis'; + +import { axiosInstanceToken } from './config'; + +const postJobComplete = ({ jobId, author }: any) => { + return axiosInstanceToken({ + method: 'POST', + url: `/api/jobs/${jobId}/complete`, + data: { + author, + }, + }); +}; + +// submission 목록 조회 +const getSubmission = async ({ spaceId }: any) => { + const { data }: AxiosResponse = await axiosInstanceToken({ + method: 'GET', + url: `/api/spaces/${spaceId}/submissions`, + }); + + return data; +}; + +const apiSubmission = { postJobComplete, getSubmission }; + +export default apiSubmission; diff --git a/frontend/src/apis/task.ts b/frontend/src/apis/task.ts new file mode 100644 index 00000000..3ebb2598 --- /dev/null +++ b/frontend/src/apis/task.ts @@ -0,0 +1,42 @@ +import { AxiosResponse } from 'axios'; + +import { ID } from '@/types'; +import { ApiTaskData } from '@/types/apis'; + +import { axiosInstanceToken } from './config'; + +const postNewRunningTasks = async (jobId: ID | undefined) => { + return axiosInstanceToken({ + method: 'POST', + url: `/api/jobs/${jobId}/runningTasks/new`, + }); +}; + +const getRunningTasks = async (jobId: ID | undefined) => { + const { data }: AxiosResponse = await axiosInstanceToken({ + method: 'GET', + url: `/api/jobs/${jobId}/runningTasks`, + }); + + return data; +}; + +const getTasks = async (jobId: ID | undefined) => { + const { data }: AxiosResponse = await axiosInstanceToken({ + method: 'GET', + url: `/api/jobs/${jobId}/tasks`, + }); + + return data; +}; + +const postCheckTask = (taskId: ID | undefined) => { + return axiosInstanceToken({ + method: 'POST', + url: `/api/tasks/${taskId}/flip`, + }); +}; + +const apiTask = { postCheckTask, getRunningTasks, getTasks, postNewRunningTasks }; + +export default apiTask; diff --git a/frontend/src/assets/emptyFolder.png b/frontend/src/assets/emptyFolder.png new file mode 100644 index 00000000..087d75a8 Binary files /dev/null and b/frontend/src/assets/emptyFolder.png differ diff --git a/frontend/src/assets/favicon.png b/frontend/src/assets/favicon.png index 2fbb7cd0..d8bad888 100644 Binary files a/frontend/src/assets/favicon.png and b/frontend/src/assets/favicon.png differ diff --git a/frontend/src/components/InputModal/index.tsx b/frontend/src/components/InputModal/index.tsx deleted file mode 100644 index f6786229..00000000 --- a/frontend/src/components/InputModal/index.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { css } from '@emotion/react'; -import { useState } from 'react'; - -import Button from '@/components/_common/Button'; -import Dimmer from '@/components/_common/Dimmer'; -import Input from '@/components/_common/Input'; - -import useModal from '@/hooks/useModal'; - -import apis from '@/apis'; - -import ModalPortal from '@/ModalPortal'; - -import theme from '@/styles/theme'; - -import styles from './styles'; - -interface InputModalProps { - title: string; - detail: string; - placeholder: string; - buttonText: string; -} - -const InputModal: React.FC = ({ title, detail, placeholder, buttonText }) => { - const [isDisabledButton, setIsDisabledButton] = useState(true); - const [password, setPassword] = useState(''); - - const { closeModal } = useModal(); - - const setToken = async (password: string) => { - const { token } = await apis.postPassword({ hostId: 1, password }); - localStorage.setItem('user', token); - }; - - const handleChange = (e: React.ChangeEvent) => { - const isTyped = !!e.target.value; - setPassword(e.target.value); - setIsDisabledButton(!isTyped); - }; - - const handleClickButton = async () => { - try { - await setToken(password); - closeModal(); - window.location.reload(); - } catch (err) { - alert('비밀번호를 확인해주세요.'); - } - }; - - return ( - - -
-

{title}

- {detail} - - -
-
-
- ); -}; - -export default InputModal; diff --git a/frontend/src/components/JobCard/useJobCard.ts b/frontend/src/components/JobCard/useJobCard.ts deleted file mode 100644 index 01195147..00000000 --- a/frontend/src/components/JobCard/useJobCard.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useNavigate } from 'react-router-dom'; - -import apis from '@/apis'; - -const useJobCard = (id: number) => { - const navigate = useNavigate(); - - const onClickJobCard = async () => { - const { active } = await apis.getJobActive({ jobId: id }); - if (!active) { - await apis.postNewTasks({ jobId: id }); - } - navigate(id.toString()); - }; - return { onClickJobCard }; -}; - -export default useJobCard; diff --git a/frontend/src/components/NameModal/useNameModal.ts b/frontend/src/components/NameModal/useNameModal.ts deleted file mode 100644 index 27d5c32f..00000000 --- a/frontend/src/components/NameModal/useNameModal.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useState } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; - -import useModal from '@/hooks/useModal'; - -import apis from '@/apis'; - -const useNameModal = (jobId: string | undefined) => { - const navigate = useNavigate(); - - const [name, setName] = useState(''); - const [isDisabledButton, setIsDisabledButton] = useState(true); - const { hostId } = useParams(); - - const { closeModal } = useModal(); - - const onChangeInput = (e: React.ChangeEvent) => { - const isTyped = !!e.target.value; - setName(e.target.value); - setIsDisabledButton(!isTyped); - }; - - const onClickButton = async () => { - try { - await apis.postJobComplete({ jobId, author: name }); - alert('제출 되었습니다.'); - closeModal(); - navigate(`/enter/${hostId}/spaces`); - } catch (err) { - alert(err); - } - }; - - return { onChangeInput, onClickButton, isDisabledButton, name }; -}; - -export default useNameModal; diff --git a/frontend/src/components/Navigation/index.tsx b/frontend/src/components/Navigation/index.tsx deleted file mode 100644 index 4ee11275..00000000 --- a/frontend/src/components/Navigation/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { css } from '@emotion/react'; -import { CgHomeAlt, CgGirl } from 'react-icons/cg'; - -import navigationLogo from '@/assets/navigationLogo.png'; - -import styles from './styles'; - -const SPACE_DATA = [ - { id: 1, name: '잠실 캠퍼스' }, - { id: 2, name: '선릉 캠퍼스' }, -]; - -const Navigation: React.FC = () => { - return ( -
-
- -
- -
- Menu -
-
- - 내 정보 수정 -
-
-
- -
- 나의 공간 목록 -
- {SPACE_DATA.map(space => ( -
- - {space.name} -
- ))} -
- + 새로운 공간 추가 -
-
-
-
- ); -}; - -export default Navigation; diff --git a/frontend/src/components/TaskCard/index.tsx b/frontend/src/components/TaskCard/index.tsx deleted file mode 100644 index a3f32453..00000000 --- a/frontend/src/components/TaskCard/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import CheckBox from '@/components/_common/Checkbox'; - -import apis from '@/apis'; - -import styles from './styles'; - -type TaskType = { - id: number; - name: string; - checked: boolean; -}; - -type TaskCardProps = { - tasks: Array; - getSections: () => void; -}; - -const TaskCard: React.FC = ({ tasks, getSections }) => { - const handleClickCheckBox = async ( - e: React.MouseEvent | React.ChangeEvent, - id: number - ) => { - e.preventDefault(); - await apis.postCheckTask({ taskId: id }); - getSections(); - }; - - return ( -
- {tasks.map((task, id) => ( -
- handleClickCheckBox(e, task.id)} - checked={task.checked} - id={JSON.stringify(task.id)} - /> - {task.name} -
- ))} -
- ); -}; - -export default TaskCard; diff --git a/frontend/src/components/_common/PageTitle/index.tsx b/frontend/src/components/_common/PageTitle/index.tsx deleted file mode 100644 index 8ad781fb..00000000 --- a/frontend/src/components/_common/PageTitle/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { css } from '@emotion/react'; - -interface PageTitleProps { - children: React.ReactNode; -} - -const PageTitle: React.FC = ({ children }) => { - return ( -

- {children} -

- ); -}; - -export default PageTitle; diff --git a/frontend/src/components/_common/Button/index.tsx b/frontend/src/components/common/Button/index.tsx similarity index 100% rename from frontend/src/components/_common/Button/index.tsx rename to frontend/src/components/common/Button/index.tsx diff --git a/frontend/src/components/_common/Checkbox/index.tsx b/frontend/src/components/common/Checkbox/index.tsx similarity index 100% rename from frontend/src/components/_common/Checkbox/index.tsx rename to frontend/src/components/common/Checkbox/index.tsx diff --git a/frontend/src/components/_common/Checkbox/styles.ts b/frontend/src/components/common/Checkbox/styles.ts similarity index 100% rename from frontend/src/components/_common/Checkbox/styles.ts rename to frontend/src/components/common/Checkbox/styles.ts diff --git a/frontend/src/components/_common/Dimmer/index.tsx b/frontend/src/components/common/Dimmer/index.tsx similarity index 73% rename from frontend/src/components/_common/Dimmer/index.tsx rename to frontend/src/components/common/Dimmer/index.tsx index 80fd3407..8f7088af 100644 --- a/frontend/src/components/_common/Dimmer/index.tsx +++ b/frontend/src/components/common/Dimmer/index.tsx @@ -6,13 +6,14 @@ import theme from '@/styles/theme'; interface DimmerProps { children: React.ReactNode; + mode: 'full' | 'mobile'; isAbleClick?: boolean; } -const Dimmer: React.FC = ({ children, isAbleClick = true }) => { +const Dimmer: React.FC = ({ children, isAbleClick = true, mode = 'full' }) => { const { closeModal } = useModal(); - const handleClickDimmed = (e: React.MouseEvent) => { + const onClickDimmed = (e: React.MouseEvent) => { if (e.currentTarget === e.target && isAbleClick) closeModal(); }; @@ -20,13 +21,14 @@ const Dimmer: React.FC = ({ children, isAbleClick = true }) => {
{children}
diff --git a/frontend/src/components/_common/GitHubLoginButton/index.tsx b/frontend/src/components/common/GitHubLoginButton/index.tsx similarity index 62% rename from frontend/src/components/_common/GitHubLoginButton/index.tsx rename to frontend/src/components/common/GitHubLoginButton/index.tsx index f8d65a16..e9d5aa08 100644 --- a/frontend/src/components/_common/GitHubLoginButton/index.tsx +++ b/frontend/src/components/common/GitHubLoginButton/index.tsx @@ -2,11 +2,9 @@ import { GoMarkGithub } from 'react-icons/go'; import styles from './styles'; -const GITHUB_LOGIN_URL = 'https://github.com/login/oauth/authorize?client_id=e78f7565dee502d18ca2'; - const GitHubLoginButton = () => { return ( - +

GitHub 로그인

diff --git a/frontend/src/components/_common/GitHubLoginButton/styles.ts b/frontend/src/components/common/GitHubLoginButton/styles.ts similarity index 100% rename from frontend/src/components/_common/GitHubLoginButton/styles.ts rename to frontend/src/components/common/GitHubLoginButton/styles.ts diff --git a/frontend/src/components/_common/Input/index.tsx b/frontend/src/components/common/Input/index.tsx similarity index 61% rename from frontend/src/components/_common/Input/index.tsx rename to frontend/src/components/common/Input/index.tsx index ec229687..600f1c19 100644 --- a/frontend/src/components/_common/Input/index.tsx +++ b/frontend/src/components/common/Input/index.tsx @@ -2,14 +2,14 @@ import { css } from '@emotion/react'; import theme from '@/styles/theme'; -type InputProps = React.InputHTMLAttributes; +type InputProps = React.ClassAttributes & React.InputHTMLAttributes; -const Input: React.FC = ({ placeholder, onChange }) => { +const Input: React.FC = ({ placeholder, onChange, ...props }) => { return ( = ({ placeholder, onChange }) => { padding: 8px 16px; font-size: 16px; &::placeholder { - color: ${theme.colors.gray}; + color: ${theme.colors.gray400}; } &:focus { outline: none; @@ -25,6 +25,7 @@ const Input: React.FC = ({ placeholder, onChange }) => { `} placeholder={placeholder} onChange={onChange} + {...props} /> ); }; diff --git a/frontend/src/components/common/Loading/index.tsx b/frontend/src/components/common/Loading/index.tsx new file mode 100644 index 00000000..1f7f7bac --- /dev/null +++ b/frontend/src/components/common/Loading/index.tsx @@ -0,0 +1,15 @@ +import styles from './styles'; + +const Loading: React.FC = () => { + return ( +
+
+
+
+
+ loading... +
+
+ ); +}; +export default Loading; diff --git a/frontend/src/components/common/Loading/styles.ts b/frontend/src/components/common/Loading/styles.ts new file mode 100644 index 00000000..7cf564d6 --- /dev/null +++ b/frontend/src/components/common/Loading/styles.ts @@ -0,0 +1,64 @@ +import { css } from '@emotion/react'; + +import animation from '@/styles/animation'; +import theme from '@/styles/theme'; + +const layout = css` + width: 100vw; + height: 100vh; +`; + +const spinner = css` + width: 100%; + height: 100%; + margin: 0 0 0 -1px; + display: flex; + align-items: center; + box-sizing: border-box; + justify-content: center; + position: relative; + border: 1px solid rgba(255, 255, 255, 0.3); +`; + +const text = css` + animation: ${animation.shake} 2s 0s infinite; + font-size: 2rem; + position: absolute; + bottom: 40%; +`; + +const faceSpinner = css` + width: 12rem; + height: 12rem; + border: 10px solid ${theme.colors.primary}; + border-radius: 4rem; + display: flex; + justify-content: center; +`; + +const faceSpinnerEye = css` + width: 6rem; + position: relative; + transform: translate(-50%, 20%); + animation: ${animation.spinnerFace} 3s infinite cubic-bezier(0.76, 0, 0.24, 1) both; + + &:before, + &:after { + content: ''; + left: 0; + width: 2rem; + height: 2rem; + border-radius: 0.8rem; + position: absolute; + background-color: ${theme.colors.black}; + animation: ${animation.spinnerEye} 1.5s 1s infinite; + } + &:after { + left: inherit; + right: 0; + } +`; + +const styles = { layout, spinner, faceSpinner, faceSpinnerEye, text }; + +export default styles; diff --git a/frontend/src/components/common/ToastBar/index.tsx b/frontend/src/components/common/ToastBar/index.tsx new file mode 100644 index 00000000..cf63c86e --- /dev/null +++ b/frontend/src/components/common/ToastBar/index.tsx @@ -0,0 +1,24 @@ +import { AiOutlineCheckCircle } from 'react-icons/ai'; +import { BiError } from 'react-icons/bi'; +import { useRecoilValue } from 'recoil'; + +import { toastState } from '@/recoil/toast'; + +import ToastPortal from '@/portals/ToastPortal'; + +import styles from './styles'; + +const ToastBar: React.FC = () => { + const { type, text } = useRecoilValue(toastState); + + return ( + +
+ {type === 'SUCCESS' ? : } + {text} +
+
+ ); +}; + +export default ToastBar; diff --git a/frontend/src/components/common/ToastBar/styles.ts b/frontend/src/components/common/ToastBar/styles.ts new file mode 100644 index 00000000..69247c78 --- /dev/null +++ b/frontend/src/components/common/ToastBar/styles.ts @@ -0,0 +1,31 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const layout = (type: 'SUCCESS' | 'ERROR' | '') => css` + position: fixed; + box-shadow: 0px 2px 14px -3px ${theme.colors.black}; + color: ${theme.colors.white}; + background-color: ${type === 'SUCCESS' ? theme.colors.green : theme.colors.red}; + bottom: 20px; + left: 50%; + transform: translate(-50%, 0); + min-width: 200px; + min-height: 60px; + padding: 16px; + border-radius: 8px; + display: flex; + align-items: center; + gap: 10px; + z-index: 1001; +`; + +const text = css` + display: table-cell; + text-align: left; + vertical-align: middle; +`; + +const styles = { layout, text }; + +export default styles; diff --git a/frontend/src/components/common/ToggleSwitch/index.tsx b/frontend/src/components/common/ToggleSwitch/index.tsx new file mode 100644 index 00000000..b3a6a9de --- /dev/null +++ b/frontend/src/components/common/ToggleSwitch/index.tsx @@ -0,0 +1,20 @@ +import styles from './styles'; + +interface ToggleSwitchProps { + toggle: boolean; + onClickSwitch: () => void; + left: string; + right: string; +} + +const ToggleSwitch: React.FC = ({ toggle, onClickSwitch, left, right }) => { + return ( + + ); +}; + +export default ToggleSwitch; diff --git a/frontend/src/components/common/ToggleSwitch/styles.ts b/frontend/src/components/common/ToggleSwitch/styles.ts new file mode 100644 index 00000000..aef29f78 --- /dev/null +++ b/frontend/src/components/common/ToggleSwitch/styles.ts @@ -0,0 +1,45 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const wrapper = css` + display: flex; + justify-content: space-between; + align-items: center; + background-color: ${theme.colors.shadow20}; + border-radius: 16px; + width: 120px; + height: 30px; + position: relative; + cursor: pointer; +`; + +const element = (selector: boolean) => css` + width: 50%; + border-radius: 15px; + text-align: center; + font-size: 12px; + color: ${selector && theme.colors.shadow50}; + transition: all 0.3s ease-in-out; + z-index: 1; +`; + +const ball = (toggle: boolean) => css` + background-color: white; + width: 48%; + height: 80%; + border-radius: 12px; + position: absolute; + left: 3px; + transition: all 0.3s ease-in-out; + box-shadow: 2px 2px 2px 0px ${theme.colors.shadow30}; + ${toggle && + css` + transform: translate(98%, 0); + transition: all 0.3s ease-in-out; + `} +`; + +const styles = { wrapper, element, ball }; + +export default styles; diff --git a/frontend/src/components/common/ToggleSwitch/useToggleSwitch.ts b/frontend/src/components/common/ToggleSwitch/useToggleSwitch.ts new file mode 100644 index 00000000..60e602a4 --- /dev/null +++ b/frontend/src/components/common/ToggleSwitch/useToggleSwitch.ts @@ -0,0 +1,13 @@ +import { useState } from 'react'; + +const useToggleSwitch = () => { + const [toggle, setToggle] = useState(false); + + const onClickToggle = () => { + setToggle(prev => !prev); + }; + + return { toggle, onClickToggle }; +}; + +export default useToggleSwitch; diff --git a/frontend/src/components/host/ImageBox/index.tsx b/frontend/src/components/host/ImageBox/index.tsx new file mode 100644 index 00000000..0ab38533 --- /dev/null +++ b/frontend/src/components/host/ImageBox/index.tsx @@ -0,0 +1,50 @@ +import { HiPlus } from 'react-icons/hi'; + +import styles from './styles'; + +interface ImageBoxProps { + type: 'read' | 'create' | 'update'; + data?: { name: string; imageUrl: string; id: number }; + imageUrl: string | undefined; + onChangeImg?: (e: React.FormEvent) => void; +} + +interface ImageLabelBoxProps extends React.LabelHTMLAttributes { + children?: React.ReactNode; + imageUrl: string | undefined; +} + +const ImageLabelBox: React.FC = ({ children, imageUrl, ...props }) => { + return ( + + ); +}; + +const ImageBox: React.FC = ({ type, imageUrl, onChangeImg }) => { + if (type === 'read') { + return ; + } + + return ( + + + {!imageUrl && ( +
+ +
+ )} +

{imageUrl ? '이미지 수정 시 클릭해 주세요.' : '이미지를 추가해 주세요.'}

+
+ ); +}; + +export default ImageBox; diff --git a/frontend/src/components/host/ImageBox/styles.ts b/frontend/src/components/host/ImageBox/styles.ts new file mode 100644 index 00000000..af6342ea --- /dev/null +++ b/frontend/src/components/host/ImageBox/styles.ts @@ -0,0 +1,48 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const imageBox = (imageUrl: string | undefined, borderStyle?: string) => css` + display: block; + background: ${theme.colors.gray300}; + background-size: cover; + background-repeat: no-repeat; + border: 2px ${theme.colors.gray400}; + border-radius: 8px; + border-style: ${borderStyle}; + width: 15rem; + height: 15rem; + position: relative; + margin: 0 1.5rem; + background-image: url(${imageUrl}); + cursor: pointer; +`; + +const imageInput = css` + opacity: 0; + z-index: -1; +`; + +const imageCoverText = css` + width: 100%; + color: ${theme.colors.gray200}; + text-shadow: 0px 0px 4px ${theme.colors.shadow60}; + font-size: 0.8rem; + position: absolute; + bottom: 0; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + z-index: 0; +`; + +const iconBox = css` + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +`; + +const styles = { imageBox, imageInput, imageCoverText, iconBox }; + +export default styles; diff --git a/frontend/src/components/host/ImageBox/useImageBox.ts b/frontend/src/components/host/ImageBox/useImageBox.ts new file mode 100644 index 00000000..997c3f50 --- /dev/null +++ b/frontend/src/components/host/ImageBox/useImageBox.ts @@ -0,0 +1,52 @@ +import checkFileSize from '@/utils/checkFileSize'; +import { AxiosError } from 'axios'; +import { useEffect, useState } from 'react'; +import { useMutation } from 'react-query'; + +import useToast from '@/hooks/useToast'; + +import apiImage from '@/apis/image'; + +const useImageBox = (prevImageUrl?: string | undefined) => { + const [imageUrl, setImageUrl] = useState(''); + const { openToast } = useToast(); + + const { mutateAsync: uploadImage } = useMutation((formData: any) => apiImage.postImageUpload(formData), { + onError: (err: AxiosError<{ message: string }>) => { + openToast('ERROR', `${err.response?.data.message}`); + }, + }); + + const onChangeImg = async (e: React.FormEvent) => { + const input = e.target as HTMLInputElement; + + if (!input.files?.length) { + return; + } + + const file = input.files[0]; + const formData = new FormData(); + formData.append('image', file); + + const isOkayFileSize = checkFileSize(file); + + if (!isOkayFileSize) { + input.value = ''; + return; + } + + const { imageUrl: newImageUrl } = await uploadImage(formData); + + setImageUrl(newImageUrl); + }; + + useEffect(() => { + if (prevImageUrl) { + setImageUrl(prevImageUrl); + } + }, [prevImageUrl]); + + return { imageUrl, onChangeImg }; +}; + +export default useImageBox; diff --git a/frontend/src/components/host/JobBox/index.tsx b/frontend/src/components/host/JobBox/index.tsx new file mode 100644 index 00000000..f83bc470 --- /dev/null +++ b/frontend/src/components/host/JobBox/index.tsx @@ -0,0 +1,41 @@ +import useJobBox from './useJobBox'; + +import Button from '@/components/common/Button'; + +import { JobType } from '@/types'; + +import styles from './styles'; + +interface JobBoxProps { + job: JobType; +} + +const JobBox: React.FC = ({ job }) => { + const { onClickUpdateJobButton, onClickDeleteJobButton } = useJobBox(); + + return ( +
+ {job.name} +
+ + +
+
+ ); +}; + +export default JobBox; diff --git a/frontend/src/components/host/JobBox/styles.ts b/frontend/src/components/host/JobBox/styles.ts new file mode 100644 index 00000000..1a2ca912 --- /dev/null +++ b/frontend/src/components/host/JobBox/styles.ts @@ -0,0 +1,38 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const jobBox = css` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 56px; + padding: 12px 16px; + background-color: ${theme.colors.gray100}; + border-radius: 8px; + color: ${theme.colors.black}; + font-size: 500; + font-size: 18px; +`; + +const updateButton = css` + width: auto; + height: auto; + padding: 8px 12px; + font-size: 12px; + margin: 0 8px; +`; + +const deleteButton = css` + width: auto; + height: auto; + padding: 8px 12px; + font-size: 12px; + margin: 0; + background-color: ${theme.colors.red}; +`; + +const styles = { jobBox, updateButton, deleteButton }; + +export default styles; diff --git a/frontend/src/components/host/JobBox/useJobBox.tsx b/frontend/src/components/host/JobBox/useJobBox.tsx new file mode 100644 index 00000000..4f17580c --- /dev/null +++ b/frontend/src/components/host/JobBox/useJobBox.tsx @@ -0,0 +1,27 @@ +import { useState } from 'react'; +import { useMutation } from 'react-query'; +import { useNavigate } from 'react-router-dom'; + +import apiJobs from '@/apis/job'; + +import { ID } from '@/types'; + +const useJobBox = () => { + const navigate = useNavigate(); + + const { mutate: deleteJob } = useMutation((jobId: ID) => apiJobs.deleteJob(jobId)); + + const onClickUpdateJobButton = (jobId: ID, jobName: string) => { + navigate(`jobUpdate/${jobId}`, { state: { jobName } }); + }; + + const onClickDeleteJobButton = (jobId: ID) => { + if (confirm('해당 업무를 삭제하시겠습니까?')) { + deleteJob(jobId); + } + }; + + return { onClickUpdateJobButton, onClickDeleteJobButton }; +}; + +export default useJobBox; diff --git a/frontend/src/components/host/JobControl/index.tsx b/frontend/src/components/host/JobControl/index.tsx new file mode 100644 index 00000000..cdd24913 --- /dev/null +++ b/frontend/src/components/host/JobControl/index.tsx @@ -0,0 +1,31 @@ +import Button from '@/components/common/Button'; + +import useEditInput from '@/hooks/useEditInput'; + +import styles from './styles'; + +interface JobControlProps { + mode: 'create' | 'update'; + jobName: string; + onChangeJobName: (e: React.ChangeEvent) => void; +} + +const JobControl: React.FC = ({ mode, jobName, onChangeJobName }) => { + return ( +
+ + +
+ ); +}; + +export default JobControl; diff --git a/frontend/src/components/host/JobControl/styles.ts b/frontend/src/components/host/JobControl/styles.ts new file mode 100644 index 00000000..7e954b18 --- /dev/null +++ b/frontend/src/components/host/JobControl/styles.ts @@ -0,0 +1,49 @@ +import { css } from '@emotion/react'; + +import theme from '@/styles/theme'; + +const header = css` + display: flex; + justify-content: space-between; + align-items: center; + height: 100px; + width: 100%; + border-bottom: 1px solid ${theme.colors.gray300}; + padding: 16px 32px; + font-size: 16px; + + @media screen and (max-width: 720px) { + font-size: 14px; + } +`; + +const createButton = css` + margin: 0; + margin-left: 12px; + font-size: 1.2em; + width: fit-content; + height: fit-content; + padding: 12px; + background-color: ${theme.colors.green}; +`; + +const jobNameInput = css` + border: none; + border-radius: 12px; + width: 55%; + height: 1.5em; + padding: 1em; + font-size: 1.5em; + font-weight: 500; + margin: 12px 0; + background-color: ${theme.colors.white}; + outline: 1px solid ${theme.colors.gray400}; + + :focus { + outline: 2px solid ${theme.colors.primary}; + } +`; + +const styles = { header, jobNameInput, createButton }; + +export default styles; diff --git a/frontend/src/components/host/JobList/index.tsx b/frontend/src/components/host/JobList/index.tsx deleted file mode 100644 index c2575054..00000000 --- a/frontend/src/components/host/JobList/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import Button from '@/components/_common/Button'; - -import slackIcon from '@/assets/slackIcon.svg'; - -import styles from './styles'; - -const JOB_LIST = [ - { id: 1, name: '청소' }, - { id: 2, name: '마감' }, -]; - -const JobList: React.FC = () => { - const handleClickSlackButton = () => { - alert('슬랙 버튼 클릭'); - }; - - const handleClickNewJobButton = () => { - alert('새 공간 생성 버튼 클릭'); - }; - - const handleClickUpdateJobButton = () => { - alert('작업 수정 버튼 클릭'); - }; - - const handleClickDeleteJobButton = () => { - alert('작업 삭제 버튼 클릭'); - }; - - return ( -
-
- 공간 업무 목록 -
- - -
-
- -
- {JOB_LIST.map(job => ( -
- {job.name} -
- - -
-
- ))} -
-
- ); -}; - -export default JobList; diff --git a/frontend/src/components/host/JobListCard/index.tsx b/frontend/src/components/host/JobListCard/index.tsx new file mode 100644 index 00000000..e24638ea --- /dev/null +++ b/frontend/src/components/host/JobListCard/index.tsx @@ -0,0 +1,41 @@ +import useJobListCard from './useJobListCard'; + +import Button from '@/components/common/Button'; +import JobBox from '@/components/host/JobBox'; + +import { JobType } from '@/types'; + +import emptyFolder from '@/assets/emptyFolder.png'; + +import styles from './styles'; + +interface JobListCardProps { + jobs: JobType[] | []; +} + +const JobListCard: React.FC = ({ jobs }) => { + const { onClickNewJobButton } = useJobListCard(); + + return ( +
+
+ 공간 업무 목록 + +
+
+ {jobs.length === 0 ? ( +
+ +
생성된 업무가 없어요.
+
+ ) : ( + jobs.map(job => ) + )} +
+
+ ); +}; + +export default JobListCard; diff --git a/frontend/src/components/host/JobList/styles.ts b/frontend/src/components/host/JobListCard/styles.ts similarity index 52% rename from frontend/src/components/host/JobList/styles.ts rename to frontend/src/components/host/JobListCard/styles.ts index 26fe265d..2a1d7465 100644 --- a/frontend/src/components/host/JobList/styles.ts +++ b/frontend/src/components/host/JobListCard/styles.ts @@ -3,20 +3,21 @@ import { css } from '@emotion/react'; import theme from '@/styles/theme'; const layout = css` - width: 40vw; - height: 40vh; - background-color: white; + min-width: 320px; + width: 100%; + height: 30.2rem; + background-color: ${theme.colors.white}; + box-shadow: 2px 2px 2px 2px ${theme.colors.shadow10}; + border-radius: 8px; `; const title = css` display: flex; align-items: center; justify-content: space-between; - padding: 24px; - height: 20%; - font-size: 18px; - border-bottom: 1px solid ${theme.colors.lightGray}; - color: ${theme.colors.blackHost}; + padding: 1rem 1.25rem; + font-size: 1.4rem; + border-bottom: 1px solid ${theme.colors.gray300}; `; const jobListWrapper = css` @@ -26,20 +27,6 @@ const jobListWrapper = css` align-items: center; padding: 24px; overflow-y: scroll; - -ms-overflow-style: none; - - ::-webkit-scrollbar { - display: hidden; - width: 4px; - } - ::-webkit-scrollbar-thumb { - background-color: ${theme.colors.lightGrayHost}; - height: 4px; - border: 100%; - } - ::-webkit-scrollbar-track { - display: none; - } div + div { margin-top: 16px; @@ -52,32 +39,21 @@ const jobList = css` justify-content: space-between; width: 100%; padding: 12px 16px; - background-color: ${theme.colors.lightGrayHost}; + background-color: ${theme.colors.gray100}; border-radius: 8px; - color: ${theme.colors.blackHost}; -`; - -const slackButton = css` - width: auto; - height: auto; - padding: 6px 10px; - font-size: 12px; - margin: 0 12px; - background-color: ${theme.colors.white}; color: ${theme.colors.black}; - border: 1px solid ${theme.colors.gray}; - - img { - height: 10px; - margin-right: 6px; - } + font-size: 500; + font-size: 18px; `; const newJobButton = css` width: auto; - height: auto; - padding: 6px; - font-size: 12px; + height: 2rem; + padding: 6px 10px; + font-weight: 500; + font-size: 1rem; + padding: 0 12px; + margin: 0; `; @@ -95,9 +71,22 @@ const deleteButton = css` padding: 8px 12px; font-size: 12px; margin: 0; - background-color: tomato; + background-color: ${theme.colors.red}; `; -const styles = { layout, title, jobListWrapper, jobList, slackButton, newJobButton, updateButton, deleteButton }; +const empty = css` + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + img { + max-width: 100px; + margin-bottom: 12px; + } +`; +const styles = { layout, title, jobListWrapper, jobList, newJobButton, updateButton, deleteButton, empty }; export default styles; diff --git a/frontend/src/components/host/JobListCard/useJobListCard.tsx b/frontend/src/components/host/JobListCard/useJobListCard.tsx new file mode 100644 index 00000000..a12e3ae7 --- /dev/null +++ b/frontend/src/components/host/JobListCard/useJobListCard.tsx @@ -0,0 +1,13 @@ +import { useNavigate } from 'react-router-dom'; + +const useJobListCard = () => { + const navigate = useNavigate(); + + const onClickNewJobButton = () => { + navigate('jobCreate'); + }; + + return { onClickNewJobButton }; +}; + +export default useJobListCard; diff --git a/frontend/src/components/host/Navigation/index.tsx b/frontend/src/components/host/Navigation/index.tsx new file mode 100644 index 00000000..925accac --- /dev/null +++ b/frontend/src/components/host/Navigation/index.tsx @@ -0,0 +1,54 @@ +import useHostNavigation from './useHostNavigation'; +import { CgHomeAlt } from 'react-icons/cg'; +import { RiLockPasswordLine } from 'react-icons/ri'; + +import navigationLogo from '@/assets/navigationLogo.png'; + +import styles from './styles'; + +const Navigation: React.FC = () => { + const { selectedSpaceId, spaceData, onClickPasswordUpdate, onClickSpace, onClickNewSpace } = useHostNavigation(); + + return ( +
+
+ +
+ +
+ Menu +
+
+ + 공간 입장코드 변경 +
+
+
+ +
+ 나의 공간 목록 +
+ {spaceData?.spaces.map(space => { + const isSelectedSpace = space.id === Number(selectedSpaceId); + + return ( +
onClickSpace(space.id)} + > + + {space.name} +
+ ); + })} +
+ + 새로운 공간 추가 +
+
+
+
+ ); +}; + +export default Navigation; diff --git a/frontend/src/components/Navigation/styles.ts b/frontend/src/components/host/Navigation/styles.ts similarity index 57% rename from frontend/src/components/Navigation/styles.ts rename to frontend/src/components/host/Navigation/styles.ts index 189425d8..0afe853a 100644 --- a/frontend/src/components/Navigation/styles.ts +++ b/frontend/src/components/host/Navigation/styles.ts @@ -3,13 +3,22 @@ import { css } from '@emotion/react'; import theme from '@/styles/theme'; const layout = css` + font-size: 16px; position: fixed; top: 0; left: 0; height: 100vh; - width: 224px; - background-color: white; - box-shadow: 6px 0 8px #f0f3f8; + width: 14em; + background-color: ${theme.colors.white}; + box-shadow: 6px 0 8px ${theme.colors.gray350}; + z-index: 1; + + @media screen and (max-width: 1024px) { + font-size: 14px; + } + @media screen and (max-width: 720px) { + font-size: 12px; + } `; const logo = css` @@ -33,9 +42,9 @@ const category = css` `; const categoryTitle = css` - font-size: 16px; + font-size: 1em; font-weight: 600; - color: #808080; + color: ${theme.colors.gray800}; margin: 8px 0; padding: 0 8px; `; @@ -50,12 +59,12 @@ const categoryTextWrapper = css` display: flex; align-items: center; width: 100%; - font-size: 14px; + font-size: 0.875em; font-weight: 500; - background-color: ${theme.colors.lightGrayHost}; + background-color: ${theme.colors.gray100}; padding: 12px 8px; margin: 4px 0; - color: ${theme.colors.blackHost}; + color: ${theme.colors.gray800}; cursor: pointer; svg { @@ -67,7 +76,15 @@ const categoryTextWrapper = css` } :hover { - background-color: #f1f8fe; + background-color: ${theme.colors.gray200}; + } +`; + +const selectedTextWrapper = css` + background-color: ${theme.colors.skyblue100}; + + :hover { + background-color: ${theme.colors.skyblue200}; } `; @@ -76,8 +93,8 @@ const addNewSpace = css` margin: 14px 0; justify-content: center; width: 100%; - color: #808080; - font-size: 14px; + color: ${theme.colors.gray800}; + font-size: 0.875em; span:hover { font-weight: 600; @@ -85,6 +102,16 @@ const addNewSpace = css` } `; -const styles = { layout, logo, logoImage, category, categoryTitle, categoryList, categoryTextWrapper, addNewSpace }; +const styles = { + layout, + logo, + logoImage, + category, + categoryTitle, + categoryList, + categoryTextWrapper, + selectedTextWrapper, + addNewSpace, +}; export default styles; diff --git a/frontend/src/components/host/Navigation/useHostNavigation.ts b/frontend/src/components/host/Navigation/useHostNavigation.ts new file mode 100644 index 00000000..a3a43951 --- /dev/null +++ b/frontend/src/components/host/Navigation/useHostNavigation.ts @@ -0,0 +1,36 @@ +import { useState } from 'react'; +import { useQuery } from 'react-query'; +import { useNavigate, useParams } from 'react-router-dom'; + +import apiSpace from '@/apis/space'; + +import { ID } from '@/types'; + +const useHostNavigation = () => { + const navigate = useNavigate(); + + const { spaceId } = useParams(); + + const [selectedSpaceId, setSelectedSpaceId] = useState(spaceId); + + const { data: spaceData } = useQuery(['spaces'], apiSpace.getSpaces, { + suspense: true, + }); + + const onClickPasswordUpdate = () => { + navigate('/host/manage/passwordUpdate'); + }; + + const onClickSpace = (spaceId: number) => { + setSelectedSpaceId(spaceId); + navigate(`${spaceId}`); + }; + + const onClickNewSpace = () => { + navigate('/host/manage/spaceCreate'); + }; + + return { selectedSpaceId, spaceData, onClickPasswordUpdate, onClickSpace, onClickNewSpace }; +}; + +export default useHostNavigation; diff --git a/frontend/src/components/host/SectionCard/index.tsx b/frontend/src/components/host/SectionCard/index.tsx new file mode 100644 index 00000000..7cba22b1 --- /dev/null +++ b/frontend/src/components/host/SectionCard/index.tsx @@ -0,0 +1,42 @@ +import TaskBox from '../TaskBox'; +import useSectionCardDefault from './useSectionCardDefault'; +import { BiNews, BiX } from 'react-icons/bi'; + +import { SectionType } from '@/types'; + +import styles from './styles'; + +interface SectionCardProps { + section: SectionType; + sectionIndex: number; +} + +const SectionCard: React.FC = ({ section, sectionIndex }) => { + const { onChange, onClickDelete, onClickCreate, onClickSectionDetail, hasSectionDetailInfo } = + useSectionCardDefault(sectionIndex); + + return ( +
+ +
+ + +
+ {section.tasks.map((task, taskIndex) => ( + + ))} + +
+ ); +}; + +export default SectionCard; diff --git a/frontend/src/components/host/SectionCard/styles.ts b/frontend/src/components/host/SectionCard/styles.ts new file mode 100644 index 00000000..4f978f21 --- /dev/null +++ b/frontend/src/components/host/SectionCard/styles.ts @@ -0,0 +1,97 @@ +import { css } from '@emotion/react'; + +import animation from '@/styles/animation'; +import theme from '@/styles/theme'; + +const container = css` + display: flex; + flex-direction: column; + width: 100%; + max-width: 480px; + height: 360px; + overflow-y: scroll; + padding: 32px; + background-color: ${theme.colors.white}; + box-shadow: 2px 2px 2px 2px ${theme.colors.shadow20}; + border-radius: 8px; + position: relative; + + ::-webkit-scrollbar { + display: none; + } + + ::-webkit-scrollbar-thumb { + background: ${theme.colors.shadow20}; + border-radius: 12px; + } + + ::-webkit-scrollbar-thumb:hover { + background: ${theme.colors.shadow60}; + } + + ::-webkit-scrollbar-thumb:active { + background: ${theme.colors.shadow80}; + } + + ::-webkit-scrollbar-button { + display: none; + } +`; + +const titleWrapper = css` + display: flex; + justify-content: space-between; + align-items: center; + font-size: 20px; + margin-bottom: 8px; + padding-bottom: 16px; + border-bottom: 2px solid ${theme.colors.shadow20}; +`; + +const deleteButton = css` + top: 8px; + right: 8px; + position: absolute; + margin: 0 4px; + color: ${theme.colors.gray800}; + cursor: pointer; +`; + +const detailButton = (hasSectionDetailInfo: boolean) => css` + margin: 0 4px; + cursor: pointer; + color: ${hasSectionDetailInfo ? theme.colors.green : theme.colors.gray400}; + :hover { + animation: ${animation.shake} 2s infinite linear alternate; + } +`; + +const newTaskButton = css` + align-self: center; + margin: 8px 0; + font-size: 12px; + padding: 8px 16px; + border-radius: 24px; + background-color: ${theme.colors.shadow10}; + :hover { + background-color: ${theme.colors.shadow20}; + } +`; + +const input = css` + font-size: 18px; + line-height: 38px; + border: 1px solid ${theme.colors.shadow30}; + border-radius: 12px; + padding: 0 16px; + background-color: ${theme.colors.white}; + width: 80%; + + :focus { + outline: 2px solid ${theme.colors.primary}; + } +`; + +const styles = { container, titleWrapper, input, detailButton, deleteButton, newTaskButton }; + +export default styles; diff --git a/frontend/src/components/host/SectionCard/useSectionCardDefault.tsx b/frontend/src/components/host/SectionCard/useSectionCardDefault.tsx new file mode 100644 index 00000000..c1700270 --- /dev/null +++ b/frontend/src/components/host/SectionCard/useSectionCardDefault.tsx @@ -0,0 +1,45 @@ +import SectionDetailModal from '../SectionDetailModal'; + +import useModal from '@/hooks/useModal'; +import useSections from '@/hooks/useSections'; + +const useSectionCardDefault = (sectionIndex: number) => { + const { openModal } = useModal(); + + const { editSection, deleteSection, createTask, getSectionInfo } = useSections(); + + const hasSectionDetailInfo = () => { + const { imageUrl, description } = getSectionInfo(sectionIndex); + return !!imageUrl || !!description; + }; + + const onChange = (e: React.ChangeEvent) => { + editSection(sectionIndex, e.target.value); + }; + + const onClickDelete = () => { + deleteSection(sectionIndex); + }; + + const onClickSectionDetail = () => { + const previousImageUrl = getSectionInfo(sectionIndex).imageUrl; + const previousDescription = getSectionInfo(sectionIndex).description; + + openModal( + + ); + }; + + const onClickCreate = () => { + createTask(sectionIndex); + }; + + return { onChange, onClickDelete, onClickCreate, onClickSectionDetail, hasSectionDetailInfo }; +}; + +export default useSectionCardDefault; diff --git a/frontend/src/components/host/SectionDetailModal/index.tsx b/frontend/src/components/host/SectionDetailModal/index.tsx new file mode 100644 index 00000000..8f454cad --- /dev/null +++ b/frontend/src/components/host/SectionDetailModal/index.tsx @@ -0,0 +1,71 @@ +import useSectionDetailModal from './useSectionDetailModal'; +import { BiX } from 'react-icons/bi'; + +import Button from '@/components/common/Button'; +import Dimmer from '@/components/common/Dimmer'; + +import ModalPortal from '@/portals/ModalPortal'; + +import styles from './styles'; + +interface SectionDetailModalProps { + target: 'section' | 'task'; + sectionIndex: number; + taskIndex?: number; + previousImageUrl: string; + previousDescription: string; +} + +const SectionDetailModal: React.FC = props => { + const { target, sectionIndex, taskIndex } = props; + + const { + getSectionInfo, + getTaskInfo, + fileInput, + isDisabledButton, + onChangeImage, + onChangeText, + onClickSaveButton, + closeModal, + imageUrl, + description, + } = useSectionDetailModal(props); + + return ( + + +
+ +

+ {target === 'section' + ? getSectionInfo(sectionIndex).name + : getTaskInfo(sectionIndex, taskIndex as number).name} +

+ fileInput.current?.click()} /> + +
+