fix: déplacer le miroir bokante de beforeUpdate vers afterCreate

Avec draftAndPublish sur Strapi 5, publier une entrée crée une
nouvelle ligne (document-service publishEntry -> createEntry) au lieu
de mettre à jour la ligne existante : beforeUpdate ne se déclenche
donc jamais sur une vraie action Publier, seulement sur l'édition
d'une entrée déjà publiée. Le miroir bokante est maintenant posté
dans afterCreate, sur la condition result.publishedAt, avec
synchronisation brouillon/publié pour rester idempotent à travers les
cycles dépublier/republier.
This commit is contained in:
2026-07-05 00:59:59 +04:00
parent 861e75fde2
commit 26471d8b0e
2 changed files with 64 additions and 44 deletions
@@ -118,14 +118,14 @@ describe('beforeUpdate — notifications Telegram/Revolt', () => {
});
});
describe('beforeUpdate — publication miroir bokante', () => {
describe('afterCreate — publication miroir bokante', () => {
const originalEnv = {...process.env};
const bokanteMastodon = require('../../../../../utils/bokante-mastodon');
const originalCreateStatus = bokanteMastodon.createStatus;
function buildStrapi(previous) {
function buildStrapi() {
const dbQuery = {
findOne: vi.fn(async () => previous),
findOne: vi.fn(async () => null),
updateMany: vi.fn()
};
@@ -136,11 +136,16 @@ describe('beforeUpdate — publication miroir bokante', () => {
};
}
function buildEvent(overrides = {}) {
function buildEvent(resultOverrides = {}) {
return {
state: {},
params: {
data: {documentId: 'doc-1', publishedAt: '2026-07-04T00:00:00.000Z', ...overrides}
params: {data: {titre: 'Mon titre'}},
result: {
documentId: 'doc-1',
titre: 'Mon titre',
slug: 'mon-titre',
publishedAt: '2026-07-04T00:00:00.000Z',
bokanteStatusId: null,
...resultOverrides
}
};
}
@@ -151,33 +156,43 @@ describe('beforeUpdate — publication miroir bokante', () => {
delete global.strapi;
});
it('publie un statut miroir et stocke bokanteStatusId sur data', async () => {
it('publie un statut miroir et synchronise bokanteStatusId sur le documentId', async () => {
process.env.BOKANTE_ACCESS_TOKEN = 'fake-token';
bokanteMastodon.createStatus = vi.fn(async () => ({id: '112233'}));
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: null};
const strapiMock = buildStrapi(previous);
const strapiMock = buildStrapi();
const {afterCreate} = await loadLifecycles(strapiMock);
const {beforeUpdate} = await loadLifecycles(strapiMock);
const event = buildEvent();
await beforeUpdate(event);
await afterCreate(buildEvent());
expect(bokanteMastodon.createStatus).toHaveBeenCalledTimes(1);
expect(bokanteMastodon.createStatus.mock.calls[0][0]).toContain('Mon titre');
expect(event.params.data.bokanteStatusId).toBe('112233');
expect(strapiMock.db.query('api::parole.parole').updateMany).toHaveBeenCalledWith({
where: {documentId: 'doc-1'},
data: {bokanteStatusId: '112233'}
});
});
it('ne publie rien si la ligne créée n\'est pas publiée (simple brouillon)', async () => {
process.env.BOKANTE_ACCESS_TOKEN = 'fake-token';
bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'}));
const strapiMock = buildStrapi();
const {afterCreate} = await loadLifecycles(strapiMock);
await afterCreate(buildEvent({publishedAt: null}));
expect(bokanteMastodon.createStatus).not.toHaveBeenCalled();
});
it('ne republie pas si bokanteStatusId existe déjà', async () => {
process.env.BOKANTE_ACCESS_TOKEN = 'fake-token';
bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'}));
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: '112233'};
const strapiMock = buildStrapi(previous);
const strapiMock = buildStrapi();
const {afterCreate} = await loadLifecycles(strapiMock);
const {beforeUpdate} = await loadLifecycles(strapiMock);
await beforeUpdate(buildEvent());
await afterCreate(buildEvent({bokanteStatusId: '112233'}));
expect(bokanteMastodon.createStatus).not.toHaveBeenCalled();
});
@@ -186,32 +201,27 @@ describe('beforeUpdate — publication miroir bokante', () => {
delete process.env.BOKANTE_ACCESS_TOKEN;
bokanteMastodon.createStatus = vi.fn(async () => ({id: '999'}));
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: null};
const strapiMock = buildStrapi(previous);
const strapiMock = buildStrapi();
const {afterCreate} = await loadLifecycles(strapiMock);
const {beforeUpdate} = await loadLifecycles(strapiMock);
await beforeUpdate(buildEvent());
await afterCreate(buildEvent());
expect(bokanteMastodon.createStatus).not.toHaveBeenCalled();
});
it('n\'interrompt pas la publication quand bokante échoue', async () => {
it('n\'interrompt pas la création quand bokante échoue', async () => {
process.env.BOKANTE_ACCESS_TOKEN = 'fake-token';
bokanteMastodon.createStatus = vi.fn(async () => {
throw new Error('boom');
});
const previous = {publishedAt: null, slug: 'mon-titre', titre: 'Mon titre', user: null, userAdmin: null, artistes: [], bokanteStatusId: null};
const strapiMock = buildStrapi(previous);
const strapiMock = buildStrapi();
const {afterCreate} = await loadLifecycles(strapiMock);
const {beforeUpdate} = await loadLifecycles(strapiMock);
const event = buildEvent();
await expect(beforeUpdate(event)).resolves.not.toThrow();
await expect(afterCreate(buildEvent())).resolves.not.toThrow();
expect(strapiMock.log.error).toHaveBeenCalledWith(expect.stringContaining('bokante'));
expect(event.params.data.bokanteStatusId).toBeUndefined();
expect(strapiMock.db.query('api::parole.parole').updateMany).not.toHaveBeenCalled();
});
});
@@ -192,17 +192,6 @@ module.exports = {
const previousPublishedAt = previousData.publishedAt;
const currentPublished_at = data.publishedAt;
if (currentPublished_at != previousPublishedAt) {
if (!previousData.bokanteStatusId && process.env.BOKANTE_ACCESS_TOKEN) {
try {
const status = await bokanteMastodon.createStatus(
`"${previousData.titre}" — nouvelle parole sur pawol.nu\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`
);
data.bokanteStatusId = status.id;
} catch (err) {
strapi.log.error(`Publication bokante : ${err.message}`);
}
}
const message = `<b>Nouvelle publication</b> ❤️
\n${process.env.WEBSITE_URL}/paroles/${previousData.slug}`;
if (previousData.user) {
@@ -284,6 +273,27 @@ module.exports = {
},
afterCreate: async event => {
const {data} = event.params;
// Avec draftAndPublish, "publier" crée une nouvelle ligne (la version publiée)
// au lieu de mettre à jour la ligne existante : c'est ici, et non dans
// beforeUpdate, qu'un événement de publication est détectable.
if (event.result?.publishedAt && !event.result?.bokanteStatusId && process.env.BOKANTE_ACCESS_TOKEN) {
try {
const status = await bokanteMastodon.createStatus(
`"${event.result.titre}" — nouvelle parole sur pawol.nu\n${process.env.WEBSITE_URL}/paroles/${event.result.slug}`
);
// Synchronise brouillon et version publiée pour éviter une republication
// en double au prochain cycle dépublier/republier (qui recrée la ligne
// publiée à partir du brouillon).
await strapi.db.query('api::parole.parole').updateMany({
where: {documentId: event.result.documentId},
data: {bokanteStatusId: status.id}
});
} catch (err) {
strapi.log.error(`Publication bokante : ${err.message}`);
}
}
const user = await jwennUserEpiId(data?.user?.id);
const userAdmin = await jwennUserAdminEpiId(data?.createdBy);
const superAdmin = await jwennSuperAdminEpiId(data?.createdBy);