Skip to content

Commit

Permalink
Add optional nicknames to users (#43)
Browse files Browse the repository at this point in the history
* Add optional nicknames to users

* Add nickname to user search

* Fix merge errors
  • Loading branch information
Yoronex authored Aug 16, 2023
1 parent 93fcca3 commit 543b071
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 2 deletions.
2 changes: 2 additions & 0 deletions src/controller/request/update-user-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* @typedef UpdateUserRequest
* @property {string} firstName
* @property {string} lastName
* @property {string} nickname
* @property {boolean} active
* @property {boolean} ofAge
* @property {string} email
Expand All @@ -28,6 +29,7 @@
export default interface UpdateUserRequest {
firstName?: string;
lastName?: string;
nickname?: string;
active?: boolean;
ofAge?: boolean;
email?: string;
Expand Down
2 changes: 2 additions & 0 deletions src/controller/response/user-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import { TermsOfServiceStatus } from '../../entity/user/user';
* @typedef {BaseResponse} BaseUserResponse
* @property {string} firstName.required - The name of the user.
* @property {string} lastName.required - The last name of the user
* @property {string} nickname - The nickname of the user
*/
export interface BaseUserResponse extends BaseResponse {
firstName: string,
lastName: string,
nickname?: string,
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/controller/user-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,11 @@ export default class UserController extends BaseController {
res.status(400).json('lastName too long');
return;
}
if (body.nickname !== undefined && body.nickname.length > 64) {
res.status(400).json('nickname too long');
return;
}
if (body.nickname === '') body.nickname = null;

try {
const id = parseInt(parameters.id, 10);
Expand Down
7 changes: 7 additions & 0 deletions src/entity/user/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const TOSRequired = [
* @typedef {BaseEntity} User
* @property {string} firstName.required - First name of the user.
* @property {string} lastName - Last name of the user.
* @property {string} nickname - Nickname of the user.
* @property {boolean} active - Whether the user has accepted the TOS. Defaults to false.
* @property {boolean} ofAge - Whether the user is 18+ or not.
* @property {string} email - The email of the user.
Expand All @@ -74,6 +75,12 @@ export default class User extends BaseEntity {
})
public lastName: string;

@Column({
length: 64,
nullable: true,
})
public nickname: string;

@Column({
default: false,
})
Expand Down
3 changes: 3 additions & 0 deletions src/helpers/revision-to-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function parseUserToBaseResponse(user: User, timestamps: boolean): BaseUs
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
nickname: user.nickname,
createdAt: timestamps ? user.createdAt.toISOString() : undefined,
updatedAt: timestamps ? user.updatedAt.toISOString() : undefined,
} as BaseUserResponse;
Expand Down Expand Up @@ -119,6 +120,7 @@ export interface RawUser {
id: number,
firstName: string,
lastName: string,
nickname: string,
active: number,
ofAge: number,
email: string,
Expand All @@ -138,6 +140,7 @@ export function parseRawUserToResponse(user: RawUser, timestamps = false): UserR
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
nickname: user.nickname,
createdAt: timestamps ? user.createdAt : undefined,
updatedAt: timestamps ? user.updatedAt : undefined,
active: user.active === 1,
Expand Down
5 changes: 4 additions & 1 deletion src/service/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ export default class UserService {
qb.where('user.firstName LIKE :searchTerm1 COLLATE utf8_general_ci')
.orWhere('user.lastName LIKE :searchTerm2 COLLATE utf8_general_ci')
.orWhere('user.firstName LIKE :searchTerm2 COLLATE utf8_general_ci')
.orWhere('user.lastName LIKE :searchTerm1 COLLATE utf8_general_ci');
.orWhere('user.lastName LIKE :searchTerm1 COLLATE utf8_general_ci')
.orWhere('user.nickname LIKE :searchTerm2 COLLATE utf8_general_ci')
.orWhere('user.nickname LIKE :searchTerm1 COLLATE utf8_general_ci');
}), {
searchTerm1: searchTerm1,
searchTerm2: searchTerm2,
Expand All @@ -120,6 +122,7 @@ export default class UserService {
builder.andWhere(new Brackets(qb => {
qb.where('user.firstName LIKE :search COLLATE utf8_general_ci')
.orWhere('user.lastName LIKE :search COLLATE utf8_general_ci')
.orWhere('user.nickname LIKE :search COLLATE utf8_general_ci')
.orWhere('user.email LIKE :search COLLATE utf8_general_ci');
}), {
search: searchTerm,
Expand Down
1 change: 1 addition & 0 deletions test/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ function defineUsers(
id: start + nr,
firstName: `Firstname${start + nr}`,
lastName: `Lastname${start + nr}`,
nickname: nr % 4 === 0 ? `Nickname${start + nr}` : null,
type,
active,
acceptedToS: TermsOfServiceStatus.ACCEPTED,
Expand Down
53 changes: 52 additions & 1 deletion test/unit/controller/user-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ describe('UserController', (): void => {
return (
user.firstName.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.lastName.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.nickname?.toLowerCase().includes(searchQuery.toLowerCase()) ||
fullName.toLowerCase().includes(searchQuery.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.toLowerCase())
);
Expand Down Expand Up @@ -360,6 +361,22 @@ describe('UserController', (): void => {
expect(pagination.skip).to.equal(0);

});
it('should return correct user using search on nickname', async () => {
const searchQuery = ctx.users.find((u) => u.nickname != null).nickname;
const res = await request(ctx.app)
.get('/users')
.query({ search: searchQuery })
.set('Authorization', `Bearer ${ctx.adminToken}`);
expect(res.status).to.equal(200);

const filteredUsers = await queryUserBackend(searchQuery);

const users = res.body.records as UserResponse[];
const ids = users.map((u) => u.id);
filteredUsers.forEach((u) => {
expect(ids).to.includes(u.id);
});
});
it('should give HTTP 200 when correctly creating and searching for a user', async () => {
const user = {
firstName: 'Één bier',
Expand Down Expand Up @@ -821,13 +838,47 @@ describe('UserController', (): void => {
expect(user.lastName).to.deep.equal(lastName);
verifyUserResponse(spec, user);
});
it('should give HTTP 400 if firstName is too long', async () => {
it('should give HTTP 400 if lastName is too long', async () => {
const res = await request(ctx.app)
.patch('/users/1')
.set('Authorization', `Bearer ${ctx.adminToken}`)
.send({ lastName: 'ThisIsAStringThatIsMuchTooLongToFitInASixtyFourCharacterStringBox' });
expect(res.status).to.equal(400);
});
it('should correctly change nickname if requester is admin', async () => {
const nickname = 'SudoSOSFeut';

const res = await request(ctx.app)
.patch('/users/1')
.set('Authorization', `Bearer ${ctx.adminToken}`)
.send({ nickname });
expect(res.status).to.equal(200);

const user = res.body as UserResponse;
const spec = await Swagger.importSpecification();
expect(user.nickname).to.deep.equal(nickname);
verifyUserResponse(spec, user);
});
it('should give HTTP 400 if nickName is too long', async () => {
const res = await request(ctx.app)
.patch('/users/1')
.set('Authorization', `Bearer ${ctx.adminToken}`)
.send({ nickname: 'ThisIsAStringThatIsMuchTooLongToFitInASixtyFourCharacterStringBox' });
expect(res.status).to.equal(400);
});
it('should correctly remove nickname if set to empty string', async () => {
const user = ctx.users.find((u) => u.nickname != null);

const res = await request(ctx.app)
.patch('/users/' + user.id)
.set('Authorization', `Bearer ${ctx.adminToken}`)
.send({ nickname: '' });
expect(res.status).to.equal(200);

const userResponse = res.body as UserResponse;
expect(userResponse.nickname).to.be.null;
expect((await User.findOne({ where: { id: user.id } })).nickname).to.be.null;
});
it('should correctly set user inactive if requester is admin', async () => {
const active = false;

Expand Down
1 change: 1 addition & 0 deletions test/unit/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function verifyUserResponse(
expect(userResponse.firstName).to.be.not.empty;
expect(userResponse.lastName).to.be.not.undefined;
expect(userResponse.lastName).to.be.not.null;
expect(userResponse.nickname).to.satisfy((nick: string) => nick == null || nick.length >= 1);
expect(userResponse.active).to.not.be.null;
if (canBeDeleted) {
expect(userResponse.deleted).to.be.a('boolean');
Expand Down

0 comments on commit 543b071

Please sign in to comment.