mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-13 16:22:03 +00:00
fix(api): ecritures en plusieurs temps dans une transaction
Une erreur au milieu laissait un etat partiel, et un nouvel essai du client recreait des doublons. - Lot d'annotations : creations, modifications, suppressions et trace. - Moments : session de contribution, moment, souvenirs rattaches et trace ; modification et suppression d'un moment avec ses souvenirs. - Reordonnancement des souvenirs d'une personne. - La famille ne rattache a son moment que des souvenirs libres : elle ne detache plus un souvenir du moment d'un administrateur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
82f888b1dc
commit
f20b58be32
4 files changed
+256
-135
No files matched your search
@@ -306,64 +306,67 @@ export const annotationRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.code(400).send({ error: 'invalid_annotation_photo_path' })
|
||||
}
|
||||
|
||||
// Creates
|
||||
for (const [index, item] of payload.data.create.entries()) {
|
||||
await app.prisma.annotation.create({
|
||||
// Créations, modifications, suppressions et trace : tout ou rien, un nouvel essai ne double rien
|
||||
await app.prisma.$transaction(async (tx) => {
|
||||
// Creates
|
||||
for (const [index, item] of payload.data.create.entries()) {
|
||||
await tx.annotation.create({
|
||||
data: {
|
||||
treeId,
|
||||
type: item.type,
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
content: createContents[index] as string,
|
||||
style: (item.style ?? {}) as Prisma.InputJsonValue,
|
||||
zIndex: item.zIndex ?? 0,
|
||||
createdBy: user.userId,
|
||||
},
|
||||
})
|
||||
results.created++
|
||||
}
|
||||
|
||||
// Updates
|
||||
for (const [index, item] of payload.data.update.entries()) {
|
||||
const data: Record<string, unknown> = {}
|
||||
if (item.x !== undefined) data.x = item.x
|
||||
if (item.y !== undefined) data.y = item.y
|
||||
if (item.content !== undefined) data.content = updateContents[index]
|
||||
if (item.style !== undefined) data.style = item.style
|
||||
if (item.zIndex !== undefined) data.zIndex = item.zIndex
|
||||
|
||||
if (Object.keys(data).length > 0) {
|
||||
const updated = await tx.annotation.updateMany({
|
||||
where: { id: item.id, treeId, deletedAt: null },
|
||||
data,
|
||||
})
|
||||
if (updated.count > 0) results.updated++
|
||||
}
|
||||
}
|
||||
|
||||
// Deletes
|
||||
if (payload.data.delete.length > 0) {
|
||||
const deleted = await tx.annotation.updateMany({
|
||||
where: {
|
||||
id: { in: payload.data.delete },
|
||||
treeId,
|
||||
deletedAt: null,
|
||||
},
|
||||
data: { deletedAt: new Date() },
|
||||
})
|
||||
results.deleted = deleted.count
|
||||
}
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
treeId,
|
||||
type: item.type,
|
||||
x: item.x,
|
||||
y: item.y,
|
||||
content: createContents[index] as string,
|
||||
style: (item.style ?? {}) as Prisma.InputJsonValue,
|
||||
zIndex: item.zIndex ?? 0,
|
||||
createdBy: user.userId,
|
||||
actorType: 'user',
|
||||
actorId: user.userId,
|
||||
action: 'annotations_batch',
|
||||
entityType: 'annotation',
|
||||
entityId: treeId,
|
||||
payloadJson: results,
|
||||
},
|
||||
})
|
||||
results.created++
|
||||
}
|
||||
|
||||
// Updates
|
||||
for (const [index, item] of payload.data.update.entries()) {
|
||||
const data: Record<string, unknown> = {}
|
||||
if (item.x !== undefined) data.x = item.x
|
||||
if (item.y !== undefined) data.y = item.y
|
||||
if (item.content !== undefined) data.content = updateContents[index]
|
||||
if (item.style !== undefined) data.style = item.style
|
||||
if (item.zIndex !== undefined) data.zIndex = item.zIndex
|
||||
|
||||
if (Object.keys(data).length > 0) {
|
||||
const updated = await app.prisma.annotation.updateMany({
|
||||
where: { id: item.id, treeId, deletedAt: null },
|
||||
data,
|
||||
})
|
||||
if (updated.count > 0) results.updated++
|
||||
}
|
||||
}
|
||||
|
||||
// Deletes
|
||||
if (payload.data.delete.length > 0) {
|
||||
const deleted = await app.prisma.annotation.updateMany({
|
||||
where: {
|
||||
id: { in: payload.data.delete },
|
||||
treeId,
|
||||
deletedAt: null,
|
||||
},
|
||||
data: { deletedAt: new Date() },
|
||||
})
|
||||
results.deleted = deleted.count
|
||||
}
|
||||
|
||||
await app.prisma.auditLog.create({
|
||||
data: {
|
||||
treeId,
|
||||
actorType: 'user',
|
||||
actorId: user.userId,
|
||||
action: 'annotations_batch',
|
||||
entityType: 'annotation',
|
||||
entityId: treeId,
|
||||
payloadJson: results,
|
||||
},
|
||||
})
|
||||
|
||||
return reply.send({ ok: true, ...results })
|
||||
|
||||
+29
-26
@@ -795,35 +795,38 @@ export const mediaRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.code(400).send({ error: 'invalid_media_order_payload' })
|
||||
}
|
||||
|
||||
for (let index = 0; index < orderedIds.length; index += 1) {
|
||||
await app.prisma.mediaItem.updateMany({
|
||||
where: {
|
||||
id: orderedIds[index],
|
||||
treeId: params.data.id,
|
||||
personId: params.data.personId,
|
||||
deletedAt: null,
|
||||
},
|
||||
const actorId = request.actor.userId
|
||||
await app.prisma.$transaction(async (tx) => {
|
||||
for (let index = 0; index < orderedIds.length; index += 1) {
|
||||
await tx.mediaItem.updateMany({
|
||||
where: {
|
||||
id: orderedIds[index],
|
||||
treeId: params.data.id,
|
||||
personId: params.data.personId,
|
||||
deletedAt: null,
|
||||
},
|
||||
data: {
|
||||
displayOrder: index + 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await syncPersonMediaOrder(tx, params.data.id, params.data.personId)
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
displayOrder: index + 1,
|
||||
treeId: params.data.id,
|
||||
actorType: 'user',
|
||||
actorId,
|
||||
action: 'media_reordered',
|
||||
entityType: 'media_item',
|
||||
entityId: params.data.personId,
|
||||
payloadJson: {
|
||||
personId: params.data.personId,
|
||||
orderedMediaIds: payload.data.orderedMediaIds,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
await syncPersonMediaOrder(app.prisma, params.data.id, params.data.personId)
|
||||
|
||||
await app.prisma.auditLog.create({
|
||||
data: {
|
||||
treeId: params.data.id,
|
||||
actorType: 'user',
|
||||
actorId: request.actor.userId,
|
||||
action: 'media_reordered',
|
||||
entityType: 'media_item',
|
||||
entityId: params.data.personId,
|
||||
payloadJson: {
|
||||
personId: params.data.personId,
|
||||
orderedMediaIds: payload.data.orderedMediaIds,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return reply.send({ ok: true })
|
||||
|
||||
+66
-55
@@ -2095,21 +2095,23 @@ export const treeRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.send({ link })
|
||||
})
|
||||
|
||||
// Réordonner une fratrie : displayOrder des liens parent-enfant réécrit en une transaction
|
||||
// Souvenirs rattachables : approuvés, d'une de ces personnes, dans cet arbre
|
||||
const validLifeEventMedia = async (treeId: string, ownerIds: string[], mediaIds: string[] | undefined) => {
|
||||
// Souvenirs rattachables : approuvés, d'une de ces personnes, dans cet arbre. La famille ne prend
|
||||
// que des souvenirs libres : elle ne détache pas un souvenir du moment d'un autre.
|
||||
const validLifeEventMedia = async (treeId: string, ownerIds: string[], mediaIds: string[] | undefined, onlyFree = false) => {
|
||||
if (!mediaIds?.length) return true
|
||||
const unique = [...new Set(mediaIds)]
|
||||
const count = await app.prisma.mediaItem.count({ where: { id: { in: unique }, treeId, personId: { in: ownerIds }, status: 'approved', deletedAt: null } })
|
||||
const count = await app.prisma.mediaItem.count({
|
||||
where: { id: { in: unique }, treeId, personId: { in: ownerIds }, status: 'approved', deletedAt: null, ...(onlyFree ? { lifeEventId: null } : {}) },
|
||||
})
|
||||
return count === unique.length
|
||||
}
|
||||
|
||||
// Rattache exactement ces souvenirs au moment (et détache ceux qui ne sont plus dans la liste)
|
||||
const setLifeEventMedia = async (treeId: string, eventId: string, mediaIds: string[] | undefined) => {
|
||||
const setLifeEventMedia = async (db: Prisma.TransactionClient, treeId: string, eventId: string, mediaIds: string[] | undefined) => {
|
||||
if (mediaIds === undefined) return
|
||||
const unique = [...new Set(mediaIds)]
|
||||
await app.prisma.mediaItem.updateMany({ where: { treeId, lifeEventId: eventId, id: { notIn: unique } }, data: { lifeEventId: null } })
|
||||
if (unique.length) await app.prisma.mediaItem.updateMany({ where: { treeId, id: { in: unique } }, data: { lifeEventId: eventId } })
|
||||
await db.mediaItem.updateMany({ where: { treeId, lifeEventId: eventId, id: { notIn: unique } }, data: { lifeEventId: null } })
|
||||
if (unique.length) await db.mediaItem.updateMany({ where: { treeId, id: { in: unique } }, data: { lifeEventId: eventId } })
|
||||
}
|
||||
|
||||
app.post('/trees/:id/persons/:personId/life/events', async (request, reply) => {
|
||||
@@ -2170,55 +2172,59 @@ export const treeRoutes: FastifyPluginAsync = async (app) => {
|
||||
})
|
||||
if (existing) return reply.code(409).send({ error: 'event_exists', eventId: existing.id })
|
||||
}
|
||||
if (!(await validLifeEventMedia(params.data.id, ownerIds, payload.data.mediaIds))) {
|
||||
if (!(await validLifeEventMedia(params.data.id, ownerIds, payload.data.mediaIds, !isAdmin))) {
|
||||
return reply.code(400).send({ error: 'invalid_media_reference' })
|
||||
}
|
||||
|
||||
const isPending = contribution?.autoStatus === 'pending'
|
||||
const title = payload.data.label?.trim() || payload.data.description?.trim() || payload.data.type
|
||||
const session = contribution
|
||||
? await app.prisma.contributionSession.create({
|
||||
// Session, moment, souvenirs et trace de la contribution : tout ou rien
|
||||
const event = await app.prisma.$transaction(async (tx) => {
|
||||
const session = contribution
|
||||
? await tx.contributionSession.create({
|
||||
data: {
|
||||
treeId: params.data.id,
|
||||
submittedByUserId: contribution.submittedByUserId,
|
||||
submittedByLabel: payload.data.submittedByLabel?.trim() || contribution.submittedByLabel,
|
||||
mode: contribution.mode,
|
||||
status: contribution.autoStatus,
|
||||
title: `Moment pour ${person.firstName || person.lastName}`.slice(0, 120),
|
||||
reviewedAt: isPending ? undefined : new Date(),
|
||||
reviewedBy: isPending ? undefined : contribution.submittedByUserId,
|
||||
},
|
||||
})
|
||||
: null
|
||||
|
||||
const event = await tx.lifeEvent.create({
|
||||
data: {
|
||||
treeId: params.data.id,
|
||||
submittedByUserId: contribution.submittedByUserId,
|
||||
submittedByLabel: payload.data.submittedByLabel?.trim() || contribution.submittedByLabel,
|
||||
mode: contribution.mode,
|
||||
status: contribution.autoStatus,
|
||||
title: `Moment pour ${person.firstName || person.lastName}`.slice(0, 120),
|
||||
reviewedAt: isPending ? undefined : new Date(),
|
||||
reviewedBy: isPending ? undefined : contribution.submittedByUserId,
|
||||
personId: unionId ? null : person.id,
|
||||
unionId,
|
||||
type: payload.data.type,
|
||||
...lifeEventData(payload.data, reserved),
|
||||
status: isPending ? 'pending' : 'approved',
|
||||
contributionSessionId: session?.id ?? null,
|
||||
},
|
||||
})
|
||||
: null
|
||||
await setLifeEventMedia(tx, params.data.id, event.id, payload.data.mediaIds)
|
||||
|
||||
const event = await app.prisma.lifeEvent.create({
|
||||
data: {
|
||||
treeId: params.data.id,
|
||||
personId: unionId ? null : person.id,
|
||||
unionId,
|
||||
type: payload.data.type,
|
||||
...lifeEventData(payload.data, reserved),
|
||||
status: isPending ? 'pending' : 'approved',
|
||||
contributionSessionId: session?.id ?? null,
|
||||
},
|
||||
})
|
||||
await setLifeEventMedia(params.data.id, event.id, payload.data.mediaIds)
|
||||
|
||||
if (session) {
|
||||
await app.prisma.contributionChange.create({
|
||||
data: {
|
||||
sessionId: session.id,
|
||||
entityType: 'life_event',
|
||||
entityId: event.id,
|
||||
action: 'create',
|
||||
afterJson: {
|
||||
personId: person.id, type: event.type, title, date: payload.data.date ?? null, dateEnd: payload.data.dateEnd ?? null,
|
||||
datePrecision: payload.data.datePrecision ?? null, place: event.place, notes: event.notes, sources: event.sources,
|
||||
if (session) {
|
||||
await tx.contributionChange.create({
|
||||
data: {
|
||||
sessionId: session.id,
|
||||
entityType: 'life_event',
|
||||
entityId: event.id,
|
||||
action: 'create',
|
||||
afterJson: {
|
||||
personId: person.id, type: event.type, title, date: payload.data.date ?? null, dateEnd: payload.data.dateEnd ?? null,
|
||||
datePrecision: payload.data.datePrecision ?? null, place: event.place, notes: event.notes, sources: event.sources,
|
||||
},
|
||||
decision: isPending ? undefined : 'approved',
|
||||
},
|
||||
decision: isPending ? undefined : 'approved',
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return event
|
||||
})
|
||||
return reply.code(201).send({ event: { id: event.id, status: event.status } })
|
||||
})
|
||||
|
||||
@@ -2249,11 +2255,13 @@ export const treeRoutes: FastifyPluginAsync = async (app) => {
|
||||
return reply.code(400).send({ error: 'invalid_media_reference' })
|
||||
}
|
||||
|
||||
await app.prisma.lifeEvent.update({
|
||||
where: { id: params.data.eventId },
|
||||
data: lifeEventData(payload.data, RESERVED_LIFE_EVENT_TYPES.has(existing.type)),
|
||||
await app.prisma.$transaction(async (tx) => {
|
||||
await tx.lifeEvent.update({
|
||||
where: { id: params.data.eventId },
|
||||
data: lifeEventData(payload.data, RESERVED_LIFE_EVENT_TYPES.has(existing.type)),
|
||||
})
|
||||
await setLifeEventMedia(tx, params.data.id, params.data.eventId, payload.data.mediaIds)
|
||||
})
|
||||
await setLifeEventMedia(params.data.id, params.data.eventId, payload.data.mediaIds)
|
||||
return reply.send({ ok: true })
|
||||
})
|
||||
|
||||
@@ -2266,13 +2274,16 @@ export const treeRoutes: FastifyPluginAsync = async (app) => {
|
||||
const membership = await getAdminMembership(app, user.userId, params.data.id)
|
||||
if (!hasActiveAdminMembership(membership)) return reply.code(403).send({ error: 'forbidden' })
|
||||
|
||||
const deleted = await app.prisma.lifeEvent.updateMany({
|
||||
where: { id: params.data.eventId, treeId: params.data.id, deletedAt: null, personId: { not: null }, type: { notIn: [...RESERVED_LIFE_EVENT_TYPES] } },
|
||||
data: { deletedAt: new Date() },
|
||||
const deletedCount = await app.prisma.$transaction(async (tx) => {
|
||||
const deleted = await tx.lifeEvent.updateMany({
|
||||
where: { id: params.data.eventId, treeId: params.data.id, deletedAt: null, personId: { not: null }, type: { notIn: [...RESERVED_LIFE_EVENT_TYPES] } },
|
||||
data: { deletedAt: new Date() },
|
||||
})
|
||||
// Les souvenirs restent dans la fiche, simplement détachés du moment
|
||||
if (deleted.count > 0) await tx.mediaItem.updateMany({ where: { treeId: params.data.id, lifeEventId: params.data.eventId }, data: { lifeEventId: null } })
|
||||
return deleted.count
|
||||
})
|
||||
if (deleted.count === 0) return reply.code(404).send({ error: 'event_not_found' })
|
||||
// Les souvenirs restent dans la fiche, simplement détachés du moment
|
||||
await app.prisma.mediaItem.updateMany({ where: { treeId: params.data.id, lifeEventId: params.data.eventId }, data: { lifeEventId: null } })
|
||||
if (deletedCount === 0) return reply.code(404).send({ error: 'event_not_found' })
|
||||
return reply.code(204).send()
|
||||
})
|
||||
|
||||
|
||||
@@ -80,6 +80,9 @@ const { prismaMock } = vi.hoisted(() => ({
|
||||
},
|
||||
annotation: {
|
||||
findMany: vi.fn(),
|
||||
count: vi.fn().mockResolvedValue(0),
|
||||
create: vi.fn(),
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||
},
|
||||
contributionSession: {
|
||||
create: vi.fn(),
|
||||
@@ -753,6 +756,107 @@ describe('Moments de l’onglet Vie', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Écritures en plusieurs temps : tout passe par une transaction', () => {
|
||||
// Le client de la transaction est un autre objet que app.prisma : une écriture faite hors transaction se voit
|
||||
const txClient = () => ({
|
||||
annotation: { create: vi.fn().mockResolvedValue({ id: 'a' }), updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
|
||||
mediaItem: { updateMany: vi.fn().mockResolvedValue({ count: 1 }), findMany: vi.fn().mockResolvedValue([]), update: vi.fn() },
|
||||
lifeEvent: { create: vi.fn().mockResolvedValue({ id: 'e-1', status: 'approved' }), update: vi.fn(), updateMany: vi.fn().mockResolvedValue({ count: 1 }) },
|
||||
contributionSession: { create: vi.fn() },
|
||||
contributionChange: { create: vi.fn() },
|
||||
auditLog: { create: vi.fn() },
|
||||
})
|
||||
|
||||
it('lot d’annotations : créations, modifications, suppressions et trace dans la même transaction', async () => {
|
||||
const app = createApp()
|
||||
const auth = { authorization: `Bearer ${await createUserToken(app)}` }
|
||||
const tx = txClient()
|
||||
prismaMock.$transaction.mockImplementationOnce(async (callback: (client: unknown) => Promise<unknown>) => callback(tx))
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST', url: '/trees/tree-1/annotations/batch', headers: auth,
|
||||
payload: { create: [{ type: 'text', x: 0, y: 0, content: 'Été 1952' }, { type: 'text', x: 1, y: 1, content: 'Noël' }], update: [{ id: 'a-1', x: 5 }], delete: ['a-2'] },
|
||||
})
|
||||
expect(response.statusCode).toBe(200)
|
||||
expect(response.json()).toEqual({ ok: true, created: 2, updated: 1, deleted: 1 })
|
||||
expect(tx.annotation.create).toHaveBeenCalledTimes(2)
|
||||
expect(tx.annotation.updateMany).toHaveBeenCalledTimes(2)
|
||||
expect(tx.auditLog.create).toHaveBeenCalledTimes(1)
|
||||
expect(prismaMock.annotation.create).not.toHaveBeenCalled()
|
||||
expect(prismaMock.annotation.updateMany).not.toHaveBeenCalled()
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('lot d’annotations : une erreur au milieu fait échouer toute la transaction', async () => {
|
||||
const app = createApp()
|
||||
const auth = { authorization: `Bearer ${await createUserToken(app)}` }
|
||||
const tx = txClient()
|
||||
tx.annotation.create.mockResolvedValueOnce({ id: 'a' }).mockRejectedValueOnce(new Error('base indisponible'))
|
||||
prismaMock.$transaction.mockImplementationOnce(async (callback: (client: unknown) => Promise<unknown>) => callback(tx))
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST', url: '/trees/tree-1/annotations/batch', headers: auth,
|
||||
payload: { create: [{ type: 'text', x: 0, y: 0, content: 'a' }, { type: 'text', x: 1, y: 1, content: 'b' }] },
|
||||
})
|
||||
expect(response.statusCode).toBe(500)
|
||||
expect(tx.auditLog.create).not.toHaveBeenCalled()
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('réordonner les souvenirs : positions, renumérotation et trace dans la même transaction', async () => {
|
||||
const app = createApp()
|
||||
const auth = { authorization: `Bearer ${await createUserToken(app)}` }
|
||||
const tx = txClient()
|
||||
prismaMock.mediaItem.findMany.mockResolvedValueOnce([{ id: 'm-1' }, { id: 'm-2' }])
|
||||
prismaMock.$transaction.mockImplementationOnce(async (callback: (client: unknown) => Promise<unknown>) => callback(tx))
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'PATCH', url: '/trees/tree-1/persons/person-1/media/reorder', headers: auth,
|
||||
payload: { orderedMediaIds: ['m-2', 'm-1'] },
|
||||
})
|
||||
expect(response.statusCode).toBe(200)
|
||||
expect(tx.mediaItem.updateMany).toHaveBeenCalledTimes(2)
|
||||
expect(tx.auditLog.create).toHaveBeenCalledTimes(1)
|
||||
expect(prismaMock.mediaItem.updateMany).not.toHaveBeenCalled()
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('moment : création et rattachement des souvenirs dans la même transaction', async () => {
|
||||
const app = createApp()
|
||||
const auth = { authorization: `Bearer ${await createUserToken(app)}` }
|
||||
const tx = txClient()
|
||||
prismaMock.mediaItem.count.mockResolvedValue(1)
|
||||
prismaMock.$transaction.mockImplementationOnce(async (callback: (client: unknown) => Promise<unknown>) => callback(tx))
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'POST', url: '/trees/tree-1/persons/person-1/life/events', headers: auth,
|
||||
payload: { label: 'Départ pour Paris', mediaIds: ['m-1'] },
|
||||
})
|
||||
expect(response.statusCode).toBe(201)
|
||||
expect(tx.lifeEvent.create).toHaveBeenCalledTimes(1)
|
||||
expect(tx.mediaItem.updateMany).toHaveBeenCalledWith({ where: { treeId: 'tree-1', id: { in: ['m-1'] } }, data: { lifeEventId: 'e-1' } })
|
||||
expect(prismaMock.lifeEvent.create).not.toHaveBeenCalled()
|
||||
await app.close()
|
||||
})
|
||||
|
||||
it('la famille ne prend que des souvenirs libres, l’administrateur peut déplacer un souvenir d’un moment à l’autre', async () => {
|
||||
const app = createApp()
|
||||
const auth = { authorization: `Bearer ${await createUserToken(app)}` }
|
||||
const post = () => app.inject({ method: 'POST', url: '/trees/tree-1/persons/person-1/life/events', headers: auth, payload: { label: 'Communion', mediaIds: ['m-1'] } })
|
||||
prismaMock.mediaItem.count.mockResolvedValue(1)
|
||||
prismaMock.lifeEvent.create.mockResolvedValue({ id: 'e-1', status: 'approved' })
|
||||
|
||||
expect((await post()).statusCode).toBe(201)
|
||||
expect(prismaMock.mediaItem.count.mock.calls.at(-1)[0].where).not.toHaveProperty('lifeEventId')
|
||||
|
||||
prismaMock.treeMembership.findUnique.mockResolvedValue({ role: 'member', tree: { deletedAt: null, settings: null } })
|
||||
prismaMock.contributionSession.create.mockResolvedValue({ id: 's-1' })
|
||||
expect((await post()).statusCode).toBe(201)
|
||||
expect(prismaMock.mediaItem.count.mock.calls.at(-1)[0].where).toMatchObject({ lifeEventId: null })
|
||||
await app.close()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Visit log', () => {
|
||||
it('refuses the visit log to a non-admin member', async () => {
|
||||
const app = createApp()
|
||||
|
||||
Reference in new issue
Block a user