refactor: move web auth onto shared platform provider
This commit is contained in:
parent
d78aeeffc2
commit
a4fce709f0
@ -13,6 +13,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bytelyst/kill-switch-client": "link:../../learning_ai/learning_ai_common_plat/packages/kill-switch-client",
|
"@bytelyst/kill-switch-client": "link:../../learning_ai/learning_ai_common_plat/packages/kill-switch-client",
|
||||||
|
"@bytelyst/react-auth": "link:../../learning_ai/learning_ai_common_plat/packages/react-auth",
|
||||||
"@bytelyst/react-native-platform-sdk": "link:../../learning_ai/learning_ai_common_plat/packages/react-native-platform-sdk",
|
"@bytelyst/react-native-platform-sdk": "link:../../learning_ai/learning_ai_common_plat/packages/react-native-platform-sdk",
|
||||||
"@bytelyst/telemetry-client": "link:../../learning_ai/learning_ai_common_plat/packages/telemetry-client"
|
"@bytelyst/telemetry-client": "link:../../learning_ai/learning_ai_common_plat/packages/telemetry-client"
|
||||||
},
|
},
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -11,6 +11,9 @@ importers:
|
|||||||
'@bytelyst/kill-switch-client':
|
'@bytelyst/kill-switch-client':
|
||||||
specifier: link:../../learning_ai/learning_ai_common_plat/packages/kill-switch-client
|
specifier: link:../../learning_ai/learning_ai_common_plat/packages/kill-switch-client
|
||||||
version: link:../../learning_ai/learning_ai_common_plat/packages/kill-switch-client
|
version: link:../../learning_ai/learning_ai_common_plat/packages/kill-switch-client
|
||||||
|
'@bytelyst/react-auth':
|
||||||
|
specifier: link:../../learning_ai/learning_ai_common_plat/packages/react-auth
|
||||||
|
version: link:../../learning_ai/learning_ai_common_plat/packages/react-auth
|
||||||
'@bytelyst/react-native-platform-sdk':
|
'@bytelyst/react-native-platform-sdk':
|
||||||
specifier: link:../../learning_ai/learning_ai_common_plat/packages/react-native-platform-sdk
|
specifier: link:../../learning_ai/learning_ai_common_plat/packages/react-native-platform-sdk
|
||||||
version: link:../../learning_ai/learning_ai_common_plat/packages/react-native-platform-sdk
|
version: link:../../learning_ai/learning_ai_common_plat/packages/react-native-platform-sdk
|
||||||
@ -233,6 +236,9 @@ importers:
|
|||||||
'@bytelyst/kill-switch-client':
|
'@bytelyst/kill-switch-client':
|
||||||
specifier: link:../../../learning_ai/learning_ai_common_plat/packages/kill-switch-client
|
specifier: link:../../../learning_ai/learning_ai_common_plat/packages/kill-switch-client
|
||||||
version: link:../../../learning_ai/learning_ai_common_plat/packages/kill-switch-client
|
version: link:../../../learning_ai/learning_ai_common_plat/packages/kill-switch-client
|
||||||
|
'@bytelyst/react-auth':
|
||||||
|
specifier: link:../../../learning_ai/learning_ai_common_plat/packages/react-auth
|
||||||
|
version: link:../../../learning_ai/learning_ai_common_plat/packages/react-auth
|
||||||
'@bytelyst/telemetry-client':
|
'@bytelyst/telemetry-client':
|
||||||
specifier: link:../../../learning_ai/learning_ai_common_plat/packages/telemetry-client
|
specifier: link:../../../learning_ai/learning_ai_common_plat/packages/telemetry-client
|
||||||
version: link:../../../learning_ai/learning_ai_common_plat/packages/telemetry-client
|
version: link:../../../learning_ai/learning_ai_common_plat/packages/telemetry-client
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bytelyst/kill-switch-client": "link:../../../learning_ai/learning_ai_common_plat/packages/kill-switch-client",
|
"@bytelyst/kill-switch-client": "link:../../../learning_ai/learning_ai_common_plat/packages/kill-switch-client",
|
||||||
|
"@bytelyst/react-auth": "link:../../../learning_ai/learning_ai_common_plat/packages/react-auth",
|
||||||
"@bytelyst/telemetry-client": "link:../../../learning_ai/learning_ai_common_plat/packages/telemetry-client",
|
"@bytelyst/telemetry-client": "link:../../../learning_ai/learning_ai_common_plat/packages/telemetry-client",
|
||||||
"@supabase/supabase-js": "^2.90.1",
|
"@supabase/supabase-js": "^2.90.1",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
|
|||||||
@ -7,29 +7,37 @@ import { tableNameProfiles, tableNameUsers } from '../lib/const';
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
getSessionMock,
|
getSessionMock,
|
||||||
onAuthStateChangeMock,
|
|
||||||
signOutMock,
|
signOutMock,
|
||||||
fromMock,
|
fromMock,
|
||||||
usersSingleMock,
|
usersSingleMock,
|
||||||
profilesLimitMock,
|
profilesLimitMock,
|
||||||
profilesInsertMock,
|
profilesInsertMock,
|
||||||
unsubscribeMock
|
unsubscribeMock,
|
||||||
|
tradingAuthState
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
getSessionMock: vi.fn(),
|
getSessionMock: vi.fn(),
|
||||||
onAuthStateChangeMock: vi.fn(),
|
|
||||||
signOutMock: vi.fn(),
|
signOutMock: vi.fn(),
|
||||||
fromMock: vi.fn(),
|
fromMock: vi.fn(),
|
||||||
usersSingleMock: vi.fn(),
|
usersSingleMock: vi.fn(),
|
||||||
profilesLimitMock: vi.fn(),
|
profilesLimitMock: vi.fn(),
|
||||||
profilesInsertMock: vi.fn(),
|
profilesInsertMock: vi.fn(),
|
||||||
unsubscribeMock: vi.fn()
|
unsubscribeMock: vi.fn(),
|
||||||
|
tradingAuthState: {
|
||||||
|
user: { id: 'user-1', email: 'sarah@example.com', role: 'admin', name: 'Sarah Algo' } as any,
|
||||||
|
isLoading: false,
|
||||||
|
logout: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../lib/tradingAuth', () => ({
|
||||||
|
TradingAuthProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||||
|
useTradingAuth: () => tradingAuthState
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => ({
|
vi.mock('../lib/supabaseClient', () => ({
|
||||||
supabase: {
|
supabase: {
|
||||||
auth: {
|
auth: {
|
||||||
getSession: getSessionMock,
|
getSession: getSessionMock,
|
||||||
onAuthStateChange: onAuthStateChangeMock,
|
|
||||||
signOut: signOutMock
|
signOut: signOutMock
|
||||||
},
|
},
|
||||||
from: fromMock
|
from: fromMock
|
||||||
@ -52,19 +60,15 @@ const Probe = () => {
|
|||||||
describe('AuthContext DOM behavior', () => {
|
describe('AuthContext DOM behavior', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getSessionMock.mockReset();
|
getSessionMock.mockReset();
|
||||||
onAuthStateChangeMock.mockReset();
|
|
||||||
signOutMock.mockReset();
|
signOutMock.mockReset();
|
||||||
fromMock.mockReset();
|
fromMock.mockReset();
|
||||||
usersSingleMock.mockReset();
|
usersSingleMock.mockReset();
|
||||||
profilesLimitMock.mockReset();
|
profilesLimitMock.mockReset();
|
||||||
profilesInsertMock.mockReset();
|
profilesInsertMock.mockReset();
|
||||||
unsubscribeMock.mockReset();
|
unsubscribeMock.mockReset();
|
||||||
|
tradingAuthState.user = { id: 'user-1', email: 'sarah@example.com', role: 'admin', name: 'Sarah Algo' };
|
||||||
onAuthStateChangeMock.mockReturnValue({
|
tradingAuthState.isLoading = false;
|
||||||
data: {
|
tradingAuthState.logout.mockReset();
|
||||||
subscription: { unsubscribe: unsubscribeMock }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
signOutMock.mockResolvedValue({ error: null });
|
signOutMock.mockResolvedValue({ error: null });
|
||||||
usersSingleMock.mockResolvedValue({
|
usersSingleMock.mockResolvedValue({
|
||||||
@ -115,7 +119,7 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
});
|
});
|
||||||
const dispatchSpy = vi.spyOn(window, 'dispatchEvent');
|
const dispatchSpy = vi.spyOn(window, 'dispatchEvent');
|
||||||
|
|
||||||
const { unmount } = render(
|
render(
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<Probe />
|
<Probe />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
@ -133,12 +137,10 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
]);
|
]);
|
||||||
expect(dispatchSpy).toHaveBeenCalled();
|
expect(dispatchSpy).toHaveBeenCalled();
|
||||||
expect((dispatchSpy.mock.calls[0][0] as Event).type).toBe('profiles-updated');
|
expect((dispatchSpy.mock.calls[0][0] as Event).type).toBe('profiles-updated');
|
||||||
|
|
||||||
unmount();
|
|
||||||
expect(unsubscribeMock).toHaveBeenCalledTimes(1);
|
|
||||||
}, 20000);
|
}, 20000);
|
||||||
|
|
||||||
it('handles no initial session gracefully', async () => {
|
it('handles no initial session gracefully', async () => {
|
||||||
|
tradingAuthState.user = null;
|
||||||
getSessionMock.mockResolvedValue({ data: { session: null } });
|
getSessionMock.mockResolvedValue({ data: { session: null } });
|
||||||
render(<AuthProvider><Probe /></AuthProvider>);
|
render(<AuthProvider><Probe /></AuthProvider>);
|
||||||
|
|
||||||
@ -151,15 +153,14 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
|
|
||||||
it('handles auth state changes with no session', async () => {
|
it('handles auth state changes with no session', async () => {
|
||||||
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
||||||
render(<AuthProvider><Probe /></AuthProvider>);
|
const { rerender } = render(<AuthProvider><Probe /></AuthProvider>);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('user')).toHaveTextContent('u1');
|
expect(screen.getByTestId('user')).toHaveTextContent('u1');
|
||||||
});
|
});
|
||||||
|
|
||||||
const authChangeCb = onAuthStateChangeMock.mock.calls[0][0];
|
tradingAuthState.user = null;
|
||||||
// Simulate sign out event
|
rerender(<AuthProvider><Probe /></AuthProvider>);
|
||||||
authChangeCb('SIGNED_OUT', null);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId('user')).toHaveTextContent('none');
|
expect(screen.getByTestId('user')).toHaveTextContent('none');
|
||||||
@ -169,6 +170,7 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
|
|
||||||
it('logs error when profile fetch fails', async () => {
|
it('logs error when profile fetch fails', async () => {
|
||||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
||||||
|
tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' };
|
||||||
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
||||||
usersSingleMock.mockResolvedValue({ data: null, error: { message: 'Profile Not Found' } });
|
usersSingleMock.mockResolvedValue({ data: null, error: { message: 'Profile Not Found' } });
|
||||||
|
|
||||||
@ -182,6 +184,7 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
|
|
||||||
it('handles unexpected errors in fetchProfile', async () => {
|
it('handles unexpected errors in fetchProfile', async () => {
|
||||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
||||||
|
tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' };
|
||||||
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
||||||
usersSingleMock.mockImplementation(() => { throw new Error('Crashed'); });
|
usersSingleMock.mockImplementation(() => { throw new Error('Crashed'); });
|
||||||
|
|
||||||
@ -195,6 +198,7 @@ describe('AuthContext DOM behavior', () => {
|
|||||||
|
|
||||||
it('handles unexpected errors in ensureDefaultProfile', async () => {
|
it('handles unexpected errors in ensureDefaultProfile', async () => {
|
||||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
||||||
|
tradingAuthState.user = { id: 'u1', email: 'u1@example.com', role: 'member', name: 'U One' };
|
||||||
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
getSessionMock.mockResolvedValue({ data: { session: { user: { id: 'u1' } } } });
|
||||||
profilesLimitMock.mockImplementation(() => { throw new Error('Limit Error'); });
|
profilesLimitMock.mockImplementation(() => { throw new Error('Limit Error'); });
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,15 @@ import {
|
|||||||
useAuth
|
useAuth
|
||||||
} from './AuthContext';
|
} from './AuthContext';
|
||||||
|
|
||||||
|
vi.mock('../lib/tradingAuth', () => ({
|
||||||
|
TradingAuthProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||||
|
useTradingAuth: () => ({
|
||||||
|
user: null,
|
||||||
|
isLoading: false,
|
||||||
|
logout: vi.fn(),
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => {
|
vi.mock('../lib/supabaseClient', () => {
|
||||||
const query: any = {
|
const query: any = {
|
||||||
select: vi.fn(() => query),
|
select: vi.fn(() => query),
|
||||||
@ -21,9 +30,6 @@ vi.mock('../lib/supabaseClient', () => {
|
|||||||
supabase: {
|
supabase: {
|
||||||
auth: {
|
auth: {
|
||||||
getSession: vi.fn(async () => ({ data: { session: null } })),
|
getSession: vi.fn(async () => ({ data: { session: null } })),
|
||||||
onAuthStateChange: vi.fn(() => ({
|
|
||||||
data: { subscription: { unsubscribe: vi.fn() } }
|
|
||||||
})),
|
|
||||||
signOut: vi.fn(async () => ({ error: null }))
|
signOut: vi.fn(async () => ({ error: null }))
|
||||||
},
|
},
|
||||||
from: vi.fn(() => query)
|
from: vi.fn(() => query)
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react';
|
|||||||
import type { User, Session } from '@supabase/supabase-js';
|
import type { User, Session } from '@supabase/supabase-js';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
import { supabase } from '../lib/supabaseClient';
|
||||||
import { tableNameUsers, tableNameProfiles } from '../lib/const';
|
import { tableNameUsers, tableNameProfiles } from '../lib/const';
|
||||||
|
import { TradingAuthProvider, useTradingAuth } from '../lib/tradingAuth';
|
||||||
|
|
||||||
// Define the shape of our extended user profile
|
// Define the shape of our extended user profile
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
@ -75,39 +76,49 @@ export const buildDefaultProfilePayload = (userId: string) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<TradingAuthProvider>
|
||||||
|
<AuthBridge>{children}</AuthBridge>
|
||||||
|
</TradingAuthProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function AuthBridge({ children }: { children: React.ReactNode }) {
|
||||||
|
const tradingAuth = useTradingAuth();
|
||||||
const [session, setSession] = useState<Session | null>(null);
|
const [session, setSession] = useState<Session | null>(null);
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [profileLoading, setProfileLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 1. Get initial session
|
let active = true;
|
||||||
supabase.auth.getSession().then(({ data: { session } }) => {
|
const syncSession = async () => {
|
||||||
setSession((session as Session | null) ?? null);
|
if (!tradingAuth.user?.id) {
|
||||||
setUser((session?.user as User | null) ?? null);
|
if (!active) return;
|
||||||
if (session?.user) {
|
setSession(null);
|
||||||
fetchProfile(session.user.id);
|
setUser(null);
|
||||||
} else {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. Listen for changes
|
|
||||||
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
|
||||||
setSession((session as Session | null) ?? null);
|
|
||||||
setUser((session?.user as User | null) ?? null);
|
|
||||||
if (session?.user) {
|
|
||||||
fetchProfile(session.user.id);
|
|
||||||
} else {
|
|
||||||
setProfile(null);
|
setProfile(null);
|
||||||
setLoading(false);
|
setProfileLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
return () => subscription.unsubscribe();
|
const { data: { session: nextSession } } = await supabase.auth.getSession();
|
||||||
}, []);
|
if (!active) return;
|
||||||
|
const normalizedSession = (nextSession as Session | null) ?? null;
|
||||||
|
const normalizedUser = (normalizedSession?.user as User | null) ?? buildFallbackAuthUser(tradingAuth.user);
|
||||||
|
setSession(normalizedSession);
|
||||||
|
setUser(normalizedUser);
|
||||||
|
await fetchProfile(tradingAuth.user.id, normalizedUser);
|
||||||
|
};
|
||||||
|
|
||||||
const fetchProfile = async (userId: string) => {
|
void syncSession();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [tradingAuth.user?.id]);
|
||||||
|
|
||||||
|
const fetchProfile = async (userId: string, authUserOverride?: User | null) => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from(tableNameUsers)
|
.from(tableNameUsers)
|
||||||
@ -117,7 +128,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error('Error fetching user profile:', error);
|
console.error('Error fetching user profile:', error);
|
||||||
setProfile(buildFallbackProfile(user));
|
setProfile(buildFallbackProfile(authUserOverride ?? user));
|
||||||
ensureDefaultProfile(userId);
|
ensureDefaultProfile(userId);
|
||||||
} else {
|
} else {
|
||||||
setProfile(data as UserProfile);
|
setProfile(data as UserProfile);
|
||||||
@ -126,9 +137,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Unexpected error fetching profile:', err);
|
console.error('Unexpected error fetching profile:', err);
|
||||||
setProfile(buildFallbackProfile(user));
|
setProfile(buildFallbackProfile(authUserOverride ?? user));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setProfileLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -152,6 +163,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const signOut = async () => {
|
const signOut = async () => {
|
||||||
await supabase.auth.signOut();
|
await supabase.auth.signOut();
|
||||||
|
tradingAuth.logout();
|
||||||
setSession(null);
|
setSession(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setProfile(null);
|
setProfile(null);
|
||||||
@ -167,7 +179,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
session,
|
session,
|
||||||
user,
|
user,
|
||||||
profile,
|
profile,
|
||||||
loading,
|
loading: tradingAuth.isLoading || profileLoading,
|
||||||
signOut,
|
signOut,
|
||||||
refreshProfile
|
refreshProfile
|
||||||
};
|
};
|
||||||
@ -175,6 +187,21 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildFallbackAuthUser = (authUser: { id: string; email?: string; role?: string; name?: string; } | null): User | null => {
|
||||||
|
if (!authUser?.id) return null;
|
||||||
|
return {
|
||||||
|
id: authUser.id,
|
||||||
|
email: authUser.email || '',
|
||||||
|
aud: 'authenticated',
|
||||||
|
app_metadata: {},
|
||||||
|
user_metadata: {
|
||||||
|
role: authUser.role || 'member',
|
||||||
|
displayName: authUser.name || authUser.email || '',
|
||||||
|
},
|
||||||
|
created_at: new Date(0).toISOString(),
|
||||||
|
} as User;
|
||||||
|
};
|
||||||
|
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const context = useContext(AuthContext);
|
const context = useContext(AuthContext);
|
||||||
if (context === undefined) {
|
if (context === undefined) {
|
||||||
|
|||||||
@ -18,6 +18,14 @@ vi.mock('../components/AuthContext', () => ({
|
|||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock('../lib/tradingAuth', () => ({
|
||||||
|
useTradingAuth: () => ({
|
||||||
|
login: vi.fn(async () => true),
|
||||||
|
register: vi.fn(async () => true),
|
||||||
|
error: null,
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => {
|
vi.mock('../lib/supabaseClient', () => {
|
||||||
const query: any = {
|
const query: any = {
|
||||||
select: vi.fn(() => query),
|
select: vi.fn(() => query),
|
||||||
|
|||||||
@ -4,17 +4,26 @@ import { render, screen, waitFor } from '@testing-library/react';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { Login } from './Login';
|
import { Login } from './Login';
|
||||||
|
|
||||||
const { signInWithPasswordMock, signUpMock, resetPasswordForEmailMock } = vi.hoisted(() => ({
|
const { loginMock, registerMock, resetPasswordForEmailMock, tradingAuthState } = vi.hoisted(() => ({
|
||||||
signInWithPasswordMock: vi.fn(),
|
loginMock: vi.fn(),
|
||||||
signUpMock: vi.fn(),
|
registerMock: vi.fn(),
|
||||||
resetPasswordForEmailMock: vi.fn()
|
resetPasswordForEmailMock: vi.fn(),
|
||||||
|
tradingAuthState: {
|
||||||
|
error: null as string | null
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../lib/tradingAuth', () => ({
|
||||||
|
useTradingAuth: () => ({
|
||||||
|
login: loginMock,
|
||||||
|
register: registerMock,
|
||||||
|
error: tradingAuthState.error,
|
||||||
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock('../lib/supabaseClient', () => ({
|
vi.mock('../lib/supabaseClient', () => ({
|
||||||
supabase: {
|
supabase: {
|
||||||
auth: {
|
auth: {
|
||||||
signInWithPassword: signInWithPasswordMock,
|
|
||||||
signUp: signUpMock,
|
|
||||||
resetPasswordForEmail: resetPasswordForEmailMock
|
resetPasswordForEmail: resetPasswordForEmailMock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -22,16 +31,18 @@ vi.mock('../lib/supabaseClient', () => ({
|
|||||||
|
|
||||||
describe('Login DOM flow', () => {
|
describe('Login DOM flow', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
signInWithPasswordMock.mockReset();
|
loginMock.mockReset();
|
||||||
signUpMock.mockReset();
|
registerMock.mockReset();
|
||||||
resetPasswordForEmailMock.mockReset();
|
resetPasswordForEmailMock.mockReset();
|
||||||
signInWithPasswordMock.mockResolvedValue({ error: null });
|
tradingAuthState.error = null;
|
||||||
signUpMock.mockResolvedValue({ error: null });
|
loginMock.mockResolvedValue(true);
|
||||||
|
registerMock.mockResolvedValue(true);
|
||||||
resetPasswordForEmailMock.mockResolvedValue({ error: null });
|
resetPasswordForEmailMock.mockResolvedValue({ error: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('submits sign-in credentials and surfaces auth errors', async () => {
|
it('submits sign-in credentials and surfaces auth errors', async () => {
|
||||||
signInWithPasswordMock.mockResolvedValueOnce({ error: { message: 'Invalid login credentials' } });
|
tradingAuthState.error = 'Invalid login credentials';
|
||||||
|
loginMock.mockResolvedValueOnce(false);
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
render(<Login />);
|
render(<Login />);
|
||||||
@ -41,10 +52,7 @@ describe('Login DOM flow', () => {
|
|||||||
await user.click(screen.getByRole('button', { name: 'Sign In' }));
|
await user.click(screen.getByRole('button', { name: 'Sign In' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(signInWithPasswordMock).toHaveBeenCalledWith({
|
expect(loginMock).toHaveBeenCalledWith('user@demo.com', 'bad-password');
|
||||||
email: 'user@demo.com',
|
|
||||||
password: 'bad-password'
|
|
||||||
});
|
|
||||||
expect(screen.getByText('Invalid login credentials')).toBeInTheDocument();
|
expect(screen.getByText('Invalid login credentials')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -65,10 +73,7 @@ describe('Login DOM flow', () => {
|
|||||||
await user.click(screen.getByRole('button', { name: 'Sign Up' }));
|
await user.click(screen.getByRole('button', { name: 'Sign Up' }));
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(signUpMock).toHaveBeenCalledWith({
|
expect(registerMock).toHaveBeenCalledWith('new@demo.com', 'StrongPassword1!', 'new');
|
||||||
email: 'new@demo.com',
|
|
||||||
password: 'StrongPassword1!'
|
|
||||||
});
|
|
||||||
expect(screen.getByText('Check your email for the confirmation link!')).toBeInTheDocument();
|
expect(screen.getByText('Check your email for the confirmation link!')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { supabase } from '../lib/supabaseClient';
|
import { supabase } from '../lib/supabaseClient';
|
||||||
|
import { useTradingAuth } from '../lib/tradingAuth';
|
||||||
|
|
||||||
export function Login() {
|
export function Login() {
|
||||||
|
const tradingAuth = useTradingAuth();
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -24,18 +26,12 @@ export function Login() {
|
|||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
setMessage('Password reset link sent! Check your email.');
|
setMessage('Password reset link sent! Check your email.');
|
||||||
} else if (isSignUp) {
|
} else if (isSignUp) {
|
||||||
const { error } = await supabase.auth.signUp({
|
const ok = await tradingAuth.register(email, password, email.split('@')[0] || 'Trader');
|
||||||
email,
|
if (!ok) throw new Error(tradingAuth.error || 'Registration failed');
|
||||||
password,
|
|
||||||
});
|
|
||||||
if (error) throw error;
|
|
||||||
setMessage('Check your email for the confirmation link!');
|
setMessage('Check your email for the confirmation link!');
|
||||||
} else {
|
} else {
|
||||||
const { error } = await supabase.auth.signInWithPassword({
|
const ok = await tradingAuth.login(email, password);
|
||||||
email,
|
if (!ok) throw new Error(tradingAuth.error || 'Login failed');
|
||||||
password,
|
|
||||||
});
|
|
||||||
if (error) throw error;
|
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message);
|
setError(err.message);
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
import { createAuthProvider, type BaseUser } from '@bytelyst/react-auth';
|
import { createAuthProvider, type BaseUser } from '@bytelyst/react-auth';
|
||||||
import { getRuntimeEnvironment } from './runtime.js';
|
import { tradingRuntime } from './runtime';
|
||||||
|
|
||||||
export interface TradingAuthUser extends BaseUser {
|
export interface TradingAuthUser extends BaseUser {
|
||||||
id: string;
|
id: string;
|
||||||
plan?: string;
|
plan?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const runtime = getRuntimeEnvironment('web');
|
|
||||||
|
|
||||||
export const tradingWebAuth = createAuthProvider<TradingAuthUser>({
|
export const tradingWebAuth = createAuthProvider<TradingAuthUser>({
|
||||||
baseUrl: runtime.platformApiUrl,
|
baseUrl: tradingRuntime.platformApiUrl,
|
||||||
productId: runtime.productId,
|
productId: tradingRuntime.productId,
|
||||||
storagePrefix: 'invttrdg_web',
|
storagePrefix: 'invttrdg_web',
|
||||||
loginEndpoint: '/auth/login',
|
loginEndpoint: '/auth/login',
|
||||||
registerEndpoint: '/auth/register',
|
registerEndpoint: '/auth/register',
|
||||||
@ -18,7 +16,7 @@ export const tradingWebAuth = createAuthProvider<TradingAuthUser>({
|
|||||||
changePasswordEndpoint: '/auth/change-password',
|
changePasswordEndpoint: '/auth/change-password',
|
||||||
deleteAccountEndpoint: '/auth/account',
|
deleteAccountEndpoint: '/auth/account',
|
||||||
refreshEndpoint: '/auth/refresh',
|
refreshEndpoint: '/auth/refresh',
|
||||||
mapLoginResponse: data => {
|
mapLoginResponse: (data: unknown) => {
|
||||||
const response = data as {
|
const response = data as {
|
||||||
user: TradingAuthUser;
|
user: TradingAuthUser;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
@ -34,4 +32,3 @@ export const tradingWebAuth = createAuthProvider<TradingAuthUser>({
|
|||||||
|
|
||||||
export const TradingAuthProvider = tradingWebAuth.AuthProvider;
|
export const TradingAuthProvider = tradingWebAuth.AuthProvider;
|
||||||
export const useTradingAuth = tradingWebAuth.useAuth;
|
export const useTradingAuth = tradingWebAuth.useAuth;
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user