Skip to content

Commit b225590

Browse files
authored
notifications | auto tests (#8497)
Signed-off-by: Amit Prinz Setter <alphaprinz@gmail.com>
1 parent 883e1b1 commit b225590

File tree

5 files changed

+223
-6
lines changed

5 files changed

+223
-6
lines changed

config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,7 @@ config.BUCKET_LOG_CONCURRENCY = 10;
712712
////////////////////////////////
713713
// NOTIFICATIONS //
714714
////////////////////////////////
715+
config.NOTIFICATION_CONNECT_DIR = process.env.NOTIFICATION_CONNECT_DIR || '/etc/notif_connect/';
715716
config.NOTIFICATION_LOG_NS = 'notification_logging';
716717
config.NOTIFICATION_LOG_DIR = process.env.NOTIFICATION_LOG_DIR;
717718
config.NOTIFICATION_BATCH = process.env.BATCH || 10;

src/test/unit_tests/coretest.js

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const mocha = require('mocha');
88
const assert = require('assert');
99
const crypto = require('crypto');
1010
const fs = require('fs');
11+
const path = require('path');
1112

1213
// keep me first - this is setting envs that should be set before other modules are required.
1314
const CORETEST = 'coretest';
@@ -49,6 +50,9 @@ const system_store = require('../../server/system_services/system_store').get_in
4950
const MDStore = require('../../server/object_services/md_store').MDStore;
5051
const pool_server = require('../../server/system_services/pool_server');
5152
const pool_ctrls = require('../../server/system_services/pool_controllers');
53+
const { PersistentLogger } = require('../../util/persistent_logger');
54+
const os = require('os');
55+
const { TMP_PATH } = require('../system_tests/test_utils');
5256

5357
// Set the pools server pool controller factory to create pools with
5458
// backed by in process agents.
@@ -132,11 +136,18 @@ function setup(options = {}) {
132136
_.each(server_rpc.rpc._services,
133137
(service, srv) => api_coverage.add(srv));
134138

139+
const notification_logger =
140+
new PersistentLogger(path.join(TMP_PATH, 'test_notifications', 'notif_logs'),
141+
process.env.NODE_NAME || os.hostname() + '_' + config.NOTIFICATION_LOG_NS, {
142+
locking: 'SHARED',
143+
poll_interval: config.NSFS_GLACIER_LOGS_POLL_INTERVAL,
144+
});
145+
135146
const object_io = new ObjectIO();
136147
const endpoint_request_handler = endpoint.create_endpoint_handler('S3',
137-
endpoint.create_init_request_sdk(server_rpc.rpc, rpc_client, object_io), { virtual_hosts: [] });
148+
endpoint.create_init_request_sdk(server_rpc.rpc, rpc_client, object_io), { virtual_hosts: [], notification_logger });
138149
const endpoint_request_handler_sts = endpoint.create_endpoint_handler('STS',
139-
endpoint.create_init_request_sdk(server_rpc.rpc, rpc_client, object_io), { virtual_hosts: [] });
150+
endpoint.create_init_request_sdk(server_rpc.rpc, rpc_client, object_io), { virtual_hosts: [], notification_logger });
140151

141152
async function announce(msg) {
142153
if (process.env.SUPPRESS_LOGS) return;

src/test/unit_tests/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ require('./test_tiering_ttl_worker');
9898
// require('./test_tiering_upload');
9999
//require('./test_s3_worm');
100100
require('./test_bucket_logging');
101+
require('./test_notifications');
101102

102103
// UPGRADE
103104
// require('./test_postgres_upgrade'); // TODO currently working with mongo -> once changing to postgres - need to uncomment
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/* Copyright (C) 2025 NooBaa */
2+
'use strict';
3+
4+
// disabling init_rand_seed as it takes longer than the actual test execution
5+
process.env.DISABLE_INIT_RANDOM_SEED = "true";
6+
7+
const fs = require('fs');
8+
const path = require('path');
9+
10+
const config = require('../../../config');
11+
12+
// setup coretest first to prepare the env
13+
const { get_coretest_path, generate_s3_client, TMP_PATH } = require('../system_tests/test_utils');
14+
const coretest_path = get_coretest_path();
15+
const coretest = require(coretest_path);
16+
17+
const { rpc_client, EMAIL, POOL_LIST } = coretest;
18+
19+
console.log('POOL_LIST =', POOL_LIST);
20+
21+
const tmp_path = path.join(TMP_PATH, 'test_notifications');
22+
//dir for connect files
23+
const tmp_connect = path.join(tmp_path, 'connect');
24+
//dir for notification persistent files
25+
const notif_logs_path = path.join(tmp_path, 'notif_logs');
26+
27+
coretest.setup({pools_to_create: [POOL_LIST[1]]});
28+
const mocha = require('mocha');
29+
const http = require('http');
30+
const assert = require('assert');
31+
const timer = require('node:timers/promises');
32+
const notifications_util = require('../../util/notifications_util');
33+
34+
const http_connect_filename = 'http_connect.json';
35+
const http_connect_path = path.join(tmp_connect, http_connect_filename);
36+
//content of connect file, will be written to a file in before()
37+
const http_connect = {
38+
agent_request_object: {"host": "localhost", "port": 9998, "timeout": 1500},
39+
request_options_object: {"auth": "amit:passw", "timeout": 1500},
40+
notification_protocol: 'http',
41+
name: 'http_notif'
42+
};
43+
44+
const bucket = 'bucket-notif';
45+
46+
config.NOTIFICATION_LOG_DIR = notif_logs_path;
47+
config.NOTIFICATION_CONNECT_DIR = tmp_connect;
48+
49+
//an http server that will receive the notification
50+
let http_server = null;
51+
//has server finished handling the notification?
52+
let server_done = false;
53+
let expected_bucket;
54+
let expected_event_name;
55+
56+
// eslint-disable-next-line max-lines-per-function
57+
mocha.describe('notifications', function() {
58+
59+
this.timeout(40000); // eslint-disable-line no-invalid-this
60+
let s3;
61+
62+
describe('notifications', () => {
63+
64+
mocha.before(async function() {
65+
66+
const admin_keys = (await rpc_client.account.read_account({ email: EMAIL, })).access_keys;
67+
s3 = generate_s3_client(admin_keys[0].access_key.unwrap(),
68+
admin_keys[0].secret_key.unwrap(),
69+
coretest.get_http_address());
70+
71+
//create http connect file
72+
fs.mkdirSync(tmp_connect, {recursive: true});
73+
fs.writeFileSync(http_connect_path, JSON.stringify(http_connect));
74+
75+
fs.mkdirSync(notif_logs_path, {recursive: true});
76+
77+
await s3.createBucket({
78+
Bucket: bucket,
79+
});
80+
81+
http_server = http.createServer(async function(req, res) {
82+
const chunks = [];
83+
84+
for await (const chunk of req) {
85+
chunks.push(chunk);
86+
}
87+
88+
const input = Buffer.concat(chunks);
89+
const notif = JSON.parse(input.toString());
90+
91+
if (notif !== "test notification") {
92+
assert.strictEqual(notif.Records[0].s3.bucket.name, expected_bucket, 'wrong bucket name in notification');
93+
assert.strictEqual(notif.Records[0].eventName, expected_event_name, 'wrong event name in notification');
94+
95+
}
96+
97+
res.writeHead(200, {'Content-Type': 'text/plain'});
98+
res.end();
99+
server_done = true;
100+
}).listen(9998);
101+
});
102+
103+
mocha.after(() => {
104+
http_server.close();
105+
});
106+
107+
mocha.it('set/get notif conf s3ops', async () => {
108+
await s3.putBucketNotificationConfiguration({
109+
Bucket: bucket,
110+
NotificationConfiguration: {
111+
TopicConfigurations: [{
112+
"Id": "system_test_http_no_event",
113+
"TopicArn": http_connect_filename,
114+
}],
115+
},
116+
});
117+
118+
const get = await s3.getBucketNotificationConfiguration({Bucket: bucket});
119+
assert.strictEqual(get.TopicConfigurations[0].Id, 'system_test_http_no_event');
120+
});
121+
122+
mocha.it('simple notif put', async () => {
123+
await s3.putObject({
124+
Bucket: bucket,
125+
Key: 'f1',
126+
Body: 'this is the body',
127+
});
128+
129+
await notify_await_result({bucket_name: bucket, event_name: 'ObjectCreated:Put'});
130+
});
131+
132+
133+
mocha.it('simple notif delete', async () => {
134+
await s3.deleteObject({
135+
Bucket: bucket,
136+
Key: 'f1',
137+
});
138+
139+
await notify_await_result({bucket_name: bucket, event_name: 'ObjectRemoved:Delete'});
140+
});
141+
142+
143+
mocha.it('notifications with event filtering', async () => {
144+
145+
const set = await s3.putBucketNotificationConfiguration({
146+
Bucket: bucket,
147+
NotificationConfiguration: {
148+
TopicConfigurations: [{
149+
"Id": "system_test_http_event",
150+
"TopicArn": http_connect_filename,
151+
"Events": ["s3:ObjectCreated:*"],
152+
}],
153+
},
154+
});
155+
156+
assert.strictEqual(set.$metadata.httpStatusCode, 200);
157+
158+
await s3.putObject({
159+
Bucket: bucket,
160+
Key: 'f2',
161+
Body: 'this is the body',
162+
});
163+
164+
await notify_await_result({bucket_name: bucket, event_name: 'ObjectCreated:Put'});
165+
166+
await s3.deleteObject({
167+
Bucket: bucket,
168+
Key: 'f2',
169+
});
170+
171+
//there shouldn't be a notification for the delete, wait 2 seconds to validate this
172+
await notify_await_result({timeout: 2000});
173+
});
174+
});
175+
176+
});
177+
178+
const step_wait = 100;
179+
async function notify_await_result({bucket_name, event_name, timeout}) {
180+
181+
//remember expected result here so server could compare it to actual result later
182+
expected_bucket = bucket_name;
183+
expected_event_name = event_name;
184+
server_done = false;
185+
186+
//busy-sync wait for server
187+
//eslint-disable-next-line no-unmodified-loop-condition
188+
while (!server_done) {
189+
console.log('awaiting for notification to arrive, timeout =', timeout);
190+
await new notifications_util.Notificator({
191+
name: 'coretest notificator',
192+
connect_files_dir: tmp_connect,
193+
}).process_notification_files();
194+
await timer.setTimeout(step_wait);
195+
if (timeout !== undefined) {
196+
timeout -= step_wait;
197+
//timeout if we're validating notification did not arrive
198+
if (timeout < 0) break;
199+
}
200+
}
201+
202+
//if we were not expecting to get a notification (timeout is undefined),
203+
//make sure server_done remained false
204+
assert.strictEqual(timeout === undefined || !server_done, true, "unexpected notification received");
205+
assert.strictEqual(fs.readdirSync(notif_logs_path).length, 0, "notif log dir is not empty.");
206+
}

src/util/notifications_util.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ const OP_TO_EVENT = Object.freeze({
2929
lifecycle_delete: { name: 'LifecycleExpiration' },
3030
});
3131

32-
const DEFAULT_CONNECT_FILES_DIR = '/etc/notif_connect/';
33-
3432
class Notificator {
3533

3634
/**
@@ -43,7 +41,7 @@ class Notificator {
4341
this.connect_str_to_connection = new Map();
4442
this.notif_to_connect = new Map();
4543
this.fs_context = fs_context ?? get_process_fs_context();
46-
this.connect_files_dir = connect_files_dir ?? DEFAULT_CONNECT_FILES_DIR;
44+
this.connect_files_dir = connect_files_dir ?? config.NOTIFICATION_CONNECT_DIR;
4745
this.nc_config_fs = nc_config_fs;
4846
this.batch_size = batch_size || config.NOTIFICATION_BATCH || 10;
4947

@@ -352,7 +350,7 @@ async function test_notifications(notifs, nc_config_dir) {
352350
if (!notifs) {
353351
return;
354352
}
355-
let connect_files_dir = DEFAULT_CONNECT_FILES_DIR;
353+
let connect_files_dir = config.NOTIFICATION_CONNECT_DIR;
356354
if (nc_config_dir) {
357355
connect_files_dir = new ConfigFS(nc_config_dir).connections_dir_path;
358356
}

0 commit comments

Comments
 (0)