Skip to content

Commit 3573e20

Browse files
BTrestoneliykat
andauthored
[PM-20134] Fix overwriteExisting and largeImport causing users to be deleted (#737)
* Fix mixed up bools, use whole object * disallow overwriteExisting on large syncs * remove unused file * add test, always set overwriteExisting to false for batched requests * add more tests * wip * Clean up --------- Co-authored-by: Thomas Rittson <[email protected]>
1 parent 23d285a commit 3573e20

10 files changed

+322
-68
lines changed

jslib/common/src/models/request/organizationImportRequest.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,12 @@ export class OrganizationImportRequest {
88
overwriteExisting = false;
99
largeImport = false;
1010

11-
constructor(
12-
model:
13-
| {
14-
groups: Required<OrganizationImportGroupRequest>[];
15-
users: Required<OrganizationImportMemberRequest>[];
16-
overwriteExisting: boolean;
17-
largeImport: boolean;
18-
}
19-
| ImportDirectoryRequest,
20-
) {
11+
constructor(model: {
12+
groups: Required<OrganizationImportGroupRequest>[];
13+
users: Required<OrganizationImportMemberRequest>[];
14+
overwriteExisting: boolean;
15+
largeImport: boolean;
16+
}) {
2117
if (model instanceof ImportDirectoryRequest) {
2218
this.groups = model.groups.map((g) => new OrganizationImportGroupRequest(g));
2319
this.members = model.users.map((u) => new OrganizationImportMemberRequest(u));

src/abstractions/request-builder.service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ import { OrganizationImportRequest } from "@/jslib/common/src/models/request/org
33
import { GroupEntry } from "@/src/models/groupEntry";
44
import { UserEntry } from "@/src/models/userEntry";
55

6+
export interface RequestBuilderOptions {
7+
removeDisabled: boolean;
8+
overwriteExisting: boolean;
9+
}
10+
611
export abstract class RequestBuilder {
712
buildRequest: (
813
groups: GroupEntry[],
914
users: UserEntry[],
10-
removeDisabled: boolean,
11-
overwriteExisting: boolean,
15+
options: RequestBuilderOptions,
1216
) => OrganizationImportRequest[];
1317
}

src/services/batch-request-builder.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { OrganizationImportRequest } from "@/jslib/common/src/models/request/org
33
import { GroupEntry } from "@/src/models/groupEntry";
44
import { UserEntry } from "@/src/models/userEntry";
55

6-
import { RequestBuilder } from "../abstractions/request-builder.service";
6+
import { RequestBuilder, RequestBuilderOptions } from "../abstractions/request-builder.service";
77

88
import { batchSize } from "./sync.service";
99

@@ -16,17 +16,22 @@ export class BatchRequestBuilder implements RequestBuilder {
1616
buildRequest(
1717
groups: GroupEntry[],
1818
users: UserEntry[],
19-
removeDisabled: boolean,
20-
overwriteExisting: boolean,
19+
options: RequestBuilderOptions,
2120
): OrganizationImportRequest[] {
21+
if (options.overwriteExisting) {
22+
throw new Error(
23+
"You cannot use the 'Remove and re-add organization users during the next sync' option with large imports.",
24+
);
25+
}
26+
2227
const requests: OrganizationImportRequest[] = [];
2328

2429
if (users.length > 0) {
2530
const usersRequest = users.map((u) => {
2631
return {
2732
email: u.email,
2833
externalId: u.externalId,
29-
deleted: u.deleted || (removeDisabled && u.disabled),
34+
deleted: u.deleted || (options.removeDisabled && u.disabled),
3035
};
3136
});
3237

@@ -37,7 +42,7 @@ export class BatchRequestBuilder implements RequestBuilder {
3742
groups: [],
3843
users: u,
3944
largeImport: true,
40-
overwriteExisting,
45+
overwriteExisting: false,
4146
});
4247
requests.push(req);
4348
}
@@ -59,7 +64,7 @@ export class BatchRequestBuilder implements RequestBuilder {
5964
groups: g,
6065
users: [],
6166
largeImport: true,
62-
overwriteExisting,
67+
overwriteExisting: false,
6368
});
6469
requests.push(req);
6570
}

src/services/batch-requests-builder.spec.ts

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,74 @@
1-
import { GroupEntry } from "@/src/models/groupEntry";
1+
import { GetUniqueString } from "@/jslib/common/spec/utils";
2+
23
import { UserEntry } from "@/src/models/userEntry";
34

5+
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
6+
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
7+
48
import { BatchRequestBuilder } from "./batch-request-builder";
5-
import { SingleRequestBuilder } from "./single-request-builder";
69

710
describe("BatchRequestBuilder", () => {
811
let batchRequestBuilder: BatchRequestBuilder;
9-
let singleRequestBuilder: SingleRequestBuilder;
10-
11-
function userSimulator(userCount: number) {
12-
return Array(userCount).fill(new UserEntry());
13-
}
14-
15-
function groupSimulator(groupCount: number) {
16-
return Array(groupCount).fill(new GroupEntry());
17-
}
1812

1913
beforeEach(async () => {
2014
batchRequestBuilder = new BatchRequestBuilder();
21-
singleRequestBuilder = new SingleRequestBuilder();
15+
});
16+
17+
const defaultOptions: RequestBuilderOptions = Object.freeze({
18+
overwriteExisting: false,
19+
removeDisabled: false,
2220
});
2321

2422
it("BatchRequestBuilder batches requests for > 2000 users", () => {
2523
const mockGroups = groupSimulator(11000);
2624
const mockUsers = userSimulator(11000);
27-
28-
const requests = batchRequestBuilder.buildRequest(mockGroups, mockUsers, true, true);
25+
const requests = batchRequestBuilder.buildRequest(mockGroups, mockUsers, defaultOptions);
2926

3027
expect(requests.length).toEqual(12);
3128
});
3229

33-
it("SingleRequestBuilder returns single request for 200 users", () => {
34-
const mockGroups = groupSimulator(200);
35-
const mockUsers = userSimulator(200);
30+
it("BatchRequestBuilder throws error when overwriteExisting is true", () => {
31+
const mockGroups = groupSimulator(11000);
32+
const mockUsers = userSimulator(11000);
33+
const options = { ...defaultOptions, overwriteExisting: true };
34+
35+
const r = () => batchRequestBuilder.buildRequest(mockGroups, mockUsers, options);
36+
37+
expect(r).toThrow(
38+
"You cannot use the 'Remove and re-add organization users during the next sync' option with large imports.",
39+
);
40+
});
41+
42+
it("BatchRequestBuilder returns requests with deleted users when removeDisabled is true", () => {
43+
const mockGroups = groupSimulator(11000);
44+
const mockUsers = userSimulator(11000);
45+
46+
const disabledUser1 = new UserEntry();
47+
const disabledUserEmail1 = GetUniqueString() + "@email.com";
48+
49+
const disabledUser2 = new UserEntry();
50+
const disabledUserEmail2 = GetUniqueString() + "@email.com";
51+
52+
disabledUser1.disabled = true;
53+
disabledUser1.email = disabledUserEmail1;
54+
disabledUser2.disabled = true;
55+
disabledUser2.email = disabledUserEmail2;
56+
57+
mockUsers[0] = disabledUser1;
58+
mockUsers.push(disabledUser2);
3659

37-
const requests = singleRequestBuilder.buildRequest(mockGroups, mockUsers, true, true);
60+
const options = { ...defaultOptions, removeDisabled: true };
61+
const requests = batchRequestBuilder.buildRequest(mockGroups, mockUsers, options);
3862

39-
expect(requests.length).toEqual(1);
63+
expect(requests[0].members).toContainEqual({ email: disabledUserEmail1, deleted: true });
64+
expect(requests[1].members.find((m) => m.deleted)).toBeUndefined();
65+
expect(requests[3].members.find((m) => m.deleted)).toBeUndefined();
66+
expect(requests[4].members.find((m) => m.deleted)).toBeUndefined();
67+
expect(requests[5].members).toContainEqual({ email: disabledUserEmail2, deleted: true });
4068
});
4169

4270
it("BatchRequestBuilder retuns an empty array when there are no users or groups", () => {
43-
const requests = batchRequestBuilder.buildRequest([], [], true, true);
71+
const requests = batchRequestBuilder.buildRequest([], [], defaultOptions);
4472

4573
expect(requests).toEqual([]);
4674
});
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { GetUniqueString } from "@/jslib/common/spec/utils";
2+
3+
import { UserEntry } from "@/src/models/userEntry";
4+
5+
import { RequestBuilderOptions } from "../abstractions/request-builder.service";
6+
import { groupSimulator, userSimulator } from "../utils/request-builder-helper";
7+
8+
import { SingleRequestBuilder } from "./single-request-builder";
9+
10+
describe("SingleRequestBuilder", () => {
11+
let singleRequestBuilder: SingleRequestBuilder;
12+
13+
beforeEach(async () => {
14+
singleRequestBuilder = new SingleRequestBuilder();
15+
});
16+
17+
const defaultOptions: RequestBuilderOptions = Object.freeze({
18+
overwriteExisting: false,
19+
removeDisabled: false,
20+
});
21+
22+
it("SingleRequestBuilder returns single request for 200 users", () => {
23+
const mockGroups = groupSimulator(200);
24+
const mockUsers = userSimulator(200);
25+
26+
const requests = singleRequestBuilder.buildRequest(mockGroups, mockUsers, defaultOptions);
27+
28+
expect(requests.length).toEqual(1);
29+
});
30+
31+
it("SingleRequestBuilder returns request with overwriteExisting enabled", () => {
32+
const mockGroups = groupSimulator(200);
33+
const mockUsers = userSimulator(200);
34+
35+
const options = { ...defaultOptions, overwriteExisting: true };
36+
const request = singleRequestBuilder.buildRequest(mockGroups, mockUsers, options)[0];
37+
38+
expect(request.overwriteExisting).toBe(true);
39+
});
40+
41+
it("SingleRequestBuilder returns request with deleted user when removeDisabled is true", () => {
42+
const mockGroups = groupSimulator(200);
43+
const mockUsers = userSimulator(200);
44+
45+
const disabledUser = new UserEntry();
46+
const disabledUserEmail = GetUniqueString() + "@example.com";
47+
disabledUser.disabled = true;
48+
disabledUser.email = disabledUserEmail;
49+
mockUsers.push(disabledUser);
50+
51+
const options = { ...defaultOptions, removeDisabled: true };
52+
const request = singleRequestBuilder.buildRequest(mockGroups, mockUsers, options)[0];
53+
54+
expect(request.members.length).toEqual(201);
55+
expect(request.members.pop()).toEqual(
56+
expect.objectContaining({ email: disabledUserEmail, deleted: true }),
57+
);
58+
expect(request.overwriteExisting).toBe(false);
59+
});
60+
61+
it("SingleRequestBuilder returns request with deleted user and overwriteExisting enabled when overwriteExisting and removeDisabled are true", () => {
62+
const mockGroups = groupSimulator(200);
63+
const mockUsers = userSimulator(200);
64+
65+
const disabledUser = new UserEntry();
66+
const disabledUserEmail = GetUniqueString() + "@example.com";
67+
disabledUser.disabled = true;
68+
disabledUser.email = disabledUserEmail;
69+
mockUsers.push(disabledUser);
70+
71+
const options = { overwriteExisting: true, removeDisabled: true };
72+
const request = singleRequestBuilder.buildRequest(mockGroups, mockUsers, options)[0];
73+
74+
expect(request.members.pop()).toEqual(
75+
expect.objectContaining({ email: disabledUserEmail, deleted: true }),
76+
);
77+
expect(request.overwriteExisting).toBe(true);
78+
});
79+
});

src/services/single-request-builder.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { OrganizationImportRequest } from "@/jslib/common/src/models/request/org
33
import { GroupEntry } from "@/src/models/groupEntry";
44
import { UserEntry } from "@/src/models/userEntry";
55

6-
import { RequestBuilder } from "../abstractions/request-builder.service";
6+
import { RequestBuilder, RequestBuilderOptions } from "../abstractions/request-builder.service";
77

88
/**
99
* This class is responsible for building small (<2k users) syncs as a single
@@ -15,8 +15,7 @@ export class SingleRequestBuilder implements RequestBuilder {
1515
buildRequest(
1616
groups: GroupEntry[],
1717
users: UserEntry[],
18-
removeDisabled: boolean,
19-
overwriteExisting: boolean,
18+
options: RequestBuilderOptions,
2019
): OrganizationImportRequest[] {
2120
return [
2221
new OrganizationImportRequest({
@@ -31,10 +30,10 @@ export class SingleRequestBuilder implements RequestBuilder {
3130
return {
3231
email: u.email,
3332
externalId: u.externalId,
34-
deleted: u.deleted || (removeDisabled && u.disabled),
33+
deleted: u.deleted || (options.removeDisabled && u.disabled),
3534
};
3635
}),
37-
overwriteExisting: overwriteExisting,
36+
overwriteExisting: options.overwriteExisting,
3837
largeImport: false,
3938
}),
4039
];

0 commit comments

Comments
 (0)