Skip to content
This repository was archived by the owner on Apr 17, 2023. It is now read-only.

Initial test structure #7

Merged
merged 3 commits into from
Dec 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,8 @@ reports/
junit.xml

# Node modules
node_modules/
node_modules/

# Intellij files
.idea
*.iml
4,996 changes: 3,781 additions & 1,215 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"name": "@aerogear/apollo-voyager-server-monorepo",
"private": true,
"scripts": {
"test": "echo ok",
"bootstrap": "lerna bootstrap --no-ci",
"test": "lerna exec -- npm test",
"bootstrap": "lerna bootstrap --no-ci",
"clean": "lerna clean --yes",
"lint": "tslint '*/*/src/**/*.ts' '*/*/test/**/*.ts'",
"format": "tslint '*/*/src/**/*.ts' '*/*/test/**/*.ts' --fix",
"lint": "tslint '*/*/src/**/*.ts' --exclude '*/*/src/**/*.test.ts'",
"format": "tslint '*/*/src/**/*.ts' --fix --force > /dev/null",
Copy link
Contributor

Choose a reason for hiding this comment

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

Just wondering what's the > /dev/null thing for here? Is this going to suppress error output?

Copy link
Member Author

Choose a reason for hiding this comment

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

Short answer yes:

Long answer

2 scripts:

  • lint: Complains with "ERROR: bla bla" messages, doesn't fix, exits with non-zero in case of error, only used in non-test code
  • format: It runs on test and non-test code. Don't want to see "ERROR: bla bla" in the output because the tests might have too many of them, so that's why I put "> dev/null". I don't want it to return non-zero, so I put "--force".

"cleanInstall": "lerna exec npm install --ignore-scripts",
"compile": "lerna exec -- npm run compile",
"compile:clean": "lerna exec -- npm run clean-compile",
Expand All @@ -20,8 +20,10 @@
"@types/keycloak-connect": "^4.5.0",
"@types/node": "^10.12.10",
"@types/pino": "^5.8.2",
"ava": "1.0.0-rc.2",
"graphql": "^0.13.2",
"lerna": "^3.4.3",
"ts-node": "^7.0.1",
"tslint": "^5.11.0",
"typescript": "^3.1.6"
}
Expand Down
20 changes: 17 additions & 3 deletions packages/apollo-voyager-context/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,23 @@
"scripts": {
"compile": "tsc --build tsconfig.json",
"watch": "tsc --build tsconfig.json --watch",
"test": "echo \"Error: no test specified\" && exit 1",
"compile:clean": "tsc --build tsconfig.json --clean"
"compile:clean": "tsc --build tsconfig.json --clean",
"test": "ava '*.test.ts' '**/*.test.ts'"
},
"devDependencies": {
"ava": "1.0.0-rc.2",
"ts-node": "^7.0.1",
"typescript": "^3.1.6"
},
"author": "AeroGear Team<[email protected]>",
"license": "Apache-2.0"
"license": "Apache-2.0",
"ava": {
"compileEnhancements": false,
"extensions": [
"ts"
],
"require": [
"ts-node/register"
]
}
}
5 changes: 5 additions & 0 deletions packages/apollo-voyager-context/src/deleteme.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import test from 'ava'

test('DELETE ME DUMMY', t => {
Copy link
Contributor

Choose a reason for hiding this comment

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

😄

t.is(true, true)
})
13 changes: 12 additions & 1 deletion packages/apollo-voyager-keycloak/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"compile": "tsc --build tsconfig.json",
"watch": "tsc --build tsconfig.json --watch",
"compile:clean": "tsc --build tsconfig.json --clean",
"test": "echo \"Error: run tests from root\" && exit 1"
"test": "ava '*.test.ts' '**/*.test.ts'"
},
"dependencies": {
"@aerogear/apollo-voyager-server": "^1.0.0",
Expand All @@ -31,12 +31,23 @@
"pino": "^5.9.0"
},
"devDependencies": {
"ava": "1.0.0-rc.2",
"@types/express-session": "^1.15.11",
"@types/graphql": "^14.0.3",
"@types/joi": "^14.0.0",
"@types/keycloak-connect": "^4.5.0",
"@types/node": "^10.12.10",
"@types/pino": "^5.8.2",
"ts-node": "^7.0.1",
"typescript": "^3.1.6"
},
"ava": {
"compileEnhancements": false,
"extensions": [
"ts"
],
"require": [
"ts-node/register"
]
}
}
249 changes: 249 additions & 0 deletions packages/apollo-voyager-keycloak/src/schemaDirectives/hasRole.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import test from 'ava'

import { GraphQLSchema } from 'graphql'
import { VisitableSchemaType } from 'graphql-tools/dist/schemaVisitor'
import { HasRoleDirective } from './hasRole'

import {KeycloakAuthContextProvider} from '../AuthContextProvider'

const createHasRoleDirective = (directiveArgs: any) => {
return new HasRoleDirective({
name: 'testHasRoleDirective',
args: directiveArgs,
visitedType: ({} as VisitableSchemaType),
schema: ({} as GraphQLSchema),
context: []
})
}

test('context.auth.hasRole() is called', async (t) => {
t.plan(3)
const directiveArgs = {
role: 'admin'
}

const directive = createHasRoleDirective(directiveArgs)

const field = {
resolve: (root: any, args: any, context: any, info: any) => {
t.pass()
},
name: 'testField'
}

directive.visitFieldDefinition(field)

const root = {}
const args = {}
const req = {
kauth: {
grant: {
access_token: {
hasRole: (role: string) => {
t.pass()
t.deepEqual(role, directiveArgs.role)
return true
}
}
}
}
}
const context = {
request: req,
auth: new KeycloakAuthContextProvider(req)
}

const info = {
parentType: {
name: 'testParent'
}
}

await field.resolve(root, args, context, info)
})

test('visitFieldDefinition accepts an array of roles', async (t) => {
t.plan(4)
const directiveArgs = {
role: ['foo', 'bar', 'baz']
}

const directive = createHasRoleDirective(directiveArgs)

const field = {
resolve: (root: any, args: any, context: any, info: any) => {
t.pass()
},
name: 'testField'
}

directive.visitFieldDefinition(field)

const root = {}
const args = {}
const req = {
kauth: {
grant: {
access_token: {
hasRole: (role: string) => {
t.log(`checking has role ${role}`)
t.pass()
return (role === 'baz') // this makes sure it doesn't return true instantly
}
}
}
}
}
const context = {
request: req,
auth: new KeycloakAuthContextProvider(req)
}

const info = {
parentType: {
name: 'testParent'
}
}

await field.resolve(root, args, context, info)
})

test('if there is no authentication, then an error is returned and the original resolver will not execute', async (t) => {
const directiveArgs = {
role: 'admin'
}

const directive = createHasRoleDirective(directiveArgs)

const field = {
resolve: (root: any, args: any, context: any, info: any) => {
return new Promise((resolve, reject) => {
t.fail('the original resolver should never be called when an auth error is thrown')
return reject(new Error('the original resolver should never be called when an auth error is thrown'))
})
},
name: 'testField'
}

directive.visitFieldDefinition(field)

const root = {}
const args = {}
const req = {}
const context = {
request: req,
auth: new KeycloakAuthContextProvider(req)
}

const info = {
parentType: {
name: 'testParent'
}
}

await t.throwsAsync(async () => {
await field.resolve(root, args, context, info)
}, `Unable to find authentication. Authorization is required for field ${field.name} on parent ${info.parentType.name}. Must have one of the following roles: [${directiveArgs.role}]`)
})

test('if token does not have the required role, then an error is returned and the original resolver will not execute', async (t) => {
const directiveArgs = {
role: 'admin'
}

const directive = createHasRoleDirective(directiveArgs)

const field = {
resolve: (root: any, args: any, context: any, info: any) => {
return new Promise((resolve, reject) => {
t.fail('the original resolver should never be called when an auth error is thrown')
return reject(new Error('the original resolver should never be called when an auth error is thrown'))
})
},
name: 'testField'
}

directive.visitFieldDefinition(field)

const root = {}
const args = {}
const req = {
kauth: {
grant: {
access_token: {
hasRole: (role: string) => {
t.deepEqual(role, directiveArgs.role)
return false
}
}
}
}
}
const context = {
request: req,
auth: new KeycloakAuthContextProvider(req)
}

const info = {
parentType: {
name: 'testParent'
}
}

await t.throwsAsync(async () => {
await field.resolve(root, args, context, info)
}, `user is not authorized for field ${field.name} on parent ${info.parentType.name}. Must have one of the following roles: [${directiveArgs.role}]`)
})

test('if hasRole arguments are invalid, visitSchemaDirective does not throw, but field.resolve will return a generic error to the user and original resolver will not be called', async (t) => {
const directiveArgs = {
role: 'admin',
some: 'unknown arg'
}

const directive = createHasRoleDirective(directiveArgs)

const field = {
resolve: (root: any, args: any, context: any, info: any) => {
return new Promise((resolve, reject) => {
t.fail('the original resolver should never be called when an auth error is thrown')
return reject(new Error('the original resolver should never be called when an auth error is thrown'))
})
},
name: 'testField'
}

t.notThrows(() => {
directive.visitFieldDefinition(field)
})

const root = {}
const args = {}
const req = {
id: '123',
kauth: {
grant: {
access_token: {
hasRole: (role: string) => {
t.deepEqual(role, directiveArgs.role)
return false
}
}
}
}
}
const context = {
request: req,
auth: new KeycloakAuthContextProvider(req)
}

const info = {
parentType: {
name: 'testParent'
}
}

await t.throwsAsync(async () => {
await field.resolve(root, args, context, info)
})
})
15 changes: 14 additions & 1 deletion packages/apollo-voyager-keycloak/src/schemaDirectives/hasRole.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import { ForbiddenError } from 'apollo-server-express'
import { defaultFieldResolver } from 'graphql'
import {defaultFieldResolver, GraphQLSchema} from 'graphql'
import { SchemaDirectiveVisitor } from 'graphql-tools'
import Joi from 'joi'
// import newInternalServerError from '???' // need to figure out where this comes from
import pino from 'pino' // also need to figure out where this comes from

import { VisitableSchemaType } from 'graphql-tools/dist/schemaVisitor'

const log = pino()

export class HasRoleDirective extends SchemaDirectiveVisitor {

constructor (config: {
name: string
args: { [name: string]: any }
visitedType: VisitableSchemaType
schema: GraphQLSchema
context: { [key: string]: any }
}) {
// see https://github.com/apollographql/graphql-tools/issues/837
super(config as any)
}

public visitFieldDefinition (field: any) {
const { resolve = defaultFieldResolver } = field
const { error, value } = this.validateArgs()
Expand Down
Loading