diff --git a/backend/__tests__/routes/adminFeedbackWordFilterSeverity.test.js b/backend/__tests__/routes/adminFeedbackWordFilterSeverity.test.js new file mode 100644 index 00000000..a7329ce7 --- /dev/null +++ b/backend/__tests__/routes/adminFeedbackWordFilterSeverity.test.js @@ -0,0 +1,72 @@ +/** + * Word-filter severity vocabulary. The Settings → Moderation UI offers + * low / moderate / high / block, but the validator only accepted the + * unrelated mild / moderate / severe set, so 3 of the 4 levels — including + * "block", the strongest tier — 400'd on every add. + */ +const path = require('path'); +const fs = require('fs'); +const os = require('os'); + +process.env.NODE_ENV = 'test'; +process.env.TEST_DATABASE_PATH = path.join( + fs.mkdtempSync(path.join(os.tmpdir(), 'picpeak-wfsev-')), 'db.sqlite', +); +process.env.JWT_SECRET = process.env.JWT_SECRET || 'wfsev-test-secret'; +process.env.STORAGE_PATH = fs.mkdtempSync(path.join(os.tmpdir(), 'picpeak-wfsev-storage-')); + +const request = require('supertest'); +const express = require('express'); +const cookieParser = require('cookie-parser'); +const { bootCrmDb, seedMinimal, assignAdminRole, mintAdminToken } = require('../integration/helpers/crmDb'); + +describe('word filter severity levels', () => { + let db; let cleanup; let app; let superTok; + + const auth = (req) => req.set('Authorization', `Bearer ${superTok}`); + + beforeAll(async () => { + ({ db, cleanup } = await bootCrmDb()); + const { adminId: superId } = await seedMinimal(db); + await assignAdminRole(db, superId, 'super_admin'); + superTok = mintAdminToken(superId); + + app = express(); + app.use(express.json()); + app.use(cookieParser()); + app.use('/api/admin/feedback', require('../../src/routes/adminFeedback')); + }, 120000); + + afterAll(async () => { if (cleanup) await cleanup(); }); + + it.each(['low', 'moderate', 'high', 'block'])('accepts severity "%s"', async (severity) => { + const word = `zzsev${severity}`; + const res = await auth(request(app).post('/api/admin/feedback/word-filters')) + .send({ word, severity }); + + expect(res.status).toBe(200); + const row = await db('feedback_word_filters').where({ word }).first(); + expect(row.severity).toBe(severity); + }); + + it('still rejects a severity outside the vocabulary', async () => { + const res = await auth(request(app).post('/api/admin/feedback/word-filters')) + .send({ word: 'zzsevbogus', severity: 'catastrophic' }); + + expect(res.status).toBe(400); + expect(res.body.errors.some((e) => e.path === 'severity')).toBe(true); + }); + + it('blocks a comment matching a "block" filter and only flags a "low" one', async () => { + const moderation = require('../../src/services/feedbackModeration'); + moderation.clearCache(); + + const blocked = await moderation.moderateText('this is zzsevblock speech'); + expect(blocked.approved).toBe(false); + expect(blocked.violations.map((v) => v.word)).toEqual(['zzsevblock']); + + const flagged = await moderation.moderateText('this is zzsevlow speech'); + expect(flagged.approved).toBe(true); + expect(flagged.flagged).toBe(true); + }); +}); diff --git a/backend/src/services/feedbackModeration.js b/backend/src/services/feedbackModeration.js index 73da47ce..df9babb0 100644 --- a/backend/src/services/feedbackModeration.js +++ b/backend/src/services/feedbackModeration.js @@ -66,17 +66,20 @@ class FeedbackModerationService { } } - // Check for severe violations - if (violations.some(v => v.severity === 'severe')) { + // Check for blocking violations. 'severe' is the legacy vocabulary the + // validator used to accept before it was aligned with the UI's + // low/moderate/high/block levels — rows stored under it still apply. + const isBlocking = (v) => v.severity === 'block' || v.severity === 'severe'; + if (violations.some(isBlocking)) { return { approved: false, reason: 'Content contains prohibited words', - violations: violations.filter(v => v.severity === 'severe') + violations: violations.filter(isBlocking) }; } - - // Check for moderate violations - if (violations.some(v => v.severity === 'moderate')) { + + // Check for moderate/high violations + if (violations.some(v => v.severity === 'moderate' || v.severity === 'high')) { return { approved: false, reason: 'Content requires moderation', @@ -84,7 +87,7 @@ class FeedbackModerationService { }; } - // Check for mild violations (may just flag for review) + // Check for low-severity violations (may just flag for review) if (violations.length > 0) { return { approved: true, diff --git a/backend/src/utils/feedbackValidation.js b/backend/src/utils/feedbackValidation.js index bd684ab7..2dc77b69 100644 --- a/backend/src/utils/feedbackValidation.js +++ b/backend/src/utils/feedbackValidation.js @@ -241,7 +241,7 @@ const validateWordFilter = [ .withMessage('Word must be between 2 and 100 characters'), body('severity') .optional() - .isIn(['mild', 'moderate', 'severe']) + .isIn(['low', 'moderate', 'high', 'block']) .withMessage('Invalid severity level') ];