make tests run in parallel

master
Peter Babič 3 years ago
parent 81595c8261
commit 69233bb7e1
Signed by: peter.babic
GPG Key ID: 4BB075BC1884BA40
  1. 2
      .gitignore
  2. 7
      jest.config.js
  3. 175
      package-lock.json
  4. 16
      package.json
  5. 49
      src/app.spec.ts
  6. 152
      src/app/UserResolver.spec.ts
  7. 58
      src/app/userResolver/auth.ts
  8. 24
      src/bootstrap.ts
  9. 13
      src/jestGlobalSetup.ts
  10. 26
      src/server.spec.ts
  11. 9
      src/server.ts
  12. 178
      src/server/UserResolver.spec.ts
  13. 6
      src/server/UserResolver.ts
  14. 15
      src/server/connection.ts
  15. 21
      src/server/schema.ts
  16. 40
      src/server/testing.ts
  17. 0
      src/server/userResolver/LoginTokens.ts
  18. 0
      src/server/userResolver/User.ts
  19. 43
      src/server/userResolver/auth.ts

2
.gitignore vendored

@ -1,5 +1,3 @@
*.key
*.pub
.vscode/
# Created by https://www.gitignore.io/api/node
# Edit at https://www.gitignore.io/?templates=node

@ -1,4 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
preset: "ts-jest",
testEnvironment: "node",
globalSetup: "./src/jestGlobalSetup.ts",
}

175
package-lock.json generated

@ -1064,6 +1064,131 @@
"apollo-server-types": "^0.2.4"
}
},
"apollo-server-testing": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/apollo-server-testing/-/apollo-server-testing-2.9.6.tgz",
"integrity": "sha512-pbURQD5VjNFk4GMVVxyCds9rY4/NIqjvjE4tyf1k89RHwMdk+zuVggt/DGudteorZtqAqtsOIHWojMBU4s2klA==",
"dev": true,
"requires": {
"apollo-server-core": "^2.9.6"
},
"dependencies": {
"apollo-cache-control": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.8.5.tgz",
"integrity": "sha512-2yQ1vKgJQ54SGkoQS/ZLZrDX3La6cluAYYdruFYJMJtL4zQrSdeOCy11CQliCMYEd6eKNyE70Rpln51QswW2Og==",
"dev": true,
"requires": {
"apollo-server-env": "^2.4.3",
"graphql-extensions": "^0.10.4"
}
},
"apollo-engine-reporting": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-1.4.7.tgz",
"integrity": "sha512-qsKDz9VkoctFhojM3Nj3nvRBO98t8TS2uTgtiIjUGs3Hln2poKMP6fIQ37Nm2Q2B3JJst76HQtpPwXmRJd1ZUg==",
"dev": true,
"requires": {
"apollo-engine-reporting-protobuf": "^0.4.1",
"apollo-graphql": "^0.3.4",
"apollo-server-caching": "^0.5.0",
"apollo-server-env": "^2.4.3",
"apollo-server-types": "^0.2.5",
"async-retry": "^1.2.1",
"graphql-extensions": "^0.10.4"
}
},
"apollo-engine-reporting-protobuf": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.1.tgz",
"integrity": "sha512-d7vFFZ2oUrvGaN0Hpet8joe2ZG0X0lIGilN+SwgVP38dJnOuadjsaYMyrD9JudGQJg0bJA5wVQfYzcCVy0slrw==",
"dev": true,
"requires": {
"protobufjs": "^6.8.6"
}
},
"apollo-graphql": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.3.4.tgz",
"integrity": "sha512-w+Az1qxePH4oQ8jvbhQBl5iEVvqcqynmU++x/M7MM5xqN1C7m1kyIzpN17gybXlTJXY4Oxej2WNURC2/hwpfYw==",
"dev": true,
"requires": {
"apollo-env": "^0.5.1",
"lodash.sortby": "^4.7.0"
}
},
"apollo-server-core": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.9.6.tgz",
"integrity": "sha512-2tHAWQxP7HrETI/BZvg2fem6YlahF9HUp4Y6SSL95WP3uNMOJBlN12yM1y+O2u5K5e4jwdPNaLjoL2A/26XrLw==",
"dev": true,
"requires": {
"@apollographql/apollo-tools": "^0.4.0",
"@apollographql/graphql-playground-html": "1.6.24",
"@types/graphql-upload": "^8.0.0",
"@types/ws": "^6.0.0",
"apollo-cache-control": "^0.8.5",
"apollo-datasource": "^0.6.3",
"apollo-engine-reporting": "^1.4.7",
"apollo-server-caching": "^0.5.0",
"apollo-server-env": "^2.4.3",
"apollo-server-errors": "^2.3.3",
"apollo-server-plugin-base": "^0.6.5",
"apollo-server-types": "^0.2.5",
"apollo-tracing": "^0.8.5",
"fast-json-stable-stringify": "^2.0.0",
"graphql-extensions": "^0.10.4",
"graphql-tag": "^2.9.2",
"graphql-tools": "^4.0.0",
"graphql-upload": "^8.0.2",
"sha.js": "^2.4.11",
"subscriptions-transport-ws": "^0.9.11",
"ws": "^6.0.0"
}
},
"apollo-server-plugin-base": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.5.tgz",
"integrity": "sha512-z2ve7HEPWmZI3EzL0iiY9qyt1i0hitT+afN5PzssCw594LB6DfUQWsI14UW+W+gcw8hvl8VQUpXByfUntAx5vw==",
"dev": true,
"requires": {
"apollo-server-types": "^0.2.5"
}
},
"apollo-server-types": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.2.5.tgz",
"integrity": "sha512-6iJQsPh59FWu4K7ABrVmpnQVgeK8Ockx8BcawBh+saFYWTlVczwcLyGSZPeV1tPSKwFwKZutyEslrYSafcarXQ==",
"dev": true,
"requires": {
"apollo-engine-reporting-protobuf": "^0.4.1",
"apollo-server-caching": "^0.5.0",
"apollo-server-env": "^2.4.3"
}
},
"apollo-tracing": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.8.5.tgz",
"integrity": "sha512-lZn10/GRBZUlMxVYLghLMFsGcLN0jTYDd98qZfBtxw+wEWUx+PKkZdljDT+XNoOm/kDvEutFGmi5tSLhArIzWQ==",
"dev": true,
"requires": {
"apollo-server-env": "^2.4.3",
"graphql-extensions": "^0.10.4"
}
},
"graphql-extensions": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.10.4.tgz",
"integrity": "sha512-lE6MroluEYocbR/ICwccv39w+Pz4cBPadJ11z1rJkbZv5wstISEganbDOwl9qN21rcZGiWzh7QUNxUiFUXXEDw==",
"dev": true,
"requires": {
"@apollographql/apollo-tools": "^0.4.0",
"apollo-server-env": "^2.4.3",
"apollo-server-types": "^0.2.5"
}
}
}
},
"apollo-server-types": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.2.4.tgz",
@ -1191,6 +1316,15 @@
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
"dev": true
},
"async-hook-jl": {
"version": "1.7.6",
"resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz",
"integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==",
"dev": true,
"requires": {
"stack-chain": "^1.3.7"
}
},
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
@ -1718,6 +1852,17 @@
"wrap-ansi": "^5.1.0"
}
},
"cls-hooked": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz",
"integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==",
"dev": true,
"requires": {
"async-hook-jl": "^1.7.6",
"emitter-listener": "^1.0.1",
"semver": "^5.4.1"
}
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -2106,6 +2251,15 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"emitter-listener": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
"integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==",
"dev": true,
"requires": {
"shimmer": "^1.2.0"
}
},
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
@ -5890,6 +6044,12 @@
"integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
"dev": true
},
"shimmer": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz",
"integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==",
"dev": true
},
"signal-exit": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
@ -6126,6 +6286,12 @@
"tweetnacl": "~0.14.0"
}
},
"stack-chain": {
"version": "1.3.7",
"resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz",
"integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=",
"dev": true
},
"stack-utils": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz",
@ -6705,6 +6871,15 @@
}
}
},
"typeorm-transactional-cls-hooked": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/typeorm-transactional-cls-hooked/-/typeorm-transactional-cls-hooked-0.1.8.tgz",
"integrity": "sha512-JNBuwThSNKbtT2CB6C5ZrwBgXLvzteNJQbAxqwHyJIEDTc/h4CSpRuN+8dHiJVI4Eq4wmbOrk720rvjhucIbYQ==",
"dev": true,
"requires": {
"cls-hooked": "^4.2.2"
}
},
"typescript": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz",

@ -3,17 +3,15 @@
"version": "0.0.1",
"description": "",
"main": "src/app.js",
"keywords": [],
"author": "",
"license": "ISC",
"scripts": {
"dev": "ts-node-dev --respawn src/bootstrap.ts",
"test": "jest --coverage",
"test:debug": "jest -i --verbose --detectOpenHandles",
"test:watch": "jest -i --watch",
"gen:key": "ssh-keygen -t rsa -b 2048 -f src/app/userResolver/auth/jwtRS256.key && openssl rsa -in src/app/userResolver/auth/wtRS256.key -pubout -outform PEM -out src/app/userResolver/auth/wtRS256.key.pub",
"cov": "jest --coverage",
"test": "jest -i --watch",
"debug": "ts-node-dev --inspect --respawn --transpileOnly src/bootstrap.ts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"apollo-server": "^2.9.3",
"apollo-server-express": "^2.9.4",
@ -33,11 +31,11 @@
"@types/jest": "^24.0.18",
"@types/jsonwebtoken": "^8.3.3",
"@types/node": "^12.7.5",
"class-transformer": "^0.2.3",
"isomorphic-fetch": "^2.2.1",
"apollo-server-testing": "^2.9.6",
"jest": "^24.9.0",
"ts-jest": "^24.1.0",
"ts-node-dev": "^1.0.0-pre.42",
"typeorm-transactional-cls-hooked": "^0.1.8",
"typescript": "^3.6.3"
}
}

@ -1,49 +0,0 @@
require("isomorphic-fetch")
import { ApolloServer } from "apollo-server-express"
import { getConnection } from "typeorm"
import { bootstrap } from "./app"
import { connectionOptionsforTesting } from "./app/schema"
import { signToken } from "./app/userResolver/auth"
import { User } from "./app/userResolver/User"
let server: ApolloServer
let port: number
beforeAll(async () => {
port = 4001
server = await bootstrap(connectionOptionsforTesting(), port)
})
describe("app should", () => {
it("accept auth header correctly", async () => {
const user = await User.create({
email: "email@email.com",
}).save()
let token = signToken({ userId: user.id })
const url = `http://localhost:${port}/graphql`
const rawResponse = await fetch(url, {
method: "post",
headers: {
"content-Type": "application/json",
authorization: "Bearer " + token,
},
body: JSON.stringify({
query: "{me{email}}",
}),
})
const response = await rawResponse.json()
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
me: { email: user.email },
})
await getConnection().synchronize(true)
await getConnection().close()
await server.stop()
})
})

@ -1,152 +0,0 @@
import { gql } from "apollo-server"
import { createConnection, getConnection } from "typeorm"
import { callSchema, connectionOptionsforTesting } from "./schema"
import { signToken, verifyToken } from "./userResolver/auth"
import { LoginTokens } from "./userResolver/LoginTokens"
import { User } from "./userResolver/User"
beforeAll(async () => {
return await createConnection(connectionOptionsforTesting())
})
afterAll(async () => {
return await getConnection().close()
})
afterEach(async () => {
return await getConnection().synchronize(true)
})
describe("resolver of user", () => {
describe("createUser mutation should", () => {
it("return email as it creates user with mutation", async () => {
const createUserMutation = gql`
mutation {
createUser(email: "email@email.com", password: "password") {
email
}
}
`
const response = await callSchema(createUserMutation)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
createUser: { email: "email@email.com" },
})
})
})
describe("users query should", () => {
it("return emails of registered users", async () => {
const usersQuery = gql`
query {
users {
email
}
}
`
const user = await User.create({
email: "email@email.com",
}).save()
const response = await callSchema(usersQuery)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
users: [{ email: user.email }],
})
})
})
describe("loginTokens query should", () => {
const loginTokensQuery = gql`
query {
loginTokens(email: "email@email.com", password: "good-password") {
accessToken
}
}
`
it("return error for non-existent user", async () => {
const response = await callSchema(loginTokensQuery)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
})
it("return error for bad password", async () => {
await User.create({
email: "email@email.com",
password: "BAD-password",
}).save()
const response = await callSchema(loginTokensQuery)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
})
it("return a valid access token with good credentials", async () => {
await User.create({
email: "email@email.com",
password: "good-password",
}).save()
const response = await callSchema(loginTokensQuery)
const accessToken = response.data!.loginTokens.accessToken
const loginTokens = new LoginTokens()
loginTokens.accessToken = accessToken
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({ loginTokens })
expect(verifyToken(accessToken)).toBeTruthy()
})
})
describe("me query should", () => {
const meQuery = gql`
query {
me {
email
}
}
`
it("return an error without a valid jwt token", async () => {
const context = {
req: {
headers: {
authorization: "Bearer INVALID-TOKEN",
},
},
}
const response = await callSchema(meQuery, context)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
})
it("return an user with a valid jwt token", async () => {
const user = await User.create({
email: "email@email.com",
}).save()
const context = {
req: {
headers: {
authorization: "Bearer " + signToken({ userId: user.id }),
},
},
}
const response = await callSchema(meQuery, context)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
me: { email: user.email },
})
})
})
})

@ -1,58 +0,0 @@
import { argon2id, hash, verify as argonVerify } from "argon2"
import { Request, Response } from "express"
import { readFileSync } from "fs"
import { sign, verify as jwtVerify } from "jsonwebtoken"
import { join } from "path"
import { AuthChecker } from "type-graphql"
let PRIVATE_KEY: Buffer
let PUBLIC_KEY: Buffer
export type Payload = {
userId: number
}
export interface MyContext {
req: Request
res: Response
payload?: Payload
}
export const hashPassword = async (password: string) =>
await hash(password, { type: argon2id })
export const comparePassword = async (hash: string, plain: string) =>
await argonVerify(hash, plain, { type: argon2id })
export const signToken = (payload: Payload) => {
if (!PRIVATE_KEY) {
PRIVATE_KEY = readKeyFile("jwtRS256.key")
}
return sign(payload, PRIVATE_KEY, { algorithm: "RS256" })
}
export const verifyToken = (token: string) => {
if (!PUBLIC_KEY) {
PUBLIC_KEY = readKeyFile("jwtRS256.key.pub")
}
return jwtVerify(token, PUBLIC_KEY)
}
export const customAuthChecker: AuthChecker<MyContext> = ({ context }) => {
try {
const authHeader = context.req.headers["authorization"]
const accessToken = authHeader!.split(" ")[1]
const payload = verifyToken(accessToken)
context.payload = payload as any
return true
} catch (error) {
throw new Error("the valid authorization header is required: " + error)
}
}
export const contextFunction = ({ req, res }: MyContext) => ({ req, res })
const readKeyFile = (fileName: string) => readFileSync(join(__dirname, "auth", fileName))

@ -1,18 +1,10 @@
require("dotenv").config()
import { ConnectionOptions } from "typeorm"
import { bootstrap } from "./app"
import { User } from "./app/userResolver/User"
import { createConnection } from "typeorm"
import { createServer } from "./server"
import { connectionOptions } from "./server/connection"
;(async () => {
await createConnection(connectionOptions())
let connectionOptions: ConnectionOptions = {
type: "postgres",
host: process.env.DB_HOST,
port: 5432,
database: process.env.DB_NAME,
username: process.env.DB_USER,
password: process.env.DB_PASS,
entities: [User],
synchronize: true,
logging: false,
}
bootstrap(connectionOptions, 4000)
const port = 4000
await createServer(port)
})()

@ -0,0 +1,13 @@
require("dotenv").config()
import { createConnection } from "typeorm"
import { testingConnectionOptions } from "./server/testing"
module.exports = async function() {
const connection = await createConnection({
...testingConnectionOptions(),
dropSchema: true,
synchronize: true,
})
await connection.close()
}

@ -0,0 +1,26 @@
import { ApolloServer } from "apollo-server-express"
import { createTestClient } from "apollo-server-testing"
import { createConnection, getConnection } from "typeorm"
import { createServer } from "./server"
import { testingConnectionOptions } from "./server/testing"
import auth = require("./server/userResolver/auth")
describe("app should", () => {
it("call the context function on apollo server", async () => {
const spy = jest.spyOn(auth, "contextFunction")
await createConnection(testingConnectionOptions())
const port = 4001
const server = (await createServer(port)) as any
const { query } = createTestClient(server)
await query({ query: "{me{email}}" })
expect(server).toBeInstanceOf(ApolloServer)
expect(spy).toHaveBeenCalledTimes(1)
spy.mockRestore()
await server.stop()
await getConnection().close()
})
})

@ -1,12 +1,9 @@
import express = require("express")
import { ApolloServer } from "apollo-server-express"
import { ConnectionOptions, createConnection } from "typeorm"
import { createSchema } from "./app/schema"
import { contextFunction } from "./app/userResolver/auth"
export const bootstrap = async (connectionOptions: ConnectionOptions, port: number) => {
await createConnection(connectionOptions)
import { createSchema } from "./server/schema"
import { contextFunction } from "./server/userResolver/auth"
export const createServer = async (port: number) => {
const server = new ApolloServer({
schema: await createSchema(),
playground: true,

@ -0,0 +1,178 @@
import { gql } from "apollo-server"
import { Request, Response } from "express"
import { createConnection, getConnection } from "typeorm"
import {
initializeTransactionalContext,
patchTypeORMRepositoryWithBaseRepository,
} from "typeorm-transactional-cls-hooked"
import { callSchema } from "./schema"
import { runInTransaction, testingConnectionOptions } from "./testing"
import { ContextInterface, signAccessToken, verifyAccessToken } from "./userResolver/auth"
import { LoginTokens } from "./userResolver/LoginTokens"
import { User } from "./userResolver/User"
beforeAll(async () => {
initializeTransactionalContext()
patchTypeORMRepositoryWithBaseRepository()
await createConnection(testingConnectionOptions())
})
afterAll(async () => {
await getConnection().close()
})
describe("resolver of user", () => {
describe("createUser mutation should", () => {
it(
"return email as it creates user with mutation",
runInTransaction(async () => {
const createUserMutation = gql`
mutation {
createUser(email: "email@email.com", password: "password") {
email
}
}
`
const response = await callSchema(createUserMutation)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
createUser: { email: "email@email.com" },
})
})
)
})
describe("users query should", () => {
it(
"return emails of registered users",
runInTransaction(async () => {
const usersQuery = gql`
query {
users {
email
}
}
`
const user = await User.create({
email: "email@email.com",
}).save()
const response = await callSchema(usersQuery)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
users: [{ email: user.email }],
})
})
)
})
describe("loginTokens query should", () => {
const loginTokensQuery = gql`
query {
loginTokens(email: "email@email.com", password: "good-password") {
accessToken
}
}
`
it(
"return error for non-existent user",
runInTransaction(async () => {
const response = await callSchema(loginTokensQuery)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
})
)
it(
"return error for bad password",
runInTransaction(async () => {
await User.create({
email: "email@email.com",
password: "BAD-password",
}).save()
const response = await callSchema(loginTokensQuery)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
})
)
it(
"return a valid access token with good credentials",
runInTransaction(async () => {
await User.create({
email: "email@email.com",
password: "good-password",
}).save()
const response = await callSchema(loginTokensQuery)
const accessToken = response.data!.loginTokens.accessToken
const loginTokens = new LoginTokens()
loginTokens.accessToken = accessToken
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({ loginTokens })
expect(verifyAccessToken(accessToken)).toBeTruthy()
})
)
})
describe("me query should", () => {
const meQuery = gql`
query {
me {
email
}
}
`
it(
"return an error without a valid jwt token",
runInTransaction(async () => {
const contextWithInvalidToken = contextWithAuthHeader(
"Bearer INVALID-TOKEN"
)
const response = await callSchema(meQuery, contextWithInvalidToken)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
})
)
it(
"return an user with a valid jwt token",
runInTransaction(async () => {
const user = await User.create({
email: "email@email.com",
}).save()
const contextWithValidToken = contextWithAuthHeader(
"Bearer " + signAccessToken({ userId: user.id })
)
const response = await callSchema(meQuery, contextWithValidToken)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
me: { email: user.email },
})
})
)
})
})
const contextWithAuthHeader = (header: string): ContextInterface => ({
req: {
headers: {
authorization: header,
},
} as Request,
res: {} as Response,
})

@ -1,6 +1,6 @@
import "reflect-metadata"
import { Arg, Authorized, Ctx, Mutation, Query } from "type-graphql"
import { comparePassword, MyContext, signToken } from "./userResolver/auth"
import { comparePassword, ContextInterface, signAccessToken } from "./userResolver/auth"
import { LoginTokens } from "./userResolver/LoginTokens"
import { User } from "./userResolver/User"
@ -22,7 +22,7 @@ export class UserResolver {
throw new Error()
}
const accessToken = signToken({ userId: user!.id })
const accessToken = signAccessToken({ userId: user!.id })
return {
accessToken,
@ -34,7 +34,7 @@ export class UserResolver {
@Query(() => User)
@Authorized()
async me(@Ctx() { payload }: MyContext) {
async me(@Ctx() { payload }: ContextInterface) {
const id = payload!.userId
const user = await User.findOne({ where: { id } })

@ -0,0 +1,15 @@
import { ConnectionOptions } from "typeorm"
import { User } from "./userResolver/User"
export const connectionOptions = (): ConnectionOptions => ({
type: "postgres",
host: process.env.DB_HOST as string,
port: parseInt(process.env.DB_PORT as string),
database: process.env.DB_NAME as string,
username: process.env.DB_USER as string,
password: process.env.DB_PASS as string,
entities: [User],
dropSchema: false,
synchronize: false,
logging: false,
})

@ -1,14 +1,13 @@
require("dotenv").config()
import { DocumentNode, graphql, GraphQLSchema } from "graphql"
import { buildSchema } from "type-graphql"
import { ConnectionOptions } from "typeorm"
import { UserResolver } from "./UserResolver"
import { customAuthChecker } from "./userResolver/auth"
import { User } from "./userResolver/User"
import { ContextInterface } from "./userResolver/ContextInterface"
let schema: GraphQLSchema
export const callSchema = async (document: DocumentNode, context?: any) => {
export const callSchema = async (document: DocumentNode, context?: ContextInterface) => {
if (!schema) {
schema = await createSchema()
}
@ -25,19 +24,3 @@ export const createSchema = () =>
resolvers: [UserResolver],
authChecker: customAuthChecker,
})
export const connectionOptionsforTesting = () => {
let connectionOptions: ConnectionOptions = {
type: "postgres",
host: process.env.DB_HOST,
port: 5432,
database: "testing",
username: process.env.DB_USER,
password: process.env.DB_PASS,
entities: [User],
synchronize: true,
logging: false,
}
return connectionOptions
}

@ -0,0 +1,40 @@
import { ConnectionOptions } from "typeorm"
import { Propagation, Transactional } from "typeorm-transactional-cls-hooked"
import { connectionOptions } from "./connection"
export const testingConnectionOptions = () => {
const database = process.env.DB_NAME_TESING as string
return { ...connectionOptions(), database } as ConnectionOptions
}
type RunFunction = () => Promise<void> | void
class RollbackError extends Error {
constructor(message: string) {
super(message)
this.name = this.constructor.name
}
}
class TransactionCreator {
@Transactional({ propagation: Propagation.REQUIRED })
static async run(func: RunFunction) {
await func()
throw new RollbackError(`This is thrown to cause a rollback on the transaction.`)
}
}
export function runInTransaction(func: RunFunction) {
return async () => {
try {
await TransactionCreator.run(func)
} catch (e) {
/* istanbul ignore next */
if (!(e instanceof RollbackError)) {
throw e
}
}
}
}

@ -0,0 +1,43 @@
import { argon2id, hash, verify as argonVerify } from "argon2"
import { Request, Response } from "express"
import { sign, verify as jwtVerify } from "jsonwebtoken"
import { AuthChecker } from "type-graphql"
export type Payload = {
userId: number
}
export interface ContextInterface {
req: Request
res: Response
payload?: Payload
}
export const hashPassword = async (password: string) =>
await hash(password, { type: argon2id })
export const comparePassword = async (hash: string, plain: string) =>
await argonVerify(hash, plain, { type: argon2id })
export const signAccessToken = (payload: Payload) => {
return sign(payload, process.env.ACCESS_SECRET!)
}
export const verifyAccessToken = (token: string) => {
return jwtVerify(token, process.env.ACCESS_SECRET!)
}
export const customAuthChecker: AuthChecker<ContextInterface> = ({ context }) => {
try {
const authHeader = context.req.headers["authorization"]
const accessToken = authHeader!.split(" ")[1]
const payload = verifyAccessToken(accessToken)
context.payload = payload as any
return true
} catch (error) {
throw new Error("the valid authorization header is required: " + error)
}
}
export const contextFunction = ({ req, res }: ContextInterface) => ({ req, res })
Loading…
Cancel
Save