richie-education 3.5.2-dev6 → 3.5.2-dev8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -65,6 +65,7 @@ function getUserInfo(username: DevDemoUser): User {
65
65
  access_token: accessToken,
66
66
  username: JWTPayload.username,
67
67
  full_name: JWTPayload.full_name,
68
+ email: JWTPayload.email,
68
69
  };
69
70
  }
70
71
 
@@ -31,19 +31,16 @@ describe('Fonzie Keycloak API', () => {
31
31
  jest.clearAllMocks();
32
32
  });
33
33
 
34
- it('uses its own route to get user information and enriches with keycloak account', async () => {
34
+ it('reads the email and full name from the access token claims', async () => {
35
+ // JWT payload: { email: 'john.doe@example.com', full_name: 'John Doe' }
36
+ const accessToken =
37
+ 'header.eyJlbWFpbCI6ImpvaG4uZG9lQGV4YW1wbGUuY29tIiwiZnVsbF9uYW1lIjoiSm9obiBEb2UifQ.signature';
35
38
  const user = {
36
39
  username: 'test-richie-ncl',
37
- full_name: 'n c',
38
- };
39
- const keycloakAccount = {
40
- firstName: 'John',
41
- lastName: 'Doe',
42
- email: 'john.doe@example.com',
40
+ access_token: accessToken,
43
41
  };
44
42
 
45
43
  fetchMock.get('https://demo.endpoint.api/api/v1.0/user/me', user);
46
- fetchMock.get('https://keycloak.test/auth/realms/richie-realm/account/', keycloakAccount);
47
44
 
48
45
  const api = FonzieKeycloakAPIInterface(configuration);
49
46
  await expect(api.user.me()).resolves.toEqual({
@@ -53,14 +50,14 @@ describe('Fonzie Keycloak API', () => {
53
50
  });
54
51
  });
55
52
 
56
- it('falls back to fonzie data when keycloak account call fails', async () => {
53
+ it('does not fail when the access token has no usable claims', async () => {
57
54
  const user = {
58
55
  username: 'test-richie-ncl',
59
56
  full_name: 'n c',
57
+ access_token: 'not-a-jwt',
60
58
  };
61
59
 
62
60
  fetchMock.get('https://demo.endpoint.api/api/v1.0/user/me', user);
63
- fetchMock.get('https://keycloak.test/auth/realms/richie-realm/account/', 500);
64
61
 
65
62
  const api = FonzieKeycloakAPIInterface(configuration);
66
63
  await expect(api.user.me()).resolves.toEqual(user);
@@ -8,8 +8,25 @@ import { OpenEdxApiProfile } from 'types/openEdx';
8
8
  import { checkStatus } from 'api/utils';
9
9
  import { OpenEdxFullNameFormValues } from 'components/OpenEdxFullNameForm';
10
10
  import { location } from 'utils/indirection/window';
11
+ import { base64Decode } from 'utils/base64Parser';
12
+ import { Maybe } from 'types/utils';
11
13
  import OpenEdxHawthornApiInterface from './openedx-hawthorn';
12
14
 
15
+ /**
16
+ * Extract claims from the JWT issued by Fonzie. The `user/me` route does not expose
17
+ * the email nor an up to date full name, but the token it returns always carries them.
18
+ */
19
+ const getTokenClaims = (token: Maybe<string>): Maybe<{ email?: string; full_name?: string }> => {
20
+ if (!token) return undefined;
21
+
22
+ try {
23
+ const payload = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
24
+ return JSON.parse(base64Decode(payload));
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ };
29
+
13
30
  /**
14
31
  *
15
32
  * OpenEdX completed by Fonzie API Implementation
@@ -46,27 +63,13 @@ const API = (APIConf: AuthenticationBackend): APILms => {
46
63
  me: async () => {
47
64
  const user = await ApiInterface.user.me();
48
65
  if (!user) return null;
49
- try {
50
- const keycloakAccount = await fetch(APIOptions.routes.user.account, {
51
- credentials: 'include',
52
- headers: {
53
- Accept: 'application/json',
54
- Authorization: `Bearer ${user.access_token}`,
55
- },
56
- }).then((res) => {
57
- if (!res.ok) throw new Error(`Keycloak account fetch failed: ${res.status}`);
58
- return res.json();
59
- });
60
- return {
61
- ...user,
62
- full_name:
63
- [keycloakAccount.firstName, keycloakAccount.lastName].filter(Boolean).join(' ') ||
64
- user.full_name,
65
- email: keycloakAccount.email || user.email,
66
- };
67
- } catch {
68
- return user;
69
- }
66
+
67
+ const claims = getTokenClaims(user.access_token);
68
+ return {
69
+ ...user,
70
+ full_name: user.full_name || claims?.full_name,
71
+ email: user.email ?? claims?.email,
72
+ };
70
73
  },
71
74
  login: () => {
72
75
  const next = encodeURIComponent(location.href);
@@ -706,6 +706,36 @@ describe.each([
706
706
  screen.getByTestId('walkthrough-banner');
707
707
  });
708
708
 
709
+ it('should render the account name and email from the OpenEdx profile', async () => {
710
+ const product = ProductFactory().one();
711
+ const paymentPlan = PaymentPlanFactory().one();
712
+ fetchMock
713
+ .get(
714
+ `https://joanie.endpoint/api/v1.0/orders/?${queryString.stringify(getFetchOrderQueryParams(product))}`,
715
+ [],
716
+ )
717
+ .get(
718
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/payment-plan/`,
719
+ paymentPlan,
720
+ )
721
+ .get(
722
+ `https://joanie.endpoint/api/v1.0/courses/${course.code}/products/${product.id}/deep-link/`,
723
+ {},
724
+ );
725
+
726
+ render(<Wrapper product={product} isWithdrawable={true} paymentPlan={paymentPlan} />, {
727
+ queryOptions: { client: createTestQueryClient({ user: richieUser }) },
728
+ });
729
+
730
+ screen.getByRole('heading', { level: 4, name: 'Account email' });
731
+
732
+ await screen.findByText(openApiEdxProfile.email);
733
+
734
+ screen.getByText(
735
+ 'This email will be used to send you confirmation mails, it is the one you created your account with.',
736
+ );
737
+ });
738
+
709
739
  it('should show a checkbox to waive withdrawal right if the product is not withdrawable', async () => {
710
740
  const product = ProductFactory().one();
711
741
  const paymentPlan = PaymentPlanFactory().one();
@@ -32,7 +32,10 @@ jest.mock('@openfun/cunningham-react', () => ({
32
32
  describe('<OrderWithdrawalModal/>', () => {
33
33
  setupJoanieSession();
34
34
  const user = UserFactory().one();
35
- const renderModal = (props: Partial<React.ComponentProps<typeof OrderWithdrawalModal>>) =>
35
+ const renderModal = (
36
+ props: Partial<React.ComponentProps<typeof OrderWithdrawalModal>>,
37
+ sessionUser = user,
38
+ ) =>
36
39
  render(
37
40
  <OrderWithdrawalModal
38
41
  isOpen
@@ -42,7 +45,7 @@ describe('<OrderWithdrawalModal/>', () => {
42
45
  reference="COURSE-CODE"
43
46
  {...props}
44
47
  />,
45
- { queryOptions: { client: createTestQueryClient({ user }) } },
48
+ { queryOptions: { client: createTestQueryClient({ user: sessionUser }) } },
46
49
  );
47
50
 
48
51
  it('should display order and user information, without the account update link for a non-keycloak backend', () => {
@@ -53,8 +56,11 @@ describe('<OrderWithdrawalModal/>', () => {
53
56
  expect(
54
57
  screen.getByText('You wish to cancel your subscription to', { exact: false }),
55
58
  ).toBeInTheDocument();
59
+ expect(screen.getByText('Account name')).toBeInTheDocument();
56
60
  expect(screen.getByText(user.full_name!)).toBeInTheDocument();
61
+ expect(screen.getByText('Account email')).toBeInTheDocument();
57
62
  expect(screen.getByText(user.email!)).toBeInTheDocument();
63
+ expect(screen.getByText('Order reference')).toBeInTheDocument();
58
64
  expect(screen.getByText(order.id)).toBeInTheDocument();
59
65
  expect(
60
66
  screen.queryByRole('link', { name: 'please update your account' }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "richie-education",
3
- "version": "3.5.2-dev6",
3
+ "version": "3.5.2-dev8",
4
4
  "description": "A CMS to build learning portals for Open Education",
5
5
  "main": "sandbox/manage.py",
6
6
  "scripts": {