Fix 2fa auth and token format migration (#13523)

Fix the 2FA setup and also make some changes so that the transition
towards a new token format introduced in a previous PR happens more
smoothly
This commit is contained in:
Félix Malfait
2025-07-31 14:13:12 +02:00
committed by GitHub
parent c8128c4d3f
commit f52973d71d
13 changed files with 992 additions and 27 deletions
@@ -0,0 +1,298 @@
import { renderHook } from '@testing-library/react';
import { createClient } from 'graphql-sse';
import { DatabaseEventAction } from '~/generated/graphql';
import { getTokenPair } from '~/modules/apollo/utils/getTokenPair';
import { useOnDbEvent } from '../useOnDbEvent';
jest.mock('~/modules/apollo/utils/getTokenPair');
jest.mock('graphql-sse');
const mockGetTokenPair = getTokenPair as jest.MockedFunction<
typeof getTokenPair
>;
const mockCreateClient = createClient as jest.MockedFunction<
typeof createClient
>;
// Mock environment variable
const mockServerBaseUrl = 'http://localhost:3000';
jest.mock('~/config', () => ({
REACT_APP_SERVER_BASE_URL: 'http://localhost:3000',
}));
describe('useOnDbEvent', () => {
const mockUnsubscribe = jest.fn();
const mockClient = {
subscribe: jest.fn(() => mockUnsubscribe),
dispose: jest.fn(),
};
beforeEach(() => {
jest.clearAllMocks();
mockCreateClient.mockReturnValue(mockClient as any);
});
describe('token safety checks', () => {
it('should handle undefined tokenPair gracefully', () => {
mockGetTokenPair.mockReturnValue(undefined);
renderHook(() =>
useOnDbEvent({
input: {
objectNameSingular: 'test',
action: DatabaseEventAction.CREATED,
},
onData: jest.fn(),
}),
);
expect(mockCreateClient).toHaveBeenCalledWith({
url: `${mockServerBaseUrl}/graphql`,
headers: {
Authorization: '',
},
});
});
it('should handle tokenPair with undefined accessOrWorkspaceAgnosticToken gracefully', () => {
mockGetTokenPair.mockReturnValue({
accessOrWorkspaceAgnosticToken: undefined,
refreshToken: {
token: 'refresh-token',
expiresAt: '2024-01-02T00:00:00Z',
},
} as any);
renderHook(() =>
useOnDbEvent({
input: {
objectNameSingular: 'test',
action: DatabaseEventAction.CREATED,
},
onData: jest.fn(),
}),
);
expect(mockCreateClient).toHaveBeenCalledWith({
url: `${mockServerBaseUrl}/graphql`,
headers: {
Authorization: '',
},
});
});
it('should handle tokenPair with accessOrWorkspaceAgnosticToken but undefined token gracefully', () => {
mockGetTokenPair.mockReturnValue({
accessOrWorkspaceAgnosticToken: {
token: undefined,
expiresAt: '2024-01-01T00:00:00Z',
},
refreshToken: {
token: 'refresh-token',
expiresAt: '2024-01-02T00:00:00Z',
},
} as any);
renderHook(() =>
useOnDbEvent({
input: {
objectNameSingular: 'test',
action: DatabaseEventAction.CREATED,
},
onData: jest.fn(),
}),
);
expect(mockCreateClient).toHaveBeenCalledWith({
url: `${mockServerBaseUrl}/graphql`,
headers: {
Authorization: '',
},
});
});
it('should handle tokenPair with null token gracefully', () => {
mockGetTokenPair.mockReturnValue({
accessOrWorkspaceAgnosticToken: {
token: null,
expiresAt: '2024-01-01T00:00:00Z',
},
refreshToken: {
token: 'refresh-token',
expiresAt: '2024-01-02T00:00:00Z',
},
} as any);
renderHook(() =>
useOnDbEvent({
input: {
objectNameSingular: 'test',
action: DatabaseEventAction.CREATED,
},
onData: jest.fn(),
}),
);
expect(mockCreateClient).toHaveBeenCalledWith({
url: `${mockServerBaseUrl}/graphql`,
headers: {
Authorization: '',
},
});
});
it('should properly set authorization header when token is valid', () => {
const validToken = 'valid-access-token';
mockGetTokenPair.mockReturnValue({
accessOrWorkspaceAgnosticToken: {
token: validToken,
expiresAt: '2024-01-01T00:00:00Z',
},
refreshToken: {
token: 'refresh-token',
expiresAt: '2024-01-02T00:00:00Z',
},
});
renderHook(() =>
useOnDbEvent({
input: {
objectNameSingular: 'test',
action: DatabaseEventAction.CREATED,
},
onData: jest.fn(),
}),
);
expect(mockCreateClient).toHaveBeenCalledWith({
url: `${mockServerBaseUrl}/graphql`,
headers: {
Authorization: `Bearer ${validToken}`,
},
});
});
it('should handle empty string token gracefully', () => {
mockGetTokenPair.mockReturnValue({
accessOrWorkspaceAgnosticToken: {
token: '',
expiresAt: '2024-01-01T00:00:00Z',
},
refreshToken: {
token: 'refresh-token',
expiresAt: '2024-01-02T00:00:00Z',
},
});
renderHook(() =>
useOnDbEvent({
input: {
objectNameSingular: 'test',
action: DatabaseEventAction.CREATED,
},
onData: jest.fn(),
}),
);
expect(mockCreateClient).toHaveBeenCalledWith({
url: `${mockServerBaseUrl}/graphql`,
headers: {
Authorization: '',
},
});
});
});
describe('basic functionality', () => {
const validToken = 'test-token';
beforeEach(() => {
mockGetTokenPair.mockReturnValue({
accessOrWorkspaceAgnosticToken: {
token: validToken,
expiresAt: '2024-01-01T00:00:00Z',
},
refreshToken: {
token: 'refresh-token',
expiresAt: '2024-01-02T00:00:00Z',
},
});
});
it('should create SSE client with correct URL and headers', () => {
renderHook(() =>
useOnDbEvent({
input: {
objectNameSingular: 'test',
action: DatabaseEventAction.CREATED,
},
onData: jest.fn(),
}),
);
expect(mockCreateClient).toHaveBeenCalledWith({
url: `${mockServerBaseUrl}/graphql`,
headers: {
Authorization: `Bearer ${validToken}`,
},
});
});
it('should not subscribe when skip is true', () => {
renderHook(() =>
useOnDbEvent({
input: {
objectNameSingular: 'test',
action: DatabaseEventAction.CREATED,
},
onData: jest.fn(),
skip: true,
}),
);
expect(mockClient.subscribe).not.toHaveBeenCalled();
});
it('should subscribe when skip is false or undefined', () => {
const mockOnData = jest.fn();
renderHook(() =>
useOnDbEvent({
input: {
objectNameSingular: 'test',
action: DatabaseEventAction.CREATED,
},
onData: mockOnData,
skip: false,
}),
);
expect(mockClient.subscribe).toHaveBeenCalled();
});
it('should pass correct parameters to subscription', () => {
const mockOnData = jest.fn();
renderHook(() =>
useOnDbEvent({
input: {
objectNameSingular: 'person',
action: DatabaseEventAction.CREATED,
},
onData: mockOnData,
}),
);
expect(mockClient.subscribe).toHaveBeenCalledWith(
expect.objectContaining({
query: expect.stringContaining('subscription'),
}),
expect.objectContaining({
next: expect.any(Function),
error: expect.any(Function),
}),
);
});
});
});
@@ -22,15 +22,15 @@ export const useOnDbEvent = ({
const tokenPair = getTokenPair();
const sseClient = useMemo(() => {
const token = tokenPair?.accessOrWorkspaceAgnosticToken?.token;
return createClient({
url: `${REACT_APP_SERVER_BASE_URL}/graphql`,
headers: {
Authorization: tokenPair?.accessOrWorkspaceAgnosticToken.token
? `Bearer ${tokenPair?.accessOrWorkspaceAgnosticToken.token}`
: '',
Authorization: token ? `Bearer ${token}` : '',
},
});
}, [tokenPair?.accessOrWorkspaceAgnosticToken.token]);
}, [tokenPair?.accessOrWorkspaceAgnosticToken?.token]);
useEffect(() => {
if (skip === true) {