first commit

This commit is contained in:
2025-04-24 13:11:28 +08:00
commit ff9c54d5e4
5960 changed files with 834111 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
import { ObjectId } from '../../../app/js/mongodb.js'
import { expect } from 'chai'
import * as ChatClient from './helpers/ChatClient.js'
import * as ChatApp from './helpers/ChatApp.js'
const user1Id = new ObjectId().toString()
const user2Id = new ObjectId().toString()
async function createCommentThread(projectId, threadId = new ObjectId()) {
const { response: response1 } = await ChatClient.sendMessage(
projectId,
threadId.toString(),
user1Id,
'message 1'
)
expect(response1.statusCode).to.equal(201)
const { response: response2 } = await ChatClient.sendMessage(
projectId,
threadId,
user2Id,
'message 2'
)
expect(response2.statusCode).to.equal(201)
return threadId.toString()
}
describe('Cloning comment threads', async function () {
const projectId = new ObjectId().toString()
before(async function () {
await ChatApp.ensureRunning()
this.thread1Id = await createCommentThread(projectId)
this.thread2Id = await createCommentThread(projectId)
this.thread3Id = await createCommentThread(projectId)
})
describe('with non-orphaned threads', async function () {
before(async function () {
const {
response: { body: result, statusCode },
} = await ChatClient.duplicateCommentThreads(projectId, [this.thread3Id])
this.result = result
expect(statusCode).to.equal(200)
expect(this.result).to.have.property('newThreads')
this.newThreadId = this.result.newThreads[this.thread3Id].duplicateId
})
it('should duplicate threads', function () {
expect(this.result.newThreads).to.have.property(this.thread3Id)
expect(this.result.newThreads[this.thread3Id]).to.have.property(
'duplicateId'
)
expect(this.result.newThreads[this.thread3Id].duplicateId).to.not.equal(
this.thread3Id
)
})
it('should not duplicate other threads threads', function () {
expect(this.result.newThreads).to.not.have.property(this.thread1Id)
expect(this.result.newThreads).to.not.have.property(this.thread2Id)
})
it('should duplicate the messages in the thread', async function () {
const {
response: { body: threads },
} = await ChatClient.getThreads(projectId)
function ignoreId(comment) {
return {
...comment,
id: undefined,
}
}
expect(threads[this.thread3Id].messages.map(ignoreId)).to.deep.equal(
threads[this.newThreadId].messages.map(ignoreId)
)
})
it('should have two separate unlinked threads', async function () {
await ChatClient.sendMessage(
projectId,
this.newThreadId,
user1Id,
'third message'
)
const {
response: { body: threads },
} = await ChatClient.getThreads(projectId)
expect(threads[this.thread3Id].messages.length).to.equal(2)
expect(threads[this.newThreadId].messages.length).to.equal(3)
})
})
})

View File

@@ -0,0 +1,47 @@
import { ObjectId } from '../../../app/js/mongodb.js'
import { expect } from 'chai'
import * as ChatClient from './helpers/ChatClient.js'
import * as ChatApp from './helpers/ChatApp.js'
describe('Deleting a message', async function () {
const projectId = new ObjectId().toString()
const userId = new ObjectId().toString()
const threadId = new ObjectId().toString()
before(async function () {
await ChatApp.ensureRunning()
})
describe('in a thread', async function () {
before(async function () {
const { response } = await ChatClient.sendMessage(
projectId,
threadId,
userId,
'first message'
)
expect(response.statusCode).to.equal(201)
const { response: response2, body: message } =
await ChatClient.sendMessage(
projectId,
threadId,
userId,
'deleted message'
)
expect(response2.statusCode).to.equal(201)
const { response: response3 } = await ChatClient.deleteMessage(
projectId,
threadId,
message.id
)
expect(response3.statusCode).to.equal(204)
})
it('should then remove the message from the threads', async function () {
const { response, body: threads } = await ChatClient.getThreads(projectId)
expect(response.statusCode).to.equal(200)
expect(threads[threadId].messages.length).to.equal(1)
})
})
})

View File

@@ -0,0 +1,38 @@
import { ObjectId } from '../../../app/js/mongodb.js'
import { expect } from 'chai'
import * as ChatClient from './helpers/ChatClient.js'
import * as ChatApp from './helpers/ChatApp.js'
describe('Deleting a thread', async function () {
const projectId = new ObjectId().toString()
const userId = new ObjectId().toString()
before(async function () {
await ChatApp.ensureRunning()
})
describe('with a thread that is deleted', async function () {
const threadId = new ObjectId().toString()
const content = 'deleted thread message'
before(async function () {
const { response } = await ChatClient.sendMessage(
projectId,
threadId,
userId,
content
)
expect(response.statusCode).to.equal(201)
const { response: response2 } = await ChatClient.deleteThread(
projectId,
threadId
)
expect(response2.statusCode).to.equal(204)
})
it('should then not list the thread for the project', async function () {
const { response, body: threads } = await ChatClient.getThreads(projectId)
expect(response.statusCode).to.equal(200)
expect(Object.keys(threads).length).to.equal(0)
})
})
})

View File

@@ -0,0 +1,66 @@
import { ObjectId } from '../../../app/js/mongodb.js'
import { expect } from 'chai'
import * as ChatClient from './helpers/ChatClient.js'
import * as ChatApp from './helpers/ChatApp.js'
const db = ChatApp.db
async function getMessage(messageId) {
return await db.messages.findOne({
_id: new ObjectId(messageId),
})
}
describe('Destroying a project', async function () {
const projectId = new ObjectId().toString()
const userId = new ObjectId().toString()
before(async function () {
await ChatApp.ensureRunning()
})
describe('with a project that has threads and messages', async function () {
const threadId = new ObjectId().toString()
before(async function () {
const { response } = await ChatClient.sendMessage(
projectId,
threadId,
userId,
'destroyed thread message'
)
expect(response.statusCode).to.equal(201)
this.threadMessageId = response.body.id
const { response: response2 } = await ChatClient.sendGlobalMessage(
projectId,
userId,
'destroyed global message'
)
expect(response2.statusCode).to.equal(201)
this.globalThreadMessageId = response2.body.id
const threadRooms = await db.rooms
.find({ project_id: new ObjectId(projectId) })
.toArray()
expect(threadRooms.length).to.equal(2)
const threadMessage = await getMessage(this.threadMessageId)
expect(threadMessage).to.exist
const globalThreadMessage = await getMessage(this.globalThreadMessageId)
expect(globalThreadMessage).to.exist
const { response: responseDestroy } =
await ChatClient.destroyProject(projectId)
expect(responseDestroy.statusCode).to.equal(204)
})
it('should remove the messages and threads from the database', async function () {
const threadRooms = await db.rooms
.find({ project_id: new ObjectId(projectId) })
.toArray()
expect(threadRooms.length).to.equal(0)
const threadMessage = await getMessage(this.threadMessageId)
expect(threadMessage).to.be.null
const globalThreadMessage = await getMessage(this.globalThreadMessageId)
expect(globalThreadMessage).to.be.null
})
})
})

View File

@@ -0,0 +1,96 @@
import { ObjectId } from '../../../app/js/mongodb.js'
import { expect } from 'chai'
import * as ChatClient from './helpers/ChatClient.js'
import * as ChatApp from './helpers/ChatApp.js'
describe('Editing a message', async function () {
let projectId, userId, threadId
before(async function () {
await ChatApp.ensureRunning()
})
describe('in a thread', async function () {
const content = 'thread message'
const newContent = 'updated thread message'
let messageId
beforeEach(async function () {
projectId = new ObjectId().toString()
userId = new ObjectId().toString()
threadId = new ObjectId().toString()
const { response, body: message } = await ChatClient.sendMessage(
projectId,
threadId,
userId,
content
)
expect(response.statusCode).to.equal(201)
expect(message.id).to.exist
expect(message.content).to.equal(content)
messageId = message.id
})
describe('without user', function () {
beforeEach(async function () {
const { response } = await ChatClient.editMessage(
projectId,
threadId,
messageId,
newContent
)
expect(response.statusCode).to.equal(204)
})
it('should then list the updated message in the threads', async function () {
const { response, body: threads } =
await ChatClient.getThreads(projectId)
expect(response.statusCode).to.equal(200)
expect(threads[threadId].messages.length).to.equal(1)
expect(threads[threadId].messages[0].content).to.equal(newContent)
})
})
describe('with the same user', function () {
beforeEach(async function () {
const { response } = await ChatClient.editMessageWithUser(
projectId,
threadId,
messageId,
userId,
newContent
)
expect(response.statusCode).to.equal(204)
})
it('should then list the updated message in the threads', async function () {
const { response, body: threads } =
await ChatClient.getThreads(projectId)
expect(response.statusCode).to.equal(200)
expect(threads[threadId].messages.length).to.equal(1)
expect(threads[threadId].messages[0].content).to.equal(newContent)
})
})
describe('with another user', function () {
beforeEach(async function () {
const { response } = await ChatClient.editMessageWithUser(
projectId,
threadId,
messageId,
new ObjectId(),
newContent
)
expect(response.statusCode).to.equal(404)
})
it('should then list the old message in the threads', async function () {
const { response, body: threads } =
await ChatClient.getThreads(projectId)
expect(response.statusCode).to.equal(200)
expect(threads[threadId].messages.length).to.equal(1)
expect(threads[threadId].messages[0].content).to.equal(content)
})
})
})
})

View File

@@ -0,0 +1,164 @@
import { ObjectId } from '../../../app/js/mongodb.js'
import { expect } from 'chai'
import * as ChatClient from './helpers/ChatClient.js'
import * as ChatApp from './helpers/ChatApp.js'
async function getCount() {
return await ChatClient.getMetric(line => {
return (
line.includes('timer_http_request_count') &&
line.includes('path="project_{projectId}_messages"') &&
line.includes('method="POST"')
)
})
}
describe('Getting messages', async function () {
const userId1 = new ObjectId().toString()
const userId2 = new ObjectId().toString()
const content1 = 'foo bar'
const content2 = 'hello world'
before(async function () {
await ChatApp.ensureRunning()
})
describe('globally', async function () {
const projectId = new ObjectId().toString()
before(async function () {
const previousCount = await getCount()
const { response } = await ChatClient.sendGlobalMessage(
projectId,
userId1,
content1
)
expect(response.statusCode).to.equal(201)
const { response: response2 } = await ChatClient.sendGlobalMessage(
projectId,
userId2,
content2
)
expect(response2.statusCode).to.equal(201)
const { response: response3, body } = await ChatClient.checkStatus()
expect(response3.statusCode).to.equal(200)
expect(body).to.equal('chat is alive')
expect(await getCount()).to.equal(previousCount + 2)
})
it('should contain the messages and populated users when getting the messages', async function () {
const { response, body: messages } =
await ChatClient.getGlobalMessages(projectId)
expect(response.statusCode).to.equal(200)
expect(messages.length).to.equal(2)
messages.reverse()
expect(messages[0].content).to.equal(content1)
expect(messages[0].user_id).to.equal(userId1)
expect(messages[1].content).to.equal(content2)
expect(messages[1].user_id).to.equal(userId2)
})
})
describe('from all the threads', async function () {
const projectId = new ObjectId().toString()
const threadId1 = new ObjectId().toString()
const threadId2 = new ObjectId().toString()
before(async function () {
const { response } = await ChatClient.sendMessage(
projectId,
threadId1,
userId1,
'one'
)
expect(response.statusCode).to.equal(201)
const { response: response2 } = await ChatClient.sendMessage(
projectId,
threadId2,
userId2,
'two'
)
expect(response2.statusCode).to.equal(201)
const { response: response3 } = await ChatClient.sendMessage(
projectId,
threadId1,
userId1,
'three'
)
expect(response3.statusCode).to.equal(201)
const { response: response4 } = await ChatClient.sendMessage(
projectId,
threadId2,
userId2,
'four'
)
expect(response4.statusCode).to.equal(201)
})
it('should contain a dictionary of threads with messages with populated users', async function () {
const { response, body: threads } = await ChatClient.getThreads(projectId)
expect(response.statusCode).to.equal(200)
expect(Object.keys(threads).length).to.equal(2)
const thread1 = threads[threadId1]
expect(thread1.messages.length).to.equal(2)
const thread2 = threads[threadId2]
expect(thread2.messages.length).to.equal(2)
expect(thread1.messages[0].content).to.equal('one')
expect(thread1.messages[0].user_id).to.equal(userId1)
expect(thread1.messages[1].content).to.equal('three')
expect(thread1.messages[1].user_id).to.equal(userId1)
expect(thread2.messages[0].content).to.equal('two')
expect(thread2.messages[0].user_id).to.equal(userId2)
expect(thread2.messages[1].content).to.equal('four')
expect(thread2.messages[1].user_id).to.equal(userId2)
})
})
describe('from a list of threads', function () {
const projectId = new ObjectId().toString()
const threadId1 = new ObjectId().toString()
const threadId2 = new ObjectId().toString()
const threadId3 = new ObjectId().toString()
before(async function () {
const { response } = await ChatClient.sendMessage(
projectId,
threadId1,
userId1,
'one'
)
expect(response.statusCode).to.equal(201)
const { response: response2 } = await ChatClient.sendMessage(
projectId,
threadId2,
userId2,
'two'
)
expect(response2.statusCode).to.equal(201)
const { response: response3 } = await ChatClient.sendMessage(
projectId,
threadId1,
userId1,
'three'
)
expect(response3.statusCode).to.equal(201)
})
it('should contain a dictionary of threads with messages with populated users', async function () {
const { response, body: threads } = await ChatClient.generateThreadData(
projectId,
[threadId1, threadId3]
)
expect(response.statusCode).to.equal(200)
expect(Object.keys(threads).length).to.equal(1)
const thread1 = threads[threadId1]
expect(thread1.messages.length).to.equal(2)
expect(thread1.messages[0].content).to.equal('one')
expect(thread1.messages[0].user_id).to.equal(userId1)
expect(thread1.messages[1].content).to.equal('three')
expect(thread1.messages[1].user_id).to.equal(userId1)
})
})
})

View File

@@ -0,0 +1,114 @@
import { ObjectId } from '../../../app/js/mongodb.js'
import { expect } from 'chai'
import * as ChatClient from './helpers/ChatClient.js'
import * as ChatApp from './helpers/ChatApp.js'
describe('Resolving a thread', async function () {
const projectId = new ObjectId().toString()
const userId = new ObjectId().toString()
before(async function () {
await ChatApp.ensureRunning()
})
describe('with a resolved thread', async function () {
const threadId = new ObjectId().toString()
const content = 'resolved message'
before(async function () {
const { response } = await ChatClient.sendMessage(
projectId,
threadId,
userId,
content
)
expect(response.statusCode).to.equal(201)
const { response: response2 } = await ChatClient.resolveThread(
projectId,
threadId,
userId
)
expect(response2.statusCode).to.equal(204)
})
it('should then list the thread as resolved', async function () {
const { response, body: threads } = await ChatClient.getThreads(projectId)
expect(response.statusCode).to.equal(200)
expect(threads[threadId].resolved).to.equal(true)
expect(threads[threadId].resolved_by_user_id).to.equal(userId)
const resolvedAt = new Date(threads[threadId].resolved_at)
expect(new Date() - resolvedAt).to.be.below(1000)
})
it('should list the thread id in the resolved thread ids endpoint', async function () {
const { response, body } =
await ChatClient.getResolvedThreadIds(projectId)
expect(response.statusCode).to.equal(200)
expect(body.resolvedThreadIds).to.include(threadId)
})
})
describe('when a thread is not resolved', async function () {
const threadId = new ObjectId().toString()
const content = 'open message'
before(async function () {
const { response } = await ChatClient.sendMessage(
projectId,
threadId,
userId,
content
)
expect(response.statusCode).to.equal(201)
})
it('should not list the thread as resolved', async function () {
const { response, body: threads } = await ChatClient.getThreads(projectId)
expect(response.statusCode).to.equal(200)
expect(threads[threadId].resolved).to.be.undefined
})
it('should not list the thread in the resolved thread ids endpoint', async function () {
const { response, body } =
await ChatClient.getResolvedThreadIds(projectId)
expect(response.statusCode).to.equal(200)
expect(body.resolvedThreadIds).not.to.include(threadId)
})
})
describe('when a thread is resolved then reopened', async function () {
const threadId = new ObjectId().toString()
const content = 'resolved message'
before(async function () {
const { response } = await ChatClient.sendMessage(
projectId,
threadId,
userId,
content
)
expect(response.statusCode).to.equal(201)
const { response: response2 } = await ChatClient.resolveThread(
projectId,
threadId,
userId
)
expect(response2.statusCode).to.equal(204)
const { response: response3 } = await ChatClient.reopenThread(
projectId,
threadId
)
expect(response3.statusCode).to.equal(204)
})
it('should not list the thread as resolved', async function () {
const { response, body: threads } = await ChatClient.getThreads(projectId)
expect(response.statusCode).to.equal(200)
expect(threads[threadId].resolved).to.be.undefined
})
it('should not list the thread in the resolved thread ids endpoint', async function () {
const { response, body } =
await ChatClient.getResolvedThreadIds(projectId)
expect(response.statusCode).to.equal(200)
expect(body.resolvedThreadIds).not.to.include(threadId)
})
})
})

View File

@@ -0,0 +1,143 @@
import { ObjectId } from '../../../app/js/mongodb.js'
import { expect } from 'chai'
import * as ChatClient from './helpers/ChatClient.js'
import * as ChatApp from './helpers/ChatApp.js'
describe('Sending a message', async function () {
before(async function () {
await ChatApp.ensureRunning()
})
describe('globally', async function () {
const projectId = new ObjectId().toString()
const userId = new ObjectId().toString()
const content = 'global message'
before(async function () {
const { response, body } = await ChatClient.sendGlobalMessage(
projectId,
userId,
content
)
expect(response.statusCode).to.equal(201)
expect(body.content).to.equal(content)
expect(body.user_id).to.equal(userId)
expect(body.room_id).to.equal(projectId)
})
it('should then list the message in the project messages', async function () {
const { response, body: messages } =
await ChatClient.getGlobalMessages(projectId)
expect(response.statusCode).to.equal(200)
expect(messages.length).to.equal(1)
expect(messages[0].content).to.equal(content)
})
})
describe('to a thread', async function () {
const projectId = new ObjectId().toString()
const userId = new ObjectId().toString()
const threadId = new ObjectId().toString()
const content = 'thread message'
before(async function () {
const { response, body } = await ChatClient.sendMessage(
projectId,
threadId,
userId,
content
)
expect(response.statusCode).to.equal(201)
expect(body.content).to.equal(content)
expect(body.user_id).to.equal(userId)
expect(body.room_id).to.equal(projectId)
})
it('should then list the message in the threads', async function () {
const { response, body: threads } = await ChatClient.getThreads(projectId)
expect(response.statusCode).to.equal(200)
expect(threads[threadId].messages.length).to.equal(1)
expect(threads[threadId].messages[0].content).to.equal(content)
})
it('should not appear in the global messages', async function () {
const { response, body: messages } =
await ChatClient.getGlobalMessages(projectId)
expect(response.statusCode).to.equal(200)
expect(messages.length).to.equal(0)
})
})
describe('failure cases', async function () {
const projectId = new ObjectId().toString()
const userId = new ObjectId().toString()
const threadId = new ObjectId().toString()
describe('with a malformed userId', async function () {
it('should return a graceful error', async function () {
const { response, body } = await ChatClient.sendMessage(
projectId,
threadId,
'malformed-user',
'content'
)
expect(response.statusCode).to.equal(400)
expect(body).to.equal('Invalid userId')
})
})
describe('with a malformed projectId', async function () {
it('should return a graceful error', async function () {
const { response, body } = await ChatClient.sendMessage(
'malformed-project',
threadId,
userId,
'content'
)
expect(response.statusCode).to.equal(400)
expect(body).to.equal('Invalid projectId')
})
})
describe('with a malformed threadId', async function () {
it('should return a graceful error', async function () {
const { response, body } = await ChatClient.sendMessage(
projectId,
'malformed-thread-id',
userId,
'content'
)
expect(response.statusCode).to.equal(400)
expect(body).to.equal('Invalid threadId')
})
})
describe('with no content', async function () {
it('should return a graceful error', async function () {
const { response, body } = await ChatClient.sendMessage(
projectId,
threadId,
userId,
null
)
expect(response.statusCode).to.equal(400)
// Exegesis is responding with validation errors. I can´t find a way to choose the validation error yet.
// expect(body).to.equal('No content provided')
expect(body.message).to.equal('Validation errors')
})
})
describe('with very long content', async function () {
it('should return a graceful error', async function () {
const content = '-'.repeat(10 * 1024 + 1)
const { response, body } = await ChatClient.sendMessage(
projectId,
threadId,
userId,
content
)
expect(response.statusCode).to.equal(400)
expect(body).to.equal('Content too long (> 10240 bytes)')
})
})
})
})

View File

@@ -0,0 +1,15 @@
import { createServer } from '../../../../app/js/server.js'
import { promisify } from 'node:util'
export { db } from '../../../../app/js/mongodb.js'
let serverPromise = null
export async function ensureRunning() {
if (!serverPromise) {
const { app } = await createServer()
const startServer = promisify(app.listen.bind(app))
serverPromise = startServer(3010, '127.0.0.1')
}
return serverPromise
}

View File

@@ -0,0 +1,166 @@
import Request from 'request'
const request = Request.defaults({
baseUrl: 'http://127.0.0.1:3010',
})
async function asyncRequest(options) {
return await new Promise((resolve, reject) => {
request(options, (err, response, body) => {
if (err) {
reject(err)
} else {
resolve({ response, body })
}
})
})
}
export async function sendGlobalMessage(projectId, userId, content) {
return await asyncRequest({
method: 'post',
url: `/project/${projectId}/messages`,
json: {
user_id: userId,
content,
},
})
}
export async function getGlobalMessages(projectId) {
return await asyncRequest({
method: 'get',
url: `/project/${projectId}/messages`,
json: true,
})
}
export async function sendMessage(projectId, threadId, userId, content) {
return await asyncRequest({
method: 'post',
url: `/project/${projectId}/thread/${threadId}/messages`,
json: {
user_id: userId,
content,
},
})
}
export async function getThreads(projectId) {
return await asyncRequest({
method: 'get',
url: `/project/${projectId}/threads`,
json: true,
})
}
export async function resolveThread(projectId, threadId, userId) {
return await asyncRequest({
method: 'post',
url: `/project/${projectId}/thread/${threadId}/resolve`,
json: {
user_id: userId,
},
})
}
export async function getResolvedThreadIds(projectId) {
return await asyncRequest({
method: 'get',
url: `/project/${projectId}/resolved-thread-ids`,
json: true,
})
}
export async function editMessage(projectId, threadId, messageId, content) {
return await asyncRequest({
method: 'post',
url: `/project/${projectId}/thread/${threadId}/messages/${messageId}/edit`,
json: {
content,
},
})
}
export async function editMessageWithUser(
projectId,
threadId,
messageId,
userId,
content
) {
return await asyncRequest({
method: 'post',
url: `/project/${projectId}/thread/${threadId}/messages/${messageId}/edit`,
json: {
content,
userId,
},
})
}
export async function checkStatus() {
return await asyncRequest({
method: 'get',
url: `/status`,
json: true,
})
}
export async function getMetric(matcher) {
const { body } = await asyncRequest({
method: 'get',
url: `/metrics`,
})
const found = body.split('\n').find(matcher)
if (!found) return 0
return parseInt(found.split(' ')[1], 0)
}
export async function reopenThread(projectId, threadId) {
return await asyncRequest({
method: 'post',
url: `/project/${projectId}/thread/${threadId}/reopen`,
})
}
export async function deleteThread(projectId, threadId) {
return await asyncRequest({
method: 'delete',
url: `/project/${projectId}/thread/${threadId}`,
})
}
export async function deleteMessage(projectId, threadId, messageId) {
return await asyncRequest({
method: 'delete',
url: `/project/${projectId}/thread/${threadId}/messages/${messageId}`,
})
}
export async function destroyProject(projectId) {
return await asyncRequest({
method: 'delete',
url: `/project/${projectId}`,
})
}
export async function duplicateCommentThreads(projectId, threads) {
return await asyncRequest({
method: 'post',
url: `/project/${projectId}/duplicate-comment-threads`,
json: {
threads,
},
})
}
export async function generateThreadData(projectId, threads) {
return await asyncRequest({
method: 'post',
url: `/project/${projectId}/generate-thread-data`,
json: {
threads,
},
})
}

View File

@@ -0,0 +1,9 @@
import chai from 'chai'
import chaiAsPromised from 'chai-as-promised'
import { ObjectId } from 'mongodb'
// ensure every ObjectId has the id string as a property for correct comparisons
ObjectId.cacheHexString = true
chai.should()
chai.use(chaiAsPromised)