- Add support for filtering errors by specific error codes - Implement `errors` query that accepts array of error code IDs - Add tests for error code filtering functionality - Update GraphQL schema with new error filtering capabilities
459 lines
13 KiB
TypeScript
459 lines
13 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { supabase } from '~/lib/supabase'
|
|
import { POST } from '../route'
|
|
|
|
describe('/api/graphql errors collection', () => {
|
|
it('returns a list of errors with pagination info', async () => {
|
|
// Get the expected order of errors from the database
|
|
const { data: dbErrors } = await supabase()
|
|
.schema('content')
|
|
.from('error')
|
|
.select('id, code, ...service(service:name), httpStatusCode:http_status_code, message')
|
|
.is('deleted_at', null)
|
|
.order('id', { ascending: true })
|
|
|
|
const errorsQuery = `
|
|
query {
|
|
errors(first: 2) {
|
|
totalCount
|
|
pageInfo {
|
|
hasNextPage
|
|
hasPreviousPage
|
|
startCursor
|
|
endCursor
|
|
}
|
|
edges {
|
|
cursor
|
|
node {
|
|
code
|
|
service
|
|
httpStatusCode
|
|
message
|
|
}
|
|
}
|
|
nodes {
|
|
code
|
|
service
|
|
httpStatusCode
|
|
message
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: errorsQuery }),
|
|
})
|
|
|
|
const result = await POST(request)
|
|
const {
|
|
data: { errors },
|
|
errors: queryErrors,
|
|
} = await result.json()
|
|
|
|
expect(queryErrors).toBeUndefined()
|
|
expect(errors.totalCount).toBe(3)
|
|
expect(errors.edges).toHaveLength(2)
|
|
expect(errors.nodes).toHaveLength(2)
|
|
expect(errors.pageInfo.hasNextPage).toBe(true)
|
|
expect(errors.pageInfo.hasPreviousPage).toBe(false)
|
|
expect(errors.pageInfo.startCursor).toBeDefined()
|
|
expect(errors.pageInfo.endCursor).toBeDefined()
|
|
|
|
// Compare against the first error from the database
|
|
expect(dbErrors).not.toBe(null)
|
|
const firstDbError = dbErrors![0]
|
|
const firstError = errors.nodes[0]
|
|
expect(firstError.code).toBe(firstDbError.code)
|
|
expect(firstError.service).toBe(firstDbError.service)
|
|
expect(firstError.httpStatusCode).toBe(firstDbError.httpStatusCode)
|
|
expect(firstError.message).toBe(firstDbError.message)
|
|
|
|
const firstEdge = errors.edges[0]
|
|
expect(firstEdge.cursor).toBeDefined()
|
|
expect(firstEdge.node).toEqual(firstError)
|
|
})
|
|
|
|
it('supports cursor-based pagination', async () => {
|
|
const firstPageQuery = `
|
|
query {
|
|
errors(first: 1) {
|
|
edges {
|
|
cursor
|
|
node {
|
|
code
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
hasPreviousPage
|
|
startCursor
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const firstRequest = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: firstPageQuery }),
|
|
})
|
|
|
|
const firstResult = await POST(firstRequest)
|
|
const firstJson = await firstResult.json()
|
|
expect(firstJson.errors).toBeUndefined()
|
|
expect(firstJson.data.errors.edges).toHaveLength(1)
|
|
expect(firstJson.data.errors.pageInfo.hasNextPage).toBe(true)
|
|
expect(firstJson.data.errors.pageInfo.hasPreviousPage).toBe(false)
|
|
|
|
const firstCursor = firstJson.data.errors.edges[0].cursor
|
|
|
|
const secondPageQuery = `
|
|
query {
|
|
errors(first: 1, after: "${firstCursor}") {
|
|
edges {
|
|
cursor
|
|
node {
|
|
code
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
hasPreviousPage
|
|
startCursor
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const secondRequest = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: secondPageQuery }),
|
|
})
|
|
|
|
const secondResult = await POST(secondRequest)
|
|
const secondJson = await secondResult.json()
|
|
expect(secondJson.errors).toBeUndefined()
|
|
expect(secondJson.data.errors.edges).toHaveLength(1)
|
|
expect(secondJson.data.errors.pageInfo.hasPreviousPage).toBe(true)
|
|
|
|
expect(firstJson.data.errors.edges[0].node.code).not.toBe(
|
|
secondJson.data.errors.edges[0].node.code
|
|
)
|
|
})
|
|
|
|
it('returns empty list when paginating past available results', async () => {
|
|
// Base64 encode the UUID that's guaranteed to be after any real data
|
|
const afterCursor = Buffer.from('ffffffff-ffff-ffff-ffff-ffffffffffff', 'utf8').toString(
|
|
'base64'
|
|
)
|
|
const query = `
|
|
query {
|
|
errors(first: 1, after: "${afterCursor}") {
|
|
edges {
|
|
cursor
|
|
node {
|
|
code
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
hasPreviousPage
|
|
startCursor
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query }),
|
|
})
|
|
|
|
const result = await POST(request)
|
|
const json = await result.json()
|
|
expect(json.errors).toBeUndefined()
|
|
expect(json.data.errors.edges).toHaveLength(0)
|
|
expect(json.data.errors.pageInfo.hasNextPage).toBe(false)
|
|
expect(json.data.errors.pageInfo.hasPreviousPage).toBe(true)
|
|
})
|
|
|
|
it('supports backward pagination with last', async () => {
|
|
const lastPageQuery = `
|
|
query {
|
|
errors(last: 1) {
|
|
edges {
|
|
cursor
|
|
node {
|
|
code
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
hasPreviousPage
|
|
startCursor
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const lastRequest = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: lastPageQuery }),
|
|
})
|
|
|
|
const lastResult = await POST(lastRequest)
|
|
const lastJson = await lastResult.json()
|
|
expect(lastJson.errors).toBeUndefined()
|
|
expect(lastJson.data.errors.edges).toHaveLength(1)
|
|
expect(lastJson.data.errors.pageInfo.hasNextPage).toBe(false)
|
|
expect(lastJson.data.errors.pageInfo.hasPreviousPage).toBe(true)
|
|
const lastCursor = lastJson.data.errors.edges[0].cursor
|
|
|
|
const beforeLastQuery = `
|
|
query {
|
|
errors(last: 1, before: "${lastCursor}") {
|
|
edges {
|
|
cursor
|
|
node {
|
|
code
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
hasPreviousPage
|
|
startCursor
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const beforeLastRequest = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: beforeLastQuery }),
|
|
})
|
|
const beforeLastResult = await POST(beforeLastRequest)
|
|
|
|
const beforeLastJson = await beforeLastResult.json()
|
|
expect(beforeLastJson.errors).toBeUndefined()
|
|
expect(beforeLastJson.data.errors.edges).toHaveLength(1)
|
|
expect(beforeLastJson.data.errors.pageInfo.hasNextPage).toBe(true)
|
|
expect(beforeLastJson.data.errors.edges[0].node.code).not.toBe(
|
|
lastJson.data.errors.edges[0].node.code
|
|
)
|
|
})
|
|
|
|
it('filters by service when service argument is provided', async () => {
|
|
// First, get all errors to check we have errors from different services
|
|
const allErrorsQuery = `
|
|
query {
|
|
errors {
|
|
nodes {
|
|
service
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const allRequest = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: allErrorsQuery }),
|
|
})
|
|
const allResult = await POST(allRequest)
|
|
const allJson = await allResult.json()
|
|
expect(allJson.errors).toBeUndefined()
|
|
|
|
// Verify we have errors from multiple services
|
|
const services = new Set(allJson.data.errors.nodes.map((e: any) => e.service))
|
|
expect(services.size).toBeGreaterThan(1)
|
|
|
|
// Test filtering by AUTH service
|
|
const authErrorsQuery = `
|
|
query {
|
|
errors(service: AUTH) {
|
|
totalCount
|
|
nodes {
|
|
code
|
|
service
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const authRequest = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: authErrorsQuery }),
|
|
})
|
|
|
|
const authResult = await POST(authRequest)
|
|
const authJson = await authResult.json()
|
|
expect(authJson.errors).toBeUndefined()
|
|
|
|
// Verify all returned errors are from AUTH service
|
|
expect(authJson.data.errors.nodes.length).toBeGreaterThan(0)
|
|
expect(authJson.data.errors.nodes.every((e: any) => e.service === 'AUTH')).toBe(true)
|
|
})
|
|
|
|
it('supports service filtering with pagination', async () => {
|
|
const firstPageQuery = `
|
|
query {
|
|
errors(service: AUTH, first: 1) {
|
|
edges {
|
|
cursor
|
|
node {
|
|
code
|
|
service
|
|
}
|
|
}
|
|
pageInfo {
|
|
hasNextPage
|
|
endCursor
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const firstRequest = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: firstPageQuery }),
|
|
})
|
|
|
|
const firstResult = await POST(firstRequest)
|
|
const firstJson = await firstResult.json()
|
|
expect(firstJson.errors).toBeUndefined()
|
|
|
|
// Verify the returned error is from AUTH service
|
|
expect(firstJson.data.errors.edges[0].node.service).toBe('AUTH')
|
|
|
|
// If there are more AUTH errors, test pagination
|
|
if (firstJson.data.errors.pageInfo.hasNextPage) {
|
|
const cursor = firstJson.data.errors.pageInfo.endCursor
|
|
const secondPageQuery = `
|
|
query {
|
|
errors(service: AUTH, first: 1, after: "${cursor}") {
|
|
edges {
|
|
node {
|
|
code
|
|
service
|
|
}
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const secondRequest = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: secondPageQuery }),
|
|
})
|
|
|
|
const secondResult = await POST(secondRequest)
|
|
const secondJson = await secondResult.json()
|
|
expect(secondJson.errors).toBeUndefined()
|
|
|
|
// Verify the second page also returns AUTH errors
|
|
expect(secondJson.data.errors.edges[0].node.service).toBe('AUTH')
|
|
// And it's a different error
|
|
expect(secondJson.data.errors.edges[0].node.code).not.toBe(
|
|
firstJson.data.errors.edges[0].node.code
|
|
)
|
|
}
|
|
})
|
|
|
|
it('filters by code when code argument is provided', async () => {
|
|
// First, get the first error code from the database to test with
|
|
const { data: dbErrors } = await supabase()
|
|
.schema('content')
|
|
.from('error')
|
|
.select('code')
|
|
.is('deleted_at', null)
|
|
.limit(1)
|
|
|
|
expect(dbErrors).not.toBe(null)
|
|
expect(dbErrors).toHaveLength(1)
|
|
const testCode = dbErrors![0].code
|
|
|
|
const codeFilterQuery = `
|
|
query {
|
|
errors(code: "${testCode}") {
|
|
totalCount
|
|
nodes {
|
|
code
|
|
service
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: codeFilterQuery }),
|
|
})
|
|
|
|
const result = await POST(request)
|
|
const json = await result.json()
|
|
expect(json.errors).toBeUndefined()
|
|
|
|
// Verify all returned errors have the specified code
|
|
expect(json.data.errors.nodes.length).toBeGreaterThan(0)
|
|
expect(json.data.errors.nodes.every((e: any) => e.code === testCode)).toBe(true)
|
|
})
|
|
|
|
it('filters by both service and code when both arguments are provided', async () => {
|
|
// Get an error that exists for AUTH service
|
|
const { data: authError } = await supabase()
|
|
.schema('content')
|
|
.from('error')
|
|
.select('code, ...service(service:name)')
|
|
.is('deleted_at', null)
|
|
.eq('service.name', 'AUTH')
|
|
.limit(1)
|
|
|
|
expect(authError).not.toBe(null)
|
|
expect(authError).toHaveLength(1)
|
|
const testCode = authError![0].code
|
|
|
|
const bothFiltersQuery = `
|
|
query {
|
|
errors(service: AUTH, code: "${testCode}") {
|
|
totalCount
|
|
nodes {
|
|
code
|
|
service
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: bothFiltersQuery }),
|
|
})
|
|
|
|
const result = await POST(request)
|
|
const json = await result.json()
|
|
expect(json.errors).toBeUndefined()
|
|
|
|
// Verify all returned errors match both filters
|
|
expect(json.data.errors.nodes.length).toBeGreaterThan(0)
|
|
expect(
|
|
json.data.errors.nodes.every((e: any) => e.code === testCode && e.service === 'AUTH')
|
|
).toBe(true)
|
|
})
|
|
|
|
it('returns empty list when code filter matches no errors', async () => {
|
|
const nonExistentCodeQuery = `
|
|
query {
|
|
errors(code: "NONEXISTENT_CODE_12345") {
|
|
totalCount
|
|
nodes {
|
|
code
|
|
}
|
|
}
|
|
}
|
|
`
|
|
const request = new Request('http://localhost/api/graphql', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ query: nonExistentCodeQuery }),
|
|
})
|
|
|
|
const result = await POST(request)
|
|
const json = await result.json()
|
|
expect(json.errors).toBeUndefined()
|
|
expect(json.data.errors.totalCount).toBe(0)
|
|
expect(json.data.errors.nodes).toHaveLength(0)
|
|
})
|
|
})
|