Skip to content

Commit

Permalink
fix: redirect to login even if logout requests fail
Browse files Browse the repository at this point in the history
Refs: SHELL-248 (#513)
  • Loading branch information
beawar authored Oct 7, 2024
1 parent 2b22cb2 commit 1648f77
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 74 deletions.
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 @@ export const fetchNoOp = (): void => {
);
};

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 @@ export const getSoapFetch =
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
.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 () => {
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();
}
}

0 comments on commit 1648f77

Please sign in to comment.