Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,19 @@ const ALL_STEPS_WHEN_NEW_ACCOUNT_AND_RULE = [
'import-destination-certificate',
'create-source-account',
'create-source-bucket',
'create-destination-account',
'create-account',
'create-user',
'create-access-key',
'create-policy',
'attach-policy',
'create-role',
'attach-role-policy',
'create-bucket',
'create-location',
'create-replication-rule',
] as const;

describe('ApplyActionsStep', () => {
it('renders every ISV-worded action label and marks each step Pending... before any event lands', () => {
it('lists every provisioning action to run, all pending, before the setup starts', () => {
server.use(
rest.post(STREAM_URL, (_req, res, ctx) =>
res(ctx.set('Content-Type', 'application/x-ndjson'), ctx.body(ndjson())),
Expand All @@ -81,10 +83,12 @@ describe('ApplyActionsStep', () => {
expect(screen.getByText('Create IAM User')).toBeInTheDocument();
expect(screen.getByText('Generate Access Key')).toBeInTheDocument();
expect(screen.getByText('Create Policy')).toBeInTheDocument();
expect(screen.getByText('Attach Policy to User')).toBeInTheDocument();
expect(screen.getByText('Create IAM Role')).toBeInTheDocument();
expect(screen.getByText('Attach Policy to Role')).toBeInTheDocument();
expect(screen.getByText('Create Target Bucket: target-bucket')).toBeInTheDocument();
expect(screen.getByText('Create Location')).toBeInTheDocument();
expect(screen.getByText('Create Replication Rule')).toBeInTheDocument();
expect(screen.getAllByText('Pending...').length).toBe(10);
expect(screen.getAllByText('Pending...').length).toBe(12);
});

it('uses the destination instance name in the title when one was returned by Verify', () => {
Expand Down Expand Up @@ -117,7 +121,7 @@ describe('ApplyActionsStep', () => {
expect(screen.queryByText(/Authenticate/i)).not.toBeInTheDocument();
});

it('shows Success cells as events land and enables Continue once every step has succeeded', async () => {
it('marks each action Success as it completes and only lets the user continue once all have succeeded', async () => {
server.use(
rest.post(STREAM_URL, (_req, res, ctx) =>
res(
Expand All @@ -136,7 +140,7 @@ describe('ApplyActionsStep', () => {
);
render(<ApplyActionsStep {...VALUES} />, { wrapper: Wrapper });

await waitFor(() => expect(screen.getAllByText('Success').length).toBe(10));
await waitFor(() => expect(screen.getAllByText('Success').length).toBe(12));
const continueButton = screen.getByRole('button', { name: /Continue/i });
await waitFor(() => expect(continueButton).toBeEnabled());
await userEvent.click(continueButton);
Expand Down Expand Up @@ -214,18 +218,18 @@ describe('ApplyActionsStep', () => {
await waitFor(() => expect(requestCount).toBe(2));
});

it('drops create-source-account when the source account already exists', () => {
it('skips creating a source account when the user reuses an existing one', () => {
server.use(
rest.post(STREAM_URL, (_req, res, ctx) =>
res(ctx.set('Content-Type', 'application/x-ndjson'), ctx.body(ndjson())),
),
);
render(<ApplyActionsStep {...VALUES} accountNameType="existing" />, { wrapper: Wrapper });
expect(screen.queryByText(/Create Account on Source/i)).not.toBeInTheDocument();
expect(screen.getAllByText('Pending...').length).toBe(9);
expect(screen.getAllByText('Pending...').length).toBe(11);
});

it('drops create-source-bucket and create-replication-rule when the wizard did not opt into replication rule creation', () => {
it('skips the source bucket, target bucket and replication rule when the user opts out of creating a rule', () => {
server.use(
rest.post(STREAM_URL, (_req, res, ctx) =>
res(ctx.set('Content-Type', 'application/x-ndjson'), ctx.body(ndjson())),
Expand All @@ -235,7 +239,8 @@ describe('ApplyActionsStep', () => {
wrapper: Wrapper,
});
expect(screen.queryByText(/Create Bucket on Source/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Create Target Bucket/i)).not.toBeInTheDocument();
expect(screen.queryByText(/Create Replication Rule/i)).not.toBeInTheDocument();
expect(screen.getAllByText('Pending...').length).toBe(8);
expect(screen.getAllByText('Pending...').length).toBe(9);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,13 @@ export const ApplyActionsStep = (props: Props) => {
createReplicationRule: createReplicationRule === true,
sourceAccountName: accountName ?? '',
sourceBucketName: sourceBucketName ?? '',
targetBucketName: props.targetBucketName ?? '',
destinationAccountName: destinationAccountName ?? '',
},
setup.events,
{ globalErrorMessage },
),
[accountNameType, createReplicationRule, accountName, sourceBucketName, destinationAccountName, setup.events, globalErrorMessage],
[accountNameType, createReplicationRule, accountName, sourceBucketName, props.targetBucketName, destinationAccountName, setup.events, globalErrorMessage],
);

const hasStartedRef = useRef(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,45 @@ const baseInput: StepListInput = {
createReplicationRule: true,
sourceAccountName: 'src-account',
sourceBucketName: 'src-bucket',
targetBucketName: 'target-bucket',
destinationAccountName: 'dest-account',
};

describe('buildStepViews', () => {
it('lists the 10 canonical steps in order when both create-source-account and create-replication-rule apply', () => {
it('lists the full provisioning sequence in order when the user creates a new source account and a replication rule', () => {
const views = buildStepViews(baseInput, []);
expect(views.map((v) => v.id)).toEqual([
'import-destination-certificate',
'create-source-account',
'create-source-bucket',
'create-destination-account',
'create-account',
'create-user',
'create-access-key',
'create-policy',
'attach-policy',
'create-role',
'attach-role-policy',
'create-bucket',
'create-location',
'create-replication-rule',
]);
});

it('numbers steps starting at 1 and interpolates source/destination names', () => {
it('numbers the steps and shows the chosen source and destination account names', () => {
const [first, sourceAcc, sourceBkt, destAcc] = buildStepViews(baseInput, []);
expect(first).toMatchObject({ step: 1, label: 'Import Destination Certificate' });
expect(sourceAcc).toMatchObject({ step: 2, label: 'Create Account on Source: src-account' });
expect(sourceBkt).toMatchObject({ step: 3, label: 'Create Bucket on Source: src-bucket' });
expect(destAcc).toMatchObject({ step: 4, label: 'Create Account on Destination: dest-account' });
});

it('uses ISV wording verbatim for the reused actions', () => {
it('labels the destination IAM chain and the target bucket per the ARTESCA CRR procedure', () => {
const labels = buildStepViews(baseInput, []).map((v) => v.label);
expect(labels).toContain('Create IAM User');
expect(labels).toContain('Generate Access Key');
expect(labels).toContain('Create Policy');
expect(labels).toContain('Attach Policy to User');
expect(labels).toContain('Create IAM Role');
expect(labels).toContain('Attach Policy to Role');
expect(labels).toContain('Create Target Bucket: target-bucket');
expect(labels).toContain('Create Location');
expect(labels).toContain('Create Replication Rule');
});
Expand All @@ -49,28 +54,33 @@ describe('buildStepViews', () => {
expect(views.find((v) => v.id === 'create-source-account')).toBeUndefined();
});

it('drops the two replication-only steps when the wizard did not opt into replication rule creation', () => {
it('drops the replication-only steps when the wizard did not opt into replication rule creation', () => {
const views = buildStepViews({ ...baseInput, createReplicationRule: false }, []);
expect(views.find((v) => v.id === 'create-source-bucket')).toBeUndefined();
expect(views.find((v) => v.id === 'create-bucket')).toBeUndefined();
expect(views.find((v) => v.id === 'create-replication-rule')).toBeUndefined();
});

it('never surfaces authenticate, create-role, attach-role-policy or create-bucket-on-destination', () => {
it('never surfaces the backend authenticate step (it is covered by the Verify wizard step)', () => {
const ids = buildStepViews(baseInput, []).map((v) => v.id) as string[];
for (const id of ['authenticate', 'create-role', 'attach-role-policy', 'create-bucket']) {
expect(ids).not.toContain(id);
expect(ids).not.toContain('authenticate');
});

it('surfaces the destination role, policy attachment and target bucket steps the CRR procedure requires', () => {
const ids = buildStepViews(baseInput, []).map((v) => v.id) as string[];
for (const id of ['create-role', 'attach-role-policy', 'create-bucket']) {
expect(ids).toContain(id);
}
});

it('leaves every step pending until a matching event lands', () => {
it('shows every step as pending before the setup runs', () => {
const views = buildStepViews(baseInput, []);
expect(views.every((v) => v.state === 'pending')).toBe(true);
});

it('marks a step succeeded on step.completed and failed on step.failed with its error message', () => {
it('marks a step done once it completes and shows the reason when one fails', () => {
const events: SetupEvent[] = [
{ event: 'step.completed', step: 'import-destination-certificate', at: 't' },
{ event: 'step.completed', step: 'create-source-account', at: 't' },
{ event: 'step.completed', step: 'create-account', at: 't' },
{
event: 'step.failed',
step: 'create-user',
Expand All @@ -79,23 +89,23 @@ describe('buildStepViews', () => {
},
];
const views = buildStepViews(baseInput, events);
expect(views.find((v) => v.id === 'import-destination-certificate')?.state).toBe('succeeded');
expect(views.find((v) => v.id === 'create-account')?.state).toBe('succeeded');
const failed = views.find((v) => v.id === 'create-user');
expect(failed?.state).toBe('failed');
expect(failed?.errorMessage).toBe('IAM refused CreateUser: entity already exists');
});
});

describe('buildStepViews with a globalErrorMessage', () => {
it('marks the first pending step as failed with the global error message when no step-level failure landed', () => {
describe('when the whole setup fails without pinpointing a step', () => {
it('blames the first step still waiting to run and shows why', () => {
const views = buildStepViews(baseInput, [], { globalErrorMessage: 'network exploded' });
const firstPending = views[0];
expect(firstPending.state).toBe('failed');
expect(firstPending.errorMessage).toBe('network exploded');
for (const v of views.slice(1)) expect(v.state).toBe('pending');
});

it('marks the first still-pending step as failed after some steps already succeeded', () => {
it('blames the first unfinished step once earlier ones have already succeeded', () => {
const events: SetupEvent[] = [
{ event: 'step.completed', step: 'import-destination-certificate', at: 't' },
{ event: 'step.completed', step: 'create-source-account', at: 't' },
Expand All @@ -107,7 +117,7 @@ describe('buildStepViews with a globalErrorMessage', () => {
expect(views[2].errorMessage).toBe('stream ended without a terminal event');
});

it('leaves the step-level failure in place when both a step.failed and a global error are present', () => {
it('keeps a specific step failure visible rather than replacing it with the generic error', () => {
const events: SetupEvent[] = [
{
event: 'step.failed',
Expand Down
17 changes: 13 additions & 4 deletions src/react/locations/CRRSetupWizard/steps/ApplyActionsStep/steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ export type StepId =
| 'import-destination-certificate'
| 'create-source-account'
| 'create-source-bucket'
| 'create-destination-account'
| 'create-account'
| 'create-user'
| 'create-access-key'
| 'create-policy'
| 'attach-policy'
| 'create-role'
| 'attach-role-policy'
| 'create-bucket'
| 'create-location'
| 'create-replication-rule';

Expand All @@ -27,6 +29,7 @@ export type StepListInput = {
createReplicationRule: boolean;
sourceAccountName: string;
sourceBucketName: string;
targetBucketName: string;
destinationAccountName: string;
};

Expand Down Expand Up @@ -57,14 +60,20 @@ const STEPS: StepDef[] = [
label: (i) => `Create Bucket on Source: ${i.sourceBucketName}`,
},
{
id: 'create-destination-account',
id: 'create-account',
when: () => true,
label: (i) => `Create Account on Destination: ${i.destinationAccountName}`,
},
{ id: 'create-user', when: () => true, label: () => 'Create IAM User' },
{ id: 'create-access-key', when: () => true, label: () => 'Generate Access Key' },
{ id: 'create-policy', when: () => true, label: () => 'Create Policy' },
{ id: 'attach-policy', when: () => true, label: () => 'Attach Policy to User' },
{ id: 'create-role', when: () => true, label: () => 'Create IAM Role' },
{ id: 'attach-role-policy', when: () => true, label: () => 'Attach Policy to Role' },
{
id: 'create-bucket',
when: (i) => i.createReplicationRule,
label: (i) => `Create Target Bucket: ${i.targetBucketName}`,
},
{ id: 'create-location', when: () => true, label: () => 'Create Location' },
{
id: 'create-replication-rule',
Expand Down
Loading