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

Exercise 4 #10

Merged
merged 18 commits into from Dec 30, 2020
Merged

Exercise 4 #10

merged 18 commits into from Dec 30, 2020

Conversation

NicolasTsc
Copy link
Collaborator

@NicolasTsc NicolasTsc commented Nov 25, 2020

@NicolasTsc NicolasTsc requested review from moengers and a team November 28, 2020 07:31
@NicolasTsc NicolasTsc changed the title add graph packages and update readme Exercise 4 Nov 30, 2020
@NicolasTsc NicolasTsc requested a review from a team December 6, 2020 08:33
Copy link
Contributor

@roschaefer roschaefer left a comment

Choose a reason for hiding this comment

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

keep going

I received an E-Mail from a member of your group, asking me to do a Feedback-Review until Wednesday. Please request a review from @mentors in the first week next time.

2020-12-07_22-04

See my suggestions below.

Comment on lines 36 to 37
touch .env
echo JWTSECRET=$JWTSECRET >> .env
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
touch .env
echo JWTSECRET=$JWTSECRET >> .env

Copy link
Contributor

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for the review Robert. As I cant comment your review message I have to write it here. We actually did request a mentors-review in the first week of the exercise, so maybe the notification didnt work as expected.
image

npm install
npm run test
npm run lint
env:
CI: true
CI: true,
JWTSECRET: ${{ secrets.JWTSECRET }}
Copy link
Contributor

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)
Copy link
Contributor

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.

Comment on lines 11 to 25
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 {}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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}` } }
})

Copy link
Contributor

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)
Copy link
Contributor

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:

Suggested change
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:

Suggested change
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),

@@ -1,5 +1,8 @@
import { DataSource } from 'apollo-datasource'
import { v4 as uuidv4 } from 'uuid'
require('dotenv').config()
Copy link
Contributor

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.

Comment on lines 22 to 26
if (this.users.find(user => user.id === id)) {
return true
} else {
return false
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (this.users.find(user => user.id === id)) {
return true
} else {
return false
}
return this.users.find(user => user.id === id)


if (foundUser) {
if (bcrypt.compareSync(password, foundUser.password)) {
return jwt.sign({ id: foundUser.id }, process.env.JWTSECRET, { algorithm: 'HS256' })
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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) {
Copy link
Contributor

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')

}

addUser (name, email, password) {
if (password.length >= 8) {
Copy link
Contributor

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:

Suggested change
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')
Copy link
Collaborator Author

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?

Copy link
Contributor

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.

Copy link

@AlexHTW AlexHTW left a 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.

Comment on lines +132 to +155
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()
})
Copy link

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.

Comment on lines +157 to +174
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))
Copy link

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.

Comment on lines +71 to +72
if (foundUser) {
throw new ForbiddenError('Email already taken by another user')
Copy link

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.

Comment on lines +55 to +60
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' })
Copy link

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.

Comment on lines +70 to +76
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' })
Copy link

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.

Comment on lines +13 to +16
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)
Copy link

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.

Copy link
Contributor

@roschaefer roschaefer left a comment

Choose a reason for hiding this comment

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

well done

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.

2020-12-20_17-09

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 and context 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))]
Copy link
Contributor

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 || {}
Copy link
Contributor

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')
Copy link
Contributor

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?

@NicolasTsc NicolasTsc merged commit 968c21c into main Dec 30, 2020
@NicolasTsc NicolasTsc deleted the exercise-4 branch December 30, 2020 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants