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
33 changes: 33 additions & 0 deletions src/users/users.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('UsersController', () => {
list: jest.Mock;
findById: jest.Mock;
setStellarAddress: jest.Mock;
clearStellarAddress: jest.Mock;
};

const mockPublicUser: PublicUserDto = {
Expand All @@ -32,6 +33,9 @@ describe('UsersController', () => {
...mockPublicUser,
stellarAddress: 'GBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBWHF',
}),
clearStellarAddress: jest
.fn()
.mockResolvedValue({ ...mockPublicUser, stellarAddress: null }),
};

const module: TestingModule = await Test.createTestingModule({
Expand Down Expand Up @@ -93,4 +97,33 @@ describe('UsersController', () => {

expect(usersService.setStellarAddress).not.toHaveBeenCalled();
});

describe('clearStellarAddress (DELETE :id/stellar-address) (#714)', () => {
it('clears the stellar address for the authenticated user', async () => {
const caller: AuthenticatedUser = {
userId: 'user-1',
username: 'alice',
roles: [UserRole.CONTRIBUTOR],
};

const result = await controller.clearStellarAddress('user-1', caller);

expect(result.stellarAddress).toBeNull();
expect(usersService.clearStellarAddress).toHaveBeenCalledWith('user-1');
});

it('throws 403 ForbiddenException when trying to clear another user address', async () => {
const caller: AuthenticatedUser = {
userId: 'user-2',
username: 'bob',
roles: [UserRole.CONTRIBUTOR],
};

expect(() =>
controller.clearStellarAddress('user-1', caller),
).toThrow(ForbiddenException);

expect(usersService.clearStellarAddress).not.toHaveBeenCalled();
});
});
});
23 changes: 23 additions & 0 deletions src/users/users.controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {
Body,
Controller,
Delete,
ForbiddenException,
Get,
HttpCode,
HttpStatus,
Param,
ParseUUIDPipe,
Patch,
Expand Down Expand Up @@ -60,4 +63,24 @@ export class UsersController {
}
return this.usersService.setStellarAddress(id, dto.stellarAddress);
}

// #714: disconnect() was sending PATCH { stellarAddress: null } which the
// @IsStellarAddress() validator on SetStellarAddressDto always rejects with
// 400. A dedicated DELETE route accepts no body and sets the column to null,
// keeping the PATCH route strictly typed for real addresses.
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@Delete(':id/stellar-address')
@HttpCode(HttpStatus.OK)
clearStellarAddress(
@Param('id', new ParseUUIDPipe()) id: string,
@CurrentUser() user: AuthenticatedUser,
) {
if (user && user.userId !== id) {
throw new ForbiddenException(
'You may only update your own Stellar address',
);
}
return this.usersService.clearStellarAddress(id);
}
}
23 changes: 23 additions & 0 deletions src/users/users.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,29 @@ describe('UsersService', () => {
});
});

describe('clearStellarAddress', () => {
it('throws NotFoundException when the user does not exist', async () => {
userRepo.findOne.mockResolvedValue(null);
await expect(service.clearStellarAddress('missing')).rejects.toThrow(
NotFoundException,
);
});

it('sets stellarAddress to null and persists the change (#714)', async () => {
userRepo.findOne.mockResolvedValue({
id: 'u1',
stellarAddress: 'GLINKEDADDRESS',
});

const user = await service.clearStellarAddress('u1');

expect(user.stellarAddress).toBeNull();
expect(userRepo.save).toHaveBeenCalledWith(
expect.objectContaining({ id: 'u1', stellarAddress: null }),
);
});
});

describe('list', () => {
it('returns every user mapped to its public shape', async () => {
userRepo.find = jest.fn().mockResolvedValue([
Expand Down
6 changes: 6 additions & 0 deletions src/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ export class UsersService {
return toPublicUser(await this.userRepo.save(user));
}

async clearStellarAddress(userId: string): Promise<PublicUserDto> {
const user = await this.findOneRaw(userId);
user.stellarAddress = null;
return toPublicUser(await this.userRepo.save(user));
}

async list(): Promise<PublicUserDto[]> {
const users = await this.userRepo.find();
return users.map(toPublicUser);
Expand Down
Loading