Skip to content

Commit e45388a

Browse files
committed
test(elasticache): add unit tests for users and user group
1 parent b69223d commit e45388a

File tree

6 files changed

+1420
-3
lines changed

6 files changed

+1420
-3
lines changed

packages/@aws-cdk/aws-elasticache-alpha/lib/password-user.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class PasswordUser extends UserBase {
9191
this.userName = props.userName ?? props.userId;
9292
this.accessString = props.accessControl.accessString;
9393

94-
if (props.passwords.length > 2) {
94+
if (props.passwords.length < 1 || props.passwords.length > 2) {
9595
throw new ValidationError('Password authentication requires 1-2 passwords.', this);
9696
}
9797

packages/@aws-cdk/aws-elasticache-alpha/lib/user-group.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,7 @@ export class UserGroup extends UserGroupBase {
263263
if (this.engine === UserEngine.REDIS) {
264264
this._users.forEach(user => {
265265
if (user.engine !== UserEngine.REDIS) {
266-
throw new ValidationError(`Redis user group can only contain Redis users. User ${user.userId} has engine ${user.engine}.`, this);
266+
throw new ValidationError('Redis user group can only contain Redis users.', this);
267267
}
268268
});
269269
}
@@ -337,7 +337,7 @@ export class UserGroup extends UserGroupBase {
337337
return;
338338
}
339339
if (this.engine === UserEngine.REDIS && user.engine !== UserEngine.REDIS) {
340-
throw new ValidationError(`Redis user group can only contain Redis users. User ${user.userId} has engine ${user.engine}.`, this);
340+
throw new ValidationError('Redis user group can only contain Redis users.', this);
341341
}
342342
this._users.push(user);
343343
this.addUserDependency(user);
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
import { Template, Match } from 'aws-cdk-lib/assertions';
2+
import { Stack } from 'aws-cdk-lib';
3+
import * as iam from 'aws-cdk-lib/aws-iam';
4+
import { IamUser, AccessControl, UserEngine } from '../lib';
5+
6+
describe('IamUser', () => {
7+
describe('validation errors', () => {
8+
let stack: Stack;
9+
beforeEach(() => {
10+
stack = new Stack();
11+
});
12+
13+
test.each([
14+
{
15+
testDescription: 'when userName differs from userId throws validation error',
16+
userId: 'test-user',
17+
userName: 'different-name',
18+
errorMessage: 'For IAM authentication, userName must be equal to userId.',
19+
},
20+
])('$testDescription', ({ userId, userName, errorMessage }) => {
21+
expect(() => new IamUser(stack, 'TestUser', {
22+
userId,
23+
userName,
24+
accessControl: AccessControl.fromAccessString('on ~* +@all'),
25+
})).toThrow(errorMessage);
26+
});
27+
28+
test.each([
29+
{
30+
testDescription: 'when passing both userId and userArn throws validation error',
31+
userArn: 'arn:aws:elasticache:us-east-1:999999999999:user:test-user',
32+
userId: 'test-user',
33+
errorMessage: 'Only one of userArn or userId can be provided.',
34+
},
35+
{
36+
testDescription: 'when passing neither userId nor userArn throws validation error',
37+
errorMessage: 'One of userId or userArn is required.',
38+
},
39+
{
40+
testDescription: 'when passing invalid userArn (no user id) throws validation error',
41+
userArn: 'arn:aws:elasticache:us-east-1:999999999999:user',
42+
errorMessage: 'Unable to extract user id from ARN.',
43+
},
44+
])('$testDescription', ({ userArn, userId, errorMessage }) => {
45+
expect(() => IamUser.fromUserAttributes(stack, 'ImportedUser', { userArn, userId })).toThrow(errorMessage);
46+
});
47+
});
48+
49+
describe('constructor', () => {
50+
let stack: Stack;
51+
beforeEach(() => {
52+
stack = new Stack();
53+
});
54+
55+
test('creates user with minimal required properties', () => {
56+
new IamUser(stack, 'TestUser', {
57+
userId: 'test-user',
58+
accessControl: AccessControl.fromAccessString('on ~* +@all'),
59+
});
60+
61+
const template = Template.fromStack(stack);
62+
template.hasResourceProperties('AWS::ElastiCache::User', {
63+
Engine: 'valkey',
64+
UserId: 'test-user',
65+
UserName: 'test-user',
66+
AccessString: 'on ~* +@all',
67+
AuthenticationMode: {
68+
Type: 'iam',
69+
},
70+
NoPasswordRequired: false,
71+
Passwords: Match.absent(),
72+
});
73+
});
74+
75+
test('creates user with all possible properties', () => {
76+
new IamUser(stack, 'TestUser', {
77+
userId: 'test-user',
78+
accessControl: AccessControl.fromAccessString('on ~app:* +@read +@write'),
79+
engine: UserEngine.REDIS,
80+
userName: 'test-user',
81+
});
82+
83+
const template = Template.fromStack(stack);
84+
template.hasResourceProperties('AWS::ElastiCache::User', {
85+
Engine: 'redis',
86+
UserId: 'test-user',
87+
UserName: 'test-user',
88+
AccessString: 'on ~app:* +@read +@write',
89+
AuthenticationMode: {
90+
Type: 'iam',
91+
},
92+
NoPasswordRequired: false,
93+
Passwords: Match.absent(),
94+
});
95+
});
96+
97+
test('creates exactly one ElastiCache user resource', () => {
98+
new IamUser(stack, 'TestUser', {
99+
userId: 'test-user',
100+
accessControl: AccessControl.fromAccessString('on ~* +@all'),
101+
});
102+
103+
const template = Template.fromStack(stack);
104+
template.resourceCountIs('AWS::ElastiCache::User', 1);
105+
});
106+
});
107+
108+
describe('properties', () => {
109+
let stack: Stack;
110+
beforeEach(() => {
111+
stack = new Stack();
112+
});
113+
114+
test('exposes correct properties', () => {
115+
const user = new IamUser(stack, 'TestUser', {
116+
userId: 'test-user-id',
117+
userName: 'test-user-id',
118+
engine: UserEngine.VALKEY,
119+
accessControl: AccessControl.fromAccessString('on ~app:* +@read'),
120+
});
121+
122+
expect(user.userId).toBe('test-user-id');
123+
expect(user.userName).toBe('test-user-id');
124+
expect(user.engine).toBe('valkey');
125+
expect(user.accessString).toBe('on ~app:* +@read');
126+
expect(user.userArn).toBeDefined();
127+
expect(user.userStatus).toBeDefined();
128+
});
129+
130+
test('userName defaults to userId when not provided', () => {
131+
const user = new IamUser(stack, 'TestUser', {
132+
userId: 'my-user-id',
133+
engine: UserEngine.REDIS,
134+
accessControl: AccessControl.fromAccessString('on ~* +@all'),
135+
});
136+
137+
expect(user.userName).toBe('my-user-id');
138+
expect(user.engine).toBe('redis');
139+
});
140+
});
141+
142+
describe('isIamUser', () => {
143+
test('returns true for IamUser instances', () => {
144+
const stack = new Stack();
145+
const user = new IamUser(stack, 'TestUser', {
146+
userId: 'test-user',
147+
engine: UserEngine.VALKEY,
148+
accessControl: AccessControl.fromAccessString('on ~* +@all'),
149+
});
150+
151+
expect(IamUser.isIamUser(user)).toBe(true);
152+
});
153+
154+
test('returns false for non-IamUser objects', () => {
155+
expect(IamUser.isIamUser({})).toBe(false);
156+
expect(IamUser.isIamUser(null)).toBe(false);
157+
expect(IamUser.isIamUser(undefined)).toBe(false);
158+
expect(IamUser.isIamUser('string')).toBe(false);
159+
expect(IamUser.isIamUser(123)).toBe(false);
160+
});
161+
162+
test('returns false for imported users (not actual IamUser instances)', () => {
163+
const stack = new Stack();
164+
const importedUser = IamUser.fromUserId(stack, 'ImportedUser', 'test-user');
165+
166+
expect(IamUser.isIamUser(importedUser)).toBe(false);
167+
});
168+
});
169+
170+
describe('IAM permissions', () => {
171+
let stack: Stack;
172+
let user: IamUser;
173+
let role: iam.Role;
174+
175+
beforeEach(() => {
176+
stack = new Stack();
177+
user = new IamUser(stack, 'TestUser', {
178+
userId: 'test-user',
179+
accessControl: AccessControl.fromAccessString('on ~* +@all'),
180+
});
181+
role = new iam.Role(stack, 'TestRole', {
182+
assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'),
183+
});
184+
});
185+
186+
test('grantConnect adds correct IAM permissions', () => {
187+
user.grantConnect(role);
188+
189+
const template = Template.fromStack(stack);
190+
template.hasResourceProperties('AWS::IAM::Policy', {
191+
PolicyDocument: {
192+
Statement: Match.arrayWith([
193+
{
194+
Effect: 'Allow',
195+
Action: 'elasticache:Connect',
196+
Resource: { 'Fn::GetAtt': [Match.anyValue(), 'Arn'] },
197+
},
198+
]),
199+
},
200+
});
201+
});
202+
203+
test('grant adds custom IAM permissions', () => {
204+
user.grant(role, 'elasticache:Connect', 'elasticache:DescribeUsers');
205+
206+
const template = Template.fromStack(stack);
207+
template.hasResourceProperties('AWS::IAM::Policy', {
208+
PolicyDocument: {
209+
Statement: Match.arrayWith([
210+
{
211+
Effect: 'Allow',
212+
Action: ['elasticache:Connect', 'elasticache:DescribeUsers'],
213+
Resource: { 'Fn::GetAtt': [Match.anyValue(), 'Arn'] },
214+
},
215+
]),
216+
},
217+
});
218+
});
219+
220+
test('grant works with single action', () => {
221+
user.grant(role, 'elasticache:Connect');
222+
223+
const template = Template.fromStack(stack);
224+
template.hasResourceProperties('AWS::IAM::Policy', {
225+
PolicyDocument: {
226+
Statement: Match.arrayWith([
227+
{
228+
Effect: 'Allow',
229+
Action: 'elasticache:Connect',
230+
Resource: { 'Fn::GetAtt': [Match.anyValue(), 'Arn'] },
231+
},
232+
]),
233+
},
234+
});
235+
});
236+
});
237+
238+
describe('import methods', () => {
239+
let stack: Stack;
240+
beforeEach(() => {
241+
stack = new Stack();
242+
});
243+
244+
test('fromUserAttributes works with valid userArn', () => {
245+
const user = IamUser.fromUserAttributes(stack, 'ImportedUser', {
246+
userArn: 'arn:aws:elasticache:us-east-1:123456789012:user:my-user',
247+
});
248+
249+
expect(user.userId).toBe('my-user');
250+
expect(user.userArn).toBe('arn:aws:elasticache:us-east-1:123456789012:user:my-user');
251+
expect(user.userName).toBe(undefined);
252+
expect(user.engine).toBe(undefined);
253+
});
254+
255+
test('fromUserAttributes works with userId only', () => {
256+
const user = IamUser.fromUserAttributes(stack, 'ImportedUser', {
257+
userId: 'imported-user',
258+
});
259+
260+
expect(user.userId).toBe('imported-user');
261+
expect(user.userArn).toContain('imported-user');
262+
expect(user.userName).toBe(undefined);
263+
expect(user.engine).toBe(undefined);
264+
});
265+
266+
test('fromUserAttributes preserves engine when provided', () => {
267+
const user = IamUser.fromUserAttributes(stack, 'ImportedUser', {
268+
userId: 'test-user',
269+
engine: UserEngine.REDIS,
270+
});
271+
272+
expect(user.engine).toBe(UserEngine.REDIS);
273+
});
274+
275+
test('fromUserAttributes preserves userName when provided', () => {
276+
const user = IamUser.fromUserAttributes(stack, 'ImportedUser', {
277+
userId: 'test-user',
278+
userName: 'test-user',
279+
});
280+
281+
expect(user.userName).toBe('test-user');
282+
});
283+
284+
test('fromUserAttributes works with both engine and userName', () => {
285+
const user = IamUser.fromUserAttributes(stack, 'ImportedUser', {
286+
userId: 'test-user',
287+
engine: UserEngine.REDIS,
288+
userName: 'test-user',
289+
});
290+
291+
expect(user.userId).toBe('test-user');
292+
expect(user.engine).toBe(UserEngine.REDIS);
293+
expect(user.userName).toBe('test-user');
294+
});
295+
296+
test('fromUserAttributes with userArn preserves additional attributes', () => {
297+
const arn = 'arn:aws:elasticache:us-east-1:123456789012:user:my-user';
298+
const user = IamUser.fromUserAttributes(stack, 'ImportedUser', {
299+
userArn: arn,
300+
engine: UserEngine.VALKEY,
301+
userName: 'my-user',
302+
});
303+
304+
expect(user.userArn).toBe(arn);
305+
expect(user.userId).toBe('my-user');
306+
expect(user.engine).toBe('valkey');
307+
expect(user.userName).toBe('my-user');
308+
});
309+
310+
test('fromUserId creates user with correct properties', () => {
311+
const user = IamUser.fromUserId(stack, 'ImportedUser', 'my-user-id');
312+
313+
expect(user.userId).toBe('my-user-id');
314+
expect(user.userArn).toContain('my-user-id');
315+
expect(user.userName).toBe(undefined);
316+
expect(user.engine).toBe(undefined);
317+
});
318+
319+
test('fromUserArn creates user with correct properties', () => {
320+
const arn = 'arn:aws:elasticache:us-west-2:123456789012:user:test-user';
321+
const user = IamUser.fromUserArn(stack, 'ImportedUser', arn);
322+
323+
expect(user.userId).toBe('test-user');
324+
expect(user.userArn).toBe(arn);
325+
expect(user.userName).toBe(undefined);
326+
expect(user.engine).toBe(undefined);
327+
});
328+
329+
test('import methods do not validate userName equals userId constraint', () => {
330+
// Import methods assume external user is valid
331+
const user = IamUser.fromUserAttributes(stack, 'ImportedUser', {
332+
userId: 'test-user',
333+
userName: 'different-name', // This is allowed for imports
334+
});
335+
336+
expect(user.userName).toBe('different-name');
337+
expect(user.userId).toBe('test-user');
338+
});
339+
});
340+
});

0 commit comments

Comments
 (0)