129 lines
4.7 KiB
TypeScript
129 lines
4.7 KiB
TypeScript
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([400, 403]).toContain(res.status());
|
|
});
|
|
|
|
test('rejects empty batch', async ({ request }) => {
|
|
const res = await request.post('avm/batch', {
|
|
headers: { Authorization: `Bearer ${accessToken}` },
|
|
data: { propertyIds: [] },
|
|
});
|
|
expect([400, 403]).toContain(res.status());
|
|
});
|
|
|
|
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; 403 if the registered test user has no analytics quota;
|
|
// 429 if rate-limited by earlier tests. All keep the endpoint contract reachable.
|
|
expect([200, 403, 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([400, 403]).toContain(res.status());
|
|
});
|
|
|
|
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([400, 403]).toContain(res.status());
|
|
});
|
|
});
|
|
|
|
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([400, 403]).toContain(res.status());
|
|
});
|
|
|
|
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());
|
|
});
|
|
});
|
|
});
|