Skip to content

Commit

Permalink
Merge pull request #276 from OpenBeta/kao-history
Browse files Browse the repository at this point in the history
Update history gql queries, add integration tests
  • Loading branch information
zichongkao committed Apr 28, 2023
2 parents a116660 + 62eefdb commit cf68302
Show file tree
Hide file tree
Showing 8 changed files with 231 additions and 41 deletions.
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 }
}

0 comments on commit cf68302

Please sign in to comment.