Skip to content

Commit

Permalink
Merge pull request #31 from agiledigital-labs/feature/IE-36/force-del…
Browse files Browse the repository at this point in the history
…ete-user

feature/ie-36 force delete user
  • Loading branch information
dspasojevic authored Oct 20, 2022
2 parents 912e29d + e4c916b commit 5b88885
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 13 deletions.
6 changes: 6 additions & 0 deletions src/scripts/__fixtures__/data-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export const user: User = {
deactivated: false,
};

export const deactivatedUser: User = {
...user,
status: UserStatus.DEPROVISIONED,
deactivated: true,
};

export const group: Group = {
id: 'group_id',
name: 'test group',
Expand Down
174 changes: 174 additions & 0 deletions src/scripts/delete-user.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable functional/no-return-void */
/* eslint-disable functional/no-expression-statement */
/* eslint-disable functional/functional-parameters */
import * as TE from 'fp-ts/TaskEither';
import * as O from 'fp-ts/Option';
import { UserService } from './services/user-service';
import { deleteUser } from './delete-user';

import {
baseUserService,
user,
deactivatedUser,
} from './__fixtures__/data-providers';

describe('Deleting users without using force', () => {
it('passes when attempting to delete a deprovisioned user', async () => {
// Given a deleteUser function that can succsessfully delete a given user
const userService: UserService = {
...baseUserService(),
deleteUser: jest.fn(() => TE.right(deactivatedUser)),
getUser: () => TE.right(O.some(deactivatedUser)),
};

// When we attempt to delete an existing deprovisioned user
const result = await deleteUser(userService, deactivatedUser.id, false)();

// Then we should have a right
expect(result).toEqualRight(deactivatedUser);

// And we also expect delete user to have been called
expect(userService.deleteUser).toHaveBeenCalled();
});

it('fails when attempting to delete a non-deprovisioned user', async () => {
// Given a deleteUser function that can succsessfully delete a given user (which theoretically cannot happen if not deprovisioned)
const userService: UserService = {
...baseUserService(),
deleteUser: jest.fn(() => TE.right(user)),
getUser: () => TE.right(O.some(user)),
};

// When we attempt to delete an existing non-deprovisioned user
const result = await deleteUser(userService, user.id, false)();

// Then we should have a left
expect(result).toEqualLeft(
'User [user_id] has not been deprovisioned. Deprovision before deleting.'
);

// And we also expect delete/decativate user to have never been called
expect(userService.deleteUser).not.toHaveBeenCalled();
expect(userService.deactivateUser).not.toHaveBeenCalled();
});
});

describe('Deleting a user with force', () => {
it('passes when attempting to delete a non-deprovisioned user', async () => {
// Given a deleteUser/deactivateUser function that can succsessfully deactivate and delete a given user
const userService: UserService = {
...baseUserService(),
deleteUser: () => TE.right(user),
deactivateUser: jest.fn(() => TE.right(user)),
getUser: () => TE.right(O.some(user)),
};

// When we attempt to delete an existing non-deprovisioned user
const result = await deleteUser(userService, user.id, true)();

// Then we should have a right
expect(result).toEqualRight(user);

// And we also expect decativate to have been called
expect(userService.deactivateUser).toHaveBeenCalled();
});

it('passes when attempting to delete an already deprovisioned user', async () => {
// Given a deleteUser function that can succsessfully delete a given user
const userService: UserService = {
...baseUserService(),
deleteUser: () => TE.right(deactivatedUser),
getUser: () => TE.right(O.some(deactivatedUser)),
};

// When we attempt to delete an existing non-deprovisioned user
const result = await deleteUser(userService, deactivatedUser.id, true)();

// Then we should have a right
expect(result).toEqualRight(deactivatedUser);

// And we also expect decativate to have not been called
expect(userService.deactivateUser).not.toHaveBeenCalled();
});

it('fails when attempting to deprovision a user fails', async () => {
// Given the deactivateUser function that cannot deactivate a user, but deleteUser and getUser works fine
const userService: UserService = {
...baseUserService(),
deleteUser: jest.fn(() => TE.right(user)),
deactivateUser: () => TE.left('expected error'),
getUser: () => TE.right(O.some(user)),
};

// When we attempt to delete an existing non-deprovisioned user
const result = await deleteUser(userService, user.id, true)();

// Then we should have a left
expect(result).toEqualLeft('expected error');

// And we also expect the user service's deleteUser to not have been called
expect(userService.deleteUser).not.toHaveBeenCalled();
});
});

// Some operations should remain constant regardless of if the force flag is set or not
describe.each([
['with force', true],
['without force', false],
])(
'Deleting a user with force-keyword-independent situations, testing for %s',
(_desc, force) => {
it('fails when attempting to delete a user does not work, %s', async () => {
// Given that getUser works, but deleteUser does not
const userService: UserService = {
...baseUserService(),
deleteUser: () => TE.left('expected error'),
getUser: () => TE.right(O.some(deactivatedUser)),
};

// When we attempt to delete an existing user both with and without force
const result = await deleteUser(userService, deactivatedUser.id, force)();

// Then we should have a left
expect(result).toEqualLeft('expected error');
});

it('fails when attempting to delete a non-existent user, %s', async () => {
// Given that deleteUser works, but the user in question does not exist
const userService: UserService = {
...baseUserService(),
deleteUser: jest.fn(() => TE.right(user)),
getUser: () => TE.right(O.none),
};

// When we attempt to delete an existing user both with and without force
const result = await deleteUser(userService, user.id, force)();
// Then we should have a left
expect(result).toEqualLeft(
'User [user_id] does not exist. Can not delete.'
);

// And we also expect the user service's deleteUser to not have been called
expect(userService.deleteUser).not.toHaveBeenCalled();
});

it('fails when retreiving the user fails, %s', async () => {
// Given that deleteUser works, but getUser retrieves no valid users
const userService: UserService = {
...baseUserService(),
deleteUser: jest.fn(() => TE.right(user)),
getUser: () => TE.left('expected error'),
};

// When we attempt to delete an existing user both with and without force
const result = await deleteUser(userService, user.id, force)();

// Then we should have a left
expect(result).toEqualLeft('expected error');

// And we also expect the user service's deleteUser to not have been called
expect(userService.deleteUser).not.toHaveBeenCalled();
});
}
);
42 changes: 29 additions & 13 deletions src/scripts/delete-user.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { Argv } from 'yargs';
import { RootCommand } from '..';

import { UserService, OktaUserService } from './services/user-service';
import { UserService, OktaUserService, User } from './services/user-service';
import { oktaManageClient } from './services/client-service';
import * as TE from 'fp-ts/lib/TaskEither';
import * as E from 'fp-ts/lib/Either';
import * as O from 'fp-ts/lib/Option';
import { flow, pipe } from 'fp-ts/lib/function';
import * as Console from 'fp-ts/lib/Console';

const deleteUser = (
/**
* Deletes a user belonging to an Okta organisation/client
* @param service the service used to communicate with the client
* @param userId the ID of the user to be deleted
* @param force whether to delete the user regardless of if they are deprovisioned or not
* @returns the deleted user
*/
export const deleteUser = (
service: UserService,
userId: string
): TE.TaskEither<string, unknown> =>
userId: string,
force: boolean
): TE.TaskEither<string, User> =>
pipe(
userId,
service.getUser,
Expand All @@ -25,6 +33,11 @@ const deleteUser = (
(user) =>
user.deactivated
? service.deleteUser(userId)
: force
? pipe(
service.deactivateUser(userId),
TE.chain((user: User) => service.deleteUser(user.id))
)
: TE.left(
`User [${userId}] has not been deprovisioned. Deprovision before deleting.`
)
Expand All @@ -44,31 +57,34 @@ export default (
readonly privateKey: string;
readonly organisationUrl: string;
readonly userId: string;
readonly force: boolean;
}> =>
rootCommand.command(
'delete-user [user-id]',
'Deletes the specified user. Only works if user status is deprovisioned.',
'Deletes the specified user. Only works if user status is deprovisioned or if the --force argument is included.',
// eslint-disable-next-line functional/no-return-void, @typescript-eslint/prefer-readonly-parameter-types
(yargs) => {
// eslint-disable-next-line functional/no-expression-statement
yargs.positional('user-id', {
describe: 'the identifier of the user to delete',
type: 'string',
demandOption: true,
});
yargs
.positional('user-id', {
describe: 'the identifier of the user to delete',
type: 'string',
demandOption: true,
})
.boolean('force')
.describe('force', 'force delete the user regardless of their status');
},
async (args: {
readonly clientId: string;
readonly privateKey: string;
readonly organisationUrl: string;
readonly userId: string;
readonly force: boolean;
}) => {
// eslint-disable-next-line functional/no-try-statement

const client = oktaManageClient({ ...args });
const service = new OktaUserService(client);

const result = await deleteUser(service, args.userId)();
const result = await deleteUser(service, args.userId, args.force)();

// eslint-disable-next-line functional/no-conditional-statement
if (E.isLeft(result)) {
Expand Down

0 comments on commit 5b88885

Please sign in to comment.