import axios from 'axios';
import PropTypes from 'prop-types';
import { ensureDefinedConfig } from '../utils';
const userPropTypes = PropTypes.shape({
userId: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
roles: PropTypes.arrayOf(PropTypes.string),
administrator: PropTypes.boolean,
});
const optionsPropTypes = {
config: PropTypes.shape({
BASE_URL: PropTypes.string.isRequired,
LMS_BASE_URL: PropTypes.string.isRequired,
LOGIN_URL: PropTypes.string.isRequired,
LOGOUT_URL: PropTypes.string.isRequired,
REFRESH_ACCESS_TOKEN_ENDPOINT: PropTypes.string.isRequired,
ACCESS_TOKEN_COOKIE_NAME: PropTypes.string.isRequired,
CSRF_TOKEN_API_PATH: PropTypes.string.isRequired,
}).isRequired,
loggingService: PropTypes.shape({
logError: PropTypes.func.isRequired,
logInfo: PropTypes.func.isRequired,
}).isRequired,
// The absence of authenticatedUser means the user is anonymous.
authenticatedUser: userPropTypes,
// Must be at least a valid user, but may have other fields.
hydratedAuthenticatedUser: userPropTypes,
};
/**
* The MockAuthService class mocks authenticated user-fetching logic and allows for manually
* setting user data. It is compatible with axios-mock-adapter to wrap its HttpClients so that
* they can be mocked for testing.
*
* It wraps all methods of the service with Jest mock functions (jest.fn()). This allows test code
* to assert expectations on all functions of the service while preserving sensible behaviors. For
* instance, the login/logout methods related to redirecting maintain their real behavior.
*
* This service is NOT suitable for use in an application itself - only tests. It depends on Jest,
* which should only be a dev dependency of your project. You don't want to pull the entire suite
* of test dependencies into your application at runtime, probably even in your dev server.
*
* In a test where you would like to mock out API requests - perhaps from a redux-thunk function -
* you could do the following to set up a MockAuthService for your test:
*
* ```
* import { getConfig, mergeConfig } from '@edx/frontend-platform';
* import { configure, MockAuthService } from '@edx/frontend-platform/auth';
* import MockAdapter from 'axios-mock-adapter';
*
* const mockLoggingService = {
* logInfo: jest.fn(),
* logError: jest.fn(),
* };
* mergeConfig({
* authenticatedUser: {
* userId: 'abc123',
* username: 'Mock User',
* roles: [],
* administrator: false,
* },
* });
* configure(MockAuthService, { config: getConfig(), loggingService: mockLoggingService });
* const mockAdapter = new MockAdapter(getAuthenticatedHttpClient());
* // Mock calls for your tests. This configuration can be done in any sort of test setup.
* mockAdapter.onGet(...);
* ```
*
* Also see the `initializeMockApp` function which also automatically uses mock services for
* Logging and Analytics.
*
* @implements {AuthService}
* @memberof module:Auth
*/
class MockAuthService {
/**
* @param {Object} options
* @param {Object} options.config
* @param {string} options.config.BASE_URL
* @param {string} options.config.LMS_BASE_URL
* @param {string} options.config.LOGIN_URL
* @param {string} options.config.LOGOUT_URL
* @param {string} options.config.REFRESH_ACCESS_TOKEN_ENDPOINT
* @param {string} options.config.ACCESS_TOKEN_COOKIE_NAME
* @param {string} options.config.CSRF_TOKEN_API_PATH
* @param {Object} options.config.hydratedAuthenticatedUser
* @param {Object} options.config.authenticatedUser
* @param {Object} options.loggingService requires logError and logInfo methods
*/
constructor(options) {
this.authenticatedHttpClient = null;
this.httpClient = null;
ensureDefinedConfig(options, 'AuthService');
PropTypes.checkPropTypes(optionsPropTypes, options, 'options', 'AuthService');
this.config = options.config;
this.loggingService = options.loggingService;
// Mock user
this.authenticatedUser = this.config.authenticatedUser ? this.config.authenticatedUser : null;
this.hydratedAuthenticatedUser = this.config.hydratedAuthenticatedUser
? this.config.hydratedAuthenticatedUser
: {};
this.authenticatedHttpClient = axios.create();
this.httpClient = axios.create();
}
/**
* A Jest mock function (jest.fn())
*
* Applies middleware to the axios instances in this service.
*
* @param {Array} middleware Middleware to apply.
*/
applyMiddleware(middleware = []) {
const clients = [
this.authenticatedHttpClient, this.httpClient,
this.cachedAuthenticatedHttpClient, this.cachedHttpClient,
];
try {
(middleware).forEach((middlewareFn) => {
clients.forEach((client) => client && middlewareFn(client));
});
} catch (error) {
throw new Error(`Failed to apply middleware: ${error.message}.`);
}
}
/**
* A Jest mock function (jest.fn())
*
* Gets the authenticated HTTP client instance, which is an axios client wrapped in
* MockAdapter from axios-mock-adapter.
*
* @returns {HttpClient} An HttpClient wrapped in MockAdapter.
*/
getAuthenticatedHttpClient = jest.fn(() => this.authenticatedHttpClient);
/**
* A Jest mock function (jest.fn())
*
* Gets the unauthenticated HTTP client instance, which is an axios client wrapped in
* MockAdapter from axios-mock-adapter.
*
* @returns {HttpClient} An HttpClient wrapped in MockAdapter.
*/
getHttpClient = jest.fn(() => this.httpClient);
/**
* A Jest mock function (jest.fn())
*
* Builds a URL to the login page with a post-login redirect URL attached as a query parameter.
*
* ```
* const url = getLoginRedirectUrl('http://localhost/mypage');
* console.log(url); // http://localhost/login?next=http%3A%2F%2Flocalhost%2Fmypage
* ```
*
* @param {string} redirectUrl The URL the user should be redirected to after logging in.
*/
getLoginRedirectUrl = jest.fn(
(redirectUrl = this.config.BASE_URL) => `${this.config.LOGIN_URL}?next=${encodeURIComponent(redirectUrl)}`,
);
/**
* A Jest mock function (jest.fn())
*
* Redirects the user to the logout page in the real implementation. Is a no-op here.
*
* @param {string} redirectUrl The URL the user should be redirected to after logging in.
*/
redirectToLogin = jest.fn((redirectUrl = this.config.BASE_URL) => {
// Do nothing after getting the URL - this preserves the calls properly, but doesn't redirect.
this.getLoginRedirectUrl(redirectUrl);
});
/**
* A Jest mock function (jest.fn())
*
* Builds a URL to the logout page with a post-logout redirect URL attached as a query parameter.
*
* ```
* const url = getLogoutRedirectUrl('http://localhost/mypage');
* console.log(url); // http://localhost/logout?next=http%3A%2F%2Flocalhost%2Fmypage
* ```
*
* @param {string} redirectUrl The URL the user should be redirected to after logging out.
*/
getLogoutRedirectUrl = jest.fn((redirectUrl = this.config.BASE_URL) => `${this.config.LOGOUT_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`);
/**
* A Jest mock function (jest.fn())
*
* Redirects the user to the logout page in the real implementation. Is a no-op here.
*
* @param {string} redirectUrl The URL the user should be redirected to after logging out.
*/
redirectToLogout = jest.fn((redirectUrl = this.config.BASE_URL) => {
// Do nothing after getting the URL - this preserves the calls properly, but doesn't redirect.
this.getLogoutRedirectUrl(redirectUrl);
});
/**
* A Jest mock function (jest.fn())
*
* If it exists, returns the user data representing the currently authenticated user. If the
* user is anonymous, returns null.
*
* @returns {UserData|null}
*/
getAuthenticatedUser = jest.fn(() => this.authenticatedUser);
/**
* A Jest mock function (jest.fn())
*
* Sets the authenticated user to the provided value.
*
* @param {UserData} authUser
*/
setAuthenticatedUser = jest.fn((authUser) => {
this.authenticatedUser = authUser;
});
/**
* A Jest mock function (jest.fn())
*
* Returns the current authenticated user details, as supplied in the `authenticatedUser` field
* of the config options. Resolves to null if the user is unauthenticated / the config option
* has not been set.
*
* @returns {UserData|null} Resolves to the user's access token if they are
* logged in.
*/
fetchAuthenticatedUser = jest.fn(() => this.getAuthenticatedUser());
/**
* A Jest mock function (jest.fn())
*
* Ensures a user is authenticated. It will redirect to login when not authenticated.
*
* @param {string} [redirectUrl=config.BASE_URL] to return user after login when not
* authenticated.
* @returns {UserData|null} Resolves to the user's access token if they are
* logged in.
*/
ensureAuthenticatedUser = jest.fn((redirectUrl = this.config.BASE_URL) => {
this.fetchAuthenticatedUser();
if (this.getAuthenticatedUser() === null) {
// The user is not authenticated, send them to the login page.
this.redirectToLogin(redirectUrl);
}
return this.getAuthenticatedUser();
});
/**
* A Jest mock function (jest.fn())
*
* Adds the user data supplied in the `hydratedAuthenticatedUser` config option into the object
* returned by `getAuthenticatedUser`. This emulates the behavior of a real auth service which
* would make a request to fetch this data prior to merging it in.
*
* ```
* console.log(authenticatedUser); // Will be sparse and only contain basic information.
* await hydrateAuthenticatedUser()
* const authenticatedUser = getAuthenticatedUser();
* console.log(authenticatedUser); // Will contain additional user information
* ```
*
* @returns {Promise<null>}
*/
hydrateAuthenticatedUser = jest.fn(() => {
const user = this.getAuthenticatedUser();
if (user !== null) {
this.setAuthenticatedUser({ ...user, ...this.hydratedAuthenticatedUser });
}
});
}
export default MockAuthService;