Skip to content

Commit bbe3742

Browse files
committed
fix: preserve aggregate provider failure context (#163)
1 parent e57e2bb commit bbe3742

5 files changed

Lines changed: 98 additions & 10 deletions

File tree

server/src/routes/provider.routes.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,18 @@ router.post('/:provider/check-session', async (req: Request, res: Response) => {
125125
* Check sessions for all enabled providers
126126
*/
127127
router.post('/check-all-sessions', asyncHandler(async (_req: Request, res: Response) => {
128-
const statuses = await providerService.checkAllSessions();
129-
res.json({ success: true, data: statuses });
128+
const summary = await providerService.checkAllSessions();
129+
const failedProviders = Object.keys(summary.failures);
130+
if (failedProviders.length > 0) {
131+
res.status(503).json({
132+
success: false,
133+
error: `Failed to check ${failedProviders.length} provider session${failedProviders.length === 1 ? '' : 's'}`,
134+
details: summary
135+
});
136+
return;
137+
}
138+
139+
res.json({ success: true, data: summary.statuses });
130140
}));
131141

132142
/**

server/src/services/provider.service.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type {
88
ProviderTreeInfo,
99
ProviderOperation,
1010
ProviderOperationResult,
11+
ProviderSessionCheckSummary,
1112
EnsureAuthResult
1213
} from '@fsf/shared';
1314
import { browserService, isFamilySearchAuthUrl } from './browser.service.js';
@@ -356,19 +357,21 @@ export const providerService = {
356357
/**
357358
* Check session status for all enabled providers
358359
*/
359-
async checkAllSessions(): Promise<Record<BuiltInProvider, ProviderSessionStatus>> {
360-
const registry = loadRegistry();
361-
const results: Record<BuiltInProvider, ProviderSessionStatus> = {} as Record<BuiltInProvider, ProviderSessionStatus>;
360+
async checkAllSessions(): Promise<ProviderSessionCheckSummary> {
361+
const registry = this.getAllConfigs();
362+
const statuses: ProviderSessionCheckSummary['statuses'] = {};
363+
const failures: ProviderSessionCheckSummary['failures'] = {};
362364

363365
for (const provider of listProviders()) {
364366
if (registry.providers[provider].enabled) {
365367
const result = await this.checkSession(provider);
366-
if (!result.success) {
367-
throw new Error(result.error.message);
368+
if (result.success) {
369+
statuses[provider] = result.data;
370+
} else {
371+
failures[provider] = result.error;
368372
}
369-
results[provider] = result.data;
370373
} else {
371-
results[provider] = {
374+
statuses[provider] = {
372375
provider,
373376
enabled: false,
374377
loggedIn: false,
@@ -377,7 +380,7 @@ export const providerService = {
377380
}
378381
}
379382

380-
return results;
383+
return { statuses, failures };
381384
},
382385

383386
/**

shared/src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ export type ProviderOperationResult<T> =
202202
| { success: true; data: T }
203203
| { success: false; error: ProviderOperationFailure };
204204

205+
export interface ProviderSessionCheckSummary {
206+
statuses: Partial<Record<BuiltInProvider, ProviderSessionStatus>>;
207+
failures: Partial<Record<BuiltInProvider, ProviderOperationFailure>>;
208+
}
209+
205210
// Auto-login method type
206211
export type AutoLoginMethod = 'credentials' | 'google';
207212

tests/integration/api/providers.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
55
const mocks = vi.hoisted(() => ({
66
providerService: {
77
checkSession: vi.fn(),
8+
checkAllSessions: vi.fn(),
89
discoverTrees: vi.fn(),
910
},
1011
}));
@@ -89,6 +90,47 @@ describe('Provider routes', () => {
8990
});
9091
});
9192

93+
it('returns successful statuses and structured failures from aggregate checks', async () => {
94+
mocks.providerService.checkAllSessions.mockResolvedValue({
95+
statuses: {
96+
ancestry: {
97+
provider: 'ancestry',
98+
enabled: true,
99+
loggedIn: true,
100+
},
101+
},
102+
failures: {
103+
familysearch: failure('check-session', 'CDP refused connection').error,
104+
},
105+
});
106+
107+
const response = await request(app)
108+
.post('/api/scrape-providers/check-all-sessions')
109+
.expect(503);
110+
111+
expect(response.body).toEqual({
112+
success: false,
113+
error: 'Failed to check 1 provider session',
114+
details: {
115+
statuses: {
116+
ancestry: {
117+
provider: 'ancestry',
118+
enabled: true,
119+
loggedIn: true,
120+
},
121+
},
122+
failures: {
123+
familysearch: {
124+
code: 'PROVIDER_OPERATION_FAILED',
125+
provider: 'familysearch',
126+
operation: 'check-session',
127+
message: 'CDP refused connection',
128+
},
129+
},
130+
},
131+
});
132+
});
133+
92134
it('returns an empty tree list only when discovery succeeded', async () => {
93135
mocks.providerService.discoverTrees.mockResolvedValue({ success: true, data: [] });
94136

tests/unit/services/providerService.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@ describe('providerService operational failures', () => {
5050
browserScrapeEnabled: true,
5151
browserLoggedIn: false,
5252
});
53+
vi.spyOn(providerService, 'getAllConfigs').mockReturnValue({
54+
providers: {
55+
familysearch: {
56+
provider: 'familysearch',
57+
enabled: true,
58+
rateLimit: { minDelayMs: 1000, maxDelayMs: 2000 },
59+
browserScrapeEnabled: true,
60+
browserLoggedIn: false,
61+
},
62+
},
63+
lastUpdated: '2026-08-30T00:00:00.000Z',
64+
} as ReturnType<typeof providerService.getAllConfigs>);
5365
mocks.browserService.isConnected.mockReturnValue(true);
5466
mocks.browserService.getWorkerPage.mockResolvedValue({});
5567
mocks.scraper.checkLoginStatus.mockResolvedValue(false);
@@ -111,6 +123,22 @@ describe('providerService operational failures', () => {
111123
);
112124
});
113125

126+
it('preserves successful statuses and structured failures in aggregate checks', async () => {
127+
mocks.scraper.checkLoginStatus.mockRejectedValue(new Error('Login markup changed'));
128+
129+
const summary = await providerService.checkAllSessions();
130+
131+
expect(summary.statuses).toEqual({});
132+
expect(summary.failures).toEqual({
133+
familysearch: {
134+
code: 'PROVIDER_OPERATION_FAILED',
135+
provider: 'familysearch',
136+
operation: 'check-session',
137+
message: 'Login markup changed',
138+
},
139+
});
140+
});
141+
114142
it('keeps a verified empty tree list as a successful negative result', async () => {
115143
const result = await providerService.discoverTrees('familysearch');
116144

0 commit comments

Comments
 (0)