Add integration tests for setIntegration API

This commit is contained in:
Hongbo Wu 2022-08-08 17:09:23 +08:00
parent 8070273dad
commit 0cf3f58258
8 changed files with 341 additions and 32 deletions

View file

@ -1728,7 +1728,7 @@ export enum SetIntegrationErrorCode {
}
export type SetIntegrationInput = {
enabled?: InputMaybe<Scalars['Boolean']>;
enabled: Scalars['Boolean'];
id?: InputMaybe<Scalars['ID']>;
token: Scalars['String'];
type: IntegrationType;

View file

@ -1252,7 +1252,7 @@ enum SetIntegrationErrorCode {
}
input SetIntegrationInput {
enabled: Boolean
enabled: Boolean!
id: ID
token: String!
type: IntegrationType!

View file

@ -12,7 +12,6 @@ import { analytics } from '../../utils/analytics'
import { env } from '../../env'
import { validateToken } from '../../services/integrations'
import { deleteTask, enqueueSyncWithIntegration } from '../../utils/createTask'
import { AppDataSource } from '../../server'
export const setIntegrationResolver = authorized<
SetIntegrationSuccess,
@ -29,13 +28,9 @@ export const setIntegrationResolver = authorized<
}
}
const integrationToSave: Partial<Integration> = {
let integrationToSave: Partial<Integration> = {
user,
token: input.token,
type: input.type,
enabled: input.enabled === null ? true : input.enabled,
}
if (input.id) {
// Update
const existingIntegration = await getRepository(Integration).findOne({
@ -53,7 +48,17 @@ export const setIntegrationResolver = authorized<
}
}
integrationToSave.id = input.id
if (existingIntegration.enabled === input.enabled) {
return {
integration: existingIntegration,
}
}
integrationToSave = {
...integrationToSave,
id: existingIntegration.id,
taskName: existingIntegration.taskName,
enabled: input.enabled,
}
} else {
// Create
const existingIntegration = await getRepository(Integration).findOneBy({
@ -73,34 +78,32 @@ export const setIntegrationResolver = authorized<
errorCodes: [SetIntegrationErrorCode.InvalidToken],
}
}
integrationToSave = {
...integrationToSave,
token: input.token,
type: input.type,
enabled: true,
}
}
const integration = await AppDataSource.transaction(async (t) => {
const integration = await t
.getRepository(Integration)
.save(integrationToSave)
if (integration.enabled) {
// create a task to sync all the pages
const taskName = await enqueueSyncWithIntegration(
user.id,
integration.type
)
log.info('enqueued task', taskName)
await t
.getRepository(Integration)
.update({ id: integration.id }, { taskName })
} else if (integration.taskName) {
await deleteTask(integration.taskName)
log.info('task deleted', integration.taskName)
}
return integration
})
if (!integrationToSave.id || integrationToSave.enabled) {
// create a task to sync all the pages if new integration or enable integration
const taskName = await enqueueSyncWithIntegration(user.id, input.type)
log.info('enqueued task', taskName)
integrationToSave.taskName = taskName
} else if (integrationToSave.taskName) {
// delete the task if disable integration and task exists
await deleteTask(integrationToSave.taskName)
integrationToSave.taskName = null
log.info('task deleted', integrationToSave.taskName)
}
const integration = await getRepository(Integration).save(integrationToSave)
analytics.track({
userId: uid,
event: 'integration_set',
properties: {
id: integration.id,
id: integrationToSave.id,
env: env.server.apiEnv,
},
})

View file

@ -129,6 +129,9 @@ export function integrationsServiceRouter() {
return
}
}
await getRepository(Integration).update(integration.id, {
taskName: null,
})
} else {
logger.info('unknown action', action)
res.status(200).send('Unknown action')

View file

@ -1847,7 +1847,7 @@ const schema = gql`
id: ID
type: IntegrationType!
token: String!
enabled: Boolean
enabled: Boolean!
}
# Mutations

View file

@ -34,7 +34,7 @@ interface ReadwiseHighlight {
highlight_url?: string
}
const READWISE_API_URL = 'https://readwise.io/api/v2'
export const READWISE_API_URL = 'https://readwise.io/api/v2'
export const validateToken = async (
token: string,

View file

@ -0,0 +1,303 @@
import 'mocha'
import { User } from '../../src/entity/user'
import { createTestUser, deleteTestUser } from '../db'
import { generateFakeUuid, graphqlRequest, request } from '../util'
import {
IntegrationType,
SetIntegrationErrorCode,
} from '../../src/generated/graphql'
import { expect } from 'chai'
import { getRepository } from '../../src/entity/utils'
import {
Integration,
IntegrationType as DataIntegrationType,
} from '../../src/entity/integration'
import nock from 'nock'
import { READWISE_API_URL } from '../../src/services/integrations'
describe('Integrations resolvers', () => {
let loginUser: User
let authToken: string
before(async () => {
// create test user and login
loginUser = await createTestUser('loginUser')
const res = await request
.post('/local/debug/fake-user-login')
.send({ fakeEmail: loginUser.email })
authToken = res.body.authToken
})
after(async () => {
await deleteTestUser(loginUser.name)
})
describe('setIntegration API', () => {
const validToken = 'valid-token'
const query = (
id = '',
type: IntegrationType = IntegrationType.Readwise,
token: string = 'test token',
enabled = true
) => `
mutation {
setIntegration(input: {
id: "${id}",
type: ${type},
token: "${token}",
enabled: ${enabled},
}) {
... on SetIntegrationSuccess {
integration {
id
enabled
}
}
... on SetIntegrationError {
errorCodes
}
}
}
`
let integrationId: string
let token: string
let integrationType: IntegrationType
let enabled: boolean
// mock Readwise Auth API
before(() => {
nock(READWISE_API_URL, {
reqheaders: { Authorization: `Token ${validToken}` },
})
.get('/auth')
.reply(204)
.persist()
})
after(() => {
nock.cleanAll()
})
context('when id is not in the request', () => {
before(() => {
integrationId = ''
})
context('when integration exists', () => {
let existingIntegration: Integration
before(async () => {
existingIntegration = await getRepository(Integration).save({
user: loginUser,
type: DataIntegrationType.Readwise,
token: 'fakeToken',
})
integrationType = existingIntegration.type
})
after(async () => {
await getRepository(Integration).delete({
id: existingIntegration.id,
})
})
it('returns AlreadyExists error code', async () => {
const res = await graphqlRequest(
query(integrationId, integrationType),
authToken
)
expect(res.body.data.setIntegration.errorCodes).to.eql([
SetIntegrationErrorCode.AlreadyExists,
])
})
})
context('when integration does not exist', () => {
context('when token is invalid', () => {
before(() => {
token = 'invalid token'
})
it('returns InvalidToken error code', async () => {
const res = await graphqlRequest(
query(integrationId, integrationType, token),
authToken
)
expect(res.body.data.setIntegration.errorCodes).to.eql([
SetIntegrationErrorCode.InvalidToken,
])
})
})
context('when token is valid', () => {
before(() => {
token = validToken
})
afterEach(async () => {
await getRepository(Integration).delete({
user: loginUser,
type: integrationType,
})
})
it('creates new integration', async () => {
const res = await graphqlRequest(
query(integrationId, integrationType, token),
authToken
)
expect(res.body.data.setIntegration.integration.enabled).to.be.true
})
it('creates new cloud task to sync all existing articles and highlights', async () => {
const res = await graphqlRequest(
query(integrationId, integrationType, token),
authToken
)
const integration = await getRepository(Integration).findOneBy({
id: res.body.data.setIntegration.integration.id,
})
expect(integration?.taskName).not.to.be.null
})
})
})
})
context('when id is in the request', () => {
let existingIntegration: Integration
context('when integration does not exist', () => {
before(() => {
integrationId = generateFakeUuid()
})
it('returns NotFound error code', async () => {
const res = await graphqlRequest(
query(integrationId, integrationType),
authToken
)
expect(res.body.data.setIntegration.errorCodes).to.eql([
SetIntegrationErrorCode.NotFound,
])
})
})
context('when integration exists', () => {
context('when integration does not belong to the user', () => {
let otherUser: User
before(async () => {
otherUser = await createTestUser('otherUser')
existingIntegration = await getRepository(Integration).save({
user: otherUser,
type: DataIntegrationType.Readwise,
token: 'fakeToken',
})
integrationId = existingIntegration.id
})
after(async () => {
await deleteTestUser(otherUser.name)
await getRepository(Integration).delete({
id: existingIntegration.id,
})
})
it('returns Unauthorized error code', async () => {
const res = await graphqlRequest(
query(integrationId, integrationType),
authToken
)
expect(res.body.data.setIntegration.errorCodes).to.eql([
SetIntegrationErrorCode.Unauthorized,
])
})
})
context('when integration belongs to the user', () => {
before(async () => {
existingIntegration = await getRepository(Integration).save({
user: loginUser,
type: DataIntegrationType.Readwise,
token: 'fakeToken',
})
integrationId = existingIntegration.id
})
after(async () => {
await getRepository(Integration).delete({
id: existingIntegration.id,
})
})
context('when enable is false', () => {
before(() => {
enabled = false
})
afterEach(async () => {
await getRepository(Integration).update(existingIntegration.id, {
taskName: 'some task name',
enabled: true,
})
})
it('disables integration', async () => {
const res = await graphqlRequest(
query(integrationId, integrationType, token, enabled),
authToken
)
expect(res.body.data.setIntegration.integration.enabled).to.be
.false
})
it('deletes cloud task', async () => {
const res = await graphqlRequest(
query(integrationId, integrationType, token, enabled),
authToken
)
const integration = await getRepository(Integration).findOneBy({
id: res.body.data.setIntegration.integration.id,
})
expect(integration?.taskName).to.be.null
})
})
context('when enable is true', () => {
before(() => {
enabled = true
})
afterEach(async () => {
await getRepository(Integration).update(existingIntegration.id, {
taskName: null,
enabled: false,
})
})
it('enables integration', async () => {
const res = await graphqlRequest(
query(integrationId, integrationType, token, enabled),
authToken
)
expect(res.body.data.setIntegration.integration.enabled).to.be
.true
})
it('creates new cloud task to sync all existing articles and highlights', async () => {
const res = await graphqlRequest(
query(integrationId, integrationType, token, enabled),
authToken
)
const integration = await getRepository(Integration).findOneBy({
id: res.body.data.setIntegration.integration.id,
})
expect(integration?.taskName).not.to.be.null
})
})
})
})
})
})
})