/** * Tests for energy hooks in hooks.ts — energy pricing API wrappers. * * Coverage: * 1. useContracts — GET /api/energy/contracts * 2. useCreateContract — POST /api/energy/contracts * 3. useEnergyPrices — GET /api/energy/prices * 4. useEnergyCosts — GET /api/energy/costs * 5. useEnergyCostSummary — GET /api/energy/costs/summary * 6. useRecomputeCosts — POST /api/energy/costs/recompute */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { renderHook, waitFor, act } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import type { ReactNode } from 'react' // --------------------------------------------------------------------------- // Mock apiClient // --------------------------------------------------------------------------- const mockGet = vi.fn() const mockPost = vi.fn() const mockPatch = vi.fn() vi.mock('../api/client', () => ({ default: { GET: (...args: unknown[]) => mockGet(...args), POST: (...args: unknown[]) => mockPost(...args), PATCH: (...args: unknown[]) => mockPatch(...args), DELETE: vi.fn(), }, ApiError: class ApiError extends Error { status: number body: unknown constructor(status: number, body: unknown) { super(`API error ${status}`) this.name = 'ApiError' this.status = status this.body = body } }, registerLoginRedirect: vi.fn(), })) // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const CONTRACT = { id: 1, name: 'Test Contract', kind: 'manual', active: true, currency: 'EUR', created_at: '2026-06-01T00:00:00Z', updated_at: '2026-06-01T00:00:00Z', } const PRICE_POINT = { starts_at: '2026-06-22T10:00:00Z', buy: 0.133, sell: 0.09, level: 'NORMAL', } const COST_PERIOD = { period_start: '2026-06-22T10:00:00Z', d1_kwh: 0.1, d2_kwh: 0.2, r1_kwh: 0.0, r2_kwh: 0.05, import_cost: 0.044, export_revenue: 0.005, net_cost: 0.039, currency: 'EUR', degraded: false, contract_version_id: 1, } const SUMMARY = { currency: 'EUR', metered_import: 10.5, metered_export: 2.3, metered_net: 8.2, fixed_costs: 5.0, credits: 50.0, total_payable: 12.5, } // --------------------------------------------------------------------------- // Wrapper // --------------------------------------------------------------------------- function makeWrapper() { const qc = new QueryClient({ defaultOptions: { queries: { retry: false }, mutations: { retry: false } }, }) function Wrapper({ children }: { children: ReactNode }) { return {children} } return { qc, Wrapper } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('useContracts', () => { beforeEach(() => vi.clearAllMocks()) it('calls GET /api/energy/contracts and returns contract list', async () => { mockGet.mockResolvedValue({ data: { items: [CONTRACT], total: 1 } }) const { Wrapper } = makeWrapper() const { useContracts } = await import('./hooks') const { result } = renderHook(() => useContracts(), { wrapper: Wrapper }) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(mockGet).toHaveBeenCalledWith('/api/energy/contracts') expect(result.current.data?.items).toHaveLength(1) expect(result.current.data?.items[0].id).toBe(1) }) }) describe('useCreateContract', () => { beforeEach(() => vi.clearAllMocks()) it('calls POST /api/energy/contracts with the provided body', async () => { mockPost.mockResolvedValue({ data: { ...CONTRACT, versions: [] } }) mockGet.mockResolvedValue({ data: { items: [], total: 0 } }) const { Wrapper } = makeWrapper() const { useCreateContract } = await import('./hooks') const { result } = renderHook(() => useCreateContract(), { wrapper: Wrapper }) const body = { name: 'New Contract', kind: 'manual', currency: 'EUR', values: { energy: { buy: { normal: 0.133 } } }, } await act(async () => { await result.current.mutateAsync(body) }) expect(mockPost).toHaveBeenCalledWith('/api/energy/contracts', { body }) }) }) describe('useEnergyPrices', () => { beforeEach(() => vi.clearAllMocks()) it('calls GET /api/energy/prices with start and end params', async () => { mockGet.mockResolvedValue({ data: { kind: 'tibber', currency: 'EUR', points: [PRICE_POINT], tariff: null, }, }) const { Wrapper } = makeWrapper() const { useEnergyPrices } = await import('./hooks') const start = '2026-06-22T00:00:00Z' const end = '2026-06-23T00:00:00Z' const { result } = renderHook(() => useEnergyPrices(start, end), { wrapper: Wrapper }) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(mockGet).toHaveBeenCalledWith('/api/energy/prices', { params: { query: { start, end } }, }) expect(result.current.data?.points).toHaveLength(1) expect(result.current.data?.kind).toBe('tibber') }) it('calls GET /api/energy/prices without params when none provided', async () => { mockGet.mockResolvedValue({ data: { kind: 'manual', currency: 'EUR', points: [], tariff: null }, }) const { Wrapper } = makeWrapper() const { useEnergyPrices } = await import('./hooks') const { result } = renderHook(() => useEnergyPrices(), { wrapper: Wrapper }) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(mockGet).toHaveBeenCalledWith('/api/energy/prices', { params: { query: {} }, }) }) }) describe('useEnergyCosts', () => { beforeEach(() => vi.clearAllMocks()) it('calls GET /api/energy/costs with start, end, and limit', async () => { mockGet.mockResolvedValue({ data: { items: [COST_PERIOD], total: 1 }, }) const { Wrapper } = makeWrapper() const { useEnergyCosts } = await import('./hooks') const start = '2026-06-22T00:00:00Z' const end = '2026-06-23T00:00:00Z' const limit = 100 const { result } = renderHook( () => useEnergyCosts(start, end, limit), { wrapper: Wrapper }, ) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(mockGet).toHaveBeenCalledWith('/api/energy/costs', { params: { query: { start, end, limit } }, }) expect(result.current.data?.items).toHaveLength(1) }) }) describe('useEnergyCostSummary', () => { beforeEach(() => vi.clearAllMocks()) it('calls GET /api/energy/costs/summary with date range', async () => { mockGet.mockResolvedValue({ data: SUMMARY }) const { Wrapper } = makeWrapper() const { useEnergyCostSummary } = await import('./hooks') const start = '2026-06-01T00:00:00Z' const end = '2026-06-30T23:59:59Z' const { result } = renderHook( () => useEnergyCostSummary(start, end), { wrapper: Wrapper }, ) await waitFor(() => expect(result.current.isSuccess).toBe(true)) expect(mockGet).toHaveBeenCalledWith('/api/energy/costs/summary', { params: { query: { start, end } }, }) expect(result.current.data?.total_payable).toBe(12.5) }) }) describe('useRecomputeCosts', () => { beforeEach(() => vi.clearAllMocks()) it('calls POST /api/energy/costs/recompute and invalidates cost queries', async () => { mockPost.mockResolvedValue({ data: { recomputed: 10 } }) // Mock GET for invalidation queries (costs and summary) mockGet.mockResolvedValue({ data: { items: [], total: 0 } }) const { Wrapper } = makeWrapper() const { useRecomputeCosts } = await import('./hooks') const { result } = renderHook(() => useRecomputeCosts(), { wrapper: Wrapper }) await act(async () => { await result.current.mutateAsync({ start: '2026-06-22T00:00:00Z', end: '2026-06-23T00:00:00Z', }) }) expect(mockPost).toHaveBeenCalledWith('/api/energy/costs/recompute', { params: { query: { start: '2026-06-22T00:00:00Z', end: '2026-06-23T00:00:00Z', }, }, }) }) it('calls POST /api/energy/costs/recompute without params when none provided', async () => { mockPost.mockResolvedValue({ data: { recomputed: 0 } }) const { Wrapper } = makeWrapper() const { useRecomputeCosts } = await import('./hooks') const { result } = renderHook(() => useRecomputeCosts(), { wrapper: Wrapper }) await act(async () => { await result.current.mutateAsync({}) }) expect(mockPost).toHaveBeenCalledWith('/api/energy/costs/recompute', { params: { query: {} }, }) }) })