diff --git a/README.md b/README.md index e8881a90..2e6bb68b 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ A _Good Enough_ offline emulator for [Amazon Cognito](https://aws.amazon.com/cog | AdminConfirmSignUp | ✅ | | AdminCreateUser | 🕒 (partial support) | | AdminDeleteUser | ✅ | -| AdminDeleteUserAttributes | ❌ | +| AdminDeleteUserAttributes | ✅ | | AdminDisableProviderForUser | ❌ | | AdminDisableUser | ❌ | | AdminEnableUser | ❌ | diff --git a/integration-tests/aws-sdk/adminDeleteUserAttributes.test.ts b/integration-tests/aws-sdk/adminDeleteUserAttributes.test.ts new file mode 100644 index 00000000..baf76d0c --- /dev/null +++ b/integration-tests/aws-sdk/adminDeleteUserAttributes.test.ts @@ -0,0 +1,56 @@ +import { UUID } from "../../src/__tests__/patterns"; +import { withCognitoSdk } from "./setup"; + +describe( + "CognitoIdentityServiceProvider.adminDeleteUserAttributes", + withCognitoSdk((Cognito) => { + it("updates a user's attributes", async () => { + const client = Cognito(); + + await client + .adminCreateUser({ + UserAttributes: [ + { Name: "email", Value: "example@example.com" }, + { Name: "custom:example", Value: "1" }, + ], + Username: "abc", + UserPoolId: "test", + DesiredDeliveryMediums: ["EMAIL"], + }) + .promise(); + + let user = await client + .adminGetUser({ + UserPoolId: "test", + Username: "abc", + }) + .promise(); + + expect(user.UserAttributes).toEqual([ + { Name: "sub", Value: expect.stringMatching(UUID) }, + { Name: "email", Value: "example@example.com" }, + { Name: "custom:example", Value: "1" }, + ]); + + await client + .adminDeleteUserAttributes({ + UserPoolId: "test", + Username: "abc", + UserAttributeNames: ["custom:example"], + }) + .promise(); + + user = await client + .adminGetUser({ + UserPoolId: "test", + Username: "abc", + }) + .promise(); + + expect(user.UserAttributes).toEqual([ + { Name: "sub", Value: expect.stringMatching(UUID) }, + { Name: "email", Value: "example@example.com" }, + ]); + }); + }) +); diff --git a/src/services/userPoolService.ts b/src/services/userPoolService.ts index 1b08f557..2e50bcc2 100644 --- a/src/services/userPoolService.ts +++ b/src/services/userPoolService.ts @@ -66,6 +66,12 @@ export const attributesAppend = ( return attributesFromRecord(attributeSet); }; +export const attributesRemove = ( + attributes: AttributeListType | undefined, + ...toRemove: readonly string[] +): AttributeListType => + attributes?.filter((x) => !toRemove.includes(x.Name)) ?? []; + export const customAttributes = ( attributes: AttributeListType | undefined ): AttributeListType => diff --git a/src/targets/adminDeleteUserAttributes.test.ts b/src/targets/adminDeleteUserAttributes.test.ts new file mode 100644 index 00000000..69d5a56b --- /dev/null +++ b/src/targets/adminDeleteUserAttributes.test.ts @@ -0,0 +1,60 @@ +import { ClockFake } from "../__tests__/clockFake"; +import { newMockCognitoService } from "../__tests__/mockCognitoService"; +import { newMockUserPoolService } from "../__tests__/mockUserPoolService"; +import { TestContext } from "../__tests__/testContext"; +import { NotAuthorizedError } from "../errors"; +import { UserPoolService } from "../services"; +import { attribute } from "../services/userPoolService"; +import { + AdminDeleteUserAttributes, + AdminDeleteUserAttributesTarget, +} from "./adminDeleteUserAttributes"; +import * as TDB from "../__tests__/testDataBuilder"; + +describe("AdminDeleteUserAttributes target", () => { + let adminDeleteUserAttributes: AdminDeleteUserAttributesTarget; + let mockUserPoolService: jest.Mocked; + let clock: ClockFake; + + beforeEach(() => { + mockUserPoolService = newMockUserPoolService(); + clock = new ClockFake(new Date()); + adminDeleteUserAttributes = AdminDeleteUserAttributes({ + clock, + cognito: newMockCognitoService(mockUserPoolService), + }); + }); + + it("throws if the user doesn't exist", async () => { + await expect( + adminDeleteUserAttributes(TestContext, { + UserPoolId: "test", + Username: "abc", + UserAttributeNames: ["custom:example"], + }) + ).rejects.toEqual(new NotAuthorizedError()); + }); + + it("saves the updated attributes on the user", async () => { + const user = TDB.user({ + Attributes: [ + attribute("email", "example@example.com"), + attribute("custom:example", "1"), + ], + }); + + mockUserPoolService.getUserByUsername.mockResolvedValue(user); + + await adminDeleteUserAttributes(TestContext, { + UserPoolId: "test", + Username: "abc", + UserAttributeNames: ["custom:example"], + }); + + expect(mockUserPoolService.saveUser).toHaveBeenCalledWith(TestContext, { + ...user, + Attributes: [attribute("email", "example@example.com")], + UserLastModifiedDate: clock.get(), + }); + }); +}); diff --git a/src/targets/adminDeleteUserAttributes.ts b/src/targets/adminDeleteUserAttributes.ts new file mode 100644 index 00000000..33b5d87f --- /dev/null +++ b/src/targets/adminDeleteUserAttributes.ts @@ -0,0 +1,38 @@ +import { + AdminDeleteUserAttributesRequest, + AdminDeleteUserAttributesResponse, +} from "aws-sdk/clients/cognitoidentityserviceprovider"; +import { NotAuthorizedError } from "../errors"; +import { Services } from "../services"; +import { attributesRemove } from "../services/userPoolService"; +import { Target } from "./router"; + +export type AdminDeleteUserAttributesTarget = Target< + AdminDeleteUserAttributesRequest, + AdminDeleteUserAttributesResponse +>; + +type AdminDeleteUserAttributesServices = Pick; + +export const AdminDeleteUserAttributes = + ({ + clock, + cognito, + }: AdminDeleteUserAttributesServices): AdminDeleteUserAttributesTarget => + async (ctx, req) => { + const userPool = await cognito.getUserPool(ctx, req.UserPoolId); + const user = await userPool.getUserByUsername(ctx, req.Username); + if (!user) { + throw new NotAuthorizedError(); + } + + const updatedUser = { + ...user, + Attributes: attributesRemove(user.Attributes, ...req.UserAttributeNames), + UserLastModifiedDate: clock.get(), + }; + + await userPool.saveUser(ctx, updatedUser); + + return {}; + }; diff --git a/src/targets/router.ts b/src/targets/router.ts index a150bc27..4d63ec89 100644 --- a/src/targets/router.ts +++ b/src/targets/router.ts @@ -1,6 +1,7 @@ import { Logger } from "../log"; import { Services } from "../services"; import { UnsupportedError } from "../errors"; +import { AdminDeleteUserAttributes } from "./adminDeleteUserAttributes"; import { AdminSetUserPassword } from "./adminSetUserPassword"; import { ConfirmForgotPassword } from "./confirmForgotPassword"; import { ConfirmSignUp } from "./confirmSignUp"; @@ -33,6 +34,7 @@ export const Targets = { AdminConfirmSignUp, AdminCreateUser, AdminDeleteUser, + AdminDeleteUserAttributes, AdminGetUser, AdminInitiateAuth, AdminSetUserPassword,