Skip to content

Commit 81556e2

Browse files
committed
fixes three related pagination bugs in the command's org user fetch logic.
1 parent 4beaeab commit 81556e2

3 files changed

Lines changed: 7152 additions & 9010 deletions

File tree

packages/contentstack-export-to-csv/src/utils/api-client.ts

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -138,22 +138,16 @@ export function getOrgUsers(managementAPIClient: ManagementClient, orgUid: strin
138138
return reject(new Error('Org UID not found.'));
139139
}
140140

141-
if (organization.is_owner === true) {
142-
return managementAPIClient
143-
.organization(organization.uid)
144-
.getInvitations()
145-
.then((data: unknown) => {
146-
resolve(data as OrgUsersResponse);
147-
})
148-
.catch(reject);
149-
}
150-
151-
if (!organization.getInvitations && !find(organization.org_roles, 'admin')) {
141+
if (!organization.is_owner && !find(organization.org_roles, 'admin')) {
152142
return reject(new Error(messages.ERROR_ADMIN_ACCESS_DENIED));
153143
}
154144

155145
try {
156-
const users = await getUsers(managementAPIClient, { uid: organization.uid }, { skip: 0, page: 1, limit: 100 });
146+
const users = await getUsers(
147+
managementAPIClient,
148+
{ uid: organization.uid },
149+
{ skip: 0, page: 1, limit: config.limit },
150+
);
157151
return resolve({ items: users || [] });
158152
} catch (error) {
159153
return reject(error);
@@ -175,15 +169,18 @@ async function getUsers(
175169
try {
176170
const users = await managementAPIClient.organization(organization.uid).getInvitations(params) as unknown as OrgUsersResponse;
177171

178-
if (!users.items || (users.items && !users.items.length)) {
172+
if (!users.items || users.items.length < params.limit) {
173+
if (users.items?.length) {
174+
result = result.concat(users.items);
175+
}
179176
return result;
180-
} else {
181-
result = result.concat(users.items);
182-
params.skip = params.page * params.limit;
183-
params.page++;
184-
await wait(200);
185-
return getUsers(managementAPIClient, organization, params, result);
186177
}
178+
179+
result = result.concat(users.items);
180+
params.skip = params.page * params.limit;
181+
params.page++;
182+
await wait(200);
183+
return getUsers(managementAPIClient, organization, params, result);
187184
} catch {
188185
return result;
189186
}

packages/contentstack-export-to-csv/test/unit/utils/api-client.test.ts

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { expect } from 'chai';
2+
import sinon from 'sinon';
3+
import config from '../../../src/config';
4+
import { messages } from '../../../src/messages';
5+
import * as errorHandler from '../../../src/utils/error-handler';
26
import {
37
getOrganizations,
48
getOrganizationsWhereUserIsAdmin,
@@ -18,12 +22,66 @@ import {
1822
getTaxonomy,
1923
createImportableCSV,
2024
} from '../../../src/utils/api-client';
25+
import type { ManagementClient, OrgUser } from '../../../src/types';
2126

22-
// API client functions are tightly coupled to the Contentstack SDK
23-
// These tests verify the function signatures and basic structure
24-
// Full integration testing requires actual SDK mocking or E2E tests
27+
const ORG_UID = 'org-uid';
28+
29+
function makeUser(index: number): OrgUser {
30+
return {
31+
email: `user${index}@example.com`,
32+
user_uid: `uid-${index}`,
33+
invited_by: 'system',
34+
status: 'accepted',
35+
created_at: '2020-01-01T00:00:00.000Z',
36+
updated_at: '2020-01-01T00:00:00.000Z',
37+
};
38+
}
39+
40+
function createPaginatedMockClient(
41+
organization: {
42+
is_owner?: boolean;
43+
org_roles?: Array<{ admin?: boolean }>;
44+
},
45+
pages: OrgUser[][],
46+
): { client: ManagementClient; getInvitations: sinon.SinonStub; invitationParams: Array<Record<string, number>> } {
47+
const invitationParams: Array<Record<string, number>> = [];
48+
const getInvitations = sinon.stub().callsFake(async (params: Record<string, number>) => {
49+
invitationParams.push({ ...params });
50+
const callIndex = getInvitations.callCount - 1;
51+
const items = pages[callIndex] ?? [];
52+
return { items };
53+
});
54+
55+
const organizationClient = { getInvitations };
56+
const organizationStub = sinon.stub().returns(organizationClient);
57+
58+
const client = {
59+
getUser: sinon.stub().resolves({
60+
organizations: [
61+
{
62+
uid: ORG_UID,
63+
name: 'Test Org',
64+
...organization,
65+
},
66+
],
67+
}),
68+
organization: organizationStub,
69+
} as unknown as ManagementClient;
70+
71+
return { client, getInvitations, invitationParams };
72+
}
2573

2674
describe('api-client', () => {
75+
let waitStub: sinon.SinonStub;
76+
77+
beforeEach(() => {
78+
waitStub = sinon.stub(errorHandler, 'wait').resolves();
79+
});
80+
81+
afterEach(() => {
82+
waitStub.restore();
83+
});
84+
2785
describe('module exports', () => {
2886
it('should export all expected functions', () => {
2987
expect(getOrganizations).to.be.a('function');
@@ -46,7 +104,62 @@ describe('api-client', () => {
46104
});
47105
});
48106

49-
// Note: Full functional tests for api-client require mocking the @contentstack/management SDK
50-
// This is complex due to the SDK's internal structure. These tests are better suited for
51-
// integration testing with a test stack or using more sophisticated mocking tools.
107+
describe('getOrgUsers', () => {
108+
it('should paginate getInvitations for organization owners', async () => {
109+
const page1 = Array.from({ length: config.limit }, (_, i) => makeUser(i));
110+
const page2 = Array.from({ length: config.limit }, (_, i) => makeUser(i + config.limit));
111+
const page3 = Array.from({ length: 25 }, (_, i) => makeUser(i + config.limit * 2));
112+
113+
const { client, getInvitations, invitationParams } = createPaginatedMockClient(
114+
{ is_owner: true },
115+
[page1, page2, page3],
116+
);
117+
118+
const result = await getOrgUsers(client, ORG_UID);
119+
120+
expect(result.items).to.have.lengthOf(225);
121+
expect(getInvitations.callCount).to.equal(3);
122+
expect(invitationParams[0]).to.deep.equal({ skip: 0, page: 1, limit: config.limit });
123+
expect(invitationParams[1]).to.deep.equal({ skip: config.limit, page: 2, limit: config.limit });
124+
expect(invitationParams[2]).to.deep.equal({
125+
skip: config.limit * 2,
126+
page: 3,
127+
limit: config.limit,
128+
});
129+
});
130+
131+
it('should paginate getInvitations for organization admins', async () => {
132+
const page1 = Array.from({ length: config.limit }, (_, i) => makeUser(i));
133+
const page2 = Array.from({ length: config.limit }, (_, i) => makeUser(i + config.limit));
134+
const page3 = Array.from({ length: 25 }, (_, i) => makeUser(i + config.limit * 2));
135+
136+
const { client, getInvitations, invitationParams } = createPaginatedMockClient(
137+
{ is_owner: false, org_roles: [{ admin: true }] },
138+
[page1, page2, page3],
139+
);
140+
141+
const result = await getOrgUsers(client, ORG_UID);
142+
143+
expect(result.items).to.have.lengthOf(225);
144+
expect(getInvitations.callCount).to.equal(3);
145+
expect(invitationParams[0]).to.deep.equal({ skip: 0, page: 1, limit: config.limit });
146+
});
147+
148+
it('should reject when user is neither owner nor admin', async () => {
149+
const { client, getInvitations } = createPaginatedMockClient(
150+
{ is_owner: false, org_roles: [{ admin: false }] },
151+
[[]],
152+
);
153+
154+
try {
155+
await getOrgUsers(client, ORG_UID);
156+
expect.fail('Expected getOrgUsers to reject');
157+
} catch (error) {
158+
expect(error).to.be.instanceOf(Error);
159+
expect((error as Error).message).to.equal(messages.ERROR_ADMIN_ACCESS_DENIED);
160+
}
161+
162+
expect(getInvitations.callCount).to.equal(0);
163+
});
164+
});
52165
});

0 commit comments

Comments
 (0)