From b26021016cce9e01d5b918a1738880f009eb3313 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Tue, 23 Jun 2026 21:22:37 +0300 Subject: [PATCH] fix(frontend): clean up mock setup and route params --- .../{src/shared/lib/test => mocks}/browser.ts | 0 apps/frontend/mocks/data/auth.ts | 11 + apps/frontend/mocks/data/bonds.ts | 40 ++++ apps/frontend/mocks/data/broker.ts | 189 +++++++++++++++++ apps/frontend/mocks/data/candles.ts | 22 ++ apps/frontend/mocks/data/index.ts | 15 ++ apps/frontend/mocks/data/portfolios.ts | 138 +++++++++++++ apps/frontend/mocks/data/screener.ts | 38 ++++ apps/frontend/mocks/data/search.ts | 20 ++ apps/frontend/mocks/data/shares.ts | 50 +++++ apps/frontend/mocks/handlers.ts | 151 ++++++++++++++ .../{src/shared/lib/test => mocks}/server.ts | 0 apps/frontend/mocks/utils.ts | 5 + .../app/providers/SessionProvider.test.tsx | 2 +- .../bond/model/useBondCandles.test.tsx | 2 +- .../entities/search/model/useSearch.test.tsx | 2 +- .../entities/session/api/sessionApi.test.ts | 2 +- .../stock/model/useStockCandles.test.tsx | 2 +- .../stock/model/useStockDividends.test.tsx | 2 +- apps/frontend/src/main.tsx | 2 +- .../src/pages/login/LoginPage.test.tsx | 2 +- .../portfolios/ui/PortfolioDetailPage.tsx | 2 +- .../src/pages/profile/ProfilePage.test.tsx | 2 +- .../src/pages/register/RegisterPage.test.tsx | 2 +- .../frontend/src/pages/stock/ui/StockPage.tsx | 2 +- apps/frontend/src/shared/lib/test/handlers.ts | 190 ------------------ apps/frontend/src/shared/lib/test/setup.ts | 2 +- .../widgets/search-bar/ui/SearchBar.test.tsx | 2 +- apps/frontend/tsconfig.json | 5 +- apps/frontend/vite.config.ts | 7 +- apps/frontend/vitest.config.ts | 7 +- 31 files changed, 704 insertions(+), 212 deletions(-) rename apps/frontend/{src/shared/lib/test => mocks}/browser.ts (100%) create mode 100644 apps/frontend/mocks/data/auth.ts create mode 100644 apps/frontend/mocks/data/bonds.ts create mode 100644 apps/frontend/mocks/data/broker.ts create mode 100644 apps/frontend/mocks/data/candles.ts create mode 100644 apps/frontend/mocks/data/index.ts create mode 100644 apps/frontend/mocks/data/portfolios.ts create mode 100644 apps/frontend/mocks/data/screener.ts create mode 100644 apps/frontend/mocks/data/search.ts create mode 100644 apps/frontend/mocks/data/shares.ts create mode 100644 apps/frontend/mocks/handlers.ts rename apps/frontend/{src/shared/lib/test => mocks}/server.ts (100%) create mode 100644 apps/frontend/mocks/utils.ts delete mode 100644 apps/frontend/src/shared/lib/test/handlers.ts diff --git a/apps/frontend/src/shared/lib/test/browser.ts b/apps/frontend/mocks/browser.ts similarity index 100% rename from apps/frontend/src/shared/lib/test/browser.ts rename to apps/frontend/mocks/browser.ts diff --git a/apps/frontend/mocks/data/auth.ts b/apps/frontend/mocks/data/auth.ts new file mode 100644 index 0000000..dcdfe37 --- /dev/null +++ b/apps/frontend/mocks/data/auth.ts @@ -0,0 +1,11 @@ +export const mockUser = { + id: 1, + email: 'user@test.com', + name: 'Test User', + role: 'user', +} + +export const mockAuthResponse = { + user: mockUser, + accessToken: 'mock-access-token', +} diff --git a/apps/frontend/mocks/data/bonds.ts b/apps/frontend/mocks/data/bonds.ts new file mode 100644 index 0000000..49e6944 --- /dev/null +++ b/apps/frontend/mocks/data/bonds.ts @@ -0,0 +1,40 @@ +export const mockBond = { + secid: 'SU26238RMFS5', + isin: 'RU000A101XU7', + name: 'ОФЗ 26238', + shortName: 'ОФЗ 26238', + latName: null, + listLevel: 1, + issueSize: 500000000, + faceValue: 1000, + faceUnit: 'RUB', + matDate: '2027-05-15', + couponValue: 36.9, + couponPercent: 7.5, + couponPeriod: 182, + nextCoupon: '2024-07-15', + accruedInt: 8.45, + bondType: 'ОФЗ', + bondSubType: 'ОФЗ-ПД', + offerDate: null, + buybackDate: null, + marketData: { + price: 98.5, + yieldToMaturity: 8.2, + duration: 3.5, + accruedInt: 8.45, + couponValue: 36.9, + couponPercent: 7.5, + nextCouponDate: '2024-07-15', + open: 98.0, + high: 99.0, + low: 97.5, + volume: 1000000, + updatedAt: '2024-01-15T10:00:00Z', + }, +} + +export const mockBondHistory = [ + { date: '2024-01-15', closePrice: 98.5, yieldClose: 8.2, duration: 3.5 }, + { date: '2024-01-14', closePrice: 98.2, yieldClose: 8.3, duration: 3.5 }, +] diff --git a/apps/frontend/mocks/data/broker.ts b/apps/frontend/mocks/data/broker.ts new file mode 100644 index 0000000..b23f8c3 --- /dev/null +++ b/apps/frontend/mocks/data/broker.ts @@ -0,0 +1,189 @@ +export const mockBrokerAccounts = [ + { + id: '2084014113', + type: 'brokerage', + name: 'Т-Инвестиции', + status: 'open', + openedAt: null, + accessLevel: null, + }, +] + +export const mockBrokerPortfolio = { + account: mockBrokerAccounts[0], + positionCounts: { shares: 3, bonds: 2, etf: 0, other: 0 }, + totals: { + shares: { currency: 'RUB', units: '240000', nano: 500000000, value: 240000.5 }, + bonds: { currency: 'RUB', units: '45000', nano: 0, value: 45000 }, + etf: null, + currencies: null, + futures: null, + options: null, + structuredProducts: null, + dfa: null, + portfolio: { currency: 'RUB', units: '285000', nano: 500000000, value: 285000.5 }, + }, + yields: { + expectedPercent: null, + daily: { currency: 'RUB', units: '1200', nano: 0, value: 1200 }, + dailyPercent: null, + }, + cash: [{ currency: 'RUB', units: '15000', nano: 0, value: 15000 }], + blockedCash: [], + asOf: '2024-06-01T10:00:00Z', +} + +export const mockBrokerPositions = [ + { + figi: null, + instrumentUid: null, + positionUid: null, + ticker: 'SBER', + classCode: null, + instrumentType: 'share', + name: 'Сбер Банк', + quantity: { currency: '', units: '100', nano: 0, value: 100 }, + blockedLots: null, + currentPrice: { currency: 'RUB', units: '289', nano: 500000000, value: 289.5 }, + currentValue: { currency: 'RUB', units: '28950', nano: 0, value: 28950 }, + averagePositionPrice: null, + expectedYieldPercent: null, + dailyYield: null, + }, + { + figi: null, + instrumentUid: null, + positionUid: null, + ticker: 'VTBR', + classCode: null, + instrumentType: 'share', + name: 'ВТБ', + quantity: { currency: '', units: '5000', nano: 0, value: 5000 }, + blockedLots: null, + currentPrice: { currency: 'RUB', units: '0', nano: 23400000, value: 0.0234 }, + currentValue: { currency: 'RUB', units: '117', nano: 0, value: 117 }, + averagePositionPrice: null, + expectedYieldPercent: null, + dailyYield: null, + }, + { + figi: null, + instrumentUid: null, + positionUid: null, + ticker: 'SU26238RMFS5', + classCode: null, + instrumentType: 'bond', + name: 'ОФЗ 26238', + quantity: { currency: '', units: '10', nano: 0, value: 10 }, + blockedLots: null, + currentPrice: { currency: 'RUB', units: '985', nano: 0, value: 985 }, + currentValue: { currency: 'RUB', units: '9850', nano: 0, value: 9850 }, + averagePositionPrice: null, + expectedYieldPercent: null, + dailyYield: null, + }, +] + +export const mockBrokerOperations = [ + { + cursor: null, + accountId: '2084014113', + id: 'op-1', + parentOperationId: null, + date: '2024-06-01T10:00:00Z', + type: 'buy', + category: 'trade', + description: null, + name: 'Покупка SBER', + state: 'executed', + instrumentUid: null, + figi: null, + ticker: 'SBER', + classCode: null, + instrumentType: 'share', + payment: { currency: 'RUB', units: '-27500', nano: 0, value: -27500 }, + price: { currency: 'RUB', units: '275', nano: 0, value: 275 }, + commission: { currency: 'RUB', units: '-55', nano: 0, value: -55 }, + yield: null, + accruedInt: null, + quantity: { currency: '', units: '100', nano: 0, value: 100 }, + quantityDone: { currency: '', units: '100', nano: 0, value: 100 }, + }, + { + cursor: null, + accountId: '2084014113', + id: 'op-2', + parentOperationId: null, + date: '2024-05-20T14:30:00Z', + type: 'dividend', + category: 'income', + description: null, + name: 'Дивиденды Сбер', + state: 'executed', + instrumentUid: null, + figi: null, + ticker: 'SBER', + classCode: null, + instrumentType: 'share', + payment: { currency: 'RUB', units: '3500', nano: 0, value: 3500 }, + price: null, + commission: null, + yield: null, + accruedInt: null, + quantity: null, + quantityDone: null, + }, +] + +export const mockBrokerEvents = [ + { + id: 'ev-1', + type: 'dividend', + source: 'forecast', + category: 'cashflow', + eventDate: '2024-07-10', + paymentDate: null, + ticker: 'SBER', + name: 'Сбер Банк', + instrumentUid: 'uid-sber', + instrumentType: 'share', + quantitySnapshot: 100, + payoutPerUnit: 35, + estimatedAmount: 3500, + actualAmount: null, + currency: 'RUB', + estimateMode: 'current_position', + }, + { + id: 'ev-2', + type: 'coupon', + source: 'forecast', + category: 'cashflow', + eventDate: '2024-07-15', + paymentDate: null, + ticker: 'SU26238RMFS5', + name: 'ОФЗ 26238', + instrumentUid: 'uid-bond', + instrumentType: 'bond', + quantitySnapshot: 10, + payoutPerUnit: 36.9, + estimatedAmount: 369, + actualAmount: null, + currency: 'RUB', + estimateMode: 'current_position', + }, +] + +export const mockEventsSummary = { + eventCount: 2, + nearestEventDate: '2024-07-10', + totalEstimatedCashflow: 3869, + actualCashflow: 0, + forecastEstimatedCashflow: 3869, + dividendsTotal: 3500, + couponsTotal: 369, + principalRepaymentTotal: 0, + actualDividendsTotal: 0, + actualCouponsTotal: 0, + actualPrincipalRepaymentTotal: 0, +} diff --git a/apps/frontend/mocks/data/candles.ts b/apps/frontend/mocks/data/candles.ts new file mode 100644 index 0000000..124a48f --- /dev/null +++ b/apps/frontend/mocks/data/candles.ts @@ -0,0 +1,22 @@ +export const mockCandles = [ + { + open: 280, + high: 290, + low: 278, + close: 289.5, + volume: 1000000, + value: 280000000, + begin: '2024-01-15T10:00:00Z', + end: '2024-01-15T18:00:00Z', + }, + { + open: 289, + high: 292, + low: 285, + close: 288, + volume: 800000, + value: 231200000, + begin: '2024-01-16T10:00:00Z', + end: '2024-01-16T18:00:00Z', + }, +] diff --git a/apps/frontend/mocks/data/index.ts b/apps/frontend/mocks/data/index.ts new file mode 100644 index 0000000..3964188 --- /dev/null +++ b/apps/frontend/mocks/data/index.ts @@ -0,0 +1,15 @@ +export { mockAuthResponse, mockUser } from './auth' +export { mockBond, mockBondHistory } from './bonds' +export { + mockBrokerAccounts, + mockBrokerEvents, + mockBrokerOperations, + mockBrokerPortfolio, + mockBrokerPositions, + mockEventsSummary, +} from './broker' +export { mockCandles } from './candles' +export { mockAnalytics, mockPortfolioDetail, mockPortfolios, mockPositions } from './portfolios' +export { mockScreenerItems } from './screener' +export { mockSearchResults } from './search' +export { mockDividends, mockShare, mockShareHistory } from './shares' diff --git a/apps/frontend/mocks/data/portfolios.ts b/apps/frontend/mocks/data/portfolios.ts new file mode 100644 index 0000000..3f450d7 --- /dev/null +++ b/apps/frontend/mocks/data/portfolios.ts @@ -0,0 +1,138 @@ +export const mockPortfolios = [ + { + id: 1, + name: 'Основной портфель', + currency: 'RUB', + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-06-01T00:00:00Z', + totalValue: 285000, + positionCount: 5, + shareCount: 3, + bondCount: 2, + description: null, + }, + { + id: 2, + name: 'ИИС', + currency: 'RUB', + createdAt: '2024-03-15T00:00:00Z', + updatedAt: '2024-06-10T00:00:00Z', + totalValue: 150000, + positionCount: 3, + shareCount: 2, + bondCount: 1, + description: 'Индивидуальный инвестиционный счёт', + }, +] + +export const mockPositions = [ + { + id: 1, + secid: 'SBER', + shortName: 'Сбер', + type: 'share', + quantity: 100, + buyPrice: 275, + currentPrice: 289.5, + totalCost: 27500, + currentValue: 28950, + weightPercent: 10.2, + pnl: 1450, + pnlPercent: 5.27, + dividendIncome: 3500, + totalReturn: 4950, + totalReturnPercent: 18, + change: 14.5, + changePercent: 5.27, + notes: null, + tags: ['DIVIDEND', 'GROWTH'], + }, + { + id: 2, + secid: 'VTBR', + shortName: 'ВТБ', + type: 'share', + quantity: 5000, + buyPrice: 0.021, + currentPrice: 0.0234, + totalCost: 105, + currentValue: 117, + weightPercent: 0.04, + pnl: 12, + pnlPercent: 11.43, + dividendIncome: 0, + totalReturn: 12, + totalReturnPercent: 11.43, + change: 0.0024, + changePercent: 11.43, + notes: null, + tags: ['SPECULATIVE'], + }, + { + id: 3, + secid: 'SU26238RMFS5', + shortName: 'ОФЗ 26238', + type: 'bond', + quantity: 10, + buyPrice: 97, + currentPrice: 98.5, + totalCost: 9700, + currentValue: 9850, + weightPercent: 3.5, + pnl: 150, + pnlPercent: 1.55, + dividendIncome: 0, + totalReturn: 150, + totalReturnPercent: 1.55, + change: 1.5, + changePercent: 1.55, + yieldToMaturity: 8.2, + duration: 3.5, + couponValue: 36.9, + couponPercent: 7.5, + nextCouponDate: '2024-07-15', + matDate: '2027-05-15', + accruedInt: 8.45, + bid: 98.3, + offer: 98.6, + couponPeriod: 182, + bondType: 'ОФЗ', + }, +] + +export const mockPortfolioDetail = { + id: 1, + name: 'Основной портфель', + currency: 'RUB', + description: null, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-06-01T00:00:00Z', + positions: mockPositions, + totalValue: 285000, + analytics: { + totalInvested: 250000, + totalValue: 285000, + totalPnl: 35000, + totalPnlPercent: 14, + totalDividends: 5000, + totalReturn: 40000, + totalReturnPercent: 16, + positionCount: 5, + weightedYield: null, + }, +} + +export const mockAnalytics = { + positions: mockPositions, + summary: { + totalInvested: 250000, + totalValue: 285000, + totalPnl: 35000, + totalPnlPercent: 14, + totalDividends: 5000, + totalReturn: 40000, + totalReturnPercent: 16, + positionCount: 5, + weightedYield: 7.5, + }, +} diff --git a/apps/frontend/mocks/data/screener.ts b/apps/frontend/mocks/data/screener.ts new file mode 100644 index 0000000..27dabe6 --- /dev/null +++ b/apps/frontend/mocks/data/screener.ts @@ -0,0 +1,38 @@ +export const mockScreenerItems = [ + { + secid: 'SBER', + shortName: 'Сбер', + isin: 'RU0009029540', + type: 'share', + price: 289.5, + change: 2.5, + changePercent: 0.87, + volume: 15000000, + listLevel: 1, + capitalization: 6250000000000, + }, + { + secid: 'VTBR', + shortName: 'ВТБ', + isin: 'RU000A0JP5V6', + type: 'share', + price: 0.0234, + change: 0.0002, + changePercent: 0.86, + volume: 50000000, + listLevel: 1, + capitalization: 30000000000, + }, + { + secid: 'GAZP', + shortName: 'Газпром', + isin: 'RU0007661625', + type: 'share', + price: 198.5, + change: -1.5, + changePercent: -0.75, + volume: 8000000, + listLevel: 1, + capitalization: 4700000000000, + }, +] diff --git a/apps/frontend/mocks/data/search.ts b/apps/frontend/mocks/data/search.ts new file mode 100644 index 0000000..0308f7b --- /dev/null +++ b/apps/frontend/mocks/data/search.ts @@ -0,0 +1,20 @@ +export const mockSearchResults = [ + { + secid: 'SBER', + isin: 'RU0009029540', + shortName: 'Сбер', + type: 'share', + listLevel: 1, + currency: 'RUB', + price: 289.5, + }, + { + secid: 'VTBR', + isin: 'RU000A0JP5V6', + shortName: 'ВТБ', + type: 'share', + listLevel: 1, + currency: 'RUB', + price: 0.0234, + }, +] diff --git a/apps/frontend/mocks/data/shares.ts b/apps/frontend/mocks/data/shares.ts new file mode 100644 index 0000000..cd6fb87 --- /dev/null +++ b/apps/frontend/mocks/data/shares.ts @@ -0,0 +1,50 @@ +export const mockShare = { + secid: 'SBER', + isin: 'RU0009029540', + name: 'Сбер Банк', + shortName: 'Сбер', + latName: 'Sberbank', + listLevel: 1, + issueSize: 21586900000, + faceValue: 3, + faceUnit: 'RUB', + type: 'common_share', + marketData: { + price: 289.5, + change: 2.5, + changePercent: 0.87, + open: 287, + high: 291, + low: 286.5, + volume: 15000000, + value: 4350000000, + issueCapitalization: 6250000000000, + updatedAt: '2024-01-15T10:00:00Z', + }, +} + +export const mockDividends = [ + { registryCloseDate: '2024-07-10', value: 35.0, currency: 'RUB' }, + { registryCloseDate: '2023-10-05', value: 30.0, currency: 'RUB' }, +] + +export const mockShareHistory = [ + { + date: '2024-01-15', + open: 287, + high: 291, + low: 286.5, + close: 289.5, + volume: 15000000, + value: 4350000000, + }, + { + date: '2024-01-14', + open: 285, + high: 288, + low: 284, + close: 287, + volume: 12000000, + value: 3440000000, + }, +] diff --git a/apps/frontend/mocks/handlers.ts b/apps/frontend/mocks/handlers.ts new file mode 100644 index 0000000..5e795bf --- /dev/null +++ b/apps/frontend/mocks/handlers.ts @@ -0,0 +1,151 @@ +import { HttpResponse, http } from 'msw' +import { + mockAnalytics, + mockAuthResponse, + mockBond, + mockBondHistory, + mockBrokerAccounts, + mockBrokerEvents, + mockBrokerOperations, + mockBrokerPortfolio, + mockBrokerPositions, + mockCandles, + mockDividends, + mockEventsSummary, + mockPortfolioDetail, + mockPortfolios, + mockPositions, + mockScreenerItems, + mockSearchResults, + mockShare, + mockShareHistory, + mockUser, +} from './data' +import { envelope } from './utils' + +const API = '/api/v1' + +export const handlers = [ + http.get(`${API}/health`, () => + HttpResponse.json( + envelope({ status: 'ok', timestamp: new Date().toISOString(), uptime: 12345 }), + ), + ), + + http.get(`${API}/auth/me`, () => HttpResponse.json(envelope(mockUser))), + http.patch(`${API}/auth/me`, () => HttpResponse.json(envelope({ ...mockUser, name: 'Updated' }))), + http.post(`${API}/auth/login`, () => HttpResponse.json(envelope(mockAuthResponse))), + http.post(`${API}/auth/register`, () => HttpResponse.json(envelope(mockAuthResponse))), + http.post(`${API}/auth/refresh`, () => HttpResponse.json(envelope(mockAuthResponse))), + http.post(`${API}/auth/logout`, () => HttpResponse.json(envelope({ message: 'Logged out' }))), + + http.get(`${API}/securities/search`, ({ request }) => { + const url = new URL(request.url) + const q = url.searchParams.get('q') || '' + if (q.length < 2) return HttpResponse.json(envelope([])) + const filtered = mockSearchResults.filter( + (r) => + r.secid.toLowerCase().includes(q.toLowerCase()) || + r.shortName.toLowerCase().includes(q.toLowerCase()), + ) + return HttpResponse.json(envelope(filtered)) + }), + + http.get(`${API}/securities/shares/:secid`, ({ params }) => { + const { secid } = params + if (secid === 'NOTFOUND') return new HttpResponse(null, { status: 404 }) + return HttpResponse.json(envelope({ ...mockShare, secid })) + }), + http.get(`${API}/securities/shares/:secid/marketdata`, () => + HttpResponse.json(envelope(mockShare.marketData)), + ), + http.get(`${API}/securities/shares/:secid/dividends`, () => + HttpResponse.json(envelope(mockDividends)), + ), + http.get(`${API}/securities/shares/:secid/history`, () => + HttpResponse.json(envelope(mockShareHistory)), + ), + http.get(`${API}/securities/shares/:secid/candles`, () => + HttpResponse.json(envelope(mockCandles)), + ), + + http.get(`${API}/securities/bonds/:secid`, ({ params }) => { + const { secid } = params + if (secid === 'NOTFOUND') return new HttpResponse(null, { status: 404 }) + return HttpResponse.json(envelope({ ...mockBond, secid })) + }), + http.get(`${API}/securities/bonds/:secid/marketdata`, () => + HttpResponse.json(envelope(mockBond.marketData)), + ), + http.get(`${API}/securities/bonds/:secid/history`, () => + HttpResponse.json(envelope(mockBondHistory)), + ), + http.get(`${API}/securities/bonds/:secid/candles`, () => + HttpResponse.json(envelope(mockCandles)), + ), + + http.get(`${API}/securities/screener`, () => + HttpResponse.json( + envelope({ + items: mockScreenerItems, + total: mockScreenerItems.length, + page: 1, + pageSize: 20, + totalPages: 1, + }), + ), + ), + + http.get(`${API}/portfolios`, () => HttpResponse.json(envelope(mockPortfolios))), + http.post(`${API}/portfolios`, () => HttpResponse.json(envelope(mockPortfolios[0]))), + http.get(`${API}/portfolios/:id`, () => HttpResponse.json(envelope(mockPortfolioDetail))), + http.patch(`${API}/portfolios/:id`, () => HttpResponse.json(envelope(mockPortfolios[0]))), + http.delete(`${API}/portfolios/:id`, () => HttpResponse.json(envelope(null))), + http.post(`${API}/portfolios/:id/positions`, () => HttpResponse.json(envelope(mockPositions[0]))), + http.patch(`${API}/portfolios/:id/positions/:positionId`, () => + HttpResponse.json(envelope(mockPositions[0])), + ), + http.delete(`${API}/portfolios/:id/positions/:positionId`, () => + HttpResponse.json(envelope(null)), + ), + http.get(`${API}/portfolios/:id/analytics`, () => HttpResponse.json(envelope(mockAnalytics))), + + http.get(`${API}/broker/accounts`, () => HttpResponse.json(envelope(mockBrokerAccounts))), + http.get(`${API}/broker/accounts/:accountId/portfolio`, () => + HttpResponse.json(envelope(mockBrokerPortfolio)), + ), + http.get(`${API}/broker/accounts/:accountId/positions`, () => + HttpResponse.json( + envelope({ + accountId: '2084014113', + items: mockBrokerPositions, + nextCursor: null, + hasNext: false, + asOf: '2024-06-01T10:00:00Z', + }), + ), + ), + http.get(`${API}/broker/accounts/:accountId/operations`, () => + HttpResponse.json( + envelope({ + accountId: '2084014113', + items: mockBrokerOperations, + nextCursor: null, + hasNext: false, + asOf: '2024-06-01T10:00:00Z', + }), + ), + ), + http.get(`${API}/broker/accounts/:accountId/events`, () => + HttpResponse.json( + envelope({ + items: mockBrokerEvents, + summary: mockEventsSummary, + asOf: '2024-06-01T10:00:00Z', + }), + ), + ), + http.post(`${API}/broker/accounts/:accountId/operations/sync`, () => + HttpResponse.json(envelope({ upserted: 5 })), + ), +] diff --git a/apps/frontend/src/shared/lib/test/server.ts b/apps/frontend/mocks/server.ts similarity index 100% rename from apps/frontend/src/shared/lib/test/server.ts rename to apps/frontend/mocks/server.ts diff --git a/apps/frontend/mocks/utils.ts b/apps/frontend/mocks/utils.ts new file mode 100644 index 0000000..f9dff52 --- /dev/null +++ b/apps/frontend/mocks/utils.ts @@ -0,0 +1,5 @@ +export function envelope(data: unknown) { + return { + data: { data, meta: { fromCache: false, cachedAt: null } }, + } +} diff --git a/apps/frontend/src/app/providers/SessionProvider.test.tsx b/apps/frontend/src/app/providers/SessionProvider.test.tsx index 6f7c307..c29813e 100644 --- a/apps/frontend/src/app/providers/SessionProvider.test.tsx +++ b/apps/frontend/src/app/providers/SessionProvider.test.tsx @@ -1,3 +1,4 @@ +import { server } from '@mocks/server' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' @@ -5,7 +6,6 @@ import { HttpResponse, http } from 'msw' import { useContext } from 'react' import { describe, expect, it } from 'vitest' import { SessionContext } from '@/entities/session' -import { server } from '@/shared/lib/test/server' import { SessionProvider } from './SessionProvider' const API = '/api/v1' diff --git a/apps/frontend/src/entities/bond/model/useBondCandles.test.tsx b/apps/frontend/src/entities/bond/model/useBondCandles.test.tsx index 1e1fa2f..b0aa23f 100644 --- a/apps/frontend/src/entities/bond/model/useBondCandles.test.tsx +++ b/apps/frontend/src/entities/bond/model/useBondCandles.test.tsx @@ -1,9 +1,9 @@ +import { server } from '@mocks/server' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { renderHook, waitFor } from '@testing-library/react' import { HttpResponse, http } from 'msw' import type { ReactNode } from 'react' import { describe, expect, it } from 'vitest' -import { server } from '@/shared/lib/test/server' import { useBondCandles } from './useBondCandles' const API = '/api/v1' diff --git a/apps/frontend/src/entities/search/model/useSearch.test.tsx b/apps/frontend/src/entities/search/model/useSearch.test.tsx index a1c61e3..371f89d 100644 --- a/apps/frontend/src/entities/search/model/useSearch.test.tsx +++ b/apps/frontend/src/entities/search/model/useSearch.test.tsx @@ -1,10 +1,10 @@ +import { server } from '@mocks/server' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { renderHook, waitFor } from '@testing-library/react' import { HttpResponse, http } from 'msw' import type { ReactNode } from 'react' import { describe, expect, it } from 'vitest' import { useSearch } from '@/entities/search' -import { server } from '@/shared/lib/test/server' const API = '/api/v1' diff --git a/apps/frontend/src/entities/session/api/sessionApi.test.ts b/apps/frontend/src/entities/session/api/sessionApi.test.ts index d0947cd..94d7f95 100644 --- a/apps/frontend/src/entities/session/api/sessionApi.test.ts +++ b/apps/frontend/src/entities/session/api/sessionApi.test.ts @@ -1,6 +1,6 @@ +import { server } from '@mocks/server' import { HttpResponse, http } from 'msw' import { beforeEach, describe, expect, it } from 'vitest' -import { server } from '@/shared/lib/test/server' import { getMe, login, logout, refresh, register, updateProfile } from './sessionApi' import { getAccessToken, setAccessToken } from './tokenManager' diff --git a/apps/frontend/src/entities/stock/model/useStockCandles.test.tsx b/apps/frontend/src/entities/stock/model/useStockCandles.test.tsx index e771e24..9bb2cd3 100644 --- a/apps/frontend/src/entities/stock/model/useStockCandles.test.tsx +++ b/apps/frontend/src/entities/stock/model/useStockCandles.test.tsx @@ -1,9 +1,9 @@ +import { server } from '@mocks/server' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { renderHook, waitFor } from '@testing-library/react' import { HttpResponse, http } from 'msw' import type { ReactNode } from 'react' import { describe, expect, it } from 'vitest' -import { server } from '@/shared/lib/test/server' import { useStockCandles } from './useStockCandles' const API = '/api/v1' diff --git a/apps/frontend/src/entities/stock/model/useStockDividends.test.tsx b/apps/frontend/src/entities/stock/model/useStockDividends.test.tsx index 509f77e..7d8415c 100644 --- a/apps/frontend/src/entities/stock/model/useStockDividends.test.tsx +++ b/apps/frontend/src/entities/stock/model/useStockDividends.test.tsx @@ -1,9 +1,9 @@ +import { server } from '@mocks/server' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { renderHook, waitFor } from '@testing-library/react' import { HttpResponse, http } from 'msw' import type { ReactNode } from 'react' import { describe, expect, it } from 'vitest' -import { server } from '@/shared/lib/test/server' import { useStockDividends } from './useStockDividends' const API = '/api/v1' diff --git a/apps/frontend/src/main.tsx b/apps/frontend/src/main.tsx index 9abcc46..78ab8cf 100644 --- a/apps/frontend/src/main.tsx +++ b/apps/frontend/src/main.tsx @@ -7,7 +7,7 @@ import { env } from './shared/config/env' async function startApp() { if (env.VITE_API_MOCK) { - const { worker } = await import('./shared/lib/test/browser') + const { worker } = await import('../mocks/browser') await worker.start({ onUnhandledRequest: 'bypass' }) } diff --git a/apps/frontend/src/pages/login/LoginPage.test.tsx b/apps/frontend/src/pages/login/LoginPage.test.tsx index a1271c6..84d7cb2 100644 --- a/apps/frontend/src/pages/login/LoginPage.test.tsx +++ b/apps/frontend/src/pages/login/LoginPage.test.tsx @@ -1,3 +1,4 @@ +import { server } from '@mocks/server' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createMemoryHistory, @@ -10,7 +11,6 @@ import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { HttpResponse, http } from 'msw' import { describe, expect, it } from 'vitest' -import { server } from '@/shared/lib/test/server' import { TestSessionProvider } from '@/shared/lib/test/TestSessionProvider' import { LoginPage } from './ui/LoginPage' diff --git a/apps/frontend/src/pages/portfolios/ui/PortfolioDetailPage.tsx b/apps/frontend/src/pages/portfolios/ui/PortfolioDetailPage.tsx index 7a87fa9..ee88d50 100644 --- a/apps/frontend/src/pages/portfolios/ui/PortfolioDetailPage.tsx +++ b/apps/frontend/src/pages/portfolios/ui/PortfolioDetailPage.tsx @@ -9,7 +9,7 @@ import { PortfolioSummary } from '@/widgets/portfolio-summary' import { SharePositionTable } from '@/widgets/share-positions-table' export function PortfolioDetailPage() { - const { id } = useParams<{ id: string }>() + const { id } = useParams({ from: '/portfolios/$id' }) const portfolioId = parseInt(id!, 10) const { data: portfolio, isLoading, error } = usePortfolio(portfolioId) diff --git a/apps/frontend/src/pages/profile/ProfilePage.test.tsx b/apps/frontend/src/pages/profile/ProfilePage.test.tsx index b6365a8..1c9323a 100644 --- a/apps/frontend/src/pages/profile/ProfilePage.test.tsx +++ b/apps/frontend/src/pages/profile/ProfilePage.test.tsx @@ -1,8 +1,8 @@ +import { server } from '@mocks/server' import { screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { HttpResponse, http } from 'msw' import { describe, expect, it } from 'vitest' -import { server } from '@/shared/lib/test/server' import { renderWithProviders } from '@/shared/lib/test/test-utils' import { ProfilePage } from './ui/ProfilePage' diff --git a/apps/frontend/src/pages/register/RegisterPage.test.tsx b/apps/frontend/src/pages/register/RegisterPage.test.tsx index edeb893..c5126b8 100644 --- a/apps/frontend/src/pages/register/RegisterPage.test.tsx +++ b/apps/frontend/src/pages/register/RegisterPage.test.tsx @@ -1,3 +1,4 @@ +import { server } from '@mocks/server' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createMemoryHistory, @@ -10,7 +11,6 @@ import { render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { HttpResponse, http } from 'msw' import { describe, expect, it } from 'vitest' -import { server } from '@/shared/lib/test/server' import { TestSessionProvider } from '@/shared/lib/test/TestSessionProvider' import { RegisterPage } from './ui/RegisterPage' diff --git a/apps/frontend/src/pages/stock/ui/StockPage.tsx b/apps/frontend/src/pages/stock/ui/StockPage.tsx index eab1f85..1ee32db 100644 --- a/apps/frontend/src/pages/stock/ui/StockPage.tsx +++ b/apps/frontend/src/pages/stock/ui/StockPage.tsx @@ -5,7 +5,7 @@ import { PriceChart } from '@/widgets/price-chart' import { StockDetails } from '@/widgets/stock-details' export function StockPage() { - const { secid } = useParams<{ secid: string }>() + const { secid } = useParams({ from: '/stocks/$secid' }) const { data: stock, isLoading, error } = useStock(secid!) const till = new Date().toISOString().split('T')[0] const from = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] diff --git a/apps/frontend/src/shared/lib/test/handlers.ts b/apps/frontend/src/shared/lib/test/handlers.ts deleted file mode 100644 index 029a4d4..0000000 --- a/apps/frontend/src/shared/lib/test/handlers.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { HttpResponse, http } from 'msw' -import type { BondResponse, CandleItem, SearchResultItem, ShareResponse } from '@/shared/api' - -const API = '/api/v1' - -const mockShare: ShareResponse = { - secid: 'SBER', - isin: 'RU0009029540', - name: 'Сбер Банк', - shortName: 'Сбер', - latName: 'Sberbank', - listLevel: 1, - issueSize: 21586900000, - faceValue: 3, - faceUnit: 'RUB', - type: 'common_share', - marketData: { - price: 289.5, - change: 2.5, - changePercent: 0.87, - open: 287, - high: 291, - low: 286.5, - volume: 15000000, - value: 4350000000, - issueCapitalization: 6250000000000, - updatedAt: '2024-01-15T10:00:00Z', - }, -} - -const mockBond: BondResponse = { - secid: 'SU26238RMFS5', - isin: 'RU000A101XU7', - name: 'ОФЗ 26238', - shortName: 'ОФЗ 26238', - latName: null, - listLevel: 1, - issueSize: 500000000, - faceValue: 1000, - faceUnit: 'RUB', - matDate: '2027-05-15', - couponValue: 36.9, - couponPercent: 7.5, - couponPeriod: 182, - nextCoupon: '2024-07-15', - accruedInt: 8.45, - bondType: 'ОФЗ', - bondSubType: 'ОФЗ-ПД', - offerDate: null, - buybackDate: null, - marketData: { - price: 98.5, - yieldToMaturity: 8.2, - duration: 3.5, - accruedInt: 8.45, - couponValue: 36.9, - couponPercent: 7.5, - nextCouponDate: '2024-07-15', - open: 98.0, - high: 99.0, - low: 97.5, - volume: 1000000, - updatedAt: '2024-01-15T10:00:00Z', - }, -} - -const mockCandles: CandleItem[] = [ - { - open: 280, - high: 290, - low: 278, - close: 289.5, - volume: 1000000, - value: 280000000, - begin: '2024-01-15T10:00:00Z', - end: '2024-01-15T18:00:00Z', - }, - { - open: 289, - high: 292, - low: 285, - close: 288, - volume: 800000, - value: 231200000, - begin: '2024-01-16T10:00:00Z', - end: '2024-01-16T18:00:00Z', - }, -] - -const mockDividends = [ - { registryCloseDate: '2024-07-10', value: 35.0, currency: 'RUB' }, - { registryCloseDate: '2023-10-05', value: 30.0, currency: 'RUB' }, -] - -const mockSearchResults: SearchResultItem[] = [ - { - secid: 'SBER', - isin: 'RU0009029540', - shortName: 'Сбер', - type: 'share', - listLevel: 1, - currency: 'RUB', - price: 289.5, - }, - { - secid: 'VTBR', - isin: 'RU000A0JP5V6', - shortName: 'ВТБ', - type: 'share', - listLevel: 1, - currency: 'RUB', - price: 0.0234, - }, -] - -const userResponse = { - id: 1, - email: 'user@test.com', - name: 'Test User', - role: 'user', -} - -const authResponse = { - user: userResponse, - accessToken: 'mock-access-token', -} - -const envelope = (data: unknown) => ({ - data: { data, meta: { fromCache: false, cachedAt: null } }, -}) - -export const handlers = [ - http.get(`${API}/securities/search`, ({ request }) => { - const url = new URL(request.url) - const q = url.searchParams.get('q') || '' - if (q.length < 2) { - return HttpResponse.json(envelope([])) - } - const filtered = mockSearchResults.filter( - (r) => - r.secid.toLowerCase().includes(q.toLowerCase()) || - r.shortName.toLowerCase().includes(q.toLowerCase()), - ) - return HttpResponse.json(envelope(filtered)) - }), - - http.get(`${API}/securities/shares/:secid`, ({ params }) => { - const { secid } = params - if (secid === 'NOTFOUND') return new HttpResponse(null, { status: 404 }) - return HttpResponse.json(envelope({ ...mockShare, secid } as ShareResponse)) - }), - - http.get(`${API}/securities/shares/:secid/candles`, () => - HttpResponse.json(envelope(mockCandles)), - ), - - http.get(`${API}/securities/shares/:secid/dividends`, () => - HttpResponse.json(envelope(mockDividends)), - ), - - http.get(`${API}/securities/bonds/:secid`, ({ params }) => { - const { secid } = params - if (secid === 'NOTFOUND') return new HttpResponse(null, { status: 404 }) - return HttpResponse.json(envelope({ ...mockBond, secid } as BondResponse)) - }), - - http.get(`${API}/securities/bonds/:secid/candles`, () => - HttpResponse.json(envelope(mockCandles)), - ), - - http.get(`${API}/auth/me`, () => HttpResponse.json(envelope(userResponse))), - - http.post(`${API}/auth/login`, () => HttpResponse.json(envelope(authResponse))), - - http.post(`${API}/auth/register`, () => HttpResponse.json(envelope(authResponse))), - - http.post(`${API}/auth/refresh`, () => HttpResponse.json(envelope(authResponse))), - - http.post(`${API}/auth/logout`, () => HttpResponse.json(envelope({ message: 'Logged out' }))), - - http.patch(`${API}/auth/me`, () => - HttpResponse.json(envelope({ ...userResponse, name: 'Updated' })), - ), - - http.get(`${API}/health`, () => - HttpResponse.json( - envelope({ status: 'ok', timestamp: new Date().toISOString(), uptime: 12345 }), - ), - ), -] diff --git a/apps/frontend/src/shared/lib/test/setup.ts b/apps/frontend/src/shared/lib/test/setup.ts index f47cb6d..d36df91 100644 --- a/apps/frontend/src/shared/lib/test/setup.ts +++ b/apps/frontend/src/shared/lib/test/setup.ts @@ -1,5 +1,5 @@ import '@testing-library/jest-dom' -import { server } from './server' +import { server } from '@mocks/server' beforeAll(() => server.listen({ onUnhandledRequest: 'error' })) afterEach(() => server.resetHandlers()) diff --git a/apps/frontend/src/widgets/search-bar/ui/SearchBar.test.tsx b/apps/frontend/src/widgets/search-bar/ui/SearchBar.test.tsx index 93d4e22..3d921c2 100644 --- a/apps/frontend/src/widgets/search-bar/ui/SearchBar.test.tsx +++ b/apps/frontend/src/widgets/search-bar/ui/SearchBar.test.tsx @@ -1,10 +1,10 @@ +import { server } from '@mocks/server' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { HttpResponse, http } from 'msw' import type { ReactNode } from 'react' import { describe, expect, it, vi } from 'vitest' -import { server } from '@/shared/lib/test/server' import { SearchBar } from '@/widgets/search-bar' const API = '/api/v1' diff --git a/apps/frontend/tsconfig.json b/apps/frontend/tsconfig.json index 9c70fcd..4d24333 100644 --- a/apps/frontend/tsconfig.json +++ b/apps/frontend/tsconfig.json @@ -17,10 +17,11 @@ "noUnusedParameters": false, "noFallthroughCasesInSwitch": true, "paths": { - "@/*": ["./src/*"] + "@/*": ["./src/*"], + "@mocks/*": ["./mocks/*"] } }, - "include": ["src"], + "include": ["src", "mocks"], "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx", "src/shared/lib/test/**"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 2e4a0a3..843f245 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -5,9 +5,10 @@ import path from 'path'; export default defineConfig({ plugins: [react()], resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, + alias: [ + { find: '@mocks', replacement: path.resolve(__dirname, './mocks') }, + { find: '@', replacement: path.resolve(__dirname, './src') }, + ], }, server: { port: 5173, diff --git a/apps/frontend/vitest.config.ts b/apps/frontend/vitest.config.ts index a11c0ae..1617c16 100644 --- a/apps/frontend/vitest.config.ts +++ b/apps/frontend/vitest.config.ts @@ -6,9 +6,10 @@ export default defineConfig({ plugins: [react()], logLevel: 'error', resolve: { - alias: { - '@': path.resolve(__dirname, './src'), - }, + alias: [ + { find: '@mocks', replacement: path.resolve(__dirname, './mocks') }, + { find: '@', replacement: path.resolve(__dirname, './src') }, + ], }, test: { environment: 'jsdom',