diff --git a/README.md b/README.md index f6e5c523..bc13d2a4 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ A _Good Enough_ offline emulator for [Amazon Cognito](https://aws.amazon.com/cog | DeleteResourceServer | ❌ | | DeleteUser | ✅² | | DeleteUserAttributes | ✅ | -| DeleteUserPool | ❌ | +| DeleteUserPool | ✅² | | DeleteUserPoolClient | ✅² | | DeleteUserPoolDomain | ❌ | | DescribeIdentityProvider | ❌ | diff --git a/integration-tests/aws-sdk/deleteUserPool.test.ts b/integration-tests/aws-sdk/deleteUserPool.test.ts new file mode 100644 index 00000000..7f13bf1c --- /dev/null +++ b/integration-tests/aws-sdk/deleteUserPool.test.ts @@ -0,0 +1,41 @@ +import { withCognitoSdk } from "./setup"; + +describe( + "CognitoIdentityServiceProvider.deleteUserPool", + withCognitoSdk((Cognito) => { + it("deletes a group", async () => { + const client = Cognito(); + + // create the user pool + const up = await client + .createUserPool({ + PoolName: "newPool", + }) + .promise(); + + const listResponse = await client + .listUserPools({ + MaxResults: 1, + }) + .promise(); + + expect(listResponse.UserPools).toEqual([ + expect.objectContaining({ + Id: up.UserPool?.Id, + }), + ]); + + await client + .deleteUserPool({ + UserPoolId: up.UserPool?.Id!, + }) + .promise(); + + const listResponseAfter = await client + .listUserPools({ MaxResults: 1 }) + .promise(); + + expect(listResponseAfter.UserPools).toHaveLength(0); + }); + }) +); diff --git a/integration-tests/aws-sdk/deleteUserPoolClient.test.ts b/integration-tests/aws-sdk/deleteUserPoolClient.test.ts index 68308737..a51633e2 100644 --- a/integration-tests/aws-sdk/deleteUserPoolClient.test.ts +++ b/integration-tests/aws-sdk/deleteUserPoolClient.test.ts @@ -3,7 +3,7 @@ import { withCognitoSdk } from "./setup"; describe( "CognitoIdentityServiceProvider.deleteUserPoolClient", withCognitoSdk((Cognito) => { - it("deletes a group", async () => { + it("deletes a user pool client", async () => { const client = Cognito(); // create the user pool client diff --git a/integration-tests/cognitoService.test.ts b/integration-tests/cognitoService.test.ts index 93162704..4e7afd1f 100644 --- a/integration-tests/cognitoService.test.ts +++ b/integration-tests/cognitoService.test.ts @@ -75,4 +75,22 @@ describe("Cognito Service", () => { { ...USER_POOL_AWS_DEFAULTS, Id: "test-pool-3" }, ]); }); + + it("deletes user pools", async () => { + const cognitoService = await factory.create(TestContext, {}); + + const up1 = await cognitoService.getUserPool(TestContext, "test-pool-1"); + const up2 = await cognitoService.getUserPool(TestContext, "test-pool-2"); + + expect(fs.existsSync(`${dataDirectory}/test-pool-1.json`)).toBe(true); + expect(fs.existsSync(`${dataDirectory}/test-pool-2.json`)).toBe(true); + + await cognitoService.deleteUserPool(TestContext, up1.config); + + expect(fs.existsSync(`${dataDirectory}/test-pool-1.json`)).not.toBe(true); + + await cognitoService.deleteUserPool(TestContext, up2.config); + + expect(fs.existsSync(`${dataDirectory}/test-pool-2.json`)).not.toBe(true); + }); }); diff --git a/src/__tests__/mockCognitoService.ts b/src/__tests__/mockCognitoService.ts index 8be95fa2..0a1dd2ed 100644 --- a/src/__tests__/mockCognitoService.ts +++ b/src/__tests__/mockCognitoService.ts @@ -6,6 +6,7 @@ export const newMockCognitoService = ( userPoolClient: UserPoolService = newMockUserPoolService() ): jest.Mocked => ({ createUserPool: jest.fn(), + deleteUserPool: jest.fn(), getAppClient: jest.fn(), getUserPool: jest.fn().mockResolvedValue(userPoolClient), getUserPoolForClientId: jest.fn().mockResolvedValue(userPoolClient), diff --git a/src/services/cognitoService.ts b/src/services/cognitoService.ts index e9fa8804..d8713036 100644 --- a/src/services/cognitoService.ts +++ b/src/services/cognitoService.ts @@ -12,10 +12,7 @@ import { UserPoolService, UserPoolServiceFactory, } from "./userPoolService"; -import fs from "fs"; -import { promisify } from "util"; - -const readdir = promisify(fs.readdir); +import fs from "fs/promises"; const CLIENTS_DATABASE_NAME = "clients"; @@ -265,6 +262,7 @@ export const USER_POOL_AWS_DEFAULTS: UserPoolDefaults = { export interface CognitoService { createUserPool(ctx: Context, userPool: UserPool): Promise; + deleteUserPool(ctx: Context, userPool: UserPool): Promise; getAppClient(ctx: Context, clientId: string): Promise; getUserPool(ctx: Context, userPoolId: string): Promise; getUserPoolForClientId( @@ -325,6 +323,14 @@ export class CognitoServiceImpl implements CognitoService { return service.config; } + public async deleteUserPool(ctx: Context, userPool: UserPool): Promise { + ctx.logger.debug( + { userPoolId: userPool.Id }, + "CognitoServiceImpl.deleteUserPool" + ); + await fs.rm(path.join(this.dataDirectory, `${userPool.Id}.json`)); + } + public async getUserPool( ctx: Context, userPoolId: string @@ -378,7 +384,9 @@ export class CognitoServiceImpl implements CognitoService { public async listUserPools(ctx: Context): Promise { ctx.logger.debug("CognitoServiceImpl.listUserPools"); - const entries = await readdir(this.dataDirectory, { withFileTypes: true }); + const entries = await fs.readdir(this.dataDirectory, { + withFileTypes: true, + }); return Promise.all( entries diff --git a/src/targets/deleteUserPool.test.ts b/src/targets/deleteUserPool.test.ts new file mode 100644 index 00000000..059a74ec --- /dev/null +++ b/src/targets/deleteUserPool.test.ts @@ -0,0 +1,38 @@ +import { newMockCognitoService } from "../__tests__/mockCognitoService"; +import { newMockUserPoolService } from "../__tests__/mockUserPoolService"; +import { TestContext } from "../__tests__/testContext"; +import * as TDB from "../__tests__/testDataBuilder"; +import { CognitoService } from "../services"; +import { DeleteUserPool, DeleteUserPoolTarget } from "./deleteUserPool"; + +describe("DeleteUserPool target", () => { + let deleteUserPool: DeleteUserPoolTarget; + let mockCognitoService: jest.Mocked; + + beforeEach(() => { + mockCognitoService = newMockCognitoService(newMockUserPoolService()); + + deleteUserPool = DeleteUserPool({ + cognito: mockCognitoService, + }); + }); + + it("deletes a user pool client", async () => { + const userPool = TDB.userPool(); + + mockCognitoService.getUserPool.mockResolvedValue( + newMockUserPoolService(userPool) + ); + + await deleteUserPool(TestContext, { + UserPoolId: "test", + }); + + expect(mockCognitoService.deleteUserPool).toHaveBeenCalledWith( + TestContext, + userPool + ); + }); + + it.todo("throws if the user pool doesn't exist"); +}); diff --git a/src/targets/deleteUserPool.ts b/src/targets/deleteUserPool.ts new file mode 100644 index 00000000..f964fc83 --- /dev/null +++ b/src/targets/deleteUserPool.ts @@ -0,0 +1,22 @@ +import { DeleteUserPoolRequest } from "aws-sdk/clients/cognitoidentityserviceprovider"; +import { ResourceNotFoundError } from "../errors"; +import { Services } from "../services"; +import { Target } from "./Target"; + +export type DeleteUserPoolTarget = Target; + +type DeleteUserPoolServices = Pick; + +export const DeleteUserPool = + ({ cognito }: DeleteUserPoolServices): DeleteUserPoolTarget => + async (ctx, req) => { + // TODO: from the docs "Calling this action requires developer credentials.", can we enforce this? + const userPool = await cognito.getUserPool(ctx, req.UserPoolId); + if (!userPool) { + throw new ResourceNotFoundError(); + } + + await cognito.deleteUserPool(ctx, userPool.config); + + return {}; + }; diff --git a/src/targets/targets.ts b/src/targets/targets.ts index 638734bc..896dd78f 100644 --- a/src/targets/targets.ts +++ b/src/targets/targets.ts @@ -19,6 +19,7 @@ import { CreateUserPoolClient } from "./createUserPoolClient"; import { DeleteGroup } from "./deleteGroup"; import { DeleteUser } from "./deleteUser"; import { DeleteUserAttributes } from "./deleteUserAttributes"; +import { DeleteUserPool } from "./deleteUserPool"; import { DeleteUserPoolClient } from "./deleteUserPoolClient"; import { DescribeUserPoolClient } from "./describeUserPoolClient"; import { ForgotPassword } from "./forgotPassword"; @@ -59,6 +60,7 @@ export const Targets = { DeleteGroup, DeleteUser, DeleteUserAttributes, + DeleteUserPool, DeleteUserPoolClient, DescribeUserPoolClient, ForgotPassword,