Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: redirect to login even if logout requests fail #513

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions src/network/fetch-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* SPDX-FileCopyrightText: 2024 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { userAgent } from './user-agent';
import { JSNS } from '../constants';
import { useAccountStore } from '../store/account';
import { useNetworkStore } from '../store/network';
import type { Account } from '../types/account';
import type { RawSoapResponse } from '../types/network';

const getAccount = (
acc?: Account,
otherAccount?: string
): { by: string; _content: string } | undefined => {
if (otherAccount) {
return {
by: 'name',
_content: otherAccount
};
}
if (acc) {
if (acc.name) {
return {
by: 'name',
_content: acc.name
};
}
if (acc.id) {
return {
by: 'id',
_content: acc.id
};
}
}
return undefined;
};

export const soapFetch = async <Request, Response extends Record<string, unknown>>(
api: string,
body: Request,
otherAccount?: string,
signal?: AbortSignal
): Promise<RawSoapResponse<Response>> => {
const { zimbraVersion, account } = useAccountStore.getState();
const { notify, session } = useNetworkStore.getState();
const res = await fetch(`/service/soap/${api}Request`, {
signal,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
Body: {
[`${api}Request`]: body
},
Header: {
context: {
_jsns: JSNS.all,
notify: notify?.[0]?.seq
? {
seq: notify?.[0]?.seq
}
: undefined,
session: session ?? {},
account: getAccount(account, otherAccount),
userAgent: {
name: userAgent,
version: zimbraVersion
}
}
}
})
});
return res.json();
};
64 changes: 4 additions & 60 deletions src/network/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { find, map, maxBy } from 'lodash';

import { soapFetch } from './fetch-utils';
import { userAgent } from './user-agent';
import { goToLogin } from './utils';
import { IS_FOCUS_MODE, JSNS, SHELL_APP_ID } from '../constants';
Expand Down Expand Up @@ -34,33 +35,6 @@
);
};

const getAccount = (
acc?: Account,
otherAccount?: string
): { by: string; _content: string } | undefined => {
if (otherAccount) {
return {
by: 'name',
_content: otherAccount
};
}
if (acc) {
if (acc.name) {
return {
by: 'name',
_content: acc.name
};
}
if (acc.id) {
return {
by: 'id',
_content: acc.id
};
}
}
return undefined;
};

const getXmlAccount = (acc?: Account, otherAccount?: string): string => {
if (otherAccount) {
return `<account by="name">${otherAccount}</account>`;
Expand Down Expand Up @@ -154,44 +128,14 @@
body: Request,
otherAccount?: string,
signal?: AbortSignal
): Promise<Response> => {
const { zimbraVersion, account } = useAccountStore.getState();
const { notify, session } = useNetworkStore.getState();
return fetch(`/service/soap/${api}Request`, {
signal,
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
Body: {
[`${api}Request`]: body
},
Header: {
context: {
_jsns: JSNS.all,
notify: notify?.[0]?.seq
? {
seq: notify?.[0]?.seq
}
: undefined,
session: session ?? {},
account: getAccount(account, otherAccount),
userAgent: {
name: userAgent,
version: zimbraVersion
}
}
}
})
}) // TODO proper error handling
.then((res) => res?.json())
): Promise<Response> =>
soapFetch<Request, Response>(api, body, otherAccount, signal)
// TODO proper error handling

Check notice on line 133 in src/network/fetch.ts

View check run for this annotation

Sonarqube Zextras / carbonio-shell-ui Sonarqube Results

src/network/fetch.ts#L133

Complete the task associated to this "TODO" comment.
.then((res: RawSoapResponse<Response>) => handleResponse(api, res))
.catch((e) => {
report(app)(e);
throw e;
}) as Promise<Response>;
};

export const getXmlSoapFetch =
(app: string) =>
Expand Down
156 changes: 156 additions & 0 deletions src/network/logout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* SPDX-FileCopyrightText: 2024 Zextras <https://www.zextras.com>
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { http, HttpResponse } from 'msw';

import { logout } from './logout';
import * as utils from './utils';
import server from '../mocks/server';
import { useLoginConfigStore } from '../store/login/store';
import { controlConsoleError } from '../tests/utils';
import type { ErrorSoapResponse } from '../types/network';

describe('Logout', () => {
it('should redirect to login page if EndSession request fails', async () => {
const goToLoginFn = jest.spyOn(utils, 'goToLogin').mockImplementation();
server.use(
http.post('/service/soap/EndSessionRequest', () => HttpResponse.json({}, { status: 500 }))
);
await logout();
await jest.advanceTimersToNextTimerAsync();
expect(goToLoginFn).toHaveBeenCalled();
});

it('should redirect to login page if /logout request fails', async () => {
const goToLoginFn = jest.spyOn(utils, 'goToLogin').mockImplementation();
server.use(http.get('/logout', () => HttpResponse.json({}, { status: 500 })));
await logout();
await jest.advanceTimersToNextTimerAsync();
expect(goToLoginFn).toHaveBeenCalled();
});

it('should redirect to login page if EndSession throws error', async () => {
controlConsoleError('Failed to fetch');
const goToLoginFn = jest.spyOn(utils, 'goToLogin').mockImplementation();
server.use(http.post('/service/soap/EndSessionRequest', () => HttpResponse.error()));
await logout();
await jest.advanceTimersToNextTimerAsync();
expect(goToLoginFn).toHaveBeenCalled();
});

it('should redirect to login page if /logout throws error', async () => {
controlConsoleError('Failed to fetch');
const goToLoginFn = jest.spyOn(utils, 'goToLogin').mockImplementation();
server.use(http.get('/logout', () => HttpResponse.error()));
await logout();
await jest.advanceTimersToNextTimerAsync();
expect(goToLoginFn).toHaveBeenCalled();
});

it('should redirect to login page if EndSession request succeeded with Fault', async () => {
const goToLoginFn = jest.spyOn(utils, 'goToLogin').mockImplementation();
server.use(
http.post('/service/soap/EndSessionRequest', () =>
HttpResponse.json<ErrorSoapResponse>(
{
Header: { context: {} },
Body: {
Fault: {
Code: { Value: '' },
Detail: {
Error: {
Code: '',
Trace: ''
}
},
Reason: {
Text: ''
}
}
}
},
{ status: 200 }
)
)
);
await logout();
await jest.advanceTimersToNextTimerAsync();
expect(goToLoginFn).toHaveBeenCalled();
});

describe('with custom logout url', () => {
it('should redirect to login page if EndSession request fails', async () => {
useLoginConfigStore.setState({ carbonioWebUiLogoutURL: 'custom logout' });
const goToFn = jest.spyOn(utils, 'goTo').mockImplementation();
server.use(
http.post('/service/soap/EndSessionRequest', () => HttpResponse.json({}, { status: 500 }))
);
await logout();
await jest.advanceTimersToNextTimerAsync();
expect(goToFn).toHaveBeenCalled();
});

it('should redirect to login page if /logout request fails', async () => {
useLoginConfigStore.setState({ carbonioWebUiLogoutURL: 'custom logout' });
const goToFn = jest.spyOn(utils, 'goTo').mockImplementation();
server.use(http.get('/logout', () => HttpResponse.json({}, { status: 500 })));
await logout();
await jest.advanceTimersToNextTimerAsync();
expect(goToFn).toHaveBeenCalled();
});

it('should redirect to login page if EndSession throws error', async () => {
useLoginConfigStore.setState({ carbonioWebUiLogoutURL: 'custom logout' });
controlConsoleError('Failed to fetch');
const goToFn = jest.spyOn(utils, 'goTo').mockImplementation();
server.use(http.post('/service/soap/EndSessionRequest', () => HttpResponse.error()));
await logout();
await jest.advanceTimersToNextTimerAsync();
expect(goToFn).toHaveBeenCalled();
});

it('should redirect to login page if /logout throws error', async () => {
useLoginConfigStore.setState({ carbonioWebUiLogoutURL: 'custom logout' });
controlConsoleError('Failed to fetch');
const goToFn = jest.spyOn(utils, 'goTo').mockImplementation();
server.use(http.get('/logout', () => HttpResponse.error()));
await logout();
await jest.advanceTimersToNextTimerAsync();
expect(goToFn).toHaveBeenCalled();
});

it('should redirect to login page if EndSession request succeeded with Fault', async () => {
rodleyorosa marked this conversation as resolved.
Show resolved Hide resolved
useLoginConfigStore.setState({ carbonioWebUiLogoutURL: 'custom logout' });
const goToFn = jest.spyOn(utils, 'goTo').mockImplementation();
server.use(
http.post('/service/soap/EndSessionRequest', () =>
HttpResponse.json<ErrorSoapResponse>(
{
Header: { context: {} },
Body: {
Fault: {
Code: { Value: '' },
Detail: {
Error: {
Code: '',
Trace: ''
}
},
Reason: {
Text: ''
}
}
}
},
{ status: 200 }
)
)
);
await logout();
await jest.advanceTimersToNextTimerAsync();
expect(goToFn).toHaveBeenCalled();
});
});
});
28 changes: 14 additions & 14 deletions src/network/logout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { getSoapFetch } from './fetch';
import { soapFetch } from './fetch-utils';
import { goTo, goToLogin } from './utils';
import { JSNS, SHELL_APP_ID } from '../constants';
import { JSNS } from '../constants';
import { useLoginConfigStore } from '../store/login/store';

export function logout(): Promise<void> {
return getSoapFetch(SHELL_APP_ID)('EndSession', {
_jsns: JSNS.account,
logoff: true
})
.then(() => fetch('/logout', { redirect: 'manual' }))
.then(() => {
const customLogoutUrl = useLoginConfigStore.getState().carbonioWebUiLogoutURL;
customLogoutUrl ? goTo(customLogoutUrl) : goToLogin();
})
.catch((error) => {
console.error(error);
export async function logout(): Promise<void> {
try {
await soapFetch('EndSession', {
_jsns: JSNS.account,
logoff: true
});
await fetch('/logout', { redirect: 'manual' });
} catch (error) {
console.error(error);
} finally {
const customLogoutUrl = useLoginConfigStore.getState().carbonioWebUiLogoutURL;
customLogoutUrl ? goTo(customLogoutUrl) : goToLogin();
}
}