mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 08:52:21 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
edd83fdf85 | ||
|
|
d8c7250cf2 |
@@ -0,0 +1,76 @@
|
||||
/* @vitest-environment node */
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
vi.mock('./lib/access', () => ({
|
||||
assertRole: vi.fn(),
|
||||
requireUser: vi.fn(),
|
||||
}))
|
||||
|
||||
const { requireUser } = await import('./lib/access')
|
||||
const { __test } = await import('./comments')
|
||||
|
||||
describe('comments mutations', () => {
|
||||
afterEach(() => {
|
||||
vi.mocked(requireUser).mockReset()
|
||||
})
|
||||
|
||||
it('add updates comment count without touching updatedAt', async () => {
|
||||
vi.mocked(requireUser).mockResolvedValue({
|
||||
userId: 'users:1',
|
||||
user: { _id: 'users:1', role: 'user' },
|
||||
} as never)
|
||||
|
||||
const get = vi.fn().mockResolvedValue({
|
||||
_id: 'skills:1',
|
||||
stats: { comments: 2 },
|
||||
})
|
||||
const insert = vi.fn()
|
||||
const patch = vi.fn()
|
||||
const ctx = { db: { get, insert, patch } } as never
|
||||
|
||||
await __test.addHandler(ctx, { skillId: 'skills:1', body: ' hello ' } as never)
|
||||
|
||||
expect(patch).toHaveBeenCalledTimes(1)
|
||||
expect(patch).toHaveBeenCalledWith('skills:1', {
|
||||
stats: { comments: 3 },
|
||||
})
|
||||
const skillPatch = vi.mocked(patch).mock.calls[0]?.[1] as Record<string, unknown>
|
||||
expect(skillPatch.updatedAt).toBeUndefined()
|
||||
})
|
||||
|
||||
it('remove updates comment count without touching updatedAt', async () => {
|
||||
vi.mocked(requireUser).mockResolvedValue({
|
||||
userId: 'users:2',
|
||||
user: { _id: 'users:2', role: 'moderator' },
|
||||
} as never)
|
||||
|
||||
const comment = {
|
||||
_id: 'comments:1',
|
||||
skillId: 'skills:1',
|
||||
userId: 'users:2',
|
||||
softDeletedAt: undefined,
|
||||
}
|
||||
const skill = {
|
||||
_id: 'skills:1',
|
||||
stats: { comments: 4 },
|
||||
}
|
||||
|
||||
const get = vi.fn(async (id: string) => {
|
||||
if (id === 'comments:1') return comment
|
||||
if (id === 'skills:1') return skill
|
||||
return null
|
||||
})
|
||||
const insert = vi.fn()
|
||||
const patch = vi.fn()
|
||||
const ctx = { db: { get, insert, patch } } as never
|
||||
|
||||
await __test.removeHandler(ctx, { commentId: 'comments:1' } as never)
|
||||
|
||||
expect(patch).toHaveBeenCalledTimes(2)
|
||||
expect(patch).toHaveBeenNthCalledWith(2, 'skills:1', {
|
||||
stats: { comments: 3 },
|
||||
})
|
||||
const skillPatch = vi.mocked(patch).mock.calls[1]?.[1] as Record<string, unknown>
|
||||
expect(skillPatch.updatedAt).toBeUndefined()
|
||||
})
|
||||
})
|
||||
+68
-55
@@ -3,6 +3,67 @@ import type { Doc } from './_generated/dataModel'
|
||||
import { mutation, query } from './_generated/server'
|
||||
import { assertRole, requireUser } from './lib/access'
|
||||
|
||||
async function addHandler(
|
||||
ctx: import('./_generated/server').MutationCtx,
|
||||
args: { skillId: import('./_generated/dataModel').Id<'skills'>; body: string },
|
||||
) {
|
||||
const { userId } = await requireUser(ctx)
|
||||
const body = args.body.trim()
|
||||
if (!body) throw new Error('Comment body required')
|
||||
|
||||
const skill = await ctx.db.get(args.skillId)
|
||||
if (!skill) throw new Error('Skill not found')
|
||||
|
||||
await ctx.db.insert('comments', {
|
||||
skillId: args.skillId,
|
||||
userId,
|
||||
body,
|
||||
createdAt: Date.now(),
|
||||
softDeletedAt: undefined,
|
||||
deletedBy: undefined,
|
||||
})
|
||||
|
||||
await ctx.db.patch(skill._id, {
|
||||
stats: { ...skill.stats, comments: skill.stats.comments + 1 },
|
||||
})
|
||||
}
|
||||
|
||||
async function removeHandler(
|
||||
ctx: import('./_generated/server').MutationCtx,
|
||||
args: { commentId: import('./_generated/dataModel').Id<'comments'> },
|
||||
) {
|
||||
const { user } = await requireUser(ctx)
|
||||
const comment = await ctx.db.get(args.commentId)
|
||||
if (!comment) throw new Error('Comment not found')
|
||||
if (comment.softDeletedAt) return
|
||||
|
||||
const isOwner = comment.userId === user._id
|
||||
if (!isOwner) {
|
||||
assertRole(user, ['admin', 'moderator'])
|
||||
}
|
||||
|
||||
await ctx.db.patch(comment._id, {
|
||||
softDeletedAt: Date.now(),
|
||||
deletedBy: user._id,
|
||||
})
|
||||
|
||||
const skill = await ctx.db.get(comment.skillId)
|
||||
if (skill) {
|
||||
await ctx.db.patch(skill._id, {
|
||||
stats: { ...skill.stats, comments: Math.max(0, skill.stats.comments - 1) },
|
||||
})
|
||||
}
|
||||
|
||||
await ctx.db.insert('auditLogs', {
|
||||
actorUserId: user._id,
|
||||
action: 'comment.delete',
|
||||
targetType: 'comment',
|
||||
targetId: comment._id,
|
||||
metadata: { skillId: comment.skillId },
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
export const listBySkill = query({
|
||||
args: { skillId: v.id('skills'), limit: v.optional(v.number()) },
|
||||
handler: async (ctx, args) => {
|
||||
@@ -25,63 +86,15 @@ export const listBySkill = query({
|
||||
|
||||
export const add = mutation({
|
||||
args: { skillId: v.id('skills'), body: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const { userId } = await requireUser(ctx)
|
||||
const body = args.body.trim()
|
||||
if (!body) throw new Error('Comment body required')
|
||||
|
||||
const skill = await ctx.db.get(args.skillId)
|
||||
if (!skill) throw new Error('Skill not found')
|
||||
|
||||
await ctx.db.insert('comments', {
|
||||
skillId: args.skillId,
|
||||
userId,
|
||||
body,
|
||||
createdAt: Date.now(),
|
||||
softDeletedAt: undefined,
|
||||
deletedBy: undefined,
|
||||
})
|
||||
|
||||
await ctx.db.patch(skill._id, {
|
||||
stats: { ...skill.stats, comments: skill.stats.comments + 1 },
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
},
|
||||
handler: addHandler,
|
||||
})
|
||||
|
||||
export const remove = mutation({
|
||||
args: { commentId: v.id('comments') },
|
||||
handler: async (ctx, args) => {
|
||||
const { user } = await requireUser(ctx)
|
||||
const comment = await ctx.db.get(args.commentId)
|
||||
if (!comment) throw new Error('Comment not found')
|
||||
if (comment.softDeletedAt) return
|
||||
|
||||
const isOwner = comment.userId === user._id
|
||||
if (!isOwner) {
|
||||
assertRole(user, ['admin', 'moderator'])
|
||||
}
|
||||
|
||||
await ctx.db.patch(comment._id, {
|
||||
softDeletedAt: Date.now(),
|
||||
deletedBy: user._id,
|
||||
})
|
||||
|
||||
const skill = await ctx.db.get(comment.skillId)
|
||||
if (skill) {
|
||||
await ctx.db.patch(skill._id, {
|
||||
stats: { ...skill.stats, comments: Math.max(0, skill.stats.comments - 1) },
|
||||
updatedAt: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
await ctx.db.insert('auditLogs', {
|
||||
actorUserId: user._id,
|
||||
action: 'comment.delete',
|
||||
targetType: 'comment',
|
||||
targetId: comment._id,
|
||||
metadata: { skillId: comment.skillId },
|
||||
createdAt: Date.now(),
|
||||
})
|
||||
},
|
||||
handler: removeHandler,
|
||||
})
|
||||
|
||||
export const __test = {
|
||||
addHandler,
|
||||
removeHandler,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user