Files
omnivore/packages/api/test/resolvers/article.test.ts
2024-02-01 09:59:42 +08:00

2395 lines
66 KiB
TypeScript

import * as chai from 'chai'
import { expect } from 'chai'
import chaiString from 'chai-string'
import 'mocha'
import sinon from 'sinon'
import { DeepPartial } from 'typeorm'
import { Group } from '../../src/entity/groups/group'
import { Highlight } from '../../src/entity/highlight'
import { Label } from '../../src/entity/label'
import { LibraryItem, LibraryItemState } from '../../src/entity/library_item'
import { UploadFile } from '../../src/entity/upload_file'
import { User } from '../../src/entity/user'
import {
ArticleSavingRequestStatus,
BulkActionType,
HighlightType,
PageType,
SyncUpdatedItemEdge,
UpdateReason,
UploadFileStatus,
} from '../../src/generated/graphql'
import { getRepository } from '../../src/repository'
import { createGroup, deleteGroup } from '../../src/services/groups'
import { createLabel, deleteLabels } from '../../src/services/labels'
import {
createLibraryItem,
createLibraryItems,
deleteLibraryItemById,
deleteLibraryItemByUrl,
deleteLibraryItems,
deleteLibraryItemsByUserId,
findLibraryItemById,
findLibraryItemByUrl,
updateLibraryItem,
} from '../../src/services/library_item'
import { deleteUser } from '../../src/services/user'
import * as createTask from '../../src/utils/createTask'
import * as uploads from '../../src/utils/uploads'
import {
createHighlight,
createTestLibraryItem,
createTestUser,
saveLabelsInLibraryItem,
} from '../db'
import { generateFakeUuid, graphqlRequest, request } from '../util'
chai.use(chaiString)
const archiveLink = async (authToken: string, linkId: string) => {
const query = `
mutation {
setLinkArchived(
input: {
linkId: "${linkId}",
archived: ${true}
}
) {
... on ArchiveLinkSuccess {
linkId
}
... on ArchiveLinkError {
errorCodes
}
}
}
`
return graphqlRequest(query, authToken).expect(200)
}
const createArticleQuery = (
url: string,
source: string,
document: string,
title: string
) => {
return `
mutation {
createArticle(input: {
url: "${url}"
source: "${source}"
preparedDocument: {
document: "${document}"
pageInfo: {
contentType: "text/html"
title: "${title}"
}
}
}) {
... on CreateArticleSuccess {
createdArticle {
id
title
content
isArchived
}
user {
id
name
}
created
}
... on CreateArticleError {
errorCodes
}
}
}
`
}
const getArticleQuery = (slug: string) => {
return `
query {
article(slug: "${slug}", username: "") {
... on ArticleSuccess {
article {
id
slug
content
highlights {
id
shortId
quote
prefix
suffix
patch
annotation
sharedAt
createdAt
updatedAt
}
}
}
... on ArticleError {
errorCodes
}
}
}
`
}
const searchQuery = (keyword = '') => {
return `
query {
search(
after: ""
first: 5
query: "${keyword}") {
... on SearchSuccess {
edges {
cursor
node {
id
url
createdAt
updatedAt
highlights {
id
}
labels {
id
name
}
recommendations {
name
}
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
totalCount
}
}
... on SearchError {
errorCodes
}
}
}
`
}
const savePageQuery = (
url: string,
title: string,
originalContent: string,
state: ArticleSavingRequestStatus | null = null,
labels: string[] | null = null,
clientRequestId = generateFakeUuid(),
source = 'puppeteer-parse'
) => {
return `
mutation {
savePage(
input: {
url: "${url}",
source: "${source}",
clientRequestId: "${clientRequestId}",
title: "${title}",
originalContent: "${originalContent}"
state: ${state}
labels: ${
labels
? '[' + labels.map((label) => `{ name: "${label}" }`) + ']'
: null
}
}
) {
... on SaveSuccess {
url
}
... on SaveError {
errorCodes
}
}
}
`
}
const saveFileQuery = (
clientRequestId: string,
url: string,
uploadFileId: string
) => {
return `
mutation {
saveFile (
input: {
url: "${url}",
source: "test",
clientRequestId: "${clientRequestId}",
uploadFileId: "${uploadFileId}",
}
) {
... on SaveSuccess {
url
}
... on SaveError {
errorCodes
}
}
}
`
}
const saveUrlQuery = (
url: string,
state: ArticleSavingRequestStatus | null = null,
labels: string[] | null = null
) => {
return `
mutation {
saveUrl(
input: {
url: "${url}",
source: "test",
clientRequestId: "${generateFakeUuid()}",
state: ${state}
labels: ${
labels
? '[' + labels.map((label) => `{ name: "${label}" }`) + ']'
: null
}
}
) {
... on SaveSuccess {
url
}
... on SaveError {
errorCodes
}
}
}
`
}
const setBookmarkQuery = (articleId: string, bookmark: boolean) => {
return `
mutation {
setBookmarkArticle(
input: {
articleID: "${articleId}",
bookmark: ${bookmark}
}
) {
... on SetBookmarkArticleSuccess {
bookmarkedArticle {
id
}
}
... on SetBookmarkArticleError {
errorCodes
}
}
}
`
}
const saveArticleReadingProgressQuery = (
articleId: string,
progress: number,
topPercent: number | null = null,
force: boolean | null = null
) => {
return `
mutation {
saveArticleReadingProgress(
input: {
id: "${articleId}",
readingProgressPercent: ${progress},
readingProgressAnchorIndex: 0,
readingProgressTopPercent: ${topPercent},
force: ${force}
}
) {
... on SaveArticleReadingProgressSuccess {
updatedArticle {
id
readingProgressPercent
readAt
readingProgressTopPercent
}
}
... on SaveArticleReadingProgressError {
errorCodes
}
}
}
`
}
const typeaheadSearchQuery = (keyword: string) => {
return `
query {
typeaheadSearch(query: "${keyword}") {
... on TypeaheadSearchSuccess {
items {
id
slug
title
contentReader
}
}
... on TypeaheadSearchError {
errorCodes
}
}
}
`
}
describe('Article API', () => {
let authToken: string
let user: User
before(async () => {
// create test user and login
user = await createTestUser('fakeUser')
const res = await request
.post('/local/debug/fake-user-login')
.send({ fakeEmail: user.email })
authToken = res.body.authToken
})
after(async () => {
// clean up
await deleteUser(user.id)
})
describe('CreateArticle', () => {
let query = ''
let url = ''
let source = ''
let document = ''
let title = ''
let itemId = ''
beforeEach(async () => {
query = createArticleQuery(url, source, document, title)
})
context('when saving from document', () => {
before(() => {
url = 'https://blog.omnivore.app/p/testing-is-fun-with-omnivore'
source = 'puppeteer-parse'
document = '<p>test</p>'
title = 'new title'
})
after(async () => {
await deleteLibraryItemById(itemId, user.id)
})
it('should create an article', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.createArticle.createdArticle.title).to.eql(title)
itemId = res.body.data.createArticle.createdArticle.id
})
})
context('when saving an archived article', () => {
before(async () => {
url = 'https://blog.omnivore.app/saving-archived-article.com'
source = 'puppeteer-parse'
document = '<p>test</p>'
title = 'new title'
const item = await createLibraryItem(
{
readableContent: document,
slug: 'test saving an archived article slug',
title,
user: { id: user.id },
originalUrl: url,
archivedAt: new Date(),
state: LibraryItemState.Archived,
},
user.id
)
itemId = item.id
})
after(async () => {
await deleteLibraryItemById(itemId, user.id)
})
it('unarchives the article', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.createArticle.createdArticle.isArchived).to.false
})
})
})
describe('GetArticle', () => {
const realSlug = 'testing-is-really-fun-with-omnivore'
let query = ''
let slug = ''
let itemId: string
before(async () => {
const itemToCreate: DeepPartial<LibraryItem> = {
title: 'test title',
originalContent: '<p>test</p>',
slug: realSlug,
readingProgressTopPercent: 100,
user,
originalUrl: 'https://blog.omnivore.app/test-with-omnivore',
highlights: [
{
shortId: 'test short id',
patch: 'test patch',
quote: 'test quote',
user,
},
],
}
const item = await createLibraryItem(itemToCreate, user.id)
itemId = item.id
})
after(async () => {
await deleteLibraryItemById(itemId, user.id)
})
beforeEach(async () => {
query = getArticleQuery(slug)
})
context('when item exists', () => {
before(() => {
slug = realSlug
})
it('should return the item', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.article.article.slug).to.eql(slug)
})
it('should return highlights', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.article.article.highlights).to.length(1)
})
context('when item is failed to process', () => {
before(async () => {
await updateLibraryItem(
itemId,
{
state: LibraryItemState.Processing,
savedAt: new Date(Date.now() - 1000 * 60),
},
user.id
)
})
it('should return unable to parse', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.article.article.content).to.eql(
'<p>We were unable to parse this page.</p>'
)
})
})
})
context('query with id instead of slug', () => {
before(() => {
slug = itemId
})
it('returns the item', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.article.article.id).to.eql(slug)
})
})
context('when item does not exist', () => {
before(() => {
slug = 'not-a-real-slug'
})
it('should return an error', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.article.errorCodes).to.eql(['NOT_FOUND'])
})
})
})
describe('SavePage', () => {
let query = ''
let title = 'Example Title'
let url = 'https://blog.omnivore.app'
let originalContent = '<div>Example Content</div>'
beforeEach(() => {
query = savePageQuery(url, title, originalContent)
})
context('when we save a new item', () => {
after(async () => {
await deleteLibraryItemByUrl(url, user.id)
})
it('should return a slugged url', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.savePage.url).to.startsWith(
'http://localhost:3000/fakeUser/example-title-'
)
})
})
context('when we save a item that is already archived', () => {
before(() => {
url = 'https://blog.omnivore.app/new-url'
})
after(async () => {
await deleteLibraryItemByUrl(url, user.id)
})
it('it should return that item in the Search Query', async () => {
await graphqlRequest(
savePageQuery(url, title, originalContent),
authToken
).expect(200)
// Save a link, then archive it
let allLinks = await graphqlRequest(
searchQuery('in:inbox'),
authToken
).expect(200)
const justSavedId = allLinks.body.data.search.edges[0].node.id
await archiveLink(authToken, justSavedId)
// test the negative case, ensuring the archive link wasn't returned
allLinks = await graphqlRequest(
searchQuery('in:inbox'),
authToken
).expect(200)
expect(allLinks.body.data.search.edges[0]?.node?.url).to.not.eq(url)
// Now save the link again, and ensure it is returned
await graphqlRequest(
savePageQuery(
url,
title,
originalContent,
null,
null,
generateFakeUuid()
),
authToken
).expect(200)
allLinks = await graphqlRequest(
searchQuery('in:inbox'),
authToken
).expect(200)
expect(allLinks.body.data.search.edges[0].node.id).to.eq(justSavedId)
expect(allLinks.body.data.search.edges[0].node.url).to.eq(url)
})
})
context('when we also want to save labels and archives the item', () => {
before(() => {
url = 'https://blog.omnivore.app/new-url-2'
})
after(async () => {
await deleteLibraryItemByUrl(url, user.id)
})
it('saves the labels and archives the item', async () => {
const state = ArticleSavingRequestStatus.Archived
const labels = ['test name', 'test name 2']
await graphqlRequest(
savePageQuery(url, title, originalContent, state, labels),
authToken
).expect(200)
const savedItem = await findLibraryItemByUrl(url, user.id)
expect(savedItem?.archivedAt).to.not.be.null
expect(savedItem?.labels?.map((l) => l.name)).to.eql(labels)
})
})
context('when the source is rss-feeder and url is from youtube.com', () => {
const source = 'rss-feeder'
const stub = sinon.stub(createTask, 'enqueueParseRequest')
before(() => {
url = 'https://www.youtube.com/watch?v=123'
})
after(async () => {
await deleteLibraryItemByUrl(url, user.id)
sinon.restore()
})
it('does not parse in the backend', async () => {
await graphqlRequest(
savePageQuery(url, title, originalContent, null, null, '', source),
authToken
).expect(200)
expect(stub).not.to.have.been.called
})
})
})
describe('SaveUrl', () => {
let query = ''
let url = 'https://blog.omnivore.app/new-url-1'
before(() => {
sinon.replace(createTask, 'enqueueParseRequest', sinon.fake.resolves(''))
})
beforeEach(() => {
query = saveUrlQuery(url)
})
after(() => {
sinon.restore()
})
afterEach(async () => {
await deleteLibraryItemByUrl(url, user.id)
})
context('when we save a new url', () => {
it('should return a slugged url', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.saveUrl.url).to.startsWith(
'http://localhost:3000/fakeUser/links/'
)
})
})
})
describe('setBookmarkArticle', () => {
let itemId: string
before(async () => {
const itemToSave: DeepPartial<LibraryItem> = {
user,
title: 'test title',
readableContent: '<p>test</p>',
originalUrl: 'https://blog.omnivore.app/setBookmarkArticle',
slug: 'test-with-omnivore',
}
const item = await createLibraryItem(itemToSave, user.id)
itemId = item.id
})
after(async () => {
await deleteLibraryItemById(itemId, user.id)
})
it('marks an article as deleted', async () => {
await graphqlRequest(setBookmarkQuery(itemId, false), authToken).expect(
200
)
const item = await findLibraryItemById(itemId, user.id)
expect(item?.state).to.eql(LibraryItemState.Deleted)
})
})
describe('saveArticleReadingProgressResolver', () => {
let query = ''
let itemId = ''
let progress = 0.5
let topPercent: number | null = null
before(async () => {
itemId = (await createTestLibraryItem(user.id)).id
})
after(async () => {
await deleteLibraryItemById(itemId, user.id)
})
it('saves a reading progress on an article', async () => {
query = saveArticleReadingProgressQuery(itemId, progress, topPercent)
const res = await graphqlRequest(query, authToken).expect(200)
expect(
res.body.data.saveArticleReadingProgress.updatedArticle
.readingProgressPercent
).to.eq(progress)
expect(res.body.data.saveArticleReadingProgress.updatedArticle.readAt).not
.null
})
it('should not allow setting the reading progress lower than current progress', async () => {
const firstQuery = saveArticleReadingProgressQuery(itemId, 75)
const firstRes = await graphqlRequest(firstQuery, authToken).expect(200)
expect(
firstRes.body.data.saveArticleReadingProgress.updatedArticle
.readingProgressPercent
).to.eq(75)
// Now try to set to a lower value (50), value should not be updated
const secondQuery = saveArticleReadingProgressQuery(itemId, 50)
const secondRes = await graphqlRequest(secondQuery, authToken).expect(200)
expect(
secondRes.body.data.saveArticleReadingProgress.updatedArticle
.readingProgressPercent
).to.eq(75)
})
it('does not save topPercent if not undefined', async () => {
query = saveArticleReadingProgressQuery(itemId, progress, null)
const res = await graphqlRequest(query, authToken).expect(200)
expect(
res.body.data.saveArticleReadingProgress.updatedArticle
.readingProgressTopPercent
).to.eq(0)
})
it('saves topPercent if defined', async () => {
const topPercent = 0.2
query = saveArticleReadingProgressQuery(itemId, progress, topPercent)
const res = await graphqlRequest(query, authToken).expect(200)
expect(
res.body.data.saveArticleReadingProgress.updatedArticle
.readingProgressTopPercent
).to.eql(topPercent)
})
it('saves topPercent as 0 if defined as 0', async () => {
const topPercent = 0
query = saveArticleReadingProgressQuery(
itemId,
progress,
topPercent,
true
)
const res = await graphqlRequest(query, authToken).expect(200)
expect(
res.body.data.saveArticleReadingProgress.updatedArticle
.readingProgressTopPercent
).to.eql(topPercent)
})
it('returns BAD_DATA error if top position is greater than bottom position', async () => {
query = saveArticleReadingProgressQuery(itemId, 0.5, 0.8)
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.saveArticleReadingProgress.errorCodes).to.eql([
'BAD_DATA',
])
})
context('when force is true', () => {
before(async () => {
itemId = (
await createLibraryItem(
{
user: { id: user.id },
originalUrl: 'https://blog.omnivore.app/setBookmarkArticle',
slug: 'test-with-omnivore',
readableContent: '<p>test</p>',
title: 'test title',
readingProgressBottomPercent: 100,
readingProgressTopPercent: 80,
},
user.id
)
).id
})
after(async () => {
await deleteLibraryItemById(itemId, user.id)
})
it('ignore position check if force is true', async () => {
query = saveArticleReadingProgressQuery(itemId, 20, 10, true)
const res = await graphqlRequest(query, authToken).expect(200)
expect(
res.body.data.saveArticleReadingProgress.updatedArticle
.readingProgressPercent
).to.eql(20)
expect(
res.body.data.saveArticleReadingProgress.updatedArticle
.readingProgressTopPercent
).to.eql(10)
})
})
})
describe('SaveFile', () => {
let query = ''
let url = ''
let uploadFileId = ''
let itemId = ''
before(async () => {
const item = await createLibraryItem(
{
user: { id: user.id },
originalUrl: 'https://blog.omnivore.app/setBookmarkArticle',
slug: 'test-with-omnivore',
readableContent: '<p>test</p>',
title: 'test title',
readingProgressBottomPercent: 100,
readingProgressTopPercent: 80,
},
user.id
)
itemId = item.id
sinon.replace(
uploads,
'getStorageFileDetails',
sinon.fake.resolves({ fileUrl: 'fake url', md5Hash: 'fake hash' })
)
})
beforeEach(() => {
query = saveFileQuery(itemId, url, uploadFileId)
})
after(() => {
sinon.restore()
})
context('when the file is not uploaded', () => {
before(async () => {
url = 'fake url'
uploadFileId = generateFakeUuid()
})
it('should return Unauthorized error', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.saveFile.errorCodes).to.eql(['UNAUTHORIZED'])
})
})
context('when the file is uploaded', () => {
before(async () => {
url = 'https://blog.omnivore.app/'
const uploadFile = await getRepository(UploadFile).save({
fileName: 'test.pdf',
contentType: 'application/pdf',
url: url,
user: user,
status: UploadFileStatus.Initialized,
})
uploadFileId = uploadFile.id
})
it('should return the new url', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.saveFile.url).to.startsWith(
'http://localhost:3000/fakeUser/links'
)
})
})
})
describe('Search API', () => {
const url = 'https://blog.omnivore.app/p/getting-started-with-omnivore'
const items: LibraryItem[] = []
const highlights: Highlight[] = []
const searchedKeyword = 'aaabbbccc'
let query = ''
let keyword = ''
before(async () => {
const readingProgressArray = [0, 2, 97, 98, 100]
// Create some test items
for (let i = 0; i < 5; i++) {
const itemToSave: DeepPartial<LibraryItem> = {
user,
title: 'test title',
readableContent: `<p>test ${searchedKeyword}</p>`,
slug: 'test slug',
originalUrl: `${url}/${i}`,
siteName: 'Example',
readingProgressBottomPercent: readingProgressArray[i],
}
const item = await createLibraryItem(itemToSave, user.id)
items.push(item)
// Create some test highlights
const highlightToSave: DeepPartial<Highlight> = {
patch: 'test patch',
shortId: `test shortId${i}`,
user,
quote: '<p>search highlight</p>',
libraryItem: item,
}
const highlight = await createHighlight(
highlightToSave,
item.id,
user.id
)
highlights.push(highlight)
}
})
beforeEach(async () => {
query = searchQuery(keyword)
})
after(async () => {
await deleteLibraryItemsByUserId(user.id)
})
context('when type:highlights is not in the query', () => {
before(() => {
keyword = searchedKeyword
})
it('should return items in descending order', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.edges.length).to.eql(5)
expect(res.body.data.search.edges[0].node.id).to.eq(items[4].id)
expect(res.body.data.search.edges[1].node.id).to.eq(items[3].id)
expect(res.body.data.search.edges[2].node.id).to.eq(items[2].id)
expect(res.body.data.search.edges[3].node.id).to.eq(items[1].id)
expect(res.body.data.search.edges[4].node.id).to.eq(items[0].id)
})
it('should return highlights in items', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.edges[0].node.highlights.length).to.eql(1)
expect(res.body.data.search.edges[0].node.highlights[0].id).to.eq(
highlights[4].id
)
})
})
context('when is:unread is in the query', () => {
before(() => {
keyword = `'${searchedKeyword}' is:unread`
})
it('returns unread articles in descending order', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.edges.length).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context('when is:reading is in the query', () => {
before(() => {
keyword = `'${searchedKeyword}' is:reading`
})
it('returns reading articles in descending order', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.edges.length).to.eq(3)
expect(res.body.data.search.edges[0].node.id).to.eq(items[3].id)
expect(res.body.data.search.edges[1].node.id).to.eq(items[2].id)
expect(res.body.data.search.edges[2].node.id).to.eq(items[1].id)
})
})
context('when is:read is in the query', () => {
before(() => {
keyword = `'${searchedKeyword}' is:read`
})
it('returns fully read articles in descending order', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.edges.length).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[4].id)
})
})
context('when no:label is in the query', () => {
before(async () => {
keyword = `'${searchedKeyword}' no:label`
})
it('returns non-labeled items in descending order', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(5)
})
})
context('when no:highlight is in the query', () => {
before(async () => {
keyword = `'${searchedKeyword}' no:highlight`
})
it('returns non-highlighted items in descending order', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(0)
})
})
context('when site:${site_name} is in the query', () => {
before(async () => {
keyword = `'${searchedKeyword}' site:example`
})
it('returns items from the site', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(5)
})
})
context("when 'in:archive label:test' is in the query", () => {
let items: LibraryItem[] = []
let label: Label
before(async () => {
keyword = 'in:archive label:test'
// Create some test items
label = await createLabel('test', '', user.id)
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
archivedAt: new Date(),
state: LibraryItemState.Archived,
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
archivedAt: new Date(),
state: LibraryItemState.Archived,
},
{
user,
title: 'test title 3',
readableContent: '<p>test 3</p>',
slug: 'test slug 3',
originalUrl: `${url}/test3`,
},
],
user.id
)
await saveLabelsInLibraryItem([label], items[0].id, user.id)
await saveLabelsInLibraryItem([label], items[2].id, user.id)
})
after(async () => {
await deleteLabels({ id: label.id }, user.id)
await deleteLibraryItems(items, user.id)
})
it('returns archived items with label test', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context('when site is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'site:yes-app.com'
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
state: LibraryItemState.Succeeded,
siteName: 'yes-app.com',
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
state: LibraryItemState.Succeeded,
siteName: 'no-app.com',
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns item with matching site', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context('when wildcard site is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'site:app.com'
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
state: LibraryItemState.Succeeded,
siteName: 'yes-app.com',
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
state: LibraryItemState.Succeeded,
siteName: 'no-app.com',
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns item with matching search query', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(2)
})
})
context('when in:inbox no:subscription label:test is in the query', () => {
let items: LibraryItem[] = []
let label: Label
before(async () => {
keyword = 'in:inbox no:subscription label:test'
// Create some test items
label = await createLabel('test', '', user.id)
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
subscription: 'test subscription',
},
{
user,
title: 'test title 3',
readableContent: '<p>test 3</p>',
slug: 'test slug 3',
originalUrl: `${url}/test3`,
archivedAt: new Date(),
state: LibraryItemState.Archived,
},
],
user.id
)
await saveLabelsInLibraryItem([label], items[0].id, user.id)
await saveLabelsInLibraryItem([label], items[1].id, user.id)
await saveLabelsInLibraryItem([label], items[2].id, user.id)
})
after(async () => {
await deleteLabels({ id: label.id }, user.id)
await deleteLibraryItems(items, user.id)
})
it('returns library items with label test', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context('when wildcard search for labels', () => {
let items: LibraryItem[] = []
let labelIds: string[]
before(async () => {
keyword = 'label:test/*'
// Create some test items
const label1 = await createLabel('test/one', '', user.id)
const label2 = await createLabel('test/two', '', user.id)
labelIds = [label1.id, label2.id]
items = await createLibraryItems(
[
{
user,
title: 'test title wildcard',
readableContent: '<p>test wildcard</p>',
slug: 'test slug wildcard',
originalUrl: `${url}/wildcard`,
},
{
user,
title: 'test title wildcard 1',
readableContent: '<p>test wildcard</p>',
slug: 'test slug wildcard 1',
originalUrl: `${url}/wildcard_1`,
},
{
user,
title: 'test title wildcard 2',
readableContent: '<p>test wildcard</p>',
slug: 'test slug wildcard 2',
originalUrl: `${url}/wildcard_2`,
},
],
user.id
)
await saveLabelsInLibraryItem([label1], items[0].id, user.id)
await saveLabelsInLibraryItem([label2], items[1].id, user.id)
})
after(async () => {
await deleteLabels(labelIds, user.id)
await deleteLibraryItems(items, user.id)
})
it('returns library items with label test/one and test/two', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(2)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
expect(res.body.data.search.edges[1].node.id).to.eq(items[1].id)
})
})
context('when type:file label:test is in the query', () => {
let items: LibraryItem[] = []
let label: Label
before(async () => {
keyword = 'type:file label:test'
// Create some test items
label = await createLabel('test', '', user.id)
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
itemType: PageType.File,
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
itemType: PageType.File,
},
{
user,
title: 'test title 3',
readableContent: '<p>test 3</p>',
slug: 'test slug 3',
originalUrl: `${url}/test3`,
},
],
user.id
)
await saveLabelsInLibraryItem([label], items[0].id, user.id)
await saveLabelsInLibraryItem([label], items[2].id, user.id)
})
after(async () => {
await deleteLabels({ id: label.id }, user.id)
await deleteLibraryItems(items, user.id)
})
it('returns files with label test', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context('when in:archive is:unread is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'in:archive is:unread'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
itemType: PageType.File,
archivedAt: new Date(),
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
archivedAt: new Date(),
readingProgressBottomPercent: 100,
},
{
user,
title: 'test title 3',
readableContent: '<p>test 3</p>',
slug: 'test slug 3',
originalUrl: `${url}/test3`,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns unread archived items', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context('when rss:feed in:archive is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'rss:feed in:archive'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
subscription: 'feed',
archivedAt: new Date(),
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
subscription: 'feed',
},
{
user,
title: 'test title 3',
readableContent: '<p>test 3</p>',
slug: 'test slug 3',
originalUrl: `${url}/test3`,
archivedAt: new Date(),
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns archived feed items', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context('when in:trash is:unread is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'in:trash is:unread'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
deletedAt: new Date(),
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
readingProgressBottomPercent: 100,
deletedAt: new Date(),
},
{
user,
title: 'test title 3',
readableContent: '<p>test 3</p>',
slug: 'test slug 3',
originalUrl: `${url}/test3`,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns unfinished deleted items', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context('when readPosition:>20 readPosition:<50 is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'readPosition:>20 readPosition:<50'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
readingProgressBottomPercent: 40,
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
readingProgressBottomPercent: 10,
},
{
user,
title: 'test title 3',
readableContent: '<p>test 3</p>',
slug: 'test slug 3',
originalUrl: `${url}/test3`,
readingProgressBottomPercent: 100,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns items with reading progress between 20% and 50% exclusively', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context(
'when wordsCount:>=10000 wordsCount:<=20000 is in the query',
() => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'wordsCount:>=10000 wordsCount:<=20000'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
wordCount: 10000,
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
wordCount: 8000,
},
{
user,
title: 'test title 3',
readableContent: '<p>test 3</p>',
slug: 'test slug 3',
originalUrl: `${url}/test3`,
wordCount: 100000,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns items with words count between 10000 and 20000 inclusively', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
}
)
context('when recommendedBy:* is in the query', () => {
let items: LibraryItem[] = []
let group: Group
before(async () => {
keyword = 'recommendedBy:*'
group = (
await createGroup({
admin: user,
name: 'test group',
})
)[0]
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
recommendations: [
{
recommender: user,
group,
},
],
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
await deleteGroup(group.id)
})
it('returns recommended items', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
expect(
res.body.data.search.edges[0].node.recommendations[0].name
).to.eq(group.name)
})
})
context('when site:youtube.com is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'site:youtube.com'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: 'https://www.youtube.com/watch?v=Omnivore',
itemType: PageType.Video,
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns youtube videos', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context('when site:wikipedia is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'site:wikipedia'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: 'https://en.wikipedia.org/wiki/Omnivore',
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns wikipedia pages', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(1)
expect(res.body.data.search.edges[0].node.id).to.eq(items[0].id)
})
})
context('when label:test1,test2 is in the query', () => {
let items: LibraryItem[] = []
let label1: Label
let label2: Label
before(async () => {
keyword = 'label:test1,test2'
// Create some test items
label1 = await createLabel('test1', '', user.id)
label2 = await createLabel('test2', '', user.id)
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
savedAt: new Date(1703880588),
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
savedAt: new Date(1704880589),
},
{
user,
title: 'test title 3',
readableContent: '<p>test 3</p>',
slug: 'test slug 3',
originalUrl: `${url}/test3`,
savedAt: new Date(1705880590),
},
],
user.id
)
await saveLabelsInLibraryItem([label1], items[0].id, user.id)
await saveLabelsInLibraryItem([label2], items[1].id, user.id)
})
after(async () => {
await deleteLabels({ id: label1.id }, user.id)
await deleteLabels({ id: label2.id }, user.id)
await deleteLibraryItems(items, user.id)
})
it('returns items with label test1 or test2', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(2)
expect(res.body.data.search.edges[0].node.id).to.eq(items[1].id)
expect(res.body.data.search.edges[1].node.id).to.eq(items[0].id)
})
})
context('when label:test1 OR label:test2 is in the query', () => {
let items: LibraryItem[] = []
let label1: Label
let label2: Label
before(async () => {
keyword = 'label:test1 OR label:test2'
// Create some test items
label1 = await createLabel('test1', '', user.id)
label2 = await createLabel('test2', '', user.id)
items = await createLibraryItems(
[
{
user,
title: 'test title 1',
readableContent: '<p>test 1</p>',
slug: 'test slug 1',
originalUrl: `${url}/test1`,
savedAt: new Date(1703880588),
},
{
user,
title: 'test title 2',
readableContent: '<p>test 2</p>',
slug: 'test slug 2',
originalUrl: `${url}/test2`,
savedAt: new Date(1704880589),
},
{
user,
title: 'test title 3',
readableContent: '<p>test 3</p>',
slug: 'test slug 3',
originalUrl: `${url}/test3`,
savedAt: new Date(1705880590),
},
],
user.id
)
await saveLabelsInLibraryItem([label1], items[0].id, user.id)
await saveLabelsInLibraryItem([label2], items[1].id, user.id)
})
after(async () => {
await deleteLabels({ id: label1.id }, user.id)
await deleteLabels({ id: label2.id }, user.id)
await deleteLibraryItems(items, user.id)
})
it('returns items with label test1 or test2', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eq(2)
expect(res.body.data.search.edges[0].node.id).to.eq(items[1].id)
expect(res.body.data.search.edges[1].node.id).to.eq(items[0].id)
})
})
context('when sort:score is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'sort:score score'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'score',
slug: 'test 1',
originalUrl: `${url}/test1`,
},
{
user,
title: 'score score',
slug: 'test 2',
originalUrl: `${url}/test2`,
},
{
user,
title: 'score score score',
slug: 'test 3',
originalUrl: `${url}/test3`,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns items in descending order of score', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eql(3)
expect(res.body.data.search.edges[0].node.id).to.eq(items[2].id)
expect(res.body.data.search.edges[1].node.id).to.eq(items[1].id)
expect(res.body.data.search.edges[2].node.id).to.eq(items[0].id)
})
})
context('when sort:wordscount is in the query', () => {
let items: LibraryItem[] = []
before(async () => {
keyword = 'wordscount:>=10000 sort:wordscount'
// Create some test items
items = await createLibraryItems(
[
{
user,
title: 'test 1',
slug: 'test 1',
originalUrl: `${url}/test1`,
wordCount: 10000,
},
{
user,
title: 'test 2',
slug: 'test 2',
originalUrl: `${url}/test2`,
wordCount: 20000,
},
{
user,
title: 'test 3',
slug: 'test 3',
originalUrl: `${url}/test3`,
wordCount: 30000,
},
],
user.id
)
})
after(async () => {
await deleteLibraryItems(items, user.id)
})
it('returns items in descending order of word count', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.search.pageInfo.totalCount).to.eql(3)
expect(res.body.data.search.edges[0].node.id).to.eq(items[2].id)
expect(res.body.data.search.edges[1].node.id).to.eq(items[1].id)
expect(res.body.data.search.edges[2].node.id).to.eq(items[0].id)
})
})
})
describe('TypeaheadSearch API', () => {
const items: LibraryItem[] = []
let query = ''
let keyword = 'typeahead'
before(async () => {
// Create some test items
for (let i = 0; i < 5; i++) {
const itemToSave: DeepPartial<LibraryItem> = {
user,
title: 'typeahead search item',
readableContent: '<p>test</p>',
slug: '',
originalUrl: `https://blog.omnivore.app/p/typeahead-search-${i}`,
}
const item = await createLibraryItem(itemToSave, user.id)
items.push(item)
}
})
beforeEach(async () => {
query = typeaheadSearchQuery(keyword)
})
after(async () => {
await deleteLibraryItemsByUserId(user.id)
})
it('returns items with typeahead prefix', async () => {
const res = await graphqlRequest(query, authToken).expect(200)
expect(res.body.data.typeaheadSearch.items.length).to.eql(5)
expect(res.body.data.typeaheadSearch.items[0].id).to.eq(items[4].id)
expect(res.body.data.typeaheadSearch.items[1].id).to.eq(items[3].id)
expect(res.body.data.typeaheadSearch.items[2].id).to.eq(items[2].id)
expect(res.body.data.typeaheadSearch.items[3].id).to.eq(items[1].id)
expect(res.body.data.typeaheadSearch.items[4].id).to.eq(items[0].id)
})
})
describe('UpdatesSince API', () => {
const updatesSinceQuery = (since: string) => `
query {
updatesSince(
since: "${since}") {
... on UpdatesSinceSuccess {
edges {
cursor
node {
id
createdAt
updatedAt
pageType
highlights {
id
type
}
}
itemID
updateReason
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
totalCount
}
}
... on UpdatesSinceError {
errorCodes
}
}
}
`
let since: string
let items: LibraryItem[] = []
let deletedItems: LibraryItem[] = []
before(async () => {
// Create some test items
for (let i = 0; i < 5; i++) {
const itemToSave: DeepPartial<LibraryItem> = {
title: 'test item',
slug: '',
readableContent: '<p>test</p>',
originalUrl: `https://blog.omnivore.app/p/updates-since-${i}`,
user,
}
const item = await createLibraryItem(itemToSave, user.id)
items.push(item)
}
// set the since to be the timestamp before deletion
since = items[4].updatedAt.toISOString()
// Delete some items
for (let i = 0; i < 3; i++) {
await updateLibraryItem(
items[i].id,
{ state: LibraryItemState.Deleted, deletedAt: new Date() },
user.id
)
deletedItems.push(items[i])
}
})
after(async () => {
// Delete all items
await deleteLibraryItemsByUserId(user.id)
})
it('returns items deleted after since', async () => {
const res = await graphqlRequest(
updatesSinceQuery(since),
authToken
).expect(200)
expect(
res.body.data.updatesSince.edges.filter(
(e: SyncUpdatedItemEdge) => e.updateReason === UpdateReason.Deleted
).length
).to.eql(3)
expect(res.body.data.updatesSince.edges[0].itemID).to.eq(
deletedItems[2].id
)
expect(res.body.data.updatesSince.edges[1].itemID).to.eq(
deletedItems[1].id
)
expect(res.body.data.updatesSince.edges[2].itemID).to.eq(
deletedItems[0].id
)
expect(res.body.data.updatesSince.edges[0].updateReason).to.eq(
UpdateReason.Deleted
)
})
context(
'when since is -1000000000-01-01T00:00:00Z from android app',
() => {
before(() => {
since = '-1000000000-01-01T00:00:00Z'
})
it('returns all', async () => {
const res = await graphqlRequest(
updatesSinceQuery(since),
authToken
).expect(200)
expect(res.body.data.updatesSince.edges.length).to.eql(5)
})
}
)
context('returns highlights', () => {
let highlight: Highlight
before(async () => {
highlight = await createHighlight(
{
user,
shortId: 'test short id',
quote: 'test',
libraryItem: items[0],
},
items[0].id,
user.id
)
})
it('returns highlights', async () => {
const res = await graphqlRequest(
updatesSinceQuery(since),
authToken
).expect(200)
expect(res.body.data.updatesSince.edges[0].node.highlights[0].id).to.eq(
highlight.id
)
expect(
res.body.data.updatesSince.edges[0].node.highlights[0].type
).to.eq(HighlightType.Highlight)
})
})
})
describe('BulkAction API', () => {
const bulkActionQuery = (action: BulkActionType, query = 'in:all') => `
mutation {
bulkAction (action: ${action}, query: "${query}") {
... on BulkActionSuccess {
success
}
... on BulkActionError {
errorCodes
}
}
}
`
context('when action is MarkAsRead and query is in:unread', () => {
before(async () => {
// Create some test items
for (let i = 0; i < 5; i++) {
await createLibraryItem(
{
user,
itemType: i == 0 ? PageType.Article : PageType.File,
title: 'test item',
readableContent: '<p>test</p>',
slug: '',
state:
i == 0 ? LibraryItemState.Failed : LibraryItemState.Succeeded,
originalUrl: `https://blog.omnivore.app/p/bulk-action-${i}`,
},
user.id
)
}
})
after(async () => {
// Delete all items
await deleteLibraryItemsByUserId(user.id)
})
it('marks unread items as read', async () => {
const res = await graphqlRequest(
bulkActionQuery(BulkActionType.MarkAsRead, 'is:unread'),
authToken
).expect(200)
expect(res.body.data.bulkAction.success).to.be.true
const items = await graphqlRequest(
searchQuery('is:unread'),
authToken
).expect(200)
expect(items.body.data.search.pageInfo.totalCount).to.eql(0)
})
})
context(
'when action is Archive and query is published:*..2023-10-01',
() => {
let items: LibraryItem[] = []
before(async () => {
items = await createLibraryItems(
[
{
user,
title: 'test item',
readableContent: '<p>test</p>',
slug: 'test-item',
originalUrl: `https://blog.omnivore.app/p/bulk-action-archive`,
publishedAt: new Date('2023-10-01'),
},
{
user,
title: 'test item 2',
readableContent: '<p>test</p>',
slug: 'test-item-2',
originalUrl: `https://blog.omnivore.app/p/bulk-action-archive-2`,
publishedAt: new Date('2023-10-02'),
},
],
user.id
)
})
after(async () => {
// Delete all items
await deleteLibraryItems(items, user.id)
})
it('archives old items', async () => {
const res = await graphqlRequest(
bulkActionQuery(BulkActionType.Archive, 'published:*..2023-10-01'),
authToken
).expect(200)
expect(res.body.data.bulkAction.success).to.be.true
const response = await graphqlRequest(
searchQuery('in:archive'),
authToken
).expect(200)
expect(response.body.data.search.pageInfo.totalCount).to.eql(1)
expect(response.body.data.search.edges[0].node.id).to.eql(items[0].id)
})
}
)
context('when action is Delete and query contains item id', () => {
let items: LibraryItem[] = []
before(async () => {
// Create some test items
for (let i = 0; i < 5; i++) {
const item = await createLibraryItem(
{
user,
itemType: i == 0 ? PageType.Article : PageType.File,
title: 'test item',
readableContent: '<p>test</p>',
slug: '',
state:
i == 0 ? LibraryItemState.Failed : LibraryItemState.Succeeded,
originalUrl: `https://blog.omnivore.app/p/bulk-action-${i}`,
},
user.id
)
items.push(item)
}
})
after(async () => {
// Delete all items
await deleteLibraryItemsByUserId(user.id)
})
it('deletes all items', async () => {
const query = `includes:${items.map((i) => i.id).join(',')}`
const res = await graphqlRequest(
bulkActionQuery(BulkActionType.Delete, query),
authToken
).expect(200)
expect(res.body.data.bulkAction.success).to.be.true
const response = await graphqlRequest(
searchQuery(query),
authToken
).expect(200)
expect(response.body.data.search.pageInfo.totalCount).to.eql(0)
})
})
})
describe('SetFavoriteArticle API', () => {
const setFavoriteArticleQuery = (articleId: string) => `
mutation {
setFavoriteArticle(id: "${articleId}") {
... on SetFavoriteArticleSuccess {
success
}
... on SetFavoriteArticleError {
errorCodes
}
}
}`
let articleId = ''
before(async () => {
const itemToSave: DeepPartial<LibraryItem> = {
user,
title: 'test setFavoriteArticle',
slug: '',
readableContent: '<p>test</p>',
originalUrl: `https://blog.omnivore.app/p/setFavoriteArticle`,
}
const item = await createLibraryItem(itemToSave, user.id)
articleId = item.id
})
after(async () => {
// Delete the item
await deleteLibraryItemById(articleId, user.id)
})
it('favorites the article', async () => {
await graphqlRequest(
setFavoriteArticleQuery(articleId),
authToken
).expect(200)
const item = await findLibraryItemById(articleId, user.id)
expect(item?.labels?.map((l) => l.name)).to.eql(['Favorites'])
})
})
describe('EmptyTrash API', () => {
const emptyTrashQuery = () => `
mutation {
emptyTrash {
... on EmptyTrashSuccess {
success
}
... on EmptyTrashError {
errorCodes
}
}
}`
let items: LibraryItem[] = []
before(async () => {
// Create some test items
for (let i = 0; i < 5; i++) {
const itemToSave: DeepPartial<LibraryItem> = {
user,
title: 'test item',
readableContent: '<p>test</p>',
slug: '',
originalUrl: `https://blog.omnivore.app/p/empty-trash-${i}`,
deletedAt: new Date(),
state: LibraryItemState.Deleted,
}
const item = await createLibraryItem(itemToSave, user.id)
items.push(item)
}
})
after(async () => {
// Delete all items
await deleteLibraryItemsByUserId(user.id)
})
it('empties the trash', async () => {
let response = await graphqlRequest(
searchQuery('in:trash'),
authToken
).expect(200)
expect(response.body.data.search.pageInfo.totalCount).to.eql(5)
await graphqlRequest(emptyTrashQuery(), authToken).expect(200)
response = await graphqlRequest(
searchQuery('in:trash'),
authToken
).expect(200)
expect(response.body.data.search.pageInfo.totalCount).to.eql(0)
})
})
})