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

Moved login/logout logic into the useUser composable and updated references #12915

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 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
82 changes: 1 addition & 81 deletions kolibri/core/assets/src/state/modules/core/actions.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,12 @@
import debounce from 'lodash/debounce';
import pick from 'lodash/pick';
import client from 'kolibri/client';
import heartbeat from 'kolibri/heartbeat';
import logger from 'kolibri-logging';
import FacilityResource from 'kolibri-common/apiResources/FacilityResource';
import FacilityDatasetResource from 'kolibri-common/apiResources/FacilityDatasetResource';
import UserSyncStatusResource from 'kolibri-common/apiResources/UserSyncStatusResource';
import { setServerTime } from 'kolibri/utils/serverClock';
import urls from 'kolibri/urls';
import redirectBrowser from 'kolibri/utils/redirectBrowser';
import CatchErrors from 'kolibri/utils/CatchErrors';
import { nextTick } from 'vue';
import Lockr from 'lockr';
import {
DisconnectionErrorCodes,
LoginErrors,
ERROR_CONSTANTS,
UPDATE_MODAL_DISMISSED,
} from 'kolibri/constants';
import { browser, os } from 'kolibri/utils/browserInfo';
import { baseSessionState } from '../session';
import { DisconnectionErrorCodes } from 'kolibri/constants';

const logging = logger.getLogger(__filename);

Expand Down Expand Up @@ -67,15 +54,6 @@ export function handleApiError(store, { error, reloadOnReconnect = false } = {})
throw error;
}

export function setSession(store, { session, clientNow }) {
const serverTime = session.server_time;
if (clientNow) {
setServerTime(serverTime, clientNow);
}
session = pick(session, Object.keys(baseSessionState));
store.commit('CORE_SET_SESSION', session);
}

/**
* Sets a password that is currently not specified
* due to an account that was created while passwords
Expand All @@ -97,64 +75,6 @@ export function kolibriSetUnspecifiedPassword(store, { username, password, facil
});
}

/**
* Signs in user.
*
* @param {object} store The store.
* @param {object} sessionPayload The session payload.
*/
export function kolibriLogin(store, sessionPayload) {
Lockr.set(UPDATE_MODAL_DISMISSED, false);
return client({
data: {
...sessionPayload,
active: true,
browser,
os,
},
url: urls['kolibri:core:session_list'](),
method: 'post',
})
.then(() => {
// check redirect is disabled:
if (!sessionPayload.disableRedirect)
if (sessionPayload.next) {
// OIDC redirect
redirectBrowser(sessionPayload.next);
}
// Normal redirect on login
else {
redirectBrowser();
}
})
.catch(error => {
const errorsCaught = CatchErrors(error, [
ERROR_CONSTANTS.INVALID_CREDENTIALS,
ERROR_CONSTANTS.MISSING_PASSWORD,
ERROR_CONSTANTS.PASSWORD_NOT_SPECIFIED,
ERROR_CONSTANTS.NOT_FOUND,
]);
if (errorsCaught) {
if (errorsCaught.includes(ERROR_CONSTANTS.INVALID_CREDENTIALS)) {
return LoginErrors.INVALID_CREDENTIALS;
} else if (errorsCaught.includes(ERROR_CONSTANTS.MISSING_PASSWORD)) {
return LoginErrors.PASSWORD_MISSING;
} else if (errorsCaught.includes(ERROR_CONSTANTS.PASSWORD_NOT_SPECIFIED)) {
return LoginErrors.PASSWORD_NOT_SPECIFIED;
} else if (errorsCaught.includes(ERROR_CONSTANTS.NOT_FOUND)) {
return LoginErrors.USER_NOT_FOUND;
}
} else {
store.dispatch('handleApiError', { error });
}
});
}

export function kolibriLogout() {
// Use the logout backend URL to initiate logout
redirectBrowser(urls['kolibri:core:logout']());
}

const _setPageVisibility = debounce((store, visibility) => {
store.commit('CORE_SET_PAGE_VISIBILITY', visibility);
}, 500);
Expand Down
26 changes: 16 additions & 10 deletions packages/kolibri/__tests__/heartbeat.spec.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import mock from 'xhr-mock';
import coreStore from 'kolibri/store';
import redirectBrowser from 'kolibri/utils/redirectBrowser';
import * as serverClock from 'kolibri/utils/serverClock';
import { get, set } from '@vueuse/core';
import useSnackbar, { useSnackbarMock } from 'kolibri/composables/useSnackbar'; // eslint-disable-line
import { ref } from 'vue';
import { DisconnectionErrorCodes } from 'kolibri/constants';
import useUser, { useUserMock } from 'kolibri/composables/useUser'; // eslint-disable-line
import { HeartBeat } from '../heartbeat.js';
import { trs } from '../internal/disconnection';
import coreModule from '../../../kolibri/core/assets/src/state/modules/core';
import { stubWindowLocation } from 'testUtils'; // eslint-disable-line

jest.mock('kolibri/utils/redirectBrowser');
jest.mock('kolibri/urls');
jest.mock('lockr');
jest.mock('kolibri/composables/useSnackbar');

coreStore.registerModule('core', coreModule);
jest.mock('kolibri/composables/useUser');

describe('HeartBeat', function () {
stubWindowLocation(beforeAll, afterAll);
// replace the real XHR object with the mock XHR object before each test
beforeEach(() => mock.setup());

beforeEach(() => {
// replace the real XHR object with the mock XHR object before each test
mock.setup();
useUser.mockImplementation(() => useUserMock());
});

// put the real XHR object back and clear the mocks after each test
afterEach(() => mock.teardown());
Expand Down Expand Up @@ -206,7 +208,8 @@ describe('HeartBeat', function () {
jest.spyOn(heartBeat, '_sessionUrl').mockReturnValue('url');
});
it('should sign out if an auto logout is detected', function () {
coreStore.commit('CORE_SET_SESSION', { user_id: 'test', id: 'current' });
const { setSession } = useUser();
setSession({ session: { user_id: 'test', id: 'current' } });
mock.put(/.*/, {
status: 200,
body: JSON.stringify({ user_id: null, id: 'current' }),
Expand All @@ -218,7 +221,8 @@ describe('HeartBeat', function () {
});
});
it('should redirect if a change in user is detected', function () {
coreStore.commit('CORE_SET_SESSION', { user_id: 'test', id: 'current' });
const { setSession } = useUser();
setSession({ session: { user_id: 'test', id: 'current' } });
redirectBrowser.mockReset();
mock.put(/.*/, {
status: 200,
Expand All @@ -230,7 +234,8 @@ describe('HeartBeat', function () {
});
});
it('should not sign out if user_id changes but session is being set for first time', function () {
coreStore.commit('CORE_SET_SESSION', { user_id: undefined, id: undefined });
const { setSession } = useUser();
setSession({ session: { user_id: undefined, id: undefined } });
mock.put(/.*/, {
status: 200,
body: JSON.stringify({ user_id: null, id: 'current' }),
Expand All @@ -242,7 +247,8 @@ describe('HeartBeat', function () {
});
});
it('should call setServerTime with a clientNow value that is between the start and finish of the poll', function () {
coreStore.commit('CORE_SET_SESSION', { user_id: 'test', id: 'current' });
const { setSession } = useUser();
setSession({ session: { user_id: 'test', id: 'current' } });
const serverTime = new Date().toJSON();
mock.put(/.*/, {
status: 200,
Expand Down
40 changes: 34 additions & 6 deletions packages/kolibri/composables/__mocks__/useUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,12 @@
* useUser.mockImplementation(() => useUserMock())
* ```
*/
import { computed } from 'vue';
import { ref, computed } from 'vue';
import { UserKinds } from 'kolibri/constants';
import { jest } from '@jest/globals';
import { setServerTime } from 'kolibri/utils/serverClock';

const session = {
const MOCK_DEFAULT_SESSION = {
app_context: false,
can_manage_content: false,
facility_id: undefined,
Expand Down Expand Up @@ -63,9 +65,9 @@ const MOCK_DEFAULTS = {
userFacilityId: undefined,
getUserKind: UserKinds.ANONYMOUS,
userHasPermissions: false,
session,
//state
...session,
session: { ...MOCK_DEFAULT_SESSION },
// Mock state
...MOCK_DEFAULT_SESSION,
};

export function useUserMock(overrides = {}) {
Expand All @@ -77,7 +79,33 @@ export function useUserMock(overrides = {}) {
for (const key in mocks) {
computedMocks[key] = computed(() => mocks[key]);
}
return computedMocks;

// Module-level state reference for actions
const session = ref({ ...mocks.session });

// Mock implementation of `useUser` methods
return {
...computedMocks,
session, // Make session mutable for test scenarios

// Actions
setSession: jest.fn(({ session: newSession, clientNow }) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the failing tests re: not calling serverTime w/ clientNow may be fixed by calling it here.

const serverTime = newSession.server_time;
if (clientNow) {
setServerTime(serverTime, clientNow);
}
session.value = {
...MOCK_DEFAULT_SESSION,
...newSession,
};
}),

kolibriLogin: jest.fn(async () => Promise.resolve()),

kolibriLogout: jest.fn(() => {}),

kolibrisetUnspecifiedPassword: jest.fn(async () => Promise.resolve()),
};
}

export default jest.fn(() => useUserMock());
Loading
Loading