import { test, expect, registerUser } from '../fixtures'; /** * Smoke E2E for R5.3 AVM API upgrades: * POST /avm/batch — batch valuation, max 50 items * GET /avm/history/:id — stored historical valuations * GET /avm/compare — 2-5 property side-by-side * GET /avm/explain — confidence explanation for a valuationId * * These tests exercise the surface shape (validation, auth, error codes). * Deeper value-level assertions are covered in the unit test suite. */ test.describe('AVM API (R5.3)', () => { let accessToken: string; test.beforeAll(async ({ request }) => { const { accessToken: token } = await registerUser(request); accessToken = token; }); test.describe('POST /avm/batch', () => { test('requires authentication', async ({ request }) => { const res = await request.post('avm/batch', { data: { propertyIds: ['prop-1'] }, }); expect([401, 403]).toContain(res.status()); }); test('rejects batches over 50 items', async ({ request }) => { const propertyIds = Array.from({ length: 51 }, (_, i) => `prop-${i}`); const res = await request.post('avm/batch', { headers: { Authorization: `Bearer ${accessToken}` }, data: { propertyIds }, }); expect(res.status()).toBe(400); }); test('rejects empty batch', async ({ request }) => { const res = await request.post('avm/batch', { headers: { Authorization: `Bearer ${accessToken}` }, data: { propertyIds: [] }, }); expect(res.status()).toBe(400); }); test('accepts valid batch of valid IDs', async ({ request }) => { const res = await request.post('avm/batch', { headers: { Authorization: `Bearer ${accessToken}` }, data: { propertyIds: ['prop-seed-1', 'prop-seed-2'] }, }); // 200 on success path; 429 if rate-limited by earlier tests. Both are acceptable. expect([200, 429]).toContain(res.status()); if (res.status() === 200) { const body = await res.json(); expect(Array.isArray(body)).toBeTruthy(); expect(body.length).toBe(2); } }); }); test.describe('GET /avm/history/:propertyId', () => { test('requires authentication', async ({ request }) => { const res = await request.get('avm/history/prop-1'); expect([401, 403]).toContain(res.status()); }); test('returns chronologically ordered history shape', async ({ request }) => { const res = await request.get('avm/history/prop-seed-1?limit=10', { headers: { Authorization: `Bearer ${accessToken}` }, }); expect([200, 403]).toContain(res.status()); if (res.status() === 200) { const body = await res.json(); expect(body).toHaveProperty('propertyId', 'prop-seed-1'); expect(Array.isArray(body.history)).toBeTruthy(); // Each point includes model_version + timestamp for (const point of body.history) { expect(point).toHaveProperty('modelVersion'); expect(point).toHaveProperty('valuedAt'); } } }); }); test.describe('GET /avm/compare', () => { test('requires authentication', async ({ request }) => { const res = await request.get('avm/compare?ids=prop-1,prop-2'); expect([401, 403]).toContain(res.status()); }); test('rejects fewer than 2 IDs', async ({ request }) => { const res = await request.get('avm/compare?ids=prop-1', { headers: { Authorization: `Bearer ${accessToken}` }, }); expect(res.status()).toBe(400); }); test('rejects more than 5 IDs', async ({ request }) => { const ids = ['p1', 'p2', 'p3', 'p4', 'p5', 'p6'].join(','); const res = await request.get(`avm/compare?ids=${ids}`, { headers: { Authorization: `Bearer ${accessToken}` }, }); expect(res.status()).toBe(400); }); }); test.describe('GET /avm/explain', () => { test('requires authentication', async ({ request }) => { const res = await request.get('avm/explain?valuationId=val-xxx'); expect([401, 403]).toContain(res.status()); }); test('rejects missing valuationId', async ({ request }) => { const res = await request.get('avm/explain', { headers: { Authorization: `Bearer ${accessToken}` }, }); expect(res.status()).toBe(400); }); test('returns 404 for unknown valuationId', async ({ request }) => { const res = await request.get('avm/explain?valuationId=val-does-not-exist', { headers: { Authorization: `Bearer ${accessToken}` }, }); expect([404, 403]).toContain(res.status()); }); }); });