Skip to content

Commit 3d0e9a0

Browse files
committed
feat(api): deleteUserAttributes full support
1 parent 1a47086 commit 3d0e9a0

File tree

6 files changed

+235
-2
lines changed

6 files changed

+235
-2
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ A _Good Enough_ offline emulator for [Amazon Cognito](https://aws.amazon.com/cog
7272
| DeleteIdentityProvider ||
7373
| DeleteResourceServer ||
7474
| DeleteUser ||
75-
| DeleteUserAttributes | |
75+
| DeleteUserAttributes | |
7676
| DeleteUserPool ||
7777
| DeleteUserPoolClient ||
7878
| DeleteUserPoolDomain ||
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { UUID } from "../../src/__tests__/patterns";
2+
import { withCognitoSdk } from "./setup";
3+
4+
describe(
5+
"CognitoIdentityServiceProvider.deleteUserAttributes",
6+
withCognitoSdk((Cognito) => {
7+
it("updates a user's attributes", async () => {
8+
const client = Cognito();
9+
10+
const pool = await client
11+
.createUserPool({
12+
PoolName: "test",
13+
AutoVerifiedAttributes: ["email"],
14+
})
15+
.promise();
16+
const userPoolId = pool.UserPool?.Id as string;
17+
18+
const upc = await client
19+
.createUserPoolClient({
20+
UserPoolId: userPoolId,
21+
ClientName: "test",
22+
})
23+
.promise();
24+
25+
await client
26+
.adminCreateUser({
27+
UserAttributes: [
28+
{ Name: "email", Value: "example@example.com" },
29+
{ Name: "custom:example", Value: "1" },
30+
],
31+
Username: "abc",
32+
UserPoolId: userPoolId,
33+
TemporaryPassword: "def",
34+
DesiredDeliveryMediums: ["EMAIL"],
35+
})
36+
.promise();
37+
38+
await client
39+
.adminConfirmSignUp({
40+
UserPoolId: userPoolId,
41+
Username: "abc",
42+
})
43+
.promise();
44+
45+
// login as the user
46+
const initiateAuthResponse = await client
47+
.initiateAuth({
48+
AuthFlow: "USER_PASSWORD_AUTH",
49+
AuthParameters: {
50+
USERNAME: "abc",
51+
PASSWORD: "def",
52+
},
53+
ClientId: upc.UserPoolClient?.ClientId as string,
54+
})
55+
.promise();
56+
57+
let user = await client
58+
.adminGetUser({
59+
UserPoolId: userPoolId,
60+
Username: "abc",
61+
})
62+
.promise();
63+
64+
expect(user.UserAttributes).toEqual([
65+
{ Name: "sub", Value: expect.stringMatching(UUID) },
66+
{ Name: "email", Value: "example@example.com" },
67+
{ Name: "custom:example", Value: "1" },
68+
]);
69+
70+
await client
71+
.deleteUserAttributes({
72+
AccessToken: initiateAuthResponse.AuthenticationResult
73+
?.AccessToken as string,
74+
UserAttributeNames: ["custom:example"],
75+
})
76+
.promise();
77+
78+
user = await client
79+
.adminGetUser({
80+
UserPoolId: userPoolId,
81+
Username: "abc",
82+
})
83+
.promise();
84+
85+
expect(user.UserAttributes).toEqual([
86+
{ Name: "sub", Value: expect.stringMatching(UUID) },
87+
{ Name: "email", Value: "example@example.com" },
88+
]);
89+
});
90+
})
91+
);

integration-tests/aws-sdk/updateUserAttributes.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Pino from "pino";
21
import { UUID } from "../../src/__tests__/patterns";
32
import { withCognitoSdk } from "./setup";
43

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import jwt from "jsonwebtoken";
2+
import * as uuid from "uuid";
3+
import { ClockFake } from "../__tests__/clockFake";
4+
import { newMockCognitoService } from "../__tests__/mockCognitoService";
5+
import { newMockUserPoolService } from "../__tests__/mockUserPoolService";
6+
import { TestContext } from "../__tests__/testContext";
7+
import { InvalidParameterError, NotAuthorizedError } from "../errors";
8+
import PrivateKey from "../keys/cognitoLocal.private.json";
9+
import { UserPoolService } from "../services";
10+
import { attribute } from "../services/userPoolService";
11+
import {
12+
DeleteUserAttributes,
13+
DeleteUserAttributesTarget,
14+
} from "./deleteUserAttributes";
15+
import * as TDB from "../__tests__/testDataBuilder";
16+
17+
const clock = new ClockFake(new Date());
18+
19+
const validToken = jwt.sign(
20+
{
21+
sub: "0000-0000",
22+
event_id: "0",
23+
token_use: "access",
24+
scope: "aws.cognito.signin.user.admin",
25+
auth_time: new Date(),
26+
jti: uuid.v4(),
27+
client_id: "test",
28+
username: "0000-0000",
29+
},
30+
PrivateKey.pem,
31+
{
32+
algorithm: "RS256",
33+
issuer: `http://localhost:9229/test`,
34+
expiresIn: "24h",
35+
keyid: "CognitoLocal",
36+
}
37+
);
38+
39+
describe("DeleteUserAttributes target", () => {
40+
let deleteUserAttributes: DeleteUserAttributesTarget;
41+
let mockUserPoolService: jest.Mocked<UserPoolService>;
42+
43+
beforeEach(() => {
44+
mockUserPoolService = newMockUserPoolService();
45+
deleteUserAttributes = DeleteUserAttributes({
46+
clock,
47+
cognito: newMockCognitoService(mockUserPoolService),
48+
});
49+
});
50+
51+
it("throws if the user doesn't exist", async () => {
52+
mockUserPoolService.getUserByUsername.mockResolvedValue(null);
53+
54+
await expect(
55+
deleteUserAttributes(TestContext, {
56+
AccessToken: validToken,
57+
UserAttributeNames: ["custom:example"],
58+
})
59+
).rejects.toEqual(new NotAuthorizedError());
60+
});
61+
62+
it("throws if the token is invalid", async () => {
63+
await expect(
64+
deleteUserAttributes(TestContext, {
65+
AccessToken: "invalid token",
66+
UserAttributeNames: ["custom:example"],
67+
})
68+
).rejects.toEqual(new InvalidParameterError());
69+
});
70+
71+
it("saves the updated attributes on the user", async () => {
72+
const user = TDB.user({
73+
Attributes: [
74+
attribute("email", "example@example.com"),
75+
attribute("custom:example", "1"),
76+
],
77+
});
78+
79+
mockUserPoolService.getUserByUsername.mockResolvedValue(user);
80+
81+
await deleteUserAttributes(TestContext, {
82+
AccessToken: validToken,
83+
UserAttributeNames: ["custom:example"],
84+
});
85+
86+
expect(mockUserPoolService.saveUser).toHaveBeenCalledWith(TestContext, {
87+
...user,
88+
Attributes: [attribute("email", "example@example.com")],
89+
UserLastModifiedDate: clock.get(),
90+
});
91+
});
92+
});

src/targets/deleteUserAttributes.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
DeleteUserAttributesRequest,
3+
DeleteUserAttributesResponse,
4+
} from "aws-sdk/clients/cognitoidentityserviceprovider";
5+
import jwt from "jsonwebtoken";
6+
import { InvalidParameterError, NotAuthorizedError } from "../errors";
7+
import { Services } from "../services";
8+
import { Token } from "../services/tokenGenerator";
9+
import { attributesRemove } from "../services/userPoolService";
10+
import { Target } from "./router";
11+
12+
export type DeleteUserAttributesTarget = Target<
13+
DeleteUserAttributesRequest,
14+
DeleteUserAttributesResponse
15+
>;
16+
17+
type DeleteUserAttributesServices = Pick<Services, "clock" | "cognito">;
18+
19+
export const DeleteUserAttributes =
20+
({
21+
clock,
22+
cognito,
23+
}: DeleteUserAttributesServices): DeleteUserAttributesTarget =>
24+
async (ctx, req) => {
25+
const decodedToken = jwt.decode(req.AccessToken) as Token | null;
26+
if (!decodedToken) {
27+
ctx.logger.info("Unable to decode token");
28+
throw new InvalidParameterError();
29+
}
30+
31+
const userPool = await cognito.getUserPoolForClientId(
32+
ctx,
33+
decodedToken.client_id
34+
);
35+
const user = await userPool.getUserByUsername(ctx, decodedToken.sub);
36+
if (!user) {
37+
throw new NotAuthorizedError();
38+
}
39+
40+
const updatedUser = {
41+
...user,
42+
Attributes: attributesRemove(user.Attributes, ...req.UserAttributeNames),
43+
UserLastModifiedDate: clock.get(),
44+
};
45+
46+
await userPool.saveUser(ctx, updatedUser);
47+
48+
return {};
49+
};

src/targets/router.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { CreateGroup } from "./createGroup";
99
import { CreateUserPool } from "./createUserPool";
1010
import { CreateUserPoolClient } from "./createUserPoolClient";
1111
import { DeleteUser } from "./deleteUser";
12+
import { DeleteUserAttributes } from "./deleteUserAttributes";
1213
import { DescribeUserPoolClient } from "./describeUserPoolClient";
1314
import { ForgotPassword } from "./forgotPassword";
1415
import { ChangePassword } from "./changePassword";
@@ -46,6 +47,7 @@ export const Targets = {
4647
CreateUserPool,
4748
CreateUserPoolClient,
4849
DeleteUser,
50+
DeleteUserAttributes,
4951
DescribeUserPoolClient,
5052
ForgotPassword,
5153
GetUser,

0 commit comments

Comments
 (0)