Skip to content

Commit c0abc5e

Browse files
author
Aayush
committed
Added unit tests for nc
Signed-off-by: Aayush <aayush@li-2392f9cc-287a-11b2-a85c-9fcc04b05da6.ibm.com>
1 parent 29db382 commit c0abc5e

File tree

5 files changed

+237
-57
lines changed

5 files changed

+237
-57
lines changed

src/test/system_tests/test_utils.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,80 @@ const run_or_skip_test = cond => {
790790
} else return it.skip;
791791
};
792792

793+
/**
794+
* update_file_mtime updates the mtime of the target path
795+
* Warnings:
796+
* - This operation would change the mtime of the file to 5 days ago - which means that it changes the etag / obj_id of the object
797+
* - Please do not use on versioned objects (version_id will not be changed, but the mtime will be changed) - might cause issues.
798+
* @param {String} target_path
799+
* @returns {Promise<Void>}
800+
*/
801+
async function update_file_mtime(target_path) {
802+
const update_file_mtime_cmp = os_utils.IS_MAC ? `touch -t $(date -v -5d +"%Y%m%d%H%M.%S") ${target_path}` : `touch -d "5 days ago" ${target_path}`;
803+
await os_utils.exec(update_file_mtime_cmp, { return_stdout: true });
804+
}
805+
806+
/////////////////////////////////
807+
////// LIFECYCLE UTILS ///////
808+
/////////////////////////////////
809+
810+
/**
811+
* generate_lifecycle_rule generate an S3 lifecycle rule with optional filters and expiration currently (can be extend to support more lifecycle rule params)
812+
*
813+
* @param {number} expiration_days
814+
* @param {string} id
815+
* @param {string} [prefix]
816+
* @param {Array<Object>} [tags]
817+
* @param {number} [size_gt]
818+
* @param {number} [size_lt]
819+
* @returns {Object}
820+
*/
821+
function generate_lifecycle_rule(expiration_days, id, prefix, tags, size_gt, size_lt) {
822+
const filters = {};
823+
if (prefix) filters.Prefix = prefix;
824+
if (Array.isArray(tags) && tags.length) filters.Tags = tags;
825+
if (size_gt !== undefined) filters.ObjectSizeGreaterThan = size_gt;
826+
if (size_lt !== undefined) filters.ObjectSizeLessThan = size_lt;
827+
828+
const filter = Object.keys(filters).length > 1 ? { And: filters } : filters;
829+
830+
return {
831+
ID: id,
832+
Status: 'Enabled',
833+
Filter: filter,
834+
Expiration: { Days: expiration_days },
835+
};
836+
}
837+
838+
/**
839+
* validate_expiration_header validates the `x-amz-expiration` header against the object creation time, expected rule ID and expiration days
840+
*
841+
* The header is expected to have the format:
842+
* expiry-date="YYYY-MM-DDTHH:MM:SS.SSSZ", rule-id="RULE_ID"
843+
*
844+
* @param {string} expiration_header - expiration header value
845+
* @param {string|Date} start_time - start/create time (string or Date) of the object
846+
* @param {string} expected_rule_id - expected rule ID to match in the header
847+
* @param {number} delta_days - expected number of days between start_time and expiry-date
848+
* @returns {boolean} true if the header is valid and matches the expected_rule_id and delta_days otherwise false
849+
*/
850+
function validate_expiration_header(expiration_header, start_time, expected_rule_id, delta_days) {
851+
const match = expiration_header.match(/expiry-date="(.+)", rule-id="(.+)"/);
852+
if (!match) return false;
853+
854+
const [, expiry_str, rule_id] = match;
855+
const expiration = new Date(expiry_str);
856+
const start = new Date(start_time);
857+
start.setUTCHours(0, 0, 0, 0); // adjusting to midnight UTC otherwise the tests will fail - fix for ceph-s3 tests
858+
859+
const days_diff = Math.floor((expiration.getTime() - start.getTime()) / (24 * 60 * 60 * 1000));
860+
861+
return days_diff === delta_days && rule_id === expected_rule_id;
862+
}
863+
864+
exports.update_file_mtime = update_file_mtime;
865+
exports.generate_lifecycle_rule = generate_lifecycle_rule;
866+
exports.validate_expiration_header = validate_expiration_header;
793867
exports.run_or_skip_test = run_or_skip_test;
794868
exports.blocks_exist_on_cloud = blocks_exist_on_cloud;
795869
exports.create_hosts_pool = create_hosts_pool;

src/test/unit_tests/jest_tests/test_nc_lifecycle_posix_integration.test.js

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,10 @@ const fs = require('fs');
1111
const config = require('../../../../config');
1212
const fs_utils = require('../../../util/fs_utils');
1313
const { ConfigFS } = require('../../../sdk/config_fs');
14-
const { TMP_PATH, set_nc_config_dir_in_config, TEST_TIMEOUT, exec_manage_cli, create_system_json } = require('../../system_tests/test_utils');
14+
const { TMP_PATH, set_nc_config_dir_in_config, TEST_TIMEOUT, exec_manage_cli, create_system_json, update_file_mtime } = require('../../system_tests/test_utils');
1515
const { TYPES, ACTIONS } = require('../../../manage_nsfs/manage_nsfs_constants');
1616
const NamespaceFS = require('../../../sdk/namespace_fs');
1717
const endpoint_stats_collector = require('../../../sdk/endpoint_stats_collector');
18-
const os_utils = require('../../../util/os_utils');
1918
const { ManageCLIResponse } = require('../../../manage_nsfs/manage_nsfs_cli_responses');
2019
const { ManageCLIError } = require('../../../manage_nsfs/manage_nsfs_cli_errors');
2120
const buffer_utils = require('../../../util/buffer_utils');
@@ -2187,19 +2186,6 @@ async function create_object(object_sdk, bucket, key, size, is_old, tagging) {
21872186
return res;
21882187
}
21892188

2190-
/**
2191-
* update_file_mtime updates the mtime of the target path
2192-
* Warnings:
2193-
* - This operation would change the mtime of the file to 5 days ago - which means that it changes the etag / obj_id of the object
2194-
* - Please do not use on versioned objects (version_id will not be changed, but the mtime will be changed) - might cause issues.
2195-
* @param {String} target_path
2196-
* @returns {Promise<Void>}
2197-
*/
2198-
async function update_file_mtime(target_path) {
2199-
const update_file_mtime_cmp = os_utils.IS_MAC ? `touch -t $(date -v -5d +"%Y%m%d%H%M.%S") ${target_path}` : `touch -d "5 days ago" ${target_path}`;
2200-
await os_utils.exec(update_file_mtime_cmp, { return_stdout: true });
2201-
}
2202-
22032189
/**
22042190
* updates the number of noncurrent days xattr of target path to be 5 days older. use only on noncurrent objects.
22052191
* is use this function on latest object the xattr will be changed when the object turns noncurrent

src/test/unit_tests/nc_index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require('./test_bucketspace_versioning');
2121
require('./test_nc_bucket_logging');
2222
require('./test_nc_online_upgrade_s3_integrations');
2323
require('./test_public_access_block');
24+
require('./test_nc_bucket_lifecycle');
2425

2526
// running with iam port
2627
require('./test_nc_iam_basic_integration.js'); // please notice that we use a different setup

src/test/unit_tests/test_lifecycle.js

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ const MDStore = require('../../server/object_services/md_store').MDStore;
2222
const coretest = require('./coretest');
2323
const lifecycle = require('../../server/bg_services/lifecycle');
2424
const http_utils = require('../../util/http_utils');
25+
const test_utils = require('../../../src/test/system_tests/test_utils');
2526
const commonTests = require('../lifecycle/common');
2627
const seed = crypto.randomBytes(16);
2728
const generator = crypto.createCipheriv('aes-128-gcm', seed, Buffer.alloc(12));
@@ -824,46 +825,14 @@ mocha.describe('lifecycle', () => {
824825
res = await s3.headObject({ Bucket: bucket, Key: key });
825826
assert.ok(res.Expiration, 'expiration header missing in headObject response');
826827

827-
const valid = validate_expiration_header(res.Expiration, start_time, expected_id, expected_days);
828+
const valid = test_utils.validate_expiration_header(res.Expiration, start_time, expected_id, expected_days);
828829
assert.ok(valid, `expected rule ${expected_id} to match`);
829830
};
830831

831-
function generate_rule(id, prefix, tags, size_gt, size_lt, expiration_days) {
832-
const filters = {};
833-
if (prefix) filters.Prefix = prefix;
834-
if (Array.isArray(tags) && tags.length) filters.Tags = tags;
835-
if (size_gt !== undefined) filters.ObjectSizeGreaterThan = size_gt;
836-
if (size_lt !== undefined) filters.ObjectSizeLessThan = size_lt;
837-
838-
const filter = Object.keys(filters).length > 1 ? { And: filters } : filters;
839-
840-
return {
841-
ID: id,
842-
Status: 'Enabled',
843-
Filter: filter,
844-
Expiration: { Days: expiration_days },
845-
};
846-
}
847-
848-
function validate_expiration_header(expiration_header, start_time, expected_rule_id, delta_days) {
849-
const match = expiration_header.match(/expiry-date="(.+)", rule-id="(.+)"/);
850-
if (!match) return false;
851-
console.log("match: ", match);
852-
853-
const [, expiry_str, rule_id] = match;
854-
const expiration = new Date(expiry_str);
855-
const start = new Date(start_time);
856-
start.setUTCHours(0, 0, 0, 0); // adjusting to midnight UTC otherwise the tests will fail - fix for ceph-s3 tests
857-
858-
const days_diff = Math.floor((expiration.getTime() - start.getTime()) / (24 * 60 * 60 * 1000));
859-
860-
return days_diff === delta_days && rule_id === expected_rule_id;
861-
}
862-
863832
mocha.it('should select rule with longest prefix', async () => {
864833
const rules = [
865-
generate_rule('short-prefix', 'test1/', [], undefined, undefined, 10),
866-
generate_rule('long-prefix', 'test1/logs/', [], undefined, undefined, 17),
834+
test_utils.generate_lifecycle_rule(10, 'short-prefix', 'test1/', [], undefined, undefined),
835+
test_utils.generate_lifecycle_rule(17, 'long-prefix', 'test1/logs/', [], undefined, undefined),
867836
];
868837
await run_expiration_test({
869838
rules,
@@ -875,11 +844,11 @@ mocha.describe('lifecycle', () => {
875844

876845
mocha.it('should select rule with more tags when prefix is same', async () => {
877846
const rules = [
878-
generate_rule('one-tag', 'test2/', [{ Key: 'env', Value: 'prod' }], undefined, undefined, 5),
879-
generate_rule('two-tags', 'test2/', [
847+
test_utils.generate_lifecycle_rule(5, 'one-tag', 'test2/', [{ Key: 'env', Value: 'prod' }], undefined, undefined),
848+
test_utils.generate_lifecycle_rule(9, 'two-tags', 'test2/', [
880849
{ Key: 'env', Value: 'prod' },
881850
{ Key: 'team', Value: 'backend' }
882-
], undefined, undefined, 9),
851+
], undefined, undefined),
883852
];
884853
await run_expiration_test({
885854
rules,
@@ -892,8 +861,8 @@ mocha.describe('lifecycle', () => {
892861

893862
mocha.it('should select rule with narrower size span when prefix and tags are matching', async () => {
894863
const rules = [
895-
generate_rule('wide-range', 'test3/', [], 100, 10000, 4),
896-
generate_rule('narrow-range', 'test3/', [], 1000, 5000, 6),
864+
test_utils.generate_lifecycle_rule(4, 'wide-range', 'test3/', [], 100, 10000),
865+
test_utils.generate_lifecycle_rule(6, 'narrow-range', 'test3/', [], 1000, 5000),
897866
];
898867
await run_expiration_test({
899868
rules,
@@ -906,8 +875,8 @@ mocha.describe('lifecycle', () => {
906875

907876
mocha.it('should fallback to first matching rule if all filters are equal', async () => {
908877
const rules = [
909-
generate_rule('rule-a', 'test4/', [], 0, 10000, 7),
910-
generate_rule('rule-b', 'test4/', [], 0, 10000, 11),
878+
test_utils.generate_lifecycle_rule(7, 'rule-a', 'test4/', [], 0, 10000),
879+
test_utils.generate_lifecycle_rule(11, 'rule-b', 'test4/', [], 0, 10000),
911880
];
912881
await run_expiration_test({
913882
rules,
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/* Copyright (C) 2024 NooBaa */
2+
'use strict';
3+
4+
const path = require('path');
5+
const mocha = require('mocha');
6+
const assert = require('assert');
7+
const fs_utils = require('../../util/fs_utils');
8+
const { TYPES, ACTIONS } = require('../../manage_nsfs/manage_nsfs_constants');
9+
const { TMP_PATH, set_path_permissions_and_owner, invalid_nsfs_root_permissions, generate_s3_client, get_coretest_path,
10+
generate_lifecycle_rule, validate_expiration_header, update_file_mtime, exec_manage_cli } = require('../system_tests/test_utils');
11+
12+
const coretest_path = get_coretest_path();
13+
const coretest = require(coretest_path);
14+
const { rpc_client, EMAIL, get_admin_mock_account_details } = coretest;
15+
coretest.setup({});
16+
17+
let s3_admin;
18+
19+
const tmp_fs_root = path.join(TMP_PATH, 'test_nc_bucket_lifecycle/');
20+
21+
/**
22+
* is_nc_coretest returns true when the test runs on NC env
23+
*/
24+
const is_nc_coretest = process.env.NC_CORETEST === 'true';
25+
26+
mocha.describe('nc lifecycle - check expiration header', async function() {
27+
const bucket_path = path.join(tmp_fs_root, 'test-bucket/');
28+
const bucket_name = 'test-bucket';
29+
30+
mocha.before(async function() {
31+
this.timeout(0); // eslint-disable-line no-invalid-this
32+
if (invalid_nsfs_root_permissions()) this.skip(); // eslint-disable-line no-invalid-this
33+
// create paths
34+
await fs_utils.create_fresh_path(tmp_fs_root, 0o777);
35+
await fs_utils.create_fresh_path(bucket_path, 0o770);
36+
await fs_utils.file_must_exist(bucket_path);
37+
38+
// set permissions
39+
if (is_nc_coretest) {
40+
const { uid, gid } = get_admin_mock_account_details();
41+
await set_path_permissions_and_owner(bucket_path, { uid, gid }, 0o700);
42+
}
43+
44+
// create s3_admin client
45+
const admin = (await rpc_client.account.read_account({ email: EMAIL, }));
46+
const admin_keys = admin.access_keys;
47+
s3_admin = generate_s3_client(admin_keys[0].access_key.unwrap(),
48+
admin_keys[0].secret_key.unwrap(),
49+
coretest.get_http_address());
50+
51+
// create test bucket
52+
const cli_bucket_options = {
53+
name: bucket_name,
54+
owner: admin.name,
55+
path: bucket_path,
56+
};
57+
await exec_manage_cli(TYPES.BUCKET, ACTIONS.ADD, cli_bucket_options);
58+
});
59+
60+
mocha.after(async function() {
61+
this.timeout(0); // eslint-disable-line no-invalid-this
62+
fs_utils.folder_delete(tmp_fs_root);
63+
});
64+
65+
const run_expiration_test = async ({ rules, expected_id, expected_days, key, tagging = undefined, size = 1000}) => {
66+
const putLifecycleParams = {
67+
Bucket: bucket_name,
68+
LifecycleConfiguration: { Rules: rules }
69+
};
70+
await s3_admin.putBucketLifecycleConfiguration(putLifecycleParams);
71+
72+
const putObjectParams = {
73+
Bucket: bucket_name,
74+
Key: key,
75+
Body: 'x'.repeat(size) // default 1KB if size not specified
76+
};
77+
if (tagging) {
78+
putObjectParams.Tagging = tagging;
79+
}
80+
const start_time = new Date();
81+
let res = await s3_admin.putObject(putObjectParams);
82+
assert.ok(res.Expiration, 'expiration header missing in putObject response');
83+
84+
// update file mtime to simulate a 5-days old object
85+
await update_file_mtime(path.join(bucket_path, key));
86+
87+
res = await s3_admin.headObject({ Bucket: bucket_name, Key: key });
88+
assert.ok(res.Expiration, 'expiration header missing in headObject response');
89+
90+
const valid = validate_expiration_header(res.Expiration, start_time, expected_id, expected_days - 5);
91+
assert.ok(valid, `expected rule ${expected_id} to match`);
92+
};
93+
94+
mocha.it('should select rule with longest prefix', async () => {
95+
const rules = [
96+
generate_lifecycle_rule(10, 'short-prefix', 'lifecycle-test1/', [], undefined, undefined),
97+
generate_lifecycle_rule(17, 'long-prefix', 'lifecycle-test1/logs/', [], undefined, undefined),
98+
];
99+
await run_expiration_test({
100+
rules,
101+
key: 'lifecycle-test1/logs//file.txt',
102+
expected_id: 'long-prefix',
103+
expected_days: 17
104+
});
105+
});
106+
107+
mocha.it('should select rule with more tags when prefix is same', async () => {
108+
const rules = [
109+
generate_lifecycle_rule(5, 'one-tag', 'lifecycle-test2/', [{ Key: 'env', Value: 'prod' }], undefined, undefined),
110+
generate_lifecycle_rule(9, 'two-tags', 'lifecycle-test2/', [
111+
{ Key: 'env', Value: 'prod' },
112+
{ Key: 'team', Value: 'backend' }
113+
], undefined, undefined),
114+
];
115+
await run_expiration_test({
116+
rules,
117+
key: 'lifecycle-test2/file2.txt',
118+
tagging: 'env=prod&team=backend',
119+
expected_id: 'two-tags',
120+
expected_days: 9
121+
});
122+
});
123+
124+
mocha.it('should select rule with narrower size span when prefix and tags are matching', async () => {
125+
const rules = [
126+
generate_lifecycle_rule(4, 'wide-range', 'lifecycle-test3/', [], 100, 10000),
127+
generate_lifecycle_rule(6, 'narrow-range', 'lifecycle-test3/', [], 1000, 5000),
128+
];
129+
await run_expiration_test({
130+
rules,
131+
key: 'lifecycle-test3/file3.txt',
132+
size: 1500,
133+
expected_id: 'narrow-range',
134+
expected_days: 6
135+
});
136+
});
137+
138+
mocha.it('should fallback to first matching rule if all filters are equal', async () => {
139+
const rules = [
140+
generate_lifecycle_rule(7, 'rule-a', 'lifecycle/test4/', [], 0, 10000),
141+
generate_lifecycle_rule(11, 'rule-b', 'lifecycle/test4/', [], 0, 10000),
142+
];
143+
await run_expiration_test({
144+
rules,
145+
key: 'lifecycle/test4/file4.txt',
146+
expected_id: 'rule-a',
147+
expected_days: 7
148+
});
149+
});
150+
});

0 commit comments

Comments
 (0)