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

Update history gql queries, add integration tests #276

Merged
merged 1 commit into from
Apr 28, 2023
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
138 changes: 138 additions & 0 deletions src/__tests__/history.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { ApolloServer } from 'apollo-server'
import muuid from 'uuid-mongodb'
import { jest } from '@jest/globals'
import MutableAreaDataSource, { createInstance as createAreaInstance } from '../model/MutableAreaDataSource.js'
import MutableOrganizationDataSource, { createInstance as createOrgInstance } from '../model/MutableOrganizationDataSource.js'
import MutableClimbDataSource, { createInstance as createClimbInstance } from '../model/MutableClimbDataSource.js'
import { AreaType } from '../db/AreaTypes.js'
import { OrgType, OrganizationType } from '../db/OrganizationTypes.js'
import { muuidToString } from '../utils/helpers.js'
import { queryAPI, setUpServer } from '../utils/testUtils.js'

jest.setTimeout(60000)

describe('history API', () => {
let server: ApolloServer
let user: muuid.MUUID
let userUuid: string
let inMemoryDB

// Mongoose models for mocking pre-existing state.
let areas: MutableAreaDataSource
let organizations: MutableOrganizationDataSource
let climbs: MutableClimbDataSource

beforeAll(async () => {
({ server, inMemoryDB } = await setUpServer())
// Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format
// "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==".
user = muuid.mode('relaxed').v4()
userUuid = muuidToString(user)
})

beforeEach(async () => {
await inMemoryDB.clear()
areas = createAreaInstance()
organizations = createOrgInstance()
climbs = createClimbInstance()
})

afterAll(async () => {
await server.stop()
await inMemoryDB.close()
})

describe('queries', () => {
const FRAGMENT_CHANGE_HISTORY = `
fragment ChangeHistoryFields on History {
id
createdAt
operation
editedBy
changes {
dbOp
changeId
updateDescription {
updatedFields
}
fullDocument {
... on Area {
areaName
uuid
metadata {
leaf
areaId
}
}
... on Climb {
id
name
uuid
}
... on Organization {
orgId
}
}
}
}
`

const QUERY_RECENT_CHANGE_HISTORY = `
${FRAGMENT_CHANGE_HISTORY}
query ($filter: AllHistoryFilter) {
getChangeHistory(filter: $filter) {
...ChangeHistoryFields
}
}
`

let usa: AreaType
let ca: AreaType
let alphaOrg: OrganizationType
let climbIds: string[]

it('queries recent change history successfully', async () => {
// Make changes to be tracked.
usa = await areas.addCountry('usa')
ca = await areas.addArea(user, 'CA', usa.metadata.area_id)
const alphaFields = {
displayName: 'Alpha OpenBeta Club',
associatedAreaIds: [usa.metadata.area_id],
email: '[email protected]'
}
alphaOrg = await organizations.addOrganization(user, OrgType.localClimbingOrganization, alphaFields)
climbIds = await climbs.addOrUpdateClimbs(user, ca.metadata.area_id, [{ name: 'Alpha Climb' }])

// Query for changes and ensure they are tracked.
const resp = await queryAPI({
query: QUERY_RECENT_CHANGE_HISTORY,
variables: { filter: {} },
userUuid
})
expect(resp.statusCode).toBe(200)
const histories = resp.body.data.getChangeHistory
expect(histories.length).toBe(3)

// Latest change first
// Note: addCountry is not captured by history.
const [climbChange, orgChange, areaChange] = histories

expect(climbChange.operation).toBe('updateClimb')
expect(climbChange.editedBy).toBe(userUuid)
// Two changes: Insert the climb, update the parent area
// Ordering is non-deterministic.
expect(climbChange.changes.length).toBe(2)
const insertChange = climbChange.changes.filter(c => c.dbOp === 'insert')[0]
const updateChange = climbChange.changes.filter(c => c.dbOp === 'update')[0]
expect(insertChange.fullDocument.uuid).toBe(climbIds[0])
expect(updateChange.fullDocument.uuid).toBe(muuidToString(ca.metadata.area_id))

expect(orgChange.operation).toBe('addOrganization')
expect(orgChange.editedBy).toBe(userUuid)
expect(orgChange.changes[0].fullDocument.orgId).toBe(muuidToString(alphaOrg.orgId))

expect(areaChange.operation).toBe('addArea')
expect(areaChange.editedBy).toBe(userUuid)
})
})
})
46 changes: 7 additions & 39 deletions src/__tests__/integration.ts → src/__tests__/organizations.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,21 @@
import { ApolloServer } from 'apollo-server'
import muuid from 'uuid-mongodb'
import { jest } from '@jest/globals'
import { createServer } from '../server'
import { muuidToString, isMuuidHexStr } from '../utils/helpers'
import inMemoryDB from '../utils/inMemoryDB.js'
import request from 'supertest'
import jwt from 'jsonwebtoken'
import MutableAreaDataSource, { createInstance as createAreaInstance } from '../model/MutableAreaDataSource.js'
import MutableOrganizationDataSource, { createInstance as createOrgInstance } from '../model/MutableOrganizationDataSource.js'
import { AreaType } from '../db/AreaTypes.js'
import { OrgType, OrganizationType, OperationType, OrganizationEditableFieldsType } from '../db/OrganizationTypes.js'
import { changelogDataSource } from '../model/ChangeLogDataSource.js'
import { queryAPI, setUpServer } from '../utils/testUtils.js'
import { muuidToString, isMuuidHexStr } from '../utils/helpers.js'

jest.setTimeout(60000)
const PORT = 4000

interface QueryAPIProps {
query: string
operationName: string
variables: any
userUuid: string
roles?: string[]
}
const queryAPI = async ({ query, operationName, variables, userUuid, roles = [] }: QueryAPIProps): Promise<request.Response> => {
// Avoid needing to pass in actual signed tokens.
const jwtSpy = jest.spyOn(jwt, 'verify')
jwtSpy.mockImplementation(() => {
return {
// Roles defined at https://manage.auth0.com/dashboard/us/dev-fmjy7n5n/roles
'https://tacos.openbeta.io/roles': roles,
'https://tacos.openbeta.io/uuid': userUuid
}
})

const queryObj = { query, operationName, variables }
const response = await request(`http://localhost:${PORT}`)
.post('/')
.send(queryObj)
.set('Authorization', 'Bearer placeholder-jwt-see-SpyOn')

return response
}

describe('graphql server', () => {
describe('organizations API', () => {
let server: ApolloServer
let user: muuid.MUUID
let userUuid: string
let inMemoryDB

// Mongoose models for mocking pre-existing state.
let areas: MutableAreaDataSource
Expand All @@ -55,9 +25,7 @@ describe('graphql server', () => {
let wa: AreaType

beforeAll(async () => {
server = await createServer()
await inMemoryDB.connect()
await server.listen({ port: PORT })
({ server, inMemoryDB } = await setUpServer())
// Auth0 serializes uuids in "relaxed" mode, resulting in this hex string format
// "59f1d95a-627d-4b8c-91b9-389c7424cb54" instead of base64 "WfHZWmJ9S4yRuTicdCTLVA==".
user = muuid.mode('relaxed').v4()
Expand All @@ -78,7 +46,7 @@ describe('graphql server', () => {
await inMemoryDB.close()
})

describe('mutation API', () => {
describe('mutations', () => {
const createQuery = `
mutation addOrganization($input: AddOrganizationInput!) {
organization: addOrganization(input: $input) {
Expand Down Expand Up @@ -198,7 +166,7 @@ describe('graphql server', () => {
})
})

describe('query API', () => {
describe('queries', () => {
const organizationQuery = `
query organization($input: MUUID) {
organization(muuid: $input) {
Expand Down
4 changes: 4 additions & 0 deletions src/db/ChangeLogType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,7 @@ export interface GetHistoryInputFilterType {
export interface GetAreaHistoryInputFilterType {
areaId: string
}

export interface GetOrganizationHistoryInputFilterType {
orgId: MUUID
}
3 changes: 3 additions & 0 deletions src/graphql/history/HistoryFieldResolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ const resolvers = {
if (node.kind === 'climbs') {
return 'Climb'
}
if (node.kind === 'organizations') {
return 'Organization'
}
return null
}
}
Expand Down
12 changes: 11 additions & 1 deletion src/graphql/history/HistoryQueries.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import muid from 'uuid-mongodb'

import { GetHistoryInputFilterType, GetAreaHistoryInputFilterType } from '../../db/ChangeLogType.js'
import {
GetHistoryInputFilterType,
GetAreaHistoryInputFilterType,
GetOrganizationHistoryInputFilterType
} from '../../db/ChangeLogType.js'
import { DataSourcesType } from '../../types.js'

const HistoryQueries = {
Expand All @@ -18,6 +22,12 @@ const HistoryQueries = {
const { areaId }: GetAreaHistoryInputFilterType = filter ?? {}
const id = muid.from(areaId)
return await history.getAreaChangeSets(id)
},

getOrganizationHistory: async (_, { filter }, { dataSources }): Promise<any> => {
const { history }: DataSourcesType = dataSources
const { orgId }: GetOrganizationHistoryInputFilterType = filter ?? {}
return await history.getOrganizationChangeSets(orgId)
}
}

Expand Down
7 changes: 6 additions & 1 deletion src/graphql/schema/History.gql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ input AreaHistoryFilter {
areaId: ID
}

input OrganizationHistoryFilter {
orgId: MUUID
}

type UpdateDescription {
updatedFields: [String]
}
Expand All @@ -20,7 +24,7 @@ type Change {
updateDescription: UpdateDescription
}

union Document = Area | Climb
union Document = Area | Climb | Organization

type History {
id: ID!
Expand All @@ -33,4 +37,5 @@ type History {
type Query {
getChangeHistory(filter: AllHistoryFilter): [History]
getAreaHistory(filter: AreaHistoryFilter): [History]
getOrganizationHistory(filter: OrganizationHistoryFilter): [History]
}
6 changes: 6 additions & 0 deletions src/utils/inMemoryDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,10 @@ const clear = async (): Promise<void> => {
}
}

export interface InMemoryDB {
connect: () => Promise<void>
close: () => Promise<void>
clear: () => Promise<void>
}

export default { connect, close, clear }
56 changes: 56 additions & 0 deletions src/utils/testUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import jwt from 'jsonwebtoken'
import { jest } from '@jest/globals'
import request from 'supertest'
import inMemoryDB from './inMemoryDB.js'
import type { InMemoryDB } from './inMemoryDB.js'
import { createServer } from '../server.js'
import { ApolloServer } from 'apollo-server'

const PORT = 4000

interface QueryAPIProps {
query: string
operationName?: string
variables: any
userUuid: string
roles?: string[]
port?: number
}

/*
* Helper function for querying the locally-served API. It mocks JWT verification
* so we can pretend to have an role we want when calling the API.
*/
export const queryAPI = async ({ query, operationName, variables, userUuid, roles = [], port = PORT }: QueryAPIProps): Promise<request.Response> => {
// Avoid needing to pass in actual signed tokens.
const jwtSpy = jest.spyOn(jwt, 'verify')
jwtSpy.mockImplementation(() => {
return {
// Roles defined at https://manage.auth0.com/dashboard/us/dev-fmjy7n5n/roles
'https://tacos.openbeta.io/roles': roles,
'https://tacos.openbeta.io/uuid': userUuid
}
})

const queryObj = { query, operationName, variables }
const response = await request(`http://localhost:${port}`)
.post('/')
.send(queryObj)
.set('Authorization', 'Bearer placeholder-jwt-see-SpyOn')

return response
}

interface SetUpServerReturnType {
server: ApolloServer
inMemoryDB: InMemoryDB
}
/*
* Starts Apollo server and has Mongo inMemory replset connect to it.
*/
export const setUpServer = async (port = PORT): Promise<SetUpServerReturnType> => {
const server = await createServer()
await inMemoryDB.connect()
await server.listen({ port })
return { server, inMemoryDB }
}