@remit/config-transfer 0.0.10 → 0.0.12
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 +1 -1
- package/src/binder.ts +219 -40
- package/src/export.test.ts +8 -38
- package/src/export.ts +2 -25
- package/src/fixtures.ts +2 -20
- package/src/import-repositories.ts +0 -29
- package/src/import.test.ts +587 -160
- package/src/import.ts +71 -139
- package/src/index.ts +2 -2
- package/src/repositories.ts +0 -2
package/src/import.test.ts
CHANGED
|
@@ -6,7 +6,6 @@ import type {
|
|
|
6
6
|
AccountSettingItem,
|
|
7
7
|
AddressItem,
|
|
8
8
|
ConfigImportItem,
|
|
9
|
-
FilterAnchorItem,
|
|
10
9
|
FilterItem,
|
|
11
10
|
LabelItem,
|
|
12
11
|
MailboxItem,
|
|
@@ -17,7 +16,11 @@ import {
|
|
|
17
16
|
composeFolderRoleAppointmentName,
|
|
18
17
|
} from "@remit/data-ports/folder-role";
|
|
19
18
|
import { deriveAddressId } from "@remit/data-ports/id";
|
|
20
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
bindImportedFolders,
|
|
21
|
+
disableFiltersMissingFolders,
|
|
22
|
+
pendingImportOf,
|
|
23
|
+
} from "./binder.js";
|
|
21
24
|
import { readConfigForExport } from "./export.js";
|
|
22
25
|
import {
|
|
23
26
|
ACCOUNT_CONFIG_ID,
|
|
@@ -26,7 +29,6 @@ import {
|
|
|
26
29
|
makeAccount,
|
|
27
30
|
makeAccountConfig,
|
|
28
31
|
makeAddress,
|
|
29
|
-
makeAnchor,
|
|
30
32
|
makeFilter,
|
|
31
33
|
makeJunkOnlyFlaggedAddress,
|
|
32
34
|
makeLabel,
|
|
@@ -37,7 +39,7 @@ import {
|
|
|
37
39
|
PASSWORD_ACCOUNT_ID,
|
|
38
40
|
suggestable,
|
|
39
41
|
} from "./fixtures.js";
|
|
40
|
-
import {
|
|
42
|
+
import { importConfig } from "./import.js";
|
|
41
43
|
import type { ConfigImportDeps } from "./import-repositories.js";
|
|
42
44
|
|
|
43
45
|
const IDENTITY = {
|
|
@@ -49,7 +51,6 @@ const IDENTITY = {
|
|
|
49
51
|
|
|
50
52
|
const TARGET_CONFIG_ID = "1a2b3c4d5e6f7a8b9c0d1e2f3";
|
|
51
53
|
const TARGET_USER_ID = "9f8e7d6c-5b4a-4938-8271-605f4e3d2c1b";
|
|
52
|
-
const EMBEDDING_ID = "amazon.titan-embed-text-v2:0@1024";
|
|
53
54
|
|
|
54
55
|
const INBOX_ID = "mbx-inbox";
|
|
55
56
|
const SENT_ID = "mbx-sent";
|
|
@@ -70,7 +71,6 @@ interface Store {
|
|
|
70
71
|
mailboxes: MailboxItem[];
|
|
71
72
|
labels: LabelItem[];
|
|
72
73
|
filters: FilterItem[];
|
|
73
|
-
anchors: FilterAnchorItem[];
|
|
74
74
|
addresses: AddressItem[];
|
|
75
75
|
imports: ConfigImportItem[];
|
|
76
76
|
/** Set to make the next write of this kind fail, for the abort path. */
|
|
@@ -88,7 +88,6 @@ const emptyStore = (): Store => ({
|
|
|
88
88
|
mailboxes: [],
|
|
89
89
|
labels: [],
|
|
90
90
|
filters: [],
|
|
91
|
-
anchors: [],
|
|
92
91
|
addresses: [],
|
|
93
92
|
imports: [],
|
|
94
93
|
});
|
|
@@ -104,7 +103,6 @@ const storeFromFixture = (fixture: ConfigFixture): Store => ({
|
|
|
104
103
|
mailboxes: [...fixture.mailboxes],
|
|
105
104
|
labels: [...fixture.labels],
|
|
106
105
|
filters: [...fixture.filters],
|
|
107
|
-
anchors: [...fixture.anchors],
|
|
108
106
|
addresses: [...fixture.addresses],
|
|
109
107
|
imports: [],
|
|
110
108
|
});
|
|
@@ -185,6 +183,9 @@ const repositoriesOf = (store: Store, accountConfigId: string): any => {
|
|
|
185
183
|
mailbox: {
|
|
186
184
|
listAllByAccount: async (accountId: string) =>
|
|
187
185
|
store.mailboxes.filter((mailbox) => mailbox.accountId === accountId),
|
|
186
|
+
resolveAccountId: async (mailboxId: string) =>
|
|
187
|
+
store.mailboxes.find((mailbox) => mailbox.mailboxId === mailboxId)
|
|
188
|
+
?.accountId ?? null,
|
|
188
189
|
},
|
|
189
190
|
label: {
|
|
190
191
|
listByAccountConfig: async () => [...store.labels],
|
|
@@ -237,19 +238,6 @@ const repositoriesOf = (store: Store, accountConfigId: string): any => {
|
|
|
237
238
|
return next;
|
|
238
239
|
},
|
|
239
240
|
},
|
|
240
|
-
filterAnchor: {
|
|
241
|
-
put: async (input: Partial<FilterAnchorItem> & { filterId: string }) => {
|
|
242
|
-
const row = makeAnchor({ ...input, accountConfigId });
|
|
243
|
-
store.anchors = [
|
|
244
|
-
...store.anchors.filter(
|
|
245
|
-
(anchor) => anchor.filterId !== input.filterId,
|
|
246
|
-
),
|
|
247
|
-
row,
|
|
248
|
-
];
|
|
249
|
-
return row;
|
|
250
|
-
},
|
|
251
|
-
listByAccountConfig: async () => [...store.anchors],
|
|
252
|
-
},
|
|
253
241
|
address: {
|
|
254
242
|
// Both reads, so the stub can tell them apart: the suggest listing
|
|
255
243
|
// hides a junk-only row, and the import must not be reading it (#1029).
|
|
@@ -353,50 +341,47 @@ const appointFolderRoleInto =
|
|
|
353
341
|
);
|
|
354
342
|
};
|
|
355
343
|
|
|
356
|
-
|
|
357
|
-
* A transaction that actually rolls back. The backend's `writeSet` discards
|
|
358
|
-
* every write on a throw, so a fake that merely called through would let an
|
|
359
|
-
* atomicity test pass over a store that had kept half a document — which is the
|
|
360
|
-
* one thing these tests exist to catch. Each collection is restored to the
|
|
361
|
-
* instance it had on entry.
|
|
362
|
-
*/
|
|
363
|
-
const transactionOver =
|
|
344
|
+
const createFolderInto =
|
|
364
345
|
(store: Store) =>
|
|
365
|
-
async
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
addresses: [...store.addresses],
|
|
375
|
-
imports: [...store.imports],
|
|
376
|
-
};
|
|
377
|
-
return run().catch((error: unknown) => {
|
|
378
|
-
Object.assign(store, before);
|
|
379
|
-
throw error;
|
|
346
|
+
async (
|
|
347
|
+
accountId: string,
|
|
348
|
+
fullPath: string,
|
|
349
|
+
): Promise<{ outcome: "Created"; mailbox: MailboxItem }> => {
|
|
350
|
+
const row = makeMailbox({
|
|
351
|
+
mailboxId: nextId("mbx"),
|
|
352
|
+
accountId,
|
|
353
|
+
fullPath,
|
|
354
|
+
syncStatus: "pending",
|
|
380
355
|
});
|
|
356
|
+
store.mailboxes.push(row);
|
|
357
|
+
return { outcome: "Created", mailbox: row };
|
|
381
358
|
};
|
|
382
359
|
|
|
383
|
-
const
|
|
360
|
+
const settle = (
|
|
384
361
|
store: Store,
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
362
|
+
fullPath: string,
|
|
363
|
+
syncStatus: MailboxItem["syncStatus"],
|
|
364
|
+
confirmedPath: string = fullPath,
|
|
365
|
+
): MailboxItem => {
|
|
366
|
+
const at = store.mailboxes.findIndex(
|
|
367
|
+
(mailbox) =>
|
|
368
|
+
mailbox.fullPath === fullPath && mailbox.syncStatus === "pending",
|
|
369
|
+
);
|
|
370
|
+
assert.notEqual(at, -1, `no pending create for ${fullPath}`);
|
|
371
|
+
const next = { ...store.mailboxes[at], syncStatus, fullPath: confirmedPath };
|
|
372
|
+
store.mailboxes[at] = next;
|
|
373
|
+
return next;
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const depsOf = (store: Store, accountConfigId: string): ConfigImportDeps => ({
|
|
388
377
|
repositories: repositoriesOf(store, accountConfigId),
|
|
389
378
|
appointFolderRole: appointFolderRoleInto(store),
|
|
390
|
-
transaction: transactionOver(store),
|
|
391
|
-
embedAnchor: embed
|
|
392
|
-
? async () => ({ embedding: [0.11, 0.22, 0.33], embeddingId: EMBEDDING_ID })
|
|
393
|
-
: undefined,
|
|
394
379
|
now: () => NOW,
|
|
395
380
|
});
|
|
396
381
|
|
|
397
382
|
// ---------------------------------------------------------------------------
|
|
398
383
|
// A configuration worth carrying across: two accounts, both auth types, folder
|
|
399
|
-
// roles, per-folder overrides, labels,
|
|
384
|
+
// roles, per-folder overrides, labels, two filters, and two
|
|
400
385
|
// flagged senders.
|
|
401
386
|
// ---------------------------------------------------------------------------
|
|
402
387
|
|
|
@@ -504,14 +489,12 @@ const sourceFixture = (): ConfigFixture => ({
|
|
|
504
489
|
actionMailboxId: INVOICES_ID,
|
|
505
490
|
}),
|
|
506
491
|
makeFilter({
|
|
507
|
-
filterId: "flt-
|
|
508
|
-
name: "
|
|
492
|
+
filterId: "flt-release",
|
|
493
|
+
name: "Release notes",
|
|
509
494
|
scope: "Temporary",
|
|
510
495
|
expiresAt: "2026-12-31T23:59:59+01:00",
|
|
511
|
-
hasAnchor: true,
|
|
512
496
|
}),
|
|
513
497
|
],
|
|
514
|
-
anchors: [makeAnchor({ filterId: "flt-semantic" })],
|
|
515
498
|
addresses: [
|
|
516
499
|
makeAddress({
|
|
517
500
|
addressId: "adr-bank",
|
|
@@ -602,6 +585,7 @@ test("a configuration survives an export, an import and a discovery unchanged",
|
|
|
602
585
|
const binder = {
|
|
603
586
|
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
604
587
|
appointFolderRole: appointFolderRoleInto(store),
|
|
588
|
+
createFolder: createFolderInto(store),
|
|
605
589
|
now: () => NOW,
|
|
606
590
|
};
|
|
607
591
|
await bindImportedFolders(binder, TARGET_CONFIG_ID, PASSWORD_ACCOUNT_ID);
|
|
@@ -828,7 +812,7 @@ test("a flag that arrives at the sentinel is stored at the sentinel", async () =
|
|
|
828
812
|
assert.equal(legacy?.flags?.category?.setAt, 0);
|
|
829
813
|
});
|
|
830
814
|
|
|
831
|
-
test("a folder
|
|
815
|
+
test("a folder discovery did not produce is created, and bound once the server confirms it", async () => {
|
|
832
816
|
const document = await exportSource();
|
|
833
817
|
const store = emptyStore();
|
|
834
818
|
|
|
@@ -849,6 +833,7 @@ test("a folder no mailbox answers to yet is kept, then bound when discovery find
|
|
|
849
833
|
const binder = {
|
|
850
834
|
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
851
835
|
appointFolderRole: appointFolderRoleInto(store),
|
|
836
|
+
createFolder: createFolderInto(store),
|
|
852
837
|
now: () => NOW,
|
|
853
838
|
};
|
|
854
839
|
const first = await bindImportedFolders(
|
|
@@ -858,6 +843,7 @@ test("a folder no mailbox answers to yet is kept, then bound when discovery find
|
|
|
858
843
|
);
|
|
859
844
|
|
|
860
845
|
assert.equal(first.bound > 0, true);
|
|
846
|
+
assert.equal(first.created, 2);
|
|
861
847
|
assert.equal(first.stillPending > 0, true);
|
|
862
848
|
const invoices = store.mailboxes.find(
|
|
863
849
|
(mailbox) => mailbox.fullPath === "INBOX.Facturen",
|
|
@@ -868,16 +854,28 @@ test("a folder no mailbox answers to yet is kept, then bound when discovery find
|
|
|
868
854
|
invoices?.mailboxId,
|
|
869
855
|
);
|
|
870
856
|
|
|
871
|
-
// Replayable: a second discovery over the same folders
|
|
857
|
+
// Replayable: a second discovery over the same folders creates nothing again.
|
|
872
858
|
const again = await bindImportedFolders(
|
|
873
859
|
binder,
|
|
874
860
|
TARGET_CONFIG_ID,
|
|
875
861
|
PASSWORD_ACCOUNT_ID,
|
|
876
862
|
);
|
|
877
863
|
assert.equal(again.bound, 0);
|
|
864
|
+
assert.equal(again.created, 0);
|
|
865
|
+
assert.equal(
|
|
866
|
+
store.mailboxes.filter((mailbox) => mailbox.fullPath === "INBOX.Sent")
|
|
867
|
+
.length,
|
|
868
|
+
1,
|
|
869
|
+
);
|
|
878
870
|
assert.equal(store.imports[0].state, "Pending");
|
|
879
871
|
|
|
880
|
-
|
|
872
|
+
settle(store, "INBOX.Sent", "synced");
|
|
873
|
+
const lists = settle(
|
|
874
|
+
store,
|
|
875
|
+
"INBOX.Lists.dev-null",
|
|
876
|
+
"synced",
|
|
877
|
+
"INBOX.Lists.dev-null-normalized",
|
|
878
|
+
);
|
|
881
879
|
discover(store, OAUTH_ACCOUNT_ID, ["INBOX"]);
|
|
882
880
|
await bindImportedFolders(binder, TARGET_CONFIG_ID, PASSWORD_ACCOUNT_ID);
|
|
883
881
|
|
|
@@ -890,15 +888,499 @@ test("a folder no mailbox answers to yet is kept, then bound when discovery find
|
|
|
890
888
|
)?.value.kind,
|
|
891
889
|
"String",
|
|
892
890
|
);
|
|
893
|
-
const lists = store.mailboxes.find(
|
|
894
|
-
(mailbox) => mailbox.fullPath === "INBOX.Lists.dev-null",
|
|
895
|
-
);
|
|
896
891
|
assert.equal(
|
|
897
|
-
store.settings.get(`MailboxDisplayName#${lists
|
|
892
|
+
store.settings.get(`MailboxDisplayName#${lists.mailboxId}`)?.value.value,
|
|
898
893
|
"dev/null",
|
|
899
894
|
);
|
|
900
895
|
});
|
|
901
896
|
|
|
897
|
+
test("a filter whose folder is missing lands disabled, awaiting the folder, and the report says so", async () => {
|
|
898
|
+
const document = await exportSource();
|
|
899
|
+
const store = emptyStore();
|
|
900
|
+
|
|
901
|
+
const report = reportOf(await apply(store, document));
|
|
902
|
+
|
|
903
|
+
const invoices = store.filters.find(
|
|
904
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
905
|
+
);
|
|
906
|
+
assert.equal(invoices?.state, "Disabled");
|
|
907
|
+
assert.equal(invoices?.disabledReason, "AwaitingFolder");
|
|
908
|
+
assert.match(
|
|
909
|
+
report.items.find((item) => item.key === "Invoices to Facturen")?.reason ??
|
|
910
|
+
"",
|
|
911
|
+
/Off until "INBOX\.Facturen" exists/,
|
|
912
|
+
);
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
test("the dry run names the filters an apply would leave off", async () => {
|
|
916
|
+
const document = await exportSource();
|
|
917
|
+
const store = emptyStore();
|
|
918
|
+
|
|
919
|
+
const outcome = await importConfig(depsOf(store, TARGET_CONFIG_ID), {
|
|
920
|
+
accountConfigId: TARGET_CONFIG_ID,
|
|
921
|
+
userId: TARGET_USER_ID,
|
|
922
|
+
document,
|
|
923
|
+
mode: "validate",
|
|
924
|
+
onExisting: "abort",
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
assert.match(
|
|
928
|
+
reportOf(outcome).items.find((item) => item.key === "Invoices to Facturen")
|
|
929
|
+
?.reason ?? "",
|
|
930
|
+
/Off until "INBOX\.Facturen" exists/,
|
|
931
|
+
);
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
test("an imported filter turns on once the folder the binder created settles, under the path the server chose", async () => {
|
|
935
|
+
const document = await exportSource();
|
|
936
|
+
const store = emptyStore();
|
|
937
|
+
await apply(store, document);
|
|
938
|
+
|
|
939
|
+
discover(store, PASSWORD_ACCOUNT_ID, ["INBOX", "INBOX.Sent"]);
|
|
940
|
+
const binder = {
|
|
941
|
+
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
942
|
+
appointFolderRole: appointFolderRoleInto(store),
|
|
943
|
+
createFolder: createFolderInto(store),
|
|
944
|
+
now: () => NOW,
|
|
945
|
+
};
|
|
946
|
+
await bindImportedFolders(binder, TARGET_CONFIG_ID, PASSWORD_ACCOUNT_ID);
|
|
947
|
+
|
|
948
|
+
const waiting = store.filters.find(
|
|
949
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
950
|
+
);
|
|
951
|
+
assert.equal(waiting?.state, "Disabled");
|
|
952
|
+
assert.equal(waiting?.disabledReason, "AwaitingFolder");
|
|
953
|
+
assert.equal(waiting?.actionMailboxId, "None");
|
|
954
|
+
|
|
955
|
+
const created = settle(store, "INBOX.Facturen", "synced", "INBOX/Facturen");
|
|
956
|
+
await bindImportedFolders(binder, TARGET_CONFIG_ID, PASSWORD_ACCOUNT_ID);
|
|
957
|
+
|
|
958
|
+
const bound = store.filters.find(
|
|
959
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
960
|
+
);
|
|
961
|
+
assert.equal(bound?.state, "Active");
|
|
962
|
+
assert.equal(bound?.disabledReason, "None");
|
|
963
|
+
assert.equal(bound?.actionMailboxId, created.mailboxId);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
test("a folder the server refused to create turns its filter off with FolderCreateFailed", async () => {
|
|
967
|
+
const document = await exportSource();
|
|
968
|
+
const store = emptyStore();
|
|
969
|
+
await apply(store, document);
|
|
970
|
+
|
|
971
|
+
discover(store, PASSWORD_ACCOUNT_ID, ["INBOX", "INBOX.Sent"]);
|
|
972
|
+
const binder = {
|
|
973
|
+
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
974
|
+
appointFolderRole: appointFolderRoleInto(store),
|
|
975
|
+
createFolder: createFolderInto(store),
|
|
976
|
+
now: () => NOW,
|
|
977
|
+
};
|
|
978
|
+
await bindImportedFolders(binder, TARGET_CONFIG_ID, PASSWORD_ACCOUNT_ID);
|
|
979
|
+
settle(store, "INBOX.Facturen", "failed");
|
|
980
|
+
|
|
981
|
+
const result = await bindImportedFolders(
|
|
982
|
+
binder,
|
|
983
|
+
TARGET_CONFIG_ID,
|
|
984
|
+
PASSWORD_ACCOUNT_ID,
|
|
985
|
+
);
|
|
986
|
+
|
|
987
|
+
assert.equal(result.disabled, 1);
|
|
988
|
+
const refused = store.filters.find(
|
|
989
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
990
|
+
);
|
|
991
|
+
assert.equal(refused?.state, "Disabled");
|
|
992
|
+
assert.equal(refused?.disabledReason, "FolderCreateFailed");
|
|
993
|
+
assert.equal(
|
|
994
|
+
pendingImportOf(store.imports)?.folderPaths.includes("INBOX.Facturen") ??
|
|
995
|
+
false,
|
|
996
|
+
false,
|
|
997
|
+
);
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
test("a created folder that disappears before it settles means the create never landed", async () => {
|
|
1001
|
+
const document = await exportSource();
|
|
1002
|
+
const store = emptyStore();
|
|
1003
|
+
await apply(store, document);
|
|
1004
|
+
|
|
1005
|
+
discover(store, PASSWORD_ACCOUNT_ID, ["INBOX", "INBOX.Sent"]);
|
|
1006
|
+
const binder = {
|
|
1007
|
+
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1008
|
+
appointFolderRole: appointFolderRoleInto(store),
|
|
1009
|
+
createFolder: createFolderInto(store),
|
|
1010
|
+
now: () => NOW,
|
|
1011
|
+
};
|
|
1012
|
+
await bindImportedFolders(binder, TARGET_CONFIG_ID, PASSWORD_ACCOUNT_ID);
|
|
1013
|
+
store.mailboxes = store.mailboxes.filter(
|
|
1014
|
+
(mailbox) => mailbox.fullPath !== "INBOX.Facturen",
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
const result = await bindImportedFolders(
|
|
1018
|
+
binder,
|
|
1019
|
+
TARGET_CONFIG_ID,
|
|
1020
|
+
PASSWORD_ACCOUNT_ID,
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
assert.equal(result.disabled, 1);
|
|
1024
|
+
const refused = store.filters.find(
|
|
1025
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
1026
|
+
);
|
|
1027
|
+
assert.equal(refused?.state, "Disabled");
|
|
1028
|
+
assert.equal(refused?.disabledReason, "FolderCreateFailed");
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
test("a create that finds its path already taken binds the existing folder on the next sync", async () => {
|
|
1032
|
+
const document = await exportSource();
|
|
1033
|
+
const store = emptyStore();
|
|
1034
|
+
await apply(store, document);
|
|
1035
|
+
|
|
1036
|
+
discover(store, PASSWORD_ACCOUNT_ID, ["INBOX", "INBOX.Sent"]);
|
|
1037
|
+
const binder = {
|
|
1038
|
+
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1039
|
+
appointFolderRole: appointFolderRoleInto(store),
|
|
1040
|
+
createFolder: async () => ({ outcome: "PathTaken" as const }),
|
|
1041
|
+
now: () => NOW,
|
|
1042
|
+
};
|
|
1043
|
+
const first = await bindImportedFolders(
|
|
1044
|
+
binder,
|
|
1045
|
+
TARGET_CONFIG_ID,
|
|
1046
|
+
PASSWORD_ACCOUNT_ID,
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
assert.equal(first.created, 0);
|
|
1050
|
+
assert.equal(
|
|
1051
|
+
store.imports[0].unresolvedRefs.find(
|
|
1052
|
+
(ref) => ref.folderPath === "INBOX.Facturen",
|
|
1053
|
+
)?.mailboxId,
|
|
1054
|
+
"None",
|
|
1055
|
+
);
|
|
1056
|
+
|
|
1057
|
+
discover(store, PASSWORD_ACCOUNT_ID, ["INBOX.Facturen"]);
|
|
1058
|
+
await bindImportedFolders(binder, TARGET_CONFIG_ID, PASSWORD_ACCOUNT_ID);
|
|
1059
|
+
|
|
1060
|
+
const existing = store.mailboxes.find(
|
|
1061
|
+
(mailbox) => mailbox.fullPath === "INBOX.Facturen",
|
|
1062
|
+
);
|
|
1063
|
+
const bound = store.filters.find(
|
|
1064
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
1065
|
+
);
|
|
1066
|
+
assert.equal(bound?.state, "Active");
|
|
1067
|
+
assert.equal(bound?.actionMailboxId, existing?.mailboxId);
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
const withUnprefixedPaths = async () => {
|
|
1071
|
+
const document = (await exportSource()) as unknown as {
|
|
1072
|
+
accounts: {
|
|
1073
|
+
folderRoles: { role: string; folderPath: string }[];
|
|
1074
|
+
}[];
|
|
1075
|
+
filters: { actionFolder: { folderPath: string } | null }[];
|
|
1076
|
+
};
|
|
1077
|
+
for (const filter of document.filters) {
|
|
1078
|
+
if (filter.actionFolder?.folderPath === "INBOX.Facturen") {
|
|
1079
|
+
filter.actionFolder.folderPath = "Facturen";
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
for (const account of document.accounts) {
|
|
1083
|
+
for (const role of account.folderRoles) {
|
|
1084
|
+
if (role.folderPath === "INBOX.Sent") role.folderPath = "Sent";
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return document;
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
const discoverUnderInbox = (store: Store, paths: readonly string[]): void => {
|
|
1091
|
+
for (const fullPath of ["INBOX", ...paths]) {
|
|
1092
|
+
store.mailboxes.push(
|
|
1093
|
+
makeMailbox({
|
|
1094
|
+
mailboxId: nextId("mbx"),
|
|
1095
|
+
accountId: PASSWORD_ACCOUNT_ID,
|
|
1096
|
+
fullPath,
|
|
1097
|
+
namespacePrefix: "INBOX.",
|
|
1098
|
+
}),
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
test("a folder the account already holds under its namespace is bound, not created", async () => {
|
|
1104
|
+
const store = emptyStore();
|
|
1105
|
+
await apply(store, await withUnprefixedPaths());
|
|
1106
|
+
|
|
1107
|
+
discoverUnderInbox(store, ["INBOX.Facturen", "INBOX.Sent"]);
|
|
1108
|
+
const result = await bindImportedFolders(
|
|
1109
|
+
{
|
|
1110
|
+
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1111
|
+
appointFolderRole: appointFolderRoleInto(store),
|
|
1112
|
+
createFolder: createFolderInto(store),
|
|
1113
|
+
now: () => NOW,
|
|
1114
|
+
},
|
|
1115
|
+
TARGET_CONFIG_ID,
|
|
1116
|
+
PASSWORD_ACCOUNT_ID,
|
|
1117
|
+
);
|
|
1118
|
+
|
|
1119
|
+
const facturen = store.mailboxes.find(
|
|
1120
|
+
(mailbox) => mailbox.fullPath === "INBOX.Facturen",
|
|
1121
|
+
);
|
|
1122
|
+
const sent = store.mailboxes.find(
|
|
1123
|
+
(mailbox) => mailbox.fullPath === "INBOX.Sent",
|
|
1124
|
+
);
|
|
1125
|
+
assert.equal(
|
|
1126
|
+
store.mailboxes.some((mailbox) => mailbox.fullPath === "Facturen"),
|
|
1127
|
+
false,
|
|
1128
|
+
);
|
|
1129
|
+
assert.equal(result.disabled, 0);
|
|
1130
|
+
const bound = store.filters.find(
|
|
1131
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
1132
|
+
);
|
|
1133
|
+
assert.equal(bound?.state, "Active");
|
|
1134
|
+
assert.equal(bound?.actionMailboxId, facturen?.mailboxId);
|
|
1135
|
+
assert.equal(
|
|
1136
|
+
store.settings.get(
|
|
1137
|
+
composeFolderRoleAppointmentName(PASSWORD_ACCOUNT_ID, "Sent"),
|
|
1138
|
+
)?.value.value,
|
|
1139
|
+
sent?.mailboxId,
|
|
1140
|
+
);
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
test("a created folder the worker folds into one the server already held binds to that one", async () => {
|
|
1144
|
+
const store = emptyStore();
|
|
1145
|
+
await apply(store, await withUnprefixedPaths());
|
|
1146
|
+
|
|
1147
|
+
discoverUnderInbox(store, ["INBOX.Sent"]);
|
|
1148
|
+
const binder = {
|
|
1149
|
+
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1150
|
+
appointFolderRole: appointFolderRoleInto(store),
|
|
1151
|
+
createFolder: createFolderInto(store),
|
|
1152
|
+
now: () => NOW,
|
|
1153
|
+
};
|
|
1154
|
+
const first = await bindImportedFolders(
|
|
1155
|
+
binder,
|
|
1156
|
+
TARGET_CONFIG_ID,
|
|
1157
|
+
PASSWORD_ACCOUNT_ID,
|
|
1158
|
+
);
|
|
1159
|
+
assert.equal(first.created > 0, true);
|
|
1160
|
+
|
|
1161
|
+
store.mailboxes = store.mailboxes.filter(
|
|
1162
|
+
(mailbox) => mailbox.fullPath !== "Facturen",
|
|
1163
|
+
);
|
|
1164
|
+
const holder = makeMailbox({
|
|
1165
|
+
mailboxId: nextId("mbx"),
|
|
1166
|
+
accountId: PASSWORD_ACCOUNT_ID,
|
|
1167
|
+
fullPath: "INBOX.Facturen",
|
|
1168
|
+
namespacePrefix: "INBOX.",
|
|
1169
|
+
});
|
|
1170
|
+
store.mailboxes.push(holder);
|
|
1171
|
+
|
|
1172
|
+
const second = await bindImportedFolders(
|
|
1173
|
+
binder,
|
|
1174
|
+
TARGET_CONFIG_ID,
|
|
1175
|
+
PASSWORD_ACCOUNT_ID,
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
assert.equal(second.disabled, 0);
|
|
1179
|
+
const bound = store.filters.find(
|
|
1180
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
1181
|
+
);
|
|
1182
|
+
assert.equal(bound?.state, "Active");
|
|
1183
|
+
assert.equal(bound?.disabledReason, "None");
|
|
1184
|
+
assert.equal(bound?.actionMailboxId, holder.mailboxId);
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
test("a filter the user pointed at a folder keeps it when the imported folder appears", async () => {
|
|
1188
|
+
const document = await exportSource();
|
|
1189
|
+
const store = emptyStore();
|
|
1190
|
+
await apply(store, document);
|
|
1191
|
+
|
|
1192
|
+
const invoices = store.filters.find(
|
|
1193
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
1194
|
+
);
|
|
1195
|
+
assert.ok(invoices);
|
|
1196
|
+
invoices.actionMailboxId = "mbx-picked";
|
|
1197
|
+
|
|
1198
|
+
discover(store, PASSWORD_ACCOUNT_ID, [
|
|
1199
|
+
"INBOX",
|
|
1200
|
+
"INBOX.Sent",
|
|
1201
|
+
"INBOX.Facturen",
|
|
1202
|
+
]);
|
|
1203
|
+
await bindImportedFolders(
|
|
1204
|
+
{
|
|
1205
|
+
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1206
|
+
appointFolderRole: appointFolderRoleInto(store),
|
|
1207
|
+
createFolder: createFolderInto(store),
|
|
1208
|
+
now: () => NOW,
|
|
1209
|
+
},
|
|
1210
|
+
TARGET_CONFIG_ID,
|
|
1211
|
+
PASSWORD_ACCOUNT_ID,
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
const picked = store.filters.find(
|
|
1215
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
1216
|
+
);
|
|
1217
|
+
assert.equal(picked?.actionMailboxId, "mbx-picked");
|
|
1218
|
+
assert.equal(picked?.state, "Disabled");
|
|
1219
|
+
assert.equal(picked?.disabledReason, "UserDisabled");
|
|
1220
|
+
assert.equal(
|
|
1221
|
+
store.imports[0].unresolvedRefs.some(
|
|
1222
|
+
(ref) => ref.target === invoices.filterId,
|
|
1223
|
+
),
|
|
1224
|
+
false,
|
|
1225
|
+
);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
test("a filter the user already turned on with their own folder survives the import's failed create", async () => {
|
|
1229
|
+
const document = await exportSource();
|
|
1230
|
+
const store = emptyStore();
|
|
1231
|
+
await apply(store, document);
|
|
1232
|
+
|
|
1233
|
+
discover(store, PASSWORD_ACCOUNT_ID, ["INBOX", "INBOX.Sent"]);
|
|
1234
|
+
const binder = {
|
|
1235
|
+
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1236
|
+
appointFolderRole: appointFolderRoleInto(store),
|
|
1237
|
+
createFolder: createFolderInto(store),
|
|
1238
|
+
now: () => NOW,
|
|
1239
|
+
};
|
|
1240
|
+
await bindImportedFolders(binder, TARGET_CONFIG_ID, PASSWORD_ACCOUNT_ID);
|
|
1241
|
+
|
|
1242
|
+
const invoices = store.filters.find(
|
|
1243
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
1244
|
+
);
|
|
1245
|
+
assert.ok(invoices);
|
|
1246
|
+
Object.assign(invoices, {
|
|
1247
|
+
actionMailboxId: "mbx-picked",
|
|
1248
|
+
state: "Active",
|
|
1249
|
+
disabledReason: "None",
|
|
1250
|
+
});
|
|
1251
|
+
settle(store, "INBOX.Facturen", "failed");
|
|
1252
|
+
|
|
1253
|
+
const result = await bindImportedFolders(
|
|
1254
|
+
binder,
|
|
1255
|
+
TARGET_CONFIG_ID,
|
|
1256
|
+
PASSWORD_ACCOUNT_ID,
|
|
1257
|
+
);
|
|
1258
|
+
|
|
1259
|
+
assert.equal(result.disabled, 0);
|
|
1260
|
+
const kept = store.filters.find(
|
|
1261
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
1262
|
+
);
|
|
1263
|
+
assert.equal(kept?.state, "Active");
|
|
1264
|
+
assert.equal(kept?.actionMailboxId, "mbx-picked");
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
test("a failed rename of the created folder is not read as a failed create", async () => {
|
|
1268
|
+
const document = await exportSource();
|
|
1269
|
+
const store = emptyStore();
|
|
1270
|
+
await apply(store, document);
|
|
1271
|
+
|
|
1272
|
+
discover(store, PASSWORD_ACCOUNT_ID, ["INBOX", "INBOX.Sent"]);
|
|
1273
|
+
const binder = {
|
|
1274
|
+
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1275
|
+
appointFolderRole: appointFolderRoleInto(store),
|
|
1276
|
+
createFolder: createFolderInto(store),
|
|
1277
|
+
now: () => NOW,
|
|
1278
|
+
};
|
|
1279
|
+
await bindImportedFolders(binder, TARGET_CONFIG_ID, PASSWORD_ACCOUNT_ID);
|
|
1280
|
+
const created = settle(store, "INBOX.Facturen", "failed");
|
|
1281
|
+
Object.assign(created, { pendingPath: "INBOX.Invoices" });
|
|
1282
|
+
|
|
1283
|
+
const result = await bindImportedFolders(
|
|
1284
|
+
binder,
|
|
1285
|
+
TARGET_CONFIG_ID,
|
|
1286
|
+
PASSWORD_ACCOUNT_ID,
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1289
|
+
assert.equal(result.disabled, 0);
|
|
1290
|
+
const waiting = store.filters.find(
|
|
1291
|
+
(filter) => filter.name === "Invoices to Facturen",
|
|
1292
|
+
);
|
|
1293
|
+
assert.equal(waiting?.disabledReason, "AwaitingFolder");
|
|
1294
|
+
assert.equal(
|
|
1295
|
+
store.imports[0].unresolvedRefs.some(
|
|
1296
|
+
(ref) => ref.mailboxId === created.mailboxId,
|
|
1297
|
+
),
|
|
1298
|
+
true,
|
|
1299
|
+
);
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
test("a folder still pending on the server is waited for, never bound", async () => {
|
|
1303
|
+
const document = await exportSource();
|
|
1304
|
+
const store = emptyStore();
|
|
1305
|
+
await apply(store, document);
|
|
1306
|
+
|
|
1307
|
+
store.mailboxes.push(
|
|
1308
|
+
makeMailbox({
|
|
1309
|
+
mailboxId: nextId("mbx"),
|
|
1310
|
+
accountId: PASSWORD_ACCOUNT_ID,
|
|
1311
|
+
fullPath: "INBOX.Facturen",
|
|
1312
|
+
syncStatus: "pending",
|
|
1313
|
+
}),
|
|
1314
|
+
);
|
|
1315
|
+
discover(store, PASSWORD_ACCOUNT_ID, ["INBOX", "INBOX.Sent"]);
|
|
1316
|
+
const result = await bindImportedFolders(
|
|
1317
|
+
{
|
|
1318
|
+
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1319
|
+
appointFolderRole: appointFolderRoleInto(store),
|
|
1320
|
+
createFolder: createFolderInto(store),
|
|
1321
|
+
now: () => NOW,
|
|
1322
|
+
},
|
|
1323
|
+
TARGET_CONFIG_ID,
|
|
1324
|
+
PASSWORD_ACCOUNT_ID,
|
|
1325
|
+
);
|
|
1326
|
+
|
|
1327
|
+
assert.equal(
|
|
1328
|
+
store.mailboxes.filter((mailbox) => mailbox.fullPath === "INBOX.Facturen")
|
|
1329
|
+
.length,
|
|
1330
|
+
1,
|
|
1331
|
+
);
|
|
1332
|
+
assert.equal(result.stillPending > 0, true);
|
|
1333
|
+
assert.equal(
|
|
1334
|
+
store.filters.find((filter) => filter.name === "Invoices to Facturen")
|
|
1335
|
+
?.actionMailboxId,
|
|
1336
|
+
"None",
|
|
1337
|
+
);
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1340
|
+
test("a running filter whose folder another client deleted turns off with FolderMissing", async () => {
|
|
1341
|
+
const store = emptyStore();
|
|
1342
|
+
store.mailboxes.push(
|
|
1343
|
+
makeMailbox({
|
|
1344
|
+
mailboxId: INVOICES_ID,
|
|
1345
|
+
accountId: PASSWORD_ACCOUNT_ID,
|
|
1346
|
+
fullPath: "INBOX.Facturen",
|
|
1347
|
+
}),
|
|
1348
|
+
);
|
|
1349
|
+
store.filters.push(
|
|
1350
|
+
makeFilter({
|
|
1351
|
+
filterId: "flt-live",
|
|
1352
|
+
name: "Live",
|
|
1353
|
+
actionMailboxId: INVOICES_ID,
|
|
1354
|
+
}),
|
|
1355
|
+
makeFilter({
|
|
1356
|
+
filterId: "flt-gone",
|
|
1357
|
+
name: "Gone",
|
|
1358
|
+
actionMailboxId: LISTS_ID,
|
|
1359
|
+
}),
|
|
1360
|
+
makeFilter({
|
|
1361
|
+
filterId: "flt-off",
|
|
1362
|
+
name: "Off",
|
|
1363
|
+
state: "Disabled",
|
|
1364
|
+
disabledReason: "UserDisabled",
|
|
1365
|
+
actionMailboxId: SENT_ID,
|
|
1366
|
+
}),
|
|
1367
|
+
);
|
|
1368
|
+
|
|
1369
|
+
const disabled = await disableFiltersMissingFolders(
|
|
1370
|
+
repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1371
|
+
TARGET_CONFIG_ID,
|
|
1372
|
+
);
|
|
1373
|
+
|
|
1374
|
+
assert.equal(disabled, 1);
|
|
1375
|
+
const byId = new Map(
|
|
1376
|
+
store.filters.map((filter) => [filter.filterId, filter]),
|
|
1377
|
+
);
|
|
1378
|
+
assert.equal(byId.get("flt-live")?.state, "Active");
|
|
1379
|
+
assert.equal(byId.get("flt-gone")?.state, "Disabled");
|
|
1380
|
+
assert.equal(byId.get("flt-gone")?.disabledReason, "FolderMissing");
|
|
1381
|
+
assert.equal(byId.get("flt-off")?.disabledReason, "UserDisabled");
|
|
1382
|
+
});
|
|
1383
|
+
|
|
902
1384
|
test("an import with no folders to wait for is complete the moment it lands", async () => {
|
|
903
1385
|
const document = await exportSource();
|
|
904
1386
|
const store = emptyStore();
|
|
@@ -1032,67 +1514,6 @@ test("a filter filing into an account the file does not carry is refused", async
|
|
|
1032
1514
|
assert.equal(report.errors[0]?.code, "unknown_account");
|
|
1033
1515
|
});
|
|
1034
1516
|
|
|
1035
|
-
test("an anchor this deployment cannot embed is stored as text for the repair to pick up", async () => {
|
|
1036
|
-
const document = await exportSource();
|
|
1037
|
-
const store = emptyStore();
|
|
1038
|
-
|
|
1039
|
-
const outcome = await importConfig(depsOf(store, TARGET_CONFIG_ID, false), {
|
|
1040
|
-
accountConfigId: TARGET_CONFIG_ID,
|
|
1041
|
-
userId: TARGET_USER_ID,
|
|
1042
|
-
document,
|
|
1043
|
-
mode: "apply",
|
|
1044
|
-
onExisting: "abort",
|
|
1045
|
-
});
|
|
1046
|
-
const report = reportOf(outcome);
|
|
1047
|
-
|
|
1048
|
-
const anchor = store.anchors[0];
|
|
1049
|
-
assert.equal(anchor.anchorEmbeddingId, ANCHOR_EMBEDDING_PENDING);
|
|
1050
|
-
assert.deepEqual(anchor.anchorEmbedding, []);
|
|
1051
|
-
assert.equal(
|
|
1052
|
-
anchor.anchorSourceText,
|
|
1053
|
-
"the release note this filter was drawn from",
|
|
1054
|
-
);
|
|
1055
|
-
assert.equal(
|
|
1056
|
-
report.warnings.some((warning) => warning.code === "anchor_not_embedded"),
|
|
1057
|
-
true,
|
|
1058
|
-
);
|
|
1059
|
-
});
|
|
1060
|
-
|
|
1061
|
-
test("an embedder that fails costs the anchor its vector, not the import", async () => {
|
|
1062
|
-
const document = await exportSource();
|
|
1063
|
-
const store = emptyStore();
|
|
1064
|
-
const deps = depsOf(store, TARGET_CONFIG_ID);
|
|
1065
|
-
|
|
1066
|
-
const report = reportOf(
|
|
1067
|
-
await importConfig(
|
|
1068
|
-
{
|
|
1069
|
-
...deps,
|
|
1070
|
-
embedAnchor: () => Promise.reject(new Error("embedder unavailable")),
|
|
1071
|
-
},
|
|
1072
|
-
{
|
|
1073
|
-
accountConfigId: TARGET_CONFIG_ID,
|
|
1074
|
-
userId: TARGET_USER_ID,
|
|
1075
|
-
document,
|
|
1076
|
-
mode: "apply",
|
|
1077
|
-
onExisting: "abort",
|
|
1078
|
-
},
|
|
1079
|
-
),
|
|
1080
|
-
);
|
|
1081
|
-
|
|
1082
|
-
assert.equal(report.applied, true);
|
|
1083
|
-
assert.deepEqual(report.errors, []);
|
|
1084
|
-
assert.equal(
|
|
1085
|
-
report.warnings.some((warning) => warning.code === "anchor_not_embedded"),
|
|
1086
|
-
true,
|
|
1087
|
-
);
|
|
1088
|
-
assert.equal(store.anchors[0]?.anchorEmbeddingId, ANCHOR_EMBEDDING_PENDING);
|
|
1089
|
-
assert.equal(
|
|
1090
|
-
store.anchors[0]?.anchorSourceText,
|
|
1091
|
-
"the release note this filter was drawn from",
|
|
1092
|
-
);
|
|
1093
|
-
assert.equal(store.accounts.length > 0, true);
|
|
1094
|
-
});
|
|
1095
|
-
|
|
1096
1517
|
test("the dry run names the same folders and credentials the apply does", async () => {
|
|
1097
1518
|
const document = await exportSource();
|
|
1098
1519
|
const planStore = emptyStore();
|
|
@@ -1121,7 +1542,7 @@ test("the dry run names the same folders and credentials the apply does", async
|
|
|
1121
1542
|
assert.deepEqual(planStore.accounts, []);
|
|
1122
1543
|
});
|
|
1123
1544
|
|
|
1124
|
-
test("a write that fails
|
|
1545
|
+
test("a write that fails part-way reports what it did leave behind and names where it stopped", async () => {
|
|
1125
1546
|
const document = await exportSource();
|
|
1126
1547
|
const store = emptyStore();
|
|
1127
1548
|
store.failOn = "filter";
|
|
@@ -1129,40 +1550,8 @@ test("a write that fails after validation leaves nothing behind and names where
|
|
|
1129
1550
|
const report = reportOf(await apply(store, document));
|
|
1130
1551
|
|
|
1131
1552
|
assert.equal(report.valid, true);
|
|
1132
|
-
assert.equal(report.applied, false);
|
|
1133
1553
|
assert.equal(report.errors[0]?.code, "import_write_failed");
|
|
1134
1554
|
assert.equal(report.errors[0]?.details?.section, "filters");
|
|
1135
|
-
assert.match(report.errors[0]?.message ?? "", /Nothing was written/);
|
|
1136
|
-
assert.equal(report.items[0]?.verdict, "rejected");
|
|
1137
|
-
|
|
1138
|
-
// Filters fail after accounts and labels have been written, so these are the
|
|
1139
|
-
// assertions that make the claim: the rollback undid them too.
|
|
1140
|
-
assert.deepEqual(store.imports, []);
|
|
1141
|
-
assert.deepEqual(store.accounts, []);
|
|
1142
|
-
assert.deepEqual(store.labels, []);
|
|
1143
|
-
assert.deepEqual(store.filters, []);
|
|
1144
|
-
assert.equal(store.accountConfigs.size, 0);
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
test("a write that fails without a transaction reports what it did leave behind", async () => {
|
|
1148
|
-
const document = await exportSource();
|
|
1149
|
-
const store = emptyStore();
|
|
1150
|
-
store.failOn = "filter";
|
|
1151
|
-
|
|
1152
|
-
const deps = depsOf(store, TARGET_CONFIG_ID);
|
|
1153
|
-
const report = reportOf(
|
|
1154
|
-
await importConfig(
|
|
1155
|
-
{ ...deps, transaction: undefined },
|
|
1156
|
-
{
|
|
1157
|
-
accountConfigId: TARGET_CONFIG_ID,
|
|
1158
|
-
userId: TARGET_USER_ID,
|
|
1159
|
-
document,
|
|
1160
|
-
mode: "apply",
|
|
1161
|
-
onExisting: "abort",
|
|
1162
|
-
},
|
|
1163
|
-
),
|
|
1164
|
-
);
|
|
1165
|
-
|
|
1166
1555
|
assert.equal(report.applied, true);
|
|
1167
1556
|
assert.match(report.errors[0]?.message ?? "", /were written and remain/);
|
|
1168
1557
|
assert.equal(
|
|
@@ -1174,6 +1563,29 @@ test("a write that fails without a transaction reports what it did leave behind"
|
|
|
1174
1563
|
assert.equal(store.accounts.length > 0, true);
|
|
1175
1564
|
});
|
|
1176
1565
|
|
|
1566
|
+
test("a stopped import imported again with merge reaches what one clean import holds", async () => {
|
|
1567
|
+
const document = await exportSource();
|
|
1568
|
+
const clean = emptyStore();
|
|
1569
|
+
await apply(clean, document);
|
|
1570
|
+
|
|
1571
|
+
const store = emptyStore();
|
|
1572
|
+
store.failOn = "filter";
|
|
1573
|
+
await apply(store, document);
|
|
1574
|
+
store.failOn = undefined;
|
|
1575
|
+
const report = reportOf(await apply(store, document, "merge"));
|
|
1576
|
+
|
|
1577
|
+
assert.deepEqual(report.errors, []);
|
|
1578
|
+
const held = (target: Store) => ({
|
|
1579
|
+
accounts: target.accounts.map((account) => account.email).sort(),
|
|
1580
|
+
labels: target.labels.map((label) => label.name).sort(),
|
|
1581
|
+
filters: target.filters.length,
|
|
1582
|
+
addresses: target.addresses.length,
|
|
1583
|
+
settings: [...target.settings.keys()].sort(),
|
|
1584
|
+
});
|
|
1585
|
+
assert.deepEqual(held(store), held(clean));
|
|
1586
|
+
assert.ok(held(clean).filters > 0, "the file carries filters to land");
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1177
1589
|
test("the configuration row is created when the database holds none, and named from the file", async () => {
|
|
1178
1590
|
const document = await exportSource();
|
|
1179
1591
|
const store = emptyStore();
|
|
@@ -1226,12 +1638,19 @@ test("a binder run on a configuration with no import does nothing", async () =>
|
|
|
1226
1638
|
{
|
|
1227
1639
|
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1228
1640
|
appointFolderRole: appointFolderRoleInto(store),
|
|
1641
|
+
createFolder: createFolderInto(store),
|
|
1229
1642
|
now: () => NOW,
|
|
1230
1643
|
},
|
|
1231
1644
|
TARGET_CONFIG_ID,
|
|
1232
1645
|
PASSWORD_ACCOUNT_ID,
|
|
1233
1646
|
);
|
|
1234
|
-
assert.deepEqual(result, {
|
|
1647
|
+
assert.deepEqual(result, {
|
|
1648
|
+
bound: 0,
|
|
1649
|
+
dropped: 0,
|
|
1650
|
+
created: 0,
|
|
1651
|
+
disabled: 0,
|
|
1652
|
+
stillPending: 0,
|
|
1653
|
+
});
|
|
1235
1654
|
assert.equal(pendingImportOf([]), undefined);
|
|
1236
1655
|
});
|
|
1237
1656
|
|
|
@@ -1258,6 +1677,7 @@ test("a filter deleted before its folder is discovered drops its reference inste
|
|
|
1258
1677
|
const binder = {
|
|
1259
1678
|
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1260
1679
|
appointFolderRole: appointFolderRoleInto(store),
|
|
1680
|
+
createFolder: createFolderInto(store),
|
|
1261
1681
|
now: () => NOW,
|
|
1262
1682
|
};
|
|
1263
1683
|
|
|
@@ -1282,7 +1702,13 @@ test("a filter deleted before its folder is discovered drops its reference inste
|
|
|
1282
1702
|
TARGET_CONFIG_ID,
|
|
1283
1703
|
PASSWORD_ACCOUNT_ID,
|
|
1284
1704
|
);
|
|
1285
|
-
assert.deepEqual(again, {
|
|
1705
|
+
assert.deepEqual(again, {
|
|
1706
|
+
bound: 0,
|
|
1707
|
+
dropped: 0,
|
|
1708
|
+
created: 0,
|
|
1709
|
+
disabled: 0,
|
|
1710
|
+
stillPending: 0,
|
|
1711
|
+
});
|
|
1286
1712
|
});
|
|
1287
1713
|
|
|
1288
1714
|
test("a deleted filter's reference is dropped even when its folder never appears", async () => {
|
|
@@ -1303,6 +1729,7 @@ test("a deleted filter's reference is dropped even when its folder never appears
|
|
|
1303
1729
|
{
|
|
1304
1730
|
repositories: repositoriesOf(store, TARGET_CONFIG_ID),
|
|
1305
1731
|
appointFolderRole: appointFolderRoleInto(store),
|
|
1732
|
+
createFolder: createFolderInto(store),
|
|
1306
1733
|
now: () => NOW,
|
|
1307
1734
|
},
|
|
1308
1735
|
TARGET_CONFIG_ID,
|