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

Add optional nicknames to users #43

Merged
merged 4 commits into from
Aug 16, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
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
Yoronex marked this conversation as resolved.
Show resolved Hide resolved
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
2 changes: 1 addition & 1 deletion test/unit/service/invoice-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export async function createTransactions(debtorId: number, creditorId: number, t
export async function
createInvoiceWithTransfers(debtorId: number, creditorId: number,
transactionCount: number) {
const { transactions, total } = await createTransactions(debtorId, creditorId, transactionCount);
const { transactions } = await createTransactions(debtorId, creditorId, transactionCount);
await new Promise((f) => setTimeout(f, 1000));

const createInvoiceRequest: CreateInvoiceParams = {
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
Loading