Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/contentstack-export-to-csv/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
35 changes: 16 additions & 19 deletions packages/contentstack-export-to-csv/src/utils/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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({
Expand All @@ -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);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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<Record<string, number>> } {
const invitationParams: Array<Record<string, number>> = [];
const getInvitations = sinon.stub().callsFake(async (params: Record<string, number>) => {
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');
Expand All @@ -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);
});
});
});
Loading