Skip to content
Merged
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
10 changes: 10 additions & 0 deletions docs/api/trust-graph.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,12 @@ paths:
id:
type: string
format: uuid
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"503":
$ref: "#/components/responses/ServiceUnavailable"
"500":
$ref: "#/components/responses/InternalError"

Expand Down Expand Up @@ -288,6 +294,10 @@ paths:
type: integer
"400":
$ref: "#/components/responses/BadRequest"
"404":
$ref: "#/components/responses/NotFound"
"503":
$ref: "#/components/responses/ServiceUnavailable"
"500":
$ref: "#/components/responses/InternalError"

Expand Down
21 changes: 17 additions & 4 deletions services/trust-graph/src/routes/trust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ export function createTrustRoutes(db: DbClient, discoveryUrl?: string) {
const id = await trustService.recordIncident(db, body)
return c.json({ id }, 201)
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return c.json({ error: message }, 400)
const mapped = mapTrustWriteError(error)
return c.json({ error: mapped.message }, mapped.status)
}
})

Expand All @@ -94,8 +94,8 @@ export function createTrustRoutes(db: DbClient, discoveryUrl?: string) {
const result = await trustService.recordRevocation(db, body)
return c.json(result, 201)
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'
return c.json({ error: message }, 400)
const mapped = mapTrustWriteError(error)
return c.json({ error: mapped.message }, mapped.status)
}
})

Expand Down Expand Up @@ -140,3 +140,16 @@ function mapTrustLookupError(error: unknown): { status: 404 | 500 | 503; message

return { status: 500, message: 'Internal server error' }
}

function mapTrustWriteError(error: unknown): { status: 400 | 404 | 500 | 503; message: string } {
const lookup = mapTrustLookupError(error)
if (lookup.status !== 500) {
return lookup
}

if (error instanceof TrustError || error instanceof Error) {
return { status: 400, message: error.message }
}

return { status: 400, message: 'Unknown error' }
}
88 changes: 88 additions & 0 deletions services/trust-graph/test/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,50 @@ describe('HTTP Routes', () => {
expect(json.error).toContain('signed revocation record')
})

it('POST /v1/revocations should return 404 when revoker identity is absent from discovery', async () => {
const record = await signedRevocationRecord()
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve(new Response('not found', { status: 404 }))))
mockDb.select = vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({
limit: vi.fn(() => Promise.resolve([])),
})),
})),
}))

const app = createTrustRoutes(mockDb, 'http://discovery.test')
const res = await app.request('/v1/revocations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ record }),
})

expect(res.status).toBe(404)
expect((await res.json()).error).toContain('Identity not found: did:fides:principal')
})

it('POST /v1/revocations should return 503 when discovery is unavailable for revoker identity', async () => {
const record = await signedRevocationRecord()
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve(new Response('unavailable', { status: 503 }))))
mockDb.select = vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({
limit: vi.fn(() => Promise.resolve([])),
})),
})),
}))

const app = createTrustRoutes(mockDb, 'http://discovery.test')
const res = await app.request('/v1/revocations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ record }),
})

expect(res.status).toBe(503)
expect((await res.json()).error).toContain('Discovery service unavailable')
})

it('GET /v1/revocations/:did should return latest revocation state', async () => {
const record = await signedRevocationRecord()
mockDb.select = vi.fn(() => ({
Expand Down Expand Up @@ -522,6 +566,50 @@ describe('HTTP Routes', () => {
expect(json.error).toContain('signed incident record')
})

it('POST /v1/incidents should return 404 when reporter identity is absent from discovery', async () => {
const record = await signedIncidentRecord()
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve(new Response('not found', { status: 404 }))))
mockDb.select = vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({
limit: vi.fn(() => Promise.resolve([])),
})),
})),
}))

const app = createTrustRoutes(mockDb, 'http://discovery.test')
const res = await app.request('/v1/incidents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ actorDid: record.actor, record }),
})

expect(res.status).toBe(404)
expect((await res.json()).error).toContain('Identity not found: did:fides:principal')
})

it('POST /v1/incidents should return 503 when discovery is unavailable for reporter identity', async () => {
const record = await signedIncidentRecord()
vi.stubGlobal('fetch', vi.fn(() => Promise.resolve(new Response('unavailable', { status: 503 }))))
mockDb.select = vi.fn(() => ({
from: vi.fn(() => ({
where: vi.fn(() => ({
limit: vi.fn(() => Promise.resolve([])),
})),
})),
}))

const app = createTrustRoutes(mockDb, 'http://discovery.test')
const res = await app.request('/v1/incidents', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ actorDid: record.actor, record }),
})

expect(res.status).toBe(503)
expect((await res.json()).error).toContain('Discovery service unavailable')
})

it('POST /v1/incidents should accept agentd incident payloads', async () => {
const app = createTrustRoutes(mockDb)
const record = await signedAgentdIncidentRecord()
Expand Down