Skip to content

Commit

Permalink
Merge pull request #4183 from greenbone/add-enhance-error-feed-for-mi…
Browse files Browse the repository at this point in the history
…ssing-detials

Enhance error message feed for missing details
  • Loading branch information
a-h-abdelsalam authored Oct 22, 2024
2 parents a0d5ec2 + f4393fb commit bfbd85a
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 17 deletions.
2 changes: 2 additions & 0 deletions allowedSnakeCase.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ module.exports = [
'family_list',
'feed_event',
'feed_type',
'feed_owner_set',
'feed_resources_access',

Check warning on line 180 in allowedSnakeCase.cjs

View check run for this annotation

Codecov / codecov/patch

allowedSnakeCase.cjs#L179-L180

Added lines #L179 - L180 were not covered by tests
'field_value',
'filtered_count',
'filter_func',
Expand Down
3 changes: 3 additions & 0 deletions public/locales/gsa-de.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"About GSA": "Über GSA",
"Access Complexity": "Zugangskomplexität",
"Access Vector": "Zugangsvektor",
"Access to the feed resources is currently restricted.": "Der Zugriff auf die Feed-Ressourcen ist derzeit eingeschränkt.",
"Actions": "Aktionen",
"Activate the \"attach\" option to allow changes here.": "Aktivieren Sie sie \"Anhängen\"-Option, um hier Änderungen vorzunehmen.",
"Activate the \"include\" option to make changes here.": "Aktivieren Sie die \"Einfügen\"-Option, um hier Änderungen vorzunehmen.",
Expand Down Expand Up @@ -1690,6 +1691,7 @@
"The families selection is STATIC. New families will NOT automatically be added and considered.": "Die Familien-Auswahl ist STATISCH. Neue Familien werden NICHT automatisch hinzugefügt und berücksichtigt.",
"The family selection is DYNAMIC. New families will automatically be added and considered.": "Die Familien-Auswahl ist DYNAMISCH. Neue Familien werden automatisch hinzugefügt und berücksichtigt.",
"The family selection is STATIC. New families will NOT automatically be added and considered.": "Die Familien-Auswahl ist STATISCH. Neue Familien werden NICHT automatisch hinzugefügt und berücksichtigt.",
"The feed owner is currently not set.": "Der Feed-Besitzer ist derzeit nicht festgelegt.",
"The following filter is currently applied: ": "Angewandter Filter: ",
"The last": "Jeden letzten",
"The last {{weekday}} every month": "Jeden letzten {{weekday}} jeden Monat",
Expand Down Expand Up @@ -1721,6 +1723,7 @@
"This form received invalid values. Please check the inputs and submit again.": "Dieses Formular erhielt ungültige Werte. Bitte überprüfen Sie Ihre Eingaben und senden sie erneut ab.",
"This is an Alterable Audit. Reports may not relate to current Policy or Target!": "Dies ist ein änderbares Audit. Berichte könnten sich nicht auf die aktuelle Richtlinie oder das aktuelle Ziel beziehen!",
"This is an Alterable Task. Reports may not relate to current Scan Config or Target!": "Dies ist eine änderbare Aufgabe. Berichte könnten sich nicht auf die aktuelle Scan-Konfiguration oder das aktuelle Ziel beziehen!",
"This issue may be due to the feed not having completed its synchronization.\nPlease try again shortly.": "Dieses Problem könnte daran liegen, dass der Feed seine Synchronisation noch nicht abgeschlossen hat.\nBitte versuchen Sie es in Kürze erneut.",
"This setting is not alterable once task has been run at least once.": "Diese Einstellung ist nicht änderbar sobald die Aufgabe mindestens einmal ausgeführt wurde.",
"This setting is not alterable once the audit has been run at least once.": "Diese Einstellung ist nicht änderbar sobald das audit mindestens einmal ausgeführt wurde.",
"This web application uses cookies to store session information. The cookies are not stored on the server side hard disk and not submitted anywhere. They are lost when the session is closed or expired. The cookies are stored temporarily in your browser as well where you can examine the content.": "Diese Web-Anwendung nutzt Cookies, um Sitzungsinformationen zu speichern. Die Cookies werden nicht auf der serverseitigen Festplatte gespeichert und nirgendwohin übermittelt. Sie gehen verloren, wenn die Sitzung beendet wird oder ausläuft. Die Cookies werden außerdem temporär in Ihrem Browser gespeichert, wo Sie den Inhalt einsehen können.",
Expand Down
43 changes: 42 additions & 1 deletion src/gmp/commands/__tests__/feedstatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import {describe, test, expect} from '@gsa/testing';
import {describe, test, expect, testing} from '@gsa/testing';

import {createResponse, createHttp} from '../testing';

Expand Down Expand Up @@ -113,4 +113,45 @@ describe('FeedStatusCommand tests', () => {

await expect(cmd.checkFeedSync()).rejects.toThrow('Network error');
});

describe('checkFeedOwnerAndPermissions', () => {
test('should return feed owner and permissions', async () => {
const response = createResponse({
get_feeds: {
get_feeds_response: {
feed_owner_set: 0,
feed_resources_access: 1,
},
},
});

const fakeHttp = createHttp(response);
const cmd = new FeedStatus(fakeHttp);

const result = await cmd.checkFeedOwnerAndPermissions();

expect(fakeHttp.request).toHaveBeenCalledWith('get', {
args: {
cmd: 'get_feeds',
},
});

expect(result.isFeedOwner).toBe(false);
expect(result.isFeedResourcesAccess).toBe(true);
});

test('should log an error when checkFeedSync fails', async () => {
const fakeHttp = createHttp(Promise.reject(new Error('Network error')));
const cmd = new FeedStatus(fakeHttp);

console.error = testing.fn();

await expect(cmd.checkFeedSync()).rejects.toThrow('Network error');

expect(console.error).toHaveBeenCalledWith(
'Error checking if feed is syncing:',
expect.any(Error),
);
});
});
});
36 changes: 36 additions & 0 deletions src/gmp/commands/feedstatus.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,42 @@ export class FeedStatus extends HttpCommand {
throw error;
}
}

/**
* Checks if the current user is the owner of the feed and if they have access to feed resources.
*
* @async
* @function checkFeedOwnerAndPermissions
* @returns {Promise<Object>} An object containing two boolean properties:
* - `isFeedOwner`: Indicates if the user is the owner of the feed.
* - `isFeedResourcesAccess`: Indicates if the user has access to feed resources.
* @throws Will throw an error if the HTTP request fails.
*/
async checkFeedOwnerAndPermissions() {
try {
const {
data: {
get_feeds: {
get_feeds_response: {
feed_owner_set: feedOwner,
feed_resources_access: feedResourcesAccess,
},
},
},
} = await this.httpGet();

const feedOwnerBoolean = Boolean(feedOwner);
const feedResourcesAccessBoolean = Boolean(feedResourcesAccess);

return {
isFeedOwner: feedOwnerBoolean,
isFeedResourcesAccess: feedResourcesAccessBoolean,
};
} catch (error) {
console.error('Error checking feed owner and permissions:', error);
throw error;
}

Check warning on line 117 in src/gmp/commands/feedstatus.js

View check run for this annotation

Codecov / codecov/patch

src/gmp/commands/feedstatus.js#L115-L117

Added lines #L115 - L117 were not covered by tests
}
}

registerCommand('feedstatus', FeedStatus);
Expand Down
88 changes: 88 additions & 0 deletions src/gmp/http/__tests__/http.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* SPDX-FileCopyrightText: 2024 Greenbone AG
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import {describe, test, expect, testing, beforeEach} from '@gsa/testing';

import Http from 'gmp/http/http';
import Rejection from '../rejection';
import {vi} from 'vitest';

const mockGetFeedAccessStatusMessage = testing.fn();
const mockFindActionInXMLString = testing.fn();

vi.mock('gmp/http/utils', async () => {
return {
getFeedAccessStatusMessage: () => mockGetFeedAccessStatusMessage(),
findActionInXMLString: () => mockFindActionInXMLString(),
};
});

global.XMLHttpRequest = testing.fn(() => ({
open: testing.fn(),
send: testing.fn(),
setRequestHeader: testing.fn(),
status: 0,
responseText: '',
onreadystatechange: null,
readyState: 0,
}));

describe('Http', () => {
describe('handleResponseError', () => {
let instance;
let reject;
let resolve;
let xhr;
let options;

beforeEach(() => {
instance = new Http();
resolve = testing.fn();
reject = testing.fn();
xhr = {status: 500};
options = {};
testing.clearAllMocks();
});
test('should handle response error without error handlers', async () => {
await instance.handleResponseError(xhr, reject, resolve, options);
expect(reject).toHaveBeenCalledWith(expect.any(Rejection));
});

test('401 error should call error handler', async () => {
xhr.status = 401;
await instance.handleResponseError(resolve, reject, xhr, options);
expect(reject).toHaveBeenCalledWith(expect.any(Rejection));
expect(reject.mock.calls[0][0].reason).toBe(
Rejection.REASON_UNAUTHORIZED,
);
});

test('404 error should append additional message', async () => {
xhr.status = 404;
const additionalMessage = 'Additional feed access status message';
mockGetFeedAccessStatusMessage.mockResolvedValue(additionalMessage);
mockFindActionInXMLString.mockReturnValue(true);

await instance.handleResponseError(resolve, reject, xhr, options);
expect(mockGetFeedAccessStatusMessage).toHaveBeenCalled();

expect(reject).toHaveBeenCalledWith(expect.any(Rejection));
const rejectedResponse = reject.mock.calls[0][0];
expect(rejectedResponse.message).toContain(additionalMessage);
});

test('404 error should not append additional message', async () => {
xhr.status = 404;
mockFindActionInXMLString.mockReturnValue(false);

await instance.handleResponseError(resolve, reject, xhr, options);
expect(mockGetFeedAccessStatusMessage).not.toHaveBeenCalled();

expect(reject).toHaveBeenCalledWith(expect.any(Rejection));
const rejectedResponse = reject.mock.calls[0][0];
expect(rejectedResponse.message).toContain('Unknown Error');
});
});
});
98 changes: 98 additions & 0 deletions src/gmp/http/__tests__/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* SPDX-FileCopyrightText: 2024 Greenbone AG
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import {describe, test, expect} from '@gsa/testing';

import {createResponse, createHttp} from 'gmp/commands/testing';

import {
getFeedAccessStatusMessage,
findActionInXMLString,
} from 'gmp/http/utils';
import {FeedStatus} from 'gmp/commands/feedstatus';

describe('Http', () => {
describe('getFeedAccessStatusMessage', () => {
const setupTest = async (feedOwnerSet, feedResourcesAccess) => {
const response = createResponse({
get_feeds: {
get_feeds_response: {
feed_owner_set: feedOwnerSet,
feed_resources_access: feedResourcesAccess,
},
},
});
const fakeHttp = createHttp(response);
const feedCmd = new FeedStatus(fakeHttp);
await feedCmd.checkFeedOwnerAndPermissions();
return getFeedAccessStatusMessage(fakeHttp);
};

test.each([
[
0,
1,
'The feed owner is currently not set. This issue may be due to the feed not having completed its synchronization.\nPlease try again shortly.',
],
[
1,
0,
'Access to the feed resources is currently restricted. This issue may be due to the feed not having completed its synchronization.\nPlease try again shortly.',
],
[1, 1, ''],
])(
'should return correct message for feedOwnerSet: %i, feedResourcesAccess: %i',
async (feedOwnerSet, feedResourcesAccess, expectedMessage) => {
const message = await setupTest(feedOwnerSet, feedResourcesAccess);
expect(message).toBe(expectedMessage);
},
);
});

describe('findActionInXMLString', () => {
test.each([
{
description:
'should return true if an action is found in the XML string',
xmlString: `
<response>
<action>Run Wizard</action>
</response>
`,
actions: ['Run Wizard', 'Create Task', 'Save Task'],
expected: true,
},
{
description:
'should return false if no action is found in the XML string',
xmlString: `
<response>
<action>Delete Task</action>
</response>
`,
actions: ['Run Wizard', 'Create Task', 'Save Task'],
expected: false,
},
{
description: 'should return false if the XML string is empty',
xmlString: '',
actions: ['Run Wizard', 'Create Task', 'Save Task'],
expected: false,
},
{
description: 'should return false if the actions array is empty',
xmlString: `
<response>
<action>Run Wizard</action>
</response>
`,
actions: [],
expected: false,
},
])('$description', ({xmlString, actions, expected}) => {
expect(findActionInXMLString(xmlString, actions)).toBe(expected);
});
});
});
54 changes: 41 additions & 13 deletions src/gmp/http/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import Response from './response';

import DefaultTransform from './transform/default';

import {buildUrlParams} from './utils';
import {
buildUrlParams,
getFeedAccessStatusMessage,
findActionInXMLString,
} from './utils';

const log = logger.getLogger('gmp.http');

Expand Down Expand Up @@ -157,26 +161,50 @@ class Http {
}
}

handleResponseError(resolve, reject, xhr, options) {
let promise = Promise.reject(xhr);
async handleResponseError(_resolve, reject, xhr, options) {
try {
let request = xhr;

for (const interceptor of this.errorHandlers) {
promise = promise.catch(interceptor);
}
for (const interceptor of this.errorHandlers) {
try {
await interceptor(request);
} catch (err) {
request = err;
}
}

Check warning on line 174 in src/gmp/http/http.js

View check run for this annotation

Codecov / codecov/patch

src/gmp/http/http.js#L169-L174

Added lines #L169 - L174 were not covered by tests

promise.catch(request => {
const {status} = request;
const rej = new Rejection(
request,
status === 401 ? Rejection.REASON_UNAUTHORIZED : Rejection.REASON_ERROR,
);
try {
reject(this.transformRejection(rej, options));
} catch (error) {
log.error('Could not transform rejection', error, rej);
reject(rej);

let rejectedResponse = await this.transformRejection(rej, options);

const actionsRequiringFeedAccess = [
'Run Wizard',
'Create Task',
'Save Task',
'Create Target',
'Save Target',
];

if (
rej.status === 404 &&
findActionInXMLString(request.response, actionsRequiringFeedAccess)
) {
const additionalMessage = await getFeedAccessStatusMessage(this);

if (additionalMessage) {
rejectedResponse.message = `${rejectedResponse.message}\n${additionalMessage}`;
}
}
});

reject(rejectedResponse);
} catch (error) {
log.error('Could not handle response error', error);
reject(error);
}

Check warning on line 207 in src/gmp/http/http.js

View check run for this annotation

Codecov / codecov/patch

src/gmp/http/http.js#L205-L207

Added lines #L205 - L207 were not covered by tests
}

handleRequestError(resolve, reject, xhr, options) {
Expand Down
Loading

0 comments on commit bfbd85a

Please sign in to comment.