chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
import crypto from 'node:crypto'
|
||||
import { NextRequest } from 'next/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { incidentioHandler } from '@/lib/webhooks/providers/incidentio'
|
||||
|
||||
const SECRET_BYTES = Buffer.from('incidentio-test-secret-key-padding!!')
|
||||
const SIGNING_SECRET = `whsec_${SECRET_BYTES.toString('base64')}`
|
||||
|
||||
function signIncidentioBody(msgId: string, timestamp: string, rawBody: string): string {
|
||||
const toSign = `${msgId}.${timestamp}.${rawBody}`
|
||||
const sig = crypto.createHmac('sha256', SECRET_BYTES).update(toSign, 'utf8').digest('base64')
|
||||
return `v1,${sig}`
|
||||
}
|
||||
|
||||
function requestWithSvixHeaders(
|
||||
msgId: string,
|
||||
timestamp: string,
|
||||
rawBody: string,
|
||||
signature?: string
|
||||
): NextRequest {
|
||||
const headers: Record<string, string> = {
|
||||
'webhook-id': msgId,
|
||||
'webhook-timestamp': timestamp,
|
||||
}
|
||||
if (signature !== undefined) {
|
||||
headers['webhook-signature'] = signature
|
||||
}
|
||||
return new NextRequest('http://localhost/test', { headers })
|
||||
}
|
||||
|
||||
const baseAuthCtx = {
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
rawBody: '',
|
||||
}
|
||||
|
||||
describe('incident.io webhook provider', () => {
|
||||
it('rejects requests when the signing secret is missing', async () => {
|
||||
const res = await incidentioHandler.verifyAuth!({
|
||||
...baseAuthCtx,
|
||||
request: requestWithSvixHeaders('msg_1', `${Math.floor(Date.now() / 1000)}`, '{}'),
|
||||
rawBody: '{}',
|
||||
requestId: 'incidentio-t1',
|
||||
providerConfig: {},
|
||||
})
|
||||
|
||||
expect(res?.status).toBe(401)
|
||||
})
|
||||
|
||||
it('rejects requests missing Svix signature headers', async () => {
|
||||
const rawBody = JSON.stringify({ event_type: 'public_incident.incident_created_v2' })
|
||||
const ts = `${Math.floor(Date.now() / 1000)}`
|
||||
|
||||
const res = await incidentioHandler.verifyAuth!({
|
||||
...baseAuthCtx,
|
||||
request: requestWithSvixHeaders('msg_1', ts, rawBody),
|
||||
rawBody,
|
||||
requestId: 'incidentio-t2',
|
||||
providerConfig: { signingSecret: SIGNING_SECRET },
|
||||
})
|
||||
|
||||
expect(res?.status).toBe(401)
|
||||
})
|
||||
|
||||
it('rejects requests with an invalid signature', async () => {
|
||||
const rawBody = JSON.stringify({ event_type: 'public_incident.incident_created_v2' })
|
||||
const ts = `${Math.floor(Date.now() / 1000)}`
|
||||
|
||||
const res = await incidentioHandler.verifyAuth!({
|
||||
...baseAuthCtx,
|
||||
request: requestWithSvixHeaders('msg_1', ts, rawBody, 'v1,not-a-valid-signature'),
|
||||
rawBody,
|
||||
requestId: 'incidentio-t3',
|
||||
providerConfig: { signingSecret: SIGNING_SECRET },
|
||||
})
|
||||
|
||||
expect(res?.status).toBe(401)
|
||||
})
|
||||
|
||||
it('rejects requests when the timestamp skew is too large', async () => {
|
||||
const rawBody = JSON.stringify({ event_type: 'public_incident.incident_created_v2' })
|
||||
const ts = `${Math.floor(Date.now() / 1000) - 600}`
|
||||
const signature = signIncidentioBody('msg_1', ts, rawBody)
|
||||
|
||||
const res = await incidentioHandler.verifyAuth!({
|
||||
...baseAuthCtx,
|
||||
request: requestWithSvixHeaders('msg_1', ts, rawBody, signature),
|
||||
rawBody,
|
||||
requestId: 'incidentio-t4',
|
||||
providerConfig: { signingSecret: SIGNING_SECRET },
|
||||
})
|
||||
|
||||
expect(res?.status).toBe(401)
|
||||
})
|
||||
|
||||
it('accepts a correctly signed request within the allowed window', async () => {
|
||||
const rawBody = JSON.stringify({ event_type: 'public_incident.incident_created_v2' })
|
||||
const ts = `${Math.floor(Date.now() / 1000)}`
|
||||
const signature = signIncidentioBody('msg_1', ts, rawBody)
|
||||
|
||||
const res = await incidentioHandler.verifyAuth!({
|
||||
...baseAuthCtx,
|
||||
request: requestWithSvixHeaders('msg_1', ts, rawBody, signature),
|
||||
rawBody,
|
||||
requestId: 'incidentio-t5',
|
||||
providerConfig: { signingSecret: SIGNING_SECRET },
|
||||
})
|
||||
|
||||
expect(res).toBeNull()
|
||||
})
|
||||
|
||||
it('matches events by event_type for a specific trigger', async () => {
|
||||
const body = { event_type: 'public_incident.incident_created_v2' }
|
||||
|
||||
const matched = await incidentioHandler.matchEvent!({
|
||||
body,
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
request: new NextRequest('http://localhost/test'),
|
||||
requestId: 'incidentio-t6',
|
||||
providerConfig: { triggerId: 'incidentio_incident_created' },
|
||||
})
|
||||
expect(matched).toBe(true)
|
||||
|
||||
const mismatched = await incidentioHandler.matchEvent!({
|
||||
body,
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
request: new NextRequest('http://localhost/test'),
|
||||
requestId: 'incidentio-t7',
|
||||
providerConfig: { triggerId: 'incidentio_incident_updated' },
|
||||
})
|
||||
expect(mismatched).toBe(false)
|
||||
})
|
||||
|
||||
it('formats incident_created input from the directly-nested wrapper', async () => {
|
||||
const incident = {
|
||||
id: 'inc_123',
|
||||
reference: 'INC-123',
|
||||
name: 'Database outage',
|
||||
summary: 'DB is sad',
|
||||
incident_status: { id: 'st_1', name: 'Investigating' },
|
||||
severity: { id: 'sev_1', name: 'Major' },
|
||||
mode: 'standard',
|
||||
visibility: 'public',
|
||||
permalink: 'https://app.incident.io/incidents/123',
|
||||
created_at: '2021-08-17T13:28:57.801578Z',
|
||||
updated_at: '2021-08-17T13:28:57.801578Z',
|
||||
}
|
||||
const body = {
|
||||
event_type: 'public_incident.incident_created_v2',
|
||||
'public_incident.incident_created_v2': incident,
|
||||
}
|
||||
|
||||
const result = await incidentioHandler.formatInput!({
|
||||
body,
|
||||
webhook: {},
|
||||
workflow: { id: 'wf_1', userId: 'user_1' },
|
||||
headers: {},
|
||||
requestId: 'incidentio-t8',
|
||||
})
|
||||
|
||||
expect(result.input).toEqual({
|
||||
event_type: 'public_incident.incident_created_v2',
|
||||
incident,
|
||||
incident_id: 'inc_123',
|
||||
name: 'Database outage',
|
||||
reference: 'INC-123',
|
||||
summary: 'DB is sad',
|
||||
incident_status: { id: 'st_1', name: 'Investigating' },
|
||||
severity: { id: 'sev_1', name: 'Major' },
|
||||
mode: 'standard',
|
||||
visibility: 'public',
|
||||
permalink: 'https://app.incident.io/incidents/123',
|
||||
created_at: '2021-08-17T13:28:57.801578Z',
|
||||
updated_at: '2021-08-17T13:28:57.801578Z',
|
||||
new_status: null,
|
||||
previous_status: null,
|
||||
update_message: null,
|
||||
payload: body,
|
||||
})
|
||||
})
|
||||
|
||||
it('formats incident_status_updated input from the nested incident + status change fields', async () => {
|
||||
const incident = { id: 'inc_123', reference: 'INC-123', name: 'Database outage' }
|
||||
const new_status = { id: 'st_2', name: 'Resolved' }
|
||||
const previous_status = { id: 'st_1', name: 'Investigating' }
|
||||
const body = {
|
||||
event_type: 'public_incident.incident_status_updated_v2',
|
||||
'public_incident.incident_status_updated_v2': {
|
||||
incident,
|
||||
new_status,
|
||||
previous_status,
|
||||
message: 'Fixed it',
|
||||
},
|
||||
}
|
||||
|
||||
const result = await incidentioHandler.formatInput!({
|
||||
body,
|
||||
webhook: {},
|
||||
workflow: { id: 'wf_1', userId: 'user_1' },
|
||||
headers: {},
|
||||
requestId: 'incidentio-t8b',
|
||||
})
|
||||
|
||||
expect(result.input).toMatchObject({
|
||||
event_type: 'public_incident.incident_status_updated_v2',
|
||||
incident,
|
||||
incident_id: 'inc_123',
|
||||
new_status,
|
||||
previous_status,
|
||||
update_message: 'Fixed it',
|
||||
payload: body,
|
||||
})
|
||||
})
|
||||
|
||||
it('formats alert_created (v1) input from the directly-nested wrapper', async () => {
|
||||
const alert = {
|
||||
id: 'alrt_1',
|
||||
title: 'CPU high',
|
||||
description: 'CPU exceeded 75%',
|
||||
status: 'firing',
|
||||
alert_source_id: 'src_1',
|
||||
deduplication_key: 'dedup_1',
|
||||
source_url: 'https://alerts.example.com/1',
|
||||
created_at: '2021-08-17T13:28:57.801578Z',
|
||||
updated_at: '2021-08-17T13:28:57.801578Z',
|
||||
resolved_at: '2021-08-17T14:28:57.801578Z',
|
||||
}
|
||||
const body = {
|
||||
event_type: 'public_alert.alert_created_v1',
|
||||
'public_alert.alert_created_v1': alert,
|
||||
}
|
||||
|
||||
const result = await incidentioHandler.formatInput!({
|
||||
body,
|
||||
webhook: {},
|
||||
workflow: { id: 'wf_1', userId: 'user_1' },
|
||||
headers: {},
|
||||
requestId: 'incidentio-t8c',
|
||||
})
|
||||
|
||||
expect(result.input).toEqual({
|
||||
event_type: 'public_alert.alert_created_v1',
|
||||
alert,
|
||||
alert_id: 'alrt_1',
|
||||
title: 'CPU high',
|
||||
description: 'CPU exceeded 75%',
|
||||
status: 'firing',
|
||||
alert_source_id: 'src_1',
|
||||
deduplication_key: 'dedup_1',
|
||||
source_url: 'https://alerts.example.com/1',
|
||||
created_at: '2021-08-17T13:28:57.801578Z',
|
||||
updated_at: '2021-08-17T13:28:57.801578Z',
|
||||
resolved_at: '2021-08-17T14:28:57.801578Z',
|
||||
payload: body,
|
||||
})
|
||||
})
|
||||
|
||||
it('matches the alert_created trigger against public_alert.alert_created_v1', async () => {
|
||||
const matched = await incidentioHandler.matchEvent!({
|
||||
body: { event_type: 'public_alert.alert_created_v1' },
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
request: new NextRequest('http://localhost/test'),
|
||||
requestId: 'incidentio-t9b',
|
||||
providerConfig: { triggerId: 'incidentio_alert_created' },
|
||||
})
|
||||
expect(matched).toBe(true)
|
||||
})
|
||||
|
||||
it('extracts an idempotency id from event_type and entity id', () => {
|
||||
const body = {
|
||||
event_type: 'public_incident.incident_created_v2',
|
||||
'public_incident.incident_created_v2': { incident: { id: 'inc_123' } },
|
||||
}
|
||||
|
||||
expect(incidentioHandler.extractIdempotencyId!(body)).toBe(
|
||||
'public_incident.incident_created_v2:inc_123'
|
||||
)
|
||||
expect(incidentioHandler.extractIdempotencyId!({})).toBeNull()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user