Skip to content
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
4 changes: 4 additions & 0 deletions frontend-new/src/_test_utilities/envServiceMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ jest.mock("src/envService", () => ({
getAppIconUrl: jest.fn(() => "mock-app-icon-url"),
getThemeCssVariables: jest.fn(() => "{}"),
getRegistrationCodeDisabled: jest.fn(() => "false"),
getTargetEnvironmentName: jest.fn(),
getSentryDSN: jest.fn(),
getSentryEnabled: jest.fn(),
getSentryConfig: jest.fn(),
}));
4 changes: 4 additions & 0 deletions frontend-new/src/_test_utilities/i18nMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,12 @@ jest.mock("react-i18next", () => {

// Mock the initialized instance module so app code can import it without side effects
jest.mock("src/i18n/i18n", () => {
const { Locale } = require("src/i18n/constants");

const mock = {
language: Locale.EN_GB,
t: stableT,
changeLanguage: jest.fn(),
use: jest.fn().mockReturnThis(),
init: jest.fn().mockReturnThis(),
};
Expand Down
4 changes: 2 additions & 2 deletions frontend-new/src/app/ProtectedRoute/ProtectedRoute.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { routerPaths } from "src/app/routerPaths";
import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";

import {
Language,
SensitivePersonalDataRequirement,
UserPreference,
} from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { Locale } from "src/i18n/constants";

// mock the FirebaseSocialAuthentication service
jest.mock("src/auth/services/FirebaseAuthenticationService/socialAuth/FirebaseSocialAuthentication.service", () => {
Expand Down Expand Up @@ -74,7 +74,7 @@ const getUserPreferences = (
) => {
const givenPreferences: UserPreference = {
user_id: "user1",
language: Language.en,
language: Locale.EN_GB,
accepted_tc: acceptedTC,
has_sensitive_personal_data: hasSensitiveData,
sensitive_personal_data_requirement: sensitiveDataRequirement,
Expand Down
18 changes: 8 additions & 10 deletions frontend-new/src/app/ProtectedRoute/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { isAcceptedTCValid, isSensitiveDataValid } from "src/app/ProtectedRoute/util";
import {
Language,
SensitivePersonalDataRequirement,
} from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { SensitivePersonalDataRequirement } from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { Locale } from "src/i18n/constants";

describe("protected route util", () => {
describe("isSensitiveDataValid", () => {
test("should return false if sensitive data is required but user does not have it", () => {
// GIVEN a user required to provide sensitive personal data but does not have it
const userPreferences = {
user_id: "given user id",
language: Language.en,
language: Locale.EN_GB,
accepted_tc: new Date(),
has_sensitive_personal_data: false,
sensitive_personal_data_requirement: SensitivePersonalDataRequirement.REQUIRED,
Expand All @@ -30,7 +28,7 @@ describe("protected route util", () => {
// GIVEN a user required to provide sensitive personal data and has it
const userPreferences = {
user_id: "given user id",
language: Language.en,
language: Locale.EN_GB,
accepted_tc: new Date(),
has_sensitive_personal_data: true,
sensitive_personal_data_requirement: SensitivePersonalDataRequirement.REQUIRED,
Expand All @@ -50,7 +48,7 @@ describe("protected route util", () => {
// GIVEN a user not required to provide sensitive personal data
const userPreferences = {
user_id: "given user id",
language: Language.en,
language: Locale.EN_GB,
accepted_tc: new Date(),
has_sensitive_personal_data: false,
sensitive_personal_data_requirement: SensitivePersonalDataRequirement.NOT_AVAILABLE,
Expand All @@ -72,7 +70,7 @@ describe("protected route util", () => {
// GIVEN a user without accepted_tc
const userPreferences = {
user_id: "given user id",
language: Language.en,
language: Locale.EN_GB,
accepted_tc: undefined,
has_sensitive_personal_data: false,
sensitive_personal_data_requirement: SensitivePersonalDataRequirement.NOT_AVAILABLE,
Expand All @@ -92,7 +90,7 @@ describe("protected route util", () => {
// GIVEN a user with an invalid accepted_tc
const userPreferences = {
user_id: "given user id",
language: Language.en,
language: Locale.EN_GB,
accepted_tc: "invalid date",
has_sensitive_personal_data: false,
sensitive_personal_data_requirement: SensitivePersonalDataRequirement.NOT_AVAILABLE,
Expand All @@ -112,7 +110,7 @@ describe("protected route util", () => {
// GIVEN a user with a valid accepted_tc
const userPreferences = {
user_id: "given user id",
language: Language.en,
language: Locale.EN_GB,
accepted_tc: new Date(),
has_sensitive_personal_data: false,
sensitive_personal_data_requirement: SensitivePersonalDataRequirement.NOT_AVAILABLE,
Expand Down
46 changes: 43 additions & 3 deletions frontend-new/src/app/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,16 @@ import * as AuthenticationFactoryModule from "src/auth/services/Authentication.s
import UserPreferencesService from "src/userPreferences/UserPreferencesService/userPreferences.service";
import { PersistentStorageService } from "./PersistentStorageService/PersistentStorageService";
import {
Language,
SensitivePersonalDataRequirement,
UserPreference,
} from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { Locale } from "src/i18n/constants";
import { DATA_TEST_ID as BACKDROP_DATA_TEST_ID } from "src/theme/Backdrop/Backdrop";
import { resetAllMethodMocks } from "src/_test_utilities/resetAllMethodMocks";
import AuthenticationStateService from "src/auth/services/AuthenticationState.service";
import { routerPaths } from "src/app/routerPaths";
import { AuthBroadcastChannel, AuthChannelMessage } from "src/auth/services/authBroadcastChannel/authBroadcastChannel";
import LocaleSyncService from "src/i18n/LocaleSyncService";

// mock the snackbar
jest.mock("src/theme/SnackbarProvider/SnackbarProvider", () => {
Expand Down Expand Up @@ -93,6 +94,7 @@ describe("index", () => {
// As a good practice, we should the mock*Once() methods to avoid side effects between tests
// As a precaution, we reset all method mocks to ensure that no side effects are carried over between tests
resetAllMethodMocks(UserPreferencesService.getInstance());
resetAllMethodMocks(LocaleSyncService.getInstance());

unmockBrowserIsOnLine();
// Mock the authenticationServiceFactory to return a mock instance of the authentication service
Expand Down Expand Up @@ -221,7 +223,7 @@ describe("index", () => {

const mockPreferences = {
user_id: "foo",
language: Language.en,
language: Locale.EN_GB,
sessions: [123],
accepted_tc: new Date(),
has_sensitive_personal_data: false,
Expand Down Expand Up @@ -257,7 +259,7 @@ describe("index", () => {

const mockPreferences = {
user_id: "foo",
language: Language.en,
language: Locale.EN_GB,
sessions: [123],
user_feedback_answered_questions: {},
accepted_tc: new Date(),
Expand Down Expand Up @@ -291,6 +293,44 @@ describe("index", () => {
expect(console.error).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalled();
});

test("should apply backend locale on refresh", async () => {
// GIVEN a user with preferences containing a specific locale
jest.spyOn(AuthenticationStateService.getInstance(), "getToken").mockReturnValue("valid-token");

const applyBackendLocaleFn = jest
.spyOn(LocaleSyncService.getInstance(), "applyBackendLocale")
.mockResolvedValue();

const mockPreferences = {
user_id: "foo",
language: Locale.EN_US,
sessions: [123],
accepted_tc: new Date(),
has_sensitive_personal_data: false,
getActiveSessionId: jest.fn().mockReturnValue(123),
user_feedback_answered_questions: {},
sensitive_personal_data_requirement: SensitivePersonalDataRequirement.NOT_REQUIRED,
experiments: {},
};

jest.spyOn(UserPreferencesService.getInstance(), "getUserPreferences").mockResolvedValue(mockPreferences);

// WHEN the app is rendered
render(<App />);

// THEN wait for the app to finish loading
await waitFor(() => {
expect(screen.queryByTestId(BACKDROP_DATA_TEST_ID.BACKDROP_CONTAINER)).not.toBeInTheDocument();
});

// AND expect no errors or warnings
expect(console.error).not.toHaveBeenCalled();
expect(console.warn).not.toHaveBeenCalled();

// AND expect the backend locale to be applied to the frontend
expect(applyBackendLocaleFn).toHaveBeenCalledWith(mockPreferences.language);
});
});

describe("register listeners", () => {
Expand Down
5 changes: 5 additions & 0 deletions frontend-new/src/app/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import UserPreferencesService from "src/userPreferences/UserPreferencesService/u
import { AuthenticationError } from "src/error/commonErrors";
import { RestAPIError } from "src/error/restAPIError/RestAPIError";
import { StatusCodes } from "http-status-codes";
import LocaleSyncService from "src/i18n/LocaleSyncService";
import { lazyWithPreload } from "src/utils/preloadableComponent/PreloadableComponent";
import { TokenValidationFailureCause } from "src/auth/services/Authentication.service";
import { AuthBroadcastChannel, AuthChannelMessage } from "src/auth/services/authBroadcastChannel/authBroadcastChannel";
Expand Down Expand Up @@ -148,6 +149,10 @@ const App = () => {
}

UserPreferencesStateService.getInstance().setUserPreferences(preferences);

// On page refresh, apply backend locale to frontend i18n
// This ensures the UI language matches the user's stored preference
await LocaleSyncService.getInstance().applyBackendLocale(preferences.language);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I dont see any new assertions in the index.test.tsx for this

} catch (error) {
console.error(new AuthenticationError("Error initializing authentication and user preferences state", error));
await AuthenticationServiceFactory.resetAuthenticationState();
Expand Down
14 changes: 6 additions & 8 deletions frontend-new/src/auth/components/SocialAuth/SocialAuth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import { mockBrowserIsOnLine, unmockBrowserIsOnLine } from "src/_test_utilities/
import FirebaseSocialAuthenticationService from "src/auth/services/FirebaseAuthenticationService/socialAuth/FirebaseSocialAuthentication.service";
import authStateService from "src/auth/services/AuthenticationState.service";
import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
import {
SensitivePersonalDataRequirement,
Language,
} from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { SensitivePersonalDataRequirement } from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { Locale } from "src/i18n/constants";
import { TabiyaUser } from "src/auth/auth.types";
import { useSnackbar } from "src/theme/SnackbarProvider/SnackbarProvider";
import * as EnvServiceModule from "src/envService";
Expand Down Expand Up @@ -111,7 +109,7 @@ describe("SocialAuth tests", () => {
// AND the user preferences exist for the user
jest.spyOn(UserPreferencesStateService.getInstance(), "getUserPreferences").mockReturnValue({
user_id: givenUser.id,
language: Language.en,
language: Locale.EN_GB,
sessions: [1],
user_feedback_answered_questions: {},
accepted_tc: tc,
Expand Down Expand Up @@ -617,7 +615,7 @@ describe("SocialAuth tests", () => {
const createUserPreferencesSpy = jest.spyOn(UserPreferencesService.getInstance(), "createUserPreferences");
const mockPreferences = {
user_id: givenUser.id,
language: Language.en,
language: Locale.EN_GB,
invitation_code: "valid-code",
sessions: [],
user_feedback_answered_questions: {},
Expand Down Expand Up @@ -660,7 +658,7 @@ describe("SocialAuth tests", () => {
expect(createUserPreferencesSpy).toHaveBeenCalledWith({
user_id: givenUser.id,
invitation_code: "valid-code",
language: Language.en,
language: Locale.EN_GB,
});
});

Expand Down Expand Up @@ -697,7 +695,7 @@ describe("SocialAuth tests", () => {
require("src/userPreferences/UserPreferencesService/userPreferences.service").default;
const mockPreferences = {
user_id: givenUser.id,
language: Language.en,
language: Locale.EN_GB,
invitation_code: "modal-code",
sessions: [],
user_feedback_answered_questions: {},
Expand Down
11 changes: 8 additions & 3 deletions frontend-new/src/auth/components/SocialAuth/SocialAuth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import RegistrationCodeFormModal, {
import UserPreferencesService from "src/userPreferences/UserPreferencesService/userPreferences.service";
import { invitationsService } from "src/auth/services/invitationsService/invitations.service";
import { InvitationStatus, InvitationType } from "src/auth/services/invitationsService/invitations.types";
import { Language, UserPreference } from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { UserPreference } from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { getRegistrationCodeDisabled, getRegistrationDisabled } from "src/envService";
import { validateLocale } from "src/i18n/constants";
import i18n from "src/i18n/i18n";

const uniqueId = "f0324e97-83fd-49e6-95c3-1043751fa1db";
export const DATA_TEST_ID = {
Expand Down Expand Up @@ -96,10 +98,13 @@ const SocialAuth: React.FC<Readonly<SocialAuthProps>> = ({
throw new Error("Something went wrong: No user found");
}

// Use the current i18n locale instead of a hardcoded default
const currentLocale = validateLocale(i18n.language);

if (!registrationCode) {
prefs = await UserPreferencesService.getInstance().createUserPreferences({
user_id: _user.id,
language: Language.en,
language: currentLocale,
});
} else {
const invitation = await invitationsService.checkInvitationCodeStatus(registrationCode);
Expand All @@ -115,7 +120,7 @@ const SocialAuth: React.FC<Readonly<SocialAuthProps>> = ({
prefs = await UserPreferencesService.getInstance().createUserPreferences({
user_id: _user.id,
invitation_code: invitation.invitation_code,
language: Language.en,
language: currentLocale,
});
}

Expand Down
4 changes: 2 additions & 2 deletions frontend-new/src/auth/pages/Register/Register.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import { InvitationStatus, InvitationType } from "src/auth/services/invitationsS
import UserPreferencesService from "src/userPreferences/UserPreferencesService/userPreferences.service";
import {
SensitivePersonalDataRequirement,
Language,
UserPreference,
} from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { Locale } from "src/i18n/constants";
import authStateService from "src/auth/services/AuthenticationState.service";
import { INVITATIONS_PARAM_NAME, TabiyaUser } from "src/auth/auth.types";
import UserPreferencesStateService from "src/userPreferences/UserPreferencesStateService";
Expand Down Expand Up @@ -301,7 +301,7 @@ describe("Testing Register component", () => {
// AND the user preferences service is mocked to succeed
jest.spyOn(UserPreferencesService.getInstance(), "createUserPreferences").mockResolvedValueOnce({
user_id: "foo-bar-id",
language: Language.en,
language: Locale.EN_GB,
sessions: [],
user_feedback_answered_questions: {},
accepted_tc: new Date(),
Expand Down
22 changes: 18 additions & 4 deletions frontend-new/src/auth/services/Authentication.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import AuthenticationService, { CLOCK_TOLERANCE, TokenValidationFailureCause } f
import { jwtDecode } from "jwt-decode";
import { PersistentStorageService } from "src/app/PersistentStorageService/PersistentStorageService";
import UserPreferencesService from "src/userPreferences/UserPreferencesService/userPreferences.service";
import { Language, UserPreference } from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { UserPreference } from "src/userPreferences/UserPreferencesService/userPreferences.types";
import { TabiyaUser, Token, TokenHeader } from "src/auth/auth.types";
import AuthenticationStateService from "./AuthenticationState.service";

Expand All @@ -12,6 +12,17 @@ import { RestAPIError } from "src/error/restAPIError/RestAPIError";
import { StatusCodes } from "http-status-codes";
import { resetAllMethodMocks } from "src/_test_utilities/resetAllMethodMocks";
import { nanoid } from "nanoid";
import { Locale } from "src/i18n/constants";

// Mock LocaleSyncService to prevent actual backend calls
jest.mock("src/i18n/LocaleSyncService", () => ({
__esModule: true,
default: {
getInstance: jest.fn().mockReturnValue({
syncOnLogin: jest.fn().mockResolvedValue(undefined),
}),
},
}));

// Mock jwt-decode
jest.mock("jwt-decode", () => ({
Expand Down Expand Up @@ -123,6 +134,7 @@ describe("AuthenticationService", () => {
const givenUserPreferences: UserPreference = {
user_id: "foo-id",
sessions: [],
language: Locale.EN_GB,
} as unknown as UserPreference;
jest
.spyOn(UserPreferencesService.getInstance(), "getUserPreferences")
Expand Down Expand Up @@ -236,6 +248,7 @@ describe("AuthenticationService", () => {
const givenReturnedPrefs: UserPreference = {
user_id: "foo-id",
sessions: [],
language: Locale.EN_GB,
} as unknown as UserPreference;
jest
.spyOn(UserPreferencesService.getInstance(), "createUserPreferences")
Expand All @@ -254,7 +267,7 @@ describe("AuthenticationService", () => {
expect(UserPreferencesService.getInstance().createUserPreferences).toHaveBeenCalledWith({
user_id: givenUser.id,
invitation_code: givenRegistrationCode,
language: Language.en,
language: Locale.EN_GB,
});

// AND the preferences return from the service should be set in the state
Expand Down Expand Up @@ -294,6 +307,7 @@ describe("AuthenticationService", () => {
const givenReturnedPrefs: UserPreference = {
user_id: "foo-id",
sessions: [],
language: Locale.EN_GB,
} as unknown as UserPreference;
jest
.spyOn(UserPreferencesService.getInstance(), "createUserPreferences")
Expand All @@ -312,7 +326,7 @@ describe("AuthenticationService", () => {
expect(UserPreferencesService.getInstance().createUserPreferences).toHaveBeenCalledWith({
user_id: givenUser.id,
invitation_code: undefined,
language: Language.en,
language: Locale.EN_GB,
});

// AND the preferences returned from the service should be set in the state
Expand Down Expand Up @@ -359,7 +373,7 @@ describe("AuthenticationService", () => {
expect(UserPreferencesService.getInstance().createUserPreferences).toHaveBeenCalledWith({
user_id: givenUser.id,
invitation_code: givenRegistrationCode,
language: Language.en,
language: Locale.EN_GB,
});

// AND the preferences returned from the service should be set in the state
Expand Down
Loading