Repository: svix/svix-webhooks (language: rust)

Issue:
Operational Endpoints cannot be filtered by Operational Webhook event types
## Bug Report

### Version

0.82.1

### Platform

Linux 74d00365c75d 5.10.102.1-microsoft-standard-WSL2 x86_64 GNU/Linux

### Description

Unable to specify which operational webhook events to subscribe/filter to. The API rejects a request to add an event filter to an operational webhook endpoint with an `HTTP 422`  when you try to add a event, (e.g. `message.attempt.failing` ) stating that:
"The following event types don't exist: `message.attempt.failing`"

The API request body I sent to `POST https://my.svix.address.goes.here/api/v1/app/{app_id}/endpoint/` used is here:

```
{
    "url": "https://any.old.url/srsly",
    "version": 1,
    "filterTypes": [
        "message.attempt.failing",
        "message.attempt.exhausted"
    ]
}

```

You should be able to specify the names of operational webhooks as `filterTypes` when creating OR updating an operational endpoint.


Discussion / hints:
Yup, 100%. The problem is that it's missing a command to add them to the operational webhook account.

We'll take a look!

Btw, one easy workaround is to create the event types yourself like so. The AUTH_TOKEN should be your *operational webhook auth token*, not your normal one.
```
curl -X 'POST' \
  'https://your-svix-server/api/v1/event-type/' \
  -H 'Authorization: Bearer AUTH_TOKEN' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "user.signup",
    "description": "A user has signed up"
}'
```
I wonder if it makes more sense to add it as a database migration that just puts in the required event types, or as a manual command that can be done in code.

I think a database migration would be cleaner.
~@tasn A migration would be nice, but there's not a clean way to specify the appropriate org-id in a migration, is there? I'm not sure it's safe to assume that the 'default' org-id is the one that will be used in all cases.~
Scratch the previous. I'll write the migrations using the hard-coded "management" org-id. 

Fix patch:
diff --git a/server/svix-server/migrations/20230505222507_operational_webhook_event-types.down.sql b/server/svix-server/migrations/20230505222507_operational_webhook_event-types.down.sql
new file mode 100644
index 000000000..e24c27809
--- /dev/null
+++ b/server/svix-server/migrations/20230505222507_operational_webhook_event-types.down.sql
@@ -0,0 +1,1 @@
+DELETE FROM eventtype where id in ('evtype_2POKhK6pz91yFxPrGh92s2g38js', 'evtype_2POKhK6hm0oHxMc3eXrtoeUmWGH', 'evtype_2POKhK6QKJWGeDVSWXY2vJgDSQB', 'evtype_2POKhK6vcRRnl2tyeEOfcQ15C4x', 'evtype_2POKhK70XG6GQHcqTkeGvID3EvB', 'evtype_2POKhK6MvCdGn7rQmnj8yDYBuGJ', 'evtype_2POKhK7H4djUarm92hUYHt2HDI9'); 
\ No newline at end of file
diff --git a/server/svix-server/migrations/20230505222507_operational_webhook_event-types.up.sql b/server/svix-server/migrations/20230505222507_operational_webhook_event-types.up.sql
new file mode 100644
index 000000000..85601c914
--- /dev/null
+++ b/server/svix-server/migrations/20230505222507_operational_webhook_event-types.up.sql
@@ -0,0 +1,23 @@
+INSERT INTO eventtype(
+    created_at, updated_at, id, org_id, description, deleted, schemas, name, feature_flag)
+    VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'evtype_2POKhK6pz91yFxPrGh92s2g38js', 'org_00000000000SvixManagement00', 'Sent when an endpoint is created.', false, '{"1": {"type": "object", "title": "EndpointCreatedEvent", "examples": [{"data": {"appId": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2", "appUid": "unique-app-identifier", "endpointId": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2", "endpointUid": "unique-endpoint-identifier"}, "type": "endpoint.created"}], "required": ["data"], "properties": {"data": {"$ref": "#/definitions/EndpointCreatedEventData"}, "type": {"enum": ["endpoint.created"], "type": "string", "title": "Type", "default": "endpoint.created"}}, "definitions": {"EndpointCreatedEventData": {"type": "object", "title": "EndpointCreatedEventData", "required": ["appId", "endpointId"], "properties": {"appId": {"type": "string", "title": "Appid", "example": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "appUid": {"type": "string", "title": "Appuid", "example": "unique-app-identifier", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the application"}, "endpointId": {"type": "string", "title": "Endpointid", "example": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "endpointUid": {"type": "string", "title": "Endpointuid", "example": "unique-endpoint-identifier", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the endpoint"}}}}, "description": "Sent when an endpoint is created."}}', 'endpoint.created', NULL);
+
+INSERT INTO eventtype(
+    created_at, updated_at, id, org_id, description, deleted, schemas, name, feature_flag)
+    VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'evtype_2POKhK6QKJWGeDVSWXY2vJgDSQB', 'org_00000000000SvixManagement00', 'Sent when an endpoint is updated.', false, '{"1": {"type": "object", "title": "EndpointUpdatedEvent", "examples": [{"data": {"appId": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2", "appUid": "unique-app-identifier", "endpointId": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2", "endpointUid": "unique-endpoint-identifier"}, "type": "endpoint.updated"}], "required": ["data"], "properties": {"data": {"$ref": "#/definitions/EndpointUpdatedEventData"}, "type": {"enum": ["endpoint.updated"], "type": "string", "title": "Type", "default": "endpoint.updated"}}, "definitions": {"EndpointUpdatedEventData": {"type": "object", "title": "EndpointUpdatedEventData", "required": ["appId", "endpointId"], "properties": {"appId": {"type": "string", "title": "Appid", "example": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "appUid": {"type": "string", "title": "Appuid", "example": "unique-app-identifier", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the application"}, "endpointId": {"type": "string", "title": "Endpointid", "example": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "endpointUid": {"type": "string", "title": "Endpointuid", "example": "unique-endpoint-identifier", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the endpoint"}}}}, "description": "Sent when an endpoint is updated."}}', 'endpoint.updated', NULL);
+
+INSERT INTO eventtype(
+    created_at, updated_at, id, org_id, description, deleted, schemas, name, feature_flag)
+    VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'evtype_2POKhK6vcRRnl2tyeEOfcQ15C4x', 'org_00000000000SvixManagement00', 'Sent when an endpoint is deleted.', false, '{"1": {"type": "object", "title": "EndpointDeletedEvent", "examples": [{"data": {"appId": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2", "appUid": "unique-app-identifier", "endpointId": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2", "endpointUid": "unique-endpoint-identifier"}, "type": "endpoint.deleted"}], "required": ["data"], "properties": {"data": {"$ref": "#/definitions/EndpointDeletedEventData"}, "type": {"enum": ["endpoint.deleted"], "type": "string", "title": "Type", "default": "endpoint.deleted"}}, "definitions": {"EndpointDeletedEventData": {"type": "object", "title": "EndpointDeletedEventData", "required": ["appId", "endpointId"], "properties": {"appId": {"type": "string", "title": "Appid", "example": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "appUid": {"type": "string", "title": "Appuid", "example": "unique-app-identifier", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the application"}, "endpointId": {"type": "string", "title": "Endpointid", "example": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "endpointUid": {"type": "string", "title": "Endpointuid", "example": "unique-endpoint-identifier", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the endpoint"}}}}, "description": "Sent when an endpoint is deleted."}}', 'endpoint.deleted', NULL);
+
+INSERT INTO eventtype(
+    created_at, updated_at, id, org_id, description, deleted, schemas, name, feature_flag)
+    VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'evtype_2POKhK70XG6GQHcqTkeGvID3EvB', 'org_00000000000SvixManagement00', 'Sent when an endpoint has been automatically disabled after continuous failures.', false, '{"1": {"type": "object", "title": "EndpointDisabledEvent", "examples": [{"data": {"appId": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2", "appUid": "unique-app-identifier", "failSince": "1970-01-01T00:00:00", "endpointId": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2", "endpointUid": "unique-endpoint-identifier"}, "type": "endpoint.disabled"}], "required": ["data"], "properties": {"data": {"$ref": "#/definitions/EndpointDisabledEventData"}, "type": {"enum": ["endpoint.disabled"], "type": "string", "title": "Type", "default": "endpoint.disabled"}}, "definitions": {"EndpointDisabledEventData": {"type": "object", "title": "EndpointDisabledEventData", "required": ["appId", "endpointId", "failSince"], "properties": {"appId": {"type": "string", "title": "Appid", "example": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "appUid": {"type": "string", "title": "Appuid", "example": "unique-app-identifier", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the application"}, "failSince": {"type": "string", "title": "Failsince", "format": "date-time"}, "endpointId": {"type": "string", "title": "Endpointid", "example": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "endpointUid": {"type": "string", "title": "Endpointuid", "example": "unique-endpoint-identifier", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the endpoint"}}}}, "description": "Sent when an endpoint has been automatically disabled after continuous failures."}}', 'endpoint.disabled', NULL);
+
+INSERT INTO eventtype(
+    created_at, updated_at, id, org_id, description, deleted, schemas, name, feature_flag)
+    VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'evtype_2POKhK6MvCdGn7rQmnj8yDYBuGJ', 'org_00000000000SvixManagement00', 'Sent after a message has been failing for a few times.', false, '{"1": {"type": "object", "title": "MessageAttemptFailingEvent", "examples": [{"data": {"appId": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2", "msgId": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2", "appUid": "unique-app-identifier", "endpointId": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2", "msgEventId": "unique-msg-identifier", "lastAttempt": {"id": "atmpt_1srOrx2ZWZBpBUvZwXKQmoEYga2", "timestamp": "1970-01-01T00:00:00", "responseStatusCode": 500}}, "type": "message.attempt.failing"}], "required": ["data"], "properties": {"data": {"$ref": "#/definitions/MessageAttemptFailingEventData"}, "type": {"enum": ["message.attempt.failing"], "type": "string", "title": "Type", "default": "message.attempt.failing"}}, "definitions": {"MessageAttemptFailedData": {"type": "object", "title": "MessageAttemptFailedData", "required": ["id", "responseStatusCode", "timestamp"], "properties": {"id": {"type": "string", "title": "Id", "example": "atmpt_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "timestamp": {"type": "string", "title": "Timestamp", "format": "date-time"}, "responseStatusCode": {"type": "integer", "title": "Responsestatuscode", "example": 500}}}, "MessageAttemptFailingEventData": {"type": "object", "title": "MessageAttemptFailingEventData", "required": ["appId", "msgId", "endpointId", "lastAttempt"], "properties": {"appId": {"type": "string", "title": "Appid", "example": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "msgId": {"type": "string", "title": "Msgid", "example": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "appUid": {"type": "string", "title": "Appuid", "example": "unique-app-identifier", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the application"}, "endpointId": {"type": "string", "title": "Endpointid", "example": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "msgEventId": {"type": "string", "title": "Msgeventid", "example": "evt_pNZKtWg8Azow", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the message"}, "lastAttempt": {"$ref": "#/definitions/MessageAttemptFailedData"}}}}, "description": "Sent after a message has been failing for a few times.\nSent on the fourth failure. It complements `message.attempt.exhausted` which is sent after the last failure."}}', 'message.attempt.failing', NULL);
+
+INSERT INTO eventtype(
+    created_at, updated_at, id, org_id, description, deleted, schemas, name, feature_flag)
+    VALUES (CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'evtype_2POKhK7H4djUarm92hUYHt2HDI9', 'org_00000000000SvixManagement00', 'Sent when a message delivery has failed (all of the retry attempts have been exhausted).', false, '{"1": {"type": "object", "title": "MessageAttemptExhaustedEvent", "examples": [{"data": {"appId": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2", "msgId": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2", "appUid": "unique-app-identifier", "endpointId": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2", "msgEventId": "unique-msg-identifier", "lastAttempt": {"id": "atmpt_1srOrx2ZWZBpBUvZwXKQmoEYga2", "timestamp": "1970-01-01T00:00:00", "responseStatusCode": 500}}, "type": "message.attempt.exhausted"}], "required": ["data"], "properties": {"data": {"$ref": "#/definitions/MessageAttemptExhaustedEventData"}, "type": {"enum": ["message.attempt.exhausted"], "type": "string", "title": "Type", "default": "message.attempt.exhausted"}}, "definitions": {"MessageAttemptFailedData": {"type": "object", "title": "MessageAttemptFailedData", "required": ["id", "responseStatusCode", "timestamp"], "properties": {"id": {"type": "string", "title": "Id", "example": "atmpt_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "timestamp": {"type": "string", "title": "Timestamp", "format": "date-time"}, "responseStatusCode": {"type": "integer", "title": "Responsestatuscode", "example": 500}}}, "MessageAttemptExhaustedEventData": {"type": "object", "title": "MessageAttemptExhaustedEventData", "required": ["appId", "msgId", "endpointId", "lastAttempt"], "properties": {"appId": {"type": "string", "title": "Appid", "example": "app_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "msgId": {"type": "string", "title": "Msgid", "example": "msg_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "appUid": {"type": "string", "title": "Appuid", "example": "unique-app-identifier", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the application"}, "endpointId": {"type": "string", "title": "Endpointid", "example": "ep_1srOrx2ZWZBpBUvZwXKQmoEYga2"}, "msgEventId": {"type": "string", "title": "Msgeventid", "example": "evt_pNZKtWg8Azow", "pattern": "^[a-zA-Z0-9\\-_.]+$", "nullable": true, "maxLength": 256, "minLength": 1, "description": "Optional unique identifier for the message"}, "lastAttempt": {"$ref": "#/definitions/MessageAttemptFailedData"}}}}, "description": "Sent when a message delivery has failed (all of the retry attempts have been exhausted)."}}', 'message.attempt.exhausted', NULL);
diff --git a/server/svix-server/src/core/security.rs b/server/svix-server/src/core/security.rs
index 6a57c57c8..2cc3dc5d0 100644
--- a/server/svix-server/src/core/security.rs
+++ b/server/svix-server/src/core/security.rs
@@ -26,7 +26,7 @@ pub fn default_org_id() -> OrganizationId {
     OrganizationId("org_23rb8YdGqMT0qIzpgGwdXfHirMu".to_owned())
 }
 
-/// The default org_id we use (useful for generating JWTs when testing).
+/// The default Operational Webhooks org_id
 pub fn management_org_id() -> OrganizationId {
     OrganizationId("org_00000000000SvixManagement00".to_owned())
 }


Test patch:
diff --git a/server/svix-server/tests/e2e_operational_webhooks.rs b/server/svix-server/tests/e2e_operational_webhooks.rs
index e967bbfd8..53d969cb4 100644
--- a/server/svix-server/tests/e2e_operational_webhooks.rs
+++ b/server/svix-server/tests/e2e_operational_webhooks.rs
@@ -8,6 +8,7 @@ use http::StatusCode;
 
 use reqwest::Url;
 use serde::Deserialize;
+use svix::api::EventTypeOut;
 use svix_ksuid::KsuidLike;
 use svix_server::{
     cfg::ConfigurationInner,
@@ -420,3 +421,24 @@ async fn test_message_attempt_operational_webhooks() {
         panic!("Invalid type for op_webhook_out")
     }
 }
+
+#[tokio::test]
+async fn test_operational_webhooks_event_types_exist() {
+    let cfg = get_default_test_config();
+    let (_, client_op, _, _jh) = start_svix_server_with_operational_webhooks(cfg);
+
+    for et in &[
+        "message.attempt.failing",
+        "message.attempt.exhausted",
+        "endpoint.created",
+        "endpoint.deleted",
+        "endpoint.disabled",
+        "endpoint.created",
+        "endpoint.updated",
+    ] {
+        let _: EventTypeOut = client_op
+            .get(&format!("api/v1/event-type/{et}/"), StatusCode::OK)
+            .await
+            .unwrap();
+    }
+}