Skip to content

Commit 36b0d5d

Browse files
test: 워크스페이스, 권한 테스트 코드 마무리
1 parent 5fcbec8 commit 36b0d5d

File tree

4 files changed

+219
-7
lines changed

4 files changed

+219
-7
lines changed

apps/backend/src/workspace/workspace.controller.spec.ts

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { WorkspaceResponseMessage } from './workspace.controller';
77
import { NotWorkspaceOwnerException } from '../exception/workspace-auth.exception';
88
import { UserWorkspaceDto } from './dtos/userWorkspace.dto';
99
import { TokenService } from '../auth/token/token.service';
10+
import { WorkspaceNotFoundException } from '../exception/workspace.exception';
11+
import { ForbiddenAccessException } from '../exception/access.exception';
1012

1113
describe('WorkspaceController', () => {
1214
let controller: WorkspaceController;
@@ -22,6 +24,9 @@ describe('WorkspaceController', () => {
2224
createWorkspace: jest.fn(),
2325
deleteWorkspace: jest.fn(),
2426
getUserWorkspaces: jest.fn(),
27+
generateInviteUrl: jest.fn(),
28+
processInviteUrl: jest.fn(),
29+
checkAccess: jest.fn(),
2530
},
2631
},
2732
{
@@ -168,11 +173,11 @@ describe('WorkspaceController', () => {
168173
const req = { user: { sub: 1 } };
169174
const token = 'valid-token';
170175

171-
jest.spyOn(service, 'processInviteToken').mockResolvedValue();
176+
jest.spyOn(service, 'processInviteUrl').mockResolvedValue();
172177

173178
const result = await controller.joinWorkspace(req, token);
174179

175-
expect(service.processInviteToken).toHaveBeenCalledWith(
180+
expect(service.processInviteUrl).toHaveBeenCalledWith(
176181
req.user.sub,
177182
token,
178183
);
@@ -181,4 +186,66 @@ describe('WorkspaceController', () => {
181186
});
182187
});
183188
});
189+
190+
describe('checkWorkspaceAccess', () => {
191+
it('워크스페이스에 접근 가능한 경우 메시지를 반환한다.', async () => {
192+
const workspaceId = 'workspace-snowflake-id';
193+
const userId = 'user-snowflake-id';
194+
195+
jest.spyOn(service, 'checkAccess').mockResolvedValue(undefined);
196+
197+
const result = await controller.checkWorkspaceAccess(workspaceId, userId);
198+
199+
expect(service.checkAccess).toHaveBeenCalledWith(userId, workspaceId);
200+
expect(result).toEqual({
201+
message: WorkspaceResponseMessage.WORKSPACE_ACCESS_CHECKED,
202+
});
203+
});
204+
205+
it('로그인하지 않은 사용자의 경우 null로 처리하고 접근 가능한 경우 메시지를 반환한다.', async () => {
206+
const workspaceId = 'workspace-snowflake-id';
207+
const userId = 'null'; // 로그인되지 않은 상태를 나타냄
208+
209+
jest.spyOn(service, 'checkAccess').mockResolvedValue(undefined);
210+
211+
const result = await controller.checkWorkspaceAccess(workspaceId, userId);
212+
213+
expect(service.checkAccess).toHaveBeenCalledWith(null, workspaceId);
214+
expect(result).toEqual({
215+
message: WorkspaceResponseMessage.WORKSPACE_ACCESS_CHECKED,
216+
});
217+
});
218+
219+
it('권한이 없는 경우 ForbiddenAccessException을 던진다.', async () => {
220+
const workspaceId = 'workspace-snowflake-id';
221+
const userId = 'user-snowflake-id';
222+
223+
// 권한 없음
224+
jest
225+
.spyOn(service, 'checkAccess')
226+
.mockRejectedValue(new ForbiddenAccessException());
227+
228+
await expect(
229+
controller.checkWorkspaceAccess(workspaceId, userId),
230+
).rejects.toThrow(ForbiddenAccessException);
231+
232+
expect(service.checkAccess).toHaveBeenCalledWith(userId, workspaceId);
233+
});
234+
235+
it('워크스페이스가 존재하지 않는 경우 WorkspaceNotFoundException을 던진다.', async () => {
236+
const workspaceId = 'invalid-snowflake-id';
237+
const userId = 'user-snowflake-id';
238+
239+
// 워크스페이스 없음
240+
jest
241+
.spyOn(service, 'checkAccess')
242+
.mockRejectedValue(new WorkspaceNotFoundException());
243+
244+
await expect(
245+
controller.checkWorkspaceAccess(workspaceId, userId),
246+
).rejects.toThrow(WorkspaceNotFoundException);
247+
248+
expect(service.checkAccess).toHaveBeenCalledWith(userId, workspaceId);
249+
});
250+
});
184251
});

apps/backend/src/workspace/workspace.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ export class WorkspaceController {
125125
@HttpCode(HttpStatus.OK)
126126
async joinWorkspace(@Request() req, @Query('token') token: string) {
127127
const userId = req.user.sub; // 인증된 사용자 ID
128-
await this.workspaceService.processInviteToken(userId, token);
128+
await this.workspaceService.processInviteUrl(userId, token);
129129

130130
return { message: WorkspaceResponseMessage.WORKSPACE_INVITED };
131131
}

apps/backend/src/workspace/workspace.service.spec.ts

Lines changed: 148 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,27 @@ import { CreateWorkspaceDto } from './dtos/createWorkspace.dto';
1010
import { Workspace } from './workspace.entity';
1111
import { Role } from '../role/role.entity';
1212
import { User } from '../user/user.entity';
13-
import { TokenModule } from '../auth/token/token.module';
13+
import { TokenService } from '../auth/token/token.service';
14+
import { ForbiddenAccessException } from '../exception/access.exception';
1415

1516
describe('WorkspaceService', () => {
1617
let service: WorkspaceService;
1718
let workspaceRepository: WorkspaceRepository;
1819
let userRepository: UserRepository;
1920
let roleRepository: RoleRepository;
21+
let tokenService: TokenService;
2022

2123
beforeEach(async () => {
2224
const module: TestingModule = await Test.createTestingModule({
23-
imports: [TokenModule],
2425
providers: [
2526
WorkspaceService,
2627
{
2728
provide: WorkspaceRepository,
2829
useValue: {
29-
save: jest.fn(),
3030
findOneBy: jest.fn(),
31+
findOne: jest.fn(),
3132
delete: jest.fn(),
33+
save: jest.fn(),
3234
},
3335
},
3436
{
@@ -41,17 +43,26 @@ describe('WorkspaceService', () => {
4143
provide: RoleRepository,
4244
useValue: {
4345
findOneBy: jest.fn(),
46+
findOne: jest.fn(),
4447
find: jest.fn(),
4548
save: jest.fn(),
4649
},
4750
},
51+
{
52+
provide: TokenService,
53+
useValue: {
54+
generateInviteToken: jest.fn(),
55+
verifyInviteToken: jest.fn(),
56+
},
57+
},
4858
],
4959
}).compile();
5060

5161
service = module.get<WorkspaceService>(WorkspaceService);
5262
workspaceRepository = module.get<WorkspaceRepository>(WorkspaceRepository);
5363
userRepository = module.get<UserRepository>(UserRepository);
5464
roleRepository = module.get<RoleRepository>(RoleRepository);
65+
tokenService = module.get<TokenService>(TokenService);
5566
});
5667

5768
it('서비스 클래스가 정상적으로 인스턴스화된다.', () => {
@@ -248,4 +259,138 @@ describe('WorkspaceService', () => {
248259
});
249260
});
250261
});
262+
263+
describe('generateInviteUrl', () => {
264+
it('정상적으로 초대 링크를 생성한다.', async () => {
265+
const userId = 1;
266+
const workspaceId = 'workspace-snowflake-id';
267+
const workspaceMock = { id: 1 } as Workspace;
268+
const tokenMock = 'invite-token';
269+
270+
jest
271+
.spyOn(workspaceRepository, 'findOneBy')
272+
.mockResolvedValue(workspaceMock);
273+
jest
274+
.spyOn(roleRepository, 'findOneBy')
275+
.mockResolvedValue({ role: 'owner' } as Role);
276+
jest
277+
.spyOn(tokenService, 'generateInviteToken')
278+
.mockReturnValue(tokenMock);
279+
280+
const result = await service.generateInviteUrl(userId, workspaceId);
281+
282+
expect(workspaceRepository.findOneBy).toHaveBeenCalledWith({
283+
snowflakeId: workspaceId,
284+
});
285+
expect(roleRepository.findOneBy).toHaveBeenCalledWith({
286+
userId,
287+
workspaceId: workspaceMock.id,
288+
role: 'owner',
289+
});
290+
expect(result).toEqual(
291+
`https://octodocs.local/api/workspace/join?token=${tokenMock}`,
292+
);
293+
});
294+
295+
it('워크스페이스가 존재하지 않으면 예외를 던진다.', async () => {
296+
jest.spyOn(workspaceRepository, 'findOneBy').mockResolvedValue(null);
297+
298+
await expect(
299+
service.generateInviteUrl(1, 'invalid-workspace-id'),
300+
).rejects.toThrow(WorkspaceNotFoundException);
301+
});
302+
303+
it('소유자가 아닌 사용자가 초대 링크를 생성하려고 하면 예외를 던진다.', async () => {
304+
const workspaceMock = { id: 1 } as Workspace;
305+
306+
jest
307+
.spyOn(workspaceRepository, 'findOneBy')
308+
.mockResolvedValue(workspaceMock);
309+
jest.spyOn(roleRepository, 'findOneBy').mockResolvedValue(null);
310+
311+
await expect(
312+
service.generateInviteUrl(1, 'workspace-snowflake-id'),
313+
).rejects.toThrow(NotWorkspaceOwnerException);
314+
});
315+
});
316+
317+
describe('processInviteUrl', () => {
318+
it('정상적으로 초대 링크를 처리한다.', async () => {
319+
const userId = 1;
320+
const token = 'invite-token';
321+
const decodedToken = { workspaceId: '1', role: 'guest' };
322+
323+
jest
324+
.spyOn(tokenService, 'verifyInviteToken')
325+
.mockReturnValue(decodedToken);
326+
jest.spyOn(roleRepository, 'findOneBy').mockResolvedValue(null);
327+
328+
await service.processInviteUrl(userId, token);
329+
330+
expect(tokenService.verifyInviteToken).toHaveBeenCalledWith(token);
331+
expect(roleRepository.save).toHaveBeenCalledWith({
332+
workspaceId: 1,
333+
userId,
334+
role: 'guest',
335+
});
336+
});
337+
338+
it('이미 워크스페이스에 등록된 사용자는 예외를 던진다.', async () => {
339+
const userId = 1;
340+
const token = 'invite-token';
341+
const decodedToken = { workspaceId: '1', role: 'guest' };
342+
343+
jest
344+
.spyOn(tokenService, 'verifyInviteToken')
345+
.mockReturnValue(decodedToken);
346+
jest.spyOn(roleRepository, 'findOneBy').mockResolvedValue({} as Role);
347+
348+
await expect(service.processInviteUrl(userId, token)).rejects.toThrow(
349+
Error,
350+
);
351+
});
352+
});
353+
354+
describe('checkAccess', () => {
355+
it('퍼블릭 워크스페이스는 접근을 허용한다.', async () => {
356+
jest
357+
.spyOn(workspaceRepository, 'findOne')
358+
.mockResolvedValue({ visibility: 'public' } as Workspace);
359+
360+
await expect(
361+
service.checkAccess(null, 'workspace-snowflake-id'),
362+
).resolves.toBeUndefined();
363+
});
364+
365+
it('프라이빗 워크스페이스는 권한이 없으면 예외를 던진다.', async () => {
366+
jest
367+
.spyOn(workspaceRepository, 'findOne')
368+
.mockResolvedValue({ visibility: 'private' } as Workspace);
369+
jest
370+
.spyOn(userRepository, 'findOneBy')
371+
.mockResolvedValue({ id: 1 } as User);
372+
jest.spyOn(roleRepository, 'findOne').mockResolvedValue(null);
373+
374+
await expect(
375+
service.checkAccess('user-snowflake-id', 'workspace-snowflake-id'),
376+
).rejects.toThrow(ForbiddenAccessException);
377+
});
378+
379+
it('프라이빗 워크스페이스는 권한이 있으면 접근을 허용한다.', async () => {
380+
const userMock = { id: 1 };
381+
const workspaceMock = { id: 1, visibility: 'private' };
382+
383+
jest
384+
.spyOn(workspaceRepository, 'findOne')
385+
.mockResolvedValue(workspaceMock as Workspace);
386+
jest
387+
.spyOn(userRepository, 'findOneBy')
388+
.mockResolvedValue(userMock as User);
389+
jest.spyOn(roleRepository, 'findOne').mockResolvedValue({} as Role);
390+
391+
await expect(
392+
service.checkAccess('user-snowflake-id', 'workspace-snowflake-id'),
393+
).resolves.toBeUndefined();
394+
});
395+
});
251396
});

apps/backend/src/workspace/workspace.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export class WorkspaceService {
123123
return `https://octodocs.local/api/workspace/join?token=${token}`;
124124
}
125125

126-
async processInviteToken(userId: number, token: string): Promise<void> {
126+
async processInviteUrl(userId: number, token: string): Promise<void> {
127127
// 토큰 검증 및 디코딩
128128
const decodedToken = this.tokenService.verifyInviteToken(token);
129129
const { workspaceId, role } = decodedToken;

0 commit comments

Comments
 (0)