Skip to content

Commit

Permalink
Endpoint for updating custom projects (#208)
Browse files Browse the repository at this point in the history
  • Loading branch information
catalin-oancea authored Jan 7, 2025
1 parent 64b8326 commit df15b8b
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 2 deletions.
35 changes: 34 additions & 1 deletion api/src/modules/custom-projects/custom-projects.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { AuthGuard } from '@nestjs/passport';
import { RolesGuard } from '@api/modules/auth/guards/roles.guard';
import { RequiredRoles } from '@api/modules/auth/decorators/roles.decorator';
import { ROLES } from '@shared/entities/users/roles.enum';
import { CustomProject } from '@shared/entities/custom-project.entity';

@Controller()
export class CustomProjectsController {
Expand Down Expand Up @@ -145,6 +146,35 @@ export class CustomProjectsController {
);
}

@UseGuards(AuthGuard('jwt'), RolesGuard)
@RequiredRoles(ROLES.PARTNER, ROLES.ADMIN)
@TsRestHandler(customProjectContract.updateCustomProject)
async updateCustomProject(
@GetUser() user: User,
@Body(new ValidationPipe({ enableDebugMessages: true, transform: true }))
dto: Partial<CustomProject>,
): Promise<ControllerResponse> {
return tsRestHandler(
customProjectContract.updateCustomProject,
async ({ params: { id } }) => {
if (
!(await this.customProjects.areProjectsCreatedByUser(user.id, [id]))
) {
return {
status: 401,
body: null,
};
}

const updatedEntity = await this.customProjects.update(id, dto);
return {
status: 200,
body: updatedEntity,
};
},
);
}

@UseGuards(AuthGuard('jwt'), RolesGuard)
@RequiredRoles(ROLES.PARTNER, ROLES.ADMIN)
@TsRestHandler(customProjectContract.deleteCustomProjects)
Expand All @@ -156,7 +186,10 @@ export class CustomProjectsController {
customProjectContract.deleteCustomProjects,
async () => {
if (
!(await this.customProjects.canUserDeleteProjects(user.id, body.ids))
!(await this.customProjects.areProjectsCreatedByUser(
user.id,
body.ids,
))
) {
return {
status: 401,
Expand Down
2 changes: 1 addition & 1 deletion api/src/modules/custom-projects/custom-projects.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export class CustomProjectsService extends AppBaseService<
return query;
}

async canUserDeleteProjects(
async areProjectsCreatedByUser(
userId: string,
projectIds: string[],
): Promise<boolean> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { TestManager } from '../../utils/test-manager';
import { customProjectContract } from '@shared/contracts/custom-projects.contract';
import { User } from '@shared/entities/users/user.entity';

describe('Update custom projects', () => {
let testManager: TestManager;
let jwtToken: string;
let user: User;

beforeAll(async () => {
testManager = await TestManager.createTestManager();
});

beforeEach(async () => {
({ jwtToken, user } = await testManager.setUpTestUser());
await testManager.ingestCountries();
await testManager.ingestExcel(jwtToken);
});

afterEach(async () => {
await testManager.clearDatabase();
});

afterAll(async () => {
await testManager.close();
});

describe('Update Custom Projects', () => {
test('An authenticated user should be able to update/edit one of their custom projects by id', async () => {
// Given
const customProject = await testManager.mocks().createCustomProject({
user: { id: user.id } as User,
projectName: 'A',
});

// When
const response = await testManager
.request()
.patch(
`${customProjectContract.updateCustomProject.path.replace(':id', customProject.id)}`,
)
.set('Authorization', `Bearer ${jwtToken}`)
.send({
projectName: 'B',
});

// Then
expect(response.status).toBe(200);
expect(response.body.projectName).toBe('B');
});

test('An authenticated user should not be able to update projects that do not belong to them', async () => {
// Given a custom project exists
const customProject = await testManager.mocks().createCustomProject();

// When updating the custom project
const response = await testManager
.request()
.patch(
`${customProjectContract.updateCustomProject.path.replace(':id', customProject.id)}`,
)
.set('Authorization', `Bearer ${jwtToken}`)
.send({
projectName: 'B',
});

// Then
expect(response.status).toBe(401);
});

test('An unauthenticated user should not be able to update a custom project', async () => {
// Given a custom project exists
const customProject = await testManager.mocks().createCustomProject();

// When updating the custom project
const response = await testManager
.request()
.patch(
`${customProjectContract.updateCustomProject.path.replace(':id', customProject.id)}`,
)
.send({
projectName: 'B',
});

// Then
expect(response.status).toBe(401);
});
});
});
12 changes: 12 additions & 0 deletions shared/contracts/custom-projects.contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,18 @@ export const customProjectContract = contract.router({
},
body: CreateCustomProjectSchema,
},
updateCustomProject: {
method: "PATCH",
path: "/custom-projects/:id",
pathParams: z.object({
id: z.coerce.string(),
}),
responses: {
201: contract.type<CustomProject>(),
},
body: contract.type<Partial<CustomProject>>(),
summary: "Update an existing custom-project",
},
getCustomProjects: {
method: "GET",
path: "/custom-projects",
Expand Down

0 comments on commit df15b8b

Please sign in to comment.