Skip to content

Commit

Permalink
Merge pull request #564 from boostcampwm2023/develop
Browse files Browse the repository at this point in the history
[Release] v1.0 배포
  • Loading branch information
lsh23 authored Dec 11, 2023
2 parents a2970fd + 1824f27 commit 2478d90
Show file tree
Hide file tree
Showing 161 changed files with 3,143 additions and 534 deletions.
9 changes: 8 additions & 1 deletion .github/workflows/be.cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ jobs:
SWAGGER_VERSION=${{ secrets.SWAGGER_VERSION }}
SWAGGER_TAG=${{ secrets.SWAGGER_TAG }}
APPLE_PUBLIC_KEY_URL=${{ secrets.APPLE_PUBLIC_KEY_URL }}
APPLE_TOKEN_URL=${{ secrets.APPLE_TOKEN_URL }}
APPLE_REVOKE_URL=${{ secrets.APPLE_REVOKE_URL }}
APPLE_CLIENT_ID=${{ secrets.APPLE_CLIENT_ID }}
APPLE_KEY_ID=${{ secrets.APPLE_KEY_ID }}
APPLE_TEAM_ID=${{ secrets.APPLE_TEAM_ID }}
APPLE_AUD=${{ secrets.APPLE_AUD }}
APPLE_PRIVATE_KEY=${{ secrets.APPLE_PRIVATE_KEY }}
JWT_SECRET=${{ secrets.JWT_SECRET }}
JWT_VALIDITY=${{ secrets.JWT_VALIDITY }}
REFRESH_JWT_SECRET=${{ secrets.REFRESH_JWT_SECRET }}
Expand Down Expand Up @@ -107,4 +114,4 @@ jobs:
docker pull ${{ secrets.NCP_REGISTRY }}/motimate:latest
docker stop motimate
docker rm motimate
docker run -v /home/iOS02-moti/BE/.production.env:/usr/src/app/.production.env -d -p 3000:3000 --name motimate ${{ secrets.NCP_REGISTRY }}/motimate:latest
docker run -v /home/iOS02-moti/BE/.production.env:/usr/src/app/.production.env -e TZ=Asia/Seoul -d -p 3000:3000 --name motimate ${{ secrets.NCP_REGISTRY }}/motimate:latest
7 changes: 7 additions & 0 deletions .github/workflows/be.ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ jobs:
SWAGGER_VERSION=${{ secrets.SWAGGER_VERSION }}
SWAGGER_TAG=${{ secrets.SWAGGER_TAG }}
APPLE_PUBLIC_KEY_URL=${{ secrets.APPLE_PUBLIC_KEY_URL }}
APPLE_TOKEN_URL=${{ secrets.APPLE_TOKEN_URL }}
APPLE_REVOKE_URL=${{ secrets.APPLE_REVOKE_URL }}
APPLE_CLIENT_ID=${{ secrets.APPLE_CLIENT_ID }}
APPLE_KEY_ID=${{ secrets.APPLE_KEY_ID }}
APPLE_TEAM_ID=${{ secrets.APPLE_TEAM_ID }}
APPLE_AUD=${{ secrets.APPLE_AUD }}
APPLE_PRIVATE_KEY=${{ secrets.APPLE_PRIVATE_KEY }}
JWT_SECRET=${{ secrets.JWT_SECRET }}
JWT_VALIDITY=${{ secrets.JWT_VALIDITY }}
REFRESH_JWT_SECRET=${{ secrets.REFRESH_JWT_SECRET }}
Expand Down
4 changes: 3 additions & 1 deletion BE/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@

<p>모티 앱 v0.3 <a href="itms-services://?action=download-manifest&amp;url=https://kr.object.ncloudstorage.com/motimate-deploy/manifest-0.3.plist">Download</a></p>

<p>Last updated: 2023-12.07 23:59</p>
<p>모티 앱 v1.0 <a href="itms-services://?action=download-manifest&amp;url=https://kr.object.ncloudstorage.com/motimate-deploy/manifest-1.0.plist">Download</a></p>

<p>Last updated: 2023-12.11 10:00</p>

</body>
</html>
2 changes: 1 addition & 1 deletion BE/src/achievement/entities/achievement.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export class AchievementEntity extends BaseTimeEntity {
@PrimaryGeneratedColumn()
id: number;

@ManyToOne(() => UserEntity)
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id' })
user: UserEntity;

Expand Down
26 changes: 25 additions & 1 deletion BE/src/achievement/entities/achievement.repository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { CategoryTestModule } from '../../../test/category/category-test.module'
import { Achievement } from '../domain/achievement.domain';
import { PaginateAchievementRequest } from '../dto/paginate-achievement-request';
import { ImageFixture } from '../../../test/image/image-fixture';
import { Category } from '../../category/domain/category.domain';

describe('AchievementRepository test', () => {
let achievementRepository: AchievementRepository;
Expand Down Expand Up @@ -300,6 +299,31 @@ describe('AchievementRepository test', () => {
});
});

test('자신의 미설정 카테고리의 도전기록을 조회할 수 있다.', async () => {
await transactionTest(dataSource, async () => {
// given
const otherUser1 = await usersFixture.getUser('BCD');
const otherUser2 = await usersFixture.getUser('BCD');
await achievementFixture.getAchievements(10, otherUser1, null);
await achievementFixture.getAchievements(10, otherUser2, null);

const user = await usersFixture.getUser('ABC');

const achievements: Achievement[] =
await achievementFixture.getAchievements(10, user, null);

// when
const achievementDetail =
await achievementRepository.findAchievementDetail(
user.id,
achievements[9].id,
);

// then
expect(achievementDetail.category.achieveCount).toEqual(10);
});
});

test('유효하지 않은 달성 기록 id를 통해 조회하면 null을 반환한다.', async () => {
await transactionTest(dataSource, async () => {
// given
Expand Down
5 changes: 1 addition & 4 deletions BE/src/achievement/entities/achievement.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import { FindOptionsWhere, IsNull, LessThan } from 'typeorm';
import { PaginateAchievementRequest } from '../dto/paginate-achievement-request';
import { AchievementDetailResponse } from '../dto/achievement-detail-response';
import { IAchievementDetail } from '../index';
import { User } from '../../users/domain/user.domain';
import { ICategoryMetaData } from '../../category';
import { CategoryMetaData } from '../../category/dto/category-metadata';

@CustomRepository(AchievementEntity)
export class AchievementRepository extends TransactionalRepository<AchievementEntity> {
Expand Down Expand Up @@ -62,7 +59,7 @@ export class AchievementRepository extends TransactionalRepository<AchievementEn
.leftJoin(
'achievement',
'a',
'COALESCE(a.category_id, -1) = COALESCE(achievement.category_id, -1) AND a.id <= achievement.id',
'COALESCE(a.category_id, -1) = COALESCE(achievement.category_id, -1) AND a.id <= achievement.id and a.user_id = achievement.user_id',
)
.leftJoin('image', 'i', 'i.achievement_id = achievement.id')
.where('achievement.id = :achievementId', { achievementId })
Expand Down
2 changes: 1 addition & 1 deletion BE/src/admin/entities/admin.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class AdminEntity {
@PrimaryColumn()
userId: number;

@ManyToOne(() => UserEntity)
@ManyToOne(() => UserEntity, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'user_id', referencedColumnName: 'id' })
user: UserEntity;

Expand Down
206 changes: 152 additions & 54 deletions BE/src/auth/application/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,21 @@ import { RefreshTokenNotFoundException } from '../exception/refresh-token-not-fo
import { AuthTestModule } from '../../../test/auth/auth-test.module';
import { anyString, instance, mock, when } from 'ts-mockito';
import { AvatarHolder } from './avatar.holder';
import { RevokeAppleAuthRequest } from '../dto/revoke-apple-auth-request.dto';
import { UsersFixture } from '../../../test/user/users-fixture';
import { UsersTestModule } from '../../../test/user/users-test.module';
import { InvalidIdentifierException } from '../exception/invalid-identifier.exception';
import { RevokeRequestFailException } from '../exception/revoke-request-fail.exception';
import { transactionTest } from '../../../test/common/transaction-test';
import { DataSource } from 'typeorm';

describe('AuthService', () => {
let authService: AuthService;
let mockOauthHandler: OauthHandler;
let refreshTokenStore: Cache;
let userFixture: UsersFixture;
let userRepository: UserRepository;
let dataSource: DataSource;

beforeAll(async () => {
mockOauthHandler = mock<OauthHandler>(OauthHandler);
Expand All @@ -35,6 +45,7 @@ describe('AuthService', () => {
HttpModule,
CacheModule.register(),
AuthTestModule,
UsersTestModule,
JwtModule.register({}),
CustomTypeOrmModule.forCustomRepository([UserRepository]),
ConfigModule.forRoot(configServiceModuleOptions),
Expand All @@ -54,74 +65,161 @@ describe('AuthService', () => {
.compile();

authService = module.get<AuthService>(AuthService);
userRepository = module.get<UserRepository>(UserRepository);
userFixture = module.get<UsersFixture>(UsersFixture);
refreshTokenStore = module.get<Cache>(CACHE_MANAGER);
dataSource = module.get<DataSource>(DataSource);
});

afterAll(async () => {
await dataSource.destroy();
});

test('authService가 정의되어 있어야 한다.', () => {
expect(authService).toBeDefined();
});

test('apple login을 처리하고 자체 accessToken, refreshToken을 발급한다.', async () => {
// given
const appleLoginRequest = new AppleLoginRequest(
'eyJraWQiOiJmaDZCczhDIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1vdGltYXRlLm1vdGkiLCJleHAiOjE2OTk2OTkxMzIsImlhdCI6MTY5OTYxMjczMiwic3ViIjoiMDAwODQ4LjEyNTU5ZWUxNTkyYjQ0YWY5NzA1ZmRhYmYyOGFlMzhiLjEwMjQiLCJjX2hhc2giOiJxSkd3ZEhyNEZYb055Qllobm5vQ21RIiwiYXV0aF90aW1lIjoxNjk5NjEyNzMyLCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.dGi4dKG9ezLUu0Zm51sgZaORtQwlbsLj8kMqrIb9wZ52pLeT7-SwFbo1rA-ATZh5PEXS-VGpw6fz7AyTzzYWzxWxvJ8oEDOJE8fHI5JMJiYLHjujim565RT7t36zKWwhWDS1pOn9eQc-ivIEmfSeslzOgre8HucYWfoHu1bsiyWbP1mp3e6Tu_RfR41KUD_E0oOFVw-sDHHkUejjLu2ZjyFYQ75AvivpArfOabsF3D1kl-ONtP2-ImRxLMgFZ9D5y9_8SCtP57kTgFFUm_07ik6srgcg2sn5qhKzQOxOsdHG46pI7XUHuX8N5XoU0cYbA-HefDHCPVfy4N8ikUcQkQ',
);
when(mockOauthHandler.getUserIdentifier(anyString())).thenResolve(
'123456.12559ee1592b44af9705fdabf28ae38b.1234',
);

// when
const response = await authService.appleLogin(appleLoginRequest);

// then
const refreshTokenFromStore = await refreshTokenStore.get(
response.user.userCode,
);
expect(response.user).toBeDefined();
expect(response.user.userCode).toBeDefined();
expect(response.user.avatarUrl).toBeDefined();
expect(response.accessToken).toBeDefined();
expect(response.refreshToken).toBeDefined();
expect(response.refreshToken).toBeDefined();
expect(refreshTokenFromStore).toBeDefined();
await transactionTest(dataSource, async () => {
// given
const appleLoginRequest = new AppleLoginRequest(
'eyJraWQiOiJmaDZCczhDIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1vdGltYXRlLm1vdGkiLCJleHAiOjE2OTk2OTkxMzIsImlhdCI6MTY5OTYxMjczMiwic3ViIjoiMDAwODQ4LjEyNTU5ZWUxNTkyYjQ0YWY5NzA1ZmRhYmYyOGFlMzhiLjEwMjQiLCJjX2hhc2giOiJxSkd3ZEhyNEZYb055Qllobm5vQ21RIiwiYXV0aF90aW1lIjoxNjk5NjEyNzMyLCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.dGi4dKG9ezLUu0Zm51sgZaORtQwlbsLj8kMqrIb9wZ52pLeT7-SwFbo1rA-ATZh5PEXS-VGpw6fz7AyTzzYWzxWxvJ8oEDOJE8fHI5JMJiYLHjujim565RT7t36zKWwhWDS1pOn9eQc-ivIEmfSeslzOgre8HucYWfoHu1bsiyWbP1mp3e6Tu_RfR41KUD_E0oOFVw-sDHHkUejjLu2ZjyFYQ75AvivpArfOabsF3D1kl-ONtP2-ImRxLMgFZ9D5y9_8SCtP57kTgFFUm_07ik6srgcg2sn5qhKzQOxOsdHG46pI7XUHuX8N5XoU0cYbA-HefDHCPVfy4N8ikUcQkQ',
);
when(mockOauthHandler.getUserIdentifier(anyString())).thenResolve(
'123456.12559ee1592b44af9705fdabf28ae38b.1234',
);

// when
const response = await authService.appleLogin(appleLoginRequest);

// then
const refreshTokenFromStore = await refreshTokenStore.get(
response.user.userCode,
);
expect(response.user).toBeDefined();
expect(response.user.userCode).toBeDefined();
expect(response.user.avatarUrl).toBeDefined();
expect(response.accessToken).toBeDefined();
expect(response.refreshToken).toBeDefined();
expect(response.refreshToken).toBeDefined();
expect(refreshTokenFromStore).toBeDefined();
});
});

test('refreshToken을 통해 새로운 accessToken을 발급한다.', async () => {
// given
const appleLoginRequest = new AppleLoginRequest(
'eyJraWQiOiJmaDZCczhDIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1vdGltYXRlLm1vdGkiLCJleHAiOjE2OTk2OTkxMzIsImlhdCI6MTY5OTYxMjczMiwic3ViIjoiMDAwODQ4LjEyNTU5ZWUxNTkyYjQ0YWY5NzA1ZmRhYmYyOGFlMzhiLjEwMjQiLCJjX2hhc2giOiJxSkd3ZEhyNEZYb055Qllobm5vQ21RIiwiYXV0aF90aW1lIjoxNjk5NjEyNzMyLCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.dGi4dKG9ezLUu0Zm51sgZaORtQwlbsLj8kMqrIb9wZ52pLeT7-SwFbo1rA-ATZh5PEXS-VGpw6fz7AyTzzYWzxWxvJ8oEDOJE8fHI5JMJiYLHjujim565RT7t36zKWwhWDS1pOn9eQc-ivIEmfSeslzOgre8HucYWfoHu1bsiyWbP1mp3e6Tu_RfR41KUD_E0oOFVw-sDHHkUejjLu2ZjyFYQ75AvivpArfOabsF3D1kl-ONtP2-ImRxLMgFZ9D5y9_8SCtP57kTgFFUm_07ik6srgcg2sn5qhKzQOxOsdHG46pI7XUHuX8N5XoU0cYbA-HefDHCPVfy4N8ikUcQkQ',
);
when(mockOauthHandler.getUserIdentifier(anyString())).thenResolve(
'123456.12559ee1592b44af9705fdabf28ae38b.1234',
);

const loginResponse = await authService.appleLogin(appleLoginRequest);
const user = User.from('123456.12559ee1592b44af9705fdabf28ae38b.1234');
user.assignUserCode(loginResponse.user.userCode);

// // when
const response = await authService.refresh(
user,
new RefreshAuthRequestDto(loginResponse.refreshToken),
);

// then
expect(response.user).toBeDefined();
expect(response.accessToken).toBeDefined();
await transactionTest(dataSource, async () => {
// given
const appleLoginRequest = new AppleLoginRequest(
'eyJraWQiOiJmaDZCczhDIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLm1vdGltYXRlLm1vdGkiLCJleHAiOjE2OTk2OTkxMzIsImlhdCI6MTY5OTYxMjczMiwic3ViIjoiMDAwODQ4LjEyNTU5ZWUxNTkyYjQ0YWY5NzA1ZmRhYmYyOGFlMzhiLjEwMjQiLCJjX2hhc2giOiJxSkd3ZEhyNEZYb055Qllobm5vQ21RIiwiYXV0aF90aW1lIjoxNjk5NjEyNzMyLCJub25jZV9zdXBwb3J0ZWQiOnRydWV9.dGi4dKG9ezLUu0Zm51sgZaORtQwlbsLj8kMqrIb9wZ52pLeT7-SwFbo1rA-ATZh5PEXS-VGpw6fz7AyTzzYWzxWxvJ8oEDOJE8fHI5JMJiYLHjujim565RT7t36zKWwhWDS1pOn9eQc-ivIEmfSeslzOgre8HucYWfoHu1bsiyWbP1mp3e6Tu_RfR41KUD_E0oOFVw-sDHHkUejjLu2ZjyFYQ75AvivpArfOabsF3D1kl-ONtP2-ImRxLMgFZ9D5y9_8SCtP57kTgFFUm_07ik6srgcg2sn5qhKzQOxOsdHG46pI7XUHuX8N5XoU0cYbA-HefDHCPVfy4N8ikUcQkQ',
);
when(mockOauthHandler.getUserIdentifier(anyString())).thenResolve(
'123456.12559ee1592b44af9705fdabf28ae38b.1234',
);

const loginResponse = await authService.appleLogin(appleLoginRequest);
const user = User.from('123456.12559ee1592b44af9705fdabf28ae38b.1234');
user.assignUserCode(loginResponse.user.userCode);

// // when
const response = await authService.refresh(
user,
new RefreshAuthRequestDto(loginResponse.refreshToken),
);

// then
expect(response.user).toBeDefined();
expect(response.accessToken).toBeDefined();
});
});

test('유효하지 않은 refreshToken에 대한 요청에 대해서 RefreshTokenNotFoundException를 던진다.', async () => {
// given
const user = User.from('123456.12559ee1592b44af9705fdabf28ae38b.1234');
user.assignUserCode('ABCDEF1');

// when
// then
await expect(
authService.refresh(
await transactionTest(dataSource, async () => {
// given
const user = User.from('123456.12559ee1592b44af9705fdabf28ae38b.1234');
user.assignUserCode('ABCDEF1');

// when
// then
await expect(
authService.refresh(
user,
new RefreshAuthRequestDto('INVALID_REFRESH_TOKEN'),
),
).rejects.toThrow(RefreshTokenNotFoundException);
});
});

test('apple login에 대해 revoke 처리하고 유저를 삭제한다.', async () => {
await transactionTest(dataSource, async () => {
// given
const user = await userFixture.getUser(1);

when(mockOauthHandler.getUserIdentifier(anyString())).thenResolve(
user.userIdentifier,
);
when(mockOauthHandler.revokeUser(anyString())).thenResolve({
status: 200,
statusText: 'OK',
data: null,
headers: null,
config: null,
});

// when
const revokeAppleAuthResponse = await authService.revoke(
user,
new RefreshAuthRequestDto('INVALID_REFRESH_TOKEN'),
),
).rejects.toThrow(RefreshTokenNotFoundException);
new RevokeAppleAuthRequest(user.userIdentifier, 'authorizationCode'),
);

//then
const removed = await userRepository.findOneByUserCode(user.userCode);
expect(removed).toBeUndefined();
expect(revokeAppleAuthResponse.userCode).toEqual(user.userCode);
});
});

test('유효하지 않은 userIdentifier 요청에는 InvalidIdentifierException를 던진다.', async () => {
await transactionTest(dataSource, async () => {
// given
const user = await userFixture.getUser(1);
when(mockOauthHandler.getUserIdentifier(anyString())).thenResolve(
'unmatched',
);
// when
//then
await expect(
authService.revoke(
user,
new RevokeAppleAuthRequest(user.userIdentifier, 'authorizationCode'),
),
).rejects.toThrow(InvalidIdentifierException);
});
});

test('애플 서버로부터 revoke 실패 응답을 받는 경우에는 RevokeRequestFailException 던진다.', async () => {
await transactionTest(dataSource, async () => {
// given
const user = await userFixture.getUser(1);

when(mockOauthHandler.getUserIdentifier(anyString())).thenResolve(
user.userIdentifier,
);
when(mockOauthHandler.revokeUser(anyString())).thenResolve({
status: 500,
statusText: 'Internal Server Error',
data: null,
headers: null,
config: null,
});

// when
//then
await expect(
authService.revoke(
user,
new RevokeAppleAuthRequest(user.userIdentifier, 'authorizationCode'),
),
).rejects.toThrow(RevokeRequestFailException);
});
});
});
Loading

0 comments on commit 2478d90

Please sign in to comment.