Stop substituting synthetic market data when providers fail
- Replace synthetic fallback in getQuote()/getPriceHistory() with null returns
- Add QuoteResult/PriceHistoryResult types with { value, stale } structure
- Implement stale-while-revalidate: return cached value with stale=true on live fetch failure
- Cache failures for 30s to avoid hammering provider
- Update CompanyAnalysis type to use PriceData<T> wrapper
- Update task-processors to track failed/stale tickers explicitly
- Update price-history-card UI to show unavailable state and stale indicator
- Add comprehensive tests for failure cases
- Add e2e tests for null data, stale data, and live data scenarios
Resolves #14
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { expect, test, type Page, type TestInfo } from '@playwright/test';
|
||||
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
const PASSWORD = 'Sup3rSecure!123';
|
||||
|
||||
function toSlug(value: string) {
|
||||
@@ -23,6 +25,104 @@ async function signUp(page: Page, testInfo: TestInfo) {
|
||||
await expect(page).toHaveURL(/\/$/, { timeout: 30_000 });
|
||||
}
|
||||
|
||||
function buildMockAnalysisPayload(overrides: {
|
||||
ticker?: string;
|
||||
companyName?: string;
|
||||
quote?: { value: number | null; stale: boolean };
|
||||
priceHistory?: { value: Array<{ date: string; close: number }> | null; stale: boolean };
|
||||
benchmarkHistory?: { value: Array<{ date: string; close: number }> | null; stale: boolean };
|
||||
} = {}) {
|
||||
return {
|
||||
company: {
|
||||
ticker: overrides.ticker ?? 'MSFT',
|
||||
companyName: overrides.companyName ?? 'Microsoft Corporation',
|
||||
sector: 'Technology',
|
||||
category: null,
|
||||
tags: [],
|
||||
cik: '0000789019'
|
||||
},
|
||||
quote: overrides.quote ?? { value: 425.12, stale: false },
|
||||
position: null,
|
||||
priceHistory: overrides.priceHistory ?? {
|
||||
value: [
|
||||
{ date: '2025-01-01T00:00:00.000Z', close: 380 },
|
||||
{ date: '2026-01-01T00:00:00.000Z', close: 425.12 }
|
||||
],
|
||||
stale: false
|
||||
},
|
||||
benchmarkHistory: overrides.benchmarkHistory ?? {
|
||||
value: [
|
||||
{ date: '2025-01-01T00:00:00.000Z', close: 5000 },
|
||||
{ date: '2026-01-01T00:00:00.000Z', close: 5400 }
|
||||
],
|
||||
stale: false
|
||||
},
|
||||
financials: [],
|
||||
filings: [],
|
||||
aiReports: [],
|
||||
coverage: null,
|
||||
journalPreview: [],
|
||||
recentAiReports: [],
|
||||
latestFilingSummary: null,
|
||||
keyMetrics: {
|
||||
referenceDate: null,
|
||||
revenue: null,
|
||||
netIncome: null,
|
||||
totalAssets: null,
|
||||
cash: null,
|
||||
debt: null,
|
||||
netMargin: null
|
||||
},
|
||||
companyProfile: {
|
||||
description: 'Microsoft builds cloud and software products worldwide.',
|
||||
exchange: 'NASDAQ',
|
||||
industry: 'Software',
|
||||
country: 'United States',
|
||||
website: 'https://www.microsoft.com',
|
||||
fiscalYearEnd: '06/30',
|
||||
employeeCount: 220000,
|
||||
source: 'sec_derived'
|
||||
},
|
||||
valuationSnapshot: {
|
||||
sharesOutstanding: 7430000000,
|
||||
marketCap: 3150000000000,
|
||||
enterpriseValue: 3200000000000,
|
||||
trailingPe: 35,
|
||||
evToRevenue: 12,
|
||||
evToEbitda: null,
|
||||
source: 'derived'
|
||||
},
|
||||
bullBear: {
|
||||
source: 'memo_fallback',
|
||||
bull: ['Azure and Copilot demand remain durable.'],
|
||||
bear: ['Valuation leaves less room for execution misses.'],
|
||||
updatedAt: '2026-03-13T00:00:00.000Z'
|
||||
},
|
||||
recentDevelopments: {
|
||||
status: 'ready',
|
||||
items: [{
|
||||
id: 'msft-1',
|
||||
kind: '8-K',
|
||||
title: 'Microsoft filed an 8-K',
|
||||
url: 'https://www.sec.gov/Archives/test.htm',
|
||||
source: 'SEC filings',
|
||||
publishedAt: '2026-03-10',
|
||||
summary: 'The company disclosed a current report with updated commercial details.',
|
||||
accessionNumber: '0000000000-26-000001'
|
||||
}],
|
||||
weeklySnapshot: {
|
||||
summary: 'The week centered on filing-driven updates.',
|
||||
highlights: ['An 8-K added current commercial context.'],
|
||||
itemCount: 1,
|
||||
startDate: '2026-03-07',
|
||||
endDate: '2026-03-13',
|
||||
updatedAt: '2026-03-13T00:00:00.000Z',
|
||||
source: 'heuristic'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
test('shows the overview skeleton while analysis is loading', async ({ page }, testInfo) => {
|
||||
await signUp(page, testInfo);
|
||||
|
||||
@@ -32,89 +132,7 @@ test('shows the overview skeleton while analysis is loading', async ({ page }, t
|
||||
await route.fulfill({
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
analysis: {
|
||||
company: {
|
||||
ticker: 'MSFT',
|
||||
companyName: 'Microsoft Corporation',
|
||||
sector: 'Technology',
|
||||
category: null,
|
||||
tags: [],
|
||||
cik: '0000789019'
|
||||
},
|
||||
quote: 425.12,
|
||||
position: null,
|
||||
priceHistory: [
|
||||
{ date: '2025-01-01T00:00:00.000Z', close: 380 },
|
||||
{ date: '2026-01-01T00:00:00.000Z', close: 425.12 }
|
||||
],
|
||||
benchmarkHistory: [
|
||||
{ date: '2025-01-01T00:00:00.000Z', close: 5000 },
|
||||
{ date: '2026-01-01T00:00:00.000Z', close: 5400 }
|
||||
],
|
||||
financials: [],
|
||||
filings: [],
|
||||
aiReports: [],
|
||||
coverage: null,
|
||||
journalPreview: [],
|
||||
recentAiReports: [],
|
||||
latestFilingSummary: null,
|
||||
keyMetrics: {
|
||||
referenceDate: null,
|
||||
revenue: null,
|
||||
netIncome: null,
|
||||
totalAssets: null,
|
||||
cash: null,
|
||||
debt: null,
|
||||
netMargin: null
|
||||
},
|
||||
companyProfile: {
|
||||
description: 'Microsoft builds cloud and software products worldwide.',
|
||||
exchange: 'NASDAQ',
|
||||
industry: 'Software',
|
||||
country: 'United States',
|
||||
website: 'https://www.microsoft.com',
|
||||
fiscalYearEnd: '06/30',
|
||||
employeeCount: 220000,
|
||||
source: 'sec_derived'
|
||||
},
|
||||
valuationSnapshot: {
|
||||
sharesOutstanding: 7430000000,
|
||||
marketCap: 3150000000000,
|
||||
enterpriseValue: 3200000000000,
|
||||
trailingPe: 35,
|
||||
evToRevenue: 12,
|
||||
evToEbitda: null,
|
||||
source: 'derived'
|
||||
},
|
||||
bullBear: {
|
||||
source: 'memo_fallback',
|
||||
bull: ['Azure and Copilot demand remain durable.'],
|
||||
bear: ['Valuation leaves less room for execution misses.'],
|
||||
updatedAt: '2026-03-13T00:00:00.000Z'
|
||||
},
|
||||
recentDevelopments: {
|
||||
status: 'ready',
|
||||
items: [{
|
||||
id: 'msft-1',
|
||||
kind: '8-K',
|
||||
title: 'Microsoft filed an 8-K',
|
||||
url: 'https://www.sec.gov/Archives/test.htm',
|
||||
source: 'SEC filings',
|
||||
publishedAt: '2026-03-10',
|
||||
summary: 'The company disclosed a current report with updated commercial details.',
|
||||
accessionNumber: '0000000000-26-000001'
|
||||
}],
|
||||
weeklySnapshot: {
|
||||
summary: 'The week centered on filing-driven updates.',
|
||||
highlights: ['An 8-K added current commercial context.'],
|
||||
itemCount: 1,
|
||||
startDate: '2026-03-07',
|
||||
endDate: '2026-03-13',
|
||||
updatedAt: '2026-03-13T00:00:00.000Z',
|
||||
source: 'heuristic'
|
||||
}
|
||||
}
|
||||
}
|
||||
analysis: buildMockAnalysisPayload()
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -125,3 +143,94 @@ test('shows the overview skeleton while analysis is loading', async ({ page }, t
|
||||
await expect(page.getByRole('heading', { name: 'Microsoft Corporation' })).toBeVisible();
|
||||
await expect(page.getByText('Bull vs Bear')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows price chart with live data when quote and history are available', async ({ page }, testInfo) => {
|
||||
await signUp(page, testInfo);
|
||||
|
||||
await page.route('**/api/analysis/company**', async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
analysis: buildMockAnalysisPayload()
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/analysis?ticker=MSFT');
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Microsoft Corporation' })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByText('Spot price $425.12')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="interactive-price-chart"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows unavailable message when price data is null', async ({ page }, testInfo) => {
|
||||
await signUp(page, testInfo);
|
||||
|
||||
await page.route('**/api/analysis/company**', async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
analysis: buildMockAnalysisPayload({
|
||||
quote: { value: null, stale: false },
|
||||
priceHistory: { value: null, stale: false },
|
||||
benchmarkHistory: { value: null, stale: false }
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/analysis?ticker=FAIL');
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Microsoft Corporation' })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByText('Spot price unavailable')).toBeVisible();
|
||||
await expect(page.getByText('Price data unavailable')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="interactive-price-chart"]')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('shows stale indicator when quote data is stale', async ({ page }, testInfo) => {
|
||||
await signUp(page, testInfo);
|
||||
|
||||
await page.route('**/api/analysis/company**', async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
analysis: buildMockAnalysisPayload({
|
||||
quote: { value: 425.12, stale: true },
|
||||
priceHistory: {
|
||||
value: [
|
||||
{ date: '2025-01-01T00:00:00.000Z', close: 380 },
|
||||
{ date: '2026-01-01T00:00:00.000Z', close: 425.12 }
|
||||
],
|
||||
stale: true
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/analysis?ticker=STALE');
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Microsoft Corporation' })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByText(/Spot price.*\(stale\)/)).toBeVisible();
|
||||
await expect(page.locator('[data-testid="interactive-price-chart"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows chart when price history is available but benchmark is null', async ({ page }, testInfo) => {
|
||||
await signUp(page, testInfo);
|
||||
|
||||
await page.route('**/api/analysis/company**', async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify({
|
||||
analysis: buildMockAnalysisPayload({
|
||||
benchmarkHistory: { value: null, stale: false }
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/analysis?ticker=MSFT');
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'Microsoft Corporation' })).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.locator('[data-testid="interactive-price-chart"]')).toBeVisible();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user