chore(server): migrate remaining tests to TS (#4772)

* auth tests migrated

* core tests

* pwdreset

* authz tests
This commit is contained in:
Kristaps Fabians Geikins
2025-05-20 14:24:48 +03:00
committed by GitHub
parent eabfab2555
commit d2f2d95bb5
7 changed files with 386 additions and 281 deletions
+2
View File
@@ -29,5 +29,7 @@ secret:
name: setup/keycloak/speckle-realm.json - secret for dev keycloak
- match: 2e1b3675a4049cd39fe6db081735f747730969071528270800f00fa98720d198
name: setup/keycloak/speckle-realm.json - algorithm name
- match: 7aa3f40885a6914c798c95568f04d840f50164b4e2ba632da0edb9602ca5609b
name: apps.graphql.spec.ts - fake password
version: 2
@@ -1,68 +1,59 @@
/* eslint-disable camelcase */
/* istanbul ignore file */
const chai = require('chai')
import chai from 'chai'
const expect = chai.expect
const {
import {
createBareToken,
createAppTokenFactory,
createPersonalAccessTokenFactory
} = require('@/modules/core/services/tokens')
const { beforeEachContext, initializeTestServer } = require('@/test/hooks')
const { Scopes } = require('@speckle/shared')
const {
} from '@/modules/core/services/tokens'
import { beforeEachContext, initializeTestServer } from '@/test/hooks'
import { Scopes } from '@speckle/shared'
import {
createAuthorizationCodeFactory,
getAuthorizationCodeFactory,
deleteAuthorizationCodeFactory,
getAppFactory,
createRefreshTokenFactory
} = require('@/modules/auth/repositories/apps')
const { db } = require('@/db/knex')
const {
createAppTokenFromAccessCodeFactory
} = require('@/modules/auth/services/serverApps')
const {
} from '@/modules/auth/repositories/apps'
import { db } from '@/db/knex'
import { createAppTokenFromAccessCodeFactory } from '@/modules/auth/services/serverApps'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} = require('@/modules/core/repositories/userEmails')
const {
requestNewEmailVerificationFactory
} = require('@/modules/emails/services/verification/request')
const {
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} = require('@/modules/core/repositories/users')
const {
deleteOldAndInsertNewVerificationFactory
} = require('@/modules/emails/repositories')
const { renderEmail } = require('@/modules/emails/services/emailRendering')
const { sendEmail } = require('@/modules/emails/services/sending')
const { createUserFactory } = require('@/modules/core/services/users/management')
const {
validateAndCreateUserEmailFactory
} = require('@/modules/core/services/userEmails')
const {
finalizeInvitedServerRegistrationFactory
} = require('@/modules/serverinvites/services/processing')
const {
} from '@/modules/core/repositories/users'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} = require('@/modules/serverinvites/repositories/serverInvites')
const {
} from '@/modules/serverinvites/repositories/serverInvites'
import {
storeApiTokenFactory,
storeTokenScopesFactory,
storeTokenResourceAccessDefinitionsFactory,
storeUserServerAppTokenFactory,
storePersonalApiTokenFactory
} = require('@/modules/core/repositories/tokens')
const { getServerInfoFactory } = require('@/modules/core/repositories/server')
const { getEventBus } = require('@/modules/shared/services/eventBus')
} from '@/modules/core/repositories/tokens'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { BasicTestUser } from '@/test/authHelper'
let sendRequest
let sendRequest: Awaited<ReturnType<typeof initializeTestServer>>['sendRequest']
const createAppToken = createAppTokenFactory({
storeApiToken: storeApiTokenFactory({ db }),
@@ -120,10 +111,10 @@ const createPersonalAccessToken = createPersonalAccessTokenFactory({
})
describe('GraphQL @apps-api', () => {
let testUser
let testUser2
let testToken
let testToken2
let testUser: BasicTestUser
let testUser2: BasicTestUser
let testToken: string
let testToken2: string
before(async () => {
const ctx = await beforeEachContext()
@@ -131,7 +122,8 @@ describe('GraphQL @apps-api', () => {
testUser = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@example.org',
password: 'wtfwtfwtf'
password: 'wtfwtfwtf',
id: ''
}
testUser.id = await createUser(testUser)
@@ -144,7 +136,8 @@ describe('GraphQL @apps-api', () => {
testUser2 = {
name: 'Mr. Mac',
email: 'steve@jobs.com',
password: 'wtfwtfwtf'
password: 'wtfwtfwtf',
id: ''
}
testUser2.id = await createUser(testUser2)
@@ -155,8 +148,8 @@ describe('GraphQL @apps-api', () => {
])}`
})
let testAppId
let testApp
let testAppId: string
let testApp: { secret: string }
it('Should create an app', async () => {
const query =
@@ -1,17 +1,17 @@
/* istanbul ignore file */
const expect = require('chai').expect
import { expect } from 'chai'
const {
import {
createBareToken,
createAppTokenFactory,
validateTokenFactory
} = require(`@/modules/core/services/tokens`)
const { beforeEachContext } = require(`@/test/hooks`)
} from '@/modules/core/services/tokens'
import { beforeEachContext } from '@/test/hooks'
const { Scopes } = require('@/modules/core/helpers/mainConstants')
const { knex } = require('@/db/knex')
const cryptoRandomString = require('crypto-random-string')
const {
import { Scopes } from '@/modules/core/helpers/mainConstants'
import { knex } from '@/db/knex'
import cryptoRandomString from 'crypto-random-string'
import {
getAppFactory,
updateDefaultAppFactory,
getAllPublicAppsFactory,
@@ -26,43 +26,35 @@ const {
getRefreshTokenFactory,
revokeRefreshTokenFactory,
getTokenAppInfoFactory
} = require('@/modules/auth/repositories/apps')
const {
} from '@/modules/auth/repositories/apps'
import {
createAppTokenFromAccessCodeFactory,
refreshAppTokenFactory
} = require('@/modules/auth/services/serverApps')
const {
} from '@/modules/auth/services/serverApps'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} = require('@/modules/core/repositories/userEmails')
const {
requestNewEmailVerificationFactory
} = require('@/modules/emails/services/verification/request')
const {
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory,
getUserRoleFactory
} = require('@/modules/core/repositories/users')
const {
deleteOldAndInsertNewVerificationFactory
} = require('@/modules/emails/repositories')
const { renderEmail } = require('@/modules/emails/services/emailRendering')
const { sendEmail } = require('@/modules/emails/services/sending')
const { createUserFactory } = require('@/modules/core/services/users/management')
const {
validateAndCreateUserEmailFactory
} = require('@/modules/core/services/userEmails')
const {
finalizeInvitedServerRegistrationFactory
} = require('@/modules/serverinvites/services/processing')
const {
} from '@/modules/core/repositories/users'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} = require('@/modules/serverinvites/repositories/serverInvites')
const {
} from '@/modules/serverinvites/repositories/serverInvites'
import {
storeApiTokenFactory,
storeTokenScopesFactory,
storeTokenResourceAccessDefinitionsFactory,
@@ -72,9 +64,16 @@ const {
getTokenScopesByIdFactory,
getTokenResourceAccessDefinitionsByIdFactory,
updateApiTokenFactory
} = require('@/modules/core/repositories/tokens')
const { getServerInfoFactory } = require('@/modules/core/repositories/server')
const { getEventBus } = require('@/modules/shared/services/eventBus')
} from '@/modules/core/repositories/tokens'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { BasicTestUser } from '@/test/authHelper'
import { AppScopes, ensureError } from '@speckle/shared'
import { ValidTokenResult } from '@/modules/core/helpers/types'
import {
DefaultAppIds,
DefaultAppWithUnwrappedScopes
} from '@/modules/auth/defaultApps'
const db = knex
const getApp = getAppFactory({ db: knex })
@@ -156,10 +155,11 @@ const validateToken = validateTokenFactory({
})
describe('Services @apps-services', () => {
const actor = {
const actor: BasicTestUser = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@example.org',
password: 'wtfwtfwtf'
password: 'wtfwtfwtf',
id: ''
}
before(async () => {
@@ -173,7 +173,8 @@ describe('Services @apps-services', () => {
name: testAppName,
public: true,
scopes: [Scopes.Streams.Read],
redirectUrl: 'http://127.0.0.1:1335'
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id
})
expect(res).to.have.property('id')
@@ -183,7 +184,7 @@ describe('Services @apps-services', () => {
expect(res.secret).to.be.a('string')
const app = await getApp({ id: res.id })
expect(app.id).to.equal(res.id)
expect(app?.id).to.equal(res.id)
})
it('Should get all the public apps on this server', async () => {
@@ -193,7 +194,12 @@ describe('Services @apps-services', () => {
})
it('Should fail to register an app with no scopes', async () => {
await createApp({ name: 'test application2', redirectUrl: 'http://127.0.0.1:1335' })
await createApp({
name: 'test application2',
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id,
scopes: undefined as unknown as AppScopes[]
})
.then(() => {
throw new Error('this should have been rejected')
})
@@ -207,7 +213,8 @@ describe('Services @apps-services', () => {
name: cryptoRandomString({ length: 10 }),
public: true,
scopes: [Scopes.Streams.Read],
redirectUrl: 'http://127.0.0.1:1335'
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id
})
const res = await updateApp({
app: {
@@ -219,10 +226,10 @@ describe('Services @apps-services', () => {
expect(res).to.be.a('string')
const app = await getApp({ id: myTestApp.id })
expect(app.name).to.equal('updated test application')
expect(app.scopes).to.be.an('array')
expect(app.scopes.map((s) => s.name)).to.include(Scopes.Users.Read)
expect(app.scopes.map((s) => s.name)).to.include(Scopes.Streams.Read)
expect(app?.name).to.equal('updated test application')
expect(app?.scopes).to.be.an('array')
expect(app?.scopes.map((s) => s.name)).to.include(Scopes.Users.Read)
expect(app?.scopes.map((s) => s.name)).to.include(Scopes.Streams.Read)
})
const challenge = 'random'
@@ -232,7 +239,8 @@ describe('Services @apps-services', () => {
name: cryptoRandomString({ length: 10 }),
public: true,
scopes: [Scopes.Streams.Read],
redirectUrl: 'http://127.0.0.1:1335'
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id
})
const authorizationCode = await createAuthorizationCode({
appId: myTestApp.id,
@@ -247,7 +255,8 @@ describe('Services @apps-services', () => {
name: cryptoRandomString({ length: 10 }),
public: true,
scopes: [Scopes.Streams.Read],
redirectUrl: 'http://127.0.0.1:1335'
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id
})
const authorizationCode = await createAuthorizationCode({
appId: myTestApp.id,
@@ -267,7 +276,7 @@ describe('Services @apps-services', () => {
expect(response).to.have.property('refreshToken')
expect(response.refreshToken).to.be.a('string')
const validation = await validateToken(response.token)
const validation = (await validateToken(response.token)) as ValidTokenResult
expect(validation.valid).to.equal(true)
expect(validation.userId).to.equal(actor.id)
expect(validation.scopes[0]).to.equal(Scopes.Streams.Read)
@@ -278,7 +287,8 @@ describe('Services @apps-services', () => {
name: cryptoRandomString({ length: 10 }),
public: true,
scopes: [Scopes.Streams.Read],
redirectUrl: 'http://127.0.0.1:1335'
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id
})
const authorizationCode = await createAuthorizationCode({
appId: myTestApp.id,
@@ -301,14 +311,13 @@ describe('Services @apps-services', () => {
const res = await refreshAppToken({
refreshToken: tokenCreateResponse.refreshToken,
appId: myTestApp.id,
appSecret: myTestApp.secret,
userId: actor.id
appSecret: myTestApp.secret
})
expect(res.token).to.be.a('string')
expect(res.refreshToken).to.be.a('string')
const validation = await validateToken(res.token)
const validation = (await validateToken(res.token)) as ValidTokenResult
expect(validation.valid).to.equal(true)
expect(validation.userId).to.equal(actor.id)
})
@@ -318,7 +327,8 @@ describe('Services @apps-services', () => {
name: cryptoRandomString({ length: 10 }),
public: true,
scopes: [Scopes.Streams.Read],
redirectUrl: 'http://127.0.0.1:1335'
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id
})
const unusedAccessCode = await createAuthorizationCode({
appId: myTestApp.id,
@@ -385,8 +395,8 @@ describe('Services @apps-services', () => {
it(`Should get the default app: ${speckleAppId}`, async () => {
const app = await getApp({ id: speckleAppId })
expect(app).to.be.an('object')
expect(app.redirectUrl).to.be.a('string')
expect(app.scopes).to.be.a('array')
expect(app?.redirectUrl).to.be.a('string')
expect(app?.scopes).to.be.a('array')
})
it(`Should not invalidate tokens, refresh tokens and access codes for default app: ${speckleAppId}, if updated`, async () => {
const [unusedAccessCode, usedAccessCode] = await Promise.all([
@@ -418,14 +428,14 @@ describe('Services @apps-services', () => {
await updateDefaultApp(
{
name: 'updated test application',
id: speckleAppId,
id: speckleAppId as DefaultAppIds,
scopes: newScopes
},
existingApp
} as DefaultAppWithUnwrappedScopes,
existingApp!
)
const updatedApp = await getApp({ id: speckleAppId })
expect(updatedApp.scopes.map((s) => s.name)).to.equalInAnyOrder(newScopes)
expect(updatedApp?.scopes.map((s) => s.name)).to.deep.equalInAnyOrder(newScopes)
const validationResponse = await validateToken(apiTokenResponse.token)
expect(validationResponse.valid).to.equal(true)
@@ -447,7 +457,7 @@ describe('Services @apps-services', () => {
expect(appToken.token).to.exist
expect(appToken.refreshToken).to.exist
const apiTokens = await knex('user_server_app_tokens')
const apiTokens = (await knex('user_server_app_tokens')
.join(
'token_scopes',
'user_server_app_tokens.tokenId',
@@ -456,7 +466,7 @@ describe('Services @apps-services', () => {
)
.where({
appId: speckleAppId
})
})) as { scopeName: string }[]
expect(newScopes).to.include.members(apiTokens.map((t) => t.scopeName))
})
@@ -471,19 +481,21 @@ describe('Services @apps-services', () => {
name: 'updated test application',
id: speckleAppId,
scopes: ['aWeird:Scope']
},
existingApp
} as unknown as DefaultAppWithUnwrappedScopes,
existingApp!
)
throw new Error('This should have failed')
} catch (err) {
// check that the weird:Scope violates a foreign key constraint...
// leaky abstractions i know, but no better way to test this for now
expect(err.message).to.contain('server_apps_scopes_scopename_foreign')
expect(ensureError(err).message).to.contain(
'server_apps_scopes_scopename_foreign'
)
}
const notUpdatedApp = await getApp({ id: speckleAppId })
// check that no harm was done
expect(notUpdatedApp.name).to.equal(existingApp.name)
expect(notUpdatedApp.scopes).to.equalInAnyOrder(existingApp.scopes)
expect(notUpdatedApp?.name).to.equal(existingApp?.name)
expect(notUpdatedApp?.scopes).to.deep.equalInAnyOrder(existingApp?.scopes)
})
it('Should revoke access for a given user', async () => {
@@ -491,12 +503,14 @@ describe('Services @apps-services', () => {
name: cryptoRandomString({ length: 10 }),
public: true,
scopes: [Scopes.Streams.Read],
redirectUrl: 'http://127.0.0.1:1335'
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id
})
const secondUser = {
const secondUser: BasicTestUser = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie.wow@example.org',
password: 'wtfwtfwtf'
password: 'wtfwtfwtf',
id: ''
}
secondUser.id = await createUser(secondUser)
@@ -557,7 +571,8 @@ describe('Services @apps-services', () => {
name: cryptoRandomString({ length: 10 }),
public: true,
scopes: [Scopes.Streams.Read],
redirectUrl: 'http://127.0.0.1:1335'
redirectUrl: 'http://127.0.0.1:1335',
authorId: actor.id
})
const res = await deleteApp({ id: myTestApp.id })
expect(res).to.equal(1)
@@ -1,9 +1,9 @@
/* istanbul ignore file */
const expect = require('chai').expect
import { expect } from 'chai'
const { init } = require('@/app')
const { knex } = require('@/db/knex')
const { beforeEachContext } = require('@/test/hooks')
import { init } from '@/app'
import { knex } from '@/db/knex'
import { beforeEachContext } from '@/test/hooks'
// NOTE:
// These tests check that the initialization routine of the whole server
@@ -1,45 +1,38 @@
const request = require('supertest')
import request from 'supertest'
const { knex } = require('@/db/knex')
const ResetTokens = () => knex('pwdreset_tokens')
import { knex } from '@/db/knex'
const { beforeEachContext } = require('@/test/hooks')
const { localAuthRestApi } = require('@/modules/auth/tests/helpers/registration')
const { expectToThrow } = require('@/test/assertionHelper')
const { expect } = require('chai')
const {
import { beforeEachContext } from '@/test/hooks'
import { localAuthRestApi } from '@/modules/auth/tests/helpers/registration'
import { expectToThrow } from '@/test/assertionHelper'
import { expect } from 'chai'
import {
findEmailFactory,
createUserEmailFactory,
ensureNoPrimaryEmailForUserFactory
} = require('@/modules/core/repositories/userEmails')
const {
requestNewEmailVerificationFactory
} = require('@/modules/emails/services/verification/request')
const {
} from '@/modules/core/repositories/userEmails'
import { requestNewEmailVerificationFactory } from '@/modules/emails/services/verification/request'
import {
getUserFactory,
storeUserFactory,
countAdminUsersFactory,
storeUserAclFactory
} = require('@/modules/core/repositories/users')
const {
deleteOldAndInsertNewVerificationFactory
} = require('@/modules/emails/repositories')
const { renderEmail } = require('@/modules/emails/services/emailRendering')
const { sendEmail } = require('@/modules/emails/services/sending')
const { createUserFactory } = require('@/modules/core/services/users/management')
const {
validateAndCreateUserEmailFactory
} = require('@/modules/core/services/userEmails')
const {
finalizeInvitedServerRegistrationFactory
} = require('@/modules/serverinvites/services/processing')
const {
} from '@/modules/core/repositories/users'
import { deleteOldAndInsertNewVerificationFactory } from '@/modules/emails/repositories'
import { renderEmail } from '@/modules/emails/services/emailRendering'
import { sendEmail } from '@/modules/emails/services/sending'
import { createUserFactory } from '@/modules/core/services/users/management'
import { validateAndCreateUserEmailFactory } from '@/modules/core/services/userEmails'
import { finalizeInvitedServerRegistrationFactory } from '@/modules/serverinvites/services/processing'
import {
deleteServerOnlyInvitesFactory,
updateAllInviteTargetsFactory
} = require('@/modules/serverinvites/repositories/serverInvites')
const { getServerInfoFactory } = require('@/modules/core/repositories/server')
const { getEventBus } = require('@/modules/shared/services/eventBus')
} from '@/modules/serverinvites/repositories/serverInvites'
import { getServerInfoFactory } from '@/modules/core/repositories/server'
import { getEventBus } from '@/modules/shared/services/eventBus'
import { BasicTestUser } from '@/test/authHelper'
const ResetTokens = () => knex('pwdreset_tokens')
const db = knex
const getServerInfo = getServerInfoFactory({ db })
const findEmail = findEmailFactory({ db })
@@ -71,17 +64,18 @@ const createUser = createUserFactory({
})
describe('Password reset requests @passwordresets', () => {
let app
let app: Awaited<ReturnType<typeof beforeEachContext>>['app']
before(async () => {
;({ app } = await beforeEachContext())
})
it('Should carefully send a password request email', async () => {
const userA = {
const userA: BasicTestUser = {
name: 'd1',
email: 'd@speckle.systems',
password: 'wowwow8charsplease'
password: 'wowwow8charsplease',
id: ''
}
userA.id = await createUser(userA)
@@ -108,10 +102,11 @@ describe('Password reset requests @passwordresets', () => {
})
it('Should reset passwords', async () => {
const userB = {
const userB: BasicTestUser = {
name: 'd2',
email: 'd2@speckle.systems',
password: 'w0ww0w8charsplease'
password: 'w0ww0w8charsplease',
id: ''
}
userB.id = await createUser(userB)
@@ -163,7 +158,7 @@ describe('Password reset requests @passwordresets', () => {
async () =>
await authRestApi.login({
email: userB.email,
password: userB.password,
password: userB.password!,
challenge: '123'
})
)
+4 -1
View File
@@ -36,12 +36,15 @@ import { ProjectRecordVisibility } from '@/modules/core/helpers/types'
import { moduleAuthLoaders } from '@/modules/index'
export { AuthContext, AuthParams }
interface AuthFailedResult extends AuthResult {
export interface AuthFailedResult extends AuthResult {
authorized: false
error: BaseError | null
fatal?: boolean
}
export const isAuthFailedResult = (result: AuthResult): result is AuthFailedResult =>
('error' in result || ('fatal' in result && !!result.fatal)) && !result.authorized
interface AuthFailedData extends AuthData {
authResult: AuthFailedResult
}
@@ -1,5 +1,5 @@
const expect = require('chai').expect
const {
import { expect } from 'chai'
import {
authPipelineCreator,
authFailed,
authSuccess,
@@ -9,167 +9,227 @@ const {
allowForRegisteredUsersOnPublicStreamsEvenWithoutRole,
allowForServerAdmins,
validateResourceAccess,
validateRequiredStreamFactory
} = require('@/modules/shared/authz')
const {
ForbiddenError: SFE,
UnauthorizedError: SUE,
validateRequiredStreamFactory,
AuthContext,
isAuthFailedResult,
AuthFailedResult
} from '@/modules/shared/authz'
import {
ForbiddenError as SFE,
UnauthorizedError as SUE,
UnauthorizedError,
ContextError,
NotFoundError
} = require('@/modules/shared/errors')
const { Roles } = require('@speckle/shared')
const {
TokenResourceIdentifierType
} = require('@/modules/core/graph/generated/graphql')
const { ProjectRecordVisibility } = require('@/modules/core/helpers/types')
NotFoundError,
BaseError
} from '@/modules/shared/errors'
import { AvailableRoles, ensureError, Roles } from '@speckle/shared'
import { TokenResourceIdentifierType } from '@/modules/core/graph/generated/graphql'
import { ProjectRecordVisibility, StreamRecord } from '@/modules/core/helpers/types'
import {
AuthData,
AuthPipelineFunction,
AuthResult
} from '@/modules/shared/domain/authz/types'
import { UserRoleData } from '@/modules/shared/domain/rolesAndScopes/types'
describe('AuthZ @shared', () => {
const buildFooAuthData = (): AuthData =>
({
context: { foo: 'bar' } as unknown as AuthContext
} as AuthData)
const buildEmptyContext = (): AuthContext => ({} as unknown as AuthContext)
const buildEmptySuccess = () => authSuccess(buildEmptyContext())
describe('Auth pipeline', () => {
it('Empty pipeline returns no authorization', async () => {
const pipeline = authPipelineCreator([])
const { authResult } = await pipeline({ context: { foo: 'bar' } })
const { authResult } = await pipeline(buildFooAuthData())
expect(authResult.authorized).to.equal(false)
})
it('Pipeline breaks on fatal error', async () => {
const errorMessage = 'dummy'
const fatalFail = async () => authFailed({}, new Error(errorMessage), true)
const shouldRescue = async () => authSuccess()
const fatalFail = async () =>
authFailed(buildEmptyContext(), new BaseError(errorMessage), true)
const shouldRescue = async () => buildEmptySuccess()
const pipeline = authPipelineCreator([shouldRescue, fatalFail, shouldRescue])
const { authResult } = await pipeline({ context: { foo: 'bar' } })
const { authResult } = await pipeline(buildFooAuthData())
if (!isAuthFailedResult(authResult)) {
throw new Error('AuthResult should be an auth failed result')
}
expect(authResult.authorized).to.equal(false)
expect(authResult.fatal).to.equal(true)
expect(authResult.error.message).to.equal(errorMessage)
expect(authResult.error?.message).to.equal(errorMessage)
})
it('Pipeline continues for non fatal errors', async () => {
const nonFatalFail = async () => authFailed({}, new Error('errorMessage'), false)
const shouldRescue = async () => authSuccess()
const nonFatalFail = async () =>
authFailed(buildEmptyContext(), new BaseError('errorMessage'), false)
const shouldRescue = async () => buildEmptySuccess()
const pipeline = authPipelineCreator([shouldRescue, nonFatalFail, shouldRescue])
const { authResult } = await pipeline({ context: { foo: 'bar' } })
const { authResult } = await pipeline(buildFooAuthData())
if (isAuthFailedResult(authResult)) {
throw new Error('AuthResult should not be an auth failed result')
}
expect(authResult.authorized).to.equal(true)
expect(authResult.fatal).to.not.exist
expect(authResult.error).to.not.exist
})
it('Pipeline throws Error if authorized but has error', async () => {
const borkedStep = async () => ({
const borkedStep: AuthPipelineFunction = async () => ({
authResult: {
authorized: true,
error: new UnauthorizedError('Weird stuff'),
fatal: false
}
},
context: undefined as unknown as AuthContext
})
const pipeline = authPipelineCreator([borkedStep])
try {
await pipeline({ context: { foo: 'bar' } })
await pipeline(buildFooAuthData())
throw new Error('This should have thrown')
} catch (err) {
expect(err.message).to.equal('Auth failure')
expect(ensureError(err).message).to.equal('Auth failure')
}
})
})
describe('Role validation', () => {
const rolesLookup = async () => [
{ name: '1', weight: 1 },
{ name: 'server:2', weight: 2 },
{ name: '3', weight: 3 },
{ name: 'goku', weight: 9001 },
{ name: '42', weight: 42 }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rolesLookup: () => Promise<UserRoleData<any>[]> = async () => [
{ name: '1', weight: 1, description: '', public: false },
{ name: 'server:2', weight: 2, description: '', public: false },
{ name: '3', weight: 3, description: '', public: false },
{ name: 'goku', weight: 9001, description: '', public: false },
{ name: '42', weight: 42, description: '', public: false }
]
const testData = [
{
name: 'Having lower privileged role than required results auth failed',
requiredRole: 'server:2',
context: { auth: true, role: '1' },
context: { auth: true, role: '1' } as unknown as AuthContext,
expectedResult: authFailed(
null,
buildEmptyContext(),
new SFE('You do not have the required server role')
)
},
{
name: 'Not having auth fails role validation',
requiredRole: 'server:2',
context: { auth: false },
expectedResult: authFailed(null, new SUE('Must provide an auth token'))
context: { auth: false } as unknown as AuthContext,
expectedResult: authFailed(
buildEmptyContext(),
new SUE('Must provide an auth token')
)
},
{
name: 'Requiring a junk role fails auth',
requiredRole: 'knock knock...',
context: { auth: true, role: '1' },
expectedResult: authFailed(null, new SFE('Invalid role requirement specified'))
context: { auth: true, role: '1' } as unknown as AuthContext,
expectedResult: authFailed(
buildEmptyContext(),
new SFE('Invalid role requirement specified')
)
},
{
name: 'Having a junk role fails auth',
requiredRole: 'server:2',
context: { auth: true, role: 'iddqd' },
expectedResult: authFailed(null, new SFE('Your role is not valid'))
context: { auth: true, role: 'iddqd' } as unknown as AuthContext,
expectedResult: authFailed(
buildEmptyContext(),
new SFE('Your role is not valid')
)
},
{
name: 'Not having the required level fails',
requiredRole: 'goku',
context: { auth: true, role: '3' },
context: { auth: true, role: '3' } as unknown as AuthContext,
expectedResult: authFailed(
null,
buildEmptyContext(),
new SFE('You do not have the required goku role')
)
},
{
name: 'Having the god mode role defeats even higher privilege requirement',
requiredRole: 'goku',
context: { auth: true, role: '42' },
expectedResult: authSuccess()
context: { auth: true, role: '42' } as unknown as AuthContext,
expectedResult: buildEmptySuccess()
},
{
name: 'Having equal role weight to required succeeds',
requiredRole: '3',
context: { auth: true, role: '3' },
expectedResult: authSuccess()
context: { auth: true, role: '3' } as unknown as AuthContext,
expectedResult: buildEmptySuccess()
},
{
name: 'Having bigger role weight than required succeeds',
requiredRole: '3',
context: { auth: true, role: 'goku' },
expectedResult: authSuccess()
context: { auth: true, role: 'goku' } as unknown as AuthContext,
expectedResult: buildEmptySuccess()
}
]
testData.forEach((testCase) =>
it(`${testCase.name}`, async () => {
const step = validateRole({
requiredRole: testCase.requiredRole,
requiredRole: testCase.requiredRole as unknown as AvailableRoles,
rolesLookup,
iddqd: '42',
roleGetter: (context) => context.role
iddqd: '42' as AvailableRoles,
roleGetter: (context) => context.role || null
})
const { authResult, context } = await step({
context: testCase.context,
authResult: authFailed()
authResult: { authorized: false }
})
expect(authResult.authorized).to.exist
expect(authResult.authorized).to.equal(
testCase.expectedResult.authResult.authorized
)
// this also needs to check for the error type... is this how do you do that in JS????
expect(authResult.error?.name).to.equal(
testCase.expectedResult.authResult.error?.name
)
expect(authResult.error?.message).to.equal(
testCase.expectedResult.authResult.error?.message
)
if (
isAuthFailedResult(authResult) ||
isAuthFailedResult(testCase.expectedResult.authResult)
) {
if (
!isAuthFailedResult(authResult) ||
!isAuthFailedResult(testCase.expectedResult.authResult)
) {
throw new Error('AuthResult should be an auth failed result')
}
// this also needs to check for the error type... is this how do you do that in JS????
expect(authResult.error?.name).to.equal(
testCase.expectedResult.authResult.error?.name
)
expect(authResult.error?.message).to.equal(
testCase.expectedResult.authResult.error?.message
)
}
expect(context).to.deep.equal(testCase.context)
})
)
it('Role validation fails if input authResult is already in an error state', async () => {
const step = validateRole({ requiredRole: 'goku', rolesLookup, iddqd: '42' })
const step = validateRole({
requiredRole: 'goku' as AvailableRoles,
rolesLookup,
iddqd: '42' as AvailableRoles,
roleGetter: (context) => context.role || null
})
const error = new SFE('This will be echoed back')
const { authResult } = await step({
context: {},
authResult: { authorized: false, error }
context: buildEmptyContext(),
authResult: { authorized: false, error } as AuthFailedResult
})
if (!isAuthFailedResult(authResult)) {
throw new Error('AuthResult should be an auth failed result')
}
expect(authResult.authorized).to.be.false
expect(authResult.error.message).to.equal(error.message)
expect(authResult.error.name).to.equal(error.name)
expect(authResult.error?.message).to.equal(error.message)
expect(authResult.error?.name).to.equal(error.name)
})
})
@@ -178,53 +238,77 @@ describe('AuthZ @shared', () => {
const step = validateScope({ requiredScope: 'play mahjong' })
const expectedError = new SFE("Scope validation doesn't rescue the auth pipeline")
const { authResult } = await step({
context: {},
authResult: { authorized: false, error: expectedError }
context: buildEmptyContext(),
authResult: { authorized: false, error: expectedError } as AuthFailedResult
})
if (!isAuthFailedResult(authResult)) {
throw new Error('AuthResult should be an auth failed result')
}
expect(authResult.authorized).to.be.false
expect(authResult.error.message).to.equal(expectedError.message)
expect(authResult.error.name).to.equal(expectedError.name)
expect(authResult.error?.message).to.equal(expectedError.message)
expect(authResult.error?.name).to.equal(expectedError.name)
})
it('Without having any scopes on the context cannot validate scopes', async () => {
const step = validateScope({ requiredScope: 'play mahjong' })
const { authResult } = await step({ context: {}, authResult: {} })
const { authResult } = await step({
context: buildEmptyContext(),
authResult: { authorized: false }
})
if (!isAuthFailedResult(authResult)) {
throw new Error('AuthResult should be an auth failed result')
}
expect(authResult.authorized).to.equal(false)
const expectedError = new SFE(
'Your auth token does not have the required scope: play mahjong.'
)
expect(authResult.error.message).to.equal(expectedError.message)
expect(authResult.error.name).to.equal(expectedError.name)
expect(authResult.error?.message).to.equal(expectedError.message)
expect(authResult.error?.name).to.equal(expectedError.name)
})
it('Not having the right scopes results auth failed', async () => {
const step = validateScope({ requiredScope: 'play mahjong' })
const { authResult } = await step({
context: { scopes: ['sit around and wait', 'try to be cool'] },
authResult: {}
context: { scopes: ['sit around and wait', 'try to be cool'] } as AuthContext,
authResult: { authorized: false }
})
if (!isAuthFailedResult(authResult)) {
throw new Error('AuthResult should be an auth failed result')
}
expect(authResult.authorized).to.equal(false)
const expectedError = new SFE(
'Your auth token does not have the required scope: play mahjong.'
)
expect(authResult.error.message).to.equal(expectedError.message)
expect(authResult.error.name).to.equal(expectedError.name)
expect(authResult.error?.message).to.equal(expectedError.message)
expect(authResult.error?.name).to.equal(expectedError.name)
})
it('Having the right scopes results auth success', async () => {
const step = validateScope({ requiredScope: 'play mahjong' })
const { authResult } = await step({
context: { scopes: ['sit around and wait', 'try to be cool', 'play mahjong'] },
authResult: {}
context: {
scopes: ['sit around and wait', 'try to be cool', 'play mahjong']
} as AuthContext,
authResult: { authorized: false }
})
if (isAuthFailedResult(authResult)) {
throw new Error('AuthResult should not be an auth failed result')
}
expect(authResult.authorized).to.equal(true)
expect(authResult.error).to.not.exist
})
})
describe('Validate resource access', () => {
it('Succeeds when no resource access rules present', async () => {
const res = await validateResourceAccess({
context: {},
authResult: {}
context: buildEmptyContext(),
authResult: { authorized: false }
})
expect(res.authResult.authorized).to.be.true
@@ -236,8 +320,8 @@ describe('AuthZ @shared', () => {
resourceAccessRules: [
{ id: 'foo', type: TokenResourceIdentifierType.Project }
]
},
authResult: {}
} as AuthContext,
authResult: { authorized: false }
})
expect(res.authResult.authorized).to.be.true
@@ -249,8 +333,8 @@ describe('AuthZ @shared', () => {
resourceAccessRules: [
{ id: 'foo', type: TokenResourceIdentifierType.Project }
]
},
authResult: { authorized: false, error: new Error('dummy') }
} as AuthContext,
authResult: { authorized: false, error: new Error('dummy') } as AuthFailedResult
})
expect(res.authResult.authorized).to.be.false
@@ -263,12 +347,16 @@ describe('AuthZ @shared', () => {
{ id: 'foo', type: TokenResourceIdentifierType.Project }
],
stream: { id: 'bar' }
},
authResult: {}
} as AuthContext,
authResult: { authorized: false }
})
if (!isAuthFailedResult(res.authResult)) {
throw new Error('AuthResult should be an auth failed result')
}
expect(res.authResult.authorized).to.be.false
expect(res.authResult.error.message).to.equal(
expect(res.authResult.error?.message).to.equal(
'You are not authorized to access this resource.'
)
})
@@ -281,8 +369,8 @@ describe('AuthZ @shared', () => {
{ id: 'bar', type: TokenResourceIdentifierType.Project }
],
stream: { id: 'bar' }
},
authResult: {}
} as AuthContext,
authResult: { authorized: false }
})
expect(res.authResult.authorized).to.be.true
@@ -295,8 +383,8 @@ describe('AuthZ @shared', () => {
{ id: 'foo', type: 'fake' },
{ id: 'bar', type: 'fake' }
]
},
authResult: {}
} as unknown as AuthContext,
authResult: { authorized: false }
})
expect(res.authResult.authorized).to.be.true
@@ -304,18 +392,21 @@ describe('AuthZ @shared', () => {
})
describe('Context requires stream', () => {
const expectAuthError = (expectedError, authResult) => {
const expectAuthError = (expectedError: Error, authResult: AuthResult) => {
if (!isAuthFailedResult(authResult)) {
throw new Error('AuthResult should be an auth failed result')
}
expect(authResult.authorized).to.be.false
expect(authResult.error).to.exist
expect(authResult.error.message).to.equal(expectedError.message)
expect(authResult.error.name).to.equal(expectedError.name)
expect(authResult.error?.message).to.equal(expectedError.message)
expect(authResult.error?.name).to.equal(expectedError.name)
}
it('Without streamId in the params it raises context error', async () => {
const step = validateRequiredStreamFactory({
getStream: async () => ({ ur: 'bamboozled' }),
getAutomationProject: async () => null
getStream: async () => ({ ur: 'bamboozled' } as unknown as StreamRecord)
})
const { authResult } = await step({ params: {} })
const { authResult } = await step({ params: {} } as AuthData)
expectAuthError(
new ContextError("The context doesn't have a streamId"),
authResult
@@ -323,10 +414,9 @@ describe('AuthZ @shared', () => {
})
it('If params is not defined it raises context error', async () => {
const step = validateRequiredStreamFactory({
getStream: async () => ({ ur: 'bamboozled' }),
getAutomationProject: async () => null
getStream: async () => ({ ur: 'bamboozled' } as unknown as StreamRecord)
})
const { authResult } = await step({})
const { authResult } = await step({} as AuthData)
expectAuthError(
new ContextError("The context doesn't have a streamId"),
authResult
@@ -336,23 +426,24 @@ describe('AuthZ @shared', () => {
const demoStream = {
id: 'foo',
name: 'bar'
}
} as StreamRecord
const step = validateRequiredStreamFactory({
getStream: async () => demoStream,
getAutomationProject: async () => null
getStream: async () => demoStream
})
const { context } = await step({
context: {},
context: buildEmptyContext(),
params: { streamId: 'this is fake and its fine' }
})
} as AuthData)
expect(context.stream).to.deep.equal(demoStream)
})
it('If context is not defined return auth failure', async () => {
const step = validateRequiredStreamFactory({
getStream: async () => {},
getAutomationProject: async () => null
getStream: async () => undefined
})
const { authResult } = await step({ params: { streamId: 'the need for stream' } })
const { authResult } = await step({
params: { streamId: 'the need for stream' }
} as AuthData)
expectAuthError(new ContextError('The context is not defined'), authResult)
})
@@ -361,25 +452,23 @@ describe('AuthZ @shared', () => {
const step = validateRequiredStreamFactory({
getStream: async () => {
throw new Error(errorMessage)
},
getAutomationProject: async () => null
}
})
const { authResult } = await step({
context: {},
params: { streamId: 'the need for stream' }
})
} as AuthData)
expectAuthError(new ContextError(errorMessage), authResult)
})
it("If stream getter doesn't find a stream it returns fatal auth failure", async () => {
const step = validateRequiredStreamFactory({
getStream: async () => {},
getAutomationProject: async () => null
getStream: async () => undefined
})
const { authResult } = await step({
params: { streamId: 'the need for stream' },
context: {}
})
} as AuthData)
expectAuthError(
new NotFoundError(
@@ -393,19 +482,25 @@ describe('AuthZ @shared', () => {
describe('Escape hatches', () => {
describe('Admin override', () => {
it('server:admins get authSuccess', async () => {
const input = { context: { role: Roles.Server.Admin }, authResult: 'fake' }
const input = {
context: { role: Roles.Server.Admin },
authResult: 'fake'
} as unknown as AuthData
const result = await allowForServerAdmins(input)
expect(result).to.deep.equal(authSuccess(input.context))
})
it('server:users get the previous authResult', async () => {
const input = { context: { role: Roles.Server.User }, authResult: 'fake' }
const input = {
context: { role: Roles.Server.User },
authResult: 'fake'
} as unknown as AuthData
const result = await allowForServerAdmins(input)
expect(result).to.deep.equal(input)
})
})
describe('Allow for public stream no role', () => {
it('not public stream, no auth returns same context ', async () => {
const input = { context: 'dummy', authResult: 'fake' }
const input = { context: 'dummy', authResult: 'fake' } as unknown as AuthData
const result = await allowForRegisteredUsersOnPublicStreamsEvenWithoutRole(
input
)
@@ -415,7 +510,7 @@ describe('AuthZ @shared', () => {
const input = {
context: { stream: { visibility: ProjectRecordVisibility.Public } },
authResult: 'fake'
}
} as unknown as AuthData
const result = await allowForRegisteredUsersOnPublicStreamsEvenWithoutRole(
input
)
@@ -428,7 +523,7 @@ describe('AuthZ @shared', () => {
stream: { visibility: ProjectRecordVisibility.Private }
},
authResult: 'fake'
}
} as unknown as AuthData
const result = await allowForRegisteredUsersOnPublicStreamsEvenWithoutRole(
input
)
@@ -441,7 +536,7 @@ describe('AuthZ @shared', () => {
stream: { visibility: ProjectRecordVisibility.Public }
},
authResult: 'fake'
}
} as unknown as AuthData
const result = await allowForRegisteredUsersOnPublicStreamsEvenWithoutRole(
input
)
@@ -560,7 +655,9 @@ describe('AuthZ @shared', () => {
sameContextTestData.map(([caseName, context]) =>
it(`${caseName} returns same context`, async () => {
const result =
await allowForAllRegisteredUsersOnPublicStreamsWithPublicComments(context)
await allowForAllRegisteredUsersOnPublicStreamsWithPublicComments(
context as unknown as AuthData
)
expect(result).to.deep.equal(context)
})
)
@@ -574,7 +671,7 @@ describe('AuthZ @shared', () => {
}
},
authResult: 'fake'
}
} as unknown as AuthData
const result =
await allowForAllRegisteredUsersOnPublicStreamsWithPublicComments(input)
expect(result).to.deep.equal(authSuccess(input.context))