Files
cnapp/src/api.test.ts
T

224 lines
7.5 KiB
TypeScript

import { applyMiddleware, legacy_createStore as createStore } from 'redux';
import { thunk } from 'redux-thunk';
import {
requestDeleteUser,
requestLogin,
requestRegisterUser,
requestStripePaymentSheet,
requestUserData,
} from '@/api';
import rootReducer from '@/reducers';
const createTestStore = () => createStore(rootReducer, undefined, applyMiddleware(thunk));
const fetchMock = global.fetch as jest.Mock;
const mockResponseOnce = (json: unknown, ok = true) =>
fetchMock.mockResolvedValueOnce({ ok, status: ok ? 200 : 400, json: async () => json });
beforeEach(() => {
fetchMock.mockClear();
});
describe('dev fake users (__DEV__ only)', () => {
it.each([
['nonmember', false],
['member', true],
])('logs in "%s" with a fake user without hitting the network', async (username, isMember) => {
const store = createTestStore();
await store.dispatch(requestLogin(username as string, 'any-password'));
expect(store.getState().isAuthenticated).toBe(true);
await store.dispatch(requestUserData(store.getState().userToken ?? ''));
expect(store.getState().user?.is_member).toBe(isMember);
expect(global.fetch).not.toHaveBeenCalled();
});
});
describe('requestLogin', () => {
it('posts credentials and stores the token on success', async () => {
const store = createTestStore();
mockResponseOnce({ token: 'token-123' });
await store.dispatch(requestLogin('grisen', 'hunter2'));
expect(fetchMock).toHaveBeenCalledWith(
'https://dusken.example.test/auth/obtain-token/',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ username: 'grisen', password: 'hunter2' }),
}),
);
expect(store.getState()).toMatchObject({
isAuthenticated: true,
isLoggingIn: false,
userToken: 'token-123',
loginError: null,
});
});
it('stores the API error body on a rejected login', async () => {
jest.spyOn(console, 'log').mockImplementation(() => {});
const store = createTestStore();
const apiError = { non_field_errors: ['Kan ikke logge inn med disse opplysningene'] };
mockResponseOnce(apiError, false);
await store.dispatch(requestLogin('grisen', 'wrong-password'));
expect(store.getState()).toMatchObject({
isAuthenticated: false,
isLoggingIn: false,
userToken: null,
loginError: apiError,
});
});
it('falls back to a generic error message when the API returns an empty body', async () => {
jest.spyOn(console, 'log').mockImplementation(() => {});
const store = createTestStore();
mockResponseOnce(null, false);
await store.dispatch(requestLogin('grisen', 'hunter2'));
expect(store.getState().loginError).toEqual({ non_field_errors: ['Kunne ikke logge inn, prøv igjen'] });
});
});
describe('requestUserData', () => {
it('fetches /api/me/ with the auth token and stores the user', async () => {
const store = createTestStore();
const user = { username: 'grisen', email: 'grisen@example.test', is_member: true };
mockResponseOnce(user);
await store.dispatch(requestUserData('token-123'));
expect(fetchMock).toHaveBeenCalledWith(
'https://dusken.example.test/api/me/',
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({ Authorization: 'Token token-123' }),
}),
);
expect(store.getState()).toMatchObject({ isFetchingUserData: false, user });
});
it('stores the API error body when fetching user data fails', async () => {
const store = createTestStore();
const apiError = { detail: 'Ugyldig token.' };
mockResponseOnce(apiError, false);
await store.dispatch(requestUserData('expired-token'));
expect(store.getState()).toMatchObject({ isFetchingUserData: false, user: null, userError: apiError });
});
});
describe('requestRegisterUser', () => {
const formValues = {
first_name: 'Grisen',
last_name: 'Testersen',
email: 'grisen@example.test',
phone_number: '+4712345678',
password: 'hunter2',
};
it('authenticates with the returned auth_token and stores the user without it', async () => {
const store = createTestStore();
mockResponseOnce({ username: 'grisen', email: 'grisen@example.test', auth_token: 'token-456' });
await store.dispatch(requestRegisterUser(formValues));
expect(fetchMock).toHaveBeenCalledWith(
'https://dusken.example.test/api/user/register/',
expect.objectContaining({ method: 'POST', body: JSON.stringify(formValues) }),
);
expect(store.getState()).toMatchObject({
isRegisteringUser: false,
isAuthenticated: true,
userToken: 'token-456',
user: { username: 'grisen', email: 'grisen@example.test' },
});
expect(store.getState().user).not.toHaveProperty('auth_token');
});
it('stores field errors from the API when registration fails', async () => {
const store = createTestStore();
const apiError = { email: ['Denne e-postadressen er allerede i bruk'] };
mockResponseOnce(apiError, false);
await store.dispatch(requestRegisterUser(formValues));
expect(store.getState()).toMatchObject({
isRegisteringUser: false,
isAuthenticated: false,
registerError: apiError,
});
});
});
describe('requestDeleteUser', () => {
it('resets the store to its initial state on success', async () => {
const store = createTestStore();
// Authenticate first (via the fake-user shortcut) so the reset is observable
await store.dispatch(requestLogin('member', 'any-password'));
expect(store.getState().isAuthenticated).toBe(true);
mockResponseOnce({});
await store.dispatch(requestDeleteUser('token-123'));
expect(fetchMock).toHaveBeenCalledWith(
'https://dusken.example.test/api/me/delete/',
expect.objectContaining({
method: 'DELETE',
headers: expect.objectContaining({ Authorization: 'Token token-123' }),
}),
);
expect(store.getState()).toMatchObject({ isAuthenticated: false, userToken: null, user: null });
});
it('stores the API error body when deletion fails', async () => {
const store = createTestStore();
const apiError = { detail: 'Kan ikke slette bruker med aktivt medlemskap' };
mockResponseOnce(apiError, false);
await store.dispatch(requestDeleteUser('token-123'));
expect(store.getState()).toMatchObject({ isDeletingUser: false, deleteUserError: apiError });
});
});
describe('requestStripePaymentSheet', () => {
it('posts the membership type and returns the payment sheet parameters', async () => {
const sheet = { paymentIntent: 'pi_secret', ephemeralKey: 'ek_secret', customer: 'cus_123' };
mockResponseOnce(sheet);
await expect(requestStripePaymentSheet('token-123', 'standard')).resolves.toEqual(sheet);
expect(fetchMock).toHaveBeenCalledWith(
'https://dusken.example.test/api/stripe/payment-sheet/',
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ membership_type: 'standard' }),
headers: expect.objectContaining({ Authorization: 'Token token-123' }),
}),
);
});
it('rejects with the API detail message on failure', async () => {
mockResponseOnce({ detail: 'Ugyldig medlemskapstype' }, false);
await expect(requestStripePaymentSheet('token-123', 'bogus')).rejects.toThrow('Ugyldig medlemskapstype');
});
it('rejects with a generic message when the API gives no detail', async () => {
mockResponseOnce({}, false);
await expect(requestStripePaymentSheet('token-123', 'standard')).rejects.toThrow(
'Kunne ikke belaste bankkortet, prøv igjen',
);
});
});