Skip to content

Commit 53f0266

Browse files
authored
Merge pull request #400 from Pvsaint/resumable-ingestion
Resumable ingestion
2 parents b0b0ded + 6347405 commit 53f0266

22 files changed

Lines changed: 1634 additions & 177 deletions

client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@
2121
"@stellar/stellar-sdk": "^14.4.3",
2222
"@supabase/supabase-js": "^2.76.1",
2323
"@tailwindcss/vite": "^4.1.13",
24+
"canvas-confetti": "^1.9.4",
2425
"i18next": "^26.0.7",
2526
"i18next-browser-languagedetector": "^8.2.1",
2627
"i18next-http-backend": "^3.0.6",
27-
"canvas-confetti": "^1.9.4",
2828
"lucide-react": "^0.544.0",
2929
"react": "^19.2.0",
3030
"react-dom": "^19.2.0",
@@ -49,6 +49,7 @@
4949
"eslint": "^9.33.0",
5050
"eslint-plugin-react-hooks": "^5.2.0",
5151
"eslint-plugin-react-refresh": "^0.4.20",
52+
"fast-check": "^4.7.0",
5253
"globals": "^16.3.0",
5354
"jsdom": "^22.1.0",
5455
"react-hook-form": "^7.72.0",

client/pnpm-lock.yaml

Lines changed: 422 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
/**
2+
* Tests for SignInButton component
3+
* Covers all 5 render states and the truncateAddress helper.
4+
*/
5+
6+
import { render, screen, fireEvent } from '@testing-library/react';
7+
import { vi } from 'vitest';
8+
import * as fc from 'fast-check';
9+
import SignInButton from './SignInButton';
10+
11+
// ── Mock providers ────────────────────────────────────────────────────────────
12+
13+
const mockLogin = vi.fn();
14+
const mockLogout = vi.fn();
15+
16+
let walletCtx = { isConnected: true, address: 'GABCDEFGHIJKLMNOP' as string | null };
17+
let authCtx = {
18+
isAuthenticated: false,
19+
isAuthenticating: false,
20+
error: null as string | null,
21+
login: mockLogin,
22+
logout: mockLogout,
23+
};
24+
25+
vi.mock('../providers/WalletProvider', () => ({
26+
useWalletContext: () => walletCtx,
27+
}));
28+
29+
vi.mock('../providers/AuthProvider', () => ({
30+
useAuthContext: () => authCtx,
31+
}));
32+
33+
// ── Helpers ───────────────────────────────────────────────────────────────────
34+
35+
function resetCtx() {
36+
walletCtx = { isConnected: true, address: 'GABCDEFGHIJKLMNOP' };
37+
authCtx = {
38+
isAuthenticated: false,
39+
isAuthenticating: false,
40+
error: null,
41+
login: mockLogin,
42+
logout: mockLogout,
43+
};
44+
mockLogin.mockReset();
45+
mockLogout.mockReset();
46+
}
47+
48+
// ── 5.1 Returns null when wallet not connected or address is null ──────────────
49+
50+
describe('5.1 - returns null when wallet not connected or address is null', () => {
51+
afterEach(resetCtx);
52+
53+
test('returns null when isConnected is false', () => {
54+
walletCtx = { isConnected: false, address: 'GABCDEFGHIJKLMNOP' };
55+
const { container } = render(<SignInButton />);
56+
expect(container.firstChild).toBeNull();
57+
});
58+
59+
test('returns null when address is null', () => {
60+
walletCtx = { isConnected: true, address: null };
61+
const { container } = render(<SignInButton />);
62+
expect(container.firstChild).toBeNull();
63+
});
64+
65+
test('returns null when both isConnected is false and address is null', () => {
66+
walletCtx = { isConnected: false, address: null };
67+
const { container } = render(<SignInButton />);
68+
expect(container.firstChild).toBeNull();
69+
});
70+
});
71+
72+
// ── 5.2 Renders "Sign In" button in default connected-unauthenticated state ───
73+
74+
describe('5.2 - renders Sign In button in default connected-unauthenticated state', () => {
75+
afterEach(resetCtx);
76+
77+
test('renders a button labelled Sign In', () => {
78+
render(<SignInButton />);
79+
expect(screen.getByRole('button', { name: /sign in/i })).toBeInTheDocument();
80+
});
81+
82+
test('Sign In button is not disabled', () => {
83+
render(<SignInButton />);
84+
expect(screen.getByRole('button', { name: /sign in/i })).not.toBeDisabled();
85+
});
86+
});
87+
88+
// ── 5.3 Renders disabled "Signing in..." button when isAuthenticating=true ───
89+
90+
describe('5.3 - renders disabled Signing in button when isAuthenticating=true', () => {
91+
afterEach(resetCtx);
92+
93+
test('renders a disabled button labelled Signing in...', () => {
94+
authCtx = { ...authCtx, isAuthenticating: true };
95+
render(<SignInButton />);
96+
const btn = screen.getByRole('button', { name: /signing in/i });
97+
expect(btn).toBeInTheDocument();
98+
expect(btn).toBeDisabled();
99+
});
100+
});
101+
102+
// ── 5.4 Renders "Signed in as G...XXXX" with truncated address ───────────────
103+
104+
describe('5.4 - renders Signed in as truncated when authenticated', () => {
105+
afterEach(resetCtx);
106+
107+
test('shows truncated address in button text', () => {
108+
const addr = 'GABCDEFGHIJKLMNOP';
109+
walletCtx = { isConnected: true, address: addr };
110+
authCtx = { ...authCtx, isAuthenticated: true };
111+
render(<SignInButton />);
112+
// truncated: GABC...MNOP
113+
expect(screen.getByRole('button', { name: /GABC\.\.\.MNOP/i })).toBeInTheDocument();
114+
});
115+
});
116+
117+
// ── 5.5 Clicking authenticated button calls logout() ─────────────────────────
118+
119+
describe('5.5 - clicking authenticated button calls logout', () => {
120+
afterEach(resetCtx);
121+
122+
test('calls logout when authenticated button is clicked', () => {
123+
walletCtx = { isConnected: true, address: 'GABCDEFGHIJKLMNOP' };
124+
authCtx = { ...authCtx, isAuthenticated: true };
125+
render(<SignInButton />);
126+
fireEvent.click(screen.getByRole('button', { name: /signed in as/i }));
127+
expect(mockLogout).toHaveBeenCalledTimes(1);
128+
});
129+
});
130+
131+
// ── 5.6 Renders error indicator when error is non-null and not authenticating ─
132+
133+
describe('5.6 - renders error indicator when error is non-null and not authenticating', () => {
134+
afterEach(resetCtx);
135+
136+
test('renders error indicator for non-null error', () => {
137+
authCtx = { ...authCtx, error: 'Something went wrong', isAuthenticating: false };
138+
render(<SignInButton />);
139+
expect(screen.getByText(/auth error/i)).toBeInTheDocument();
140+
});
141+
142+
test('does NOT render error indicator when isAuthenticating is true', () => {
143+
authCtx = { ...authCtx, error: 'Something went wrong', isAuthenticating: true };
144+
render(<SignInButton />);
145+
expect(screen.queryByText(/auth error/i)).not.toBeInTheDocument();
146+
expect(screen.getByRole('button', { name: /signing in/i })).toBeInTheDocument();
147+
});
148+
});
149+
150+
// ── 5.7 truncateAddress returns addr.slice(0,4) + "..." + addr.slice(-4) ─────
151+
152+
describe('5.7 - truncateAddress helper', () => {
153+
afterEach(resetCtx);
154+
155+
const cases: [string, string][] = [
156+
['GABCDEFGHIJKLMNOP', 'GABC...MNOP'],
157+
['GABCDEFGHI', 'GABC...FGHI'],
158+
['123456789', '1234...6789'],
159+
];
160+
161+
test.each(cases)('truncates %s to %s', (addr: string, expected: string) => {
162+
walletCtx = { isConnected: true, address: addr };
163+
authCtx = { ...authCtx, isAuthenticated: true };
164+
render(<SignInButton />);
165+
const button = screen.getByRole('button', { name: /signed in as/i });
166+
expect(button.textContent).toContain(expected);
167+
});
168+
169+
test('does not truncate addresses of 8 chars or fewer', () => {
170+
const addr = '12345678';
171+
walletCtx = { isConnected: true, address: addr };
172+
authCtx = { ...authCtx, isAuthenticated: true };
173+
render(<SignInButton />);
174+
const button = screen.getByRole('button', { name: /signed in as/i });
175+
expect(button.textContent).toContain('12345678');
176+
expect(button.textContent).not.toContain('...');
177+
});
178+
});
179+
180+
// ── P13: Address truncation (property-based) ──────────────────────────────────
181+
182+
describe('P13: Address truncation property', () => {
183+
// Feature: siws-auth, Property 13: For any Stellar address string longer than 8 characters,
184+
// truncateAddress(address) must return address.slice(0, 4) + "..." + address.slice(-4).
185+
afterEach(resetCtx);
186+
187+
it('truncates any address longer than 8 chars to first4...last4', () => {
188+
fc.assert(
189+
fc.property(fc.string({ minLength: 9 }), (address) => {
190+
walletCtx = { isConnected: true, address };
191+
authCtx = { ...authCtx, isAuthenticated: true };
192+
const { container, unmount } = render(<SignInButton />);
193+
194+
const first4 = address.slice(0, 4);
195+
const last4 = address.slice(-4);
196+
const expected = first4 + '...' + last4;
197+
198+
// Use textContent to avoid accessible-name whitespace collapsing
199+
const button = container.querySelector('button');
200+
expect(button).not.toBeNull();
201+
expect(button!.textContent).toContain(expected);
202+
203+
unmount();
204+
}),
205+
{ numRuns: 100 },
206+
);
207+
});
208+
});

0 commit comments

Comments
 (0)