Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,15 +86,15 @@ APP_SWAGGER_SERVER_URL=http://localhost:8080
```bash
curl -X POST http://localhost:8080/api/auth/signup \
-H "Content-Type: application/json" \
-d '{"id":"test_user","password":"password123!","nickname":"테스터"}'
-d '{"id":"testuser","password":"password123!","nickname":"테스터"}'
```

### 로그인

```bash
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"id":"test_user","password":"password123!"}'
-d '{"id":"testuser","password":"password123!"}'
```

응답은 공통 래퍼 형태이며, `data.accessToken`을 이후 요청에 사용합니다.
Expand Down Expand Up @@ -131,7 +131,7 @@ curl -X POST http://localhost:8080/api/return-routes/results/{resultId} \
curl -X POST http://localhost:8080/api/companions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer {accessToken}" \
-d '{"loginId":"friend_user"}'
-d '{"loginId":"frienduser"}'
```

자세한 요청/응답 스키마는 Swagger에서 확인할 수 있습니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ public AuthController(AuthService authService) {
description = """
아이디, 비밀번호, 닉네임으로 회원가입합니다.

- 아이디는 영문/숫자/밑줄 4~20자이며 중복될 수 없습니다.
- 비밀번호는 6~30자이며 BCrypt로 암호화되어 저장됩니다.
- 닉네임은 한글/영문/숫자 10자 이하입니다.
- 아이디는 소문자 영문/숫자 4~20자이며 중복될 수 없습니다.
- 비밀번호는 영문/숫자/특수문자를 모두 포함한 8~20자이며 BCrypt로 암호화되어 저장됩니다.
- 가입 직후 자동 로그인되지 않으므로, 이어서 로그인 API를 호출해 Access Token을 발급받아야 합니다.
"""
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

@Schema(description = "로그인 요청")
public record LoginRequest(
@Schema(description = "로그인 아이디", example = "sleepair_user")
@Schema(description = "로그인 아이디", example = "sleepair123")
@NotBlank(message = "아이디는 필수입니다.")
String id,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,25 @@

@Schema(description = "회원가입 요청")
public record SignupRequest(
@Schema(description = "로그인 아이디 (영문, 숫자, 밑줄만 사용, 4~20자, 중복 불가)", example = "sleepair_user")
@Schema(description = "로그인 아이디 (소문자 영문, 숫자만 사용, 4~20자, 중복 불가)", example = "sleepair123")
@NotBlank(message = "아이디는 필수입니다.")
@Size(min = 4, max = 20, message = "아이디는 4자 이상 20자 이하여야 합니다.")
@Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "아이디는 영문, 숫자, 밑줄만 사용할 수 있습니다.")
@Pattern(regexp = "^[a-z0-9]+$", message = "아이디는 소문자 영문과 숫자만 사용할 수 있습니다.")
String id,

@Schema(description = "비밀번호 (6~30자, 서버에는 BCrypt로 암호화되어 저장됨)", example = "password123!")
@Schema(description = "비밀번호 (영문, 숫자, 특수문자를 모두 포함한 8~20자, 서버에는 BCrypt로 암호화되어 저장됨)", example = "password123!")
@NotBlank(message = "비밀번호는 필수입니다.")
@Size(min = 6, max = 30, message = "비밀번호는 6자 이상 30자 이하여야 합니다.")
@Size(min = 8, max = 20, message = "비밀번호는 8자 이상 20자 이하여야 합니다.")
@Pattern(
regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?`~])[A-Za-z\\d!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?`~]+$",
message = "비밀번호는 영문, 숫자, 특수문자를 모두 포함해야 합니다."
)
String password,

@Schema(description = "닉네임 (동행자 카드 등에 노출, 30자 이하)", example = "채은")
@Schema(description = "닉네임 (한글, 영문, 숫자만 사용, 10자 이하)", example = "채은")
@NotBlank(message = "닉네임은 필수입니다.")
@Size(max = 30, message = "닉네임은 30자 이하여야 합니다.")
@Size(max = 10, message = "닉네임은 10자 이하여야 합니다.")
@Pattern(regexp = "^[가-힣a-zA-Z0-9]+$", message = "닉네임은 한글, 영문, 숫자만 사용할 수 있습니다.")
String nickname
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public record MemberResponse(
@Schema(description = "회원 고유 ID", example = "1")
Long memberId,

@Schema(description = "로그인 아이디", example = "sleepair_user")
@Schema(description = "로그인 아이디", example = "sleepair123")
String id,

@Schema(description = "닉네임", example = "채은")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.cotato.cokerthon.domain.auth.dto.request;

import static org.assertj.core.api.Assertions.assertThat;

import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import java.util.Set;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

class SignupRequestValidationTest {

private static ValidatorFactory validatorFactory;
private static Validator validator;

@BeforeAll
static void setUp() {
validatorFactory = Validation.buildDefaultValidatorFactory();
validator = validatorFactory.getValidator();
}

@AfterAll
static void tearDown() {
validatorFactory.close();
}

@Test
void 회원가입_요청은_화면_제약을_만족하면_유효하다() {
SignupRequest request = new SignupRequest("sleepair123", "password1!", "채은A1");

Set<ConstraintViolation<SignupRequest>> violations = validator.validate(request);

assertThat(violations).isEmpty();
}

@Test
void 닉네임은_필수이며_한글_영문_숫자_10자_이하여야_한다() {
assertThat(validate(new SignupRequest("sleepair123", "password1!", ""))).isNotEmpty();
assertThat(validate(new SignupRequest("sleepair123", "password1!", "abcdefghijk"))).isNotEmpty();
assertThat(validate(new SignupRequest("sleepair123", "password1!", "채은!"))).isNotEmpty();
}

@Test
void 아이디는_필수이며_소문자_영문과_숫자_4자_이상_20자_이하여야_한다() {
assertThat(validate(new SignupRequest("", "password1!", "채은"))).isNotEmpty();
assertThat(validate(new SignupRequest("abc", "password1!", "채은"))).isNotEmpty();
assertThat(validate(new SignupRequest("abcdefghijklmnopqrstu", "password1!", "채은"))).isNotEmpty();
assertThat(validate(new SignupRequest("Sleepair123", "password1!", "채은"))).isNotEmpty();
assertThat(validate(new SignupRequest("sleep_air", "password1!", "채은"))).isNotEmpty();
}

@Test
void 비밀번호는_필수이며_영문_숫자_특수문자를_포함한_8자_이상_20자_이하여야_한다() {
assertThat(validate(new SignupRequest("sleepair123", "", "채은"))).isNotEmpty();
assertThat(validate(new SignupRequest("sleepair123", "pass1!", "채은"))).isNotEmpty();
assertThat(validate(new SignupRequest("sleepair123", "passwordpassword123!!", "채은"))).isNotEmpty();
assertThat(validate(new SignupRequest("sleepair123", "password!", "채은"))).isNotEmpty();
assertThat(validate(new SignupRequest("sleepair123", "password1", "채은"))).isNotEmpty();
assertThat(validate(new SignupRequest("sleepair123", "12345678!", "채은"))).isNotEmpty();
}

private Set<ConstraintViolation<SignupRequest>> validate(SignupRequest request) {
return validator.validate(request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,25 +26,25 @@ class CompanionIntegrationTest {

@Test
void 아이디로_동행자를_검색한다() throws Exception {
signupAndLogin("comp_search_me", "password1!", "나");
signupAndLogin("comp_search_target", "password1!", "민주");
String accessToken = login("comp_search_me", "password1!");
signupAndLogin("compsearchme", "password1!", "나");
signupAndLogin("compsearchtarget", "password1!", "민주");
String accessToken = login("compsearchme", "password1!");

ResponseEntity<String> response = search(accessToken, "comp_search_target");
ResponseEntity<String> response = search(accessToken, "compsearchtarget");

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode data = objectMapper.readTree(response.getBody()).path("data");
assertThat(data.path("loginId").asText()).isEqualTo("comp_search_target");
assertThat(data.path("loginId").asText()).isEqualTo("compsearchtarget");
assertThat(data.path("nickname").asText()).isEqualTo("민주");
assertThat(data.path("alreadyCompanion").asBoolean()).isFalse();
}

@Test
void 동행자를_추가하면_즉시_목록에_나타난다() throws Exception {
String accessToken = signupAndLogin("comp_add_me", "password1!", "나");
signupAndLogin("comp_add_target", "password1!", "민지");
String accessToken = signupAndLogin("compaddme", "password1!", "나");
signupAndLogin("compaddtarget", "password1!", "민지");

ResponseEntity<String> addResponse = add(accessToken, "comp_add_target");
ResponseEntity<String> addResponse = add(accessToken, "compaddtarget");
assertThat(addResponse.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonNode addedData = objectMapper.readTree(addResponse.getBody()).path("data");
assertThat(addedData.path("nickname").asText()).isEqualTo("민지");
Expand All @@ -63,8 +63,8 @@ class CompanionIntegrationTest {

@Test
void 동행자의_수면시차_계산_기록이_있으면_카드에_함께_내려온다() throws Exception {
String myToken = signupAndLogin("comp_city_me", "password1!", "나");
String friendToken = signupAndLogin("comp_city_friend", "password1!", "민지");
String myToken = signupAndLogin("compcityme", "password1!", "나");
String friendToken = signupAndLogin("compcityfriend", "password1!", "민지");

// 기획안 예시와 동일한 입력 (03:00~10:00 / 23:00~07:00) → 3시간30분, WEST(gap=210: 뉴델리/콜롬보 tie)
String jetlagBody = """
Expand All @@ -80,7 +80,7 @@ class CompanionIntegrationTest {
jetlagHeaders.setBearerAuth(friendToken);
restTemplate.postForEntity("/api/sleep/jetlag", new HttpEntity<>(jetlagBody, jetlagHeaders), String.class);

add(myToken, "comp_city_friend");
add(myToken, "compcityfriend");

ResponseEntity<String> listResponse = getCompanions(myToken);
JsonNode companion = objectMapper.readTree(listResponse.getBody()).path("data").get(0);
Expand All @@ -95,30 +95,30 @@ class CompanionIntegrationTest {

@Test
void 자기_자신은_동행자로_추가할_수_없다() throws Exception {
String accessToken = signupAndLogin("comp_self", "password1!", "나");
String accessToken = signupAndLogin("compself", "password1!", "나");

ResponseEntity<String> response = add(accessToken, "comp_self");
ResponseEntity<String> response = add(accessToken, "compself");

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
}

@Test
void 같은_동행자를_중복으로_추가할_수_없다() throws Exception {
String accessToken = signupAndLogin("comp_dup_me", "password1!", "나");
signupAndLogin("comp_dup_target", "password1!", "민지");
String accessToken = signupAndLogin("compdupme", "password1!", "나");
signupAndLogin("compduptarget", "password1!", "민지");

add(accessToken, "comp_dup_target");
ResponseEntity<String> secondAdd = add(accessToken, "comp_dup_target");
add(accessToken, "compduptarget");
ResponseEntity<String> secondAdd = add(accessToken, "compduptarget");

assertThat(secondAdd.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
}

@Test
void 동행자를_삭제하면_목록에서_사라진다() throws Exception {
String accessToken = signupAndLogin("comp_del_me", "password1!", "나");
signupAndLogin("comp_del_target", "password1!", "민지");
String accessToken = signupAndLogin("compdelme", "password1!", "나");
signupAndLogin("compdeltarget", "password1!", "민지");

ResponseEntity<String> addResponse = add(accessToken, "comp_del_target");
ResponseEntity<String> addResponse = add(accessToken, "compdeltarget");
Long companionMemberId = objectMapper.readTree(addResponse.getBody())
.path("data").path("companionMemberId").asLong();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class MemberIntegrationTest {

@Test
void 수면시차_계산_전에는_서울이_기본_위치로_내려온다() throws Exception {
String accessToken = signupAndLogin("member_me_before", "password1!", "채은");
String accessToken = signupAndLogin("membermebefore", "password1!", "채은");

ResponseEntity<String> response = getMyInfo(accessToken);

Expand All @@ -43,7 +43,7 @@ class MemberIntegrationTest {

@Test
void 수면시차_계산_후에는_현재_위치의_위경도가_내려온다() throws Exception {
String accessToken = signupAndLogin("member_me_after", "password1!", "채은");
String accessToken = signupAndLogin("membermeafter", "password1!", "채은");

// 기획안 예시 (03:00~10:00 / 23:00~07:00) → WEST(gap=210: 뉴델리/콜롬보 tie)
String jetlagBody = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class SleepJetlagIntegrationTest {

@Test
void 목표보다_늦게_자는_경우_서쪽_방향_도시로_매칭된다() throws Exception {
String accessToken = signupAndLogin("jetlag_west", "password1!", "웨스트");
String accessToken = signupAndLogin("jetlagwest", "password1!", "웨스트");

// 기획안 예시: 현재 03:00~10:00, 목표 23:00~07:00 → 시차 3시간30분, WEST(gap=210: 뉴델리/콜롬보 tie)
String requestBody = """
Expand Down Expand Up @@ -58,7 +58,7 @@ class SleepJetlagIntegrationTest {

@Test
void 같은_시차_구간에_여러_도시가_있으면_랜덤으로_매칭된다() throws Exception {
String accessToken = signupAndLogin("jetlag_tie", "password1!", "타이");
String accessToken = signupAndLogin("jetlagtie", "password1!", "타이");

// 현재 01:00~09:00(중간 05:00) vs 목표 00:00~08:00(중간 04:00) → 시차 1시간, WEST
// 베이징/싱가포르/타이베이가 같은 시차 구간(gap=60)에 매핑되어 있어 반복 호출 시 셋 다 나와야 한다
Expand Down Expand Up @@ -87,7 +87,7 @@ class SleepJetlagIntegrationTest {

@Test
void 목표보다_일찍_자는_경우_동쪽_방향_도시로_매칭된다() throws Exception {
String accessToken = signupAndLogin("jetlag_east", "password1!", "이스트");
String accessToken = signupAndLogin("jetlageast", "password1!", "이스트");

// 현재 22:00~06:00(중간 02:00) vs 목표 24:00~08:00(중간 04:00) → 시차 2시간, EAST
String requestBody = """
Expand All @@ -110,7 +110,7 @@ class SleepJetlagIntegrationTest {

@Test
void 시차가_거의_없으면_서울로_매칭된다() throws Exception {
String accessToken = signupAndLogin("jetlag_same", "password1!", "세임");
String accessToken = signupAndLogin("jetlagsame", "password1!", "세임");

String requestBody = """
{
Expand Down
Loading