From df0eb9eb722b5f44d5818d9274c3271a470b5423 Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Mon, 29 Jun 2026 16:24:27 +0200 Subject: [PATCH 1/2] Apply prettier formatting to testServerAccessLogFile.js Pure formatting pass with no behavior change, isolated into its own commit so the functional change that follows has a clean, reviewable diff. The file predated prettier adoption and had never been reformatted. Issue: CLDSRV-923 Claude-Session: https://claude.ai/code/session_01QtnfMnRoAwPDEAdqnzPzjj --- .../testServerAccessLogFile.js | 604 ++++++++++-------- 1 file changed, 331 insertions(+), 273 deletions(-) diff --git a/tests/functional/aws-node-sdk/test/serverAccessLogs/testServerAccessLogFile.js b/tests/functional/aws-node-sdk/test/serverAccessLogs/testServerAccessLogFile.js index 5673027c8e..fe1f0dfaa1 100644 --- a/tests/functional/aws-node-sdk/test/serverAccessLogs/testServerAccessLogFile.js +++ b/tests/functional/aws-node-sdk/test/serverAccessLogs/testServerAccessLogFile.js @@ -26,7 +26,10 @@ function truncateLogFileIfExists(filePath) { async function waitForLogs(filePath, expectedLines, maxRetries, delayMs) { for (let attempt = 0; attempt < maxRetries; attempt++) { const logEntries = fs.readFileSync(filePath, 'utf8'); - const lines = logEntries.trim().split('\n').filter(line => line.length > 0); + const lines = logEntries + .trim() + .split('\n') + .filter(line => line.length > 0); if (lines.length >= expectedLines) { try { return lines.map(line => JSON.parse(line)); @@ -45,7 +48,10 @@ async function waitForLogs(filePath, expectedLines, maxRetries, delayMs) { async function waitForAction(filePath, action, maxRetries, delayMs) { for (let attempt = 0; attempt < maxRetries; attempt++) { const logEntries = fs.readFileSync(filePath, 'utf8'); - const lines = logEntries.trim().split('\n').filter(line => line.length > 0); + const lines = logEntries + .trim() + .split('\n') + .filter(line => line.length > 0); for (const line of lines) { try { const obj = JSON.parse(line); @@ -105,13 +111,15 @@ async function cleanupBuckets(s3) { for (const bucket of bucketsResponse.Buckets) { const listMPUResponse = await s3.listMultipartUploads({ Bucket: bucket.Name }); if (listMPUResponse.Uploads && listMPUResponse.Uploads.length > 0) { - await Promise.all(listMPUResponse.Uploads.map(upload => - s3.abortMultipartUpload({ - Bucket: bucket.Name, - Key: upload.Key, - UploadId: upload.UploadId, - }), - )); + await Promise.all( + listMPUResponse.Uploads.map(upload => + s3.abortMultipartUpload({ + Bucket: bucket.Name, + Key: upload.Key, + UploadId: upload.UploadId, + }), + ), + ); } await emptyBucket(s3, bucket.Name, true); @@ -150,11 +158,11 @@ describe('Server Access Logs - File Output', async () => { // 'time': '', // UNKNOWN // 'hostname': '', // UNKNOWN // 'pid': '', // UNKNOWN - 'action': 'REQUIRED', // DYNAMIC - 'accountName': 'Bart', // STATIC - 'userName': null, // TODO: Add test with IAM user to get a non null userName. + action: 'REQUIRED', // DYNAMIC + accountName: 'Bart', // STATIC + userName: null, // TODO: Add test with IAM user to get a non null userName. // 'clientPort': '', // UNKNOWN - 'httpMethod': 'REQUIRED', // DYNAMIC + httpMethod: 'REQUIRED', // DYNAMIC // 'bytesDeleted': '', // TODO // 'bytesReceived': '', // TODO // 'bodyLength': '', // TODO @@ -162,35 +170,35 @@ describe('Server Access Logs - File Output', async () => { // 'elapsed_ms': '', // UNKNOWN // 'httpURL': '', // TODO // 'startTime': '', // UNKNOWN - 'requester': '79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be', // STATIC - 'operation': 'REQUIRED', // DYNAMIC + requester: '79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be', // STATIC + operation: 'REQUIRED', // DYNAMIC // 'requestURI': '', // TODO - 'errorCode': null, // DYNAMIC + errorCode: null, // DYNAMIC // 'objectSize': '', // TODO // 'totalTime': '', // UNKNOWN // 'turnAroundTime': '', // UNKNOWN - 'referer': null, // TODO: Add test that sets the referer. + referer: null, // TODO: Add test that sets the referer. // 'userAgent': // UNKNOWN // 'versionID': '', // UNKNOWN - 'signatureVersion': 'SigV4', // STATIC - 'cipherSuite': null, // TODO: Add https tests. - 'authenticationType': 'AuthHeader', // STATIC + signatureVersion: 'SigV4', // STATIC + cipherSuite: null, // TODO: Add https tests. + authenticationType: 'AuthHeader', // STATIC // 'hostHeader': '', // UNKNOWN - 'tlsVersion': null, // TODO: Add https tests. - 'aclRequired': null, // DYNAMIC (absent for owner, "Yes" when ACL is consulted) - 'bucketOwner': '79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be', // DYNAMIC + tlsVersion: null, // TODO: Add https tests. + aclRequired: null, // DYNAMIC (absent for owner, "Yes" when ACL is consulted) + bucketOwner: '79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be', // DYNAMIC bucketName, // DYNAMIC // 'req_id': '', // UNKNOWN // 'bytesSent': '', // TODO // 'clientIP': '', // UNKNOWN - 'httpCode': 200, // DYNAMIC - 'objectKey': null, // DYNAMIC - 'logFormatVersion': '0', // STATIC - 'loggingEnabled': false, // DYNAMIC - 'loggingTargetBucket': null, // DYNAMIC - 'loggingTargetPrefix': null, // DYNAMIC - 'awsAccessKeyID': 'accessKey1', // STATIC - 'raftSessionID': null, // UNKNOWN but available with scality backend, null otherwise + httpCode: 200, // DYNAMIC + objectKey: null, // DYNAMIC + logFormatVersion: '0', // STATIC + loggingEnabled: false, // DYNAMIC + loggingTargetBucket: null, // DYNAMIC + loggingTargetPrefix: null, // DYNAMIC + awsAccessKeyID: 'accessKey1', // STATIC + raftSessionID: null, // UNKNOWN but available with scality backend, null otherwise }; const operations = [ @@ -217,7 +225,7 @@ describe('Server Access Logs - File Output', async () => { action: 'DeleteBucket', httpCode: 204, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -230,12 +238,14 @@ describe('Server Access Logs - File Output', async () => { await s3.putBucketCors({ Bucket: bucketName, CORSConfiguration: { - CORSRules: [{ - AllowedHeaders: ['*'], - AllowedMethods: ['GET', 'PUT'], - AllowedOrigins: ['*'], - }] - } + CORSRules: [ + { + AllowedHeaders: ['*'], + AllowedMethods: ['GET', 'PUT'], + AllowedOrigins: ['*'], + }, + ], + }, }); await s3.deleteBucketCors({ Bucket: bucketName }); }; @@ -262,7 +272,7 @@ describe('Server Access Logs - File Output', async () => { action: 'DeleteBucketCors', httpCode: 204, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -279,10 +289,10 @@ describe('Server Access Logs - File Output', async () => { { ApplyServerSideEncryptionByDefault: { SSEAlgorithm: 'AES256', - } - } - ] - } + }, + }, + ], + }, }); await s3.deleteBucketEncryption({ Bucket: bucketName }); }; @@ -309,7 +319,7 @@ describe('Server Access Logs - File Output', async () => { action: 'DeleteBucketEncryption', httpCode: 204, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -352,7 +362,7 @@ describe('Server Access Logs - File Output', async () => { action: 'DeleteBucketWebsite', httpCode: 204, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -388,7 +398,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.BUCKET', action: 'ListObjectsV2', httpMethod: 'GET', - } + }, ], }; })(), @@ -424,7 +434,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.BUCKET', action: 'ListObjects', httpMethod: 'GET', - } + }, ], }; })(), @@ -451,7 +461,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.ACL', action: 'GetBucketAcl', httpMethod: 'GET', - } + }, ], }; })(), @@ -464,8 +474,8 @@ describe('Server Access Logs - File Output', async () => { { AllowedOrigins: ['*'], AllowedMethods: ['GET', 'POST'], - } - ] + }, + ], }; const method = async () => { await s3.createBucket({ Bucket: bucketName }); @@ -494,7 +504,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.CORS', action: 'GetBucketCors', httpMethod: 'GET', - } + }, ], }; })(), @@ -523,7 +533,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.OBJECT', action: 'GetObjectLockConfiguration', httpMethod: 'GET', - } + }, ], }; })(), @@ -549,7 +559,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.VERSIONING', action: 'GetBucketVersioning', httpMethod: 'GET', - } + }, ], }; })(), @@ -587,7 +597,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.WEBSITE', action: 'GetBucketWebsite', httpMethod: 'GET', - } + }, ], }; })(), @@ -613,7 +623,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.LOCATION', action: 'GetBucketLocation', httpMethod: 'GET', - } + }, ], }; })(), @@ -628,10 +638,10 @@ describe('Server Access Logs - File Output', async () => { { ApplyServerSideEncryptionByDefault: { SSEAlgorithm: 'AES256', - } - } - ] - } + }, + }, + ], + }, }); await s3.getBucketEncryption({ Bucket: bucketName }); }; @@ -657,7 +667,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.ENCRYPTION', action: 'GetBucketEncryption', httpMethod: 'GET', - } + }, ], }; })(), @@ -683,7 +693,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.HEAD.BUCKET', action: 'HeadBucket', httpMethod: 'HEAD', - } + }, ], }; })(), @@ -702,7 +712,7 @@ describe('Server Access Logs - File Output', async () => { bucketOwner: null, action: 'CreateBucket', httpMethod: 'PUT', - } + }, ], }; })(), @@ -728,7 +738,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.ACL', action: 'PutBucketAcl', httpMethod: 'PUT', - } + }, ], }; })(), @@ -739,12 +749,14 @@ describe('Server Access Logs - File Output', async () => { await s3.putBucketCors({ Bucket: bucketName, CORSConfiguration: { - CORSRules: [{ - AllowedHeaders: ['*'], - AllowedMethods: ['GET', 'PUT'], - AllowedOrigins: ['*'], - }] - } + CORSRules: [ + { + AllowedHeaders: ['*'], + AllowedMethods: ['GET', 'PUT'], + AllowedOrigins: ['*'], + }, + ], + }, }); }; return { @@ -763,7 +775,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.CORS', action: 'PutBucketCors', httpMethod: 'PUT', - } + }, ], }; })(), @@ -772,7 +784,8 @@ describe('Server Access Logs - File Output', async () => { const method = async () => { await s3.createBucket({ Bucket: bucketName }); await s3.putBucketVersioning({ - Bucket: bucketName, VersioningConfiguration: { Status: 'Enabled' }, + Bucket: bucketName, + VersioningConfiguration: { Status: 'Enabled' }, }); }; return { @@ -791,7 +804,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.VERSIONING', action: 'PutBucketVersioning', httpMethod: 'PUT', - } + }, ], }; })(), @@ -802,8 +815,8 @@ describe('Server Access Logs - File Output', async () => { await s3.putBucketTagging({ Bucket: bucketName, Tagging: { - TagSet: [{ Key: 'testKey', Value: 'testValue' }] - } + TagSet: [{ Key: 'testKey', Value: 'testValue' }], + }, }); }; return { @@ -822,7 +835,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.TAGGING', action: 'PutBucketTagging', httpMethod: 'PUT', - } + }, ], }; })(), @@ -833,8 +846,8 @@ describe('Server Access Logs - File Output', async () => { await s3.putBucketTagging({ Bucket: bucketName, Tagging: { - TagSet: [{ Key: 'testKey', Value: 'testValue' }] - } + TagSet: [{ Key: 'testKey', Value: 'testValue' }], + }, }); await s3.deleteBucketTagging({ Bucket: bucketName }); }; @@ -861,7 +874,7 @@ describe('Server Access Logs - File Output', async () => { action: 'DeleteBucketTagging', httpCode: 204, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -872,8 +885,8 @@ describe('Server Access Logs - File Output', async () => { await s3.putBucketTagging({ Bucket: bucketName, Tagging: { - TagSet: [{ Key: 'testKey', Value: 'testValue' }] - } + TagSet: [{ Key: 'testKey', Value: 'testValue' }], + }, }); await s3.getBucketTagging({ Bucket: bucketName }); }; @@ -899,7 +912,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.TAGGING', action: 'GetBucketTagging', httpMethod: 'GET', - } + }, ], }; })(), @@ -908,22 +921,25 @@ describe('Server Access Logs - File Output', async () => { const method = async () => { await s3.createBucket({ Bucket: bucketName }); await s3.putBucketVersioning({ - Bucket: bucketName, VersioningConfiguration: { Status: 'Enabled' }, + Bucket: bucketName, + VersioningConfiguration: { Status: 'Enabled' }, }); await s3.putBucketReplication({ Bucket: bucketName, ReplicationConfiguration: { Role: 'arn:aws:iam::123456789012:role/src-role,arn:aws:iam::123456789012:role/dest-role', - Rules: [{ - ID: 'rule1', - Status: 'Enabled', - Priority: 1, - Filter: { Prefix: '' }, - Destination: { - Bucket: 'arn:aws:s3:::destination-bucket' - } - }] - } + Rules: [ + { + ID: 'rule1', + Status: 'Enabled', + Priority: 1, + Filter: { Prefix: '' }, + Destination: { + Bucket: 'arn:aws:s3:::destination-bucket', + }, + }, + ], + }, }); }; return { @@ -948,7 +964,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.REPLICATION', action: 'PutBucketReplication', httpMethod: 'PUT', - } + }, ], }; })(), @@ -957,22 +973,25 @@ describe('Server Access Logs - File Output', async () => { const method = async () => { await s3.createBucket({ Bucket: bucketName }); await s3.putBucketVersioning({ - Bucket: bucketName, VersioningConfiguration: { Status: 'Enabled' }, + Bucket: bucketName, + VersioningConfiguration: { Status: 'Enabled' }, }); await s3.putBucketReplication({ Bucket: bucketName, ReplicationConfiguration: { Role: 'arn:aws:iam::123456789012:role/src-role,arn:aws:iam::123456789012:role/dest-role', - Rules: [{ - ID: 'rule1', - Status: 'Enabled', - Priority: 1, - Filter: { Prefix: '' }, - Destination: { - Bucket: 'arn:aws:s3:::destination-bucket' - } - }] - } + Rules: [ + { + ID: 'rule1', + Status: 'Enabled', + Priority: 1, + Filter: { Prefix: '' }, + Destination: { + Bucket: 'arn:aws:s3:::destination-bucket', + }, + }, + ], + }, }); await s3.getBucketReplication({ Bucket: bucketName }); }; @@ -1004,7 +1023,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.REPLICATION', action: 'GetBucketReplication', httpMethod: 'GET', - } + }, ], }; })(), @@ -1013,22 +1032,25 @@ describe('Server Access Logs - File Output', async () => { const method = async () => { await s3.createBucket({ Bucket: bucketName }); await s3.putBucketVersioning({ - Bucket: bucketName, VersioningConfiguration: { Status: 'Enabled' }, + Bucket: bucketName, + VersioningConfiguration: { Status: 'Enabled' }, }); await s3.putBucketReplication({ Bucket: bucketName, ReplicationConfiguration: { Role: 'arn:aws:iam::123456789012:role/src-role,arn:aws:iam::123456789012:role/dest-role', - Rules: [{ - ID: 'rule1', - Status: 'Enabled', - Priority: 1, - Filter: { Prefix: '' }, - Destination: { - Bucket: 'arn:aws:s3:::destination-bucket' - } - }] - } + Rules: [ + { + ID: 'rule1', + Status: 'Enabled', + Priority: 1, + Filter: { Prefix: '' }, + Destination: { + Bucket: 'arn:aws:s3:::destination-bucket', + }, + }, + ], + }, }); await s3.deleteBucketReplication({ Bucket: bucketName }); }; @@ -1061,7 +1083,7 @@ describe('Server Access Logs - File Output', async () => { action: 'DeleteBucketReplication', httpCode: 204, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -1072,13 +1094,15 @@ describe('Server Access Logs - File Output', async () => { await s3.putBucketLifecycleConfiguration({ Bucket: bucketName, LifecycleConfiguration: { - Rules: [{ - ID: 'rule1', - Status: 'Enabled', - Filter: { Prefix: 'documents/' }, - Expiration: { Days: 365 } - }] - } + Rules: [ + { + ID: 'rule1', + Status: 'Enabled', + Filter: { Prefix: 'documents/' }, + Expiration: { Days: 365 }, + }, + ], + }, }); }; return { @@ -1097,7 +1121,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.LIFECYCLE', action: 'PutBucketLifecycleConfiguration', httpMethod: 'PUT', - } + }, ], }; })(), @@ -1108,13 +1132,15 @@ describe('Server Access Logs - File Output', async () => { await s3.putBucketLifecycleConfiguration({ Bucket: bucketName, LifecycleConfiguration: { - Rules: [{ - ID: 'rule1', - Status: 'Enabled', - Filter: { Prefix: 'documents/' }, - Expiration: { Days: 365 } - }] - } + Rules: [ + { + ID: 'rule1', + Status: 'Enabled', + Filter: { Prefix: 'documents/' }, + Expiration: { Days: 365 }, + }, + ], + }, }); await s3.getBucketLifecycleConfiguration({ Bucket: bucketName }); }; @@ -1140,7 +1166,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.LIFECYCLE', action: 'GetBucketLifecycleConfiguration', httpMethod: 'GET', - } + }, ], }; })(), @@ -1151,13 +1177,15 @@ describe('Server Access Logs - File Output', async () => { await s3.putBucketLifecycleConfiguration({ Bucket: bucketName, LifecycleConfiguration: { - Rules: [{ - ID: 'rule1', - Status: 'Enabled', - Filter: { Prefix: 'documents/' }, - Expiration: { Days: 365 } - }] - } + Rules: [ + { + ID: 'rule1', + Status: 'Enabled', + Filter: { Prefix: 'documents/' }, + Expiration: { Days: 365 }, + }, + ], + }, }); await s3.deleteBucketLifecycle({ Bucket: bucketName }); }; @@ -1184,7 +1212,7 @@ describe('Server Access Logs - File Output', async () => { action: 'DeleteBucketLifecycle', httpCode: 204, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -1196,13 +1224,15 @@ describe('Server Access Logs - File Output', async () => { Bucket: bucketName, Policy: JSON.stringify({ Version: '2012-10-17', - Statement: [{ - Effect: 'Allow', - Principal: '*', - Action: 's3:GetObject', - Resource: `arn:aws:s3:::${bucketName}/*` - }] - }) + Statement: [ + { + Effect: 'Allow', + Principal: '*', + Action: 's3:GetObject', + Resource: `arn:aws:s3:::${bucketName}/*`, + }, + ], + }), }); }; return { @@ -1221,7 +1251,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.BUCKETPOLICY', action: 'PutBucketPolicy', httpMethod: 'PUT', - } + }, ], }; })(), @@ -1233,13 +1263,15 @@ describe('Server Access Logs - File Output', async () => { Bucket: bucketName, Policy: JSON.stringify({ Version: '2012-10-17', - Statement: [{ - Effect: 'Allow', - Principal: '*', - Action: 's3:GetObject', - Resource: `arn:aws:s3:::${bucketName}/*` - }] - }) + Statement: [ + { + Effect: 'Allow', + Principal: '*', + Action: 's3:GetObject', + Resource: `arn:aws:s3:::${bucketName}/*`, + }, + ], + }), }); await s3.getBucketPolicy({ Bucket: bucketName }); }; @@ -1265,7 +1297,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.BUCKETPOLICY', action: 'GetBucketPolicy', httpMethod: 'GET', - } + }, ], }; })(), @@ -1277,13 +1309,15 @@ describe('Server Access Logs - File Output', async () => { Bucket: bucketName, Policy: JSON.stringify({ Version: '2012-10-17', - Statement: [{ - Effect: 'Allow', - Principal: '*', - Action: 's3:GetObject', - Resource: `arn:aws:s3:::${bucketName}/*` - }] - }) + Statement: [ + { + Effect: 'Allow', + Principal: '*', + Action: 's3:GetObject', + Resource: `arn:aws:s3:::${bucketName}/*`, + }, + ], + }), }); await s3.deleteBucketPolicy({ Bucket: bucketName }); }; @@ -1310,7 +1344,7 @@ describe('Server Access Logs - File Output', async () => { action: 'DeleteBucketPolicy', httpCode: 204, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -1328,10 +1362,10 @@ describe('Server Access Logs - File Output', async () => { Rule: { DefaultRetention: { Mode: 'GOVERNANCE', - Days: 1 - } - } - } + Days: 1, + }, + }, + }, }); }; return { @@ -1350,7 +1384,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.OBJECT', action: 'PutObjectLockConfiguration', httpMethod: 'PUT', - } + }, ], }; })(), @@ -1360,7 +1394,7 @@ describe('Server Access Logs - File Output', async () => { await s3.createBucket({ Bucket: bucketName }); await s3.putBucketNotificationConfiguration({ Bucket: bucketName, - NotificationConfiguration: {} + NotificationConfiguration: {}, }); }; return { @@ -1379,7 +1413,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.NOTIFICATION', action: 'PutBucketNotification', httpMethod: 'PUT', - } + }, ], }; })(), @@ -1405,7 +1439,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.NOTIFICATION', action: 'GetBucketNotification', httpMethod: 'GET', - } + }, ], }; })(), @@ -1420,10 +1454,10 @@ describe('Server Access Logs - File Output', async () => { { ApplyServerSideEncryptionByDefault: { SSEAlgorithm: 'AES256', - } - } - ] - } + }, + }, + ], + }, }); }; return { @@ -1442,7 +1476,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.ENCRYPTION', action: 'PutBucketEncryption', httpMethod: 'PUT', - } + }, ], }; })(), @@ -1452,7 +1486,7 @@ describe('Server Access Logs - File Output', async () => { await s3.createBucket({ Bucket: bucketName }); await s3.putBucketLogging({ Bucket: bucketName, - BucketLoggingStatus: {} + BucketLoggingStatus: {}, }); }; return { @@ -1471,7 +1505,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.PUT.LOGGING_STATUS', action: 'PutBucketLogging', httpMethod: 'PUT', - } + }, ], }; })(), @@ -1488,7 +1522,7 @@ describe('Server Access Logs - File Output', async () => { await s3.getBucketLogging({ Bucket: bucketName }); await s3.putBucketLogging({ Bucket: bucketName, - BucketLoggingStatus: {} + BucketLoggingStatus: {}, }); }; return { @@ -1525,7 +1559,7 @@ describe('Server Access Logs - File Output', async () => { loggingTargetBucket: bucketName, loggingTargetPrefix: 'prefix', httpMethod: 'PUT', - } + }, ], }; })(), @@ -1533,25 +1567,26 @@ describe('Server Access Logs - File Output', async () => { // This operation tests completing a multipart upload. const method = async () => { await s3.createBucket({ Bucket: bucketName }); - const uploadId = - (await s3.createMultipartUpload({ Bucket: bucketName, Key: objectKey })).UploadId; + const uploadId = (await s3.createMultipartUpload({ Bucket: bucketName, Key: objectKey })).UploadId; const uploadPartResponse = await s3.uploadPart({ Bucket: bucketName, Key: objectKey, PartNumber: 1, UploadId: uploadId, - Body: 'test data' + Body: 'test data', }); await s3.completeMultipartUpload({ Bucket: bucketName, Key: objectKey, UploadId: uploadId, MultipartUpload: { - Parts: [{ - ETag: uploadPartResponse.ETag, - PartNumber: 1 - }] - } + Parts: [ + { + ETag: uploadPartResponse.ETag, + PartNumber: 1, + }, + ], + }, }); }; return { @@ -1585,7 +1620,7 @@ describe('Server Access Logs - File Output', async () => { action: 'CompleteMultipartUpload', objectKey, httpMethod: 'POST', - } + }, ], }; })(), @@ -1612,7 +1647,7 @@ describe('Server Access Logs - File Output', async () => { action: 'CreateMultipartUpload', objectKey, httpMethod: 'POST', - } + }, ], }; })(), @@ -1646,7 +1681,7 @@ describe('Server Access Logs - File Output', async () => { operation: 'REST.GET.UPLOADS', action: 'ListMultipartUploads', httpMethod: 'GET', - } + }, ], }; })(), @@ -1654,14 +1689,13 @@ describe('Server Access Logs - File Output', async () => { // This operation tests listing parts of a multipart upload. const method = async () => { await s3.createBucket({ Bucket: bucketName }); - const uploadId = - (await s3.createMultipartUpload({ Bucket: bucketName, Key: objectKey })).UploadId; + const uploadId = (await s3.createMultipartUpload({ Bucket: bucketName, Key: objectKey })).UploadId; await s3.uploadPart({ Bucket: bucketName, Key: objectKey, PartNumber: 1, UploadId: uploadId, - Body: 'test data' + Body: 'test data', }); await s3.listParts({ Bucket: bucketName, Key: objectKey, UploadId: uploadId }); }; @@ -1696,7 +1730,7 @@ describe('Server Access Logs - File Output', async () => { action: 'ListParts', objectKey, httpMethod: 'GET', - } + }, ], }; })(), @@ -1712,9 +1746,9 @@ describe('Server Access Logs - File Output', async () => { Objects: [ { Key: objectKey }, { Key: `${objectKey}2` }, - { Key: `${objectKey}-non-existent` } - ] - } + { Key: `${objectKey}-non-existent` }, + ], + }, }); }; return { @@ -1784,7 +1818,7 @@ describe('Server Access Logs - File Output', async () => { action: 'DeleteObjects', httpMethod: 'POST', objectKey: null, - } + }, ], }; })(), @@ -1792,8 +1826,7 @@ describe('Server Access Logs - File Output', async () => { // This operation tests aborting a multipart upload. const method = async () => { await s3.createBucket({ Bucket: bucketName }); - const uploadId = - (await s3.createMultipartUpload({ Bucket: bucketName, Key: objectKey })).UploadId; + const uploadId = (await s3.createMultipartUpload({ Bucket: bucketName, Key: objectKey })).UploadId; await s3.abortMultipartUpload({ Bucket: bucketName, Key: objectKey, UploadId: uploadId }); }; return { @@ -1821,7 +1854,7 @@ describe('Server Access Logs - File Output', async () => { httpCode: 204, objectKey, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -1857,7 +1890,7 @@ describe('Server Access Logs - File Output', async () => { httpCode: 204, objectKey, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -1870,8 +1903,8 @@ describe('Server Access Logs - File Output', async () => { Bucket: bucketName, Key: objectKey, Tagging: { - TagSet: [{ Key: 'testKey', Value: 'testValue' }] - } + TagSet: [{ Key: 'testKey', Value: 'testValue' }], + }, }); await s3.deleteObjectTagging({ Bucket: bucketName, Key: objectKey }); }; @@ -1907,7 +1940,7 @@ describe('Server Access Logs - File Output', async () => { httpCode: 204, objectKey, httpMethod: 'DELETE', - } + }, ], }; })(), @@ -1942,7 +1975,7 @@ describe('Server Access Logs - File Output', async () => { action: 'GetObject', objectKey, httpMethod: 'GET', - } + }, ], }; })(), @@ -1977,7 +2010,7 @@ describe('Server Access Logs - File Output', async () => { action: 'GetObjectAttributes', objectKey, httpMethod: 'GET', - } + }, ], }; })(), @@ -2012,7 +2045,7 @@ describe('Server Access Logs - File Output', async () => { action: 'GetObjectAcl', objectKey, httpMethod: 'GET', - } + }, ], }; })(), @@ -2027,13 +2060,13 @@ describe('Server Access Logs - File Output', async () => { await s3.putObjectLegalHold({ Bucket: bucketName, Key: objectKey, - LegalHold: { Status: 'ON' } + LegalHold: { Status: 'ON' }, }); await s3.getObjectLegalHold({ Bucket: bucketName, Key: objectKey }); await s3.putObjectLegalHold({ Bucket: bucketName, Key: objectKey, - LegalHold: { Status: 'OFF' } + LegalHold: { Status: 'OFF' }, }); }; return { @@ -2074,7 +2107,7 @@ describe('Server Access Logs - File Output', async () => { action: 'PutObjectLegalHold', objectKey, httpMethod: 'PUT', - } + }, ], }; })(), @@ -2093,8 +2126,8 @@ describe('Server Access Logs - File Output', async () => { Key: objectKey, Retention: { Mode: 'GOVERNANCE', - RetainUntilDate: retainUntilDate - } + RetainUntilDate: retainUntilDate, + }, }); await s3.getObjectRetention({ Bucket: bucketName, Key: objectKey }); }; @@ -2129,7 +2162,7 @@ describe('Server Access Logs - File Output', async () => { action: 'GetObjectRetention', objectKey, httpMethod: 'GET', - } + }, ], }; })(), @@ -2164,7 +2197,7 @@ describe('Server Access Logs - File Output', async () => { action: 'GetObjectTagging', objectKey, httpMethod: 'GET', - } + }, ], }; })(), @@ -2176,7 +2209,7 @@ describe('Server Access Logs - File Output', async () => { await s3.copyObject({ Bucket: bucketName, CopySource: `${bucketName}/${objectKey}`, - Key: `${objectKey}-copy` + Key: `${objectKey}-copy`, }); }; return { @@ -2215,7 +2248,7 @@ describe('Server Access Logs - File Output', async () => { action: 'CopyObject', objectKey: `${objectKey}-copy`, httpMethod: 'PUT', - } + }, ], }; })(), @@ -2250,7 +2283,7 @@ describe('Server Access Logs - File Output', async () => { action: 'PutObjectAcl', objectKey, httpMethod: 'PUT', - } + }, ], }; })(), @@ -2265,12 +2298,12 @@ describe('Server Access Logs - File Output', async () => { await s3.putObjectLegalHold({ Bucket: bucketName, Key: objectKey, - LegalHold: { Status: 'ON' } + LegalHold: { Status: 'ON' }, }); await s3.putObjectLegalHold({ Bucket: bucketName, Key: objectKey, - LegalHold: { Status: 'OFF' } + LegalHold: { Status: 'OFF' }, }); }; return { @@ -2304,7 +2337,7 @@ describe('Server Access Logs - File Output', async () => { action: 'PutObjectLegalHold', objectKey, httpMethod: 'PUT', - } + }, ], }; })(), @@ -2317,8 +2350,8 @@ describe('Server Access Logs - File Output', async () => { Bucket: bucketName, Key: objectKey, Tagging: { - TagSet: [{ Key: 'testKey', Value: 'testValue' }] - } + TagSet: [{ Key: 'testKey', Value: 'testValue' }], + }, }); }; return { @@ -2345,7 +2378,7 @@ describe('Server Access Logs - File Output', async () => { action: 'PutObjectTagging', objectKey, httpMethod: 'PUT', - } + }, ], }; })(), @@ -2353,14 +2386,13 @@ describe('Server Access Logs - File Output', async () => { // This operation tests uploading a part in a multipart upload. const method = async () => { await s3.createBucket({ Bucket: bucketName }); - const uploadId = - (await s3.createMultipartUpload({ Bucket: bucketName, Key: objectKey })).UploadId; + const uploadId = (await s3.createMultipartUpload({ Bucket: bucketName, Key: objectKey })).UploadId; await s3.uploadPart({ Bucket: bucketName, Key: objectKey, PartNumber: 1, UploadId: uploadId, - Body: 'test data' + Body: 'test data', }); }; return { @@ -2387,7 +2419,7 @@ describe('Server Access Logs - File Output', async () => { action: 'UploadPart', objectKey, httpMethod: 'PUT', - } + }, ], }; })(), @@ -2396,15 +2428,14 @@ describe('Server Access Logs - File Output', async () => { const method = async () => { await s3.createBucket({ Bucket: bucketName }); await s3.putObject({ Bucket: bucketName, Key: objectKey, Body: 'test data for copy' }); - const uploadId = - (await s3.createMultipartUpload({ Bucket: bucketName, Key: `${objectKey}-mpu` })) - .UploadId; + const uploadId = (await s3.createMultipartUpload({ Bucket: bucketName, Key: `${objectKey}-mpu` })) + .UploadId; await s3.uploadPartCopy({ Bucket: bucketName, Key: `${objectKey}-mpu`, PartNumber: 1, UploadId: uploadId, - CopySource: `${bucketName}/${objectKey}` + CopySource: `${bucketName}/${objectKey}`, }); }; return { @@ -2450,7 +2481,7 @@ describe('Server Access Logs - File Output', async () => { action: 'UploadPartCopy', objectKey: `${objectKey}-mpu`, httpMethod: 'PUT', - } + }, ], }; })(), @@ -2469,8 +2500,8 @@ describe('Server Access Logs - File Output', async () => { Key: objectKey, Retention: { Mode: 'GOVERNANCE', - RetainUntilDate: retainUntilDate - } + RetainUntilDate: retainUntilDate, + }, }); }; return { @@ -2497,11 +2528,11 @@ describe('Server Access Logs - File Output', async () => { action: 'PutObjectRetention', objectKey, httpMethod: 'PUT', - } + }, ], }; })(), - // Note: objectRestore can only be called on objects in GLACIER, DEEP_ARCHIVE, or + // Note: objectRestore can only be called on objects in GLACIER, DEEP_ARCHIVE, or // GLACIER_IR storage classes. Since CloudServer only supports STANDARD storage class // by default, this operation returns "InvalidObjectState" error and cannot be tested. // This test is commented out until archive storage class support is added. @@ -2509,9 +2540,9 @@ describe('Server Access Logs - File Output', async () => { // // This operation tests the restore object API call. // const method = async () => { // await s3.createBucket({ Bucket: bucketName }); - // await s3.putObject({ - // Bucket: bucketName, - // Key: objectKey, + // await s3.putObject({ + // Bucket: bucketName, + // Key: objectKey, // Body: 'test data', // StorageClass: 'GLACIER' // Not supported in CloudServer // }); @@ -2549,7 +2580,7 @@ describe('Server Access Logs - File Output', async () => { action: 'PutObject', objectKey, httpMethod: 'PUT', - } + }, ], }; })(), @@ -2584,7 +2615,7 @@ describe('Server Access Logs - File Output', async () => { action: 'HeadObject', objectKey, httpMethod: 'HEAD', - } + }, ], }; })(), @@ -2612,7 +2643,7 @@ describe('Server Access Logs - File Output', async () => { bucketOwner: null, bucketName: null, httpMethod: 'GET', - } + }, ], }; })(), @@ -2620,7 +2651,7 @@ describe('Server Access Logs - File Output', async () => { // Test errorCode is set. const method = async () => { try { - await s3.deleteBucket({ Bucket: 'xxx'}); + await s3.deleteBucket({ Bucket: 'xxx' }); } catch { return; } @@ -2638,7 +2669,7 @@ describe('Server Access Logs - File Output', async () => { bucketOwner: null, bucketName: 'xxx', httpMethod: 'DELETE', - } + }, ], }; })(), @@ -2665,7 +2696,7 @@ describe('Server Access Logs - File Output', async () => { bucketName: 'xxx', httpMethod: 'PUT', objectKey: 'key', - } + }, ], }; })(), @@ -2692,10 +2723,10 @@ describe('Server Access Logs - File Output', async () => { bucketName: 'xxx', httpMethod: 'GET', objectKey: 'key', - } + }, ], }; - })() + })(), // TODO: CLDSRV-799 // (() => { // // Test errorCode is set. @@ -2747,34 +2778,48 @@ describe('Server Access Logs - File Output', async () => { afterEach(async () => { const lastAction = await cleanupBuckets(s3, bucketName); - await waitForAction(logFilePath, lastAction, - TEST_CONFIG.MAX_LOG_WAIT_RETRIES, TEST_CONFIG.LOG_POLL_DELAY_MS); + await waitForAction( + logFilePath, + lastAction, + TEST_CONFIG.MAX_LOG_WAIT_RETRIES, + TEST_CONFIG.LOG_POLL_DELAY_MS, + ); truncateLogFileIfExists(logFilePath); }); // Helper function to validate a log entry against expected properties const validateLogEntry = (logEntry, properties) => { const result = tv4.validateResult(logEntry, schema); - assert.strictEqual(result.valid, true, - `Log entry should match schema: ${JSON.stringify(result.error)}`); + assert.strictEqual(result.valid, true, `Log entry should match schema: ${JSON.stringify(result.error)}`); for (const [key, val] of Object.entries(properties)) { if (val === null) { - assert.strictEqual(key in logEntry, false, - `Field ${key} should be omitted when null, action ${properties.action}`); + assert.strictEqual( + key in logEntry, + false, + `Field ${key} should be omitted when null, action ${properties.action}`, + ); } else { - assert.strictEqual(logEntry[key], val, - `Invalid value for ${key}, action ${properties.action}`); + assert.strictEqual(logEntry[key], val, `Invalid value for ${key}, action ${properties.action}`); } } if (config.backends.metadata === 'scality') { - assert.strictEqual('raftSessionID' in logEntry, true, - `raftSessionID should be present for action ${properties.action}`); - assert.strictEqual(typeof logEntry.raftSessionID, 'string', - `raftSessionID should be a string for action ${properties.action}`); - assert.strictEqual(logEntry.raftSessionID.length > 0, true, - `raftSessionID should not be empty for action ${properties.action}`); + assert.strictEqual( + 'raftSessionID' in logEntry, + true, + `raftSessionID should be present for action ${properties.action}`, + ); + assert.strictEqual( + typeof logEntry.raftSessionID, + 'string', + `raftSessionID should be a string for action ${properties.action}`, + ); + assert.strictEqual( + logEntry.raftSessionID.length > 0, + true, + `raftSessionID should not be empty for action ${properties.action}`, + ); } }; @@ -2786,10 +2831,17 @@ describe('Server Access Logs - File Output', async () => { for (const exp of operation.expected) { totalExpected += exp.unordered ? exp.unordered.length : 1; } - const logEntries = await waitForLogs(logFilePath, totalExpected, - TEST_CONFIG.MAX_LOG_WAIT_RETRIES, TEST_CONFIG.LOG_POLL_DELAY_MS); - assert.strictEqual(logEntries.length, totalExpected, - `Expected ${totalExpected} log entries, got ${logEntries.length}`); + const logEntries = await waitForLogs( + logFilePath, + totalExpected, + TEST_CONFIG.MAX_LOG_WAIT_RETRIES, + TEST_CONFIG.LOG_POLL_DELAY_MS, + ); + assert.strictEqual( + logEntries.length, + totalExpected, + `Expected ${totalExpected} log entries, got ${logEntries.length}`, + ); let logIdx = 0; @@ -2805,15 +2857,21 @@ describe('Server Access Logs - File Output', async () => { for (const logEntry of unorderedLogs) { const matchIdx = remaining.findIndex(exp => exp.objectKey === logEntry.objectKey); - assert.notStrictEqual(matchIdx, -1, - `Unexpected log entry with objectKey: ${logEntry.objectKey}`); + assert.notStrictEqual( + matchIdx, + -1, + `Unexpected log entry with objectKey: ${logEntry.objectKey}`, + ); validateLogEntry(logEntry, remaining[matchIdx]); remaining.splice(matchIdx, 1); } - assert.strictEqual(remaining.length, 0, - `Missing expected entries: ${JSON.stringify(remaining)}`); + assert.strictEqual( + remaining.length, + 0, + `Missing expected entries: ${JSON.stringify(remaining)}`, + ); logIdx += expected.unordered.length; } else { From 9bb875dc8f0737b1a5caefad8e5aaa74749e3f0c Mon Sep 17 00:00:00 2001 From: Thomas Flament Date: Tue, 30 Jun 2026 15:30:48 +0200 Subject: [PATCH 2/2] Match server access log entries instead of asserting an exact count The serverAccessLogs tests asserted an exact entry count per operation while isolating tests by truncating the shared access-log file. That file is shared by the whole server process, and a request's line is written on the response 'close' event, which under HTTP keep-alive can fire much later, on socket teardown. A request from another suite (captured in CI: GetObject on buckettestgetobject, from test/object/get.js) can therefore have its line written during a serverAccessLogs test, landing inside the truncate window and inflating the count by one. The failing operation varied run to run because the victim was whichever test the foreign write happened to land in. Match each expected entry against the collected entries by its fields and ignore anything else, instead of asserting logEntries.length === totalExpected. Unordered groups are flattened and matched independently, so interleaved foreign entries are tolerated regardless of when they arrive. Issue: CLDSRV-923 Claude-Session: https://claude.ai/code/session_01QtnfMnRoAwPDEAdqnzPzjj --- .../testServerAccessLogFile.js | 131 +++++++++--------- 1 file changed, 64 insertions(+), 67 deletions(-) diff --git a/tests/functional/aws-node-sdk/test/serverAccessLogs/testServerAccessLogFile.js b/tests/functional/aws-node-sdk/test/serverAccessLogs/testServerAccessLogFile.js index fe1f0dfaa1..7893b29f3d 100644 --- a/tests/functional/aws-node-sdk/test/serverAccessLogs/testServerAccessLogFile.js +++ b/tests/functional/aws-node-sdk/test/serverAccessLogs/testServerAccessLogFile.js @@ -23,35 +23,61 @@ function truncateLogFileIfExists(filePath) { } } -async function waitForLogs(filePath, expectedLines, maxRetries, delayMs) { +function readLogLines(filePath) { + return fs + .readFileSync(filePath, 'utf8') + .trim() + .split('\n') + .filter(line => line.length > 0); +} + +// Whether a collected log entry satisfies an expected entry's constrained +// fields (same semantics as validateLogEntry). Lets us ignore unrelated lines +// from other suites sharing the server's access log file (CLDSRV-923). +function entryMatchesExpected(entry, properties) { + for (const [key, val] of Object.entries(properties)) { + if (key === 'unordered') { + continue; + } + if (val === null) { + if (key in entry) { + return false; + } + } else if (entry[key] !== val) { + return false; + } + } + return true; +} + +async function waitForExpectedLogs(filePath, expectedEntries, maxRetries, delayMs) { for (let attempt = 0; attempt < maxRetries; attempt++) { - const logEntries = fs.readFileSync(filePath, 'utf8'); - const lines = logEntries - .trim() - .split('\n') - .filter(line => line.length > 0); - if (lines.length >= expectedLines) { - try { - return lines.map(line => JSON.parse(line)); - } catch (err) { - // FIXME(CLDSRV-800): readFileSync may read partial lines making JSON.parse fail, so we need to retry. - if (attempt == maxRetries) { - throw new Error(`Failed to read log entries from ${filePath} after ${maxRetries} attempts: ${err}`); + const lines = readLogLines(filePath); + try { + const entries = lines.map(line => JSON.parse(line)); + const available = [...entries]; + const allFound = expectedEntries.every(exp => { + const idx = available.findIndex(entry => entryMatchesExpected(entry, exp)); + if (idx === -1) { + return false; } + available.splice(idx, 1); + return true; + }); + if (allFound) { + return entries; } + } catch { + // FIXME(CLDSRV-800): readFileSync may read partial lines making JSON.parse fail, so we need to retry. } await sleep(delayMs); } - throw new Error(`Failed to read log entries from ${filePath} after ${maxRetries} attempts`); + throw new Error(`Did not find all expected log entries in ${filePath} after ${maxRetries} attempts`); } async function waitForAction(filePath, action, maxRetries, delayMs) { for (let attempt = 0; attempt < maxRetries; attempt++) { - const logEntries = fs.readFileSync(filePath, 'utf8'); - const lines = logEntries - .trim() - .split('\n') - .filter(line => line.length > 0); + const lines = readLogLines(filePath); for (const line of lines) { try { const obj = JSON.parse(line); @@ -2826,59 +2852,30 @@ describe('Server Access Logs - File Output', async () => { for (const operation of operations) { it(`should log correct ${operation.methodName} operation with all required fields`, async () => { await operation.method(); - // Count total expected logs, including unordered entries - let totalExpected = 0; - for (const exp of operation.expected) { - totalExpected += exp.unordered ? exp.unordered.length : 1; - } - const logEntries = await waitForLogs( + // Flatten unordered groups; each entry is matched independently. + const expectedEntries = operation.expected.flatMap(exp => (exp.unordered ? exp.unordered : [exp])); + // The access log file is shared across the server process and a + // line can be written on a late res 'close', so other suites' + // lines can interleave. Match expected entries by field and ignore + // the rest instead of asserting an exact count (CLDSRV-923). + const logEntries = await waitForExpectedLogs( logFilePath, - totalExpected, + expectedEntries, TEST_CONFIG.MAX_LOG_WAIT_RETRIES, TEST_CONFIG.LOG_POLL_DELAY_MS, ); - assert.strictEqual( - logEntries.length, - totalExpected, - `Expected ${totalExpected} log entries, got ${logEntries.length}`, - ); - - let logIdx = 0; - - // Validate entries (ordered or unordered) - for (let i = 0; i < operation.expected.length; i++) { - const expected = operation.expected[i]; - - if (expected.unordered) { - // Handle unordered entries - const unorderedLogs = logEntries.slice(logIdx, logIdx + expected.unordered.length); - const remaining = [...expected.unordered]; - for (const logEntry of unorderedLogs) { - const matchIdx = remaining.findIndex(exp => exp.objectKey === logEntry.objectKey); - - assert.notStrictEqual( - matchIdx, - -1, - `Unexpected log entry with objectKey: ${logEntry.objectKey}`, - ); - - validateLogEntry(logEntry, remaining[matchIdx]); - remaining.splice(matchIdx, 1); - } - - assert.strictEqual( - remaining.length, - 0, - `Missing expected entries: ${JSON.stringify(remaining)}`, - ); - - logIdx += expected.unordered.length; - } else { - // Handle ordered entry - validateLogEntry(logEntries[logIdx], expected); - logIdx++; - } + const available = [...logEntries]; + for (const expected of expectedEntries) { + const idx = available.findIndex(entry => entryMatchesExpected(entry, expected)); + const keyInfo = expected.objectKey ? ` (objectKey ${expected.objectKey})` : ''; + assert.notStrictEqual( + idx, + -1, + `Missing expected log entry for action ${expected.action}${keyInfo}`, + ); + validateLogEntry(available[idx], expected); + available.splice(idx, 1); } }); }