web/test.js
const assert = require('assert');
const { WebApi } = require('ourbigbook/web_api')
const {
assertArraysEqual,
assertRows,
assert_xpath,
} = require('ourbigbook/test_lib')
const ourbigbook = require('ourbigbook')
const app = require('./app')
const config = require('./front/config')
const routes = require('./front/routes')
const convert = require('./convert')
const models = require('./models')
const test_lib = require('./test_lib')
const { AUTH_COOKIE_NAME } = require('./front/js')
const web_api = require('ourbigbook/web_api');
const { QUERY_TRUE_VAL } = web_api
const testNext = process.env.OURBIGBOOK_TEST_NEXT === 'true'
async function assertNestedSets(sequelize, expects) {
const articles = await sequelize.models.Article.treeFindInOrder({
includeNulls: true,
refs: true
})
let i = 0
const articleObjs = []
for (const article of articles) {
const expect = expects[i]
const articleObj = {}
for (const key in expect) {
if (key !== 'to_id_index') {
articleObj[key] = article.get(key)
}
}
const ref = article.file.toplevelId
let to_id_index, parentId
if (ref === null) {
to_id_index = null
parentId = null
} else {
const tos = ref.to
assert.strictEqual(tos.length, 1)
to_id_index = tos[0].to_id_index
parentId = tos[0].from_id
}
articleObj.to_id_index = to_id_index
articleObj.parentId = parentId
articleObjs.push(articleObj)
i++
}
assertRows(
articleObjs,
expects,
{
msgFn: () => 'actual:\nnestedSetIndex, nestedSetNextSibling, depth, to_id_index, slug, parentId\n' +
articleObjs.map(a => `${a.nestedSetIndex}, ${a.nestedSetNextSibling}, ${a.depth}, ${a.to_id_index}, ${a.slug}, ${a.parentId}`).join('\n')
}
)
}
// assertRows helpers.
const ne = (expect) => (v) => v !== expect
async function assertNestedSetsMultiuser(sequelize, users, rows) {
const newRows = []
for (const user of users) {
for (const row of rows) {
const slug = row.slug
let sep
if (slug) {
sep = '/'
} else {
sep = ''
}
const newRow = Object.assign({}, row)
newRow.slug = user.username + sep + slug
newRows.push(newRow)
}
}
return assertNestedSets(sequelize, newRows)
}
// 200 status assertion helper that also prints the data to help
// quickly see what the error is about.
function assertStatus(status, data) {
if (status !== 200) {
console.error(require('util').inspect(data));
assert.strictEqual(status, 200)
}
}
/** We set the parent to index by default. To leave it unset,
* e.g. to set it implicitly via previousSiblingId you need to explicitly set
* parentId to undefined. It's quite bad. */
async function createArticleApi(test, article, opts={}, reqOpts={}) {
if (!opts.hasOwnProperty('parentId') && test.user) {
opts = Object.assign({ parentId: `${ourbigbook.AT_MENTION_CHAR}${test.user.username}` }, opts)
}
return test.webApi.articleCreate(article, opts, reqOpts)
}
async function createOrUpdateArticleApi(test, article, opts={}, reqOpts={}) {
if (
!opts.hasOwnProperty('parentId') &&
test.user &&
// This is just a heuristic to detect index editing. Index can also be achieved e.g. with {id=},
// but let's KISS it for now.
article.titleSource !== ''
) {
opts = Object.assign({ parentId: `${ourbigbook.AT_MENTION_CHAR}${test.user.username}` }, opts)
}
return test.webApi.articleCreateOrUpdate(article, opts, reqOpts)
}
async function createArticles(sequelize, author, opts) {
const articleArg = createArticleArg(opts, author)
const { articles } = await convert.convertArticle({
author,
bodySource: articleArg.bodySource,
path: opts.path,
parentId: articleArg.parentId || `${ourbigbook.AT_MENTION_CHAR}${author.username}`,
render: opts.render,
sequelize,
titleSource: articleArg.titleSource,
})
return articles
}
async function createArticle(sequelize, author, opts) {
return (await createArticles(sequelize, author, opts))[0]
}
function createArticleArg(opts, author) {
const i = opts.i
const ret = {}
if (opts.hasOwnProperty('titleSource')) {
ret.titleSource = opts.titleSource
} else {
ret.titleSource = `Title ${i}`
}
if (opts.hasOwnProperty('bodySource')) {
ret.bodySource = opts.bodySource
} else {
ret.bodySource = `Body ${i}\.`
}
if (author) {
ret.authorId = author.id
}
ret.parentId = opts.parentId
return ret
}
async function createArticleApiMultiuser(test, users, articleArg, meta={}) {
for (const user of users) {
test.loginUser(user)
const parentId = meta.parentId
const newMeta = Object.assign({}, meta)
if (parentId) {
newMeta.parentId = `@${user.username}/${parentId}`
}
;({data, status} = await createArticleApi(test, articleArg, newMeta))
assertStatus(status, data)
}
}
function createIssueArg(i, j, k, opts={}) {
const ret = {
titleSource: `The \\i[title] ${i} ${j} ${k}.`,
bodySource: `The \\i[body] ${i} ${j} ${k}.`,
}
if (opts.titleSource !== undefined) {
ret.titleSource = opts.titleSource
}
if (opts.bodySource !== undefined) {
ret.bodySource = opts.bodySource
}
return ret
}
async function createUser(sequelize, i) {
const user = new sequelize.models.User(createUserArg(i, { password: false }))
sequelize.models.User.setPassword(user, 'asdf')
return user.save()
}
function createUserArg(i, opts={}) {
let { password } = opts
if (password === undefined) {
password = true
}
const ret = {
email: `user${i}@mail.com`,
username: `user${i}`,
displayName: `User ${i}`,
}
if (opts.username !== undefined) {
ret.username = opts.username
}
if (opts.displayName !== undefined) {
ret.displayName = opts.displayName
}
if (opts.email !== undefined) {
ret.email = opts.email
}
if (password) {
ret.password = 'asdf'
}
return ret
}
// https://stackoverflow.com/questions/8175093/simple-function-to-sort-an-array-of-objects
function sortByKey(arr, key) {
return arr.sort((a, b) => {
let x = a[key]
var y = b[key]
return ((x < y) ? -1 : ((x > y) ? 1 : 0));
})
}
function testApp(cb, opts={}) {
let {
canTestNext,
// If given then all api requests automatically check this status, usually 200.
// Then if you want to assert a non-200 status inside that test, you have to
// pass "expectStatus" to the corresponding API call.
//
// Once every test is migrated to use 200, we'll just make it default.
// This should have been our initial approach to start with. It just requires
// some work of adding the extra parameter to every single api call.
defaultExpectStatus,
} = opts
canTestNext = canTestNext === undefined ? false : canTestNext
return app.start(0, canTestNext && testNext, async (server, sequelize, app) => {
const test = {
app,
sequelize
}
test.user = undefined
test.userSave = undefined
test.loginUser = function(newUser) {
if (newUser) {
test.user = newUser
test.userSave = newUser
} else {
test.user = test.userSave
}
}
test.disableToken = function() {
test.user = undefined
}
const jsonHttpOpts = {
expectStatus: defaultExpectStatus,
getToken: function () { return test.user ? test.user.token : undefined },
https: false,
port: server.address().port,
hostname: 'localhost',
validateStatus: () => true,
}
test.sendJsonHttp = async function (method, path, opts={}) {
const { body, useToken } = opts
let token, headers = {}
if (useToken === undefined || useToken) {
token = test.user ? test.user.token : undefined
if (token) {
// So we can test logged-in Next.js GET requests.
// This is never done on the actual website from js (cookies are sent by browser automatically only).
headers.Cookie = `${AUTH_COOKIE_NAME}=${token}`
}
} else {
token = undefined
}
return web_api.sendJsonHttp(
method,
path,
Object.assign({ body, headers }, jsonHttpOpts)
)
}
// Create user and save the token for future requests.
test.createUserApi = async function(i, opts) {
const { data, status } = await test.webApi.userCreate(createUserArg(i, opts))
assertStatus(status, data)
test.tokenSave = data.user.token
test.loginUser()
assert.strictEqual(data.user.username, `user${i}`)
return data.user
}
test.webApi = new WebApi(jsonHttpOpts)
await cb(test)
server.close()
})
}
beforeEach(async function () {
this.currentTest.sequelize = await test_lib.generateDemoData({ empty: true })
})
afterEach(async function () {
return this.currentTest.sequelize.close()
})
it('User.findAndCountArticlesByFollowed', async function() {
const sequelize = this.test.sequelize
const user0 = await createUser(sequelize, 0)
const user1 = await createUser(sequelize, 1)
const user2 = await createUser(sequelize, 2)
const user3 = await createUser(sequelize, 3)
await (user0.addFollowSideEffects(user1))
await (user0.addFollowSideEffects(user3))
const article0_0 = await createArticle(sequelize, user0, { i: 0 })
const article1_0 = await createArticle(sequelize, user1, { i: 0 })
const article1_1 = await createArticle(sequelize, user1, { i: 1 })
const article1_2 = await createArticle(sequelize, user1, { i: 2 })
const article1_3 = await createArticle(sequelize, user1, { i: 3 })
const article2_0 = await createArticle(sequelize, user2, { i: 0 })
const article2_1 = await createArticle(sequelize, user2, { i: 1 })
const article3_0 = await createArticle(sequelize, user3, { i: 0 })
const article3_1 = await createArticle(sequelize, user3, { i: 1 })
const { count, rows } = await user0.findAndCountArticlesByFollowed(1, 3)
assert.strictEqual(rows[0].titleRender, 'Title 0')
assert.strictEqual(rows[0].file.authorId, user3.id)
assert.strictEqual(rows[1].titleRender, 'Title 3')
assert.strictEqual(rows[1].file.authorId, user1.id)
assert.strictEqual(rows[2].titleRender, 'Title 2')
assert.strictEqual(rows[2].file.authorId, user1.id)
assert.strictEqual(rows.length, 3)
// 6 manually from all follows + 2 for the automatically created indexes.
assert.strictEqual(count, 8)
})
it('Article.getArticlesInSamePage simple', async function test_Article__getArticlesInSamePage() {
let rows
let article_0_0, article_0_0_0, article_1_0, article
const sequelize = this.test.sequelize
const { Article } = sequelize.models
const user0 = await createUser(sequelize, 0)
const user1 = await createUser(sequelize, 1)
// Create some articles.
await createArticle(sequelize, user0, { titleSource: 'Title 0' })
await createArticle(sequelize, user0, { titleSource: 'Title 0 1', parentId: '@user0/title-0' })
await createArticle(sequelize, user0, {
titleSource: 'Title 0 0',
parentId: '@user0/title-0',
bodySource: '{tag=Title 0 1}\n'
})
await createArticle(sequelize, user0, { titleSource: 'Title 0 0 0', parentId: '@user0/title-0-0' })
// Single user tests.
article = await Article.getArticle({ sequelize, slug: 'user0/title-0' })
rows = await Article.getArticlesInSamePage({
article,
getTagged: true,
loggedInUser: user0,
sequelize,
})
assertRows(rows, [
{ slug: 'user0/title-0-0', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
{ slug: 'user0/title-0-0-0', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
{ slug: 'user0/title-0-1', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
])
assertRows(rows[2].taggedArticles, [
{ slug: 'user0/title-0-0' },
])
// Hidden articles don't show by default.
await Article.update({ list: false }, { where: { slug: 'user0/title-0-0' } })
article = await Article.getArticle({ sequelize, slug: 'user0/title-0' })
rows = await Article.getArticlesInSamePage({ sequelize, article, loggedInUser: user0, list: true })
assertRows(rows, [
{ slug: 'user0/title-0-0-0', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
{ slug: 'user0/title-0-1', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
])
await Article.update({ list: true }, { where: { slug: 'user0/title-0-0' } })
article = await Article.getArticle({ sequelize, slug: 'user0/title-0' })
rows = await Article.getArticlesInSamePage({ sequelize, article, loggedInUser: user0, h1: true })
assertRows(rows, [
{ slug: 'user0/title-0', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
])
article = await Article.getArticle({ sequelize, slug: 'user0/title-0-0' })
rows = await Article.getArticlesInSamePage({ sequelize, article, loggedInUser: user0 })
assertRows(rows, [
{ slug: 'user0/title-0-0-0', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
])
article = await Article.getArticle({ sequelize, slug: 'user0/title-0-0-0' })
rows = await Article.getArticlesInSamePage({ sequelize, article, loggedInUser: user0 })
assertRows(rows, [])
await createArticle(sequelize, user1, { titleSource: 'Title 0' })
await createArticle(sequelize, user1, { titleSource: 'Title 0 1', parentId: '@user1/title-0' })
await createArticle(sequelize, user1, { titleSource: 'Title 0 1 0', parentId: '@user1/title-0-1' })
await createArticle(sequelize, user1, { titleSource: 'Title 0 0', parentId: '@user1/title-0' })
// We have to refetch here because the counts involved are changed by other articles/issues/likes.
article_0_0 = await Article.getArticle({ sequelize, slug: 'user0/title-0' })
article_0_0_0 = await Article.getArticle({ sequelize, slug: 'user0/title-0-0' })
article_1_0 = await Article.getArticle({ sequelize, slug: 'user1/title-0' })
// User1 likes user0/title-0-0
await user1.addArticleLikeSideEffects(article_0_0_0)
// Add an issue to Title 0 0 0.
await convert.convertDiscussion({
article: article_0_0_0,
bodySource: '',
number: 1,
sequelize,
titleSource: 'a',
user: user0
})
// Multi user tests.
rows = await Article.getArticlesInSamePage({
sequelize,
article: article_0_0,
loggedInUser: user0,
})
assertRows(rows, [
{ slug: 'user0/title-0-0', topicCount: 2, issueCount: 1, hasSameTopic: true, liked: false },
{ slug: 'user0/title-0-0-0', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
{ slug: 'user0/title-0-1', topicCount: 2, issueCount: 0, hasSameTopic: true, liked: false },
])
rows = await Article.getArticlesInSamePage({
sequelize,
article: article_0_0,
loggedInUser: user1,
})
assertRows(rows, [
{ slug: 'user0/title-0-0', topicCount: 2, issueCount: 1, hasSameTopic: true, liked: true },
{ slug: 'user0/title-0-0-0', topicCount: 1, issueCount: 0, hasSameTopic: false, liked: false },
{ slug: 'user0/title-0-1', topicCount: 2, issueCount: 0, hasSameTopic: true, liked: false },
])
rows = await Article.getArticlesInSamePage({
sequelize,
article: article_1_0,
loggedInUser: user0,
})
assertRows(rows, [
{ slug: 'user1/title-0-0', topicCount: 2, issueCount: 0, hasSameTopic: true, liked: false },
{ slug: 'user1/title-0-1', topicCount: 2, issueCount: 0, hasSameTopic: true, liked: false },
{ slug: 'user1/title-0-1-0', topicCount: 1, issueCount: 0, hasSameTopic: false, liked: false },
])
rows = await Article.getArticlesInSamePage({
sequelize,
article: article_1_0,
loggedInUser: user1,
})
assertRows(rows, [
{ slug: 'user1/title-0-0', topicCount: 2, issueCount: 0, hasSameTopic: true, liked: false },
{ slug: 'user1/title-0-1', topicCount: 2, issueCount: 0, hasSameTopic: true, liked: false },
{ slug: 'user1/title-0-1-0', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
])
})
it('Article.getArticlesInSamePage with render=false then render=true', async function test_Article__getArticlesInSamePageRenderFalse() {
let rows
let article_0_0, article_0_0_0, article_1_0, article
const sequelize = this.test.sequelize
const { Article } = sequelize.models
const user0 = await createUser(sequelize, 0)
// Create some articles.
for (const render of [false, true]) {
await createArticle(sequelize, user0, { render, titleSource: 'Title 0' })
await createArticle(sequelize, user0, { render, titleSource: 'Title 0 1', parentId: '@user0/title-0' })
await createArticle(sequelize, user0, {
titleSource: 'Title 0 0',
bodySource: '{tag=Title 0 1}\n',
parentId: '@user0/title-0',
render,
})
await createArticle(sequelize, user0, { render, titleSource: 'Title 0 0 0', parentId: '@user0/title-0-0' })
}
// Test it.
article = await Article.getArticle({ sequelize, slug: 'user0/title-0' })
rows = await Article.getArticlesInSamePage({
article,
getTagged: true,
loggedInUser: user0,
sequelize,
})
assertRows(rows, [
{ slug: 'user0/title-0-0', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
{ slug: 'user0/title-0-0-0', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
{ slug: 'user0/title-0-1', topicCount: 1, issueCount: 0, hasSameTopic: true, liked: false },
])
assertRows(rows[2].taggedArticles, [
{ slug: 'user0/title-0-0' },
])
})
it('Article.rerender', async function() {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const { Article } = sequelize.models
const user = await test.createUserApi(0)
test.loginUser(user)
// Create articles
article = createArticleArg({
i: 0,
titleSource: 'Mathematics',
// We had some predefined ourbigbook KaTeX macro issues in the past.
bodySource: `$$
\\abs{x}
$$
`,
})
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Physics' })
;({data, status} = await createArticleApi(test, article, { parentId: undefined, previousSiblingId: '@user0/mathematics' }))
assertStatus(status, data)
const physicsArticle = data.articles[0]
const physicsHash = physicsArticle.file.hash
// Sanity check.
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 1, to_id_index: 1, slug: 'user0/physics' },
])
// Rerender does not set previousSibligId to undefined (thus moving article as first child).
await Article.rerender({ slugs: ['user0/physics'] })
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 1, to_id_index: 1, slug: 'user0/physics' },
])
// Rerender does not modify the article hash. Was happening because we were calculating hash
// with previousSiblingId undefined https://github.com/ourbigbook/ourbigbook/issues/322
;({data, status} = await test.webApi.article('user0/physics'))
assertStatus(status, data)
assert.strictEqual(data.file.hash, physicsHash)
// It also does not modify updatedAt.
assert.strictEqual(data.updatedAt, physicsArticle.updatedAt)
// Works with OurBigBook predefined macros.
await Article.rerender({ slugs: ['user0/mathematics'] })
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 1, to_id_index: 1, slug: 'user0/physics' },
])
// Works for root article.
await Article.rerender({ slugs: ['user0'] })
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 1, to_id_index: 1, slug: 'user0/physics' },
])
})
})
it('normalize nested-set', async function() {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
// Create articles
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Physics' })
;({data, status} = await createArticleApi(test, article, { parentId: undefined, previousSiblingId: '@user0/mathematics' }))
assertStatus(status, data)
// The nested-set for the API is correctly normalized.
await models.normalize({
check: true,
sequelize,
whats: ['nested-set'],
})
})
})
it('Article.updateTopicsNewArticles', async function() {
const sequelize = this.test.sequelize
async function getTopicIds(topicIds, count=true) {
const ret = await sequelize.models.Topic.getTopics({
sequelize,
articleOrder: 'topicId',
articleWhere: { topicId: topicIds },
count,
})
if (count) {
return ret.rows
} else {
return ret
}
}
const nArticles = config.topicConsiderNArticles + 1
const users = []
for (let i = 0; i < nArticles; i++) {
users.push(await createUser(sequelize, i))
}
const articles = []
articles.push(await createArticle(sequelize, users[0], { i: 0 }))
assertRows(
await getTopicIds(['title-0']),
[{ articleId: articles[0].id, articleCount: 1 }]
)
// Also check that count: false works.
assertRows(
await getTopicIds(['title-0'], false),
[{ articleId: articles[0].id, articleCount: 1 }]
)
// Article update does not increment the Topic.articleCount.
await createArticle(sequelize, users[0], { i: 0, body: 'Body 0 hacked' })
assertRows(
await getTopicIds(['title-0']),
[{ articleId: articles[0].id, articleCount: 1 }]
)
articles.push(await createArticle(sequelize, users[1], { i: 99, path: 'title-0' }))
// The topic title-0 is tied with two different titles, "Title 0" and "Title 99".
// So we keep the oldest one, "Title 0".
assertRows(
await getTopicIds(['title-0']),
[{ articleId: articles[0].id, articleCount: 2 }]
)
articles.push(await createArticle(sequelize, users[2], { i: 99, path: 'title-0' }))
// This broke the above tie, now "Title 99" became the representative title with 2 entries,
// so we update the topic to point to the oldest "Title 99".
assertRows(
await getTopicIds(['title-0']),
[{ articleId: articles[1].id, articleCount: 3 }]
)
for (let i = 3; i < 7; i++) {
articles.push(await createArticle(sequelize, users[i], { i: 99, path: 'title-0' }))
}
for (let i = 7; i < nArticles; i++) {
articles.push(await createArticle(sequelize, users[i], { i: 0, path: 'title-0' }))
}
// Now we have 6 "Title 99" and 5 "Title 0". All have score 0.
// Only top 10 most voted are considered, but we take lower IDs first,
// and the Title 99 are IDs 1 through 6, so it wins, 6 to 4.
assertRows(
await getTopicIds(['title-0']),
[{ articleId: articles[1].id, articleCount: nArticles }]
)
// So let's upvote the 4 trailing ones to bring them up in score.
// This will bring the count 5 to 5, and "Title 0" will win because
// it holds the smallest ID 0.
for (let i = 7; i < nArticles; i++) {
await users[0].addArticleLikeSideEffects(articles[i])
}
assertRows(
await getTopicIds(['title-0']),
[{ articleId: articles[0].id, articleCount: nArticles }]
)
})
it('api: create an article and see it on global feed', async () => {
await testApp(async (test) => {
let data, status, article
// User
// Create user errors
// Invalid username: too short
;({ data, status } = await test.webApi.userCreate(createUserArg(0, { username: 'a'.repeat(config.usernameMinLength - 1) })))
assert.strictEqual(status, 422)
// Invalid username: too long
;({ data, status } = await test.webApi.userCreate(createUserArg(0, { username: 'a'.repeat(config.usernameMaxLength + 1) })))
assert.strictEqual(status, 422)
// Invalid username char: _
;({ data, status } = await test.webApi.userCreate(createUserArg(0, { username: 'ab_cd' })))
assert.strictEqual(status, 422)
// Invalid username char: uppercase
;({ data, status } = await test.webApi.userCreate(createUserArg(0, { username: 'abCd' })))
assert.strictEqual(status, 422)
// Invalid username: starts in -, ends in -, double -
;({ data, status } = await test.webApi.userCreate(createUserArg(0, { username: '-abcd' })))
assert.strictEqual(status, 422)
;({ data, status } = await test.webApi.userCreate(createUserArg(0, { username: 'abcd-' })))
assert.strictEqual(status, 422)
;({ data, status } = await test.webApi.userCreate(createUserArg(0, { username: 'ab--cd' })))
assert.strictEqual(status, 422)
// Create users
const user = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
const user2 = await test.createUserApi(2)
// Make user2 admin via direct DB access (the only way).
await test.sequelize.models.User.update({ admin: true }, { where: { username: 'user2' } })
test.loginUser(user)
// User GET
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assertRows([data], [{ username: 'user0', displayName: 'User 0' }])
// Edit users
;({data, status} = await test.webApi.userUpdate('user0', { displayName: 'User 0 hacked' }))
assertStatus(status, data)
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assertRows([data], [{ username: 'user0', displayName: 'User 0 hacked' }])
// Non-admin users cannot edit other users.
test.loginUser(user1)
;({data, status} = await test.webApi.userUpdate('user0', { displayName: 'User 0 hacked 2' }))
assert.strictEqual(status, 403)
test.loginUser(user)
// Admin users can edit other users.
test.loginUser(user2)
;({data, status} = await test.webApi.userUpdate('user0', { displayName: 'User 0 hacked 3' }))
assertStatus(status, data)
test.loginUser(user)
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assertRows([data], [{ username: 'user0', displayName: 'User 0 hacked 3' }])
// Users see their own email on GET.
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assertRows([data], [{ email: 'user0@mail.com' }])
// Non-admin users don't see other users' email on GET.
;({data, status} = await test.webApi.user('user1'))
assertStatus(status, data)
assert.strictEqual(data.email, undefined)
// Admin users see other users emails on GET.
test.loginUser(user2)
;({data, status} = await test.webApi.user('user1'))
assertStatus(status, data)
assertRows([data], [{ email: 'user1@mail.com' }])
test.loginUser(user)
// Cannot modify username.
;({data, status} = await test.webApi.userUpdate('user0', { username: 'user0hacked' }))
assert.strictEqual(status, 422)
// Cannot modify email.
// TODO https://github.com/ourbigbook/ourbigbook/issues/268
// Once the above is fixed, this will likely just be allowed on dev mode and then
// we will just remove this test.
;({data, status} = await test.webApi.userUpdate('user0', { email: 'user0hacked@mail.com' }))
assert.strictEqual(status, 422)
// Create article in one go
article = createArticleArg({ i: 0 })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
assertRows(data.articles, [{
// New articles are listed by default.
list: true,
titleRender: 'Title 0',
}])
// Create article errors
// Cannot create article if logged out.
test.disableToken()
article = createArticleArg({ i: 1 })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 401)
test.loginUser()
// Cannot create article if token is given but wrong.
test.loginUser('asdfqwer')
article = createArticleArg({ i: 1 })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 401)
test.loginUser(user)
// Recreating an article with POST is not allowed.
article = createArticleArg({ i: 0, bodySource: 'Body 1' })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 422)
// Wrong field type.
;({data, status} = await createArticleApi(test, { titleSource: 1, bodySource: 'Body 1' }))
assert.strictEqual(status, 422)
// Missing title and no path existing article to take it from
;({data, status} = await createArticleApi(test, { bodySource: 'Body 1' }))
assert.strictEqual(status, 422)
// Newline in title
;({data, status} = await createArticleApi(test, { titleSource: 'a\nb', bodySource: 'Body 1' }))
assert.strictEqual(status, 422)
// Newline in literal in title
;({data, status} = await createArticleApi(test, {
titleSource: 'a `',
bodySource: '` b'
}))
assert.strictEqual(status, 422)
// Title ending in backslash is an error because it adds newline to shorthand header
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({
titleSource: 'ab\\',
bodySource: 'cd',
})))
assert.strictEqual(status, 422)
// Missing all data and no path to existing article to take it from
;({data, status} = await createArticleApi(test, {}))
assert.strictEqual(status, 422)
// Missing data, has path to existing article, but is not render.
// Doesn't make sense as no changes can come from this.
;({data, status} = await createArticleApi(test, {}, { path: 'title-0' }))
assert.strictEqual(status, 422)
// Markup errors.
;({data, status} = await createArticleApi(test, {
titleSource: 'The \\notdefined', bodySource: 'The \\i[body]' }))
assert.strictEqual(status, 422)
;({data, status} = await createArticleApi(test,
{ titleSource: 'Error', bodySource: 'The \\notdefined' }))
assert.strictEqual(status, 422)
// View articles.
// Access the article directly
;({data, status} = await test.webApi.article('user0/title-0'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Title 0')
assert.strictEqual(data.titleSource, 'Title 0')
assert.match(data.render, /Body 0\./)
// See articles on global feed.
;({data, status} = await test.webApi.articles())
assertStatus(status, data)
assertRows(data.articles, [
{ titleRender: 'Title 0', slug: 'user0/title-0', render: /Body 0/ },
{ titleRender: ourbigbook.HTML_HOME_MARKER, slug: 'user2' },
{ titleRender: ourbigbook.HTML_HOME_MARKER, slug: 'user1' },
{ titleRender: ourbigbook.HTML_HOME_MARKER, slug: 'user0' },
])
// See latest articles by a user.
;({data, status} = await test.webApi.articles({ author: 'user0' }))
assertStatus(status, data)
assertRows(data.articles, [
{ titleRender: 'Title 0', slug: 'user0/title-0', render: /Body 0/ },
{ titleRender: ourbigbook.HTML_HOME_MARKER, slug: 'user0' },
])
// Edit article.
article = createArticleArg({ i: 0, bodySource: 'Body 0 hacked.' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
assertRows(data.articles, [{ render: /Body 0 hacked\./ }])
;({data, status} = await test.webApi.article('user0/title-0'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Title 0')
assert.match(data.render, /Body 0 hacked\./)
// Undo it for test sanity.
article = createArticleArg({ i: 0 })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
assertRows(data.articles, [{ render: /Body 0\./ }])
// Edit article with render: false followed by render: true without parameters.
// Take bodySource parameter from the database state of the previous render: false.
// render: false
article = createArticleArg({ i: 0, bodySource: 'Body 0 hacked.' })
;({data, status} = await createOrUpdateArticleApi(test,
article,
{ path: 'title-0', render: false }
))
assertStatus(status, data)
// Maybe we could return the pre-existing article here.
assertRows(data.articles, [])
// Also take this chance to check that /hash renderOutdated is correct.
;({data, status} = await test.webApi.articlesHash({ author: 'user0' }))
assertStatus(status, data)
assertRows(data.articles, [
{ path: '@user0/index.bigb', renderOutdated: false, },
{ path: '@user0/title-0.bigb', renderOutdated: true, },
])
// render: true
article = createArticleArg({ i: 0, bodySource: undefined })
;({data, status} = await createOrUpdateArticleApi(test,
article,
{ path: 'title-0', render: true }
))
assertStatus(status, data)
assertRows(data.articles, [{ render: /Body 0 hacked\./ }])
// And now not outdated after render.
;({data, status} = await test.webApi.articlesHash({ author: 'user0' }))
assertStatus(status, data)
assertRows(data.articles, [
{ path: '@user0/index.bigb', renderOutdated: false, },
{ path: '@user0/title-0.bigb', renderOutdated: false, },
])
// Undo it for test sanity.
article = createArticleArg({ i: 0 })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
assertRows(data.articles, [{ render: /Body 0\./ }])
// Edit index article.
;({data, status} = await createOrUpdateArticleApi(test, {
titleSource: '',
bodySource: `{id=}
Welcome to my home page hacked!
`
}))
assertStatus(status, data)
assertRows(data.articles, [{ render: /Welcome to my home page hacked!/ }])
;({data, status} = await test.webApi.article('user0'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, ourbigbook.HTML_HOME_MARKER)
assert.match(data.render, /Welcome to my home page hacked!/)
// View articles
// Test global feed paging.
;({data, status} = await test.webApi.articles({ limit: 2, page: 0 }))
assertStatus(status, data)
assertRows(data.articles, [
{ slug: 'user0/title-0' },
{ slug: 'user2' },
])
;({data, status} = await test.webApi.articles({ limit: 2, page: 1 }))
assertStatus(status, data)
assertRows(data.articles, [
{ slug: 'user1' },
{ slug: 'user0' },
])
// Invalid limit or page.
;({data, status} = await test.webApi.articles({ limit: 'dontexist', page: 1 }))
assert.strictEqual(status, 422)
;({data, status} = await test.webApi.articles({ limit: 2, page: 'dontexist' }))
assert.strictEqual(status, 422)
// Limit too large
;({data, status} = await test.webApi.articles({ limit: config.articleLimitMax + 1, page: 1 }))
assert.strictEqual(status, 422)
// Update articles
// Create article with PUT.
article = createArticleArg({ i: 1 })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
articles = data.articles
assert.strictEqual(articles[0].titleRender, 'Title 1')
assert.strictEqual(articles.length, 1)
// Access the article directly
;({data, status} = await test.webApi.article('user0/title-1'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Title 1')
assert.match(data.render, /Body 1/)
// Update article with PUT.
article = createArticleArg({ i: 1, bodySource: 'Body 2' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
// Access the article directly
;({data, status} = await test.webApi.article('user0/title-1'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Title 1')
assert.match(data.render, /Body 2/)
// User following.
// user2 follows user0
test.loginUser(user2)
;({data, status} = await test.webApi.userFollow('user0'))
assertStatus(status, data)
// Follower count increases.
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.username, 'user0')
assert.strictEqual(data.followerCount, 1)
// user2 follows user2
;({data, status} = await test.webApi.userFollow('user2'))
assertStatus(status, data)
test.loginUser(user)
// user0 follows user1
;({data, status} = await test.webApi.userFollow('user1'))
// Users cannot follow another user twice.
;({data, status} = await test.webApi.userFollow('user1'))
assert.strictEqual(status, 403)
// Trying to follow an user that does not exist fails gracefully.
;({data, status} = await test.webApi.userFollow('dontexist'))
assert.strictEqual(status, 404)
// users followedBy
;({data, status} = await test.webApi.users({ followedBy: 'user0' }))
assertStatus(status, data)
assertRows(data.users, [
{ username: 'user1' },
])
;({data, status} = await test.webApi.users({ followedBy: 'user1' }))
assertStatus(status, data)
assertRows(data.users, [])
;({data, status} = await test.webApi.users({ followedBy: 'user2' }))
assertStatus(status, data)
assertRows(data.users, [
{ username: 'user2' },
{ username: 'user0' },
])
;({data, status} = await test.webApi.users({ followedBy: 'user2', limit: 1 }))
assertStatus(status, data)
assertRows(data.users, [
{ username: 'user2' },
])
// users following
;({data, status} = await test.webApi.users({ following: 'user0' }))
assertStatus(status, data)
assertRows(data.users, [
{ username: 'user2' },
])
;({data, status} = await test.webApi.users({ following: 'user1' }))
assertStatus(status, data)
assertRows(data.users, [
{ username: 'user0' },
])
;({data, status} = await test.webApi.users({ following: 'user2' }))
assertStatus(status, data)
assertRows(data.users, [
{ username: 'user2' },
])
// Both followedBy and following together also works.
;({data, status} = await test.webApi.users({
following: 'user2',
followedBy: 'user2',
}))
assertStatus(status, data)
assertRows(data.users, [
{ username: 'user2' },
])
// User unfollowing.
// user2 unfollows user0
test.loginUser(user2)
;({data, status} = await test.webApi.userUnfollow('user0'))
assertStatus(status, data)
// Follower count decreases.
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.username, 'user0')
assert.strictEqual(data.followerCount, 0)
// user0 unfollows user2
;({data, status} = await test.webApi.userUnfollow('user2'))
assertStatus(status, data)
test.loginUser(user)
// user0 unfollows user1
;({data, status} = await test.webApi.userUnfollow('user1'))
// Users cannot unfollow another user twice.
;({data, status} = await test.webApi.userUnfollow('user1'))
assert.strictEqual(status, 403)
// Trying to follow an user that does not exist fails gracefully.
;({data, status} = await test.webApi.userUnfollow('dontexist'))
assert.strictEqual(status, 404)
// No more article index operations after this point, we are not going to create some more specialized articles.
// Link to another article.
article = createArticleArg({ titleSource: 'x', bodySource: '<Title 1>' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
// https://github.com/ourbigbook/ourbigbook/issues/283
assert_xpath("//x:a[@href='/user0/title-1' and text()='Title 1']", data.articles[0].render)
// Create issues
;({data, status} = await test.webApi.issueCreate('user0',
{
titleSource: 'The \\i[title] 0 index 0.',
bodySource: 'The \\i[body] 0 index 0.',
}
))
assertStatus(status, data)
assert.match(data.issue.titleRender, /The <i>title<\/i> 0 index 0\./)
assert.match(data.issue.render, /The <i>body<\/i> 0 index 0\./)
assert.strictEqual(data.issue.number, 1)
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 0)))
assertStatus(status, data)
assert.match(data.issue.titleRender, /The <i>title<\/i> 0 0 0\./)
assert.match(data.issue.render, /The <i>body<\/i> 0 0 0\./)
assert.strictEqual(data.issue.number, 1)
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 1)))
assertStatus(status, data)
assert.match(data.issue.titleRender, /The <i>title<\/i> 0 0 1\./)
assert.match(data.issue.render, /The <i>body<\/i> 0 0 1\./)
assert.strictEqual(data.issue.number, 2)
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 2)))
assertStatus(status, data)
assert.match(data.issue.titleRender, /The <i>title<\/i> 0 0 2\./)
assert.match(data.issue.render, /The <i>body<\/i> 0 0 2\./)
assert.strictEqual(data.issue.number, 3)
// Users can create issues on other users articles.
test.loginUser(user1)
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 3)))
assertStatus(status, data)
assert.match(data.issue.titleRender, /The <i>title<\/i> 0 0 3\./)
assert.match(data.issue.render, /The <i>body<\/i> 0 0 3\./)
assert.strictEqual(data.issue.number, 4)
test.loginUser(user)
// Issue with empty ID is fine unlike article.
;({data, status} = await test.webApi.issueCreate('user0/title-0', { titleSource: '.' }))
assertStatus(status, data)
;({data, status} = await test.webApi.issueCreate('user0/title-1', createIssueArg(0, 1, 0)))
assertStatus(status, data)
assert.match(data.issue.titleRender, /The <i>title<\/i> 0 1 0\./)
assert.match(data.issue.render, /The <i>body<\/i> 0 1 0\./)
assert.strictEqual(data.issue.number, 1)
;({data, status} = await test.webApi.issueCreate('user0/title-1', createIssueArg(0, 1, 1)))
assertStatus(status, data)
assert.match(data.issue.titleRender, /The <i>title<\/i> 0 1 1\./)
assert.match(data.issue.render, /The <i>body<\/i> 0 1 1\./)
assert.strictEqual(data.issue.number, 2)
;({data, status} = await test.webApi.issueCreate('user1',
{ titleSource: 'The \\i[title] 1 index 0.', bodySource: 'The \\i[body] 1 index 0.' }))
assertStatus(status, data)
assert.match(data.issue.titleRender, /The <i>title<\/i> 1 index 0\./)
assert.match(data.issue.render, /The <i>body<\/i> 1 index 0\./)
assert.strictEqual(data.issue.number, 1)
// Create issue errors
// Article does not exist
;({data, status} = await test.webApi.issueCreate('user0/dontexist', createIssueArg(0, 2, 0)))
assert.strictEqual(status, 404)
// Markup error on title
;({data, status} = await test.webApi.issueCreate('user0/title-0', {
titleSource: 'The \\notdefined 0 2.', bodySource: 'The \\i[body] 0 2.' }))
assert.strictEqual(status, 422)
// Markup error on body
;({data, status} = await test.webApi.issueCreate('user0/title-0',
{ titleSource: 'The \\i[title] 0 2.', bodySource: 'The \\notdefined 0 2.' }))
assert.strictEqual(status, 422)
// Get issues
// Get one issue.
;({data, status} = await test.webApi.issues({ id: 'user0/title-0' }))
assertStatus(status, data)
assertRows(data.issues, [
{ number: 5, titleRender: /\./ },
{ number: 4, titleRender: /The <i>title<\/i> 0 0 3\./ },
{ number: 3, titleRender: /The <i>title<\/i> 0 0 2\./ },
{ number: 2, titleRender: /The <i>title<\/i> 0 0 1\./ },
{ number: 1, titleRender: /The <i>title<\/i> 0 0 0\./ },
])
// Article issue count increments when new issues are created.
;({data, status} = await test.webApi.article('user0/title-0'))
assertStatus(status, data)
assert.strictEqual(data.issueCount, 5)
// Get another issue.
;({data, status} = await test.webApi.issues({ id: 'user0/title-1' }))
assertStatus(status, data)
assertRows(data.issues, [
{ number: 2, titleRender: /The <i>title<\/i> 0 1 1\./ },
{ number: 1, titleRender: /The <i>title<\/i> 0 1 0\./ },
])
// Article issue count increments when new issues are created.
;({data, status} = await test.webApi.article('user0/title-1'))
assertStatus(status, data)
assert.strictEqual(data.issueCount, 2)
// Get an index page issue.
;({data, status} = await test.webApi.issues({ id: 'user0' }))
assertStatus(status, data)
assertRows(data.issues, [
{ number: 1, titleRender: /The <i>title<\/i> 0 index 0\./ },
])
// Article issue count increments when new issues are created.
;({data, status} = await test.webApi.article('user0'))
assertStatus(status, data)
assert.strictEqual(data.issueCount, 1)
// Get another index page issue.
;({data, status} = await test.webApi.issues({ id: 'user1' }))
assertStatus(status, data)
assertRows(data.issues, [
{ number: 1, titleRender: /The <i>title<\/i> 1 index 0\./ },
])
// Article issue count increments when new issues are created.
;({data, status} = await test.webApi.article('user1'))
assertStatus(status, data)
assert.strictEqual(data.issueCount, 1)
// Getting issues from an article that doesn't exist fails gracefully.
;({data, status} = await test.webApi.issues({ id: 'user0/dontexist' }))
assert.strictEqual(status, 404)
// Edit issue.
;({data, status} = await test.webApi.issueEdit('user1', 1,
{ bodySource: 'The \\i[body] 1 index 0 hacked.' }))
assertStatus(status, data)
assert.match(data.issue.titleRender, /The <i>title<\/i> 1 index 0\./)
assert.match(data.issue.render, /The <i>body<\/i> 1 index 0 hacked\./)
assert.strictEqual(data.issue.number, 1)
;({data, status} = await test.webApi.issues({ id: 'user1' }))
assertRows(data.issues, [
{
number: 1,
titleRender: /The <i>title<\/i> 1 index 0\./,
render: /The <i>body<\/i> 1 index 0 hacked\./,
},
])
;({data, status} = await test.webApi.issueEdit('user1', 1,
{ titleSource: 'The \\i[title] 1 index 0 hacked.' }))
assertStatus(status, data)
assert.match(data.issue.titleRender, /The <i>title<\/i> 1 index 0 hacked\./)
assert.match(data.issue.render, /The <i>body<\/i> 1 index 0 hacked\./)
assert.strictEqual(data.issue.number, 1)
;({data, status} = await test.webApi.issues({ id: 'user1' }))
assertRows(data.issues, [
{
number: 1,
titleRender: /The <i>title<\/i> 1 index 0 hacked\./,
render: /The <i>body<\/i> 1 index 0 hacked\./,
},
])
// Edit issue errors
// Article does not exist
;({data, status} = await test.webApi.issueEdit('user0/dontexist', 1, { titleSource: 'asdf' }))
assert.strictEqual(status, 404)
// Markup error on title
;({data, status} = await test.webApi.issueEdit('user0/title-0', 1, { titleSource: '\\notdefined' }))
assert.strictEqual(status, 422)
// Markup error on body
;({data, status} = await test.webApi.issueEdit('user0/title-0', 1, { bodySource: '\\notdefined' }))
assert.strictEqual(status, 422)
// Trying to edit someone else's issue fails.
test.loginUser(user1)
;({data, status} = await test.webApi.issueEdit('user1', 1,
{ bodySource: 'The \\i[body] 1 index 0 hacked by user1.' }))
assert.strictEqual(status, 403)
test.loginUser(user)
// The issue didn't change.
;({data, status} = await test.webApi.issues({ id: 'user1' }))
assertRows(data.issues, [
{
number: 1,
titleRender: /The <i>title<\/i> 1 index 0 hacked\./,
render: /The <i>body<\/i> 1 index 0 hacked\./,
},
])
// Issue likes.
// Make user1 like one of the issues.
test.loginUser(user1)
;({data, status} = await test.webApi.issueLike('user0/title-1', 1))
assertStatus(status, data)
test.loginUser(user)
// Score goes up.
;({data, status} = await test.webApi.issues({ id: 'user0/title-1', sort: 'score' } ))
assertStatus(status, data)
assertRows(data.issues, [
{ number: 1, titleRender: /The <i>title<\/i> 0 1 0\./, score: 1 },
{ number: 2, titleRender: /The <i>title<\/i> 0 1 1\./, score: 0 },
])
// Users cannot like issue twice.
test.loginUser(user1)
;({data, status} = await test.webApi.issueLike('user0/title-1', 1))
assert.strictEqual(status, 403)
test.loginUser(user)
// Users cannot like their own issue.
test.loginUser(user1)
;({data, status} = await test.webApi.issueLike('user0/title-0', 4))
assert.strictEqual(status, 403)
test.loginUser(user1)
// Trying to like issue that does not exist fails gracefully.
test.loginUser(user1)
;({data, status} = await test.webApi.issueLike('user0/dontexist', 1))
assert.strictEqual(status, 404)
;({data, status} = await test.webApi.issueLike('user0/title-1', 999))
assert.strictEqual(status, 404)
test.loginUser(user)
// Make user1 unlike one of an issues.
test.loginUser(user1)
;({data, status} = await test.webApi.issueUnlike('user0/title-1', 1))
assertStatus(status, data)
test.loginUser(user)
// Score goes up.
;({data, status} = await test.webApi.issues({ id: 'user0/title-1' }))
assertStatus(status, data)
assertRows(data.issues, [
{ number: 2, titleRender: /The <i>title<\/i> 0 1 1\./, score: 0 },
{ number: 1, titleRender: /The <i>title<\/i> 0 1 0\./, score: 0 },
])
// Cannot unlike issue twice.
test.loginUser(user1)
;({data, status} = await test.webApi.issueUnlike('user0/title-1', 1))
assert.strictEqual(status, 403)
test.loginUser(user)
// Trying to like article that does not exist fails gracefully.
test.loginUser(user1)
;({data, status} = await test.webApi.issueUnlike('user0/dontexist', 1))
assert.strictEqual(status, 404)
;({data, status} = await test.webApi.issueUnlike('user0/title-1', 999))
assert.strictEqual(status, 404)
test.loginUser(user)
// No more issue indexes after this point.
// Link to article by issue author.
;({data, status} = await test.webApi.issueCreate('user1',
{
titleSource: 'x',
bodySource: '<Title 1>',
}
))
assertStatus(status, data)
// Has to account for go/issues/<number>/<username>, so four levels.
assert_xpath("//x:a[@href='/user0/title-1' and text()='Title 1']", data.issue.render)
// Create comments
;({data, status} = await test.webApi.commentCreate('user0', 1, 'The \\i[body] 0 index 0.'))
assertStatus(status, data)
assert.match(data.comment.render, /The <i>body<\/i> 0 index 0\./)
assert.strictEqual(data.comment.number, 1)
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, 'The \\i[body] 0 0 0.'))
assertStatus(status, data)
assert.match(data.comment.render, /The <i>body<\/i> 0 0 0\./)
assert.strictEqual(data.comment.number, 1)
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, 'The \\i[body] 0 0 1.'))
assertStatus(status, data)
assert.match(data.comment.render, /The <i>body<\/i> 0 0 1\./)
assert.strictEqual(data.comment.number, 2)
;({data, status} = await test.webApi.commentCreate('user0/title-0', 2, 'The \\i[body] 0 1 0.'))
assertStatus(status, data)
assert.match(data.comment.render, /The <i>body<\/i> 0 1 0\./)
assert.strictEqual(data.comment.number, 1)
;({data, status} = await test.webApi.commentCreate('user0/title-1', 1, 'The \\i[body] 1 0 0.'))
assertStatus(status, data)
assert.match(data.comment.render, /The <i>body<\/i> 1 0 0\./)
assert.strictEqual(data.comment.number, 1)
// Create comment errors
// Article does not exist
;({data, status} = await test.webApi.commentCreate('user0/title-1', 999, 'The \\i[body] 1 0 0.'))
assert.strictEqual(status, 404)
// Issues does not exist
;({data, status} = await test.webApi.commentCreate('user0/dontexist', 1, 'The \\i[body] 1 0 0.'))
assert.strictEqual(status, 404)
// Markup error
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, 'The \\notdefined 0 0 0.'))
assert.strictEqual(status, 422)
// Get some comments.
// Get all comments in article.
;({data, status} = await test.webApi.comments('user0/title-0', 1))
assertStatus(status, data)
assertRows(data.comments, [
{ number: 1, render: /The <i>body<\/i> 0 0 0\./ },
{ number: 2, render: /The <i>body<\/i> 0 0 1\./ },
])
// Get one comment in article
;({data, status} = await test.webApi.comment('user0/title-0', 1, 1))
assertStatus(status, data)
assertRows([data], [
{ number: 1, render: /The <i>body<\/i> 0 0 0\./ },
])
// Get another comment in article
;({data, status} = await test.webApi.comment('user0/title-0', 1, 2))
assertStatus(status, data)
assertRows([data], [
{ number: 2, render: /The <i>body<\/i> 0 0 1\./ },
])
// Getting a comment that doesn't exist fails gracefully.
;({data, status} = await test.webApi.comment('user0/title-0', 1, 3))
assert.strictEqual(status, 404)
// The issue comment count goes up with comment creation.
;({data, status} = await test.webApi.issue('user0/title-0', 1))
assertStatus(status, data)
assert.strictEqual(data.commentCount, 2)
// Get all comments in another article
;({data, status} = await test.webApi.comments('user0/title-0', 2))
assertRows(data.comments, [
{ number: 1, render: /The <i>body<\/i> 0 1 0\./ },
])
// The issue comment count goes up with comment creation.
;({data, status} = await test.webApi.issue('user0/title-0', 2))
assertStatus(status, data)
assert.strictEqual(data.commentCount, 1)
// Getting comments from articles or issues that don't exist fails gracefully.
;({data, status} = await test.webApi.comments('user0/title-1', 999))
assert.strictEqual(status, 404)
;({data, status} = await test.webApi.comments('user0/dontexist', 1))
assert.strictEqual(status, 404)
// Delete comment
// Non-admin users cannot delete comments.
;({data, status} = await test.webApi.commentDelete('user0/title-0', 1, 1))
assert.strictEqual(status, 403)
// Trying to delete a comment that doesn't exist fails gracefully.
test.loginUser(user2)
;({data, status} = await test.webApi.commentDelete('user0/title-0', 1, 3))
assert.strictEqual(status, 404)
test.loginUser(user)
// Admin can delete comments.
test.loginUser(user2)
;({data, status} = await test.webApi.commentDelete('user0/title-0', 1, 1))
assert.strictEqual(status, 204)
test.loginUser(user)
// The issue comment count goes down with comment deletion.
;({data, status} = await test.webApi.issue('user0/title-0', 1))
assertStatus(status, data)
assert.strictEqual(data.commentCount, 1)
// The deleted comment is no longer visible
;({data, status} = await test.webApi.comment('user0/title-0', 1, 1))
assert.strictEqual(status, 404)
// No more comment index gets from now on.
// Link to article by comment author.
// https://github.com/ourbigbook/ourbigbook/issues/277
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, '<Title 1>'))
assertStatus(status, data)
assert_xpath("//x:a[@href='/user0/title-1' and text()='Title 1']", data.comment.render)
if (testNext) {
// Tests with the same result for logged in or off.
async function testNextLoggedInOrOff(loggedInUser) {
// Index.
;({data, status} = await test.sendJsonHttp('GET', routes.home(), ))
assertStatus(status, data)
// Articles
;({data, status} = await test.sendJsonHttp('GET', routes.articles(), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.articles({ sort: 'created' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.articles({ sort: 'updated' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.articles({ sort: 'score' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.articles({ sort: 'follower-count' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.articles({ sort: 'issues' }), ))
assertStatus(status, data)
// Article
;({data, status} = await test.sendJsonHttp('GET', routes.article('user0/title-0'), ))
assertStatus(status, data)
// Article source
;({data, status} = await test.sendJsonHttp('GET', routes.articleSource('user0/title-0'), ))
assertStatus(status, data)
// Article that doesn't exist.
;({data, status} = await test.sendJsonHttp('GET', routes.article('user0/dontexist'), ))
assert.strictEqual(status, 404)
// Article source that doesn't exist.
;({data, status} = await test.sendJsonHttp('GET', routes.articleSource('user0/dontexist'), ))
assert.strictEqual(status, 404)
// Article issues
;({data, status} = await test.sendJsonHttp('GET', routes.articleIssues('user0/title-0'), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.articleIssues('user0/title-0', { sort: 'created' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.articleIssues('user0/title-0', { sort: 'updated' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.articleIssues('user0/title-0', { sort: 'score' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.articleIssues('user0/title-0', { sort: 'follower-count' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.articleIssues('user0/title-0', { sort: 'comments' }), ))
assertStatus(status, data)
// Article links
;({data, status} = await test.sendJsonHttp('GET', routes.userArticlesChildren('user0', ''), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userArticlesChildren('user0', 'title-0'), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userArticlesIncoming('user0', ''), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userArticlesIncoming('user0', 'title-0'), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userArticlesTagged('user0', ''), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userArticlesTagged('user0', 'title-0'), ))
assertStatus(status, data)
// Issues
;({data, status} = await test.sendJsonHttp('GET', routes.issues(), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.issues({ sort: 'created' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.issues({ sort: 'updated' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.issues({ sort: 'score' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.issues({ sort: 'follower-count' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.issues({ sort: 'comments' }), ))
assertStatus(status, data)
// Issue
;({data, status} = await test.sendJsonHttp('GET', routes.issue('user0/title-0', 1), ))
assertStatus(status, data)
// An issue that doesn't exist.
;({data, status} = await test.sendJsonHttp('GET', routes.issue('user0/title-0', 999), ))
assert.strictEqual(status, 404)
// Topics
;({data, status} = await test.sendJsonHttp('GET', routes.topics({ loggedInUser }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.topics({ loggedInUser, sort: 'article-count' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.topics({ loggedInUser, sort: 'updated' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.topics({ loggedInUser, sort: 'created' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.topics({ loggedInUser, sort: 'score' }), ))
assert.strictEqual(status, 422)
// Topic
;({data, status} = await test.sendJsonHttp('GET', routes.topic('title-0'), ))
assertStatus(status, data)
// Empty topic.
;({data, status} = await test.sendJsonHttp('GET', routes.topic('dontexist'), ))
assertStatus(status, data)
// Comments
;({data, status} = await test.sendJsonHttp('GET', routes.comments(), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.comments({ sort: 'created' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.comments({ sort: 'updated' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.comments({ sort: 'score' }), ))
assert.strictEqual(status, 422)
// Users
;({data, status} = await test.sendJsonHttp('GET', routes.users(), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.users({ sort: 'created' }), ))
assertStatus(status, data)
// Users sort by updated not allowed. Feels weird given all private data?
;({data, status} = await test.sendJsonHttp('GET', routes.users({ sort: 'updated' }), ))
assert.strictEqual(status, 422)
;({data, status} = await test.sendJsonHttp('GET', routes.users({ sort: 'score' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.users({ sort: 'username' }), ))
assertStatus(status, data)
// User
;({data, status} = await test.sendJsonHttp('GET', routes.user('user0'), ))
assertStatus(status, data)
// User that doesn't exist.
;({data, status} = await test.sendJsonHttp('GET', routes.user('dontexist'), ))
assert.strictEqual(status, 404)
// User articles
;({data, status} = await test.sendJsonHttp('GET', routes.userArticles('user0'), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userArticles('user0', { sort: 'created' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userArticles('user0', { sort: 'updated' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userArticles('user0', { sort: 'score' }), ))
assertStatus(status, data)
// User liked
;({data, status} = await test.sendJsonHttp('GET', routes.userLiked('user0'), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userLiked('user0', { sort: 'created' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userLiked('user0', { sort: 'updated' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userLiked('user0', { sort: 'score' }), ))
assert.strictEqual(status, 422)
// User likes
;({data, status} = await test.sendJsonHttp('GET', routes.userLikes('user0'), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userLikes('user0', { sort: 'created' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userLikes('user0', { sort: 'updated' }), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userLikes('user0', { sort: 'score' }), ))
assert.strictEqual(status, 422)
// User follows
;({data, status} = await test.sendJsonHttp('GET', routes.userFollows('user0'), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userFollowed('user0'), ))
assertStatus(status, data)
}
// Logged in.
await testNextLoggedInOrOff(true)
// Logged out.
test.disableToken()
await testNextLoggedInOrOff(false)
test.loginUser(user)
// Cases where logged out access leads to redirect to signup page.
async function testRedirIfLoggedOff(cb) {
test.disableToken()
let {data, status} = await cb()
assert.strictEqual(status, 307)
test.loginUser(user)
;({data, status} = await cb())
assertStatus(status, data)
}
await testRedirIfLoggedOff(async () => test.sendJsonHttp( 'GET', routes.articleNew(), ))
await testRedirIfLoggedOff(async () => test.sendJsonHttp( 'GET', routes.articleEdit('user0/title-0'), ))
await testRedirIfLoggedOff(async () => test.sendJsonHttp( 'GET', routes.issueNew('user0/title-0'), ))
await testRedirIfLoggedOff(async () => test.sendJsonHttp( 'GET', routes.issueEdit('user0/title-0', 1), ))
await testRedirIfLoggedOff(async () => test.sendJsonHttp( 'GET', routes.userEdit('user0'), ))
// Non admins cannot see the settings page of other users.
test.loginUser(user)
;({data, status} = await test.sendJsonHttp( 'GET', routes.userEdit('user1'), ))
assert.strictEqual(status, 404)
// Admins can see the settings page of other users.
test.loginUser(user2)
;({data, status} = await test.sendJsonHttp( 'GET', routes.userEdit('user1'), ))
assertStatus(status, data)
test.loginUser(user)
}
}, { canTestNext: true })
})
it('api: article like', async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
const user2 = await test.createUserApi(2)
test.loginUser(user0)
// user0 creates another article
article = createArticleArg({ i: 0 })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
assertRows(data.articles, [{ titleRender: 'Title 0' }])
// Make user1 like article user0
test.loginUser(user1)
;({data, status} = await test.webApi.articleLike('user0'))
assertStatus(status, data)
test.loginUser(user0)
// Article score goes up.
;({data, status} = await test.webApi.article('user0'))
assertStatus(status, data)
assert.strictEqual(data.score, 1)
// Make user2 like article user0
test.loginUser(user2)
;({data, status} = await test.webApi.articleLike('user0'))
assertStatus(status, data)
test.loginUser(user0)
// Like effects.
// Article score goes up.
;({data, status} = await test.webApi.article('user0'))
assertStatus(status, data)
assert.strictEqual(data.score, 2)
// Shows on likedBy list of user1.
;({data, status} = await test.webApi.articles({ likedBy: 'user1' }))
assertStatus(status, data)
assertRows(data.articles, [
{ titleRender: ourbigbook.HTML_HOME_MARKER, slug: 'user0' },
])
// Does not show up on likedBy list of user0.
;({data, status} = await test.webApi.articles({ likedBy: 'user0' }))
assertStatus(status, data)
assertRows(data.articles, [])
// Top articles by a user.
;({data, status} = await test.webApi.articles({ author: 'user0', sort: 'score' }))
assertStatus(status, data)
assertRows(data.articles, [
{ titleRender: ourbigbook.HTML_HOME_MARKER, slug: 'user0', score: 2 },
{ titleRender: 'Title 0', slug: 'user0/title-0', render: /Body 0/, score: 0 },
])
// Invalid sort.
;({data, status} = await test.webApi.articles({ author: 'user0', sort: 'dontexist' }))
assert.strictEqual(status, 422)
// User score.
;({data, status} = await test.webApi.users({ sort: 'score' }))
assertStatus(status, data)
assertRows(data.users, [
{ username: 'user0', score: 2 },
{ username: 'user2', score: 0 },
{ username: 'user1', score: 0 },
])
// Article like errors.
// Users cannot like articles twice.
test.loginUser(user1)
;({data, status} = await test.webApi.articleLike('user0'))
assert.strictEqual(status, 403)
test.loginUser(user0)
// Users cannot like their own article.
test.loginUser(user1)
;({data, status} = await test.webApi.articleLike('user1'))
assert.strictEqual(status, 403)
test.loginUser(user0)
// Trying to like article that does not exist fails gracefully.
test.loginUser(user1)
;({data, status} = await test.webApi.articleLike('user0/dontexist'))
assert.strictEqual(status, 404)
test.loginUser(user0)
// Make user1 unlike one of the articles.
test.loginUser(user1)
;({data, status} = await test.webApi.articleUnlike('user0'))
assertStatus(status, data)
test.loginUser(user0)
// Make user2 unlike one of the articles.
test.loginUser(user2)
;({data, status} = await test.webApi.articleUnlike('user0'))
assertStatus(status, data)
test.loginUser(user0)
// Unlike effects
// Score goes back down.
;({data, status} = await test.webApi.article('user0'))
assertStatus(status, data)
assert.strictEqual(data.score, 0)
// User score.
;({data, status} = await test.webApi.users())
assertStatus(status, data)
assertRows(data.users, [
{ username: 'user2', score: 0 },
{ username: 'user1', score: 0 },
{ username: 'user0', score: 0 },
])
// Unlike errors.
// Cannot unlike article twice.
test.loginUser(user1)
;({data, status} = await test.webApi.articleUnlike('user0'))
assert.strictEqual(status, 403)
test.loginUser(user0)
// Trying to like article that does not exist fails gracefully.
test.loginUser(user1)
;({data, status} = await test.webApi.articleUnlike('user0/dontexist'))
assert.strictEqual(status, 404)
test.loginUser(user0)
})
})
it('api: article follow', async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
// user1 creates another article
test.loginUser(user1)
article = createArticleArg({ i: 0 })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
assertRows(data.articles, [{ titleRender: 'Title 0' }])
test.loginUser(user0)
// Users follow their own articles by default.
;({data, status} = await test.webApi.article('user0'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
assert.strictEqual(data.followed, true)
;({data, status} = await test.webApi.article('user1'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
assert.strictEqual(data.followed, false)
test.loginUser(user1)
;({data, status} = await test.webApi.article('user0'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
assert.strictEqual(data.followed, false)
;({data, status} = await test.webApi.article('user1'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
assert.strictEqual(data.followed, true)
;({data, status} = await test.webApi.article('user1/title-0'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
assert.strictEqual(data.followed, true)
test.loginUser(user0)
// Make user0 follow article user1/title-0
;({data, status} = await test.webApi.articleFollow('user1/title-0'))
assertStatus(status, data)
// Follow effects.
// Article follower count goes up and shows on logged in user as followed.
;({data, status} = await test.webApi.article('user1/title-0'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 2)
assert.strictEqual(data.followed, true)
// Shows on user0's followedBy list.
;({data, status} = await test.webApi.articles({ followedBy: 'user0' }))
assertStatus(status, data)
assertRows(data.articles, [
{ slug: 'user1/title-0' },
{ slug: 'user0' },
])
// Most followed articles by user
;({data, status} = await test.webApi.articles({ author: 'user1', sort: 'follower-count' }))
assertStatus(status, data)
assertRows(data.articles, [
{ slug: 'user1/title-0', followerCount: 2 },
{ slug: 'user1', followerCount: 1 },
])
// Article follow errors.
// Users cannot follow articles twice.
;({data, status} = await test.webApi.articleFollow('user1/title-0'))
assert.strictEqual(status, 403)
// Trying to follow article that does not exist fails gracefully.
;({data, status} = await test.webApi.articleFollow('user1/dontexist'))
assert.strictEqual(status, 404)
// Make user0 unfollow article user1/title-0.
;({data, status} = await test.webApi.articleUnfollow('user1/title-0'))
assertStatus(status, data)
// Unfollow effects
// Follower count goes back down.
;({data, status} = await test.webApi.article('user1/title-0'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
assert.strictEqual(data.followed, false)
// Unfollow errors.
// Cannot unfollow article twice.
;({data, status} = await test.webApi.articleUnfollow('user1/title-0'))
assert.strictEqual(status, 403)
// Trying to follow article that does not exist fails gracefully.
;({data, status} = await test.webApi.articleUnfollow('user0/dontexist'))
assert.strictEqual(status, 404)
// Updating your own article does not make you follow it, only new article creation does.
// Make user1 unfollowe article user1/title-0.
test.loginUser(user1)
;({data, status} = await test.webApi.articleUnfollow('user1/title-0'))
assertStatus(status, data)
// Follower count goes back down.
;({data, status} = await test.webApi.article('user1/title-0'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 0)
assert.strictEqual(data.followed, false)
// Edit the article.
article = createArticleArg({ i: 0, bodySource: 'hacked' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
// This does not make user1 follow the article.
;({data, status} = await test.webApi.article('user1/title-0'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 0)
assert.strictEqual(data.followed, false)
test.loginUser(user0)
})
})
// TODO this is an initial sketch of https://docs.ourbigbook.com/todo/delete-articles
// Some steps have been skipped because we are initially only enabling this for article moves:
// and issues and children are moved out to the new merge target by the migration code prior
// to deletion. For this reason, it is also not yet exposed on the API, and is tested by calling
// via the sequelize model directly.
it('api: article delete', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
// Create users
const user = await test.createUserApi(0)
const admin = await test.createUserApi(1)
await test.sequelize.models.User.update({ admin: true }, { where: { username: 'user1' } })
test.loginUser(user)
// Create a basic hierarchy.
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Algebra' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Calculus', bodySource: '\\Image[http://jpg]{title=My calculus image}\n\n<Algebra>\n' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Geometry' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Derivative', bodySource: '<Calculus>\n' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/calculus' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Limit' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/calculus' }))
assertStatus(status, data)
// Create some data from another user
test.loginUser(admin)
// Add user1 metadata to user0/calculus
// Like.
;({data, status} = await test.webApi.articleLike('user0/calculus'))
assertStatus(status, data)
// Create issue.
;({data, status} = await test.webApi.issueCreate('user0/calculus',
{ titleSource: 'Calculus issue 1' }
))
assertStatus(status, data)
// Create another "calculus" article to see handling of topic count side effects.
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user1' }))
assertStatus(status, data)
test.loginUser(user)
// Create a comment
;({data, status} = await test.webApi.commentCreate('user0/calculus', 1, 'user0/title-1 1 1'))
assertStatus(status, data)
// Sanity checks before deletion.
// User score is now up to 1 after like
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.username, 'user0')
assert.strictEqual(data.score, 1)
// The topic has 2 entries
;({data, status} = await test.webApi.topics({ id: 'calculus' }))
assertStatus(status, data)
assert.strictEqual(data.topics[0].articleCount, 2)
// Issue is visible.
;({data, status} = await test.webApi.issue('user0/calculus', 1))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Calculus issue 1')
// Comment is visible
;({data, status} = await test.webApi.comment('user0/calculus', 1, 1))
assertStatus(status, data)
assert.strictEqual(data.source, 'user0/title-1 1 1')
// The in-article ID image-my-calculus-image is defined.
{
const id = await sequelize.models.Id.findOne({ where: { idid: '@user0/image-my-calculus-image' } })
assert.notStrictEqual(id, null)
}
// References defined in calculus are present
{
const count = await sequelize.models.Ref.count({
include: [
{
model: sequelize.models.File,
as: 'definedAt',
where: { path: '@user0/calculus.bigb' }
}
]
})
assert.notStrictEqual(count, 0)
}
// The File object exists.
{
const file = await sequelize.models.File.findOne({ where: { path: '@user0/calculus.bigb' } })
assert.notStrictEqual(file, null)
}
// Parent and previous siblings that involve calculus.
{
// Current tree state:
// * 0 user0/Index
// * 1 Mathematics
// * 2 Geometry
// * 3 Calculus
// * 4 Limit
// * 5 Derivative
// * 6 Algebra
// * 0 user1/Index
// * 1 Calculus
const algebra = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
sequelize,
slug: 'user0/algebra',
})
assert.strictEqual(algebra.parentId.idid, '@user0/mathematics')
assert.strictEqual(algebra.previousSiblingId.idid, '@user0/calculus')
const limit = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
slug: 'user0/limit',
sequelize
})
assert.strictEqual(limit.parentId.idid, '@user0/calculus')
assert.strictEqual(limit.previousSiblingId, undefined)
const derivative = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
slug: 'user0/derivative',
sequelize
})
assert.strictEqual(derivative.parentId.idid, '@user0/calculus')
assert.strictEqual(derivative.previousSiblingId.idid, '@user0/limit')
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 7, depth: 0, to_id_index: null, slug: 'user0', parentId: null },
{ nestedSetIndex: 1, nestedSetNextSibling: 7, depth: 1, to_id_index: 0, slug: 'user0/mathematics', parentId: '@user0' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, to_id_index: 0, slug: 'user0/geometry', parentId: '@user0/mathematics' },
{ nestedSetIndex: 3, nestedSetNextSibling: 6, depth: 2, to_id_index: 1, slug: 'user0/calculus', parentId: '@user0/mathematics' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 3, to_id_index: 0, slug: 'user0/limit', parentId: '@user0/calculus' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 3, to_id_index: 1, slug: 'user0/derivative', parentId: '@user0/calculus' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 2, to_id_index: 2, slug: 'user0/algebra', parentId: '@user0/mathematics' },
{ nestedSetIndex: 0, nestedSetNextSibling: 2, depth: 0, to_id_index: null, slug: 'user1', parentId: null },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user1/calculus', parentId: '@user1' },
])
}
// Delete user0/calculus and check the side effects!!!
{
const article = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
sequelize,
slug: 'user0/calculus',
})
await article.destroySideEffects()
}
// User score is is decremented when an article is deleted
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.username, 'user0')
assert.strictEqual(data.score, 0)
// Topic article count is decremented when the article the topic pointed to is deleted
// Here the topic pointed to user0/calculus because it was created before user1/calculus.
;({data, status} = await test.webApi.topics({ id: 'calculus' }))
assertStatus(status, data)
assert.strictEqual(data.topics[0].articleCount, 1)
//// Issues are deleted TODO
//;({data, status} = await test.webApi.issue('user0/calculus', 1))
//assert.strictEqual(status, 404)
//// Comments are deleted TODO
//;({data, status} = await test.webApi.comment('user0/calculus', 1, 1))
////assert.strictEqual(status, 404)
// The in-article ID image-my-calculus-image was deleted
{
const id = await sequelize.models.Id.findOne({ where: { idid: '@user0/image-my-calculus-image' } })
assert.strictEqual(id, null)
}
// References defined in calculus were deleted
{
const count = await sequelize.models.Ref.count({
include: [
{
model: sequelize.models.File,
as: 'definedAt',
where: { path: '@user0/calculus.bigb' }
}
]
})
assert.strictEqual(count, 0)
}
// The File object were deleted
{
const file = await sequelize.models.File.findOne({ where: { path: '@user0/calculus.bigb' } })
assert.strictEqual(file, null)
}
// Parent and previous siblings that involve deleted article are updated.
// All child pages are moved up the tree and placed where the parent was.
{
// Current tree state:
// * 0 user0/Index
// * 1 Mathematics
// * 2 Geometry
// * 3 Limit
// * 4 Derivative
// * 5 Algebra
// * 0 user1/Index
// * 1 Calculus
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 6, depth: 0, to_id_index: null, slug: 'user0', parentId: null },
{ nestedSetIndex: 1, nestedSetNextSibling: 6, depth: 1, to_id_index: 0, slug: 'user0/mathematics', parentId: '@user0' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, to_id_index: 0, slug: 'user0/geometry', parentId: '@user0/mathematics' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 2, to_id_index: 1, slug: 'user0/limit', parentId: '@user0/mathematics' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 2, to_id_index: 2, slug: 'user0/derivative', parentId: '@user0/mathematics' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 2, to_id_index: 3, slug: 'user0/algebra', parentId: '@user0/mathematics' },
{ nestedSetIndex: 0, nestedSetNextSibling: 2, depth: 0, to_id_index: null, slug: 'user1', parentId: null },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user1/calculus', parentId: '@user1' },
])
const limit = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
slug: 'user0/limit',
sequelize
})
assert.strictEqual(limit.parentId.idid, '@user0/mathematics')
assert.strictEqual(limit.previousSiblingId.idid, '@user0/geometry')
const derivative = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
slug: 'user0/derivative',
sequelize
})
assert.strictEqual(derivative.parentId.idid, '@user0/mathematics')
assert.strictEqual(derivative.previousSiblingId.idid, '@user0/limit')
const algebra = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
sequelize,
slug: 'user0/algebra',
})
assert.strictEqual(algebra.parentId.idid, '@user0/mathematics')
assert.strictEqual(algebra.previousSiblingId.idid, '@user0/derivative')
}
// Delete user1/calculus and check the topic deletion effects.
{
const article = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
sequelize,
slug: 'user1/calculus',
})
await article.destroySideEffects()
}
// Topics without articles are deleted
// This behavior might have to change later on if we implement features
// such as topic following and topic issues:
// https://docs.ourbigbook.com/todo/follow-topic
// https://github.com/ourbigbook/ourbigbook/issues/257
;({data, status} = await test.webApi.topics({ id: 'calculus' }))
assertStatus(status, data)
assertRows(data.topics, [])
})
})
it('api: issue follow', async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
// user1 creates issue user0#1
test.loginUser(user1)
;({data, status} = await test.webApi.issueCreate('user0', createIssueArg(0, 0, 0)))
assertStatus(status, data)
assert.strictEqual(data.issue.number, 1)
test.loginUser(user0)
// Users follow their own issues by default
;({data, status} = await test.webApi.issue('user0', 1))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
assert.strictEqual(data.followed, false)
test.loginUser(user1)
;({data, status} = await test.webApi.issue('user0', 1))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
assert.strictEqual(data.followed, true)
test.loginUser(user0)
// Make user0 follow issue user0#1
;({data, status} = await test.webApi.issueFollow('user0', 1))
assertStatus(status, data)
// Follow effects
// Issue follower count goes up and shows on logged in user as followed.
;({data, status} = await test.webApi.issue('user0', 1))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 2)
assert.strictEqual(data.followed, true)
//// TODO Shows on user0's followedBy list.
//;({data, status} = await test.webApi.issues({ followedBy: 'user0' }))
//assertStatus(status, data)
//assertRows(data.issues, [
// { slug: 'user1/title-0' },
// { slug: 'user0' },
//])
// issue follow errors.
// Users cannot follow issues twice.
;({data, status} = await test.webApi.issueFollow('user0', 1))
assert.strictEqual(status, 403)
// Trying to follow issue on article that does not exist fails gracefully.
;({data, status} = await test.webApi.issueFollow('user0/dontexist', 1))
assert.strictEqual(status, 404)
// Trying to follow issue that does not exist fails gracefully.
;({data, status} = await test.webApi.issueFollow('user0', 2))
assert.strictEqual(status, 404)
// Make user0 unfollow issue user1/title-0.
;({data, status} = await test.webApi.issueUnfollow('user0', 1))
assertStatus(status, data)
// Unfollow effects
// Follower count goes back down.
;({data, status} = await test.webApi.issue('user0', 1))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
assert.strictEqual(data.followed, false)
// Unfollow errors
// Cannot unfollow issue twice.
;({data, status} = await test.webApi.issueUnfollow('user0', 1))
assert.strictEqual(status, 403)
// Trying to follow issue on article that does not exist fails gracefully.
;({data, status} = await test.webApi.issueUnfollow('user0/dontexist', 1))
assert.strictEqual(status, 404)
// Trying to follow issue that does not exist fails gracefully.
;({data, status} = await test.webApi.issueUnfollow('user0', 2))
assert.strictEqual(status, 404)
// Commenting on an issue makes you follow it automatically
;({data, status} = await test.webApi.commentCreate('user0', 1, 'The \\i[body] 0 index 0.'))
assertStatus(status, data)
;({data, status} = await test.webApi.issue('user0', 1))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 2)
assert.strictEqual(data.followed, true)
// Commenting again does not keep increasing the followerCount.
;({data, status} = await test.webApi.commentCreate('user0', 1, 'The \\i[body] 0 index 0.'))
assertStatus(status, data)
;({data, status} = await test.webApi.issue('user0', 1))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 2)
assert.strictEqual(data.followed, true)
})
})
// This used to work at one point working. But then we
// when we started exposing the parentId via API, and decided it would be
// less confusing if we instead forbade multiheader articles to start with.
// Maybe one day we can bring them back, but e.g. forbidding removal after published.
//it('api: multiheader file creates multiple articles', async () => {
// await testApp(async (test) => {
// let res,
// data,
// article
//
// // Create user.
// const user = await test.createUserApi(0)
//
// // Create article.
// article = createArticleArg({ i: 0, bodySource: `Body 0.
//
//== Title 0 0
//
//Body 0 0.
//
//== Title 0 1
//
//Body 0 1.
//`})
// ;({data, status} = await createArticleApi(test, article))
// assertStatus(status, data)
// assertRows(data.articles, [
// { titleRender: 'Title 0', slug: 'user0/title-0' },
// { titleRender: 'Title 0 0', slug: 'user0/title-0-0' },
// { titleRender: 'Title 0 1', slug: 'user0/title-0-1' },
// ])
// assert.match(data.articles[0].render, /Body 0\./)
// assert.match(data.articles[0].render, /Body 0 0\./)
// assert.match(data.articles[0].render, /Body 0 1\./)
// assert.match(data.articles[1].render, /Body 0 0\./)
// assert.match(data.articles[2].render, /Body 0 1\./)
//
// // See them on global feed.
// ;({data, status} = await test.webApi.articles())
// assertStatus(status, data)
// sortByKey(data.articles, 'slug')
// assertRows(data.articles, [
// { titleRender: ourbigbook.HTML_HOME_MARKER, slug: 'user0' },
// { titleRender: 'Title 0', slug: 'user0/title-0' },
// { titleRender: 'Title 0 0', slug: 'user0/title-0-0' },
// { titleRender: 'Title 0 1', slug: 'user0/title-0-1' },
// ])
//
// // Access one of the articles directly.
// ;({data, status} = await test.webApi.article('user0/title-0-0'))
// assertStatus(status, data)
// assert.strictEqual(data.titleRender, 'Title 0 0')
// assert.match(data.render, /Body 0 0\./)
// assert.doesNotMatch(data.render, /Body 0 1\./)
//
// // Modify the file.
// article = createArticleArg({ i: 0, bodySource: `Body 0.
//
//== Title 0 0 hacked
//
//Body 0 0 hacked.
//
//== Title 0 1
//
//Body 0 1.
//`})
// ;({data, status} = await createOrUpdateArticleApi(test, article))
// assertStatus(status, data)
// assertRows(data.articles, [
// { titleRender: 'Title 0', slug: 'user0/title-0' },
// { titleRender: 'Title 0 0 hacked', slug: 'user0/title-0-0-hacked' },
// { titleRender: 'Title 0 1', slug: 'user0/title-0-1' },
// ])
// assert.match(data.articles[0].render, /Body 0\./)
// assert.match(data.articles[0].render, /Body 0 0 hacked\./)
// assert.match(data.articles[0].render, /Body 0 1\./)
// assert.match(data.articles[1].render, /Body 0 0 hacked\./)
// assert.match(data.articles[2].render, /Body 0 1\./)
//
// // See them on global feed.
// ;({data, status} = await test.webApi.articles())
// assertStatus(status, data)
// sortByKey(data.articles, 'slug')
// assertRows(data.articles, [
// { titleRender: ourbigbook.HTML_HOME_MARKER, slug: 'user0', },
// { titleRender: 'Title 0', slug: 'user0/title-0', render: /Body 0 0 hacked\./ },
// { titleRender: 'Title 0 0', slug: 'user0/title-0-0', render: /Body 0 0\./ },
// { titleRender: 'Title 0 0 hacked', slug: 'user0/title-0-0-hacked', render: /Body 0 0 hacked\./ },
// { titleRender: 'Title 0 1', slug: 'user0/title-0-1', render: /Body 0 1\./ },
// ])
//
// // Topic shows only one subarticle.
// ;({data, status} = await test.webApi.articles({ topicId: 'title-0-0' }))
// assertStatus(status, data)
// sortByKey(data.articles, 'slug')
// assertRows(data.articles, [
// { titleRender: 'Title 0 0', slug: 'user0/title-0-0', render: /Body 0 0\./ },
// ])
// })
//})
it('api: resource limits', async () => {
await testApp(async (test) => {
let data, status, article
const user = await test.createUserApi(0)
const admin = await test.createUserApi(1)
await test.sequelize.models.User.update({ admin: true }, { where: { username: 'user1' } })
test.loginUser(user)
// Non-admin users cannot edit their own resource limits.
;({data, status} = await test.webApi.userUpdate('user0', {
maxArticles: 1,
}))
assert.strictEqual(status, 403)
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.maxArticles, config.maxArticles)
// Admin users can edit other users' resource limits.
test.loginUser(admin)
;({data, status} = await test.webApi.userUpdate('user0', {
maxArticles: 3,
maxArticleSize: 3,
maxIssuesPerMinute: 3,
maxIssuesPerHour: 3,
locked: true,
}))
assertStatus(status, data)
test.loginUser(user)
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assertRows([data], [{
username: 'user0',
maxArticles: 3,
maxArticleSize: 3,
maxIssuesPerMinute: 3,
maxIssuesPerHour: 3,
locked: true,
}])
// Restore locked to false because this will cause problems with the following tests.
test.loginUser(admin)
;({data, status} = await test.webApi.userUpdate('user0', {
locked: false,
}))
assertStatus(status, data)
test.loginUser(user)
// Article.
// maxArticleSize resource limit is enforced for non-admins.
article = createArticleArg({ i: 0, bodySource: 'abcd' })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 403)
// maxArticleSize resource limit is not enforced for admins.
test.loginUser(admin)
article = createArticleArg({ i: 0, bodySource: 'abcd' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
test.loginUser(user)
// OK, second article including Index.
article = createArticleArg({ i: 0, bodySource: 'abc' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
// maxArticleSize resource limit is enforced for all users.
article = createArticleArg({ titleSource: '0'.repeat(config.maxArticleTitleSize + 1), bodySource: 'abc' })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 422)
// Even admin.
test.loginUser(admin)
article = createArticleArg({ titleSource: '0'.repeat(config.maxArticleTitleSize + 1), bodySource: 'abc' })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 422)
test.loginUser(user)
// OK 2, third article including Index.
article = createArticleArg({ titleSource: '0'.repeat(config.maxArticleTitleSize), bodySource: 'abc' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
// maxArticles resource limit is enforced for non-admins.
article = createArticleArg({ i: 2, bodySource: 'abc' })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 403)
// maxArticles resource limit is enforced for non-admins when creating article with PUT.
article = createArticleArg({ i: 2, bodySource: 'abc' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assert.strictEqual(status, 403)
// OK 2 for admin.
test.loginUser(admin)
article = createArticleArg({ i: 1, bodySource: 'abc' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
test.loginUser(user)
// maxArticles resource limit is not enforced for admins.
test.loginUser(admin)
article = createArticleArg({ i: 2, bodySource: 'abc' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
test.loginUser(user)
// Multiheader articles count as just one article.
// We forbade multiheader articles at one point. Might be reallowed later on.
// // Increment article limit by two from 3 to 5. User had 3, so now there are two left.
// // Also increment article size so we can fit the header in.
// test.loginUser(admin)
// ;({data, status} = await test.webApi.userUpdate('user0', {
// maxArticles: 5,
// maxArticleSize: 100,
// }))
// assertStatus(status, data)
// test.loginUser(user)
//
// // This should count as just one, totalling 4.
// article = createArticleArg({ i: 2, bodySource: `== Title 2 1
//` })
// ;({data, status} = await createArticleApi(test, article))
// assertStatus(status, data)
//
// // So now we can still do one more, totalling 5.
// article = createArticleArg({ i: 3, bodySource: `abc`})
// ;({data, status} = await createArticleApi(test, article))
// assertStatus(status, data)
// Issue.
// Change limit to 2 now that we don't have Index.
test.loginUser(admin)
;({data, status} = await test.webApi.userUpdate('user0', {
maxArticles: 2,
maxArticleSize: 3,
}))
assertStatus(status, data)
;({data, status} = await test.webApi.user('user0'))
test.loginUser(user)
// maxArticleSize resource limit is enforced for non-admins.
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 0, { bodySource: 'abcd' })))
assert.strictEqual(status, 403)
// maxArticleSize resource limit is not enforced for admins.
test.loginUser(admin)
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 0, { bodySource: 'abcd' })))
assertStatus(status, data)
test.loginUser(user)
// OK.
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 0, { bodySource: 'abc' })))
assertStatus(status, data)
// maxArticleTitleSize resource limit is enforced for all users.
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(
0, 0, 0, { titleSource: '0'.repeat(config.maxArticleTitleSize + 1), bodySource: 'abc' })))
assert.strictEqual(status, 422)
// Even admin.
test.loginUser(admin)
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(
0, 0, 0, { titleSource: '0'.repeat(config.maxArticleTitleSize + 1), bodySource: 'abc' })))
assert.strictEqual(status, 422)
test.loginUser(user)
// OK 2.
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(
0, 0, 0, { titleSource: '0'.repeat(config.maxArticleTitleSize), bodySource: 'abc' })))
assertStatus(status, data)
// maxArticles resource limit is enforced for non-admins.
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 0, { bodySource: 'abc' })))
assert.strictEqual(status, 403)
// OK 2 for admin.
test.loginUser(admin)
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 0, { bodySource: 'abc' })))
assertStatus(status, data)
test.loginUser(user)
// maxArticles resource limit is not enforced for admins.
test.loginUser(admin)
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 0, { bodySource: 'abc' })))
assertStatus(status, data)
test.loginUser(user)
// Comment.
// maxArticleSize resource limit is enforced for non-admins.
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, 'abcd'))
assert.strictEqual(status, 403)
// maxArticleSize resource limit is not enforced for admins.
test.loginUser(admin)
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, 'abcd'))
assertStatus(status, data)
test.loginUser(user)
// OK.
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, 'abc'))
assertStatus(status, data)
// OK 2.
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, 'abc'))
assertStatus(status, data)
// maxArticles resource limit is enforced for non-admins.
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, 'abc'))
assert.strictEqual(status, 403)
// OK 2 for admin.
test.loginUser(admin)
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, 'abc'))
assertStatus(status, data)
test.loginUser(user)
// maxArticles resource limit is not enforced for admins.
test.loginUser(admin)
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, 'abc'))
assertStatus(status, data)
test.loginUser(user)
})
})
it(`api: user: locked users can't do much`, async () => {
await testApp(async (test) => {
let data, status, article
const user0 = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
const admin = await test.createUserApi(2)
await test.sequelize.models.User.update({ admin: true }, { where: { username: 'user2' } })
// Create a test article and issue by user1.
test.loginUser(user1)
article = createArticleArg({ i: 0 })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 1 })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
;({data, status} = await test.webApi.issueCreate('user1/title-0', createIssueArg(1, 0, 0)))
assertStatus(status, data)
;({data, status} = await test.webApi.issueCreate('user1/title-0', createIssueArg(1, 0, 0)))
assertStatus(status, data)
;({data, status} = await test.webApi.commentCreate('user1/title-0', 1, '= The header\n\n==The body\n'))
assertStatus(status, data)
// Create some items by user0.
test.loginUser(user0)
article = createArticleArg({ i: 0 })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(1, 0, 0)))
assertStatus(status, data)
;({data, status} = await test.webApi.articleLike('user1/title-0'))
assertStatus(status, data)
;({data, status} = await test.webApi.articleFollow('user1/title-0'))
assertStatus(status, data)
;({data, status} = await test.webApi.issueFollow('user1/title-0', 1))
assertStatus(status, data)
// Lock user0
test.loginUser(admin)
;({data, status} = await test.webApi.userUpdate('user0', {
locked: true,
}))
assertStatus(status, data)
// Check that user0 cannot do stuff.
test.loginUser(user0)
// Locked users cannot edit their own profile
;({ data, status } = await test.webApi.userUpdate(
'user0', { displayName: 'hacked', },
))
assert.strictEqual(status, 403)
// Locked users cannot follow users.
;({data, status} = await test.webApi.userFollow('user0'))
assert.strictEqual(status, 403)
// Locked users cannot create articles.
article = createArticleArg({ i: 1 })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 403)
// Locked users cannot edit their own articles.
article = createArticleArg({ i: 0, body: 'hacked' })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 403)
// Locked users cannot announce their own articles.
;({data, status} = await test.webApi.articleAnnounce('user0/title-0', 'My message.'))
assert.strictEqual(status, 403)
// Locked users cannot unlike articles.
;({data, status} = await test.webApi.articleUnlike('user1/title-0'))
assert.strictEqual(status, 403)
// Locked users cannot like articles.
;({data, status} = await test.webApi.articleLike('user1/title-1'))
assert.strictEqual(status, 403)
// Locked users cannot unfollow articles.
;({data, status} = await test.webApi.articleUnfollow('user1/title-0'))
assert.strictEqual(status, 403)
// Locked users cannot follow articles.
;({data, status} = await test.webApi.articleFollow('user1/title-1'))
assert.strictEqual(status, 403)
// Locked users cannot create discussions.
article = createArticleArg({ i: 1 })
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(1, 0, 0)))
assert.strictEqual(status, 403)
// Locked users cannot edit their own discussions.
;({data, status} = await test.webApi.issueEdit('user0/title-0', 1, { bodySource: 'hacked' }))
assert.strictEqual(status, 403)
// Locked users cannot unfollow discussions
;({data, status} = await test.webApi.issueUnfollow('user1/title-0', 1))
assert.strictEqual(status, 403)
// Locked users cannot follow discussions
;({data, status} = await test.webApi.issueFollow('user1/title-0', 2))
assert.strictEqual(status, 403)
// Locked users cannot create comments.
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, '= The header\n\n==The body\n'))
assert.strictEqual(status, 403)
})
})
it('api: article tree: single user', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
// Create a second user and index to ensure that the nested set indexes are independent for each user.
// Because of course we didn't do this when originally implementing.
const user1 = await test.createUserApi(1)
test.loginUser(user)
// Article.
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 2, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 3, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// It is possible to change a parent ID.
// Create a new test ID.
article = createArticleArg({ i: 0, titleSource: 'Derivative' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
// Current tree state:
// * 0 Index
// * 1 Mathematics
// * 2 Derivative
// * 3 Calculus
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 4, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 4, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, to_id_index: 0, slug: 'user0/derivative' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 2, to_id_index: 1, slug: 'user0/calculus' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// Modify the parent of derivative from Mathematics to its next sibling Calculus.
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/calculus' }))
assertStatus(status, data)
// Current tree state:
// * 0 Index
// * 1 Mathematics
// * 2 Calculus
// * 3 Derivative
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 4, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 4, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 4, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 3, to_id_index: 0, slug: 'user0/derivative' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// parentId errors
// The convert function has some massive if(render) cases, so let's test all error cases for both
for (const render of [false, true]) {
// Parent ID that doesn't exist gives an error on new article.
article = createArticleArg({ i: 0, titleSource: 'Physics' })
;({data, status} = await createArticleApi(test, article, { parentId: '@user0/dontexist', render }))
assert.strictEqual(status, 422)
// Parent ID that doesn't exist gives an error on existing article.
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/dontexist', render }))
assert.strictEqual(status, 422)
// It is not possible to change the index parentId.
article = createArticleArg({ i: 0, titleSource: '' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics', render }))
assert.strictEqual(status, 422)
// It it not possible to set the parentId to an article of another user.
article = createArticleArg({ i: 0, titleSource: 'Physics' })
;({data, status} = await createArticleApi(test, article, { parentId: '@user1', render }))
assert.strictEqual(status, 422)
// Circular parent loops fail gracefully.
// Related:
// * https://github.com/ourbigbook/ourbigbook/issues/204
// * https://github.com/ourbigbook/ourbigbook/issues/319#issuecomment-2662912799
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/calculus', render }))
assert.strictEqual(status, 422)
{
const article = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
sequelize,
slug: 'user0/mathematics',
})
assert.strictEqual(article.parentId.idid, '@user0')
}
// This is where it might go infinite if it hadn't been prevented above.
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics', render }))
assertStatus(status, data)
// Circular parent loops to self fail gracefully.
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics', render }))
assert.strictEqual(status, 422)
}
// previousSiblingId
// Also add \\Image here, as we once had a bug where non header children were messing up the header tree
article = createArticleArg({ i: 0, titleSource: 'Integral', bodySource: '\\Image[http://example.com]{title=My image}\n' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/calculus', previousSiblingId: '@user0/derivative' }))
assertStatus(status, data)
// Current tree state:
// * Index
// * 0 Mathematics
// * 1 Calculus
// * 2 Derivative
// * 3 Integral
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 5, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 5, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 5, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 3, to_id_index: 0, slug: 'user0/derivative' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 3, to_id_index: 1, slug: 'user0/integral' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// Refresh the parent index to show this new child.
// TODO restore toc asserts. Requires next, not currently exposed on the API.
//assert_xpath("//*[@id='toc']//x:a[@href='/user0/mathematics' and @data-test='0' and text()='Mathematics']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/calculus' and @data-test='1' and text()='Calculus']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/derivative' and @data-test='2' and text()='Derivative']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/integral' and @data-test='3' and text()='Integral']", data.articles[0].render)
// Add another one, and update it a few times.
// Empty goes first.
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit' }),
{ parentId: '@user0/calculus' }
))
assertStatus(status, data)
// Current tree state:
// * Index
// * 0 Mathematics
// * 1 Calculus
// * 2 Limit
// * 3 Derivative
// * 4 Integral
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 6, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 6, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 6, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// TODO restore toc asserts.
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit' and @data-test='2' and text()='Limit']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/derivative' and @data-test='3' and text()='Derivative']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/integral' and @data-test='4' and text()='Integral']", data.articles[0].render)
// Add some children to limit as we will be moving it around a bit,
// and want to ensure that the children move with it.
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit of a function' }),
{ parentId: '@user0/limit' }
))
assertStatus(status, data)
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit of a sequence' }),
{ parentId: '@user0/limit' }
))
assertStatus(status, data)
// Current tree state:
// * Index
// * 0 Mathematics
// * 1 Calculus
// * 2 Limit
// * 3 Limit of a sequence
// * 4 Limit of a function
// * 5 Derivative
// * 6 Integral
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 8, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 8, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 8, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 6, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-sequence' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 4, to_id_index: 1, slug: 'user0/limit-of-a-function' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 7, nestedSetNextSibling: 8, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// TODO restore toc asserts.
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit' and @data-test='2' and text()='Limit']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit-of-a-sequence' and @data-test='3' and text()='Limit of a sequence']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit-of-a-function' and @data-test='4' and text()='Limit of a function']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/derivative' and @data-test='5' and text()='Derivative']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/integral' and @data-test='6' and text()='Integral']", data.articles[0].render)
// Move Limit to after a later sibling. Give a parentId as well as sibling. parentId is not necessary
// in this case as it is implied by previousSibling, but it is allowed.
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit' }),
{ parentId: '@user0/calculus', previousSiblingId: '@user0/integral' }
))
assertStatus(status, data)
// Current tree state:
// * 0 Index
// * 1 Mathematics
// * 2 Calculus
// * 3 Derivative
// * 4 Integral
// * 5 Limit
// * 6 Limit of a sequence
// * 7 Limit of a function
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 8, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 8, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 8, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 3, to_id_index: 0, slug: 'user0/derivative' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 3, to_id_index: 1, slug: 'user0/integral' },
{ nestedSetIndex: 5, nestedSetNextSibling: 8, depth: 3, to_id_index: 2, slug: 'user0/limit' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-sequence' },
{ nestedSetIndex: 7, nestedSetNextSibling: 8, depth: 4, to_id_index: 1, slug: 'user0/limit-of-a-function' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// TODO restore toc asserts.
//assert_xpath("//*[@id='toc']//x:a[@href='user0/derivative' and @data-test='2' and text()='Derivative']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/integral' and @data-test='3' and text()='Integral']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit' and @data-test='4' and text()='Limit']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit-of-a-sequence' and @data-test='5' and text()='Limit of a sequence']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit-of-a-function' and @data-test='6' and text()='Limit of a function']", data.articles[0].render)
// Move to previous sibling. Don't give parentId on update. Parent will be derived from sibling.
// First create a link to derivative from here. This is to stress the case where there is a non-parent
// link to the previousSiblingId, to ensure that the Ref type is chosen on the query. This failed to exercise
// the bug concretely, as it is an undocumented behavior ordering issue.
article = createArticleArg({ i: 0, titleSource: 'Mathematics', bodySource: '<derivative>' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit' }),
{ parentId: undefined, previousSiblingId: '@user0/derivative' }
))
assertStatus(status, data)
// Current tree state:
// * 0 Index
// * 1 Mathematics
// * 2 Calculus
// * 3 Derivative
// * 4 Limit
// * 5 Limit of a sequence
// * 6 Limit of a function
// * 7 Integral
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 8, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 8, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 8, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 3, to_id_index: 0, slug: 'user0/derivative' },
{ nestedSetIndex: 4, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/limit' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-sequence' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 4, to_id_index: 1, slug: 'user0/limit-of-a-function' },
{ nestedSetIndex: 7, nestedSetNextSibling: 8, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// Move limit to before ancestor to check that nested set doesn't blow up.
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit' }),
{ parentId: '@user0/mathematics', previousSiblingId: undefined }
))
assertStatus(status, data)
// Ancestors placeholder is present.
assert_xpath("//x:div[@class='nav ancestors']", data.articles[0].h1Render)
// Current tree state:
// * Index
// * Mathematics
// * Limit
// * Limit of a sequence
// * Limit of a function
// * Calculus
// * Derivative
// * Integral
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 8, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 8, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 5, depth: 2, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 3, to_id_index: 0, slug: 'user0/limit-of-a-sequence' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 3, to_id_index: 1, slug: 'user0/limit-of-a-function' },
{ nestedSetIndex: 5, nestedSetNextSibling: 8, depth: 2, to_id_index: 1, slug: 'user0/calculus' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 3, to_id_index: 0, slug: 'user0/derivative' },
{ nestedSetIndex: 7, nestedSetNextSibling: 8, depth: 3, to_id_index: 1, slug: 'user0/integral' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// Move limit back to where it was.
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit' }),
{ parentId: undefined, previousSiblingId: '@user0/derivative' }
))
assertStatus(status, data)
// Current tree state:
// * Index
// * 0 Mathematics
// * 1 Calculus
// * 2 Derivative
// * 3 Limit
// * 4 Limit of a sequence
// * 5 Limit of a function
// * 6 Integral
// TODO restore toc asserts.
//assert_xpath("//*[@id='toc']//x:a[@href='user0/derivative' and @data-test='2' and text()='Derivative']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit' and @data-test='3' and text()='Limit']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit-of-a-sequence' and @data-test='4' and text()='Limit of a sequence']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit-of-a-function' and @data-test='5' and text()='Limit of a function']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/integral' and @data-test='6' and text()='Integral']", data.articles[0].render)
// Move back to first by not giving previousSiblingId. previousSiblingId is not maintained like most updated properties.
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit' }),
{ parentId: undefined }
))
assertStatus(status, data)
// Ancestors placeholder is when neither parentId nor previousSiblingId are given.
assert_xpath("//x:div[@class='nav ancestors']", data.articles[0].h1Render)
// Current tree state:
// * Index
// * 0 Mathematics
// * 1 Calculus
// * 2 Limit
// * 3 Limit of a sequence
// * 4 Limit of a function
// * 5 Derivative
// * 6 Integral
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 8, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 8, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 8, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 6, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-sequence' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 4, to_id_index: 1, slug: 'user0/limit-of-a-function' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 7, nestedSetNextSibling: 8, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// TODO restore toc asserts.
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit' and @data-test='2' and text()='Limit']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit-of-a-sequence' and @data-test='3' and text()='Limit of a sequence']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/limit-of-a-function' and @data-test='4' and text()='Limit of a function']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/derivative' and @data-test='5' and text()='Derivative']", data.articles[0].render)
//assert_xpath("//*[@id='toc']//x:a[@href='user0/integral' and @data-test='6' and text()='Integral']", data.articles[0].render)
// Deduce parentId from previousSiblingid on new article.
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Measure' }),
{ parentId: undefined, previousSiblingId: '@user0/integral' }
))
assertStatus(status, data)
// Current tree state:
// * Index
// * 0 Mathematics
// * 1 Calculus
// * 2 Limit
// * 3 Limit of a sequence
// * 4 Limit of a function
// * 5 Derivative
// * 6 Integral
// * 7 Measure
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 9, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 9, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 9, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 6, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-sequence' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 4, to_id_index: 1, slug: 'user0/limit-of-a-function' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 7, nestedSetNextSibling: 8, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 8, nestedSetNextSibling: 9, depth: 3, to_id_index: 3, slug: 'user0/measure' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// TODO restore toc asserts.
// assert_xpath("//*[@id='toc']//x:a[@href='user0/limit' and @data-test='2' and text()='Limit']", data.articles[0].render)
// assert_xpath("//*[@id='toc']//x:a[@href='user0/limit-of-a-sequence' and @data-test='3' and text()='Limit of a sequence']", data.articles[0].render)
// assert_xpath("//*[@id='toc']//x:a[@href='user0/limit-of-a-function' and @data-test='4' and text()='Limit of a function']", data.articles[0].render)
// assert_xpath("//*[@id='toc']//x:a[@href='user0/derivative' and @data-test='5' and text()='Derivative']", data.articles[0].render)
// assert_xpath("//*[@id='toc']//x:a[@href='user0/integral' and @data-test='6' and text()='Integral']", data.articles[0].render)
// assert_xpath("//*[@id='toc']//x:a[@href='user0/measure' and @data-test='7' and text()='Measure']", data.articles[0].render)
// Refresh Mathematics to show the source ToC.
// Add a reference to the article self: we once had a bug where this was preventing the ToC from showing.
article = createArticleArg({ i: 0, titleSource: 'Mathematics', bodySource: 'I like mathematics.' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0' }))
assertStatus(status, data)
// TODO restore toc asserts.
// assert_xpath("//*[@id='toc']//x:a[@href='/user0/calculus' and @data-test='0' and text()='Calculus']", data.articles[0].render)
// Article.getArticle includeParentAndPreviousSibling argument test.
// Used on editor only for now, so a bit hard to test on UI. But this tests the crux MEGAJOIN just fine.
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 9, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 9, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 9, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 6, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-sequence' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 4, to_id_index: 1, slug: 'user0/limit-of-a-function' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 7, nestedSetNextSibling: 8, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 8, nestedSetNextSibling: 9, depth: 3, to_id_index: 3, slug: 'user0/measure' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// First sibling.
article = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
sequelize,
slug: 'user0/limit',
})
assert.strictEqual(article.parentId.idid, '@user0/calculus')
assert.strictEqual(article.previousSiblingId, undefined)
// Not first sibling.
article = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
sequelize,
slug: 'user0/derivative',
})
assert.strictEqual(article.parentId.idid, '@user0/calculus')
assert.strictEqual(article.previousSiblingId.idid, '@user0/limit')
// Index.
article = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
sequelize,
slug: 'user0',
})
assert.strictEqual(article.parentId, undefined)
assert.strictEqual(article.previousSiblingId, undefined)
// Hopefully the above tests would have caught any wrong to_id_index issues, but just in case.
const refs = await sequelize.models.Ref.findAll({
where: {
type: sequelize.models.Ref.Types[ourbigbook.REFS_TABLE_PARENT],
},
include: [
{
model: sequelize.models.Id,
as: 'to',
where: {
macro_name: ourbigbook.Macro.HEADER_MACRO_NAME,
},
},
],
order: [
['from_id', 'ASC'],
['to_id_index', 'ASC'],
['to_id', 'ASC'],
],
})
assertRows(refs, [
{ from_id: '@user0', to_id: '@user0/mathematics', to_id_index: 0, },
{ from_id: '@user0/calculus', to_id: '@user0/limit', to_id_index: 0, },
{ from_id: '@user0/calculus', to_id: '@user0/derivative', to_id_index: 1, },
{ from_id: '@user0/calculus', to_id: '@user0/integral', to_id_index: 2, },
{ from_id: '@user0/calculus', to_id: '@user0/measure', to_id_index: 3, },
{ from_id: '@user0/limit', to_id: '@user0/limit-of-a-sequence', to_id_index: 0, },
{ from_id: '@user0/limit', to_id: '@user0/limit-of-a-function', to_id_index: 1, },
{ from_id: '@user0/mathematics', to_id: '@user0/calculus', to_id_index: 0, },
])
// previousSiblingId errors
for (const render of [false, true]) {
// previousSiblingId that does not exist fails
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit' }),
{ parentId: undefined, previousSiblingId: '@user0/dontexist', render }
))
assert.strictEqual(status, 422)
// previousSiblingId empty string fails
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit' }),
{ parentId: undefined, previousSiblingId: '', render }
))
assert.strictEqual(status, 422)
// previousSiblingId that is not a child of parentId fails
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: 'Limit' }),
{ parentId: '@user0/mathematics', previousSiblingId: '@user0/derivative', render }
))
assert.strictEqual(status, 422) }
// Forbidden elements
// Cannot use Include on web.
article = createArticleArg({ i: 0, titleSource: 'Physics', bodySource: `\\Include[mathematics]` })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 422)
// Cannot have multiple headers per article on web.
article = createArticleArg({ i: 0, titleSource: 'Physics', bodySource: `== Mechanics` })
;({data, status} = await createArticleApi(test, article))
assert.strictEqual(status, 422)
})
})
it('api: article tree: update first sibling to become a child', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user0' },
])
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 2, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
])
article = createArticleArg({ i: 0, titleSource: 'Physics' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/physics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 1, to_id_index: 1, slug: 'user0/mathematics' },
])
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/physics' }))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 3, depth: 1, to_id_index: 0, slug: 'user0/physics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, to_id_index: 0, slug: 'user0/mathematics' },
])
})
})
it('api: article tree: multiuser', async () => {
await testApp(async (test) => {
let article, data, status
const sequelize = test.sequelize
const user0 = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
const users = [user0, user1]
await assertNestedSetsMultiuser(sequelize, users, [
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, slug: '' },
])
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
await createArticleApiMultiuser(test, users, article)
await assertNestedSetsMultiuser(sequelize, users, [
{ nestedSetIndex: 0, nestedSetNextSibling: 2, depth: 0, slug: '' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, slug: 'mathematics' },
])
article = createArticleArg({ i: 0, titleSource: 'Calculus', })
await createArticleApiMultiuser(test, users, article, { parentId: 'mathematics' })
await assertNestedSetsMultiuser(sequelize, users, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, slug: '' },
{ nestedSetIndex: 1, nestedSetNextSibling: 3, depth: 1, slug: 'mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, slug: 'calculus' },
])
article = createArticleArg({ i: 0, titleSource: 'Natural science' })
await createArticleApiMultiuser(test, users, article)
await assertNestedSetsMultiuser(sequelize, users, [
{ nestedSetIndex: 0, nestedSetNextSibling: 4, depth: 0, slug: '' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, slug: 'natural-science' },
{ nestedSetIndex: 2, nestedSetNextSibling: 4, depth: 1, slug: 'mathematics' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 2, slug: 'calculus' },
])
// Sanity check because now we are going to start modifying just one tree.
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 4, depth: 0, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, slug: 'user0/natural-science' },
{ nestedSetIndex: 2, nestedSetNextSibling: 4, depth: 1, slug: 'user0/mathematics' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 2, slug: 'user0/calculus' },
{ nestedSetIndex: 0, nestedSetNextSibling: 4, depth: 0, slug: 'user1' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, slug: 'user1/natural-science' },
{ nestedSetIndex: 2, nestedSetNextSibling: 4, depth: 1, slug: 'user1/mathematics' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 2, slug: 'user1/calculus' },
])
// Move user0/mathematics before natural science.
test.loginUser(user0)
article = createArticleArg({ i: 0, titleSource: 'Mathematics', })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
// Moving a user0 article does not affect user1's tree.
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 4, depth: 0, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 3, depth: 1, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 1, slug: 'user0/natural-science' },
{ nestedSetIndex: 0, nestedSetNextSibling: 4, depth: 0, slug: 'user1' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, slug: 'user1/natural-science' },
{ nestedSetIndex: 2, nestedSetNextSibling: 4, depth: 1, slug: 'user1/mathematics' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 2, slug: 'user1/calculus' },
])
})
})
it('api: article tree render=false', async () => {
// This is what we have to do on mass upload with ourbigbook --web
// in order to handle circular references without having one massive
// server-side operation.
await testApp(async (test) => {
let data, status, article, ref
const sequelize = test.sequelize
const { Ref } = sequelize.models
const user = await test.createUserApi(0)
test.loginUser(user)
let render
render = false
// Create user0/mathematics
article = createArticleArg({ i: 0, titleSource: 'Mathematics', bodySource: '<calculus>' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render }))
assertStatus(status, data)
// Parent Ref is setup correctly even with render=false.
ref = await Ref.findOne({
where: {
from_id: '@user0',
to_id: '@user0/mathematics',
type: Ref.Types[ourbigbook.REFS_TABLE_PARENT],
},
})
assert.strictEqual(ref.to_id_index, 0)
// Create user0/calculus
article = createArticleArg({ i: 0, titleSource: 'Calculus', bodySource: '<mathematics>' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics', render }))
assertStatus(status, data)
// Parent Ref is setup correctly even with render=false.
ref = await Ref.findOne({
where: {
from_id: '@user0/mathematics',
to_id: '@user0/calculus',
type: Ref.Types[ourbigbook.REFS_TABLE_PARENT],
},
})
assert.strictEqual(ref.to_id_index, 0)
// Create user0/physics
article = createArticleArg({ i: 0, titleSource: 'Physics' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: undefined, previousSiblingId: '@user0/mathematics', render }))
assertStatus(status, data)
// Parent Ref is setup correctly even with render=false.
ref = await Ref.findOne({
where: {
from_id: '@user0',
to_id: '@user0/physics',
type: Ref.Types[ourbigbook.REFS_TABLE_PARENT],
},
})
assert.strictEqual(ref.to_id_index, 1)
// Now the same sequence with render=true
render = true
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user0' },
])
article = createArticleArg({ i: 0, titleSource: 'Mathematics', bodySource: '<calculus>' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render }))
assertStatus(status, data)
// The author is following the article.
;({data, status} = await test.webApi.article('user0/mathematics'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
// Topics are created.
;({data, status} = await test.webApi.topics({ topicId: 'mathematics' }))
assertStatus(status, data)
assert.strictEqual(data.topics[0].topicId, 'mathematics')
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 2, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
])
article = createArticleArg({ i: 0, titleSource: 'Calculus', bodySource: '<mathematics>' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics', render }))
assertStatus(status, data)
// The author is following the article.
;({data, status} = await test.webApi.article('user0/calculus'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
// Topics are created.
;({data, status} = await test.webApi.topics({ topicId: 'calculus' }))
assertStatus(status, data)
assert.strictEqual(data.topics[0].topicId, 'calculus')
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 3, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
])
article = createArticleArg({ i: 0, titleSource: 'Physics' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: undefined, previousSiblingId: '@user0/mathematics', render }))
assertStatus(status, data)
// The author is following the article.
;({data, status} = await test.webApi.article('user0/physics'))
assertStatus(status, data)
assert.strictEqual(data.followerCount, 1)
// Topics are created.
;({data, status} = await test.webApi.topics({ topicId: 'physics' }))
assertStatus(status, data)
assert.strictEqual(data.topics[0].topicId, 'physics')
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 4, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 3, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 1, to_id_index: 1, slug: 'user0/physics' },
])
})
})
it('api: article tree render=true on parent that only has render=false does not blow up', async () => {
await testApp(async (test) => {
let data, status, article, ref
const user = await test.createUserApi(0)
test.loginUser(user)
let render
render = false
// Create user0/mathematics
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render }))
assertStatus(status, data)
// Create user0/calculus
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics', render }))
assertStatus(status, data)
// Now the same sequence with render=true
render = true
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics', render }))
assertStatus(status, data)
})
})
it('api: article tree render=true on previousSiblingId that only has render=false does not blow up', async () => {
await testApp(async (test) => {
let data, status, article, ref
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
let render
render = false
// Create user0/mathematics
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render }))
assertStatus(status, data)
// Create user0/calculus
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics', render }))
assertStatus(status, data)
// Create user0/calculus
article = createArticleArg({ i: 0, titleSource: 'Algebra' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: undefined, previousSiblingId: '@user0/calculus', render }))
assertStatus(status, data)
// Now the same sequence with render=true
render = true
article = createArticleArg({ i: 0, titleSource: 'Algebra' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: undefined, previousSiblingId: '@user0/calculus', render }))
assertStatus(status, data)
})
})
it('api: article tree: updateNestedSetIndex=false and /article/update-nested-set', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
// New articles with updateNestedSetIndex=false
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user0' },
])
// user.nestedSetNeedsUpdate starts off as false.
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, false)
// Conversion with updateNestedSetIndex: false makes nestedSetNeedsUpdate true.
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createOrUpdateArticleApi(test, article, { updateNestedSetIndex: false }))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, true)
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, true)
// The new article simply does not have nested set index position.
// The parent Ref is correct however.
await assertNestedSets(sequelize, [
{ nestedSetIndex: null, nestedSetNextSibling: null, depth: null, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user0' },
])
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createArticleApi(test, article, { parentId: '@user0/mathematics', updateNestedSetIndex: false }))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, true)
await assertNestedSets(sequelize, [
{ nestedSetIndex: null, nestedSetNextSibling: null, depth: null, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: null, nestedSetNextSibling: null, depth: null, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user0' },
])
// articleUpdatedNestedSet fixes the tree and updates the nested set index
;({data, status} = await test.webApi.articleUpdatedNestedSet('user0'))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 3, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
])
// User.nestedSetNeedsUpdate becomes false after the nested se is updated
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, false)
// Update existing articles with updateNestedSetIndex=false
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createOrUpdateArticleApi(test, article, { updateNestedSetIndex: false }))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, true)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 3, depth: 1, to_id_index: 1, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
])
;({data, status} = await test.webApi.articleUpdatedNestedSet('user0'))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 1, to_id_index: 1, slug: 'user0/mathematics' },
])
// nestedSetNeedsUpdate remains false when there are no tree changes.
article = createArticleArg({ i: 0, titleSource: 'Calculus', bodySource: 'Hacked' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, false)
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, false)
// nestedSetNeedsUpdate becomes true when there are tree changes and render=false.
article = createArticleArg({ i: 0, titleSource: 'Mathematics', bodySource: 'Hacked' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render: false }))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, true)
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, true)
// createOrUpdateArticleApi nestedSetNeedsUpdate return is false when there are no tree changes and render=false.
// It does not consider the current user.nestedSetNeedsUpdate state, it only informs if the
// current change would have modified that state or not.
article = createArticleArg({ i: 0, titleSource: 'Mathematics', bodySource: 'Hacked 2' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render: false }))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, false)
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, true)
// Move math up with a full render.
article = createArticleArg({ i: 0, titleSource: 'Mathematics', bodySource: 'Hacked 2' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
;({data, status} = await test.webApi.articleUpdatedNestedSet('user0'))
assertStatus(status, data)
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, false)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 1, to_id_index: 1, slug: 'user0/calculus' },
])
// nestedSetNeedsUpdate remains false when there are tree changes, but we are updating the index;
article = createArticleArg({ i: 0, titleSource: 'Calculus', bodySource: 'Hacked 2' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, false)
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.nestedSetNeedsUpdate, false)
})
})
it('api: article tree: updateNestedSetIndex=false circular loop check is done with Ref and not nested set index', async () => {
// Reproduction for: https://github.com/ourbigbook/ourbigbook/issues/319#issuecomment-2662912799
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
article = createArticleArg({
i: 0,
titleSource: 'Mathematics',
})
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Physics' })
;({data, status} = await createArticleApi(test, article, { parentId: undefined, previousSiblingId: '@user0/mathematics' }))
assertStatus(status, data)
const physicsHash = data.articles[0].file.hash
// New articles with updateNestedSetIndex=false
// Create.
// 0
// 1
// 2
// 3
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 1 }), { updateNestedSetIndex: false }))
assertStatus(status, data)
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 2 }), { parentId: '@user0/title-1', updateNestedSetIndex: false }))
assertStatus(status, data)
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 3 }), { parentId: undefined, previousSiblingId: '@user0/title-2', updateNestedSetIndex: false }))
assertStatus(status, data)
//;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 4 }), { previousSiblingId: '@user0/title-3', updateNestedSetIndex: false }))
//assertStatus(status, data)
;({data, status} = await test.webApi.articleUpdatedNestedSet('user0'))
assertStatus(status, data)
//;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 5 }), { previousSiblingId: '@user0/title-4', updateNestedSetIndex: false }))
//assertStatus(status, data)
//;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 1 }), { parentId: '@user0/title-5', updateNestedSetIndex: false }))
//assertStatus(status, data)
// Move to.
// 0
// 1
// 3
// 2
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 2 }), { parentId: undefined, previousSiblingId: '@user0/title-1', updateNestedSetIndex: false }))
assertStatus(status, data)
// Move to.
// 0
// 2
// 1
// 3
// This is the main initial point of this test
// At one point this was blowing up on an incorrect safety check because were checking
// for parentId loops based on nested set, which is out of date relative to the canonical Ref.
// The database was semi-safe because we also did a check for this at render time, but it was horrendous.
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 1 }), { parentId: '@user0/title-2', updateNestedSetIndex: false }))
assertStatus(status, data)
})
})
it('api: child articles inherit scope from parent', async () => {
// This is what we have to do on mass upload with ourbigbook --web
// in order to handle circular references without having one massive
// server-side operation.
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
article = createArticleArg({ i: 0, titleSource: 'Mathematics', bodySource: '{scope}' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Calculus', bodySource: '{scope}' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
;({data, status} = await test.webApi.issueCreate('user0/mathematics/calculus', createIssueArg(0, 0, 0)))
assertStatus(status, data)
;({data, status} = await test.webApi.article('user0/mathematics/calculus'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Calculus')
article = createArticleArg({ i: 0, titleSource: 'Derivative' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics/calculus' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Physics', bodySource: `<mathematics/calculus>
<mathematics/calculus/derivative>
` })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
assert_xpath("//x:a[@href='/user0/mathematics/calculus' and text()='calculus']", data.articles[0].render)
assert_xpath("//x:a[@href='/user0/mathematics/calculus/derivative' and text()='derivative']", data.articles[0].render)
if (testNext) {
// Tests with the same result for logged in or off.
async function testNextLoggedInOrOff(loggedInUser) {
// Article
;({data, status} = await test.sendJsonHttp('GET', routes.article('user0/mathematics/calculus'), ))
assertStatus(status, data)
// Article links
;({data, status} = await test.sendJsonHttp('GET', routes.userArticlesChildren('user0', 'mathematics/calculus'), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userArticlesIncoming('user0', 'mathematics/calculus'), ))
assertStatus(status, data)
;({data, status} = await test.sendJsonHttp('GET', routes.userArticlesTagged('user0', 'mathematics/calculus'), ))
assertStatus(status, data)
// Issue
;({data, status} = await test.sendJsonHttp('GET', routes.issue('user0/mathematics/calculus', 1), ))
assertStatus(status, data)
}
// Logged in.
await testNextLoggedInOrOff(true)
// Logged out.
test.disableToken()
await testNextLoggedInOrOff(false)
test.loginUser(user)
}
}, { canTestNext: true })
})
it('api: child articles inherit scope from previousSiblingId', async () => {
// Parent is calculated from previousSiblingId, and then its scope
// is used as if parentId had been passed. Yes it is that bad, this was once broken.
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
article = createArticleArg({ i: 0, titleSource: 'Mathematics', bodySource: '{scope}' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Algebra' })
;({data, status} = await createOrUpdateArticleApi(test, article, {
parentId: undefined,
previousSiblingId: '@user0/mathematics/calculus'
}))
assertStatus(status, data)
;({data, status} = await test.webApi.article('user0/mathematics/algebra'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Algebra')
})
})
it('api: synonym rename', async () => {
// This is what we have to do on mass upload with ourbigbook --web
// in order to handle circular references without having one massive
// server-side operation.
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
test.loginUser(user)
// Create a basic hierarchy.
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
// Has Calculus as previous sibling.
article = createArticleArg({ i: 0, titleSource: 'Algebra' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Calculus', bodySource: '\\Image[http://jpg]{title=My image}\n' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Integral' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/calculus' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Fundamental theorem of calculus' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/integral' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Derivative', bodySource: '<Calculus>\n' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/calculus' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Chain rule' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/derivative' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Limit' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/calculus' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Limit of a series' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/limit' }))
assertStatus(status, data)
// Add some metadata to our article of interest.
;({data, status} = await test.webApi.issueCreate('user0/calculus',
{ titleSource: 'Calculus issue 0' }
))
assertStatus(status, data)
test.loginUser(user1)
;({data, status} = await test.webApi.articleLike('user0/calculus'))
assertStatus(status, data)
test.loginUser(user)
// Sanity checks
;({data, status} = await test.webApi.issue('user0/calculus', 1))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Calculus issue 0')
// Current tree state:
// * 0 user0/Index
// * 1 Mathematics
// * 2 Calculus
// * 3 Limit
// * 4 Limit of a series
// * 5 Derivative
// * 6 Chain rule
// * 7 Integral
// * 8 Fundamental theorem of calculus
// * 9 Algebra
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 10, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 10, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 9, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 5, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-series' },
{ nestedSetIndex: 5, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 4, to_id_index: 0, slug: 'user0/chain-rule' },
{ nestedSetIndex: 7, nestedSetNextSibling: 9, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 8, nestedSetNextSibling: 9, depth: 4, to_id_index: 0, slug: 'user0/fundamental-theorem-of-calculus' },
{ nestedSetIndex: 9, nestedSetNextSibling: 10, depth: 2, to_id_index: 1, slug: 'user0/algebra' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// Sanity check that the parent and previous sibling are correct.
;({data, status} = await test.webApi.article('user0/derivative', { 'include-parent': QUERY_TRUE_VAL }))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Derivative')
assert.strictEqual(data.parentId, '@user0/calculus')
;({data, status} = await test.webApi.article('user0/algebra', { 'include-parent': QUERY_TRUE_VAL }))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Algebra')
assert.strictEqual(data.previousSiblingId, '@user0/calculus')
// Sanity check scores
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.username, 'user0')
assert.strictEqual(data.score, 1)
;({data, status} = await test.webApi.article('user0/calculus'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Calculus')
assert.strictEqual(data.score, 1)
// Add calculus-2 as a synonym of calculus without changing title.
article = createArticleArg({
i: 0,
titleSource: 'Calculus',
bodySource: `= Calculus 2
{synonym}
\\Image[http://jpg]{title=My image}
`
})
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
// Current tree state:
// * 0 user0/Index
// * 1 Mathematics
// * 2 Calculus (Calculus 2)
// * 3 Limit
// * 4 Limit of a series
// * 5 Derivative
// * 6 Chain rule
// * 7 Integral
// * 8 Fundamental theorem of calculus
// * 9 Algebra
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 10, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 10, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 9, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 5, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-series' },
{ nestedSetIndex: 5, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 4, to_id_index: 0, slug: 'user0/chain-rule' },
{ nestedSetIndex: 7, nestedSetNextSibling: 9, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 8, nestedSetNextSibling: 9, depth: 4, to_id_index: 0, slug: 'user0/fundamental-theorem-of-calculus' },
{ nestedSetIndex: 9, nestedSetNextSibling: 10, depth: 2, to_id_index: 1, slug: 'user0/algebra' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// The synonym exists as a redirect.
;({data, status} = await test.webApi.articleRedirects({ id: 'user0/calculus-2' }))
assertStatus(status, data)
assert.strictEqual(data.redirects['user0/calculus-2'], 'user0/calculus')
// synonym does not break parentId and previousSibling
;({data, status} = await test.webApi.article('user0/derivative', { 'include-parent': QUERY_TRUE_VAL }))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Derivative')
assert.strictEqual(data.parentId, '@user0/calculus')
;({data, status} = await test.webApi.article('user0/algebra', { 'include-parent': QUERY_TRUE_VAL }))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Algebra')
assert.strictEqual(data.previousSiblingId, '@user0/calculus')
// webApi.article( uses Article.getArticles, just double check
// with the Article.getARticle (singular) version.
const algebra = await test.sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
sequelize,
slug: 'user0/algebra',
})
assert.strictEqual(algebra.parentId.idid, '@user0/mathematics')
assert.strictEqual(algebra.previousSiblingId.idid, '@user0/calculus')
// Issues are not broken by adding the synonym.
;({data, status} = await test.webApi.issue('user0/calculus', 1))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Calculus issue 0')
// Add a link to the new synonym.
article = createArticleArg({ i: 0, titleSource: 'Derivative', bodySource: '<Calculus>\n\n<Calculus 2>\n' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/calculus', previousSiblingId: '@user0/limit' }))
assertStatus(status, data)
// TODO Links to synonym header from have fragment
// https://docs.ourbigbook.com/todo/links-to-synonym-header-have-fragment
assert_xpath("//x:a[@href='/user0/calculus' and text()='Calculus']", data.articles[0].render)
assert_xpath("//x:a[@href='/user0/calculus' and text()='Calculus 2']", data.articles[0].render)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 10, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 10, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 9, depth: 2, to_id_index: 0, slug: 'user0/calculus' },
{ nestedSetIndex: 3, nestedSetNextSibling: 5, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-series' },
{ nestedSetIndex: 5, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 4, to_id_index: 0, slug: 'user0/chain-rule' },
{ nestedSetIndex: 7, nestedSetNextSibling: 9, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 8, nestedSetNextSibling: 9, depth: 4, to_id_index: 0, slug: 'user0/fundamental-theorem-of-calculus' },
{ nestedSetIndex: 9, nestedSetNextSibling: 10, depth: 2, to_id_index: 1, slug: 'user0/algebra' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// Add a tag to the new synonym.
article = createArticleArg({ i: 0, titleSource: 'Integral', bodySource: '{tag=Calculus 2}\n' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/calculus', previousSiblingId: '@user0/derivative' }))
assertStatus(status, data)
// Sanity: the File object exists.
{
const file = await sequelize.models.File.findOne({ where: { path: '@user0/calculus.bigb' } })
assert.notStrictEqual(file, null)
}
// Rename Calculus to Calculus 3
article = createArticleArg({
i: 0,
titleSource: 'Calculus 3',
bodySource: `= Calculus
{synonym}
= Calculus 2
{synonym}
\\Image[http://jpg]{title=My image}
`
})
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
// The File object was removed.
{
const file = await sequelize.models.File.findOne({ where: { path: '@user0/calculus.bigb' } })
assert.strictEqual(file, null)
}
// Current tree state:
// * 0 user0/Index
// * 1 Mathematics
// * 2 Calculus 3 (Calculus, Calculus 2)
// * 3 Limit
// * 4 Limit of a series
// * 5 Derivative
// * 6 Chain rule
// * 7 Integral
// * 8 Fundamental theorem of calculus
// * 9 Algebra
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 10, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 10, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 9, depth: 2, to_id_index: 0, slug: 'user0/calculus-3' },
{ nestedSetIndex: 3, nestedSetNextSibling: 5, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-series' },
{ nestedSetIndex: 5, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 4, to_id_index: 0, slug: 'user0/chain-rule' },
{ nestedSetIndex: 7, nestedSetNextSibling: 9, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 8, nestedSetNextSibling: 9, depth: 4, to_id_index: 0, slug: 'user0/fundamental-theorem-of-calculus' },
{ nestedSetIndex: 9, nestedSetNextSibling: 10, depth: 2, to_id_index: 1, slug: 'user0/algebra' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// Check that the latest name exists as the main one.
;({data, status} = await test.webApi.article('user0/calculus-3'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Calculus 3')
assert.strictEqual(data.file.bodySource, `= Calculus
{synonym}
= Calculus 2
{synonym}
\\Image[http://jpg]{title=My image}
`
)
// The score is zeroed on rename to prevent rename spam.
assert.strictEqual(data.score, 0)
// The user score is reduce accordingly.
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.username, 'user0')
assert.strictEqual(data.score, 0)
// Check that the metadata is now associated to the article with the new main title.
;({data, status} = await test.webApi.issue('user0/calculus-3', 1))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Calculus issue 0')
// Check that the previous name now exists as a redirect.
;({data, status} = await test.webApi.articleRedirects({ id: 'user0/calculus' }))
assertStatus(status, data)
assert.strictEqual(data.redirects['user0/calculus'], 'user0/calculus-3')
;({data, status} = await test.webApi.articleRedirects({ id: 'user0/calculus-2' }))
assertStatus(status, data)
assert.strictEqual(data.redirects['user0/calculus-2'], 'user0/calculus-3')
// Children of the renamed article now point to the new parent,
// Previous sibling is also updated.
;({data, status} = await test.webApi.article('user0/derivative', { 'include-parent': true }))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Derivative')
assert.strictEqual(data.parentId, '@user0/calculus-3')
;({data, status} = await test.webApi.article('user0/algebra'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Algebra')
// TODO https://docs.ourbigbook.com/todo/fix-parentid-and-previoussiblingid-on-articles-api
//assert.strictEqual(data.previousSiblingId, '@user0/calculus-3')
{
const algebra = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
slug: 'user0/algebra',
sequelize
})
assert.strictEqual(algebra.parentId.idid, '@user0/mathematics')
assert.strictEqual(algebra.previousSiblingId.idid, '@user0/calculus-3')
}
// Only the main Article retains a File object.
assert.notStrictEqual(await sequelize.models.File.findOne({ where: { path: '@user0/calculus-3.bigb' } }), null)
assert.strictEqual(await sequelize.models.File.findOne({ where: { path: '@user0/calculus.bigb' } }), null)
assert.strictEqual(await sequelize.models.File.findOne({ where: { path: '@user0/calculus-2.bigb' } }), null)
// extract_ids without render of header with synonym does not blow up.
// Yes, everything breaks everything.
article = createArticleArg({
i: 0,
titleSource: 'Calculus 3',
bodySource: `= Calculus
{synonym}
= Calculus 2
{synonym}
\\Image[http://jpg]{title=My image}
`
})
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: undefined, render: false }))
assertStatus(status, data)
// Current tree state:
// * 0 user0/Index
// * 1 Mathematics
// * 2 Calculus 3 (Calculus, Calculus 2)
// * 3 Limit
// * 4 Limit of a series
// * 5 Derivative
// * 6 Chain rule
// * 7 Integral
// * 8 Fundamental theorem of calculus
// * 9 Algebra
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 10, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 10, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 9, depth: 2, to_id_index: 0, slug: 'user0/calculus-3' },
{ nestedSetIndex: 3, nestedSetNextSibling: 5, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-series' },
{ nestedSetIndex: 5, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 4, to_id_index: 0, slug: 'user0/chain-rule' },
{ nestedSetIndex: 7, nestedSetNextSibling: 9, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 8, nestedSetNextSibling: 9, depth: 4, to_id_index: 0, slug: 'user0/fundamental-theorem-of-calculus' },
{ nestedSetIndex: 9, nestedSetNextSibling: 10, depth: 2, to_id_index: 1, slug: 'user0/algebra' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// Adding synonym to index is fine.
article = createArticleArg({ i: 0, titleSource: '', bodySource: '= Index 2\n{synonym}' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
// Current tree state:
// * 0 user0/Index (Index 2)
// * 1 Mathematics
// * 2 Calculus 3 (Calculus, Calculus 2)
// * 3 Limit
// * 4 Limit of a series
// * 5 Derivative
// * 6 Chain rule
// * 7 Integral
// * 8 Fundamental theorem of calculus
// * 9 Algebra
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 10, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 10, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 9, depth: 2, to_id_index: 0, slug: 'user0/calculus-3' },
{ nestedSetIndex: 3, nestedSetNextSibling: 5, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-series' },
{ nestedSetIndex: 5, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 4, to_id_index: 0, slug: 'user0/chain-rule' },
{ nestedSetIndex: 7, nestedSetNextSibling: 9, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 8, nestedSetNextSibling: 9, depth: 4, to_id_index: 0, slug: 'user0/fundamental-theorem-of-calculus' },
{ nestedSetIndex: 9, nestedSetNextSibling: 10, depth: 2, to_id_index: 1, slug: 'user0/algebra' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// Renaming index via synonyms is not allowed.
article = createArticleArg({ i: 0, titleSource: 'Index 3', bodySource: '= Index\n{synonym}\n\n= Index 2\n{synonym}\n' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assert.strictEqual(status, 422)
// Current tree state:
// * 0 user0/Index (Index 2)
// * 1 Mathematics
// * 2 Calculus 3 (Calculus, Calculus 2)
// * 3 Limit
// * 4 Limit of a series
// * 5 Derivative
// * 6 Chain rule
// * 7 Integral
// * 8 Fundamental theorem of calculus
// * 9 Algebra
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 10, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 10, depth: 1, to_id_index: 0, slug: 'user0/mathematics' },
{ nestedSetIndex: 2, nestedSetNextSibling: 9, depth: 2, to_id_index: 0, slug: 'user0/calculus-3' },
{ nestedSetIndex: 3, nestedSetNextSibling: 5, depth: 3, to_id_index: 0, slug: 'user0/limit' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-series' },
{ nestedSetIndex: 5, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/derivative' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 4, to_id_index: 0, slug: 'user0/chain-rule' },
{ nestedSetIndex: 7, nestedSetNextSibling: 9, depth: 3, to_id_index: 2, slug: 'user0/integral' },
{ nestedSetIndex: 8, nestedSetNextSibling: 9, depth: 4, to_id_index: 0, slug: 'user0/fundamental-theorem-of-calculus' },
{ nestedSetIndex: 9, nestedSetNextSibling: 10, depth: 2, to_id_index: 1, slug: 'user0/algebra' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1' },
])
// Article merge
// Add user1 metadata to user0/derivative
test.loginUser(user1)
// Like derivative.
;({data, status} = await test.webApi.articleLike('user0/derivative'))
assertStatus(status, data)
// Create issue.
;({data, status} = await test.webApi.issueCreate('user0/derivative',
{ titleSource: 'Derivative issue 0' }
))
assertStatus(status, data)
test.loginUser(user)
// Sanity check that user score is now up to 1
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.username, 'user0')
assert.strictEqual(data.score, 1)
// Sanity check that the issue is visible.
;({data, status} = await test.webApi.issue('user0/derivative', 1))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Derivative issue 0')
// Sanity: the File object exists
{
const file = await sequelize.models.File.findOne({ where: { path: '@user0/derivative.bigb' } })
assert.notStrictEqual(file, null)
}
// Rename derivative to calculus-3, triggering an article merge
// of user0/derivative to user/calculus-3.
article = createArticleArg({
i: 0,
titleSource: 'Calculus 3',
bodySource: `= Calculus
{synonym}
= Calculus 2
{synonym}
= Derivative
{synonym}
\\Image[http://jpg]{title=My image}
`,
})
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: undefined }))
assertStatus(status, data)
// The File object was removed.
{
const file = await sequelize.models.File.findOne({ where: { path: '@user0/derivative.bigb' } })
assert.strictEqual(file, null)
}
// Current tree state:
// * 0 user0/Index
// * 1 Mathematics
// * 2 Calculus 3 (Calculus, Calculus 2, Derivative)
// * 3 Limit
// * 4 Limit of a series
// * 5 Integral
// * 6 Fundamental theorem of calculus
// * 7 Chain rule
// * 8 Algebra
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 9, depth: 0, to_id_index: null, slug: 'user0', parentId: null },
{ nestedSetIndex: 1, nestedSetNextSibling: 9, depth: 1, to_id_index: 0, slug: 'user0/mathematics', parentId: '@user0' },
{ nestedSetIndex: 2, nestedSetNextSibling: 8, depth: 2, to_id_index: 0, slug: 'user0/calculus-3', parentId: '@user0/mathematics' },
{ nestedSetIndex: 3, nestedSetNextSibling: 5, depth: 3, to_id_index: 0, slug: 'user0/limit', parentId: '@user0/calculus-3' },
{ nestedSetIndex: 4, nestedSetNextSibling: 5, depth: 4, to_id_index: 0, slug: 'user0/limit-of-a-series', parentId: '@user0/limit' },
{ nestedSetIndex: 5, nestedSetNextSibling: 7, depth: 3, to_id_index: 1, slug: 'user0/integral', parentId: '@user0/calculus-3' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 4, to_id_index: 0, slug: 'user0/fundamental-theorem-of-calculus', parentId: '@user0/integral' },
{ nestedSetIndex: 7, nestedSetNextSibling: 8, depth: 3, to_id_index: 2, slug: 'user0/chain-rule', parentId: '@user0/calculus-3' },
{ nestedSetIndex: 8, nestedSetNextSibling: 9, depth: 2, to_id_index: 1, slug: 'user0/algebra', parentId: '@user0/mathematics' },
{ nestedSetIndex: 0, nestedSetNextSibling: 1, depth: 0, to_id_index: null, slug: 'user1', parentId: null },
])
// user0/derivative redirects to user0/calculus-3
;({data, status} = await test.webApi.articleRedirects({ id: 'user0/derivative' }))
assertStatus(status, data)
assert.strictEqual(data.redirects['user0/derivative'], 'user0/calculus-3')
// user0/chain-rule, previously a child of user0/derivative, is reparented
// to the new parent user0/calculus-3
{
const article = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
slug: 'user0/chain-rule',
sequelize
})
assert.strictEqual(article.parentId.idid, '@user0/calculus-3')
assert.strictEqual(article.previousSiblingId.idid, '@user0/integral')
}
// Issue 1 of user0/derivative is migrated as issue 2 of user/calculus-3,
// following the pre-existing issue 1.
;({data, status} = await test.webApi.issue('user0/calculus-3', 1))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Calculus issue 0')
;({data, status} = await test.webApi.issue('user0/calculus-3', 2))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Derivative issue 0')
// user0 score is back down to 0 since likes of merged articles are deleted to prevent spam.
;({data, status} = await test.webApi.user('user0'))
assertStatus(status, data)
assert.strictEqual(data.username, 'user0')
assert.strictEqual(data.score, 0)
// Creating a new article with a synonym does not blow up
article = createArticleArg({
i: 0,
titleSource: 'Geometry',
bodySource: `= Geometry 2
{synonym}
`,
})
;({data, status} = await createOrUpdateArticleApi(test, article, {
parentId: '@user0/mathematics',
previousSiblingId: '@user0/algebra',
}))
assertStatus(status, data)
// Current tree state:
// * 0 user0/Index
// * 1 Mathematics
// * 2 Calculus 3 (Calculus, Calculus 2, Derivative)
// * 3 Limit
// * 4 Limit of a series
// * 5 Integral
// * 6 Fundamental theorem of calculus
// * 7 Chain rule
// * 8 Algebra
// * 9 Geometry (Geometry 2)
// Creating a new article with a synonym with render: false does not blow up.
article = createArticleArg({
i: 0,
titleSource: 'Number theory',
bodySource: `= Number theory 2
{synonym}
`,
})
;({data, status} = await createOrUpdateArticleApi(test, article, {
parentId: '@user0/mathematics',
previousSiblingId: '@user0/geometry',
render: false,
}))
assertStatus(status, data)
;({data, status} = await createOrUpdateArticleApi(test, article, {
parentId: '@user0/mathematics',
previousSiblingId: '@user0/geometry',
render: true,
}))
assertStatus(status, data)
// Current tree state:
// * 0 user0/Index
// * 1 Mathematics
// * 2 Calculus 3 (Calculus, Calculus 2, Derivative)
// * 3 Limit
// * 4 Limit of a series
// * 5 Integral
// * 6 Fundamental theorem of calculus
// * 7 Chain rule
// * 8 Algebra
// * 9 Geometry (Geometry 2)
// * 10 Number theory (Number theory 2)
// Creating a new article with id and a synonym does not blow up.
article = createArticleArg({
i: 0,
titleSource: 'Ł',
bodySource: `{id=weird-l}
{title2=wa}
{title2=wo}
= L with a stroke
{synonym}
`,
})
;({data, status} = await createOrUpdateArticleApi(test, article,))
assertStatus(status, data)
assert.strictEqual(data.articles[0].slug, 'user0/weird-l')
})
})
it('api: uppercase article IDs are forbidden', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
// The only way to currently obtain them on toplevel article is with the path: argument.
// id= does nothing on web as of writing as it gets overridden by path:.
article = createArticleArg({ i: 0, titleSource: 'Aa' })
;({data, status} = await createOrUpdateArticleApi(test, article, { path: 'bB' }))
assert.strictEqual(status, 422)
// Similar for synonym subobjects.
// TODO: Forbid uppercase IDs on web and CLI by default
//article = createArticleArg({ i: 0, titleSource: 'Aa', bodySource: '= Bb\n{synonym}\n{id=cC}\n' })
//;({data, status} = await createOrUpdateArticleApi(test, article))
//assert.strictEqual(status, 422)
// Similar for other subobjects.
// TODO: Forbid uppercase IDs on web and CLI by default
//article = createArticleArg({ i: 0, titleSource: 'Aa', bodySource: '\\Image[tmp.png]{id=cC}' })
//;({data, status} = await createOrUpdateArticleApi(test, article))
//assert.strictEqual(status, 422)
// Uppercase is allowed with {file} however.
article = createArticleArg({ i: 0, titleSource: 'path/to/main.S', bodySource: '{file}' })
;({data, status} = await createOrUpdateArticleApi(test, article, { path: '_file/path/to/main.S' }))
assertStatus(status, data)
;({data, status} = await test.webApi.article('user0/_file/path/to/main.S'))
assertStatus(status, data)
assert.notStrictEqual(data, undefined)
// Also works without explicit path.
article = createArticleArg({ i: 0, titleSource: 'path/to/main2.S', bodySource: '{file}' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
;({data, status} = await test.webApi.article('user0/_file/path/to/main2.S'))
assertStatus(status, data)
assert.notStrictEqual(data, undefined)
await models.normalize({
check: true,
sequelize,
whats: ['nested-set'],
})
})
})
it('api: hideArticleDates', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
// New articles created with hideArticleDates=false don't have the dummy date.
article = createArticleArg({ i: 0, titleSource: 'Before' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
assert.notStrictEqual(data.articles[0].createdAt, config.hideArticleDatesDate)
assert.notStrictEqual(data.articles[0].updatedAt, config.hideArticleDatesDate)
// Set hideArticleDates to true.
;({data, status} = await test.webApi.userUpdate('user0', {
hideArticleDates: true,
}))
// New articles created after hideArticleDates=true have the dummy date.
article = createArticleArg({ i: 0, titleSource: 'After' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
assert.strictEqual(data.articles[0].createdAt, config.hideArticleDatesDate)
assert.strictEqual(data.articles[0].updatedAt, config.hideArticleDatesDate)
// Updates change the createdAt and updatedAt dates of existing articles.
article = createArticleArg({ i: 0, titleSource: 'Before' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
// TODO would be slightly better if this were also reset. However it appears
// that bulkCreate doesn't set createdAt even if if is passed explicitly on
// updateOnDuplicate.
assert.notStrictEqual(data.articles[0].createdAt, config.hideArticleDatesDate)
assert.strictEqual(data.articles[0].updatedAt, config.hideArticleDatesDate)
})
})
it('api: editor/fetch-files', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
// Create articles
article = createArticleArg({ i: 0, titleSource: 'Mathematics' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
// Fetch and check some files.
;({data, status} = await test.webApi.editorFetchFiles([ '@user0/mathematics.bigb', '@user0/calculus.bigb' ]))
assertStatus(status, data)
assertRows(data.files, [
{ path: '@user0/calculus.bigb', toplevel_id: '@user0/calculus' },
{ path: '@user0/mathematics.bigb', toplevel_id: '@user0/mathematics' },
])
})
})
it('api: ourbigbook LaTeX macros are defined', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
article = createArticleArg({
i: 0,
titleSource: 'Mathematics',
bodySource: `$$
\\abs{x}
$$
`,
})
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
})
})
it(`api: topic links don't have the domain name`, async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
article = createArticleArg({
i: 0,
titleSource: 'Mathematics',
bodySource: `<#My Topic>
`,
})
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
assert_xpath(`//x:div[@class='p']//x:a[@href='/go/topic/my-topic' and text()='My Topic']`, data.articles[0].render)
article = createArticleArg({
i: 0,
titleSource: '',
bodySource: `<#My Topic>
`,
})
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
assert_xpath(`//x:div[@class='p']//x:a[@href='/go/topic/my-topic' and text()='My Topic']`, data.articles[0].render)
})
})
it('api: parent and child to unrelated synonyms with updateNestedSetIndex', async () => {
// Attempt to reproduce: https://docs.ourbigbook.com/todo/5
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
article = createArticleArg({ i: 0, titleSource: 'h2' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'h2-2' })
;({data, status} = await createArticleApi(test, article, { parentId: '@user0/h2' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'h1' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'h1-2' })
;({data, status} = await createArticleApi(test, article, { parentId: '@user0/h1' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'h1-1' })
;({data, status} = await createArticleApi(test, article, { parentId: '@user0/h1' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'h1-1-1' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/h1-1' }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'h1-2-1' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/h1-2' }))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 8, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 6, depth: 1, to_id_index: 0, slug: 'user0/h1' },
{ nestedSetIndex: 2, nestedSetNextSibling: 4, depth: 2, to_id_index: 0, slug: 'user0/h1-1' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 3, to_id_index: 0, slug: 'user0/h1-1-1' },
{ nestedSetIndex: 4, nestedSetNextSibling: 6, depth: 2, to_id_index: 1, slug: 'user0/h1-2' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 3, to_id_index: 0, slug: 'user0/h1-2-1' },
{ nestedSetIndex: 6, nestedSetNextSibling: 8, depth: 1, to_id_index: 1, slug: 'user0/h2' },
{ nestedSetIndex: 7, nestedSetNextSibling: 8, depth: 2, to_id_index: 0, slug: 'user0/h2-2' },
])
// Update sanity check without synonym.
article = createArticleArg({ i: 0, titleSource: 'h1-2', bodySource: 'asdf' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/h1', previousSiblingId: '@user0/h1-1' }))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 8, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 6, depth: 1, to_id_index: 0, slug: 'user0/h1' },
{ nestedSetIndex: 2, nestedSetNextSibling: 4, depth: 2, to_id_index: 0, slug: 'user0/h1-1' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 3, to_id_index: 0, slug: 'user0/h1-1-1' },
{ nestedSetIndex: 4, nestedSetNextSibling: 6, depth: 2, to_id_index: 1, slug: 'user0/h1-2' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 3, to_id_index: 0, slug: 'user0/h1-2-1' },
{ nestedSetIndex: 6, nestedSetNextSibling: 8, depth: 1, to_id_index: 1, slug: 'user0/h2' },
{ nestedSetIndex: 7, nestedSetNextSibling: 8, depth: 2, to_id_index: 0, slug: 'user0/h2-2' },
])
// Update with synonym.
article = createArticleArg({ i: 0, titleSource: 'h1-2', bodySource: '= h2-2\n{synonym}' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/h1', previousSiblingId: '@user0/h1-1' }))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 7, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 6, depth: 1, to_id_index: 0, slug: 'user0/h1' },
{ nestedSetIndex: 2, nestedSetNextSibling: 4, depth: 2, to_id_index: 0, slug: 'user0/h1-1' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 3, to_id_index: 0, slug: 'user0/h1-1-1' },
{ nestedSetIndex: 4, nestedSetNextSibling: 6, depth: 2, to_id_index: 1, slug: 'user0/h1-2' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 3, to_id_index: 0, slug: 'user0/h1-2-1' },
{ nestedSetIndex: 6, nestedSetNextSibling: 7, depth: 1, to_id_index: 1, slug: 'user0/h2' },
])
article = createArticleArg({ i: 0, titleSource: 'h1-1', bodySource: '= h2\n{synonym}' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/h1' }))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 6, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 6, depth: 1, to_id_index: 0, slug: 'user0/h1' },
{ nestedSetIndex: 2, nestedSetNextSibling: 4, depth: 2, to_id_index: 0, slug: 'user0/h1-1' },
{ nestedSetIndex: 3, nestedSetNextSibling: 4, depth: 3, to_id_index: 0, slug: 'user0/h1-1-1' },
{ nestedSetIndex: 4, nestedSetNextSibling: 6, depth: 2, to_id_index: 1, slug: 'user0/h1-2' },
{ nestedSetIndex: 5, nestedSetNextSibling: 6, depth: 3, to_id_index: 0, slug: 'user0/h1-2-1' },
])
})
})
// https://github.com/ourbigbook/ourbigbook/issues/306
it('api: article create with synonym parent uses the synonym target', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
// Create an article with synonym.
article = createArticleArg({ i: 0, titleSource: 'h2', bodySource: '= h2 2\n{synonym}' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 2, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 2, depth: 1, to_id_index: 0, slug: 'user0/h2' },
])
// Point the parent of h3 to the synonym h2-2.
article = createArticleArg({ i: 0, titleSource: 'h3' })
;({data, status} = await createArticleApi(test, article, { parentId: '@user0/h2-2' }))
assertStatus(status, data)
await assertNestedSets(sequelize, [
{ nestedSetIndex: 0, nestedSetNextSibling: 3, depth: 0, to_id_index: null, slug: 'user0' },
{ nestedSetIndex: 1, nestedSetNextSibling: 3, depth: 1, to_id_index: 0, slug: 'user0/h2' },
{ nestedSetIndex: 2, nestedSetNextSibling: 3, depth: 2, to_id_index: 0, slug: 'user0/h3' },
])
{
const article = await sequelize.models.Article.getArticle({
includeParentAndPreviousSibling: true,
sequelize,
slug: 'user0/h3',
})
assert.strictEqual(article.parentId.idid, '@user0/h2')
}
})
})
it('api: circular parent loop to self synonym fails gracefully', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
article = createArticleArg({ i: 0, titleSource: 'h2', bodySource: '= h2 2\n{synonym}\n' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render: false }))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'h2', bodySource: '= h2 2\n{synonym}\n' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/h2-2', render: false }))
assert.strictEqual(status, 422)
article = createArticleArg({ i: 0, titleSource: 'h2' })
;({data, status} = await createOrUpdateArticleApi(test, article, { parentId: '@user0/h2-2', render: false }))
assert.strictEqual(status, 422)
})
})
it('api: synonyms lead to o sensible redirects', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
article = createArticleArg({ i: 0, titleSource: 'h2', bodySource: '= h2 2\n{synonym}\n' })
;({data, status} = await createOrUpdateArticleApi(test, article))
assertStatus(status, data)
if (testNext) {
// Tests with the same result for logged in or off.
async function testNextLoggedInOrOff(loggedInUser) {
// Non-synonym sanity check.
;({data, status} = await test.sendJsonHttp('GET', routes.article('user0/h2')))
assertStatus(status, data)
// Article page synonym redirect
;({data, status} = await test.sendJsonHttp('GET', routes.article('user0/h2-2')))
assert.strictEqual(status, 308)
assert.strictEqual(data, routes.article('user0/h2'))
// Article source page synonym redirect
;({data, status} = await test.sendJsonHttp('GET', routes.articleSource('user0/h2-2')))
assert.strictEqual(status, 308)
assert.strictEqual(data, routes.articleSource('user0/h2'))
// Discussion page synonym redirect
;({data, status} = await test.sendJsonHttp('GET', routes.articleIssues('user0/h2-2')))
assert.strictEqual(status, 308)
assert.strictEqual(data, routes.articleIssues('user0/h2'))
}
// Logged in.
await testNextLoggedInOrOff(true)
// Logged out.
test.disableToken()
await testNextLoggedInOrOff(false)
test.loginUser(user)
}
}, { canTestNext: true })
})
it('api: article split synonym to descendant does not go into infinite loop', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
// user0
// h2 (h2 2)
article = createArticleArg({ i: 0, titleSource: 'h2', bodySource: '= h2 2\n{synonym}' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render: false }))
assertStatus(status, data)
// We need render=false here because we are temporarily duplicating the ID,
// h2-2, and duplicate checks run on render=true only.
//
// user0
// h2 2
// h2 (h2 2)
article = createArticleArg({ i: 0, titleSource: 'h2 2' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render: false }))
assertStatus(status, data)
// user0
// h2 2
// h2
article = createArticleArg({ i: 0, titleSource: 'h2' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render: false, parentId: '@user0/h2-2' }))
assertStatus(status, data)
// This is where it was going infinite, because when we set h2's parent to h2-2,
// at one point it was picking up that h2-2 is still a synonym of h2, which made
// h2 the parent of itself. Then when adding h3 below it, the h3 parent loop check
// went into an infinite loop.
//
// user0
// h2 2
// h2
// h3
article = createArticleArg({ i: 0, titleSource: 'h3' })
;({data, status} = await createOrUpdateArticleApi(test, article, { render: false, parentId: '@user0/h2' }))
assertStatus(status, data)
})
})
it(`api: /hash: cleanupIfDeleted is correct`, async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
// Empty non-hidden article needs to be cleaned.
article = createArticleArg({
i: 0,
titleSource: 'Mathematics',
bodySource: '',
})
;({data, status} = await createOrUpdateArticleApi(test, article))
assert.strictEqual(data.articles[0].list, true)
;({data, status} = await test.webApi.articlesHash({ author: 'user0' }))
assertStatus(status, data)
assertRows(data.articles, [
{ path: '@user0/index.bigb', cleanupIfDeleted: true, },
{ path: '@user0/mathematics.bigb', cleanupIfDeleted: true, },
])
// Empty hidden article does not need to be cleaned.
article = createArticleArg({
i: 0,
titleSource: 'Mathematics',
bodySource: '',
})
;({data, status} = await createOrUpdateArticleApi(test, article, { list: false, } ))
assertStatus(status, data)
assert.strictEqual(data.articles[0].list, false)
;({data, status} = await test.webApi.articlesHash({ author: 'user0' }))
assertStatus(status, data)
assertRows(data.articles, [
{ path: '@user0/index.bigb', cleanupIfDeleted: true, },
{ path: '@user0/mathematics.bigb', cleanupIfDeleted: false, },
])
// Non-empty hidden article needs to be cleaned.
article = createArticleArg({
i: 0,
titleSource: 'Mathematics',
bodySource: 'blabla',
})
;({data, status} = await createOrUpdateArticleApi(test, article ))
assertStatus(status, data)
assert.strictEqual(data.articles[0].list, false)
;({data, status} = await test.webApi.articlesHash({ author: 'user0' }))
assertStatus(status, data)
assertRows(data.articles, [
{ path: '@user0/index.bigb', cleanupIfDeleted: true, },
{ path: '@user0/mathematics.bigb', cleanupIfDeleted: true, },
])
// Render false does not blow up /hash with empty body
article = createArticleArg({
i: 0,
titleSource: 'Physics',
bodySource: '',
})
;({data, status} = await createOrUpdateArticleApi(test, article, { render: false }))
assertStatus(status, data)
;({data, status} = await test.webApi.articlesHash({ author: 'user0' }))
assertStatus(status, data)
assertRows(data.articles, [
{ path: '@user0/index.bigb', cleanupIfDeleted: true, },
{ path: '@user0/mathematics.bigb', cleanupIfDeleted: true, },
// We don't need to cleanup as it was never rendered.
{ path: '@user0/physics.bigb', cleanupIfDeleted: false, },
])
})
})
it(`api: admin can edit other user's articles`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
const user2 = await test.createUserApi(2)
await test.sequelize.models.User.update({ admin: true }, { where: { username: 'user2' } })
test.loginUser(user0)
// Create article as user0
article = createArticleArg({ i: 0 })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
assertRows(data.articles, [{ titleRender: 'Title 0' }])
// Sanity check.
;({data, status} = await test.webApi.article('user0/title-0'))
assertStatus(status, data)
assert.strictEqual(data.file.bodySource, 'Body 0.')
// owner that does not exist fails gracefully
test.loginUser(user1)
article = createArticleArg({ i: 0, bodySource: 'hacked' })
;({data, status} = await createOrUpdateArticleApi(test, article, { owner: 'idontexist', parentId: undefined }))
assert.strictEqual(status, 403)
// Non-admin cannot edit other users' articles
test.loginUser(user1)
article = createArticleArg({ i: 0, bodySource: 'hacked' })
;({data, status} = await createOrUpdateArticleApi(test, article, { owner: 'user0', parentId: undefined }))
assert.strictEqual(status, 403)
// Article unchanged.
;({data, status} = await test.webApi.article('user0/title-0'))
assertStatus(status, data)
assert.strictEqual(data.file.bodySource, 'Body 0.')
// Admin can edit other users' articles
test.loginUser(user2)
article = createArticleArg({ i: 0, bodySource: 'hacked' })
;({data, status} = await createOrUpdateArticleApi(test, article, { owner: 'user0', parentId: undefined }))
assertStatus(status, data)
test.loginUser(user0)
// Article changed.
;({data, status} = await test.webApi.article('user0/title-0'))
assertStatus(status, data)
assert.strictEqual(data.file.bodySource, 'hacked')
})
})
it(`api: user validation`, async () => {
await testApp(async (test) => {
let data, status, article
// New
// OK sanity check.
;({ data, status } = await test.webApi.userCreate({
username: 'john-smith',
displayName: 'John Smith',
email: 'john.smith@mail.com',
password: 'asdf',
}))
assertStatus(status, data)
const user0 = data.user
assert.strictEqual(data.user.username, 'john-smith')
assert.strictEqual(data.user.emailNotifications, true)
assert.strictEqual(data.user.emailNotificationsForArticleAnnouncement, true)
// Logged off get.
;({ data, status } = await test.webApi.user('john-smith'))
assert.strictEqual(data.username, 'john-smith')
assert.strictEqual(data.emailNotifications, undefined)
assert.strictEqual(data.emailNotificationsForArticleAnnouncement, undefined)
// Logged in get.
test.loginUser(user0)
;({ data, status } = await test.webApi.user('john-smith'))
assert.strictEqual(data.username, 'john-smith')
assert.strictEqual(data.emailNotifications, true)
assert.strictEqual(data.emailNotificationsForArticleAnnouncement, true)
test.loginUser()
// Username taken.
;({ data, status } = await test.webApi.userCreate({
username: 'john-smith',
displayName: 'Mary Jane',
email: 'mary.jane@mail.com',
password: 'asdf',
}))
assert.strictEqual(status, 422)
// Email taken.
;({ data, status } = await test.webApi.userCreate({
username: 'mary-jane',
displayName: 'Mary Jane',
email: 'john.smith@mail.com',
password: 'asdf',
}))
assert.strictEqual(status, 422)
// Missing display name.
;({ data, status } = await test.webApi.userCreate({
username: 'mary-jane',
email: 'mary.jane@mail.com',
password: 'asdf',
}))
assert.strictEqual(status, 422)
// Empty display name.
;({ data, status } = await test.webApi.userCreate({
username: 'mary-jane',
displayName: '',
email: 'mary.jane@mail.com',
password: 'asdf',
}))
assert.strictEqual(status, 422)
// Missing password.
;({ data, status } = await test.webApi.userCreate({
username: 'mary-jane',
displayName: 'Mary Jane',
email: 'mary.jane@mail.com',
}))
assert.strictEqual(status, 422)
// Missing username
;({ data, status } = await test.webApi.userCreate({
displayName: 'Mary Jane',
email: 'mary.jane@mail.com',
password: 'asdf',
}))
assert.strictEqual(status, 422)
// Empty username
;({ data, status } = await test.webApi.userCreate({
username: '',
displayName: 'Mary Jane',
email: 'mary.jane@mail.com',
password: 'asdf',
}))
assert.strictEqual(status, 422)
// Missing email
;({ data, status } = await test.webApi.userCreate({
username: 'mary-jane',
displayName: 'Mary Jane',
password: 'asdf',
}))
assert.strictEqual(status, 422)
// Empty email
;({ data, status } = await test.webApi.userCreate({
username: 'mary-jane',
displayName: 'Mary Jane',
email: '',
password: 'asdf',
}))
assert.strictEqual(status, 422)
// Edit
// OK sanity check.
test.loginUser(user0)
;({ data, status } = await test.webApi.userUpdate(
'john-smith',
{
displayName: 'John Smith 2',
emailNotifications: false,
emailNotificationsForArticleAnnouncement: false,
},
))
assertStatus(status, data)
assert.strictEqual(data.user.displayName, 'John Smith 2')
assert.strictEqual(data.user.emailNotifications, false)
assert.strictEqual(data.user.emailNotificationsForArticleAnnouncement, false)
;({ data, status } = await test.webApi.user('john-smith'))
assert.strictEqual(data.displayName, 'John Smith 2')
assert.strictEqual(data.emailNotifications, false)
assert.strictEqual(data.emailNotificationsForArticleAnnouncement, false)
// Empty display name.
test.loginUser(user0)
;({ data, status } = await test.webApi.userUpdate(
'john-smith',
{ displayName: '', },
))
assert.strictEqual(status, 422)
})
})
// Generated with:
// convert -size 1x1 xc:white empty.png
// od -t x1 -An empty.png | tr -d '\n '
const PNG_1X1_WHITE = '89504e470d0a1a0a0000000d4948445200000001000000010100000000376ef924000000206348524d00007a26000080840000fa00000080e8000075300000ea6000003a98000017709cba513c00000002624b47440001dd8a13a40000000774494d4507e80c10123327ede92e940000000a4944415408d76368000000820081dd436af40000000049454e44ae426082'
it(`api: profile picture`, async () => {
await testApp(async (test) => {
let data, status, article
const user0 = await test.createUserApi(0)
test.loginUser(user0)
const base64 = Buffer.from(PNG_1X1_WHITE, 'hex').toString('base64')
// Success.
;({ data, status } = await test.webApi.userUpdateProfilePicture(
'user0',
`data:image/png;base64,${base64}`,
))
assertStatus(status, data)
// Format not allowed.
;({ data, status } = await test.webApi.userUpdateProfilePicture(
'user0',
`data:image/asdf;base64,${base64}`,
))
assert.strictEqual(status, 422)
// Input too large. Adding a bunch of zeros at the end
// still produces a valid PNG I think.
;({ data, status } = await test.webApi.userUpdateProfilePicture(
'user0',
`data:image/png;base64,${Buffer.from(PNG_1X1_WHITE + '00'.repeat(2 * config.profilePictureMaxUploadSize), 'hex').toString('base64')}`,
))
assert.strictEqual(status, 422)
// Invalid image.
;({ data, status } = await test.webApi.userUpdateProfilePicture(
'user0',
`data:image/png;base64,${Buffer.from(PNG_1X1_WHITE.substring(Math.floor(PNG_1X1_WHITE.length / 2)), 'hex').toString('base64')}`,
))
assert.strictEqual(status, 422)
// User does not exist
;({ data, status } = await test.webApi.userUpdateProfilePicture(
'not-exists',
`data:image/png;base64,${base64}`,
))
assert.strictEqual(status, 404)
// Logged out
test.disableToken()
;({ data, status } = await test.webApi.userUpdateProfilePicture(
'user0',
`data:image/png;base64,${base64}`,
))
assert.strictEqual(status, 401)
})
})
it(`api: explicit id`, async () => {
// We made this work, but it is never used on ourbigbook upload and
// likely should be explicitly forbidden instead. The best general idea for
// now seems to be to leave anything that determines an article ID out of the
// article source itself on web, and have that only on local source. Similar applies
// to scope and disambiguate.
// Then on web, these are editable outside of the article source itself.
// https://github.com/ourbigbook/ourbigbook/issues/304
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
test.loginUser(user0)
// Create the article
article = createArticleArg({
i: 0,
titleSource: 'asdf',
bodySource: `{id=qwer}
`,
})
;({data, status} = await createOrUpdateArticleApi(test, article,))
assertStatus(status, data)
assert.strictEqual(data.articles[0].slug, 'user0/qwer')
})
})
it(`api: comment: that starts with title does not blow up`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
test.loginUser(user0)
// Create article user0/title-0
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 0 })))
assertStatus(status, data)
// Create issue user0/title-0#1
;({data, status} = await test.webApi.issueCreate('user0/title-0', createIssueArg(0, 0, 0)))
assertStatus(status, data)
// Create comment user0/title-0#1#1 that starts with header.
;({data, status} = await test.webApi.commentCreate('user0/title-0', 1, '= The header\n\n==The body\n'))
assertStatus(status, data)
})
})
it(`api: min`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
test.loginUser(user0)
;({data, status} = await test.webApi.min())
assertStatus(status, data)
assert.strictEqual(data.loggedIn, true)
})
})
// Closely related: https://github.com/ourbigbook/ourbigbook/issues/334
it(`api: link to home article`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
test.loginUser(user0)
// Index creation does not create an extra dummy ID.
assert.notStrictEqual(await test.sequelize.models.Id.findOne({ where: { idid: '@user0' } }), null)
assert.strictEqual(await test.sequelize.models.Id.findOne({ where: { idid: '@user0/' } }), null)
// Add alternate title to home article
;({data, status} = await createOrUpdateArticleApi(test, {
titleSource: 'My custom home',
bodySource: `{id=}
<>
<My custom home>
`,
},
{
// This example misses our heuristic parentId calculation in the tests.
parentId: undefined,
}
))
;({data, status} = await test.webApi.article('user0'))
// titleSource actually changed on DB.
assert.strictEqual(data.titleSource, 'My custom home')
assert_xpath(`//x:div[@class='p']//x:a[@href='/user0' and text()='My custom home']`, data.render)
assert_xpath(`//x:div[@class='p']//x:a[@href='/user0' and text()=' Home']`, data.render)
// Create article user0/title-0
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({
i: 0,
bodySource: `<>
<My custom home>
`,
})))
;({data, status} = await test.webApi.article('user0/title-0'))
assert_xpath(`//x:div[@class='p']//x:a[@href='/user0' and text()='My custom home']`, data.render)
assert_xpath(`//x:div[@class='p']//x:a[@href='/user0' and text()=' Home']`, data.render)
// Can also edit the index with path and without {id=}
test.loginUser(user1)
;({data, status} = await createOrUpdateArticleApi(test, {
titleSource: 'My custom home 2',
bodySource: `<>
<My custom home 2>
`,
},
{
parentId: undefined,
path: 'index',
}
))
;({data, status} = await test.webApi.article('user1'))
// titleSource actually changed on DB.
assert.strictEqual(data.titleSource, 'My custom home 2')
assert_xpath(`//x:div[@class='p']//x:a[@href='/user1' and text()='My custom home 2']`, data.render)
assert_xpath(`//x:div[@class='p']//x:a[@href='/user1' and text()=' Home']`, data.render)
}, { defaultExpectStatus: 200 })
})
// Closely related: https://github.com/ourbigbook/ourbigbook/issues/364
it(`api: link to self`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
test.loginUser(user0)
;({data, status} = await createOrUpdateArticleApi(test, {
titleSource: 'Test data 0',
bodySource: '',
}))
;({data, status} = await createOrUpdateArticleApi(test, {
titleSource: 'Test data',
bodySource: `<test-data>
<test data>{id=dut}
<test data 0>{id=sanity}
`,
}))
;({data, status} = await test.webApi.article('user0/test-data'))
assert_xpath(`//x:a[@id='user0/dut' and @href='/user0/test-data']`, data.render)
assert_xpath(`//x:a[@id='user0/sanity' and @href='/user0/test-data-0']`, data.render)
}, { defaultExpectStatus: 200 })
})
it(`api: article path argument`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
test.loginUser(user0)
// path takes precedence over title and id=
;({data, status} = await createOrUpdateArticleApi(test, {
titleSource: 'fromtitle',
bodySource: `{id=fromid}\n`
},
{ path: 'frompath' }
))
;({data, status} = await test.webApi.article('user0/frompath'))
assert.notStrictEqual(data, undefined)
;({data, status} = await test.webApi.article('user0/fromid'))
assert.strictEqual(data, undefined)
;({data, status} = await test.webApi.article('user0/fromtitle'))
assert.strictEqual(data, undefined)
// Without path, id= wins
;({data, status} = await createOrUpdateArticleApi(test, {
titleSource: 'fromtitle',
bodySource: `{id=fromid}\n`
},
{ path: undefined }
))
;({data, status} = await test.webApi.article('user0/fromid'))
assert.notStrictEqual(data, undefined)
;({data, status} = await test.webApi.article('user0/fromtitle'))
assert.strictEqual(data, undefined)
// Without path and id, title wins
;({data, status} = await createOrUpdateArticleApi(test, {
titleSource: 'fromtitle',
bodySource: ``
},
{ path: undefined }
))
;({data, status} = await test.webApi.article('user0/fromtitle'))
assert.notStrictEqual(data, undefined)
// Path cannot be empty
;({data, status} = await createOrUpdateArticleApi(test,
{ titleSource: 'given' },
{ path: '' },
{ expectStatus: 422 },
))
// path equals possibly special value of "index".
// TODO we need to think about this. Arguably this should force empty id=''
// as being the home article.
// Closely related is: https://github.com/ourbigbook/ourbigbook/issues/334
// perhaps is we made this be recognized that would be immediately solved.
;({data, status} = await createOrUpdateArticleApi(test, {
titleSource: 'My custom home',
bodySource: `hacked`
},
{
path: 'index',
parentId: undefined,
}
))
;({data, status} = await test.webApi.article('user0'))
assert.strictEqual(data.titleSource, 'My custom home')
;({data, status} = await test.webApi.article('user0/my-custom-home'))
assert.strictEqual(data, undefined)
}, { defaultExpectStatus: 200 })
})
it(`api: article: with named argument`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
test.loginUser(user0)
// Create article user0/title-0
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({
i: 0,
bodySource: `{title2=Asdf qwer}`
})))
assertStatus(status, data)
})
})
it(`api: article: create with disambiguate`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
test.loginUser(user0)
// Create article user0/title-0
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 0, bodySource: '{disambiguate=that type}' })))
assertStatus(status, data)
// Check that the article is there
;({data, status} = await test.webApi.article('user0/title-0-that-type'))
assertStatus(status, data)
assert.strictEqual(data.titleRender, 'Title 0 (that type)')
assert.strictEqual(data.titleSource, 'Title 0')
})
})
it(`api: article: announce`, async () => {
await testApp(async (test) => {
let data, status
// Create users
const user0 = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
// user1 follows user0
test.loginUser(user1)
;({data, status} = await test.webApi.userFollow('user0'))
test.loginUser(user0)
// Create article user0/title-0
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 0 })))
assertStatus(status, data)
// Announcement date is empty before announce.
;({data, status} = await test.webApi.article('user0/title-0'))
assertStatus(status, data)
assert.strictEqual(data.announcedAt, undefined)
// Can't announce with message that is too long.
;({data, status} = await test.webApi.articleAnnounce(
'user0/title-0', 'a'.repeat(config.maxArticleAnnounceMessageLength + 1)))
assert.strictEqual(status, 422)
// Can't announce other person's article.
test.loginUser(user1)
;({data, status} = await test.webApi.articleAnnounce('user0/title-0', 'My message.'))
assert.strictEqual(status, 403)
test.loginUser(user0)
// Can't announce article that does not exist
;({data, status} = await test.webApi.articleAnnounce('user0/not-exists', 'My message.'))
assert.strictEqual(status, 404)
// Can announce the first time.
;({data, status} = await test.webApi.articleAnnounce('user0/title-0', 'My message.'))
assertStatus(status, data)
// Followers receive an email notification.
const emails = test.app.get('emails')
const lastEmail = emails[emails.length - 1]
assert.strictEqual(lastEmail.to, 'user1@mail.com')
assert.strictEqual(lastEmail.subject, 'Announcement: Title 0')
// Announcement date is not empty anymore.
;({data, status} = await test.webApi.article('user0/title-0'))
assertStatus(status, data)
assert.notStrictEqual(data.announcedAt, undefined)
// Can't announce the second time.
;({data, status} = await test.webApi.articleAnnounce('user0/title-0', 'My message.'))
assert.strictEqual(status, 422)
// Followers don't receive an email notification if their notification setting is off.
test.loginUser(user1)
;({ data, status } = await test.webApi.userUpdate(
'user1',
{ emailNotificationsForArticleAnnouncement: false, },
))
assertStatus(status, data)
test.loginUser(user0)
assert.strictEqual(lastEmail.to, 'user1@mail.com')
assert.strictEqual(lastEmail.subject, 'Announcement: Title 0')
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 1 })))
assertStatus(status, data)
;({data, status} = await test.webApi.articleAnnounce(`user0/title-1`, 'My message.'))
assertStatus(status, data)
assert.strictEqual(lastEmail.to, 'user1@mail.com')
assert.strictEqual(lastEmail.subject, 'Announcement: Title 0')
// The announcement limit prevents new announcements if hit.
for (let i = 2; i < config.maxArticleAnnouncesPerMonth; i++) {
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i })))
assertStatus(status, data)
;({data, status} = await test.webApi.articleAnnounce(`user0/title-${i}`, 'My message.'))
assertStatus(status, data)
}
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: config.maxArticleAnnouncesPerMonth })))
assertStatus(status, data)
;({data, status} = await test.webApi.articleAnnounce(`user0/title-${config.maxArticleAnnouncesPerMonth}`, 'My message.'))
assert.strictEqual(status, 422)
})
})
it(`api: article: search`, async () => {
await testApp(async (test) => {
let data, status, article
const user0 = await test.createUserApi(0)
test.loginUser(user0)
// Create 10 articles
for (let i = 0; i < 2; i++) {
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i, titleSource: `Prefix anywhere suffix ${i}` })))
assertStatus(status, data)
}
for (let i = 0; i < 2; i++) {
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i, titleSource: `Anywhere middle suffix ${i}` })))
assertStatus(status, data)
}
;({data, status} = await createOrUpdateArticleApi(test,
createArticleArg({ i: 0, titleSource: `Oneword` })))
assertStatus(status, data)
for (const [apiGet, items, field, pref] of [
[test.webApi.articles.bind(test.webApi), 'articles', 'slug', 'user0/'],
[test.webApi.topics.bind(test.webApi), 'topics', 'topicId', ''],
]) {
;({data, status} = await apiGet({ search: 'pref' }))
assertStatus(status, data)
assertRows(data[items], [
{ [field]: `${pref}prefix-anywhere-suffix-0` },
{ [field]: `${pref}prefix-anywhere-suffix-1` },
])
// Spaces are converted to hyphen
;({data, status} = await apiGet({ search: 'prefix anywh' }))
assertStatus(status, data)
assertRows(data[items], [
{ [field]: `${pref}prefix-anywhere-suffix-0` },
{ [field]: `${pref}prefix-anywhere-suffix-1` },
])
if (test.sequelize.options.dialect === 'postgres') {
// Check that:
// - prefix search is working
// - full prefix hits come first
;({data, status} = await apiGet({ search: 'anyw' }))
assertStatus(status, data)
assertRows(data[items], [
{ [field]: `${pref}anywhere-middle-suffix-0` },
{ [field]: `${pref}anywhere-middle-suffix-1` },
{ [field]: `${pref}prefix-anywhere-suffix-0` },
{ [field]: `${pref}prefix-anywhere-suffix-1` },
])
// Limit is respected when joining up FTS and non FTS. Prefix still gets preferred.
;({data, status} = await apiGet({ limit: 3, search: 'anyw' }))
assertStatus(status, data)
assertRows(data[items], [
{ [field]: `${pref}anywhere-middle-suffix-0` },
{ [field]: `${pref}anywhere-middle-suffix-1` },
{ [field]: `${pref}prefix-anywhere-suffix-0` },
])
;({data, status} = await apiGet({ search: 'middle anyw' }))
assertStatus(status, data)
assertRows(data[items], [
{ [field]: `${pref}anywhere-middle-suffix-0` },
{ [field]: `${pref}anywhere-middle-suffix-1` },
])
}
// Single word does ID not get repeated twice
;({data, status} = await apiGet({ search: 'oneword' }))
assertStatus(status, data)
assertRows(data[items], [
{ [field]: `${pref}oneword` },
])
// Single word does ID not get repeated twice with trailing space on search
;({data, status} = await apiGet({ search: 'oneword ' }))
assertStatus(status, data)
assertRows(data[items], [
{ [field]: `${pref}oneword` },
])
}
})
})
it('api: article: parent and parent-type', async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const user = await test.createUserApi(0)
test.loginUser(user)
// Create articles
article = createArticleArg({ i: 0, titleSource: 'Good' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Mathematics', bodySource: '{tag=Good}' })
;({data, status} = await createArticleApi(test, article))
assertStatus(status, data)
article = createArticleArg({ i: 0, titleSource: 'Calculus' })
;({data, status} = await createArticleApi(test, article, { parentId: '@user0/mathematics' }))
assertStatus(status, data)
// Test parent and parent-type
// Calculus is a direct child of mathematics
;({data, status} = await test.webApi.articles({ parent: '@user0/mathematics' }))
assertStatus(status, data)
assertRows(data.articles, [
{ slug: 'user0/calculus' },
])
// Mathematics is tagged as good
;({data, status} = await test.webApi.articles({
parent: '@user0/good',
'parent-type': ourbigbook.REFS_TABLE_X_CHILD }
))
assertStatus(status, data)
assertRows(data.articles, [
{ slug: 'user0/mathematics' },
])
// Unknown parent-type blows up gracefully.
;({data, status} = await test.webApi.articles({
parent: '@user0/good',
'parent-type': 'i-dont-exist' }
))
assert.strictEqual(status, 422)
})
})
it(`api: article: automatic topic linking`, async () => {
// https://github.com/ourbigbook/ourbigbook/issues/356
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
await test.sequelize.models.User.update({ admin: true }, { where: { username: 'user0' } })
test.loginUser(user0)
// Fix the max just in case the default chances one day.
;({ data, status } = await test.webApi.siteSettingsUpdate({
automaticTopicLinksMaxWords: 3
}))
// Create some pre-existing articles.
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ titleSource: 'aa1', i: 0 })))
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ titleSource: 'aa2 bb2', i: 0 })))
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ titleSource: 'aa3 bb3 cc3', i: 0 })))
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ titleSource: 'dog', i: 0 })))
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ titleSource: 'common1 common2 common3 common4 common5', i: 0 })))
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ titleSource: 'common1 common2 common3 common4', i: 0 })))
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ titleSource: 'common1 common2 common3', i: 0 })))
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ titleSource: 'common1 common2', i: 0 })))
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ titleSource: 'common1', i: 0 })))
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ titleSource: 'i', i: 0 })))
assertStatus(status, data)
// Create the final article title0
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({
bodySource: `> XXX aa1 YYY
{id=1}
> XXX aa2 bb2 YYY
{id=2}
> XXX aa3 bb3 cc3 YYY
{id=3}
> XXX aa2 bb2 YYY
{id=two-spaces}
> XXX aa2.bb2 YYY
{id=punct}
> XXX aa2. bb2 YYY
{id=punct-space}
> XXX aa2 \\i[bb2] YYY
{id=inline}
> XXX common1 YYY
{id=test-common-1}
> XXX common1 common2 YYY
{id=test-common-1-2}
> XXX common1 common2 common3 YYY
{id=test-common-1-3}
> XXX common1 common2 common3 common4 YYY
{id=test-common-1-4}
> XXX common1 common2 common3 common4 common5 YYY
{id=test-common-1-5}
> XXX common2 YYY
{id=test-common-2}
> XXX common2 common3 YYY
{id=test-common-2-3}
> XXX common2 common3 common4 YYY
{id=test-common-2-4}
> XXX common2 common3 common5 YYY
{id=test-common-2-5}
> XXX common3 YYY
{id=test-common-3}
> XXX common3 common4 YYY
{id=test-common-3-4}
> XXX common3 common4 common5 YYY
{id=test-common-3-5}
> XXX http://example.com[aa1] YYY
{id=inlink}
> XXX <inxto>[aa2 bb2] YYY
{id=inx}
> aa1
{id=inxto}
> \\c[aa1]
{id=incode}
> \\m[aa1]
{id=inmath}
|| aa1
| 11
{id=in-table-header}
> http://aa1.com
{id=in-a}
> \\b[http://aa1.com]
{id=in-a-in-b}
> I
{id=single-letter-word}
> He and I are nice.
{id=single-letter-word-in-sentence}
> me
{id=blacklisted-word}
> dog
{id=singular}
> dogs
{id=plural}
`,
i: 0,
})))
;({data, status} = await test.webApi.article('user0/title-0'))
assert_xpath(`//x:div[@id='user0/1']//x:blockquote//x:a[@href='/go/topic/aa1' and text()='aa1']`, data.render)
assert_xpath(`//x:div[@id='user0/2']//x:blockquote//x:a[@href='/go/topic/aa2-bb2' and text()='aa2 bb2']`, data.render)
assert_xpath(`//x:div[@id='user0/3']//x:blockquote//x:a[@href='/go/topic/aa3-bb3-cc3' and text()='aa3 bb3 cc3']`, data.render)
assert_xpath(`//x:div[@id='user0/two-spaces']//x:blockquote//x:a[@href='/go/topic/aa2-bb2' and text()='aa2 bb2']`, data.render)
assert_xpath(`//x:div[@id='user0/punct']//x:blockquote[text()='XXX aa2.bb2 YYY']`, data.render)
assert_xpath(`//x:div[@id='user0/punct-space']//x:blockquote[text()='XXX aa2. bb2 YYY']`, data.render)
// This could be potentially changed one day. But for now it's hard and rare so leave it.
assert_xpath(`//x:div[@id='user0/inline']//x:blockquote//x:a[@href='/go/topic/aa2-bb2']`, data.render, { count: 0 })
assert_xpath(`//x:div[@id='user0/test-common-1']//x:blockquote//x:a[@href='/go/topic/common1' and text()='common1']`, data.render)
assert_xpath(`//x:div[@id='user0/test-common-1-2']//x:blockquote//x:a[@href='/go/topic/common1-common2' and text()='common1 common2']`, data.render)
assert_xpath(`//x:div[@id='user0/test-common-1-3']//x:blockquote//x:a[@href='/go/topic/common1-common2-common3' and text()='common1 common2 common3']`, data.render)
assert_xpath(`//x:div[@id='user0/inlink']//x:blockquote//x:a[@href='/go/topic/aa1']`, data.render, { count: 0 })
assert_xpath(`//x:div[@id='user0/inx']//x:blockquote//x:a[@href='/go/topic/aa1']`, data.render, { count: 0 })
assert_xpath(`//x:div[@id='user0/incode']//x:blockquote//x:a[@href='/go/topic/aa1']`, data.render, { count: 0 })
assert_xpath(`//x:div[@id='user0/inmath']//x:blockquote//x:a[@href='/go/topic/aa1']`, data.render, { count: 0 })
assert_xpath(`//x:div[@id='user0/in-table-header']//x:blockquote//x:a[@href='/go/topic/aa1']`, data.render, { count: 0 })
assert_xpath(`//x:div[@id='user0/in-a']//x:blockquote//x:a[@href='http://aa1.com' and text()='aa1.com']`, data.render)
assert_xpath(`//x:div[@id='user0/in-a-in-b']//x:blockquote//x:b//x:a[@href='http://aa1.com' and text()='aa1.com']`, data.render)
// These can be debated. We had removed them earlier, but decided to restore when we made the links invisible.
assert_xpath(`//x:div[@id='user0/single-letter-word']//x:blockquote//x:a[@href='/go/topic/i' and text()='I']`, data.render)
assert_xpath(`//x:div[@id='user0/single-letter-word-in-sentence']//x:blockquote//x:a[@href='/go/topic/i' and text()='I']`, data.render)
assert_xpath(`//x:div[@id='user0/blacklisted-word']//x:blockquote//x:a[@href='/go/topic/me' and text()='Me']`, data.render, { count: 0 })
assert_xpath(`//x:div[@id='user0/singular']//x:blockquote//x:a[@href='/go/topic/dog' and text()='dog']`, data.render)
assert_xpath(`//x:div[@id='user0/plural']//x:blockquote//x:a[@href='/go/topic/dog' and text()='dogs']`, data.render)
}, { defaultExpectStatus: 200 })
})
it(`api: site settings`, async () => {
await testApp(
async (test) => {
let data, status, article
const webApi = test.webApi
// Create users
const user0 = await test.createUserApi(0)
await test.sequelize.models.User.update({ admin: true }, { where: { username: 'user0' } })
const user1 = await test.createUserApi(1)
test.loginUser(user0)
// Create article user0/title-0
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 0 })))
// Create article user0/title-1
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 1 })))
// Update automaticTopicLinksMaxWords
;({ data, status } = await webApi.siteSettingsUpdate({
automaticTopicLinksMaxWords: 1
}))
;({ data, status } = await webApi.siteSettingsGet())
assert.strictEqual(data.automaticTopicLinksMaxWords, 1)
;({ data, status } = await webApi.siteSettingsUpdate({
automaticTopicLinksMaxWords: 2
}))
;({ data, status } = await webApi.siteSettingsGet())
assert.strictEqual(data.automaticTopicLinksMaxWords, 2)
// Zero is fine.
;({ data, status } = await webApi.siteSettingsUpdate({
automaticTopicLinksMaxWords: 0
}))
;({ data, status } = await webApi.siteSettingsGet())
assert.strictEqual(data.automaticTopicLinksMaxWords, 0)
// Negative is not fine.
;({ data, status } = await webApi.siteSettingsUpdate(
{ automaticTopicLinksMaxWords: -1 },
{ expectStatus: 422 },
))
;({ data, status } = await webApi.siteSettingsGet())
assert.strictEqual(data.automaticTopicLinksMaxWords, 0)
// Has to be integer, string is not fine.
;({ data, status } = await webApi.siteSettingsUpdate(
{ automaticTopicLinksMaxWords: '1' },
{ expectStatus: 422 },
))
;({ data, status } = await webApi.siteSettingsGet())
assert.strictEqual(data.automaticTopicLinksMaxWords, 0)
// Non-admin cannot edit site settings
test.loginUser(user1)
;({ data, status } = await webApi.siteSettingsUpdate(
{ automaticTopicLinksMaxWords: 1 },
{ expectStatus: 403 },
))
test.loginUser(user0)
;({ data, status } = await webApi.siteSettingsGet())
assert.strictEqual(data.automaticTopicLinksMaxWords, 0)
// Non-admin can read site settings
test.loginUser(user1)
;({ data, status } = await webApi.siteSettingsGet())
assert.strictEqual(data.automaticTopicLinksMaxWords, 0)
test.loginUser(user0)
// Update pinned article.
;({ data, status } = await webApi.siteSettingsUpdate({
pinnedArticle: 'user0/title-0'
}))
;({ data, status } = await webApi.siteSettingsGet())
assert.strictEqual(data.pinnedArticle, 'user0/title-0')
;({ data, status } = await webApi.siteSettingsUpdate({
pinnedArticle: 'user0/title-1'
}))
;({ data, status } = await webApi.siteSettingsGet())
assert.strictEqual(data.pinnedArticle, 'user0/title-1')
// Article that does not exit fails gracefully.
;({ data, status } = await webApi.siteSettingsUpdate(
{ pinnedArticle: 'user0/title-2' },
{ expectStatus: 404 },
))
;({ data, status } = await webApi.siteSettingsGet())
assert.strictEqual(data.pinnedArticle, 'user0/title-1')
},
{ defaultExpectStatus: 200 }
)
})
it(`api: requests`, async () => {
await testApp(async (test) => {
let data, status, article
const sequelize = test.sequelize
const { ReferrerDomainBlacklist, Request } = sequelize.models
config.trackRequests = true
// Requests without referrer are not tracked.
config.devIp = '123.123.123.1'
const user0 = await test.createUserApi(0)
assertRows(
await Request.findAll({ order: [['createdAt', 'ASC']] }),
[]
)
// Requests with referrer are tracked.
config.devIp = '123.123.123.1'
;({data, status} = await test.webApi.article('user0', {}, { headers: { referrer: 'http://example.com' } }))
assertRows(
await Request.findAll({ order: [['createdAt', 'ASC']] }),
[
{ ip: '123.123.123.1', path: '/api/articles?id=user0', referrer: 'http://example.com' },
]
)
// Again to repeat
config.devIp = '123.123.123.1'
;({data, status} = await test.webApi.article('user0', {}, { headers: { referrer: 'http://example.com' } }))
assertRows(
await Request.findAll({ order: [['createdAt', 'ASC']] }),
[
{ ip: '123.123.123.1', path: '/api/articles?id=user0', referrer: 'http://example.com' },
{ ip: '123.123.123.1', path: '/api/articles?id=user0', referrer: 'http://example.com' },
]
)
// Another IP
config.devIp = '123.123.123.2'
;({data, status} = await test.webApi.article('user0', {}, { headers: { referrer: 'http://example.com' } }))
assertRows(
await Request.findAll({ order: [['createdAt', 'ASC']] }),
[
{ ip: '123.123.123.1', path: '/api/articles?id=user0', referrer: 'http://example.com' },
{ ip: '123.123.123.1', path: '/api/articles?id=user0', referrer: 'http://example.com' },
{ ip: '123.123.123.2', path: '/api/articles?id=user0', referrer: 'http://example.com' },
]
)
// Add to blacklist and check it does not get added anymore
await ReferrerDomainBlacklist.create({ domain: 'example.com' })
config.devIp = '123.123.123.1'
;({data, status} = await test.webApi.article('user0', {}, { headers: { referrer: 'http://example.com' } }))
assertRows(
await Request.findAll({ order: [['createdAt', 'ASC']] }),
[
{ ip: '123.123.123.1', path: '/api/articles?id=user0', referrer: 'http://example.com' },
{ ip: '123.123.123.1', path: '/api/articles?id=user0', referrer: 'http://example.com' },
{ ip: '123.123.123.2', path: '/api/articles?id=user0', referrer: 'http://example.com' },
]
)
}, { defaultExpectStatus: 200 })
})
it(`api: article: bulk update`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
await test.sequelize.models.User.update({ admin: true }, { where: { username: 'user0' } })
// Create article user1/title-0
test.loginUser(user1)
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 0 })))
;({data, status} = await test.webApi.articles())
assertRows(data.articles, [
{ slug: 'user1/title-0', list: true },
{ slug: 'user1', list: true },
{ slug: 'user0', list: true },
])
// Non-admin users cannot do bulk update.
;({ data, status } = await test.webApi.articlesBulkUpdate(
{ username: 'user1' },
{ list: false },
{ expectStatus: 403 },
))
;({data, status} = await test.webApi.articles())
assertRows(data.articles, [
{ slug: 'user1/title-0', list: true },
{ slug: 'user1', list: true },
{ slug: 'user0', list: true },
])
// Admin can do bulk update
test.loginUser(user0)
;({ data, status } = await test.webApi.articlesBulkUpdate(
{ username: 'user1' },
{ list: false },
))
assert.strictEqual(data.count, 2)
;({data, status} = await test.webApi.articles())
assertRows(data.articles, [
{ slug: 'user1/title-0', list: false },
{ slug: 'user1', list: false },
{ slug: 'user0', list: true },
])
// Admin can do bulk update without username
test.loginUser(user0)
;({ data, status } = await test.webApi.articlesBulkUpdate(
{ },
{ list: false },
))
assert.strictEqual(data.count, 3)
;({data, status} = await test.webApi.articles())
assertRows(data.articles, [
{ slug: 'user1/title-0', list: false },
{ slug: 'user1', list: false },
{ slug: 'user0', list: false },
])
}, { defaultExpectStatus: 200 })
})
it(`api: article with {file}`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
test.loginUser(user0)
// Create article user0/_file/subdir/myfile.txt
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({
titleSource: `subdir/myfile.txt`,
bodySource: `{file}`
})))
// Check that the article is there
;({data, status} = await test.webApi.article('user0/_file/subdir/myfile.txt'))
assert.strictEqual(data.titleRender, 'subdir/myfile.txt')
}, { defaultExpectStatus: 200 })
})
it(`api: article {synonymNoScope}`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
const user1 = await test.createUserApi(1)
test.loginUser(user0)
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({
titleSource: `With scope`,
bodySource: `{scope}
= With synonymNoScope
{synonymNoScope}
`
})))
;({data, status} = await test.webApi.articleRedirects({ id: 'user0/with-synonymnoscope' }))
assert.strictEqual(data.redirects['user0/with-synonymnoscope'], 'user0/with-scope')
// Previously the @username/ scope was being removed and
// this would blow up with duplicate id "with-synonymNoScope".
test.loginUser(user1)
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({
titleSource: `With scope`,
bodySource: `{scope}
= With synonymNoScope
{synonymNoScope}
`
})))
;({data, status} = await test.webApi.articleRedirects({ id: 'user1/with-synonymnoscope' }))
assert.strictEqual(data.redirects['user1/with-synonymnoscope'], 'user1/with-scope')
}, { defaultExpectStatus: 200 })
})
it(`api: article: create simple`, async () => {
await testApp(async (test) => {
let data, status, article
// Create users
const user0 = await test.createUserApi(0)
test.loginUser(user0)
// Create article user0/title-0
;({data, status} = await createOrUpdateArticleApi(test, createArticleArg({ i: 0 })))
// Check that the article is there
;({data, status} = await test.webApi.article('user0/title-0'))
assert.strictEqual(data.titleSource, 'Title 0')
assert.match(data.render, /Body 0\./)
}, { defaultExpectStatus: 200 })
})