diff --git a/packages/contentstack-export-to-csv/package.json b/packages/contentstack-export-to-csv/package.json index a7bf6d363..edbcfd0ac 100644 --- a/packages/contentstack-export-to-csv/package.json +++ b/packages/contentstack-export-to-csv/package.json @@ -1,7 +1,7 @@ { "name": "@contentstack/cli-cm-export-to-csv", "description": "Export entries, taxonomies, terms, or organization users to CSV", - "version": "2.0.0-beta.7", + "version": "2.0.0-beta.8", "author": "Contentstack", "bugs": "https://github.com/contentstack/cli/issues", "dependencies": { diff --git a/packages/contentstack-export-to-csv/src/utils/api-client.ts b/packages/contentstack-export-to-csv/src/utils/api-client.ts index cec010028..887a0514e 100644 --- a/packages/contentstack-export-to-csv/src/utils/api-client.ts +++ b/packages/contentstack-export-to-csv/src/utils/api-client.ts @@ -138,22 +138,16 @@ export function getOrgUsers(managementAPIClient: ManagementClient, orgUid: strin return reject(new Error('Org UID not found.')); } - if (organization.is_owner === true) { - return managementAPIClient - .organization(organization.uid) - .getInvitations() - .then((data: unknown) => { - resolve(data as OrgUsersResponse); - }) - .catch(reject); - } - - if (!organization.getInvitations && !find(organization.org_roles, 'admin')) { + if (!organization.is_owner && !find(organization.org_roles, 'admin')) { return reject(new Error(messages.ERROR_ADMIN_ACCESS_DENIED)); } try { - const users = await getUsers(managementAPIClient, { uid: organization.uid }, { skip: 0, page: 1, limit: 100 }); + const users = await getUsers( + managementAPIClient, + { uid: organization.uid }, + { skip: 0, page: 1, limit: config.limit }, + ); return resolve({ items: users || [] }); } catch (error) { return reject(error); @@ -175,15 +169,18 @@ async function getUsers( try { const users = await managementAPIClient.organization(organization.uid).getInvitations(params) as unknown as OrgUsersResponse; - if (!users.items || (users.items && !users.items.length)) { + if (!users.items || users.items.length < params.limit) { + if (users.items?.length) { + result = result.concat(users.items); + } return result; - } else { - result = result.concat(users.items); - params.skip = params.page * params.limit; - params.page++; - await wait(200); - return getUsers(managementAPIClient, organization, params, result); } + + result = result.concat(users.items); + params.skip = params.page * params.limit; + params.page++; + await wait(200); + return getUsers(managementAPIClient, organization, params, result); } catch { return result; } diff --git a/packages/contentstack-export-to-csv/test/unit/utils/api-client.functional.test.ts b/packages/contentstack-export-to-csv/test/unit/utils/api-client.functional.test.ts index 6d93b3ec3..8b0310f04 100644 --- a/packages/contentstack-export-to-csv/test/unit/utils/api-client.functional.test.ts +++ b/packages/contentstack-export-to-csv/test/unit/utils/api-client.functional.test.ts @@ -131,7 +131,8 @@ describe('api-client functional', () => { }), } as any; const res = await getOrgUsers(client, 'org-1'); - expect(res).to.equal(invitations); + expect(res.items).to.have.lengthOf(1); + expect(res.items[0].email).to.equal('a@b.com'); }); it('getOrgUsers rejects when org uid missing', async () => { @@ -148,8 +149,8 @@ describe('api-client functional', () => { it('getOrgUsers resolves paginated invitations for non-owner admin org', async () => { const getInvitations = sandbox.stub(); - getInvitations.onFirstCall().resolves({ items: [{ email: 'a@b.com' }] }); - getInvitations.onSecondCall().resolves({ items: [] }); + getInvitations.onFirstCall().resolves({ items: Array.from({ length: 100 }, (_, i) => ({ email: `u${i}@b.com` })) }); + getInvitations.onSecondCall().resolves({ items: [{ email: 'a@b.com' }] }); const client = { getUser: () => Promise.resolve({ @@ -160,7 +161,7 @@ describe('api-client functional', () => { }), } as any; const res = await getOrgUsers(client, 'org-1'); - expect(res.items).to.have.lengthOf(1); + expect(res.items).to.have.lengthOf(101); expect(getInvitations.calledTwice).to.equal(true); }); diff --git a/packages/contentstack-export-to-csv/test/unit/utils/api-client.test.ts b/packages/contentstack-export-to-csv/test/unit/utils/api-client.test.ts index cad6b7781..c3b1b3ec7 100644 --- a/packages/contentstack-export-to-csv/test/unit/utils/api-client.test.ts +++ b/packages/contentstack-export-to-csv/test/unit/utils/api-client.test.ts @@ -1,4 +1,8 @@ import { expect } from 'chai'; +import sinon from 'sinon'; +import config from '../../../src/config'; +import { messages } from '../../../src/messages'; +import * as errorHandler from '../../../src/utils/error-handler'; import { getOrganizations, getOrganizationsWhereUserIsAdmin, @@ -19,8 +23,66 @@ import { getTaxonomy, createImportableCSV, } from '../../../src/utils/api-client'; +import type { ManagementClient, OrgUser } from '../../../src/types'; + +const ORG_UID = 'org-uid'; + +function makeUser(index: number): OrgUser { + return { + email: `user${index}@example.com`, + user_uid: `uid-${index}`, + invited_by: 'system', + status: 'accepted', + created_at: '2020-01-01T00:00:00.000Z', + updated_at: '2020-01-01T00:00:00.000Z', + }; +} + +function createPaginatedMockClient( + organization: { + is_owner?: boolean; + org_roles?: Array<{ admin?: boolean }>; + }, + pages: OrgUser[][], +): { client: ManagementClient; getInvitations: sinon.SinonStub; invitationParams: Array> } { + const invitationParams: Array> = []; + const getInvitations = sinon.stub().callsFake(async (params: Record) => { + invitationParams.push({ ...params }); + const callIndex = getInvitations.callCount - 1; + const items = pages[callIndex] ?? []; + return { items }; + }); + + const organizationClient = { getInvitations }; + const organizationStub = sinon.stub().returns(organizationClient); + + const client = { + getUser: sinon.stub().resolves({ + organizations: [ + { + uid: ORG_UID, + name: 'Test Org', + ...organization, + }, + ], + }), + organization: organizationStub, + } as unknown as ManagementClient; + + return { client, getInvitations, invitationParams }; +} describe('api-client', () => { + let waitStub: sinon.SinonStub; + + beforeEach(() => { + waitStub = sinon.stub(errorHandler, 'wait').resolves(); + }); + + afterEach(() => { + waitStub.restore(); + }); + describe('module exports', () => { it('should export all expected functions', () => { expect(getOrganizations).to.be.a('function'); @@ -44,5 +106,62 @@ describe('api-client', () => { }); }); - // Note: Functional tests use mocked SDK chains; keep in a dedicated file when re-adding coverage. + describe('getOrgUsers', () => { + it('should paginate getInvitations for organization owners', async () => { + const page1 = Array.from({ length: config.limit }, (_, i) => makeUser(i)); + const page2 = Array.from({ length: config.limit }, (_, i) => makeUser(i + config.limit)); + const page3 = Array.from({ length: 25 }, (_, i) => makeUser(i + config.limit * 2)); + + const { client, getInvitations, invitationParams } = createPaginatedMockClient( + { is_owner: true }, + [page1, page2, page3], + ); + + const result = await getOrgUsers(client, ORG_UID); + + expect(result.items).to.have.lengthOf(225); + expect(getInvitations.callCount).to.equal(3); + expect(invitationParams[0]).to.deep.equal({ skip: 0, page: 1, limit: config.limit }); + expect(invitationParams[1]).to.deep.equal({ skip: config.limit, page: 2, limit: config.limit }); + expect(invitationParams[2]).to.deep.equal({ + skip: config.limit * 2, + page: 3, + limit: config.limit, + }); + }); + + it('should paginate getInvitations for organization admins', async () => { + const page1 = Array.from({ length: config.limit }, (_, i) => makeUser(i)); + const page2 = Array.from({ length: config.limit }, (_, i) => makeUser(i + config.limit)); + const page3 = Array.from({ length: 25 }, (_, i) => makeUser(i + config.limit * 2)); + + const { client, getInvitations, invitationParams } = createPaginatedMockClient( + { is_owner: false, org_roles: [{ admin: true }] }, + [page1, page2, page3], + ); + + const result = await getOrgUsers(client, ORG_UID); + + expect(result.items).to.have.lengthOf(225); + expect(getInvitations.callCount).to.equal(3); + expect(invitationParams[0]).to.deep.equal({ skip: 0, page: 1, limit: config.limit }); + }); + + it('should reject when user is neither owner nor admin', async () => { + const { client, getInvitations } = createPaginatedMockClient( + { is_owner: false, org_roles: [{ admin: false }] }, + [[]], + ); + + try { + await getOrgUsers(client, ORG_UID); + expect.fail('Expected getOrgUsers to reject'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.equal(messages.ERROR_ADMIN_ACCESS_DENIED); + } + + expect(getInvitations.called).to.equal(false); + }); + }); });