From 9c665d150bc84bc757d9cb45a8162ff9aae2749b Mon Sep 17 00:00:00 2001 From: williamlardier Date: Mon, 13 Oct 2025 14:14:42 +0200 Subject: [PATCH 1/2] Handle 404 from Scuba backend For Veeam reoutes, this will set 0 as the metric. The value is correct: if scuba returns a 404 error, it means there is no metric for the resource. Issue: CLDSRV-758 --- lib/routes/veeam/get.js | 19 + tests/sur/routeVeeam.js | 105 ++++++ tests/unit/routes/veeam-routes.js | 569 ++++++++++++++++++++++++++++++ 3 files changed, 693 insertions(+) create mode 100644 tests/unit/routes/veeam-routes.js diff --git a/lib/routes/veeam/get.js b/lib/routes/veeam/get.js index c4a1929132..d802a29dfb 100644 --- a/lib/routes/veeam/get.js +++ b/lib/routes/veeam/get.js @@ -57,6 +57,25 @@ function getVeeamFile(request, response, bucketMd, log) { const bucketKey = `${bucketMd._name}_${new Date(bucketMd._creationDate).getTime()}`; return UtilizationService.getUtilizationMetrics('bucket', bucketKey, null, {}, (err, bucketMetrics) => { if (err) { + // Handle errors from UtilizationService/scubaclient + // axios errors have status in err.response.status + const statusCode = err.response?.status || err.statusCode || err.code; + // Only handle 404 gracefully (no metrics available yet, e.g. post-install) + // For 404, continue with static capacity data (Used=0 from bucket metadata) + if (statusCode === 404) { + log.warn('UtilizationService returned 404 when fetching capacity metrics', { + method: 'getVeeamFile', + bucket: request.bucketName, + error: err.message || err.code, + }); + return finalizeRequest(); + } + log.error('error fetching capacity metrics from UtilizationService', { + method: 'getVeeamFile', + bucket: request.bucketName, + error: err.message || err.code, + statusCode, + }); return responseXMLBody(errors.InternalError, null, response, log); } return finalizeRequest(bucketMetrics); diff --git a/tests/sur/routeVeeam.js b/tests/sur/routeVeeam.js index 640fb3fa2c..340d2923d1 100644 --- a/tests/sur/routeVeeam.js +++ b/tests/sur/routeVeeam.js @@ -383,6 +383,111 @@ function makeVeeamRequest(params, callback) { return done(); })); }); + + it('GET capacity.xml should return 200 when scubaclient returns 404 (post-install scenario)', done => { + // This test simulates the post-install scenario where scubaclient returns 404 + // because no metrics are available yet. By not calling scuba.incrementBytesForBucket, + // the mock scuba server will return 404 for this bucket. + + async.waterfall([ + next => makeVeeamRequest({ + method: 'PUT', + bucket: TEST_BUCKET, + objectKey: '.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/capacity.xml', + headers: { + 'content-length': testCapacity.length, + 'content-md5': testCapacityMd5, + 'x-scal-canonical-id': testArn, + }, + authCredentials: veeamAuthCredentials, + requestBody: testCapacity, + }, (err, response) => { + if (err) { + return done(err); + } + assert.strictEqual(response.statusCode, 200); + return next(); + }), + next => makeVeeamRequest({ + method: 'GET', + bucket: TEST_BUCKET, + objectKey: '.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/capacity.xml', + headers: { + 'x-scal-canonical-id': testArn, + }, + authCredentials: veeamAuthCredentials, + }, (err, response) => { + if (err) { + return done(err); + } + // Critical assertion: for 404 from scubaclient (no metrics yet), + // should return 200 with static capacity data (Used=0) + assert.strictEqual(response.statusCode, 200, + 'should return 200 when scubaclient returns 404 (no metrics available)'); + // Should return capacity.xml with static data + assert(response.body.includes(''), + 'should return capacity.xml content'); + assert(response.body.includes('0'), + 'Used should be 0 from static bucket metadata'); + return next(); + }), + ], err => { + assert.ifError(err); + return done(); + }); + }); + + it('GET system.xml should return 200 even when scubaclient is down', done => { + // system.xml doesn't use scubaclient, so it should always work + // This test stops scuba to verify system.xml is independent of utilization metrics + async.waterfall([ + next => makeVeeamRequest({ + method: 'PUT', + bucket: TEST_BUCKET, + objectKey: '.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/system.xml', + headers: { + 'content-length': testSystem.length, + 'content-md5': testSystemMd5, + 'x-scal-canonical-id': testArn, + }, + authCredentials: veeamAuthCredentials, + requestBody: testSystem, + }, (err, response) => { + if (err) { + return done(err); + } + assert.strictEqual(response.statusCode, 200); + return next(); + }), + next => { + // Stop scuba - system.xml should still work + scuba.stop(); + return next(); + }, + next => makeVeeamRequest({ + method: 'GET', + bucket: TEST_BUCKET, + objectKey: '.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/system.xml', + headers: { + 'x-scal-canonical-id': testArn, + }, + authCredentials: veeamAuthCredentials, + }, (err, response) => { + if (err) { + return done(err); + } + assert.strictEqual(response.statusCode, 200, + 'system.xml should always return 200 even when scuba is down'); + assert.strictEqual(response.body.replaceAll(' ', ''), testSystem.replaceAll(' ', '')); + return next(); + }), + ], err => { + // Restart scuba for subsequent tests + scuba.start(); + assert.ifError(err); + return done(); + }); + }); }); describe('veeam DELETE routes:', () => { diff --git a/tests/unit/routes/veeam-routes.js b/tests/unit/routes/veeam-routes.js new file mode 100644 index 0000000000..61e942a51c --- /dev/null +++ b/tests/unit/routes/veeam-routes.js @@ -0,0 +1,569 @@ +const assert = require('assert'); +const sinon = require('sinon'); +const getVeeamFile = require('../../../lib/routes/veeam/get'); +const headVeeamFile = require('../../../lib/routes/veeam/head'); +const listVeeamFiles = require('../../../lib/routes/veeam/list'); +const UtilizationService = require('../../../lib/utilization/instance'); +const metadata = require('../../../lib/metadata/wrapper'); +const { DummyRequestLogger } = require('../helpers'); + +describe('Veeam routes - comprehensive unit tests', () => { + let utilizationStub; + let metadataStub; + let log; + let logWarnSpy; + + const bucketMd = { + _name: 'test-bucket', + _creationDate: '2024-01-01T00:00:00.000Z', + getName: () => 'test-bucket', + _capabilities: { + VeeamSOSApi: { + CapacityInfo: { + Capacity: 1099511627776, + Available: 549755813888, + Used: 0, + LastModified: '2024-01-01T00:00:00.000Z', + }, + }, + }, + }; + + beforeEach(() => { + log = new DummyRequestLogger(); + logWarnSpy = sinon.spy(); + log.warn = logWarnSpy; + + // Mock log.end() to return an object with addDefaultFields and info methods + const logEndStub = { + addDefaultFields: sinon.stub().returnsThis(), + info: sinon.stub().returnsThis(), + }; + log.end = sinon.stub().returns(logEndStub); + log.debug = sinon.stub(); + + utilizationStub = sinon.stub(UtilizationService, 'getUtilizationMetrics'); + metadataStub = sinon.stub(metadata, 'getBucket'); + // By default, metadata.getBucket succeeds + metadataStub.callsArgWith(2, null, bucketMd); + }); + + afterEach(() => { + sinon.restore(); + }); + + const createRequest = (objectKey = '.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/capacity.xml') => ({ + bucketName: 'test-bucket', + objectKey, + query: {}, + headers: { + host: 'test-bucket.s3.amazonaws.com', + }, + }); + + const createResponse = () => { + const response = { + writeHead: sinon.stub(), + end: sinon.stub(), + setHeader: sinon.stub(), + on: sinon.stub(), + once: sinon.stub(), + emit: sinon.stub(), + write: sinon.stub(), + pipe: sinon.stub(), + removeListener: sinon.stub(), + statusCode: 200, + headersSent: false, + }; + // Make response.on return the response object for chaining + response.on.returns(response); + response.once.returns(response); + // Simulate successful end + response.end.callsFake(() => { + response.end.called = true; + response.headersSent = true; + // Emit finish event when end is called + const finishHandlers = response.on.getCalls() + .filter(call => call.args[0] === 'finish') + .map(call => call.args[1]); + finishHandlers.forEach(handler => handler()); + }); + // Simulate write + response.write.callsFake(() => { + response.headersSent = true; + return true; + }); + // Simulate writeHead + response.writeHead.callsFake(statusCode => { + response.statusCode = statusCode; + response.headersSent = true; + }); + return response; + }; + + it('should handle 404 error from UtilizationService and return 200', done => { + const error404 = new Error('Not Found'); + error404.response = { status: 404 }; + utilizationStub.callsArgWith(4, error404); + + const request = createRequest(); + const response = createResponse(); + + getVeeamFile(request, response, bucketMd, log); + + // Give async callback time to execute + setImmediate(() => { + assert(logWarnSpy.calledOnce, 'log.warn should have been called once'); + const warnCall = logWarnSpy.getCall(0); + assert(warnCall.args[0].includes('UtilizationService returned 404'), + 'warning message should mention 404'); + assert.strictEqual(warnCall.args[1].method, 'getVeeamFile'); + assert.strictEqual(warnCall.args[1].bucket, 'test-bucket'); + + assert(response.writeHead.calledWith(200), + 'should return 200 despite 404 from UtilizationService'); + assert(response.end.called, 'response should be ended'); + done(); + }); + }); + + it('should handle 500 error from UtilizationService and return 500', done => { + const error500 = new Error('Internal Server Error'); + error500.response = { status: 500 }; + utilizationStub.callsArgWith(4, error500); + + const request = createRequest(); + const response = createResponse(); + + getVeeamFile(request, response, bucketMd, log); + + setImmediate(() => { + // For 500 errors, we should return error to client + assert(response.headersSent || response.write.called || response.writeHead.called, + 'should send error response for 500 errors'); + done(); + }); + }); + + it('should handle connection error from UtilizationService and return 500', done => { + const errorConn = new Error('Connection refused'); + errorConn.code = 'ECONNREFUSED'; + utilizationStub.callsArgWith(4, errorConn); + + const request = createRequest(); + const response = createResponse(); + + getVeeamFile(request, response, bucketMd, log); + + setImmediate(() => { + // For connection errors, we should return error to client + assert(response.headersSent || response.write.called || response.writeHead.called, + 'should send error response for connection errors'); + done(); + }); + }); + + it('should successfully use metrics when UtilizationService returns data', done => { + const bucketMetrics = { + bytesTotal: 123456789, + }; + utilizationStub.callsArgWith(4, null, bucketMetrics); + + const request = createRequest(); + const response = createResponse(); + + getVeeamFile(request, response, bucketMd, log); + + setImmediate(() => { + assert(!logWarnSpy.called, 'log.warn should not have been called'); + assert(response.writeHead.calledWith(200), 'should return 200 with metrics'); + assert(utilizationStub.calledOnce, 'should call UtilizationService once'); + assert(response.end.called, 'response should be ended'); + done(); + }); + }); + + it('should not call UtilizationService for system.xml requests', done => { + const bucketMdWithSystem = { + ...bucketMd, + _capabilities: { + VeeamSOSApi: { + SystemInfo: { + ProtocolVersion: '1.0', + ModelName: 'ARTESCA', + LastModified: '2024-01-01T00:00:00.000Z', + }, + }, + }, + }; + metadataStub.callsArgWith(2, null, bucketMdWithSystem); + + const request = createRequest('.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/system.xml'); + const response = createResponse(); + + getVeeamFile(request, response, bucketMdWithSystem, log); + + setImmediate(() => { + assert(!utilizationStub.called, 'should not call UtilizationService for system.xml'); + assert(response.writeHead.calledWith(200), 'should return 200 for system.xml'); + assert(response.end.called, 'response should be ended'); + done(); + }); + }); + + it('should verify the post-install scenario: 404 returns 200 with Used=0', done => { + // This test reproduces the post-install scenario where scubaclient returns 404 + // because no metrics are available yet + const error404 = new Error('Not Found'); + error404.response = { status: 404 }; + utilizationStub.callsArgWith(4, error404); + + const request = createRequest(); + const response = createResponse(); + + getVeeamFile(request, response, bucketMd, log); + + setImmediate(() => { + // Verify we logged a warning + assert(logWarnSpy.calledOnce, 'should log warning for 404'); + + // The critical assertion: for 404, we should return 200 with static capacity data + assert(response.writeHead.calledWith(200), + 'should return 200 with static capacity data for 404'); + + assert(response.end.called, 'response should be ended'); + + // Verify it's specifically handling 404 gracefully + const warnCall = logWarnSpy.getCall(0); + assert(warnCall.args[0].includes('404'), 'warning should mention 404'); + + done(); + }); + }); + + it('should handle metadata.getBucket errors gracefully', done => { + const metadataError = new Error('Metadata service error'); + metadataStub.callsArgWith(2, metadataError); + + const request = createRequest(); + const response = createResponse(); + + getVeeamFile(request, response, bucketMd, log); + + setImmediate(() => { + // Metadata errors are returned via responseXMLBody + assert(response.headersSent || response.write.called || response.writeHead.called, + 'should send response for metadata errors'); + done(); + }); + }); + + it('should handle tagging query parameter', done => { + const request = createRequest(); + request.query = { tagging: '' }; + const response = createResponse(); + + getVeeamFile(request, response, bucketMd, log); + + setImmediate(() => { + assert(response.writeHead.calledWith(200), + 'should return 200 for tagging query'); + assert(response.end.called, 'response should be ended'); + done(); + }); + }); +}); + +describe('Veeam routes - HEAD request UtilizationService error handling', () => { + let metadataStub; + let log; + let logWarnSpy; + + const bucketMd = { + _name: 'test-bucket', + _creationDate: '2024-01-01T00:00:00.000Z', + getName: () => 'test-bucket', + _capabilities: { + VeeamSOSApi: { + CapacityInfo: { + Capacity: 1099511627776, + Available: 549755813888, + Used: 0, + LastModified: '2024-01-01T00:00:00.000Z', + }, + }, + }, + }; + + beforeEach(() => { + log = new DummyRequestLogger(); + logWarnSpy = sinon.spy(); + log.warn = logWarnSpy; + + const logEndStub = { + addDefaultFields: sinon.stub().returnsThis(), + info: sinon.stub().returnsThis(), + }; + log.end = sinon.stub().returns(logEndStub); + log.debug = sinon.stub(); + + metadataStub = sinon.stub(metadata, 'getBucket'); + metadataStub.callsArgWith(2, null, bucketMd); + }); + + afterEach(() => { + sinon.restore(); + }); + + const createRequest = (objectKey = '.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/capacity.xml') => ({ + bucketName: 'test-bucket', + objectKey, + query: {}, + headers: { + host: 'test-bucket.s3.amazonaws.com', + }, + }); + + const createResponse = () => { + const response = { + writeHead: sinon.stub(), + end: sinon.stub(), + setHeader: sinon.stub(), + on: sinon.stub(), + once: sinon.stub(), + emit: sinon.stub(), + write: sinon.stub(), + pipe: sinon.stub(), + removeListener: sinon.stub(), + statusCode: 200, + }; + response.on.returns(response); + response.once.returns(response); + response.end.callsFake(() => { + response.end.called = true; + }); + return response; + }; + + it('should handle HEAD request for system.xml', done => { + const bucketMdWithSystem = { + ...bucketMd, + _capabilities: { + VeeamSOSApi: { + SystemInfo: { + ProtocolVersion: '1.0', + ModelName: 'ARTESCA', + LastModified: '2024-01-01T00:00:00.000Z', + }, + }, + }, + }; + metadataStub.callsArgWith(2, null, bucketMdWithSystem); + + const request = createRequest('.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/system.xml'); + const response = createResponse(); + + headVeeamFile(request, response, bucketMdWithSystem, log); + + setImmediate(() => { + assert(response.setHeader.called, 'should set headers'); + assert(response.end.called, 'response should be ended'); + done(); + }); + }); + + it('should handle HEAD request for capacity.xml', done => { + const request = createRequest('.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/capacity.xml'); + const response = createResponse(); + + headVeeamFile(request, response, bucketMd, log); + + setImmediate(() => { + assert(response.setHeader.called, 'should set headers'); + assert(response.end.called, 'response should be ended'); + done(); + }); + }); + + it('should return 404 when no VeeamSOSApi capabilities', done => { + const bucketMdWithoutVeeam = { + ...bucketMd, + _capabilities: {}, + }; + metadataStub.callsArgWith(2, null, bucketMdWithoutVeeam); + + const request = createRequest(); + const response = createResponse(); + + headVeeamFile(request, response, bucketMdWithoutVeeam, log); + + setImmediate(() => { + // HEAD should return 404 via headers, not body + assert(response.end.called, 'response should be ended'); + done(); + }); + }); +}); + +describe('Veeam routes - LIST request handling', () => { + let metadataStub; + let log; + let logWarnSpy; + + const bucketMd = { + _name: 'test-bucket', + _creationDate: '2024-01-01T00:00:00.000Z', + getName: () => 'test-bucket', + _capabilities: { + VeeamSOSApi: { + SystemInfo: { + ProtocolVersion: '1.0', + ModelName: 'ARTESCA', + LastModified: '2024-01-01T00:00:00.000Z', + }, + CapacityInfo: { + Capacity: 1099511627776, + Available: 549755813888, + Used: 0, + LastModified: '2024-01-01T00:00:00.000Z', + }, + }, + }, + }; + + beforeEach(() => { + log = new DummyRequestLogger(); + logWarnSpy = sinon.spy(); + log.warn = logWarnSpy; + + const logEndStub = { + addDefaultFields: sinon.stub().returnsThis(), + info: sinon.stub().returnsThis(), + }; + log.end = sinon.stub().returns(logEndStub); + log.debug = sinon.stub(); + log.trace = sinon.stub(); + + metadataStub = sinon.stub(metadata, 'getBucket'); + metadataStub.callsArgWith(2, null, bucketMd); + }); + + afterEach(() => { + sinon.restore(); + }); + + const createRequest = (query = { 'list-type': '2' }) => ({ + bucketName: 'test-bucket', + objectKey: '.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/', + query, + headers: { + host: 'test-bucket.s3.amazonaws.com', + }, + url: '/_/veeam/test-bucket/.system-d26a9498-cb7c-4a87-a44a-8ae204f5ba6c/', + }); + + const createResponse = () => { + const response = { + writeHead: sinon.stub(), + end: sinon.stub(), + setHeader: sinon.stub(), + on: sinon.stub(), + once: sinon.stub(), + emit: sinon.stub(), + write: sinon.stub(), + pipe: sinon.stub(), + removeListener: sinon.stub(), + statusCode: 200, + }; + response.on.returns(response); + response.once.returns(response); + response.end.callsFake(() => { + response.end.called = true; + const finishHandlers = response.on.getCalls() + .filter(call => call.args[0] === 'finish') + .map(call => call.args[1]); + finishHandlers.forEach(handler => handler()); + }); + return response; + }; + + it('should list both system.xml and capacity.xml when both are present', done => { + const request = createRequest(); + const response = createResponse(); + + listVeeamFiles(request, response, bucketMd, log); + + setImmediate(() => { + assert(response.writeHead.calledWith(200), 'should return 200'); + assert(response.end.called, 'response should be ended'); + done(); + }); + }); + + it('should handle versions query parameter', done => { + const request = createRequest({ versions: '' }); + const response = createResponse(); + + listVeeamFiles(request, response, bucketMd, log); + + setImmediate(() => { + assert(response.writeHead.calledWith(200), 'should return 200 for versions query'); + assert(response.end.called, 'response should be ended'); + done(); + }); + }); + + it('should return error for invalid query parameters', done => { + const request = createRequest({ 'invalid-param': 'value' }); + const response = createResponse(); + + listVeeamFiles(request, response, bucketMd, log); + + setImmediate(() => { + // Should return error for invalid query parameter + assert(response.end.called, 'response should be ended'); + done(); + }); + }); + + it('should handle missing bucket metadata', done => { + const request = createRequest(); + const response = createResponse(); + + listVeeamFiles(request, response, null, log); + + setImmediate(() => { + // Should return NoSuchBucket error + assert(response.end.called, 'response should be ended'); + done(); + }); + }); + + it('should list only available files when some capabilities are missing', done => { + const bucketMdOnlySystem = { + ...bucketMd, + _capabilities: { + VeeamSOSApi: { + SystemInfo: { + ProtocolVersion: '1.0', + ModelName: 'ARTESCA', + LastModified: '2024-01-01T00:00:00.000Z', + }, + }, + }, + }; + metadataStub.callsArgWith(2, null, bucketMdOnlySystem); + + const request = createRequest(); + const response = createResponse(); + + listVeeamFiles(request, response, bucketMdOnlySystem, log); + + setImmediate(() => { + assert(response.writeHead.calledWith(200), 'should return 200'); + assert(response.end.called, 'response should be ended'); + done(); + }); + }); +}); + From 3f8ed8129e2f046e69ef2ece4e0230b2d64a1fc4 Mon Sep 17 00:00:00 2001 From: williamlardier Date: Thu, 23 Oct 2025 10:21:18 +0200 Subject: [PATCH 2/2] Improve test logic Issue: CLDSRV-758 --- tests/unit/routes/veeam-routes.js | 45 +++++++++++++------------------ 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/tests/unit/routes/veeam-routes.js b/tests/unit/routes/veeam-routes.js index 61e942a51c..47134d07e9 100644 --- a/tests/unit/routes/veeam-routes.js +++ b/tests/unit/routes/veeam-routes.js @@ -7,6 +7,9 @@ const UtilizationService = require('../../../lib/utilization/instance'); const metadata = require('../../../lib/metadata/wrapper'); const { DummyRequestLogger } = require('../helpers'); +// Helper function to give async callbacks time to execute +const giveAsyncCallbackTimeToExecute = setImmediate; + describe('Veeam routes - comprehensive unit tests', () => { let utilizationStub; let metadataStub; @@ -111,8 +114,7 @@ describe('Veeam routes - comprehensive unit tests', () => { getVeeamFile(request, response, bucketMd, log); - // Give async callback time to execute - setImmediate(() => { + giveAsyncCallbackTimeToExecute(() => { assert(logWarnSpy.calledOnce, 'log.warn should have been called once'); const warnCall = logWarnSpy.getCall(0); assert(warnCall.args[0].includes('UtilizationService returned 404'), @@ -137,8 +139,7 @@ describe('Veeam routes - comprehensive unit tests', () => { getVeeamFile(request, response, bucketMd, log); - setImmediate(() => { - // For 500 errors, we should return error to client + giveAsyncCallbackTimeToExecute(() => { assert(response.headersSent || response.write.called || response.writeHead.called, 'should send error response for 500 errors'); done(); @@ -155,8 +156,7 @@ describe('Veeam routes - comprehensive unit tests', () => { getVeeamFile(request, response, bucketMd, log); - setImmediate(() => { - // For connection errors, we should return error to client + giveAsyncCallbackTimeToExecute(() => { assert(response.headersSent || response.write.called || response.writeHead.called, 'should send error response for connection errors'); done(); @@ -174,7 +174,7 @@ describe('Veeam routes - comprehensive unit tests', () => { getVeeamFile(request, response, bucketMd, log); - setImmediate(() => { + giveAsyncCallbackTimeToExecute(() => { assert(!logWarnSpy.called, 'log.warn should not have been called'); assert(response.writeHead.calledWith(200), 'should return 200 with metrics'); assert(utilizationStub.calledOnce, 'should call UtilizationService once'); @@ -223,17 +223,11 @@ describe('Veeam routes - comprehensive unit tests', () => { getVeeamFile(request, response, bucketMd, log); - setImmediate(() => { - // Verify we logged a warning + giveAsyncCallbackTimeToExecute(() => { assert(logWarnSpy.calledOnce, 'should log warning for 404'); - - // The critical assertion: for 404, we should return 200 with static capacity data assert(response.writeHead.calledWith(200), 'should return 200 with static capacity data for 404'); - assert(response.end.called, 'response should be ended'); - - // Verify it's specifically handling 404 gracefully const warnCall = logWarnSpy.getCall(0); assert(warnCall.args[0].includes('404'), 'warning should mention 404'); @@ -250,8 +244,7 @@ describe('Veeam routes - comprehensive unit tests', () => { getVeeamFile(request, response, bucketMd, log); - setImmediate(() => { - // Metadata errors are returned via responseXMLBody + giveAsyncCallbackTimeToExecute(() => { assert(response.headersSent || response.write.called || response.writeHead.called, 'should send response for metadata errors'); done(); @@ -265,7 +258,7 @@ describe('Veeam routes - comprehensive unit tests', () => { getVeeamFile(request, response, bucketMd, log); - setImmediate(() => { + giveAsyncCallbackTimeToExecute(() => { assert(response.writeHead.calledWith(200), 'should return 200 for tagging query'); assert(response.end.called, 'response should be ended'); @@ -365,7 +358,7 @@ describe('Veeam routes - HEAD request UtilizationService error handling', () => headVeeamFile(request, response, bucketMdWithSystem, log); - setImmediate(() => { + giveAsyncCallbackTimeToExecute(() => { assert(response.setHeader.called, 'should set headers'); assert(response.end.called, 'response should be ended'); done(); @@ -378,7 +371,7 @@ describe('Veeam routes - HEAD request UtilizationService error handling', () => headVeeamFile(request, response, bucketMd, log); - setImmediate(() => { + giveAsyncCallbackTimeToExecute(() => { assert(response.setHeader.called, 'should set headers'); assert(response.end.called, 'response should be ended'); done(); @@ -397,7 +390,7 @@ describe('Veeam routes - HEAD request UtilizationService error handling', () => headVeeamFile(request, response, bucketMdWithoutVeeam, log); - setImmediate(() => { + giveAsyncCallbackTimeToExecute(() => { // HEAD should return 404 via headers, not body assert(response.end.called, 'response should be ended'); done(); @@ -493,7 +486,7 @@ describe('Veeam routes - LIST request handling', () => { listVeeamFiles(request, response, bucketMd, log); - setImmediate(() => { + giveAsyncCallbackTimeToExecute(() => { assert(response.writeHead.calledWith(200), 'should return 200'); assert(response.end.called, 'response should be ended'); done(); @@ -506,7 +499,7 @@ describe('Veeam routes - LIST request handling', () => { listVeeamFiles(request, response, bucketMd, log); - setImmediate(() => { + giveAsyncCallbackTimeToExecute(() => { assert(response.writeHead.calledWith(200), 'should return 200 for versions query'); assert(response.end.called, 'response should be ended'); done(); @@ -519,7 +512,7 @@ describe('Veeam routes - LIST request handling', () => { listVeeamFiles(request, response, bucketMd, log); - setImmediate(() => { + giveAsyncCallbackTimeToExecute(() => { // Should return error for invalid query parameter assert(response.end.called, 'response should be ended'); done(); @@ -532,8 +525,8 @@ describe('Veeam routes - LIST request handling', () => { listVeeamFiles(request, response, null, log); - setImmediate(() => { - // Should return NoSuchBucket error + giveAsyncCallbackTimeToExecute(() => { + assert(response.writeHead.calledWith(404), 'should return 404'); assert(response.end.called, 'response should be ended'); done(); }); @@ -559,7 +552,7 @@ describe('Veeam routes - LIST request handling', () => { listVeeamFiles(request, response, bucketMdOnlySystem, log); - setImmediate(() => { + giveAsyncCallbackTimeToExecute(() => { assert(response.writeHead.calledWith(200), 'should return 200'); assert(response.end.called, 'response should be ended'); done();