Skip to content

Commit 78b7881

Browse files
committed
S3UTILS-211: ft tests for listObjectsByReplicationStatus
1 parent 4cffe0d commit 78b7881

File tree

2 files changed

+489
-0
lines changed

2 files changed

+489
-0
lines changed
Lines changed: 367 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,367 @@
1+
const vaultclient = require('vaultclient');
2+
const { Logger } = require('werelogs');
3+
const { listObjectsByReplicationStatus } = require('../../listObjectsByReplicationStatus');
4+
5+
const {
6+
iamHost,
7+
iamPort,
8+
s3Host,
9+
s3Port,
10+
adminAccessKeyId,
11+
adminSecretAccessKey,
12+
createTestAccount,
13+
deleteTestAccount,
14+
} = require('./utils/S3Setup');
15+
16+
const log = new Logger('listObjectsByReplicationStatus:test');
17+
18+
async function configureCrr(accountSource, accountDest) {
19+
// activate bucket versionning on source and destination buckets
20+
log.info('Enabling bucket versioning on source and destination buckets');
21+
await accountSource.s3Client.putBucketVersioning({
22+
Bucket: accountSource.bucketName,
23+
VersioningConfiguration: {
24+
Status: 'Enabled',
25+
},
26+
}).promise();
27+
28+
await accountDest.s3Client.putBucketVersioning({
29+
Bucket: accountDest.bucketName,
30+
VersioningConfiguration: {
31+
Status: 'Enabled',
32+
},
33+
}).promise();
34+
35+
log.info('Creating IAM policies and roles for CRR');
36+
// create policy
37+
const policy = {
38+
Version:'2012-10-17',
39+
Statement:[
40+
{
41+
Effect:'Allow',
42+
Action:[
43+
's3:GetObjectVersion',
44+
's3:GetObjectVersionAcl',
45+
's3:ReplicateObject'
46+
],
47+
Resource:[
48+
`arn:aws:s3:::${accountSource.bucketName}/*`
49+
]
50+
},
51+
{
52+
Effect:'Allow',
53+
Action:[
54+
's3:ListBucket',
55+
's3:GetReplicationConfiguration'
56+
],
57+
Resource:[
58+
'arn:aws:s3:::source'
59+
]
60+
},
61+
{
62+
Effect:'Allow',
63+
Action:[
64+
's3:ReplicateObject',
65+
's3:ReplicateDelete'
66+
],
67+
Resource:`arn:aws:s3:::${accountDest.bucketName}/*`
68+
}
69+
]
70+
};
71+
await accountSource.iamClient.createPolicy({
72+
PolicyName: 'crr-policy',
73+
PolicyDocument: JSON.stringify(policy),
74+
}).promise();
75+
76+
await accountDest.iamClient.createPolicy({
77+
PolicyName: 'crr-policy',
78+
PolicyDocument: JSON.stringify(policy),
79+
}).promise();
80+
81+
log.info('Creating IAM roles');
82+
// create trust
83+
const trust = {
84+
Version:'2012-10-17',
85+
Statement:[
86+
{
87+
Effect:'Allow',
88+
Principal:{
89+
Service:'backbeat'
90+
},
91+
Action:'sts:AssumeRole'
92+
}
93+
]
94+
};
95+
await accountSource.iamClient.createRole({
96+
RoleName: 'crr-trust-role',
97+
AssumeRolePolicyDocument: JSON.stringify(trust),
98+
}).promise();
99+
await accountDest.iamClient.createRole({
100+
RoleName: 'crr-trust-role',
101+
AssumeRolePolicyDocument: JSON.stringify(trust),
102+
}).promise();
103+
104+
log.info('Attaching policies to roles');
105+
// attach role to policy
106+
await accountSource.iamClient.attachRolePolicy({
107+
RoleName: 'crr-trust-role',
108+
PolicyArn: `arn:aws:iam::${accountSource.account.account.id}:policy/crr-policy`,
109+
}).promise();
110+
await accountDest.iamClient.attachRolePolicy({
111+
RoleName: 'crr-trust-role',
112+
PolicyArn: `arn:aws:iam::${accountDest.account.account.id}:policy/crr-policy`,
113+
}).promise();
114+
115+
log.info('Setting bucket replication configuration on source bucket');
116+
const replication = {
117+
Role: `arn:aws:iam::${accountSource.account.account.id}:role/crr-trust-role,arn:aws:iam::${accountDest.account.account.id}:role/crr-trust-role`,
118+
Rules: [
119+
{
120+
Prefix: '',
121+
Status: 'Enabled',
122+
Destination: {
123+
Bucket: `arn:aws:s3:::${accountDest.bucketName}`
124+
}
125+
}
126+
]
127+
};
128+
await accountSource.s3Client.putBucketReplication({
129+
Bucket: accountSource.bucketName,
130+
ReplicationConfiguration: replication
131+
}).promise();
132+
133+
}
134+
135+
async function removeCrrConfiguration(account) {
136+
log.info('Removing bucket crr configuration', { bucket: account.bucketName });
137+
try {
138+
await account.s3Client.deleteBucketReplication({ Bucket: account.bucketName }).promise();
139+
} catch (err) {
140+
log.error('Error removing bucket replication configuration', {
141+
bucket: account.bucketName,
142+
error: err.message,
143+
});
144+
}
145+
// remove versioning
146+
await account.s3Client.putBucketVersioning({
147+
Bucket: account.bucketName,
148+
VersioningConfiguration: {
149+
Status: 'Suspended',
150+
},
151+
}).promise();
152+
153+
// Clean up IAM resources first (roles and policies must be deleted before account)
154+
log.info('Cleaning up IAM resources', { account: account.accountName });
155+
try {
156+
// Detach policy from role
157+
await account.iamClient.detachRolePolicy({
158+
RoleName: 'crr-trust-role',
159+
PolicyArn: `arn:aws:iam::${account.account.account.id}:policy/crr-policy`,
160+
}).promise();
161+
log.info('Detached policy from role');
162+
} catch (err) {
163+
log.error('Error detaching policy from role', { error: err.message });
164+
}
165+
166+
try {
167+
// Delete role
168+
await account.iamClient.deleteRole({ RoleName: 'crr-trust-role' }).promise();
169+
log.info('Deleted IAM role');
170+
} catch (err) {
171+
log.error('Error deleting role', { error: err.message });
172+
}
173+
174+
try {
175+
// Delete policy
176+
await account.iamClient.deletePolicy({
177+
PolicyArn: `arn:aws:iam::${account.account.account.id}:policy/crr-policy`,
178+
}).promise();
179+
log.info('Deleted IAM policy');
180+
} catch (err) {
181+
log.error('Error deleting policy', { error: err.message });
182+
}
183+
184+
try {
185+
// Delete IAM user
186+
await account.iamClient.deleteUser({ UserName: account.iamUser }).promise();
187+
log.info('Deleted IAM user');
188+
} catch (err) {
189+
log.error('Error deleting IAM user', { error: err.message });
190+
}
191+
}
192+
193+
194+
describe('listObjectsByReplicationStatus', () => {
195+
describe('input validation', () => {
196+
const validOptions = {
197+
buckets: 'test-bucket',
198+
accessKey: 'test-access-key',
199+
secretKey: 'test-secret-key',
200+
endpoint: 'http://localhost:8000',
201+
replicationStatus: 'PENDING',
202+
};
203+
204+
it('should reject when buckets is missing', async () => {
205+
const options = { ...validOptions, buckets: null };
206+
await expect(listObjectsByReplicationStatus(options))
207+
.rejects
208+
.toThrow('No buckets given as input! Please provide a comma-separated list of buckets');
209+
});
210+
211+
it('should reject when buckets is empty string', async () => {
212+
const options = { ...validOptions, buckets: '' };
213+
await expect(listObjectsByReplicationStatus(options))
214+
.rejects
215+
.toThrow('No buckets given as input! Please provide a comma-separated list of buckets');
216+
});
217+
218+
it('should reject when buckets is only whitespace', async () => {
219+
const options = { ...validOptions, buckets: ' ' };
220+
await expect(listObjectsByReplicationStatus(options))
221+
.rejects
222+
.toThrow('No buckets given as input! Please provide a comma-separated list of buckets');
223+
});
224+
225+
it('should reject when endpoint is missing', async () => {
226+
const options = { ...validOptions, endpoint: null };
227+
await expect(listObjectsByReplicationStatus(options))
228+
.rejects
229+
.toThrow('ENDPOINT not defined!');
230+
});
231+
232+
it('should reject when accessKey is missing', async () => {
233+
const options = { ...validOptions, accessKey: null };
234+
await expect(listObjectsByReplicationStatus(options))
235+
.rejects
236+
.toThrow('ACCESS_KEY not defined');
237+
});
238+
239+
it('should reject when secretKey is missing', async () => {
240+
const options = { ...validOptions, secretKey: null };
241+
await expect(listObjectsByReplicationStatus(options))
242+
.rejects
243+
.toThrow('SECRET_KEY not defined');
244+
});
245+
246+
it('should reject when replicationStatus contains invalid status', async () => {
247+
const options = { ...validOptions, replicationStatus: 'INVALID_STATUS' };
248+
await expect(listObjectsByReplicationStatus(options))
249+
.rejects
250+
.toThrow('invalid REPLICATION_STATUS: must be a comma-separated list of replication statuses: NEW,PENDING,COMPLETED,FAILED,REPLICA.');
251+
});
252+
253+
it('should reject when replicationStatus contains mix of valid and invalid statuses', async () => {
254+
const options = { ...validOptions, replicationStatus: 'PENDING,INVALID,COMPLETED' };
255+
await expect(listObjectsByReplicationStatus(options))
256+
.rejects
257+
.toThrow('invalid REPLICATION_STATUS: must be a comma-separated list of replication statuses: NEW,PENDING,COMPLETED,FAILED,REPLICA.');
258+
});
259+
260+
it('should accept all valid replication statuses', async () => {
261+
const options = {
262+
...validOptions,
263+
replicationStatus: 'NEW,PENDING,COMPLETED,FAILED,REPLICA',
264+
};
265+
// This will fail at connection level, but should pass validation
266+
await expect(listObjectsByReplicationStatus(options))
267+
.rejects
268+
.not.toThrow('invalid REPLICATION_STATUS');
269+
});
270+
271+
it('should use default replication status FAILED when not provided', async () => {
272+
const options = { ...validOptions };
273+
delete options.replicationStatus;
274+
// This will fail at connection level, but should pass validation
275+
await expect(listObjectsByReplicationStatus(options))
276+
.rejects
277+
.not.toThrow('invalid REPLICATION_STATUS');
278+
});
279+
});
280+
281+
describe('functional tests', () => {
282+
let vaultClient;
283+
let accountSource;
284+
let accountDest;
285+
286+
beforeAll(async () => {
287+
vaultClient = new vaultclient.Client(
288+
iamHost,
289+
iamPort,
290+
false,
291+
undefined,
292+
undefined,
293+
undefined,
294+
undefined,
295+
adminAccessKeyId,
296+
adminSecretAccessKey
297+
);
298+
});
299+
300+
beforeEach(async () => {
301+
log.info('Setting up test accounts and buckets');
302+
accountSource = await createTestAccount(vaultClient);
303+
accountDest = await createTestAccount(vaultClient);
304+
log.info(`Account source: ${accountSource.accountName} and dest: ${accountDest.accountName} were created`);
305+
log.info('Configuring CRR between source and destination buckets');
306+
await configureCrr(accountSource, accountDest);
307+
});
308+
309+
afterEach(async () => {
310+
log.info('Cleaning up test accounts');
311+
await removeCrrConfiguration(accountSource);
312+
await removeCrrConfiguration(accountDest);
313+
await deleteTestAccount(vaultClient, accountSource);
314+
await deleteTestAccount(vaultClient, accountDest);
315+
log.info('Test accounts deleted');
316+
});
317+
318+
it('should list objects by replication status', async () => {
319+
// Add data to source bucket
320+
log.info('Uploading test objects to source bucket');
321+
const testObjects = [
322+
{ Key: 'test-object-1', Body: 'data to replicate 1' },
323+
{ Key: 'test-object-2', Body: 'data to replicate 2' },
324+
{ Key: 'test-object-3', Body: 'data to replicate 3' },
325+
];
326+
327+
for (const obj of testObjects) {
328+
await accountSource.s3Client.putObject({
329+
Bucket: accountSource.bucketName,
330+
Key: obj.Key,
331+
Body: obj.Body,
332+
}).promise();
333+
log.info('Uploaded object', { key: obj.Key });
334+
}
335+
336+
// Verify objects have replication status
337+
log.info('Verifying objects have replication status');
338+
for (const obj of testObjects) {
339+
const headResult = await accountSource.s3Client.headObject({
340+
Bucket: accountSource.bucketName,
341+
Key: obj.Key,
342+
}).promise();
343+
log.info('Object metadata', {
344+
key: obj.Key,
345+
replicationStatus: headResult.ReplicationStatus,
346+
versionId: headResult.VersionId
347+
});
348+
}
349+
350+
// Execute the listObjectsByReplicationStatus function directly
351+
log.info('Executing listObjectsByReplicationStatus function');
352+
const endpoint = `http://${s3Host}:${s3Port}`;
353+
354+
// Call the function directly for coverage
355+
await listObjectsByReplicationStatus({
356+
buckets: accountSource.bucketName,
357+
accessKey: accountSource.accountAccessKey,
358+
secretKey: accountSource.accountSecretKey,
359+
endpoint,
360+
replicationStatus: 'PENDING,FAILED,COMPLETED',
361+
logger: log,
362+
});
363+
364+
log.info('Function execution completed successfully');
365+
}, 30000); // Increase timeout for script execution
366+
});
367+
});

0 commit comments

Comments
 (0)