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
Exercise 4 #10
Exercise 4 #10
Conversation
f616da5
to
1e4434f
Compare
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.
touch .env | ||
echo JWTSECRET=$JWTSECRET >> .env |
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.
touch .env | |
echo JWTSECRET=$JWTSECRET >> .env |
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.
If you use dotenv
or dotenv-flow
you can overwrite those values specified in .env
with your current process.env
.
Have you tried deleting .env
and then run your sever with:
$ JWTSECRET=whatever yarn run dev
This should work.
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.
npm install | ||
npm run test | ||
npm run lint | ||
env: | ||
CI: true | ||
CI: true, | ||
JWTSECRET: ${{ secrets.JWTSECRET }} |
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.
That's the only necessary line of code.
@@ -1,5 +1,8 @@ | |||
# Apollo Backend | |||
|
|||
## Mandatory for running tests ´ | |||
Create a .env file in the root backend folder and add a secret JWTSECRET for signing the JWT token (e.g. JWTSECRET=yoursecret) |
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.
You could also check in a .env.template
with dummy secrets and then tell the user to
$ cp .env.template .env
this file.
backend/src/index.spec.ts
Outdated
require('dotenv').config() | ||
|
||
const testContext = (testToken?) => { | ||
let token = testToken || '' | ||
token = token.replace('Bearer ', '') | ||
try { | ||
const decodedJwt = jwt.verify( | ||
token, | ||
process.env.JWTSECRET | ||
) | ||
return { decodedJwt } | ||
} catch (e) { | ||
return {} | ||
} | ||
} |
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.
require('dotenv').config() | |
const testContext = (testToken?) => { | |
let token = testToken || '' | |
token = token.replace('Bearer ', '') | |
try { | |
const decodedJwt = jwt.verify( | |
token, | |
process.env.JWTSECRET | |
) | |
return { decodedJwt } | |
} catch (e) { | |
return {} | |
} | |
} | |
import context from './context' | |
const reqMock | |
const testContext = () => context({ req: reqMock }) | |
beforeEach(() => { | |
reqMock = {} // by default | |
}) | |
// later in your test cases that simulate a logged in user | |
beforeEach(() => { | |
reqMock = { headers: { authorization: `Bearer ${testToken}` } } | |
}) |
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.
const schema = makeExecutableSchema({ typeDefs, resolvers }) | ||
const server = new ApolloServer({ | ||
schema: applyMiddleware(schema, permissions), | ||
context: testContext(testToken), // not sure if this is the correct way. but we didn`t find another solution to add the token as request (see https://github.com/apollographql/apollo-server/issues/2277) |
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 will call testContext(testToken)
when you run setupServer
:
context: testContext(testToken), // not sure if this is the correct way. but we didn`t find another solution to add the token as request (see https://github.com/apollographql/apollo-server/issues/2277) | |
context: testContext(testToken), |
This gives you extra flexibility, because it's called when you run query
or mutate
:
context: testContext(testToken), // not sure if this is the correct way. but we didn`t find another solution to add the token as request (see https://github.com/apollographql/apollo-server/issues/2277) | |
context: () => testContext(testToken), |
backend/src/postsDataSource.ts
Outdated
@@ -1,5 +1,8 @@ | |||
import { DataSource } from 'apollo-datasource' | |||
import { v4 as uuidv4 } from 'uuid' | |||
require('dotenv').config() |
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.
Why do you require('dotenv').config()
so often? I would expect only one file to require it and this file gets imported wherever needed.
backend/src/postsDataSource.ts
Outdated
if (this.users.find(user => user.id === id)) { | ||
return true | ||
} else { | ||
return false | ||
} |
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.
if (this.users.find(user => user.id === id)) { | |
return true | |
} else { | |
return false | |
} | |
return this.users.find(user => user.id === id) |
backend/src/postsDataSource.ts
Outdated
|
||
if (foundUser) { | ||
if (bcrypt.compareSync(password, foundUser.password)) { | ||
return jwt.sign({ id: foundUser.id }, process.env.JWTSECRET, { algorithm: 'HS256' }) |
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.
return jwt.sign({ id: foundUser.id }, process.env.JWTSECRET, { algorithm: 'HS256' }) | |
return jwt.sign({ id: foundUser.id }, JWTSECRET, { algorithm: 'HS256' }) |
Your JWTSECRET
could be a constant imported from src/import
which is the file calling require('dotenv').config()
.
login (email, password) { | ||
const foundUser = this.getUsers().find(user => user.email === email) | ||
|
||
if (foundUser) { |
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.
import { AuthenticationError } from 'apollo-server'
// ...
if (foundUser && foundUser.checkPassword(password)) return jwt.sign({ id: foundUser.id }, JWTSECRET)
throw new Error('Wrong email/password combination')
backend/src/postsDataSource.ts
Outdated
} | ||
|
||
addUser (name, email, password) { | ||
if (password.length >= 8) { |
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.
Prefer guard-clauses over nested if-clauses, for readability:
if (password.length >= 8) { | |
if (password.length < 8) throw new UserInputError('Password must have at least 8 characters') |
} | ||
const foundUser = this.getUsers().find(user => user.email === email) | ||
if (foundUser) { | ||
throw new ForbiddenError('Email already taken by another user') |
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.
Why is the thrown error in the tests or the graphql playground "Not Authorized" even if we throw different errors here?
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 believe you must allowExternalErrors: true
in graphql-shield
.
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.
Well done! Looks like you successfully implemeted all the objectives.
it('signup with password shorter than 8 characters return error', async () => { | ||
const { mutate } = setupServerAndReturnTestClient(posts) | ||
const SIGNUP = 'mutation { signup (name: "testuser", email: "testuser@example.org", password: "short") }' | ||
const res = await mutate({ mutation: SIGNUP }) | ||
expect(res.errors[0].message).toEqual('Not Authorised!') | ||
expect(res.data.signup).toBeNull() | ||
}) | ||
|
||
it('signup with a valid password returns token', async () => { | ||
const { mutate } = setupServerAndReturnTestClient(posts) | ||
const SIGNUP = 'mutation { signup (name: "testuser", email: "testuser@example.org", password: "password") }' | ||
const res = await mutate({ mutation: SIGNUP }) | ||
expect(res.errors).toBeUndefined() | ||
expect(res.data.write.title).toEqual('TestTitle') | ||
expect(res.data.write.author.name).toEqual('user1') | ||
expect(res.data.write.author.posts.length).toEqual(2) | ||
expect(res.data.signup).toEqual(expect.any(String)) | ||
}) | ||
|
||
it('add a post from a new user adds a new user', async () => { | ||
const postData = [ | ||
{ id: 'post1', title: 'Item 1', votes: 0, voters: [], author: {} } | ||
] | ||
const userData = [ | ||
{ name: 'user1', posts: [postData[0]] } | ||
] | ||
postData[0].author = userData[0] | ||
it('trying to signup the same user twice returns error the second time', async () => { | ||
const { mutate } = setupServerAndReturnTestClient(posts) | ||
const SIGNUP = 'mutation { signup (name: "testuser", email: "testuser@example.org", password: "password") }' | ||
await mutate({ mutation: SIGNUP }) | ||
const res = await mutate({ mutation: SIGNUP }) | ||
expect(res.errors[0].message).toEqual('Not Authorised!') | ||
expect(res.data.signup).toBeNull() | ||
}) |
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.
Great tests!
⭐ For a software-tested signup feature.
it('trying to login with invalid password returns error', async () => { | ||
const { mutate } = setupServerAndReturnTestClient(posts) | ||
const SIGNUP = 'mutation { signup (name: "testuser", email: "testuser@example.org", password: "password") }' | ||
await mutate({ mutation: SIGNUP }) | ||
const LOGIN = 'mutation { login (email: "testuser@example.org", password: "invalid") }' | ||
const res = await mutate({ mutation: LOGIN }) | ||
expect(res.errors[0].message).toEqual('Not Authorised!') | ||
expect(res.data.login).toBeNull() | ||
}) | ||
|
||
const GET_USERS = '{ users { name }}' | ||
const { query } = createTestClient(server) | ||
const resQuery = await query({ query: GET_USERS }) | ||
expect(resQuery.errors).toBeUndefined() | ||
expect(resQuery.data.users.length).toEqual(2) | ||
it('trying to login with valid data returns token', async () => { | ||
const { mutate } = setupServerAndReturnTestClient(posts) | ||
const SIGNUP = 'mutation { signup (name: "testuser", email: "testuser@example.org", password: "password") }' | ||
await mutate({ mutation: SIGNUP }) | ||
const LOGIN = 'mutation { login (email: "testuser@example.org", password: "password") }' | ||
const res = await mutate({ mutation: LOGIN }) | ||
expect(res.errors).toBeUndefined() | ||
expect(res.data.login).toEqual(expect.any(String)) |
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.
One more:
⭐ For a software-tested login feature.
if (foundUser) { | ||
throw new ForbiddenError('Email already taken by another user') |
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.
You earned a
⭐ For unique email address validation.
login (email, password) { | ||
const foundUser = this.getUsers().find(user => user.email === email) | ||
|
||
if (foundUser) { | ||
if (compareSync(password, foundUser.password)) { | ||
return sign({ id: foundUser.id }, JWTSECRET, { algorithm: 'HS256' }) |
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.
Now you even earned two at once!
⭐ For an implemented login feature using JWT.
⭐ For password validation.
const foundUser = this.getUsers().find(user => user.email === email) | ||
if (foundUser) { | ||
throw new ForbiddenError('Email already taken by another user') | ||
} | ||
const user = new User(uuidv4(), name, email, hashSync(password, 10)) | ||
this.users.push(user) | ||
return sign({ id: user.id }, JWTSECRET, { algorithm: 'HS256' }) |
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.
Good job.
⭐ For an implemented signup feature.
return context.dataSources.posts.addPost(post, context.decodedJwt.id) | ||
}, | ||
upvote: (parent, { id, voter }, context, info) => { | ||
return context.dataSources.posts.upvote(id, voter) | ||
return context.dataSources.posts.upvote(id, context.decodedJwt.id) |
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.
⭐ For assigning the authenticated user to a post in write and upvote mutations.
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.
Hey Felni, this is a great submission! You achieved all 10/10 ⭐ in
particular:
⭐ For an implemented signup
feature.
⭐ For a software-tested signup
feature.
⭐ For password validation.
⭐ For unique email address validation.
⭐ For not having security issues according to instructions.
⭐ For an implemented login
feature using JWT.
⭐ For a software-tested login
feature.
⭐ For assigning the authenticated user to a post in write
and upvote
mutations.
⭐ For requesting a review and reviewing another team's PR according to the instructions.
⭐ For a successful rebase on homework/main
.
Since my last review, you haven't implemented all suggestions. So please have a
look. I think you could refactor your tests in general:
-
Check what needs to be re-initialized. Only
datasources
andcontext
needs
to be re-initialized, usually. -
Prefer to use
.toMatchObject
expectation when checking GraphQL responses. You
can check a lot more and it's easier to read. -
Move as much logic as possible from the test code into the imported code. You
loose test coverage if you implement parts of the code in your tests (e.g.
password hashing, schema setup).
] | ||
beforeEach(() => { | ||
const postData = [new Post('post1', 'Item1')] | ||
const userData = [new User('userid1', 'user1', 'user1@example.org', hashSync('user1password', 10))] |
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.
Why is it necessary that you call hashSync
in the test? Why don't you simply put that into the constructor?
this.title = title | ||
this.votes = 0 | ||
this.voters = [] | ||
this.author = author || {} |
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.
A post should never have no author!
} | ||
throw new AuthenticationError('Wrong email/password combination') |
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 think this should be below the outmost if
statement. What if foundUser
is falsy?
Unser Codereview: Systems-Development-and-Frameworks/AlElMo#7 (review)