Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
216 changes: 202 additions & 14 deletions serverless/src/edlCallback/__tests__/handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@ import * as getConfig from '../../../../sharedUtils/getConfig'
import fetchEdlProfile from '../../utils/fetchEdlProfile'
import createJwt from '../../utils/createJwt'
import * as createCookieModule from '../../utils/createCookie'
import checkNonNasaMMTAccess from '../../utils/checkNonNasaMMTAccess'

beforeAll(() => {
vi.spyOn(console, 'error').mockImplementation(() => {})
})

afterAll(() => {
vi.restoreAllMocks()
})

vi.mock('../../utils/checkNonNasaMMTAccess')
const realCreateCookie = createCookieModule.default
vi.mock('simple-oauth2')
vi.mock('../../../../sharedUtils/getConfig', () => {
Expand Down Expand Up @@ -83,7 +93,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand All @@ -93,6 +106,144 @@ describe('edlCallback', () => {
})
})

describe('when handling invalid assurance levels', () => {
test('should redirect to unauthorizedMMTAccess when assurance level is not defined', async () => {
const mockEvent = {
queryStringParameters: {
code: 'test-code',
state: encodeURIComponent(JSON.stringify({ target: '/' }))
}
}

AuthorizationCode.mockImplementation(() => ({
getToken: vi.fn().mockResolvedValue({
token: {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_at: '2023-01-01T00:00:00Z'
}
})
}))

fetchEdlProfile.mockResolvedValue({
uid: 'test-user' // No assurance level
})

const response = await edlCallback(mockEvent)

expect(response.statusCode).toBe(303)
expect(response.headers.Location).toBe('https://mmt.example.com/unauthorizedAccess?errorType=mmt')
})
})

describe('when handling assurance levels less than 4', () => {
test('should redirect to unauthorizedMMTAccess when assurance level is less than 4', async () => {
const mockEvent = {
queryStringParameters: {
code: 'test-code',
state: encodeURIComponent(JSON.stringify({ target: '/' }))
}
}

AuthorizationCode.mockImplementation(() => ({
getToken: vi.fn().mockResolvedValue({
token: {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_at: '2023-01-01T00:00:00Z'
}
})
}))

fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: '3' // Set assurance level to 3
})

const response = await edlCallback(mockEvent)

expect(response.statusCode).toBe(303)
expect(response.headers.Location).toBe('https://mmt.example.com/unauthorizedAccess?errorType=mmt')
})
})

describe('when handling assurance level 4', () => {
beforeEach(() => {
AuthorizationCode.mockImplementation(() => ({
getToken: vi.fn().mockResolvedValue({
token: {
access_token: 'test-access-token',
refresh_token: 'test-refresh-token',
expires_at: '2023-01-01T00:00:00Z'
}
})
}))
})

test('should redirect to unauthorizedNonNasaMMTAccess when checkNonNasaMMTAccess returns false', async () => {
const mockEvent = {
queryStringParameters: {
code: 'test-code',
state: encodeURIComponent(JSON.stringify({ target: '/' }))
}
}

fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: '4'
})

checkNonNasaMMTAccess.mockResolvedValue(false)

const response = await edlCallback(mockEvent)

expect(response.statusCode).toBe(303)
expect(response.headers.Location).toBe('https://mmt.example.com/unauthorizedAccess?errorType=nonNasaMMT')
Copy link
Contributor

Choose a reason for hiding this comment

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

Below this,
expect(checkNonNasaMMTAccess).toHaveBeenCalledTimes(1)

Anywhere in this file where you have a .toHAveBeenCalledWith(), there should be an accompanying .toHaveBeenCalledTimes()

expect(checkNonNasaMMTAccess).toHaveBeenCalledWith('test-user', 'test-access-token')
})

test('should continue normal flow when checkNonNasaMMTAccess returns true', async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

what is considered 'normal flow'

const mockEvent = {
queryStringParameters: {
code: 'test-code',
state: encodeURIComponent(JSON.stringify({ target: '/' }))
}
}

fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: '4'
})

checkNonNasaMMTAccess.mockResolvedValue(true)

const response = await edlCallback(mockEvent)

expect(response.statusCode).toBe(303)
expect(response.headers.Location).toBe('https://mmt.example.com/auth-callback?target=%2F')
expect(checkNonNasaMMTAccess).toHaveBeenCalledWith('test-user', 'test-access-token')
})

test('should throw an error when checkNonNasaMMTAccess fails', async () => {
const mockEvent = {
queryStringParameters: {
code: 'test-code',
state: encodeURIComponent(JSON.stringify({ target: '/' }))
}
}

fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: '4'
})

checkNonNasaMMTAccess.mockRejectedValue(new Error('Failed to check access'))

await expect(edlCallback(mockEvent)).rejects.toThrow('Failed to check access')
expect(checkNonNasaMMTAccess).toHaveBeenCalledWith('test-user', 'test-access-token')
})
})

describe('when running in offline mode', () => {
test('should issue mock tokens without calling EDL', async () => {
process.env.IS_OFFLINE = 'true'
Expand Down Expand Up @@ -191,7 +342,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand All @@ -216,7 +370,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand All @@ -243,11 +400,16 @@ describe('edlCallback', () => {
getToken: vi.fn().mockResolvedValue({ token: mockToken })
}))

const mockEdlProfile = { uid: 'test-user' }
const mockEdlProfile = {
uid: 'test-user',
assuranceLevel: 5
}
fetchEdlProfile.mockResolvedValue(mockEdlProfile)

await edlCallback(mockEvent)
const expectedExpirationSeconds = Math.floor(new Date(mockToken.expires_at).getTime() / 1000)
const expectedExpirationSeconds = Math.floor(
new Date(mockToken.expires_at).getTime() / 1000
)

expect(createJwt).toHaveBeenCalledWith(
mockToken.access_token,
Expand Down Expand Up @@ -304,7 +466,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand Down Expand Up @@ -334,7 +499,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand Down Expand Up @@ -363,7 +531,10 @@ describe('edlCallback', () => {
getToken: mockGetToken
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

await edlCallback(mockEvent)

Expand Down Expand Up @@ -433,7 +604,7 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({})
fetchEdlProfile.mockResolvedValue({ assuranceLevel: 5 })

const response = await edlCallback(mockEvent)

Expand Down Expand Up @@ -461,7 +632,10 @@ describe('edlCallback', () => {
})
}))

const mockEdlProfile = { uid: 'test-user' }
const mockEdlProfile = {
uid: 'test-user',
assuranceLevel: 5
}
fetchEdlProfile.mockResolvedValue(mockEdlProfile)

await edlCallback(mockEvent)
Expand Down Expand Up @@ -505,7 +679,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand Down Expand Up @@ -538,7 +715,11 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

createJwt.mockImplementation(() => {
throw new Error('JWT creation failed')
})
Expand All @@ -564,7 +745,11 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

createJwt.mockReturnValue('test-jwt')
createCookieSpy.mockImplementation(() => {
throw new Error('Cookie creation failed')
Expand Down Expand Up @@ -593,7 +778,10 @@ describe('edlCallback', () => {
})
}))

fetchEdlProfile.mockResolvedValue({ uid: 'test-user' })
fetchEdlProfile.mockResolvedValue({
uid: 'test-user',
assuranceLevel: 5
})

const response = await edlCallback(mockEvent)

Expand Down
30 changes: 30 additions & 0 deletions serverless/src/edlCallback/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getApplicationConfig, getEdlConfig } from '../../../sharedUtils/getConf
import fetchEdlProfile from '../utils/fetchEdlProfile'
import createJwt from '../utils/createJwt'
import createCookie from '../utils/createCookie'
import checkNonNasaMMTAccess from '../utils/checkNonNasaMMTAccess'

/**
* Handles the EDL callback during authentication
Expand Down Expand Up @@ -72,6 +73,35 @@ const edlCallback = async (event) => {
expiresAt = token.expires_at

edlProfile = await fetchEdlProfile(oauthToken)

// Convert assuranceLevel to number if it's not already
const assuranceLevel = Number(edlProfile.assuranceLevel)

if (Number.isNaN(assuranceLevel) || assuranceLevel < 4) {
return {
statusCode: 303,
headers: {
Location: `${mmtHost}/unauthorizedAccess?errorType=mmt`
}
}
}

if (assuranceLevel === 4) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add comment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

try {
const hasNonNasaDraftAccess = await checkNonNasaMMTAccess(edlProfile.uid, accessToken)
if (!hasNonNasaDraftAccess) {
return {
statusCode: 303,
headers: {
Location: `${mmtHost}/unauthorizedAccess?errorType=nonNasaMMT`
}
}
}
} catch (error) {
console.error('Error checking Non-NASA MMT access:', error)
throw error
}
}
}

// Create JWT with EDL token and edl profile
Expand Down
Loading