Skip to content

Conversation

clairep94
Copy link
Collaborator

@clairep94 clairep94 commented Oct 10, 2025

Migration of the userController to ts

Changes:

  • Add unit tests to all userController methods, which can act as examples of how to test for other contributors to do migration work
  • Organise user routes into "subdomains" (related topics of routes)
  • Move userController methods into barrel structure based on these "subdomains" -- NO LOGIC CHANGES
  • Explicitly declare userController methods as Express RequestHandler & extract the types for each route --> added to relevant /server/types file:
    • params
    • responseBody
    • requestBody
    • query
    • These are then intended to be legible by the frontend to create tighter contracts (eg. user preferences redux)
  • Extend the User type in the Express namespace so that each Request.user is our custom User type
  • Added jsdocs to each type & controller method
  • Added simple mock files for some mailer related modules that kept being used in user controllers

Notes:
I have some suggestions for logic improvements on the methods, but I will make those on a PR into this branch that I will link below when completed so that it's easier to view the diff

I have verified that this pull request:

  • has no linting errors (npm run lint)
  • has no test errors (npm run test)
  • is from a uniquely-named feature branch and is up to date with the develop branch.
  • is descriptively named and links to an issue number, i.e. Fixes #123
  • meets the standards outlined in the accessibility guidelines

…eferences to /userPreferences & add response and request types
…teCookiePreferences & move to /userPreferences
…e to /authmanagement and add request and response types
@clairep94 clairep94 marked this pull request as ready for review October 10, 2025 01:03
@clairep94
Copy link
Collaborator Author

@khanniie @raclim

Ready for review!

I know this is a big one as I have also migrated the user controller methods into somewhat of a barrel structure for organisation

I have annotated the places where the original code content has changed, but other than adding ! on req.user, I have not changed much

I will also make a PR into this branch to share for a proposal on improving the logic in updateSettings as I noticed some aspects that could be improved, but I didn't want to change it inside the same PR since it's already tricky to view the diffs.

All controller methods have been unit tested before migrating to maximum safety!

Copy link
Collaborator Author

@clairep94 clairep94 Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Migrated this to a barrel structure based on the 'subdomain' of what each route is for, please see server/routes/user

Made sure to add unit tests to each item prior to migrating & did not change any logic within functions other than asserting req.user! on authenticated routes

Screenshot 2025-10-10 at 08 58 29 Screenshot 2025-10-10 at 08 58 34

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

helper to create user with a few more properties than the mock file

i wasn't sure how sinon worked, so I just mocked a simple object with the relevant properties, and devs can override per test basis

/* @jest-environment node */

import { last } from 'lodash';
import { Request, Response } from 'jest-express';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was highlighted as not being an export when the file was changed to ts
I did some digging and found the actual place where these are exported which fixed it

User.findById = jest.fn().mockResolvedValue(null);

await createApiKey(request, response);
await createApiKey(request, response, next);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Express RequestHandler require a next function, so added one here

this doesn't seem to be an issue where it's used in the actual non-test code though (routes/user)

Comment on lines 68 to +81
const lastKey = last(user.apiKeys);

expect(lastKey.label).toBe('my key');
expect(typeof lastKey.hashedKey).toBe('string');
expect(lastKey?.label).toBe('my key');
expect(typeof lastKey?.hashedKey).toBe('string');

const responseData = response.json.mock.calls[0][0];
expect(responseData.apiKeys.length).toBe(1);
expect(responseData.apiKeys[0]).toMatchObject({
label: 'my key',
token: lastKey.hashedKey
token: lastKey?.hashedKey
Copy link
Collaborator Author

@clairep94 clairep94 Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

? was to resolve a ts error where it said lastKey might not exist

Copy link
Collaborator Author

@clairep94 clairep94 Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the logic on this controller method is a bit wonky, I will propose some changes on another PR that I'll tag you guys on

Included some comments in the test below re thoughts on the method

Comment on lines 208 to 229
// NOTE: This should not pass, but it currently does!!
describe('and when there is no new password on the request', () => {
beforeEach(async () => {
requestBody = {
...minimumValidRequest,
newPassword: undefined,
currentPassword: OLD_PASSWORD
};

// simulate check for password match passing
startingUser.comparePassword = jest.fn().mockResolvedValue(true);

request.setBody(requestBody);
await updateSettings(request, response, next);
});
it('calls saveUser with the new empty password', () => {
expect(saveUser).toHaveBeenCalledWith(response, {
...startingUser,
password: undefined
});
});
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

highlighting this

Comment on lines 135 to 136
// currently frontend doesn't seem to call password-change related things the below
// not sure if we should update the logic to be cleaner?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

highlighting this comment -- I couldn't find where we change password on the UI, so not sure if this is being used?

if not maybe we could remove this
I will otherwise propose how to make the logic cleaner on the other PR

Comment on lines +35 to +80
describe('createUser', () => {
it('should return 422 if email already exists', async () => {
User.findByEmailAndUsername = jest.fn().mockResolvedValue({
email: '[email protected]',
username: 'anyusername'
});

request.setBody({
username: 'testuser',
email: '[email protected]',
password: 'password'
});

await createUser(request, response, next);

expect(User.findByEmailAndUsername).toHaveBeenCalledWith(
'[email protected]',
'testuser'
);
expect(response.status).toHaveBeenCalledWith(422);
expect(response.send).toHaveBeenCalledWith({ error: 'Email is in use' });
});
it('should return 422 if username already exists', async () => {
User.findByEmailAndUsername = jest.fn().mockResolvedValue({
email: '[email protected]',
username: 'testuser'
});

request.setBody({
username: 'testuser',
email: '[email protected]',
password: 'password'
});

await createUser(request, response, next);

expect(User.findByEmailAndUsername).toHaveBeenCalledWith(
'[email protected]',
'testuser'
);
expect(response.status).toHaveBeenCalledWith(422);
expect(response.send).toHaveBeenCalledWith({
error: 'Username is in use'
});
});
});
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the only controller method where not all the logic was tested

I couldn't work out how to control what new User({..}) does in the test environment, but everything is otherwise 100% tested

* Description:
* - Create API key
*/
export const createApiKey: RequestHandler<
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought putting these here helped a lot with hints in the IDE when you hover the method, but let me know if it should go elsewhere, or if you prefer a different format

Screenshot 2025-10-10 at 09 05 28

res.status(404).json({ error: 'User not found' });
return;
}
user.username = req.body.username;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
user.username = req.body.username;
user.username = req.body.username ?? user.username;

Or error early if req.body doesn't have username or email

res.status(401).json({ error: 'Current password is invalid.' });
return;
}
user.password = req.body.newPassword!;
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i had to add ! to req.body.newPassword to resolve the typeError where newpassword might not exist, without changing the code logic, but I would propose to move this block within if (req.body.newPassword)

/**
* - Method: `DELETE`
* - Endpoint: `/auth/github`
* - Authenticated: `false` -- TODO: update to true?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is currently not a route that goes after the isAuthenticated middleware, should we put it behind isAuthenticated and remove the login checks?

On the UI I think I can only see if I'm logged in

/**
* - Method: `DELETE`
* - Endpoint: `/auth/google`
* - Authenticated: `false` -- TODO: update to true?
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

domain: `${protocol}://${req.headers.host}`,
link: `${protocol}://${req.headers.host}/verify?t=${token}`
},
to: req.user!.email
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added to resolve type error for user potentially not existing
safe to do so since this is after middleware

> = async (req, res) => {
try {
const token = await generateToken();
const user = await User.findById(req.user!.id).exec();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added to resolve type error for user potentially not existing
this isn't a route thats hidden past the middleware, but I think it's called after the user has signed up, so their new user is already attached to the request/passport right?

UpdatePreferencesRequestBody
> = async (req, res) => {
try {
const user = await User.findById(req.user!.id).exec();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added to resolve type error for user potentially not existing
safe to do so since this is after middleware

UpdateCookieConsentRequestBody
> = async (req, res) => {
try {
const user = await User.findById(req.user!.id).exec();
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added to resolve type error for user potentially not existing
safe to do so since this is after middleware

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this file was originally in server/utils but it's a middleware so I thought it was more appropriate here
added test before moving

"lib": ["ES2022"],
"types": ["node", "jest"]
"types": ["node", "jest", "express"],
"typeRoots": ["./types", "../node_modules/@types"]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated so that we can extend the express namespace user type to be our user type in each request

Copy link
Collaborator Author

@clairep94 clairep94 Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added so that we can extend the express namespace user type to be our user type in each request.user

@clairep94 clairep94 force-pushed the pr05/migrate_user_controller_oct branch from 899c10c to 1426142 Compare October 10, 2025 13:17
@clairep94 clairep94 changed the title Pr05/migrate User Controller pr05 Typescript Migration #14: Migrate User Controller Oct 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr05 Grant Projects pr05 Grant Projects
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant