mirror of https://github.com/buster-so/buster.git
unique ids
This commit is contained in:
parent
8b22aafa2c
commit
fb618736e3
|
@ -1,144 +0,0 @@
|
||||||
import axios, { type AxiosError, AxiosHeaders, type InternalAxiosRequestConfig } from 'axios';
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { createAxiosInstance, defaultAxiosRequestHandler } from './createAxiosInstance';
|
|
||||||
import { rustErrorHandler } from './errors';
|
|
||||||
|
|
||||||
// Mock dependencies
|
|
||||||
vi.mock('axios');
|
|
||||||
vi.mock('./buster_rest/errors');
|
|
||||||
vi.mock('./createServerInstance');
|
|
||||||
vi.mock('@tanstack/react-query', () => ({
|
|
||||||
isServer: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('createAxiosInstance', () => {
|
|
||||||
const mockBaseURL = 'https://api.example.com';
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
(axios.create as any).mockReturnValue({
|
|
||||||
interceptors: {
|
|
||||||
response: { use: vi.fn() },
|
|
||||||
request: { use: vi.fn() },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('creates an axios instance with correct configuration', () => {
|
|
||||||
createAxiosInstance(mockBaseURL);
|
|
||||||
|
|
||||||
expect(axios.create).toHaveBeenCalledWith({
|
|
||||||
baseURL: mockBaseURL,
|
|
||||||
timeout: 120000,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sets up response interceptors', () => {
|
|
||||||
const mockInstance = {
|
|
||||||
interceptors: {
|
|
||||||
response: { use: vi.fn() },
|
|
||||||
request: { use: vi.fn() },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
(axios.create as any).mockReturnValue(mockInstance);
|
|
||||||
|
|
||||||
createAxiosInstance(mockBaseURL);
|
|
||||||
|
|
||||||
expect(mockInstance.interceptors.response.use).toHaveBeenCalled();
|
|
||||||
expect(mockInstance.interceptors.request.use).toHaveBeenCalledWith(defaultAxiosRequestHandler);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('handles errors in response interceptor', async () => {
|
|
||||||
const mockError = new Error('API Error') as AxiosError;
|
|
||||||
const mockInstance = {
|
|
||||||
interceptors: {
|
|
||||||
response: { use: vi.fn() },
|
|
||||||
request: { use: vi.fn() },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
(axios.create as any).mockReturnValue(mockInstance);
|
|
||||||
(rustErrorHandler as any).mockReturnValue('Processed Error');
|
|
||||||
|
|
||||||
// Get the error handler by capturing the second argument passed to use()
|
|
||||||
createAxiosInstance(mockBaseURL);
|
|
||||||
const errorHandler = mockInstance.interceptors.response.use.mock.calls[0][1];
|
|
||||||
|
|
||||||
// Test the error handler
|
|
||||||
await expect(errorHandler(mockError)).rejects.toBe('Processed Error');
|
|
||||||
expect(rustErrorHandler).toHaveBeenCalledWith(mockError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('defaultAxiosRequestHandler', () => {
|
|
||||||
const mockConfig: InternalAxiosRequestConfig = {
|
|
||||||
headers: new AxiosHeaders(),
|
|
||||||
method: 'get',
|
|
||||||
url: 'test',
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
vi.resetModules();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('adds authorization header with token in client environment', async () => {
|
|
||||||
const mockToken = 'test-token';
|
|
||||||
const mockCheckTokenValidity = vi.fn().mockResolvedValue({
|
|
||||||
access_token: mockToken,
|
|
||||||
isTokenValid: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await defaultAxiosRequestHandler(mockConfig, {
|
|
||||||
checkTokenValidity: () => Promise.resolve(mockCheckTokenValidity()),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.headers.Authorization).toBe(`Bearer ${mockToken}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws error when token is empty', async () => {
|
|
||||||
const mockCheckTokenValidity = vi.fn().mockResolvedValue({
|
|
||||||
access_token: '',
|
|
||||||
isTokenValid: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// The function should throw an error when no token is available
|
|
||||||
await expect(
|
|
||||||
defaultAxiosRequestHandler(mockConfig, {
|
|
||||||
checkTokenValidity: () => Promise.resolve(mockCheckTokenValidity()),
|
|
||||||
})
|
|
||||||
).rejects.toThrow('User authentication error - failed to get valid token');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('throws error when checkTokenValidity fails', async () => {
|
|
||||||
const mockCheckTokenValidity = vi.fn().mockRejectedValue(new Error('Token validation failed'));
|
|
||||||
|
|
||||||
// The function should throw an error when token validation fails
|
|
||||||
await expect(
|
|
||||||
defaultAxiosRequestHandler(mockConfig, {
|
|
||||||
checkTokenValidity: () => Promise.reject(mockCheckTokenValidity()),
|
|
||||||
})
|
|
||||||
).rejects.toThrow('User authentication error - failed to get valid token');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('preserves existing config properties', async () => {
|
|
||||||
const originalConfig: InternalAxiosRequestConfig = {
|
|
||||||
...mockConfig,
|
|
||||||
timeout: 5000,
|
|
||||||
baseURL: 'https://api.example.com',
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await defaultAxiosRequestHandler(originalConfig, {
|
|
||||||
checkTokenValidity: () =>
|
|
||||||
Promise.resolve({
|
|
||||||
access_token: 'token',
|
|
||||||
isTokenValid: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result.timeout).toBe(5000);
|
|
||||||
expect(result.baseURL).toBe('https://api.example.com');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export const SupabaseIcon: React.FC<{
|
export const SupabaseIcon: React.FC<{
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
@ -6,6 +6,9 @@ export const SupabaseIcon: React.FC<{
|
||||||
size?: number;
|
size?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
}> = (props) => {
|
}> = (props) => {
|
||||||
|
const uniqueId = React.useId();
|
||||||
|
const gradientId = `paint0_linear-${uniqueId}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -18,11 +21,11 @@ export const SupabaseIcon: React.FC<{
|
||||||
<title>Supabase</title>
|
<title>Supabase</title>
|
||||||
<path
|
<path
|
||||||
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
|
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
|
||||||
fill="url(#paint0_linear)"
|
fill={`url(#${gradientId})`}
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
|
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z"
|
||||||
fill="url(#paint1_linear)"
|
fill={`url(#overlay-${uniqueId})`}
|
||||||
fillOpacity="0.2"
|
fillOpacity="0.2"
|
||||||
/>
|
/>
|
||||||
<path
|
<path
|
||||||
|
@ -31,7 +34,7 @@ export const SupabaseIcon: React.FC<{
|
||||||
/>
|
/>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint0_linear"
|
id={gradientId}
|
||||||
x1="53.9738"
|
x1="53.9738"
|
||||||
y1="54.974"
|
y1="54.974"
|
||||||
x2="94.1635"
|
x2="94.1635"
|
||||||
|
@ -42,7 +45,7 @@ export const SupabaseIcon: React.FC<{
|
||||||
<stop offset="1" stopColor="#3ECF8E" />
|
<stop offset="1" stopColor="#3ECF8E" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint1_linear"
|
id={`overlay-${uniqueId}`}
|
||||||
x1="36.1558"
|
x1="36.1558"
|
||||||
y1="30.578"
|
y1="30.578"
|
||||||
x2="54.4844"
|
x2="54.4844"
|
||||||
|
|
|
@ -24,9 +24,10 @@ import {
|
||||||
export const RootProviders: React.FC<PropsWithChildren<SupabaseContextType>> = ({
|
export const RootProviders: React.FC<PropsWithChildren<SupabaseContextType>> = ({
|
||||||
children,
|
children,
|
||||||
user,
|
user,
|
||||||
|
accessToken,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<SupabaseContextProvider user={user}>
|
<SupabaseContextProvider user={user} accessToken={accessToken}>
|
||||||
<BusterStyleProvider>{children}</BusterStyleProvider>
|
<BusterStyleProvider>{children}</BusterStyleProvider>
|
||||||
{/* <BusterReactQueryProvider>
|
{/* <BusterReactQueryProvider>
|
||||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||||
|
|
|
@ -9,14 +9,19 @@ export type SupabaseContextType = {
|
||||||
id: string;
|
id: string;
|
||||||
is_anonymous: boolean;
|
is_anonymous: boolean;
|
||||||
};
|
};
|
||||||
|
accessToken: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const supabase = getBrowserClient();
|
const supabase = getBrowserClient();
|
||||||
const fiveMinutes = 5 * 60 * 1000;
|
const fiveMinutes = 5 * 60 * 1000;
|
||||||
|
|
||||||
const useSupabaseContextInternal = ({ user }: SupabaseContextType) => {
|
const useSupabaseContextInternal = ({
|
||||||
|
user,
|
||||||
|
accessToken: accessTokenProp,
|
||||||
|
}: SupabaseContextType) => {
|
||||||
const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
const refreshTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
const [supabaseUser, setSupabaseUser] = useState<Pick<User, 'id' | 'is_anonymous'> | null>(user);
|
const [supabaseUser, setSupabaseUser] = useState<Pick<User, 'id' | 'is_anonymous'> | null>(user);
|
||||||
|
const [accessToken, setAccessToken] = useState(accessTokenProp);
|
||||||
|
|
||||||
const isAnonymousUser: boolean = !user?.id || user?.is_anonymous === true;
|
const isAnonymousUser: boolean = !user?.id || user?.is_anonymous === true;
|
||||||
|
|
||||||
|
@ -27,9 +32,12 @@ const useSupabaseContextInternal = ({ user }: SupabaseContextType) => {
|
||||||
const user = session?.user ?? null;
|
const user = session?.user ?? null;
|
||||||
const expiresAt = session?.expires_at ?? 0;
|
const expiresAt = session?.expires_at ?? 0;
|
||||||
const timerMs = expiresAt - fiveMinutes;
|
const timerMs = expiresAt - fiveMinutes;
|
||||||
|
const accessToken = session?.access_token ?? '';
|
||||||
|
|
||||||
setSupabaseUser(user);
|
setSupabaseUser(user);
|
||||||
|
|
||||||
|
if (accessToken) setAccessToken(accessToken);
|
||||||
|
|
||||||
if (refreshTimerRef.current) {
|
if (refreshTimerRef.current) {
|
||||||
clearTimeout(refreshTimerRef.current);
|
clearTimeout(refreshTimerRef.current);
|
||||||
}
|
}
|
||||||
|
@ -48,7 +56,9 @@ const useSupabaseContextInternal = ({ user }: SupabaseContextType) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
supabaseUser,
|
||||||
isAnonymousUser,
|
isAnonymousUser,
|
||||||
|
accessToken,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -58,8 +68,8 @@ const SupabaseContext = createContext<ReturnType<typeof useSupabaseContextIntern
|
||||||
|
|
||||||
export const SupabaseContextProvider: React.FC<
|
export const SupabaseContextProvider: React.FC<
|
||||||
SupabaseContextType & { children: React.ReactNode }
|
SupabaseContextType & { children: React.ReactNode }
|
||||||
> = React.memo(({ user, children }) => {
|
> = React.memo(({ user, accessToken, children }) => {
|
||||||
const value = useSupabaseContextInternal({ user });
|
const value = useSupabaseContextInternal({ user, accessToken });
|
||||||
|
|
||||||
return <SupabaseContext.Provider value={value}>{children}</SupabaseContext.Provider>;
|
return <SupabaseContext.Provider value={value}>{children}</SupabaseContext.Provider>;
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,7 +23,7 @@ function transformToAuthUserDTO(user: User): AuthUserDTO {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getSupabaseUser = createServerFn({ method: 'GET' }).handler(async () => {
|
export const getSupabaseUser = createServerFn({ method: 'GET' }).handler(async () => {
|
||||||
const supabase = await getSupabaseServerClient();
|
const supabase = getSupabaseServerClient();
|
||||||
const { data: userData } = await supabase.auth.getUser();
|
const { data: userData } = await supabase.auth.getUser();
|
||||||
|
|
||||||
if (!userData.user) {
|
if (!userData.user) {
|
||||||
|
@ -43,7 +43,7 @@ export const getSupabaseUser = createServerFn({ method: 'GET' }).handler(async (
|
||||||
created_at: anon.data.user?.created_at ?? '',
|
created_at: anon.data.user?.created_at ?? '',
|
||||||
} satisfies AuthUserDTO,
|
} satisfies AuthUserDTO,
|
||||||
accessToken: anon.data.accessToken,
|
accessToken: anon.data.accessToken,
|
||||||
} as { user: AuthUserDTO; accessToken: string | undefined };
|
} as { user: AuthUserDTO; accessToken: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the session first
|
// Get the session first
|
||||||
|
@ -56,5 +56,5 @@ export const getSupabaseUser = createServerFn({ method: 'GET' }).handler(async (
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
accessToken,
|
accessToken,
|
||||||
} as { user: AuthUserDTO; accessToken: string | undefined };
|
} as { user: AuthUserDTO; accessToken: string };
|
||||||
});
|
});
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const Route = createRootRouteWithContext<AppRouterContext>()({
|
||||||
});
|
});
|
||||||
|
|
||||||
function RootDocument({ children }: { children: React.ReactNode }) {
|
function RootDocument({ children }: { children: React.ReactNode }) {
|
||||||
const { user } = Route.useRouteContext();
|
const { user, accessToken } = Route.useRouteContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
@ -41,7 +41,9 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||||
<HeadContent />
|
<HeadContent />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<RootProviders user={user}>{children}</RootProviders>
|
<RootProviders user={user} accessToken={accessToken}>
|
||||||
|
{children}
|
||||||
|
</RootProviders>
|
||||||
<TanstackDevtools />
|
<TanstackDevtools />
|
||||||
<Scripts />
|
<Scripts />
|
||||||
</body>
|
</body>
|
||||||
|
|
Loading…
Reference in New Issue