SPB Git forge

spb/rareindex

Public
54commits 1branches 0releases
7.1 MBsize
maindefault branch
10 days agolast push
TypeScript 61.9% HTML 37.2% SQL 0.7%
4.4 KB · 98 lines typescript
Raw Blame History
1import { afterAll, beforeAll, describe, expect, it } from 'vitest';2import type { FastifyInstance } from 'fastify';3import { closeDb, getDb, apiKeys, eq } from '@rareindex/database';4import { buildApp } from './app.js';5import { mintApiKey } from './lib/keys.js';6import { toCsv, encodeCursor, decodeCursor } from './lib/envelope.js';78/** Integration tests against DATABASE_URL (rareindex_ai locally). They only read except for a temporary API key. */9let app: FastifyInstance;10let key: { id: string; key: string };1112beforeAll(async () => {13  process.env.LOG_LEVEL = 'silent';14  app = await buildApp({ logger: false });15  key = await mintApiKey({ name: 'vitest', tier: 'hobby' });16});17afterAll(async () => {18  await getDb().delete(apiKeys).where(eq(apiKeys.id, key.id));19  await app.close();20  await closeDb();21});2223describe('api', () => {24  it('health and root', async () => {25    const h = await app.inject({ method: 'GET', url: '/healthz' });26    expect(h.statusCode).toBe(200);27    expect(h.json().ok).toBe(true);28    const r = await app.inject({ method: 'GET', url: '/' });29    expect(r.json().openapi).toBe('/v1/openapi.json');30  });31  it('serves openapi', async () => {32    const r = await app.inject({ method: 'GET', url: '/v1/openapi.json' });33    expect(r.statusCode).toBe(200);34    expect(r.json().paths['/v1/assets/search']).toBeTruthy();35  });36  it('anonymous requests use the public tier with rate-limit headers', async () => {37    const r = await app.inject({ method: 'GET', url: '/v1/categories' });38    expect(r.statusCode).toBe(200);39    expect(r.headers['x-ratelimit-limit']).toBe('20');40    expect(r.headers['x-request-id']).toBeTruthy();41    const body = r.json();42    expect(body.meta.count).toBeGreaterThan(100);43    expect(body.meta.attribution).toMatch(/RareIndex/);44    expect(body.data.find((c: { slug: string }) => c.slug === 'pokemon')).toBeTruthy();45  });46  it('rejects unknown keys and accepts minted keys with tier limits', async () => {47    const bad = await app.inject({ method: 'GET', url: '/v1/indices', headers: { authorization: 'Bearer ri_live_nope_nope' } });48    expect(bad.statusCode).toBe(401);49    expect(bad.headers['content-type']).toMatch(/problem\+json/);50    const good = await app.inject({ method: 'GET', url: '/v1/indices', headers: { authorization: `Bearer ${key.key}` } });51    expect(good.statusCode).toBe(200);52    expect(good.headers['x-ratelimit-limit']).toBe('120');53    const idx = good.json();54    expect(idx.data.find((i: { ticker: string }) => i.ticker === 'RARE')).toBeTruthy();55    expect(idx.data.every((i: { published: boolean; value: unknown }) => i.published === (i.value !== null))).toBe(true);56  });57  it('validates queries and returns problem+json', async () => {58    const r = await app.inject({ method: 'GET', url: '/v1/assets/search?limit=9999' });59    expect(r.statusCode).toBe(400);60    expect(r.json().title).toBe('Invalid query');61  });62  it('404 on unknown asset/market', async () => {63    expect((await app.inject({ method: 'GET', url: '/v1/assets/does-not-exist' })).statusCode).toBe(404);64    expect((await app.inject({ method: 'GET', url: '/v1/markets/does-not-exist' })).statusCode).toBe(404);65  });66  it('search, trending, sales, records, markets and csv export respond', async () => {67    for (const url of ['/v1/assets/search?q=charizard', '/v1/trending', '/v1/sales/latest', '/v1/records', '/v1/markets', '/v1/markets/pokemon', '/v1/indices/RARE-TCG/history', '/v1/stats']) {68      const r = await app.inject({ method: 'GET', url });69      expect(r.statusCode, url).toBe(200);70    }71    const csv = await app.inject({ method: 'GET', url: '/v1/categories?format=csv' });72    expect(csv.headers['content-type']).toMatch(/text\/csv/);73    expect(csv.body.split('\n')[0]).toContain('slug');74  });75  it('rate limits by key', async () => {76    const small = await mintApiKey({ name: 'vitest-small', tier: 'free' });77    try {78      let last = 200;79      for (let i = 0; i < 65; i++) {80        const r = await app.inject({ method: 'GET', url: '/v1/stats', headers: { authorization: `Bearer ${small.key}` } });81        last = r.statusCode;82        if (last === 429) break;83      }84      expect(last).toBe(429);85    } finally {86      await getDb().delete(apiKeys).where(eq(apiKeys.id, small.id));87    }88  });89});9091describe('envelope helpers', () => {92  it('csv + cursor', () => {93    expect(toCsv([{ a: 1, b: 'x,y' }, { a: 2, b: null }])).toBe('a,b\n1,"x,y"\n2,\n');94    expect(decodeCursor(encodeCursor(150))).toBe(150);95    expect(decodeCursor('garbage')).toBe(0);96  });97});98