-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
pr05 Typescript Migration #14: Migrate User Controller #3681
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
base: develop
Are you sure you want to change the base?
pr05 Typescript Migration #14: Migrate User Controller #3681
Conversation
…e, and resolve type errors
…tePreferences, no-verify
…eferences to /userPreferences & add response and request types
…teCookiePreferences & move to /userPreferences
…e to /authmanagement and add request and response types
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' | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
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< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks great!
res.status(404).json({ error: 'User not found' }); | ||
return; | ||
} | ||
user.username = req.body.username; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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!; |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i agree!! that makes sense
/** | ||
* - Method: `DELETE` | ||
* - Endpoint: `/auth/google` | ||
* - Authenticated: `false` -- TODO: update to true? |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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"] |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
899c10c
to
1426142
Compare
… BDD, simplified after manual testingon client
describe.skip('when given old username, old email, and matching current password and no new password', () => { | ||
beforeEach(async () => { | ||
requestBody = { | ||
...minimumValidRequest, | ||
currentPassword: OLD_PASSWORD | ||
}; | ||
request.setBody(requestBody); | ||
await updateSettings(request, response, next); | ||
}); | ||
|
||
it('returns 401 with an "New password is required" message', () => { | ||
expect(response.status).toHaveBeenCalledWith(400); | ||
expect(response.json).toHaveBeenCalledWith({ | ||
error: 'New password is required.' | ||
}); | ||
}); | ||
|
||
it('does not save the user with the new password', () => { | ||
expect(saveUser).not.toHaveBeenCalled(); | ||
}); | ||
it('does not send a confirmation email to the user', () => { | ||
expect(mailerService.send).not.toHaveBeenCalled(); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's client-side protection for this, but maybe we should guard against it just in case?
Currently it would save with the user.password: undefined
describe.skip('when missing username', () => { | ||
beforeEach(async () => { | ||
request.setBody({ email: OLD_EMAIL }); | ||
await updateSettings(request, response, next); | ||
}); | ||
|
||
it('returns 401 with an "Missing username" message', () => { | ||
expect(response.status).toHaveBeenCalledWith(400); | ||
expect(response.json).toHaveBeenCalledWith({ | ||
error: 'Username is required.' | ||
}); | ||
}); | ||
|
||
it('does not save the user with the new password', () => { | ||
expect(saveUser).not.toHaveBeenCalled(); | ||
}); | ||
it('does not send a confirmation email to the user', () => { | ||
expect(mailerService.send).not.toHaveBeenCalled(); | ||
}); | ||
}); | ||
|
||
describe.skip('when missing email', () => { | ||
beforeEach(async () => { | ||
request.setBody({ username: OLD_USERNAME }); | ||
await updateSettings(request, response, next); | ||
}); | ||
|
||
it('returns 401 with an "Missing email" message', () => { | ||
expect(response.status).toHaveBeenCalledWith(400); | ||
expect(response.json).toHaveBeenCalledWith({ | ||
error: 'Email is required.' | ||
}); | ||
}); | ||
|
||
it('does not save the user with the new password', () => { | ||
expect(saveUser).not.toHaveBeenCalled(); | ||
}); | ||
it('does not send a confirmation email to the user', () => { | ||
expect(mailerService.send).not.toHaveBeenCalled(); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently doesn't return an error but maybe this would add safety, will propose on the logic-change PR
describe.skip('when given new username, new email, and new password with valid current password', () => { | ||
beforeEach(async () => { | ||
requestBody = { | ||
username: NEW_USERNAME, | ||
email: NEW_EMAIL, | ||
currentPassword: OLD_PASSWORD, | ||
newPassword: NEW_PASSWORD | ||
}; | ||
request.setBody(requestBody); | ||
await updateSettings(request, response, next); | ||
}); | ||
it('saves the user with the correct details once', () => { | ||
expect(saveUser).toHaveBeenCalledWith(response, { | ||
...startingUser, | ||
username: NEW_USERNAME, | ||
email: NEW_EMAIL, | ||
verified: STATUSES.Sent, | ||
verifiedToken: GENERATED_TOKEN, | ||
verifiedTokenExpires: TOKEN_EXPIRY_TIME, | ||
password: NEW_PASSWORD | ||
}); | ||
expect(saveUser).toHaveBeenCalledTimes(1); | ||
}); | ||
it('sends a confirmation email to the user', () => { | ||
expect(mailerService.send).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
subject: 'Mock confirm your email' | ||
}) | ||
); | ||
}); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
describe.skip('when given old username, old email, and matching current password and no new password', () => { | ||
beforeEach(async () => { | ||
requestBody = { | ||
...minimumValidRequest, | ||
currentPassword: OLD_PASSWORD | ||
}; | ||
request.setBody(requestBody); | ||
await updateSettings(request, response, next); | ||
}); | ||
|
||
it('returns 401 with an "New password is required" message', () => { | ||
expect(response.status).toHaveBeenCalledWith(401); | ||
expect(response.json).toHaveBeenCalledWith({ | ||
error: 'New password is required.' | ||
}); | ||
}); | ||
|
||
it('does not save the user with the new password', () => { | ||
expect(saveUser).not.toHaveBeenCalled(); | ||
}); | ||
it('does not send a confirmation email to the user', () => { | ||
expect(mailerService.send).not.toHaveBeenCalled(); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's client-side protection for this, but maybe we should guard against it just in case?
Currently it would save with the user.password: undefined
let request; | ||
let response; | ||
describe('user.controller > api key', () => { | ||
let request: any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should the type be MockRequest?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yeah I wanted to do that but typing request: MockRequest
and response: MockResponse
results in a bunch of type errors because the MockRequest
is missing a bunch of methods that actual express Request
types have

We could cast it like below per test instance, but it felt a bit verbose, what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I think it's kind of nice that we're being explicit about what is happening, just updated to implement this!
jest.mock('../../../../utils/mail'); | ||
|
||
describe('user.controller > auth management > 3rd party auth', () => { | ||
let request: any; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here (and for other files) is there any chance the type can be MockRequest ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
amazing, thank you so much for being thorough!
* Description: | ||
* - Create API key | ||
*/ | ||
export const createApiKey: RequestHandler< |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks great!
* - Id: `UserController.resetPasswordInitiate` | ||
* | ||
* Description: | ||
* - Send an Reset Email email to the registered email account |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
small typo? "send a reset email..." ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated!
* @returns Sanitised user | ||
*/ | ||
export function userResponse( | ||
user: PublicUser & Record<string, any> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe <string, unknown>, if possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also, i could be super wrong, but is there any chance the user parameter should be User or a similar type that has more fields? Otherwise it looks like we're not narrowing in on the fields
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to fix this!
/** | ||
* - Method: `DELETE` | ||
* - Endpoint: `/auth/github` | ||
* - Authenticated: `false` -- TODO: update to true? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i agree!! that makes sense
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you so much!!
Migration of the
userController
to tsChanges:
userController
methods, which can act as examples of how to test for other contributors to do migration workuser
routes into "subdomains" (related topics of routes)userController
methods into barrel structure based on these "subdomains" -- NO LOGIC CHANGESuserController
methods as ExpressRequestHandler
& extract the types for each route --> added to relevant/server/types
file:params
responseBody
requestBody
query
User
type in theExpress
namespace so that eachRequest.user
is our customUser
typeNotes:
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:
npm run lint
)npm run test
)develop
branch.Fixes #123