Skip to content

Commit 0ab1873

Browse files
committed
chore: add unit tests
1 parent cd5c32d commit 0ab1873

File tree

7 files changed

+238
-4
lines changed

7 files changed

+238
-4
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { ContentpassSdkProvider } from './ContentpassSdkProvider';
2+
import type { ContentpassConfig } from '../types/ContentpassConfig';
3+
import { Text } from 'react-native';
4+
import { render, screen } from '@testing-library/react-native';
5+
import Contentpass from '../Contentpass';
6+
7+
jest.mock('../Contentpass');
8+
9+
describe('ContentpassSdkProvider', () => {
10+
const mockConfig: ContentpassConfig = {
11+
issuer: 'https://my.contentpass.me',
12+
propertyId: 'my-property-id',
13+
redirectUrl: 'de.contentpass.test://oauth',
14+
};
15+
16+
afterEach(() => {
17+
jest.resetAllMocks();
18+
});
19+
20+
it('initializes Contentpass SDK with the given configuration', () => {
21+
render(
22+
<ContentpassSdkProvider contentpassConfig={mockConfig}>
23+
<Text testID="child">Test Child</Text>
24+
</ContentpassSdkProvider>
25+
);
26+
27+
expect(Contentpass).toHaveBeenCalledWith(mockConfig);
28+
expect(screen.getByTestId('child')).toHaveTextContent('Test Child');
29+
});
30+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from 'react';
2+
import { renderHook } from '@testing-library/react-native';
3+
import useContentpassSdk from './useContentpassSdk';
4+
import { contentpassSdkContext } from './ContentpassSdkProvider';
5+
import Contentpass from '../Contentpass';
6+
7+
describe('useContentpassSdk', () => {
8+
afterEach(() => {
9+
jest.restoreAllMocks();
10+
});
11+
12+
it('should return the contentpassSdk from the context', () => {
13+
const contentpassSdk = 'contentpassSdk';
14+
const wrapper = ({ children }: { children: React.ReactNode }) => (
15+
<contentpassSdkContext.Provider
16+
value={contentpassSdk as any as Contentpass}
17+
>
18+
{children}
19+
</contentpassSdkContext.Provider>
20+
);
21+
22+
const { result } = renderHook(() => useContentpassSdk(), { wrapper });
23+
24+
expect(result.current).toBe(contentpassSdk);
25+
});
26+
27+
it('should throw an error if used outside of a ContentpassSdkProvider', async () => {
28+
// mock console.error to prevent error output in test
29+
jest.spyOn(console, 'error').mockImplementation(() => {});
30+
31+
expect(() => {
32+
renderHook(() => useContentpassSdk());
33+
}).toThrow(
34+
'useContentpassSdk must be used within a ContentpassSdkProvider'
35+
);
36+
});
37+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import fetchContentpassToken from './fetchContentpassToken';
2+
3+
describe('fetchContentpassToken', () => {
4+
afterEach(() => {
5+
jest.restoreAllMocks();
6+
});
7+
8+
it('should return the contentpass token', async () => {
9+
jest.spyOn(global, 'fetch').mockResolvedValue({
10+
json: jest
11+
.fn()
12+
.mockResolvedValue({ contentpass_token: 'example_contentpass_token' }),
13+
} as any);
14+
15+
const result = await fetchContentpassToken({
16+
idToken: '123456',
17+
propertyId: '987654321',
18+
issuer: 'https://issuer.com',
19+
});
20+
21+
expect(result).toBe('example_contentpass_token');
22+
expect(global.fetch).toHaveBeenCalledWith(
23+
'https://issuer.com/auth/oidc/token',
24+
{
25+
body: 'grant_type=contentpass_token&subject_token=123456&client_id=987654321',
26+
method: 'POST',
27+
headers: {
28+
'Content-Type': 'application/x-www-form-urlencoded',
29+
},
30+
}
31+
);
32+
});
33+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import parseContentpassToken from './parseContentpassToken';
2+
3+
describe('parseContentpassToken', () => {
4+
it('should parse the contentpass token', () => {
5+
const exampleToken =
6+
'eyJhbGciOiJSUzI1NiIsImtpZCI6IjY5NzUwYTZjLTNmYjctNDUyNi05NWY4LTVhZmYxMmIyZjFjOSJ9.eyJhdXRoIjp0cnVlLCJ0eXBlIjoiY3AiLCJwbGFucyI6WyIwYWNhZTkxNy1iZTk5LTQ4ZWEtYjhmMS0yMGZhNjhhNDdkM2EiLCI0NDIxNjI4Yy05NjA2LTRjMDEtOGU1ZC1jMmE5YmNhNjhhYjQiLCI3ZThkZTBjYy0zZTk3LTQ5YTItODgxZC05ZmZiNWI4NDE1MTUiLCJhNDcyMWRiNS02N2RmLTQxNDUtYmJiZi1jYmQwOWY3ZTAzOTciLCJjNGQzYjBmNS05ODlhLTRmN2ItOGFjNy0zZDhmZmE5NTcxN2YiLCI2NGRkOTkwNS05NmUxLTRmYjItOTgwZC01MDdmMTYzNzVmZTkiXSwiYXVkIjoiY2MzZmM0YWQiLCJpYXQiOjE3MzMxMzU2ODEsImV4cCI6MTczMzMxMjA4MX0.CMtH7HRLf2HVgw3_cZRN0en8tml_SQKM73iLGJAp72-vJuRJaq85xBp6Jgy9WD3L7x4itRlBAYZxX8tLxZGogU0WP4_dMGFQ2QlcwKshwJygwRM1YqvxGWX2Az_KxEMc2QGHvpE1qe2MAr_xOU7VFfc0-vWxFc3hRzpAM5j7YHctj2t1v6h9-M7V2Hkcn37569QmtgU8gJkUxXsgUTufbb1ikjjjAvnjvTluHJo51_utbimpUbCk3EFxXVCVEI_pAqiZQXNninUQ6dbSujLb3L2UlEdQzLeUiBdYroeFzSyruLrR841ledLQ5ZP2OqzF5oUMuAGVOOhmgGdwGMCDRQ';
7+
8+
const result = parseContentpassToken(exampleToken);
9+
10+
expect(result).toEqual({
11+
body: {
12+
aud: 'cc3fc4ad',
13+
auth: true,
14+
exp: 1733312081,
15+
iat: 1733135681,
16+
plans: [
17+
'0acae917-be99-48ea-b8f1-20fa68a47d3a',
18+
'4421628c-9606-4c01-8e5d-c2a9bca68ab4',
19+
'7e8de0cc-3e97-49a2-881d-9ffb5b841515',
20+
'a4721db5-67df-4145-bbbf-cbd09f7e0397',
21+
'c4d3b0f5-989a-4f7b-8ac7-3d8ffa95717f',
22+
'64dd9905-96e1-4fb2-980d-507f16375fe9',
23+
],
24+
type: 'cp',
25+
},
26+
header: {
27+
alg: 'RS256',
28+
kid: '69750a6c-3fb7-4526-95f8-5aff12b2f1c9',
29+
},
30+
});
31+
});
32+
33+
it('should throw an error if the token is invalid', () => {
34+
const invalidToken =
35+
'eyJhdXRoIjp0cnVlLCJ0eXBlIjoiY3AiLCJwbGFucyI6WyIwYWNhZTkxNy1iZTk5LTQ4ZWEtYjhmMS0yMGZhNjhhNDdkM2EiLCI0NDIxNjI4Yy05NjA2LTRjMDEtOGU1ZC1jMmE5YmNhNjhhYjQiLCI3ZThkZTBjYy0zZTk3LTQ5YTItODgxZC05ZmZiNWI4NDE1MTUiLCJhNDcyMWRiNS02N2RmLTQxNDUtYmJiZi1jYmQwOWY3ZTAzOTciLCJjNGQzYjBmNS05ODlhLTRmN2ItOGFjNy0zZDhmZmE5NTcxN2YiLCI2NGRkOTkwNS05NmUxLTRmYjItOTgwZC01MDdmMTYzNzVmZTkiXSwiYXVkIjoiY2MzZmM0YWQiLCJpYXQiOjE3MzMxMzU2ODEsImV4cCI6MTczMzMxMjA4MX0.CMtH7HRLf2HVgw3_cZRN0en8tml_SQKM73iLGJAp72-vJuRJaq85xBp6Jgy9WD3L7x4itRlBAYZxX8tLxZGogU0WP4_dMGFQ2QlcwKshwJygwRM1YqvxGWX2Az_KxEMc2QGHvpE1qe2MAr_xOU7VFfc0-vWxFc3hRzpAM5j7YHctj2t1v6h9-M7V2Hkcn37569QmtgU8gJkUxXsgUTufbb1ikjjjAvnjvTluHJo51_utbimpUbCk3EFxXVCVEI_pAqiZQXNninUQ6dbSujLb3L2UlEdQzLeUiBdYroeFzSyruLrR841ledLQ5ZP2OqzF5oUMuAGVOOhmgGdwGMCDRQ';
36+
37+
expect(() => {
38+
parseContentpassToken(invalidToken);
39+
}).toThrow('Invalid token');
40+
});
41+
});

src/utils/parseContentpassToken.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,21 @@
1-
export default function parseContentpassToken(contentpassToken: string) {
1+
type ParsedToken = {
2+
body: {
3+
aud: string;
4+
auth: boolean;
5+
exp: number;
6+
iat: number;
7+
plans: string[];
8+
type: string;
9+
};
10+
header: {
11+
alg: string;
12+
kid: string;
13+
};
14+
};
15+
16+
export default function parseContentpassToken(
17+
contentpassToken: string
18+
): ParsedToken {
219
const tokenParts = contentpassToken.split('.');
320
if (tokenParts.length < 3) {
421
throw new Error('Invalid token');
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import parseContentpassToken, * as ParseContentpassTokenModule from './parseContentpassToken';
2+
import validateSubscription from './validateSubscription';
3+
4+
const EXAMPLE_CONTENTPASS_TOKEN: ReturnType<typeof parseContentpassToken> = {
5+
body: {
6+
aud: 'cc3fc4ad',
7+
auth: true,
8+
exp: 1733312081,
9+
iat: 1733135681,
10+
type: 'cp',
11+
plans: ['planId1'],
12+
},
13+
header: {
14+
alg: 'RS256',
15+
kid: 'kid',
16+
},
17+
};
18+
19+
describe('validateSubscription', () => {
20+
let parseContentpassTokenSpy: jest.SpyInstance;
21+
beforeEach(() => {
22+
parseContentpassTokenSpy = jest
23+
.spyOn(ParseContentpassTokenModule, 'default')
24+
.mockReturnValue(EXAMPLE_CONTENTPASS_TOKEN);
25+
});
26+
27+
afterEach(() => {
28+
jest.restoreAllMocks();
29+
});
30+
31+
it('should return true if the token is valid', () => {
32+
parseContentpassTokenSpy.mockReturnValue(EXAMPLE_CONTENTPASS_TOKEN);
33+
const result = validateSubscription('example_contentpass_token');
34+
35+
expect(result).toBe(true);
36+
});
37+
38+
it('should return false if the token is invalid', () => {
39+
parseContentpassTokenSpy.mockImplementation(() => {
40+
throw new Error('Invalid token');
41+
});
42+
const result = validateSubscription('example_contentpass_token');
43+
44+
expect(result).toBe(false);
45+
});
46+
47+
it('should return false if the user is not authenticated', () => {
48+
parseContentpassTokenSpy.mockReturnValue({
49+
...EXAMPLE_CONTENTPASS_TOKEN,
50+
body: {
51+
...EXAMPLE_CONTENTPASS_TOKEN.body,
52+
auth: false,
53+
},
54+
});
55+
const result = validateSubscription('example_contentpass_token');
56+
57+
expect(result).toBe(false);
58+
});
59+
60+
it('should return false if the user has no plans', () => {
61+
parseContentpassTokenSpy.mockReturnValue({
62+
...EXAMPLE_CONTENTPASS_TOKEN,
63+
body: {
64+
...EXAMPLE_CONTENTPASS_TOKEN.body,
65+
plans: [],
66+
},
67+
});
68+
const result = validateSubscription('example_contentpass_token');
69+
70+
expect(result).toBe(false);
71+
});
72+
});

src/utils/validateSubscription.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import parseContentpassToken from './parseContentpassToken';
22

33
export default function validateSubscription(contentpassToken: string) {
4-
const { body } = parseContentpassToken(contentpassToken);
5-
6-
return !!body.auth && !!body.plans.length;
4+
try {
5+
const { body } = parseContentpassToken(contentpassToken);
6+
return !!body.auth && !!body.plans.length;
7+
} catch (err) {
8+
// FIXME: logger for error
9+
return false;
10+
}
711
}

0 commit comments

Comments
 (0)