Skip to content
Draft
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
172 changes: 172 additions & 0 deletions __tests__/api/delegation-example.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* @jest-environment node
*
* Tests for src/app/api/delegation-example/route.ts
*
* Covers the POST handler and the isValidAddress helper (indirectly).
*/

import { NextRequest } from 'next/server'
import { POST } from '@/src/app/api/delegation-example/route'

function makeRequest(body: unknown): NextRequest {
return new NextRequest('http://localhost/api/delegation-example', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
}

const VALID_DELEGATOR = '0x1234567890123456789012345678901234567890'
const VALID_DELEGATE = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'

// ---------------------------------------------------------------------------
// Valid delegate action
// ---------------------------------------------------------------------------

describe('POST /api/delegation-example – delegate action', () => {
it('returns 200 with success payload for valid addresses', async () => {
const req = makeRequest({
delegatorAddress: VALID_DELEGATOR,
delegateAddress: VALID_DELEGATE,
action: 'delegate',
})
const res = await POST(req)
expect(res.status).toBe(200)
const json = await res.json()
expect(json.success).toBe(true)
expect(json.txHash).toMatch(/^0x/)
expect(json.delegator).toBe(VALID_DELEGATOR)
expect(json.delegate).toBe(VALID_DELEGATE)
expect(typeof json.message).toBe('string')
})
})

// ---------------------------------------------------------------------------
// Valid undelegate action
// ---------------------------------------------------------------------------

describe('POST /api/delegation-example – undelegate action', () => {
it('returns 200 with success payload for valid addresses', async () => {
const req = makeRequest({
delegatorAddress: VALID_DELEGATOR,
delegateAddress: VALID_DELEGATE,
action: 'undelegate',
})
const res = await POST(req)
expect(res.status).toBe(200)
const json = await res.json()
expect(json.success).toBe(true)
expect(json.txHash).toMatch(/^0x/)
expect(json.delegator).toBe(VALID_DELEGATOR)
})
})

// ---------------------------------------------------------------------------
// Invalid address validation
// ---------------------------------------------------------------------------

describe('POST /api/delegation-example – address validation', () => {
it('returns 400 when delegatorAddress is not a valid 0x address', async () => {
const req = makeRequest({
delegatorAddress: 'not-an-address',
delegateAddress: VALID_DELEGATE,
action: 'delegate',
})
const res = await POST(req)
expect(res.status).toBe(400)
const json = await res.json()
expect(json.error).toBe('Invalid address format')
})

it('returns 400 when delegateAddress is not a valid 0x address', async () => {
const req = makeRequest({
delegatorAddress: VALID_DELEGATOR,
delegateAddress: 'bad',
action: 'delegate',
})
const res = await POST(req)
expect(res.status).toBe(400)
const json = await res.json()
expect(json.error).toBe('Invalid address format')
})

it('returns 400 when address is too short', async () => {
const req = makeRequest({
delegatorAddress: '0x1234',
delegateAddress: VALID_DELEGATE,
action: 'delegate',
})
const res = await POST(req)
expect(res.status).toBe(400)
})

it('returns 400 when address is too long', async () => {
const req = makeRequest({
delegatorAddress: '0x12345678901234567890123456789012345678901',
delegateAddress: VALID_DELEGATE,
action: 'delegate',
})
const res = await POST(req)
expect(res.status).toBe(400)
})

it('returns 400 when address contains non-hex characters', async () => {
const req = makeRequest({
delegatorAddress: '0xgggggggggggggggggggggggggggggggggggggggg',
delegateAddress: VALID_DELEGATE,
action: 'delegate',
})
const res = await POST(req)
expect(res.status).toBe(400)
})

it('accepts addresses with uppercase hex characters', async () => {
const req = makeRequest({
delegatorAddress: '0xABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCD',
delegateAddress: VALID_DELEGATE,
action: 'delegate',
})
const res = await POST(req)
expect(res.status).toBe(200)
})
})

// ---------------------------------------------------------------------------
// Unknown action
// ---------------------------------------------------------------------------

describe('POST /api/delegation-example – unknown action', () => {
it('returns 400 for an unrecognised action', async () => {
const req = makeRequest({
delegatorAddress: VALID_DELEGATOR,
delegateAddress: VALID_DELEGATE,
action: 'transfer',
})
const res = await POST(req)
expect(res.status).toBe(400)
const json = await res.json()
expect(json.error).toBe('Unknown action')
})
})

// ---------------------------------------------------------------------------
// Malformed body → 500
// ---------------------------------------------------------------------------

describe('POST /api/delegation-example – malformed body', () => {
it('returns 500 when the request body is not valid JSON', async () => {
const req = new NextRequest(
'http://localhost/api/delegation-example',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: 'not json',
}
)
const res = await POST(req)
expect(res.status).toBe(500)
const json = await res.json()
expect(json.error).toBe('Internal server error')
})
})
99 changes: 99 additions & 0 deletions __tests__/api/frame-image.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* @jest-environment node
*
* Tests for src/app/api/frame/image/route.ts
*
* Covers the GET handler and the SVG image generation logic.
*/

import { NextRequest } from 'next/server'
import { GET } from '@/src/app/api/frame/image/route'

function makeRequest(params: Record<string, string> = {}): NextRequest {
const url = new URL('http://localhost/api/frame/image')
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v))
return new NextRequest(url.toString())
}

// ---------------------------------------------------------------------------
// Response shape
// ---------------------------------------------------------------------------

describe('GET /api/frame/image', () => {
it('returns status 200', async () => {
const res = await GET(makeRequest())
expect(res.status).toBe(200)
})

it('sets Content-Type to image/svg+xml', async () => {
const res = await GET(makeRequest())
expect(res.headers.get('Content-Type')).toBe('image/svg+xml')
})

it('sets Cache-Control header', async () => {
const res = await GET(makeRequest())
expect(res.headers.get('Cache-Control')).toContain('max-age=300')
})

it('returns a valid SVG string', async () => {
const res = await GET(makeRequest())
const body = await res.text()
expect(body).toContain('<svg')
expect(body).toContain('</svg>')
})
})

// ---------------------------------------------------------------------------
// generateFrameImage action variants
// ---------------------------------------------------------------------------

describe('GET /api/frame/image – action messages', () => {
it('default action (no params) renders the default title', async () => {
const res = await GET(makeRequest())
const body = await res.text()
expect(body).toContain('OP Delegation Frame')
})

it('action=1 renders the delegation-sent title', async () => {
const res = await GET(makeRequest({ action: '1' }))
const body = await res.text()
expect(body).toContain('Delegation Sent')
})

it('action=1 with delegate address includes the address in subtitle', async () => {
const delegate = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'
const res = await GET(makeRequest({ action: '1', delegate }))
const body = await res.text()
expect(body).toContain(delegate)
})

it('action=1 with no delegate address falls back to "address"', async () => {
const res = await GET(makeRequest({ action: '1' }))
const body = await res.text()
expect(body).toContain('address')
})

it('action=2 renders the undelegated title', async () => {
const res = await GET(makeRequest({ action: '2' }))
const body = await res.text()
expect(body).toContain('Undelegated')
})

it('action=3 renders the voting-history title', async () => {
const res = await GET(makeRequest({ action: '3' }))
const body = await res.text()
expect(body).toContain('Voting History')
})

it('unknown action falls back to the default message', async () => {
const res = await GET(makeRequest({ action: '99' }))
const body = await res.text()
expect(body).toContain('OP Delegation Frame')
})

it('action=0 renders the default title explicitly', async () => {
const res = await GET(makeRequest({ action: '0' }))
const body = await res.text()
expect(body).toContain('OP Delegation Frame')
})
})
57 changes: 57 additions & 0 deletions __tests__/components/GridCard.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* Tests for the handleStatus utility exported from GridCard.tsx.
*
* handleStatus maps an execution-status string to the corresponding
* status label. We render the returned JSX with React Testing Library
* and assert the visible text.
*/

import React from 'react'
import { render, screen } from '@testing-library/react'
import { handleStatus } from '@/src/app/(navbar)/(home)/component/GridCard'

function renderStatus(status: string) {
return render(<div>{handleStatus(status)}</div>)
}

describe('handleStatus', () => {
it('renders "Not Started" for status "not-started"', () => {
renderStatus('not-started')
expect(screen.getByText('Not Started')).toBeInTheDocument()
})

it('renders "In Discussion" for status "in-discussion"', () => {
renderStatus('in-discussion')
expect(screen.getByText('In Discussion')).toBeInTheDocument()
})

it('renders "In Progress - Open" for status "in-progress-open"', () => {
renderStatus('in-progress-open')
expect(screen.getByText('In Progress - Open')).toBeInTheDocument()
})

it('renders "In Progress - Closed" for status "in-progress-closed"', () => {
renderStatus('in-progress-closed')
expect(screen.getByText('In Progress - Closed')).toBeInTheDocument()
})

it('renders "Completed" for status "completed"', () => {
renderStatus('completed')
expect(screen.getByText('Completed')).toBeInTheDocument()
})

it('renders "Abandoned" for status "abandoned"', () => {
renderStatus('abandoned')
expect(screen.getByText('Abandoned')).toBeInTheDocument()
})

it('defaults to "Not Started" for an unknown status', () => {
renderStatus('unknown-status')
expect(screen.getByText('Not Started')).toBeInTheDocument()
})

it('defaults to "Not Started" for an empty string', () => {
renderStatus('')
expect(screen.getByText('Not Started')).toBeInTheDocument()
})
})
Loading
Loading