test: tests Vitest pour les extensions Directus (18 tests, 0 échec)

- disallow-votes (13 tests) : mock knex chaînable + VersionsService
  - versionId manquant, version introuvable, version > 3j, version obsolète
  - échec non bloquant de compare(), collection ignorée si ≠ votes
  - items.delete : voteId manquant, vote introuvable, version associée ancienne
- new-user (5 tests) : mock MailService + database
  - MailService absent, EMAIL_NEW_USER absent, email déjà utilisé
  - envoi e-mail admin, fallback URL par défaut, erreur SMTP non bloquante
- vitest.config.mjs : pointe sur extensions/*/src/__tests__/**/*.test.js

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 06:35:01 +04:00
parent d59972af91
commit 0e56909626
5 changed files with 1496 additions and 275 deletions
@@ -0,0 +1,231 @@
import {describe, it, expect, vi, beforeEach} from 'vitest'
import hookFactory from '../index.js'
// Helpers pour construire les mocks Directus ---------------------------------
/**
* Construit un mock de database (knex) chaînable.
* Chaque méthode retourne `this` sauf `.first()` qui résout la valeur fournie.
*/
const makeDatabase = (resolvedRow) => {
const db = vi.fn(() => db)
db.select = vi.fn(() => db)
db.where = vi.fn(() => db)
db.first = vi.fn(() => Promise.resolve(resolvedRow))
return db
}
/**
* Construit un mock de VersionsService.
* `compareResult` est la valeur renvoyée par `compare()`.
*/
const makeVersionsService = (compareResult) => ({
VersionsService: class {
compare() {
return Promise.resolve(compareResult)
}
},
})
/**
* Enregistre les callbacks du hook et les expose par événement.
* Permet d'appeler directement `callbacks['items.create'](input, meta, ctx)`.
*/
const mountHook = (services) => {
const callbacks = {}
const filter = (event, handler) => {
callbacks[event] = handler
}
hookFactory({filter}, {services})
return callbacks
}
// Dates de référence ---------------------------------------------------------
const RECENT_DATE = new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString() // hier
const OLD_DATE = new Date(Date.now() - 4 * 24 * 60 * 60 * 1000).toISOString() // il y a 4 jours
const SCHEMA = {}
// Tests ----------------------------------------------------------------------
describe('disallow-votes — items.create', () => {
let callbacks
beforeEach(() => {
callbacks = mountHook(makeVersionsService({outdated: false}))
})
it('lève une erreur si versionId est absent', async () => {
const db = makeDatabase(null)
await expect(
callbacks['items.create']({}, {collection: 'votes'}, {database: db, schema: SCHEMA})
).rejects.toThrow('identifiant de la version est manquant')
})
it('lève une erreur si la version est introuvable', async () => {
const db = makeDatabase(undefined)
await expect(
callbacks['items.create'](
{content_version_id: 'abc'},
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
).rejects.toThrow('Version non trouvée')
})
it('lève une erreur si la version a plus de 3 jours', async () => {
const db = makeDatabase({date_created: OLD_DATE})
await expect(
callbacks['items.create'](
{content_version_id: 'abc'},
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
).rejects.toThrow('3 jours')
})
it('lève une erreur si la version est obsolète', async () => {
const db = makeDatabase({date_created: RECENT_DATE})
callbacks = mountHook(makeVersionsService({outdated: true}))
await expect(
callbacks['items.create'](
{content_version_id: 'abc'},
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
).rejects.toThrow('version obsolète')
})
it('autorise le vote si la version est récente et valide', async () => {
const db = makeDatabase({date_created: RECENT_DATE})
const input = {content_version_id: 'abc'}
const result = await callbacks['items.create'](
input,
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
expect(result).toBe(input)
})
it('autorise le vote si compare() échoue (non bloquant)', async () => {
const db = makeDatabase({date_created: RECENT_DATE})
const servicesWithBrokenCompare = {
VersionsService: class {
compare() {
return Promise.reject(new Error('service indisponible'))
}
},
}
callbacks = mountHook(servicesWithBrokenCompare)
const input = {content_version_id: 'abc'}
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
const result = await callbacks['items.create'](
input,
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
expect(result).toBe(input)
expect(warnSpy).toHaveBeenCalled()
warnSpy.mockRestore()
})
it('ignore les collections qui ne sont pas "votes"', async () => {
const db = makeDatabase(null)
const input = {content_version_id: 'abc'}
const result = await callbacks['items.create'](
input,
{collection: 'articles'},
{database: db, schema: SCHEMA},
)
expect(result).toBe(input)
expect(db).not.toHaveBeenCalled()
})
})
describe('disallow-votes — items.update', () => {
it('applique la même logique que items.create', async () => {
const db = makeDatabase({date_created: OLD_DATE})
const callbacks = mountHook(makeVersionsService({outdated: false}))
await expect(
callbacks['items.update'](
{content_version_id: 'abc'},
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
).rejects.toThrow('3 jours')
})
})
describe('disallow-votes — items.delete', () => {
it('lève une erreur si voteId est absent', async () => {
const db = makeDatabase(null)
const callbacks = mountHook(makeVersionsService({outdated: false}))
await expect(
callbacks['items.delete']([], {collection: 'votes'}, {database: db, schema: SCHEMA})
).rejects.toThrow('identifiant du vote est manquant')
})
it('lève une erreur si le vote est introuvable', async () => {
const db = makeDatabase(undefined)
const callbacks = mountHook(makeVersionsService({outdated: false}))
await expect(
callbacks['items.delete'](['vote-1'], {collection: 'votes'}, {database: db, schema: SCHEMA})
).rejects.toThrow('Vote non trouvé')
})
it('lève une erreur si la version associée au vote a plus de 3 jours', async () => {
let callCount = 0
const db = vi.fn(() => db)
db.select = vi.fn(() => db)
db.where = vi.fn(() => db)
db.first = vi.fn(() => {
callCount++
// Premier appel : lookup du vote → retourne content_version_id
// Deuxième appel : lookup de la version → retourne date ancienne
return Promise.resolve(
callCount === 1
? {content_version_id: 'v-abc'}
: {date_created: OLD_DATE}
)
})
const callbacks = mountHook(makeVersionsService({outdated: false}))
await expect(
callbacks['items.delete'](['vote-1'], {collection: 'votes'}, {database: db, schema: SCHEMA})
).rejects.toThrow('3 jours')
})
it('autorise la suppression si la version est encore valide', async () => {
let callCount = 0
const db = vi.fn(() => db)
db.select = vi.fn(() => db)
db.where = vi.fn(() => db)
db.first = vi.fn(() => {
callCount++
return Promise.resolve(
callCount === 1
? {content_version_id: 'v-abc'}
: {date_created: RECENT_DATE}
)
})
const callbacks = mountHook(makeVersionsService({outdated: false}))
const input = ['vote-1']
const result = await callbacks['items.delete'](
input,
{collection: 'votes'},
{database: db, schema: SCHEMA},
)
expect(result).toBe(input)
})
})
@@ -0,0 +1,139 @@
import {describe, it, expect, vi, beforeEach} from 'vitest'
import hookFactory from '../index.js'
// Helpers --------------------------------------------------------------------
const makeDatabase = (existingUser) => {
const db = vi.fn(() => db)
db.select = vi.fn(() => db)
db.where = vi.fn(() => db)
db.first = vi.fn(() => Promise.resolve(existingUser))
return db
}
/**
* Monte le hook et capture le callback `users.create`.
*/
const mountHook = ({services, env}) => {
let callback
const filter = (event, handler) => {
if (event === 'users.create') {
callback = handler
}
}
hookFactory({filter}, {services, env})
return callback
}
const SCHEMA = {}
const BASE_ENV = {
EMAIL_NEW_USER: 'admin@example.com',
DIRECTUS_URL: 'http://localhost:8055',
}
// Tests ----------------------------------------------------------------------
describe('new-user — users.create', () => {
let sendMock
let services
beforeEach(() => {
sendMock = vi.fn()
services = {
MailService: class {
send(options) {
return sendMock(options)
}
},
}
})
it('renvoie input sans envoyer d\'e-mail si MailService est absent', async () => {
const db = makeDatabase(undefined)
const callback = mountHook({services: {}, env: BASE_ENV})
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const input = {email: 'user@example.com'}
const result = await callback(input, {schema: SCHEMA}, {database: db})
expect(result).toBe(input)
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('MailService'))
consoleSpy.mockRestore()
})
it('renvoie input sans envoyer d\'e-mail si EMAIL_NEW_USER est absent', async () => {
const db = makeDatabase(undefined)
const callback = mountHook({services, env: {}})
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const input = {email: 'user@example.com'}
const result = await callback(input, {schema: SCHEMA}, {database: db})
expect(result).toBe(input)
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('EMAIL_NEW_USER'))
consoleSpy.mockRestore()
})
it('renvoie input sans envoyer d\'e-mail si l\'adresse existe déjà', async () => {
const db = makeDatabase({id: 'existing-user-id'}) // utilisateur trouvé
const callback = mountHook({services, env: BASE_ENV})
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const input = {email: 'existing@example.com'}
const result = await callback(input, {schema: SCHEMA}, {database: db})
expect(result).toBe(input)
expect(sendMock).not.toHaveBeenCalled()
consoleSpy.mockRestore()
})
it('envoie un e-mail à l\'admin pour un nouvel utilisateur valide', async () => {
const db = makeDatabase(undefined) // aucun utilisateur existant
const callback = mountHook({services, env: BASE_ENV})
const input = {email: 'new@example.com', first_name: 'Alice'}
const result = await callback(input, {schema: SCHEMA}, {database: db})
expect(result).toBe(input)
expect(sendMock).toHaveBeenCalledOnce()
const callArgs = sendMock.mock.calls[0][0]
expect(callArgs.to).toBe('admin@example.com')
expect(callArgs.subject).toContain('new@example.com')
})
it('utilise l\'URL Directus par défaut si DIRECTUS_URL est absent', async () => {
const db = makeDatabase(undefined)
const envWithoutUrl = {EMAIL_NEW_USER: 'admin@example.com'}
const callback = mountHook({services, env: envWithoutUrl})
const input = {email: 'new@example.com'}
await callback(input, {schema: SCHEMA}, {database: db})
const callArgs = sendMock.mock.calls[0][0]
expect(callArgs.html).toContain('http://0.0.0.0:8055')
})
it('renvoie input si l\'envoi de mail échoue (non bloquant)', async () => {
const db = makeDatabase(undefined)
const brokenServices = {
MailService: class {
send() {
throw new Error('SMTP error')
}
},
}
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const callback = mountHook({services: brokenServices, env: BASE_ENV})
const input = {email: 'new@example.com'}
const result = await callback(input, {schema: SCHEMA}, {database: db})
expect(result).toBe(input)
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Erreur'),
expect.any(Error),
)
consoleSpy.mockRestore()
})
})
+1110 -273
View File
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -2,8 +2,10 @@
"name": "api.konstitisyon.nu",
"version": "1.0.0",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "vitest run",
"test:watch": "vitest"
},
"keywords": [],
"author": "",
@@ -12,5 +14,8 @@
"dependencies": {
"directus": "^11.2.1",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"vitest": "^4.1.4"
}
}
+9
View File
@@ -0,0 +1,9 @@
import {defineConfig} from 'vitest/config'
export default defineConfig({
test: {
globals: false,
environment: 'node',
include: ['extensions/*/src/__tests__/**/*.test.js'],
},
})