forked from jagregory/cognito-local
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): support for addCustomAttribute
- Loading branch information
Showing
6 changed files
with
312 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { USER_POOL_AWS_DEFAULTS } from "../../src/services/cognitoService"; | ||
import { withCognitoSdk } from "./setup"; | ||
|
||
describe( | ||
"CognitoIdentityServiceProvider.addCustomAttributes", | ||
withCognitoSdk((Cognito) => { | ||
it("updates a user pool", async () => { | ||
const client = Cognito(); | ||
|
||
// create the user pool client | ||
const up = await client | ||
.createUserPool({ | ||
PoolName: "pool", | ||
}) | ||
.promise(); | ||
|
||
const describeResponse = await client | ||
.describeUserPool({ | ||
UserPoolId: up.UserPool?.Id!, | ||
}) | ||
.promise(); | ||
|
||
expect(describeResponse.UserPool).toMatchObject({ | ||
SchemaAttributes: USER_POOL_AWS_DEFAULTS.SchemaAttributes, | ||
}); | ||
|
||
await client | ||
.addCustomAttributes({ | ||
UserPoolId: up.UserPool?.Id!, | ||
CustomAttributes: [ | ||
{ | ||
AttributeDataType: "String", | ||
Name: "test", | ||
}, | ||
], | ||
}) | ||
.promise(); | ||
|
||
const describeResponseAfterUpdate = await client | ||
.describeUserPool({ | ||
UserPoolId: up.UserPool?.Id!, | ||
}) | ||
.promise(); | ||
|
||
expect(describeResponseAfterUpdate.UserPool).toMatchObject({ | ||
SchemaAttributes: [ | ||
...(USER_POOL_AWS_DEFAULTS.SchemaAttributes ?? []), | ||
{ | ||
AttributeDataType: "String", | ||
DeveloperOnlyAttribute: false, | ||
Mutable: true, | ||
Name: "custom:test", | ||
Required: false, | ||
StringAttributeConstraints: {}, | ||
}, | ||
], | ||
}); | ||
}); | ||
}) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { ClockFake } from "../__tests__/clockFake"; | ||
import { newMockCognitoService } from "../__tests__/mockCognitoService"; | ||
import { newMockUserPoolService } from "../__tests__/mockUserPoolService"; | ||
import { TestContext } from "../__tests__/testContext"; | ||
import * as TDB from "../__tests__/testDataBuilder"; | ||
import { | ||
GroupNotFoundError, | ||
InvalidParameterError, | ||
UserNotFoundError, | ||
} from "../errors"; | ||
import { CognitoService, UserPoolService } from "../services"; | ||
import { | ||
AddCustomAttributes, | ||
AddCustomAttributesTarget, | ||
} from "./addCustomAttributes"; | ||
|
||
const originalDate = new Date(); | ||
|
||
describe("AddCustomAttributes target", () => { | ||
let addCustomAttributes: AddCustomAttributesTarget; | ||
let clock: ClockFake; | ||
let mockCognitoService: jest.Mocked<CognitoService>; | ||
|
||
beforeEach(() => { | ||
clock = new ClockFake(originalDate); | ||
|
||
mockCognitoService = newMockCognitoService(); | ||
addCustomAttributes = AddCustomAttributes({ | ||
clock, | ||
cognito: mockCognitoService, | ||
}); | ||
}); | ||
|
||
it("appends a custom attribute to the user pool", async () => { | ||
const userPool = TDB.userPool(); | ||
const mockUserPoolService = newMockUserPoolService(userPool); | ||
|
||
mockCognitoService.getUserPool.mockResolvedValue(mockUserPoolService); | ||
|
||
const newDate = new Date(); | ||
clock.advanceTo(newDate); | ||
|
||
await addCustomAttributes(TestContext, { | ||
UserPoolId: "test", | ||
CustomAttributes: [ | ||
{ | ||
AttributeDataType: "String", | ||
Name: "test", | ||
}, | ||
], | ||
}); | ||
|
||
expect(mockUserPoolService.updateOptions).toHaveBeenCalledWith( | ||
TestContext, | ||
{ | ||
...userPool, | ||
SchemaAttributes: [ | ||
...(userPool.SchemaAttributes ?? []), | ||
{ | ||
Name: "custom:test", | ||
AttributeDataType: "String", | ||
DeveloperOnlyAttribute: false, | ||
Mutable: true, | ||
Required: false, | ||
StringAttributeConstraints: {}, | ||
}, | ||
], | ||
LastModifiedDate: newDate, | ||
} | ||
); | ||
}); | ||
|
||
it("can create a custom attribute with no name", async () => { | ||
const userPool = TDB.userPool(); | ||
const mockUserPoolService = newMockUserPoolService(userPool); | ||
|
||
mockCognitoService.getUserPool.mockResolvedValue(mockUserPoolService); | ||
|
||
const newDate = new Date(); | ||
clock.advanceTo(newDate); | ||
|
||
await addCustomAttributes(TestContext, { | ||
UserPoolId: "test", | ||
CustomAttributes: [ | ||
{ | ||
AttributeDataType: "String", | ||
}, | ||
], | ||
}); | ||
|
||
expect(mockUserPoolService.updateOptions).toHaveBeenCalledWith( | ||
TestContext, | ||
{ | ||
...userPool, | ||
SchemaAttributes: [ | ||
...(userPool.SchemaAttributes ?? []), | ||
{ | ||
Name: "custom:null", | ||
AttributeDataType: "String", | ||
DeveloperOnlyAttribute: false, | ||
Mutable: true, | ||
Required: false, | ||
StringAttributeConstraints: {}, | ||
}, | ||
], | ||
LastModifiedDate: newDate, | ||
} | ||
); | ||
}); | ||
|
||
it("throws if an attribute with the name already exists", async () => { | ||
const userPool = TDB.userPool({ | ||
SchemaAttributes: [ | ||
{ | ||
Name: "custom:test", | ||
AttributeDataType: "String", | ||
DeveloperOnlyAttribute: false, | ||
Mutable: true, | ||
Required: false, | ||
StringAttributeConstraints: {}, | ||
}, | ||
], | ||
}); | ||
const mockUserPoolService = newMockUserPoolService(userPool); | ||
|
||
mockCognitoService.getUserPool.mockResolvedValue(mockUserPoolService); | ||
|
||
await expect( | ||
addCustomAttributes(TestContext, { | ||
UserPoolId: "test", | ||
CustomAttributes: [ | ||
{ | ||
AttributeDataType: "String", | ||
Name: "test", | ||
}, | ||
], | ||
}) | ||
).rejects.toEqual( | ||
new InvalidParameterError( | ||
"custom:test: Existing attribute already has name custom:test." | ||
) | ||
); | ||
|
||
expect(mockUserPoolService.updateOptions).not.toHaveBeenCalled(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { | ||
AddCustomAttributesRequest, | ||
AddCustomAttributesResponse, | ||
} from "aws-sdk/clients/cognitoidentityserviceprovider"; | ||
import { InvalidParameterError } from "../errors"; | ||
import { Services } from "../services"; | ||
import { assertParameterLength } from "./utils/assertions"; | ||
import { Target } from "./Target"; | ||
|
||
export type AddCustomAttributesTarget = Target< | ||
AddCustomAttributesRequest, | ||
AddCustomAttributesResponse | ||
>; | ||
|
||
type AddCustomAttributesServices = Pick<Services, "clock" | "cognito">; | ||
|
||
export const AddCustomAttributes = | ||
({ | ||
clock, | ||
cognito, | ||
}: AddCustomAttributesServices): AddCustomAttributesTarget => | ||
async (ctx, req) => { | ||
assertParameterLength("CustomAttributes", 1, 25, req.CustomAttributes); | ||
|
||
const userPool = await cognito.getUserPool(ctx, req.UserPoolId); | ||
|
||
await userPool.updateOptions(ctx, { | ||
...userPool.options, | ||
SchemaAttributes: [ | ||
...(userPool.options.SchemaAttributes ?? []), | ||
...req.CustomAttributes.map(({ Name, ...attr }) => { | ||
const name = `custom:${Name ?? "null"}`; | ||
|
||
if (userPool.options.SchemaAttributes?.find((x) => x.Name === name)) { | ||
throw new InvalidParameterError( | ||
`${name}: Existing attribute already has name ${name}.` | ||
); | ||
} | ||
|
||
return { | ||
AttributeDataType: "String", | ||
DeveloperOnlyAttribute: false, | ||
Mutable: true, | ||
Required: false, | ||
StringAttributeConstraints: {}, | ||
Name: name, | ||
...attr, | ||
}; | ||
}), | ||
], | ||
LastModifiedDate: clock.get(), | ||
}); | ||
|
||
return {}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { InvalidParameterError } from "../../errors"; | ||
|
||
/** | ||
* Assert a required parameter has a value. Throws an InvalidParameterError. | ||
* | ||
* @param name Name of the parameter to include in the error message | ||
* @param parameter Parameter to assert | ||
* @param message Custom full message for the error, optional | ||
*/ | ||
export function assertRequiredParameter<T>( | ||
name: string, | ||
parameter: T, | ||
message?: string | ||
): asserts parameter is NonNullable<T> { | ||
if (!parameter) { | ||
throw new InvalidParameterError( | ||
message ?? `Missing required parameter ${name}` | ||
); | ||
} | ||
} | ||
|
||
/** | ||
* Asserts an array parameter has a length between min and max. Throws an InvalidParameterError. | ||
* | ||
* @param name Name of the parameter to include in the error message | ||
* @param min Minimum length | ||
* @param max Maximum length | ||
* @param parameter Parameter to assert | ||
*/ | ||
export function assertParameterLength<T>( | ||
name: string, | ||
min: number, | ||
max: number, | ||
parameter: readonly T[] | ||
): asserts parameter { | ||
if (parameter.length < min) { | ||
throw new InvalidParameterError( | ||
`Invalid length for parameter ${name}, value: ${parameter.length}, valid min length: ${min}` | ||
); | ||
} | ||
if (parameter.length > max) { | ||
throw new InvalidParameterError( | ||
`Invalid length for parameter ${name}, value: ${parameter.length}, valid max length: ${max}` | ||
); | ||
} | ||
} |