Apply installation policy changes once each, per account
The last third of #207, and the only part that remembers anything. An admin turns a setting on for people who are already here -- which a default cannot do, since a default only seeds an account that has none -- and readers may still turn it back off afterwards, which enforcement does not allow. The difference between the two is entirely in the remembering. Each change carries its own version, and an account stores the ones it has had in its own settings file. Ids rather than a high-water mark, so a change dated earlier than one already applied is not silently skipped -- the reporter's analogy is a schema migration, and this is that shape. Per account rather than per device, because ihasmail's settings are not browser-local: they live in a file in the reader's own JMAP Files, with the browser holding a cache. Signing in on a phone does not apply everything a second time. A change reaches somebody who had already decided otherwise. That is intended and confirmed on the issue: the point is to reach everybody who is already here. It is applied once, and their next decision sticks. One `update` for however many are pending, since each would otherwise push a settings file of its own. Enforced values still win, being applied after. A change whose settings this build does not have at all is dropped rather than recorded, or it would never run on the ihasmail that does have them. The reader is told. A setting moving under somebody without a word is the part of this worth being uneasy about, so the count is toasted with a way into Settings. README gains the Docker half the user asked for: a mounted policy file, the same thing as environment variables for a deployment with no volume, a compose fragment, and the fact that a policy is read once at startup so editing it means a restart. Closes #207.
This commit is contained in:
@@ -122,3 +122,96 @@ describe("reset, where the installation has chosen defaults", () => {
|
||||
expect(useSettings.getState().settings.conversationMode).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* The third power: applied once each, to everybody, and changeable afterwards.
|
||||
*
|
||||
* The difference from `enforced` is entirely in the remembering. Both reach an
|
||||
* account that already exists; only this one lets the reader have the last
|
||||
* word, and only because the version is stored.
|
||||
*/
|
||||
describe("changes an installation wants applied once", () => {
|
||||
const change = (version: string, settings: Record<string, unknown>) => ({ version, settings } as never);
|
||||
|
||||
it("applies one the account has not had", () => {
|
||||
resetSettingsPolicyForTest({ changes: [change("20260902", { conversationMode: false })] });
|
||||
const applied = useSettings.getState().applyPolicyChanges();
|
||||
expect(applied.map((c) => c.version)).toEqual(["20260902"]);
|
||||
expect(useSettings.getState().settings.conversationMode).toBe(false);
|
||||
});
|
||||
|
||||
it("remembers it, so the next sign-in does not do it again", () => {
|
||||
resetSettingsPolicyForTest({ changes: [change("20260902", { conversationMode: false })] });
|
||||
useSettings.getState().applyPolicyChanges();
|
||||
// The reader decides otherwise, which is the whole difference from enforcing.
|
||||
useSettings.getState().update({ conversationMode: true });
|
||||
expect(useSettings.getState().applyPolicyChanges()).toEqual([]);
|
||||
expect(useSettings.getState().settings.conversationMode).toBe(true);
|
||||
});
|
||||
|
||||
it("reaches an account that had already chosen otherwise", () => {
|
||||
/*
|
||||
* Confirmed as intended on #207: the point is to reach everybody who is
|
||||
* already here, so somebody who turned it off last week does get it turned
|
||||
* back on -- once.
|
||||
*/
|
||||
useSettings.getState().update({ conversationMode: false });
|
||||
resetSettingsPolicyForTest({ changes: [change("20260902", { conversationMode: true })] });
|
||||
useSettings.getState().applyPolicyChanges();
|
||||
expect(useSettings.getState().settings.conversationMode).toBe(true);
|
||||
});
|
||||
|
||||
it("applies only the ones that are new, keeping what it has seen", () => {
|
||||
resetSettingsPolicyForTest({ changes: [change("A", { conversationMode: false })] });
|
||||
useSettings.getState().applyPolicyChanges();
|
||||
resetSettingsPolicyForTest({
|
||||
changes: [change("A", { conversationMode: false }), change("B", { showAvatars: false })],
|
||||
});
|
||||
const applied = useSettings.getState().applyPolicyChanges();
|
||||
expect(applied.map((c) => c.version)).toEqual(["B"]);
|
||||
expect(useSettings.getState().settings.appliedPolicyChanges).toEqual(["A", "B"]);
|
||||
});
|
||||
|
||||
it("does not skip a change dated earlier than one already applied", () => {
|
||||
// Ids, not a high-water mark. An admin backfilling a change must not find
|
||||
// it silently ignored because a later one went first.
|
||||
resetSettingsPolicyForTest({ changes: [change("20260902", { conversationMode: false })] });
|
||||
useSettings.getState().applyPolicyChanges();
|
||||
resetSettingsPolicyForTest({
|
||||
changes: [change("20260101", { showAvatars: false }), change("20260902", { conversationMode: false })],
|
||||
});
|
||||
expect(useSettings.getState().applyPolicyChanges().map((c) => c.version)).toEqual(["20260101"]);
|
||||
expect(useSettings.getState().settings.showAvatars).toBe(false);
|
||||
});
|
||||
|
||||
it("goes out as one write however many changes are pending", () => {
|
||||
resetSettingsPolicyForTest({
|
||||
changes: [change("A", { conversationMode: false }), change("B", { showAvatars: false })],
|
||||
});
|
||||
const applied = useSettings.getState().applyPolicyChanges();
|
||||
expect(applied).toHaveLength(2);
|
||||
expect(useSettings.getState().settings.conversationMode).toBe(false);
|
||||
expect(useSettings.getState().settings.showAvatars).toBe(false);
|
||||
});
|
||||
|
||||
it("does nothing, and says so, when there are none", () => {
|
||||
expect(useSettings.getState().applyPolicyChanges()).toEqual([]);
|
||||
});
|
||||
|
||||
it("cannot undo an enforced setting, which outranks it", () => {
|
||||
resetSettingsPolicyForTest({
|
||||
enforced: { conversationMode: true } as never,
|
||||
changes: [change("A", { conversationMode: false })],
|
||||
});
|
||||
useSettings.getState().applyPolicyChanges();
|
||||
expect(useSettings.getState().settings.conversationMode).toBe(true);
|
||||
});
|
||||
|
||||
it("drops a change whose settings this build does not have, rather than recording it", () => {
|
||||
// Recording it as applied would mean it never runs on the ihasmail that
|
||||
// does have the setting.
|
||||
resetSettingsPolicyForTest({ changes: [change("A", { notARealSetting: true })] });
|
||||
expect(useSettings.getState().applyPolicyChanges()).toEqual([]);
|
||||
expect(useSettings.getState().settings.appliedPolicyChanges).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user