mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Add integration tests for setIntegration API
This commit is contained in:
parent
8070273dad
commit
0cf3f58258
8 changed files with 341 additions and 32 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1252,7 +1252,7 @@ enum SetIntegrationErrorCode {
|
|||
}
|
||||
|
||||
input SetIntegrationInput {
|
||||
enabled: Boolean
|
||||
enabled: Boolean!
|
||||
id: ID
|
||||
token: String!
|
||||
type: IntegrationType!
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -1847,7 +1847,7 @@ const schema = gql`
|
|||
id: ID
|
||||
type: IntegrationType!
|
||||
token: String!
|
||||
enabled: Boolean
|
||||
enabled: Boolean!
|
||||
}
|
||||
|
||||
# Mutations
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
303
packages/api/test/resolvers/integrations.test.ts
Normal file
303
packages/api/test/resolvers/integrations.test.ts
Normal 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
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
0
packages/api/test/routers/integrations.test.ts
Normal file
0
packages/api/test/routers/integrations.test.ts
Normal file
Loading…
Reference in a new issue