fix(admin): let a setting whose default is empty actually be cleared (#280)
Linting / lint (pull_request) Successful in 8m44s
SonarQube Analysis / sonarqube (pull_request) Successful in 40m1s

intakeNotifyEmail and intakeCeilingResetAt both document empty as their default and as a working configuration — no notification address, and no ceiling reset recorded. The validator refused every empty text value, so either could be set and then never removed through the admin at all; the only way back was a DELETE against admin_settings. An admin who turned intake notifications on could not turn them off.

Whether empty is a mistake is a fact about the setting rather than about its type, so it is now declared on the setting, in the DEFINITIONS row that already carries its type and fallback. A new setting states it once, in the place someone adding one is already editing, and nothing else has to know. That is what makes this different from special-casing two names in the validator, which would have left the next such setting to rediscover the same bug.

The blanket refusal stays the default, because for a setting with a non-empty fallback an empty value really is a mistake: an empty greeting format renders every greeting as nothing at all, which reads as a broken email rather than as something a person cleared. Both those cases keep their tests.

Whitespace is normalised to empty rather than stored. Somebody clearing a field they cannot see the end of leaves spaces behind, and they meant cleared.

The tests check that the clearing survives the request rather than only being echoed back — the last one sets a value, clears it, and then reads it again through GET, which is the assertion that would have caught this had it existed.

Closes #280

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 16:51:32 -05:00
co-authored by Claude Opus 5
parent 9cc1ac1918
commit be6800181d
3 changed files with 78 additions and 7 deletions
@@ -95,6 +95,51 @@ describe('PUT /api/admin/settings', () => {
expect(res.body.error).toContain(name);
});
/**
* The other half of the same question (#280).
*
* These two settings document empty as their default and as a working
* configuration — no notification address, and no ceiling reset recorded. The
* blanket "text cannot be empty" rule meant an address could be set and then
* never removed through the admin at all, only by a DELETE against the table.
*/
it.each(['intakeNotifyEmail', 'intakeCeilingResetAt'])(
'lets %s be cleared, because empty is its documented default',
async (name) => {
const set = await request(app)
.put('/api/admin/settings')
.send({ [name]: name === 'intakeNotifyEmail' ? 'alerts@example.com' : '2026-09-01T00:00:00Z' });
expect(set.status).toBe(200);
expect(set.body[name]).not.toBe('');
const cleared = await request(app).put('/api/admin/settings').send({ [name]: '' });
expect(cleared.status).toBe(200);
expect(cleared.body[name]).toBe('');
}
);
// Whitespace is how a person clears a field they cannot see the end of, so it
// means cleared rather than being stored as spaces.
it('treats whitespace as cleared rather than storing it', async () => {
await request(app).put('/api/admin/settings').send({ intakeNotifyEmail: 'alerts@example.com' });
const res = await request(app).put('/api/admin/settings').send({ intakeNotifyEmail: ' ' });
expect(res.status).toBe(200);
expect(res.body.intakeNotifyEmail).toBe('');
});
// The clearing is real, not just echoed back in the response.
it('reads a cleared setting back as empty on a later request', async () => {
await request(app).put('/api/admin/settings').send({ intakeNotifyEmail: 'alerts@example.com' });
await request(app).put('/api/admin/settings').send({ intakeNotifyEmail: '' });
const res = await request(app).get('/api/admin/settings');
expect(res.body.intakeNotifyEmail).toBe('');
});
// Every field is validated before any is written, so a request that is part
// nonsense does not half-apply.
it('does not write anything when one field in the request is invalid', async () => {