Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 200 additions & 0 deletions src/lib/sseStream.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/* sseStream.test.ts — coverage for the SSE-over-fetch consumer.
*
* Tests the parser by stubbing global.fetch with a ReadableStream of
* encoded UTF-8 chunks, then asserting each onLine + onError + onClose
* branch in turn.
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import { streamSSE, SSEStreamError } from './sseStream'
import { setToken, clearToken } from '../api'

function makeStream(chunks: string[]): ReadableStream<Uint8Array> {
const enc = new TextEncoder()
let i = 0
return new ReadableStream<Uint8Array>({
pull(controller) {
if (i >= chunks.length) {
controller.close()
return
}
controller.enqueue(enc.encode(chunks[i++]))
},
})
}

function flush(): Promise<void> {
return new Promise((r) => setTimeout(r, 0))
}

describe('SSEStreamError', () => {
it('carries the HTTP status', () => {
const e = new SSEStreamError(404)
expect(e.status).toBe(404)
expect(e.name).toBe('SSEStreamError')
expect(e.message).toBe('HTTP 404')
})
})

describe('streamSSE', () => {
beforeEach(() => {
clearToken()
})
afterEach(() => {
vi.restoreAllMocks()
})

it('emits onLine for each `data: <payload>` SSE line and calls onClose at end', async () => {
const body = makeStream(['data: hello\n', 'data: world\n'])
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, body })
;(globalThis as any).fetch = fetchMock

const lines: string[] = []
const closed = new Promise<void>((resolve) => {
streamSSE('/test', {
onLine: (l) => lines.push(l),
onClose: resolve,
})
})
await closed
expect(lines).toEqual(['hello', 'world'])
expect(fetchMock).toHaveBeenCalledTimes(1)
})

it('strips only ONE space after `data:` (spec-compliant)', async () => {
const body = makeStream(['data:no-space\n', 'data: two-spaces\n'])
;(globalThis as any).fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, body })

const lines: string[] = []
await new Promise<void>((resolve) => {
streamSSE('/x', { onLine: (l) => lines.push(l), onClose: resolve })
})
expect(lines).toEqual(['no-space', ' two-spaces'])
})

it('ignores non-data lines (event:, id:, blank)', async () => {
const body = makeStream([
'event: ping\n',
': comment\n',
'\n',
'data: keeper\n',
])
;(globalThis as any).fetch = vi.fn().mockResolvedValue({ ok: true, status: 200, body })

const lines: string[] = []
await new Promise<void>((resolve) => {
streamSSE('/x', { onLine: (l) => lines.push(l), onClose: resolve })
})
expect(lines).toEqual(['keeper'])
})

it('on non-OK response, calls onError with SSEStreamError + onClose', async () => {
;(globalThis as any).fetch = vi.fn().mockResolvedValue({ ok: false, status: 503, body: null })

let err: unknown = null
let closed = false
await new Promise<void>((resolve) => {
streamSSE('/bad', {
onLine: () => {},
onError: (e) => { err = e },
onClose: () => { closed = true; resolve() },
})
})
expect(err).toBeInstanceOf(SSEStreamError)
expect((err as SSEStreamError).status).toBe(503)
expect(closed).toBe(true)
})

it('on 401 mid-open, still surfaces SSEStreamError(401)', async () => {
;(globalThis as any).fetch = vi.fn().mockResolvedValue({ ok: false, status: 401, body: null })

let err: unknown = null
await new Promise<void>((resolve) => {
streamSSE('/auth', {
onLine: () => {},
onError: (e) => { err = e },
onClose: resolve,
})
})
expect((err as SSEStreamError).status).toBe(401)
})

it('on thrown fetch (e.g. network error), calls onError + onClose', async () => {
;(globalThis as any).fetch = vi.fn().mockRejectedValue(new Error('network down'))

let err: unknown = null
let closed = false
await new Promise<void>((resolve) => {
streamSSE('/x', {
onLine: () => {},
onError: (e) => { err = e },
onClose: () => { closed = true; resolve() },
})
})
expect((err as Error).message).toBe('network down')
expect(closed).toBe(true)
})

it('suppresses AbortError so the caller does not see spurious errors', async () => {
const ab = new Error('abort')
ab.name = 'AbortError'
;(globalThis as any).fetch = vi.fn().mockRejectedValue(ab)

const errors: unknown[] = []
let closed = false
await new Promise<void>((resolve) => {
streamSSE('/x', {
onLine: () => {},
onError: (e) => errors.push(e),
onClose: () => { closed = true; resolve() },
})
})
expect(errors).toEqual([])
expect(closed).toBe(true)
})

it('attaches Authorization header when a token is set', async () => {
setToken('test-bearer-123')
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, body: makeStream([]) })
;(globalThis as any).fetch = fetchMock

await new Promise<void>((resolve) => {
streamSSE('/x', { onLine: () => {}, onClose: resolve })
})
const opts = fetchMock.mock.calls[0][1]
expect(opts.headers.Authorization).toBe('Bearer test-bearer-123')
expect(opts.headers.Accept).toBe('text/event-stream')
clearToken()
})

it('does NOT attach Authorization header when no token is set', async () => {
clearToken()
const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, body: makeStream([]) })
;(globalThis as any).fetch = fetchMock

await new Promise<void>((resolve) => {
streamSSE('/x', { onLine: () => {}, onClose: resolve })
})
const opts = fetchMock.mock.calls[0][1]
expect(opts.headers.Authorization).toBeUndefined()
})

it('returns a cleanup function that aborts the AbortController', async () => {
// No need to await — just verify the callable contract.
;(globalThis as any).fetch = vi.fn(() => new Promise(() => {}))
const cleanup = streamSSE('/x', { onLine: () => {} })
expect(typeof cleanup).toBe('function')
cleanup() // does not throw
await flush()
})

it('honors an external AbortSignal — aborting it aborts the compound', async () => {
;(globalThis as any).fetch = vi.fn(() => new Promise(() => {}))
const ctl = new AbortController()
const cleanup = streamSSE('/x', { onLine: () => {} }, ctl.signal)
expect(typeof cleanup).toBe('function')
ctl.abort()
// Just verify no throw + cleanup still callable.
cleanup()
await flush()
})
})
63 changes: 63 additions & 0 deletions src/pages/BlogPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* BlogPage.test.tsx — coverage for the public blog index. */
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import { render, cleanup } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { BlogPage } from './BlogPage'
import { POSTS } from '../content/posts'

let originalTitle = ''
beforeEach(() => {
originalTitle = document.title
document.title = 'instanode · Real infrastructure for AI agents'
})
afterEach(() => {
document.title = originalTitle
cleanup()
})

describe('BlogPage', () => {
it('sets document.title to "Blog · instanode" on mount', () => {
render(
<MemoryRouter>
<BlogPage />
</MemoryRouter>,
)
expect(document.title).toBe('Blog · instanode')
})

it('restores the previous document.title on unmount', () => {
const { unmount } = render(
<MemoryRouter>
<BlogPage />
</MemoryRouter>,
)
unmount()
expect(document.title).toBe('instanode · Real infrastructure for AI agents')
})

it('renders a card for every post, with link to /blog/<slug>', () => {
if (POSTS.length === 0) return
render(
<MemoryRouter>
<BlogPage />
</MemoryRouter>,
)
for (const p of POSTS) {
const link = document.querySelector(`a[href="/blog/${p.slug}"]`)
expect(link, `missing link for ${p.slug}`).toBeTruthy()
}
})

it('renders posts in reverse-chronological order', () => {
if (POSTS.length < 2) return
render(
<MemoryRouter>
<BlogPage />
</MemoryRouter>,
)
const cards = Array.from(document.querySelectorAll('a.blog-card-link'))
const slugs = cards.map((c) => (c.getAttribute('href') || '').replace('/blog/', ''))
const sorted = [...POSTS].sort((a, b) => b.date.localeCompare(a.date)).map((p) => p.slug)
expect(slugs).toEqual(sorted)
})
})
56 changes: 56 additions & 0 deletions src/pages/NotFoundPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* NotFoundPage.test.tsx — coverage tests for the SPA 404 catch-all. */
import { describe, it, expect, beforeEach } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { NotFoundPage } from './NotFoundPage'

describe('NotFoundPage', () => {
beforeEach(() => {
// jsdom defaults pathname to '/' — for one test we override.
})

it('renders 404 eyebrow and headline', () => {
render(
<MemoryRouter>
<NotFoundPage />
</MemoryRouter>,
)
expect(screen.getByText(/404/i)).toBeTruthy()
expect(screen.getByText(/that page is not provisioned/i)).toBeTruthy()
})

it('shows the current URL path in a <code>', () => {
render(
<MemoryRouter>
<NotFoundPage />
</MemoryRouter>,
)
// jsdom default location.pathname is '/' so the rendered code is '/'.
const code = document.querySelector('code.nf-url')
expect(code).toBeTruthy()
expect(code!.textContent).toBe(window.location.pathname || '/')
})

it('renders both primary and secondary CTAs', () => {
render(
<MemoryRouter>
<NotFoundPage />
</MemoryRouter>,
)
const home = document.querySelector('a.nf-cta-primary')
const docs = document.querySelector('a.nf-cta-secondary')
expect(home?.getAttribute('href')).toBe('/')
expect(docs?.getAttribute('href')).toBe('/docs')
})

it('lists the standard help links (pricing, use-cases, blog, changelog)', () => {
render(
<MemoryRouter>
<NotFoundPage />
</MemoryRouter>,
)
for (const href of ['/pricing', '/use-cases', '/blog', '/changelog']) {
expect(document.querySelector(`a[href="${href}"]`)).toBeTruthy()
}
})
})
39 changes: 39 additions & 0 deletions src/pages/PrivacyPage.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* PrivacyPage.test.tsx — coverage tests for the static legal stop-gap page. */
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { PrivacyPage } from './PrivacyPage'

describe('PrivacyPage', () => {
it('renders the privacy heading and testid wrapper', () => {
render(
<MemoryRouter>
<PrivacyPage />
</MemoryRouter>,
)
const section = screen.getByTestId('privacy-page')
expect(section).toBeTruthy()
expect(section.textContent).toContain('Privacy')
})

it('exposes a mailto link for the legal contact', () => {
render(
<MemoryRouter>
<PrivacyPage />
</MemoryRouter>,
)
const links = document.querySelectorAll('a[href^="mailto:"]')
expect(links.length).toBeGreaterThanOrEqual(1)
expect(links[0].getAttribute('href')).toBe('mailto:legal@instanode.dev')
})

it('mentions our sub-processors so reviewers can see them', () => {
render(
<MemoryRouter>
<PrivacyPage />
</MemoryRouter>,
)
expect(document.body.textContent ?? '').toContain('Razorpay')
expect(document.body.textContent ?? '').toContain('Resend')
})
})
Loading
Loading