@remit/drizzle-service 0.0.84 → 0.0.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -75,6 +75,137 @@ describe("concurrent flag merges on one address", () => {
|
|
|
75
75
|
await repo.deleteAddress(addr.accountConfigId, addr.addressId);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
test("marking a blocked sender never-spam drops the block", async () => {
|
|
79
|
+
const addr = await address();
|
|
80
|
+
await repo.mergeFlags(addr.accountConfigId, addr.addressId, {
|
|
81
|
+
blocked: { value: true, setAt: 1 },
|
|
82
|
+
muted: { value: true, setAt: 1 },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await repo.mergeFlags(addr.accountConfigId, addr.addressId, {
|
|
86
|
+
neverSpam: { value: true, setAt: 2 },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const merged = await repo.getAddress(addr.accountConfigId, addr.addressId);
|
|
90
|
+
assert.equal(merged.flags?.neverSpam?.value, true);
|
|
91
|
+
assert.equal(merged.flags?.blocked, undefined);
|
|
92
|
+
assert.equal(
|
|
93
|
+
merged.flags?.muted?.value,
|
|
94
|
+
true,
|
|
95
|
+
"a flag on another axis is untouched",
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
await repo.deleteAddress(addr.accountConfigId, addr.addressId);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("blocking a never-spam sender drops the grant", async () => {
|
|
102
|
+
const addr = await address();
|
|
103
|
+
await repo.mergeFlags(addr.accountConfigId, addr.addressId, {
|
|
104
|
+
neverSpam: { value: true, setAt: 1 },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await repo.mergeFlags(addr.accountConfigId, addr.addressId, {
|
|
108
|
+
blocked: { value: true, setAt: 2 },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const merged = await repo.getAddress(addr.accountConfigId, addr.addressId);
|
|
112
|
+
assert.equal(merged.flags?.blocked?.value, true);
|
|
113
|
+
assert.equal(merged.flags?.neverSpam, undefined);
|
|
114
|
+
|
|
115
|
+
await repo.deleteAddress(addr.accountConfigId, addr.addressId);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a never-spam grant turned off leaves a later block alone", async () => {
|
|
119
|
+
const addr = await address();
|
|
120
|
+
await repo.mergeFlags(addr.accountConfigId, addr.addressId, {
|
|
121
|
+
neverSpam: { value: false, setAt: 1 },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await repo.mergeFlags(addr.accountConfigId, addr.addressId, {
|
|
125
|
+
blocked: { value: true, setAt: 2 },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const merged = await repo.getAddress(addr.accountConfigId, addr.addressId);
|
|
129
|
+
assert.equal(merged.flags?.blocked?.value, true);
|
|
130
|
+
assert.equal(
|
|
131
|
+
merged.flags?.neverSpam?.value,
|
|
132
|
+
false,
|
|
133
|
+
"an explicit false is not a contradiction and keeps its audit trail",
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await repo.deleteAddress(addr.accountConfigId, addr.addressId);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// A row written before the invariant existed still carries both. Healing it
|
|
140
|
+
// while patching an unrelated key would revoke a placement instruction the
|
|
141
|
+
// user never touched, so the write leaves it and the read arbitrates.
|
|
142
|
+
test("a patch about another flag leaves a legacy both-set row alone", async () => {
|
|
143
|
+
const accountConfigId = randomId();
|
|
144
|
+
const addr = await repo.createAddress({
|
|
145
|
+
addressId: randomId(),
|
|
146
|
+
accountConfigId,
|
|
147
|
+
localPart: "legacy",
|
|
148
|
+
domain: "example.com",
|
|
149
|
+
normalizedEmail: "legacy@example.com",
|
|
150
|
+
normalizedCompound: "legacy@example.com:legacy",
|
|
151
|
+
flags: {
|
|
152
|
+
blocked: { value: true, setAt: 1 },
|
|
153
|
+
neverSpam: { value: true, setAt: 2 },
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await repo.mergeFlags(addr.accountConfigId, addr.addressId, {
|
|
158
|
+
muted: { value: true, setAt: 3 },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const merged = await repo.getAddress(addr.accountConfigId, addr.addressId);
|
|
162
|
+
assert.equal(merged.flags?.blocked?.value, true);
|
|
163
|
+
assert.equal(merged.flags?.neverSpam?.value, true);
|
|
164
|
+
assert.equal(merged.flags?.muted?.value, true);
|
|
165
|
+
|
|
166
|
+
await repo.deleteAddress(addr.accountConfigId, addr.addressId);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("writing either placement flag heals a legacy both-set row", async () => {
|
|
170
|
+
const accountConfigId = randomId();
|
|
171
|
+
const addr = await repo.createAddress({
|
|
172
|
+
addressId: randomId(),
|
|
173
|
+
accountConfigId,
|
|
174
|
+
localPart: "legacy",
|
|
175
|
+
domain: "example.com",
|
|
176
|
+
normalizedEmail: "legacy2@example.com",
|
|
177
|
+
normalizedCompound: "legacy2@example.com:legacy",
|
|
178
|
+
flags: {
|
|
179
|
+
blocked: { value: true, setAt: 1 },
|
|
180
|
+
neverSpam: { value: true, setAt: 2 },
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await repo.mergeFlags(addr.accountConfigId, addr.addressId, {
|
|
185
|
+
neverSpam: { value: true, setAt: 3 },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const merged = await repo.getAddress(addr.accountConfigId, addr.addressId);
|
|
189
|
+
assert.equal(merged.flags?.neverSpam?.value, true);
|
|
190
|
+
assert.equal(merged.flags?.blocked, undefined);
|
|
191
|
+
|
|
192
|
+
await repo.deleteAddress(addr.accountConfigId, addr.addressId);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("one patch naming both contradictory flags settles on the block", async () => {
|
|
196
|
+
const addr = await address();
|
|
197
|
+
await repo.mergeFlags(addr.accountConfigId, addr.addressId, {
|
|
198
|
+
blocked: { value: true, setAt: 1 },
|
|
199
|
+
neverSpam: { value: true, setAt: 1 },
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const merged = await repo.getAddress(addr.accountConfigId, addr.addressId);
|
|
203
|
+
assert.equal(merged.flags?.blocked?.value, true);
|
|
204
|
+
assert.equal(merged.flags?.neverSpam, undefined);
|
|
205
|
+
|
|
206
|
+
await repo.deleteAddress(addr.accountConfigId, addr.addressId);
|
|
207
|
+
});
|
|
208
|
+
|
|
78
209
|
test("a concurrent merge does not resurrect a deleted flag", async () => {
|
|
79
210
|
const addr = await address();
|
|
80
211
|
await repo.mergeFlags(addr.accountConfigId, addr.addressId, {
|
package/src/repos/i4-address.ts
CHANGED
|
@@ -70,6 +70,41 @@ type MergeAttempt =
|
|
|
70
70
|
| { outcome: "missing" }
|
|
71
71
|
| { outcome: "contended" };
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* `blocked` and `neverSpam` are two directly contradictory statements about
|
|
75
|
+
* where a sender's mail belongs (issue #605), so a row asserting both is not a
|
|
76
|
+
* tie to break — it is a row that should not exist. This is the only place that
|
|
77
|
+
* sees both keys at once: `buildFlagsPatch` stays a pure per-key translation,
|
|
78
|
+
* and the merge fold is the read-modify-write of the whole map.
|
|
79
|
+
*
|
|
80
|
+
* The flag the patch just raised wins, so the instruction the user gave last is
|
|
81
|
+
* the one that stands. A patch raising both resolves to `blocked` — the same
|
|
82
|
+
* direction the `blocked`/`vip` same-second tie already breaks in.
|
|
83
|
+
*
|
|
84
|
+
* Only a patch that raises one of the two is allowed to drop the other. A row
|
|
85
|
+
* that somehow already carries both survives a patch about anything else
|
|
86
|
+
* untouched: dropping a placement instruction as a side effect of writing
|
|
87
|
+
* `muted` would be a silent revocation the user never asked for. Such a row is
|
|
88
|
+
* resolved on read — `resolveSenderPlacement` reads it as `Blocked` — and
|
|
89
|
+
* healed the next time either key is written.
|
|
90
|
+
*/
|
|
91
|
+
const dropContradictedPlacementFlag = (
|
|
92
|
+
next: AddressFlags,
|
|
93
|
+
patch: FlagsMergePatch,
|
|
94
|
+
): AddressFlags => {
|
|
95
|
+
if (next.blocked?.value !== true || next.neverSpam?.value !== true)
|
|
96
|
+
return next;
|
|
97
|
+
if (patch.blocked?.value === true) {
|
|
98
|
+
const { neverSpam: _neverSpam, ...rest } = next;
|
|
99
|
+
return rest;
|
|
100
|
+
}
|
|
101
|
+
if (patch.neverSpam?.value === true) {
|
|
102
|
+
const { blocked: _blocked, ...rest } = next;
|
|
103
|
+
return rest;
|
|
104
|
+
}
|
|
105
|
+
return next;
|
|
106
|
+
};
|
|
107
|
+
|
|
73
108
|
/**
|
|
74
109
|
* The stored `"<display name> <email>"` compound, folded in JavaScript exactly
|
|
75
110
|
* as message sync folds it — SQL `lower()` stops at ASCII, and the search reads
|
|
@@ -496,7 +531,7 @@ export class AddressRepo implements IAddressRepository {
|
|
|
496
531
|
}
|
|
497
532
|
(next[key] as AddressFlags[keyof AddressFlags]) = value;
|
|
498
533
|
}
|
|
499
|
-
return next;
|
|
534
|
+
return dropContradictedPlacementFlag(next, patch);
|
|
500
535
|
});
|
|
501
536
|
}
|
|
502
537
|
|