Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

workspace 정보조회 api 구현 #365

Merged
merged 1 commit into from
Dec 2, 2024
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
27 changes: 27 additions & 0 deletions apps/backend/src/workspace/dtos/getWorkspaceResponse.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { UserWorkspaceDto } from './userWorkspace.dto';

export class GetWorkspaceResponseDto {
@ApiProperty({
example: 'OO 생성에 성공했습니다.',
description: 'api 요청 결과 메시지',
})
@IsString()
message: string;

@ApiProperty({
example: [
{
workspaceId: 'snowflake-id-1',
title: 'naver-boostcamp-9th',
description: '네이버 부스트캠프 9기 워크스페이스입니다',
thumbnailUrl: 'https://example.com/image1.png',
role: 'guest',
visibility: 'private',
},
],
description: '사용자가 접근하려고 하는 워크스페이스 데이터',
})
workspace: UserWorkspaceDto;
}
2 changes: 1 addition & 1 deletion apps/backend/src/workspace/dtos/userWorkspace.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ export class UserWorkspaceDto {
title: string;
description: string | null;
thumbnailUrl: string | null;
role: 'owner' | 'guest';
role: 'owner' | 'guest' | null;
visibility: 'public' | 'private';
}
55 changes: 38 additions & 17 deletions apps/backend/src/workspace/workspace.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('WorkspaceController', () => {
getUserWorkspaces: jest.fn(),
generateInviteUrl: jest.fn(),
processInviteUrl: jest.fn(),
checkAccess: jest.fn(),
getWorkspaceData: jest.fn(),
updateVisibility: jest.fn(),
},
},
Expand Down Expand Up @@ -117,13 +117,15 @@ describe('WorkspaceController', () => {
description: 'Description 1',
thumbnailUrl: 'http://example.com/image1.png',
role: 'owner',
visibility: 'private',
},
{
workspaceId: 'snowflake-id-2',
title: 'Workspace 2',
description: null,
thumbnailUrl: null,
role: 'guest',
visibility: 'private',
},
] as UserWorkspaceDto[];

Expand Down Expand Up @@ -188,32 +190,45 @@ describe('WorkspaceController', () => {
});
});

describe('checkWorkspaceAccess', () => {
it('워크스페이스에 접근 가능한 경우 메시지를 반환한다.', async () => {
describe('getWorkspace', () => {
it('워크스페이스에 접근 가능한 경우 워크스페이스 정보를 반환한다.', async () => {
const workspaceId = 'workspace-snowflake-id';
const userId = 'user-snowflake-id';

jest.spyOn(service, 'checkAccess').mockResolvedValue(undefined);
const mockWorkspace = {
workspaceId: 'snowflake-id-1',
title: 'Workspace 1',
description: 'Description 1',
thumbnailUrl: 'http://example.com/image1.png',
role: 'owner',
visibility: 'public',
} as UserWorkspaceDto;

const result = await controller.checkWorkspaceAccess(workspaceId, userId);
jest.spyOn(service, 'getWorkspaceData').mockResolvedValue(mockWorkspace);

expect(service.checkAccess).toHaveBeenCalledWith(userId, workspaceId);
const result = await controller.getWorkspace(workspaceId, userId);

expect(service.getWorkspaceData).toHaveBeenCalledWith(
userId,
workspaceId,
);
expect(result).toEqual({
message: WorkspaceResponseMessage.WORKSPACE_ACCESS_CHECKED,
message: WorkspaceResponseMessage.WORKSPACE_DATA_RETURNED,
workspace: mockWorkspace,
});
});

it('로그인하지 않은 사용자의 경우 null로 처리하고 접근 가능한 경우 메시지를 반환한다.', async () => {
const workspaceId = 'workspace-snowflake-id';
const userId = 'null'; // 로그인되지 않은 상태를 나타냄

jest.spyOn(service, 'checkAccess').mockResolvedValue(undefined);
jest.spyOn(service, 'getWorkspaceData').mockResolvedValue(undefined);

const result = await controller.checkWorkspaceAccess(workspaceId, userId);
const result = await controller.getWorkspace(workspaceId, userId);

expect(service.checkAccess).toHaveBeenCalledWith(null, workspaceId);
expect(service.getWorkspaceData).toHaveBeenCalledWith(null, workspaceId);
expect(result).toEqual({
message: WorkspaceResponseMessage.WORKSPACE_ACCESS_CHECKED,
message: WorkspaceResponseMessage.WORKSPACE_DATA_RETURNED,
});
});

Expand All @@ -223,14 +238,17 @@ describe('WorkspaceController', () => {

// 권한 없음
jest
.spyOn(service, 'checkAccess')
.spyOn(service, 'getWorkspaceData')
.mockRejectedValue(new ForbiddenAccessException());

await expect(
controller.checkWorkspaceAccess(workspaceId, userId),
controller.getWorkspace(workspaceId, userId),
).rejects.toThrow(ForbiddenAccessException);

expect(service.checkAccess).toHaveBeenCalledWith(userId, workspaceId);
expect(service.getWorkspaceData).toHaveBeenCalledWith(
userId,
workspaceId,
);
});

it('워크스페이스가 존재하지 않는 경우 WorkspaceNotFoundException을 던진다.', async () => {
Expand All @@ -239,14 +257,17 @@ describe('WorkspaceController', () => {

// 워크스페이스 없음
jest
.spyOn(service, 'checkAccess')
.spyOn(service, 'getWorkspaceData')
.mockRejectedValue(new WorkspaceNotFoundException());

await expect(
controller.checkWorkspaceAccess(workspaceId, userId),
controller.getWorkspace(workspaceId, userId),
).rejects.toThrow(WorkspaceNotFoundException);

expect(service.checkAccess).toHaveBeenCalledWith(userId, workspaceId);
expect(service.getWorkspaceData).toHaveBeenCalledWith(
userId,
workspaceId,
);
});
});
});
15 changes: 10 additions & 5 deletions apps/backend/src/workspace/workspace.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ import { CreateWorkspaceDto } from './dtos/createWorkspace.dto';
import { CreateWorkspaceResponseDto } from './dtos/createWorkspaceResponse.dto';
import { GetUserWorkspacesResponseDto } from './dtos/getUserWorkspacesResponse.dto';
import { CreateWorkspaceInviteUrlDto } from './dtos/createWorkspaceInviteUrl.dto';
import { GetWorkspaceResponseDto } from './dtos/getWorkspaceResponse.dto';

export enum WorkspaceResponseMessage {
WORKSPACE_CREATED = '워크스페이스를 생성했습니다.',
WORKSPACE_DELETED = '워크스페이스를 삭제했습니다.',
WORKSPACES_RETURNED = '사용자가 참여하고 있는 모든 워크스페이스들을 가져왔습니다.',
WORKSPACE_INVITED = '워크스페이스 게스트 초대 링크가 생성되었습니다.',
WORKSPACE_JOINED = '워크스페이스에 게스트로 등록되었습니다.',
WORKSPACE_ACCESS_CHECKED = '워크스페이스에 대한 사용자의 접근 권한이 확인되었습니다.',
WORKSPACE_DATA_RETURNED = '워크스페이스에 대한 정보를 가져왔습니다.',
WORKSPACE_UPDATED_TO_PUBLIC = '워크스페이스가 공개로 설정되었습니다.',
WORKSPACE_UPDATED_TO_PRIVATE = '워크스페이스가 비공개로 설정되었습니다.',
}
Expand Down Expand Up @@ -134,25 +135,29 @@ export class WorkspaceController {
}

@ApiResponse({
type: MessageResponseDto,
type: GetWorkspaceResponseDto,
})
@ApiOperation({
summary: '워크스페이스에 대한 사용자의 권한을 확인합니다.',
})
@Get('/:workspaceId/:userId')
@HttpCode(HttpStatus.OK)
async checkWorkspaceAccess(
async getWorkspace(
@Param('workspaceId') workspaceId: string,
@Param('userId') userId: string, // 로그인되지 않은 경우 'null'
) {
// workspaceId, userId 둘 다 snowflakeId
// userId 'null'인 경우 => null로 처리
const checkedUserId = userId === 'null' ? null : userId;

await this.workspaceService.checkAccess(checkedUserId, workspaceId);
const workspaceData = await this.workspaceService.getWorkspaceData(
checkedUserId,
workspaceId,
);

return {
message: WorkspaceResponseMessage.WORKSPACE_ACCESS_CHECKED,
message: WorkspaceResponseMessage.WORKSPACE_DATA_RETURNED,
workspace: workspaceData,
};
}

Expand Down
103 changes: 77 additions & 26 deletions apps/backend/src/workspace/workspace.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { User } from '../user/user.entity';
import { TokenService } from '../auth/token/token.service';
import { ForbiddenAccessException } from '../exception/access.exception';
import { Snowflake } from '@theinternetfolks/snowflake';
import { UserWorkspaceDto } from './dtos/userWorkspace.dto';

describe('WorkspaceService', () => {
let service: WorkspaceService;
Expand Down Expand Up @@ -357,46 +358,96 @@ describe('WorkspaceService', () => {
});
});

describe('checkAccess', () => {
it('퍼블릭 워크스페이스는 접근을 허용한다.', async () => {
jest
.spyOn(workspaceRepository, 'findOne')
.mockResolvedValue({ visibility: 'public' } as Workspace);
describe('getWorkspaceData', () => {
it('퍼블릭 워크스페이스는 권한 체크 없이 데이터를 받는다.', async () => {
const workspace = {
id: 1,
snowflakeId: 'workspace-snowflake-id',
owner: { id: 1 } as User,
title: 'Test Workspace',
description: null,
visibility: 'public',
thumbnailUrl: null,
} as Workspace;

await expect(
service.checkAccess(null, 'workspace-snowflake-id'),
).resolves.toBeUndefined();
const workspaceDto = {
workspaceId: 'workspace-snowflake-id',
title: 'Test Workspace',
description: null,
thumbnailUrl: null,
role: null,
visibility: 'public',
} as UserWorkspaceDto;

jest.spyOn(workspaceRepository, 'findOne').mockResolvedValue(workspace);

const result = await service.getWorkspaceData(
null,
'workspace-snowflake-id',
);

expect(result).toEqual(workspaceDto);
});

it('프라이빗 워크스페이스는 권한이 없으면 예외를 던진다.', async () => {
jest
.spyOn(workspaceRepository, 'findOne')
.mockResolvedValue({ visibility: 'private' } as Workspace);
const workspace = {
id: 1,
snowflakeId: 'workspace-snowflake-id',
owner: { id: 2 } as User,
title: 'Test Workspace',
description: null,
visibility: 'private',
thumbnailUrl: null,
} as Workspace;

jest.spyOn(workspaceRepository, 'findOne').mockResolvedValue(workspace);
jest
.spyOn(userRepository, 'findOneBy')
.mockResolvedValue({ id: 1 } as User);
.mockResolvedValue({ id: 1, snowflakeId: 'user-snowflake-id' } as User);
jest.spyOn(roleRepository, 'findOne').mockResolvedValue(null);

await expect(
service.checkAccess('user-snowflake-id', 'workspace-snowflake-id'),
service.getWorkspaceData('user-snowflake-id', 'workspace-snowflake-id'),
).rejects.toThrow(ForbiddenAccessException);
});

it('프라이빗 워크스페이스는 권한이 있으면 접근을 허용한다.', async () => {
const userMock = { id: 1 };
const workspaceMock = { id: 1, visibility: 'private' };

jest
.spyOn(workspaceRepository, 'findOne')
.mockResolvedValue(workspaceMock as Workspace);
jest
.spyOn(userRepository, 'findOneBy')
.mockResolvedValue(userMock as User);
jest.spyOn(roleRepository, 'findOne').mockResolvedValue({} as Role);
const workspace = {
id: 1,
snowflakeId: 'workspace-snowflake-id',
owner: { id: 2 } as User,
title: 'Test Workspace',
description: null,
visibility: 'private',
thumbnailUrl: null,
} as Workspace;
const user = {
id: 1,
snowflakeId: 'user-snowflake-id',
} as User;
const role = {
workspace: workspace,
user: user,
role: 'guest',
} as Role;
const workspaceDto = {
workspaceId: 'workspace-snowflake-id',
title: 'Test Workspace',
description: null,
thumbnailUrl: null,
role: 'guest',
visibility: 'private',
} as UserWorkspaceDto;
jest.spyOn(workspaceRepository, 'findOne').mockResolvedValue(workspace);
jest.spyOn(userRepository, 'findOneBy').mockResolvedValue(user);
jest.spyOn(roleRepository, 'findOne').mockResolvedValue(role);

const result = await service.getWorkspaceData(
'user-snowflake-id',
'workspace-snowflake-id',
);

await expect(
service.checkAccess('user-snowflake-id', 'workspace-snowflake-id'),
).resolves.toBeUndefined();
expect(result).toEqual(workspaceDto);
});
});
});
28 changes: 23 additions & 5 deletions apps/backend/src/workspace/workspace.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,8 @@ export class WorkspaceService {
return userRoles.map((role) => ({
workspaceId: role.workspace.snowflakeId,
title: role.workspace.title,
description: role.workspace.description || null,
thumbnailUrl: role.workspace.thumbnailUrl || null,
description: role.workspace.description,
thumbnailUrl: role.workspace.thumbnailUrl,
role: role.role as 'owner' | 'guest',
visibility: role.workspace.visibility as 'public' | 'private',
}));
Expand Down Expand Up @@ -168,7 +168,10 @@ export class WorkspaceService {
});
}

async checkAccess(userId: string | null, workspaceId: string): Promise<void> {
async getWorkspaceData(
userId: string | null,
workspaceId: string,
): Promise<UserWorkspaceDto> {
// workspace가 존재하는지 확인
const workspace = await this.workspaceRepository.findOne({
where: { snowflakeId: workspaceId },
Expand All @@ -180,7 +183,14 @@ export class WorkspaceService {

// 퍼블릭 워크스페이스인 경우
if (workspace.visibility === 'public') {
return;
return {
workspaceId: workspace.snowflakeId,
title: workspace.title,
description: workspace.description,
thumbnailUrl: workspace.thumbnailUrl,
role: null,
visibility: 'public',
};
}

// 사용자 인증 필요
Expand All @@ -199,7 +209,15 @@ export class WorkspaceService {

// role이 존재하면 접근 허용
if (role) {
return;
// 각 워크스페이스와 역할 정보를 가공하여 반환
return {
workspaceId: workspace.snowflakeId,
title: workspace.title,
description: workspace.description,
thumbnailUrl: workspace.thumbnailUrl,
role: role.role as 'owner' | 'guest',
visibility: 'private',
};
}
}

Expand Down
Loading