From 58ed57ba1c9d8e56205c09b87a613385c1dcd902 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Thu, 17 Apr 2025 10:48:15 -0700 Subject: [PATCH 1/6] feat: Implement chainhooks durable object and associated changes --- src/config.ts | 2 +- src/durable-objects/chainhooks-do.ts | 111 +++++++++++++++++++++++++++ src/index.ts | 9 ++- worker-configuration.d.ts | 1 + wrangler.toml | 16 ++++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 src/durable-objects/chainhooks-do.ts diff --git a/src/config.ts b/src/config.ts index 051f41e..be70a3f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,7 +50,7 @@ export class AppConfig { return { // supported services for API caching // each entry is a durable object that handles requests - SUPPORTED_SERVICES: ['/bns', '/hiro-api', '/stx-city', '/supabase', '/contract-calls'], + SUPPORTED_SERVICES: ['/bns', '/hiro-api', '/stx-city', '/supabase', '/contract-calls', '/chainhooks'], // VALUES BELOW CAN BE OVERRIDDEN BY DURABLE OBJECTS // default cache TTL used for KV CACHE_TTL: 900, // 15 minutes diff --git a/src/durable-objects/chainhooks-do.ts b/src/durable-objects/chainhooks-do.ts new file mode 100644 index 0000000..6ea56f0 --- /dev/null +++ b/src/durable-objects/chainhooks-do.ts @@ -0,0 +1,111 @@ +import { DurableObject } from 'cloudflare:workers'; +import { Env } from '../../worker-configuration'; +import { AppConfig } from '../config'; +import { handleRequest } from '../utils/request-handler-util'; +import { ApiError } from '../utils/api-error-util'; +import { ErrorCode } from '../utils/error-catalog-util'; +import { Logger } from '../utils/logger-util'; + +export class ChainhooksDO extends DurableObject { + // Configuration constants + private readonly BASE_PATH: string = '/chainhooks'; + private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', ''); + private readonly SUPPORTED_ENDPOINTS: string[] = ['/post_event']; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.ctx = ctx; + this.env = env; + + // Initialize AppConfig with environment + const config = AppConfig.getInstance(env).getConfig(); + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const path = url.pathname; + const method = request.method; + + return handleRequest( + async () => { + if (!path.startsWith(this.BASE_PATH)) { + throw new ApiError(ErrorCode.NOT_FOUND, { resource: path }); + } + + // Remove base path to get the endpoint + const endpoint = path.replace(this.BASE_PATH, ''); + + // Handle root path + if (endpoint === '' || endpoint === '/') { + return { + message: `Supported endpoints: ${this.SUPPORTED_ENDPOINTS.join(', ')}`, + }; + } + + // Handle post_event endpoint + if (endpoint === '/post_event') { + if (method !== 'POST') { + throw new ApiError(ErrorCode.INVALID_REQUEST, { + reason: `Method ${method} not allowed for this endpoint. Use POST.`, + }); + } + + return await this.handlePostEvent(request); + } + + // If we get here, the endpoint is not supported + throw new ApiError(ErrorCode.NOT_FOUND, { + resource: endpoint, + supportedEndpoints: this.SUPPORTED_ENDPOINTS, + }); + }, + this.env, + { + path, + method, + } + ); + } + + private async handlePostEvent(request: Request): Promise { + const logger = Logger.getInstance(this.env); + + try { + // Clone the request to read the body + const clonedRequest = request.clone(); + + // Try to parse as JSON first + let body; + try { + body = await clonedRequest.json(); + } catch (e) { + // If JSON parsing fails, get the body as text + body = await request.text(); + } + + // Log the received event + logger.info('Received chainhook event', { + body, + headers: Object.fromEntries(request.headers.entries()), + }); + + // Store the event in Durable Object storage for later analysis + const eventId = crypto.randomUUID(); + await this.ctx.storage.put(`event_${eventId}`, { + timestamp: new Date().toISOString(), + body, + headers: Object.fromEntries(request.headers.entries()), + }); + + return { + message: 'Event received and logged successfully', + eventId, + }; + } catch (error) { + logger.error('Error processing chainhook event', error instanceof Error ? error : new Error(String(error))); + throw new ApiError(ErrorCode.INTERNAL_ERROR, { + reason: 'Failed to process chainhook event', + }); + } + } +} diff --git a/src/index.ts b/src/index.ts index 676e1fa..b3ef707 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,13 +5,14 @@ import { HiroApiDO } from './durable-objects/hiro-api-do'; import { StxCityDO } from './durable-objects/stx-city-do'; import { SupabaseDO } from './durable-objects/supabase-do'; import { ContractCallsDO } from './durable-objects/contract-calls-do'; +import { ChainhooksDO } from './durable-objects/chainhooks-do'; import { corsHeaders, createErrorResponse, createSuccessResponse } from './utils/requests-responses-util'; import { ApiError } from './utils/api-error-util'; import { ErrorCode } from './utils/error-catalog-util'; import { Logger } from './utils/logger-util'; // export the Durable Object classes we're using -export { BnsApiDO, HiroApiDO, StxCityDO, SupabaseDO, ContractCallsDO }; +export { BnsApiDO, HiroApiDO, StxCityDO, SupabaseDO, ContractCallsDO, ChainhooksDO }; export default { /** @@ -90,6 +91,12 @@ export default { let stub = env.CONTRACT_CALLS_DO.get(id); // get the stub for communication return await stub.fetch(request); // forward the request to the Durable Object } + + if (path.startsWith('/chainhooks')) { + let id: DurableObjectId = env.CHAINHOOKS_DO.idFromName('chainhooks-do'); // create the instance + let stub = env.CHAINHOOKS_DO.get(id); // get the stub for communication + return await stub.fetch(request); // forward the request to the Durable Object + } } catch (error) { // Log errors from Durable Objects const duration = Date.now() - startTime; diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 2163063..35ddbb9 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -10,4 +10,5 @@ export interface Env { STX_CITY_DO: DurableObjectNamespace; SUPABASE_DO: DurableObjectNamespace; CONTRACT_CALLS_DO: DurableObjectNamespace; + CHAINHOOKS_DO: DurableObjectNamespace; } diff --git a/wrangler.toml b/wrangler.toml index f0987cb..f5fc2f0 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -17,6 +17,10 @@ new_classes = ["HiroApiDO", "SupabaseDO", "StxCityDO", "BnsApiDO"] tag = "20250323" new_classes = ["ContractCallsDO"] +[[migrations]] +tag = "20250417" +new_classes = ["ChainhooksDO"] + [env.preview] @@ -46,6 +50,10 @@ class_name = "BnsApiDO" name = "CONTRACT_CALLS_DO" class_name = "ContractCallsDO" +[[env.preview.durable_objects.bindings]] +name = "CHAINHOOKS_DO" +class_name = "ChainhooksDO" + [env.staging] @@ -75,6 +83,10 @@ class_name = "BnsApiDO" name = "CONTRACT_CALLS_DO" class_name = "ContractCallsDO" +[[env.staging.durable_objects.bindings]] +name = "CHAINHOOKS_DO" +class_name = "ChainhooksDO" + [env.production] @@ -103,3 +115,7 @@ class_name = "BnsApiDO" [[env.production.durable_objects.bindings]] name = "CONTRACT_CALLS_DO" class_name = "ContractCallsDO" + +[[env.production.durable_objects.bindings]] +name = "CHAINHOOKS_DO" +class_name = "ChainhooksDO" From 72bf01872512709d2d0c0f76eefc8701678e13a2 Mon Sep 17 00:00:00 2001 From: Jason Schrader Date: Thu, 17 Apr 2025 10:53:12 -0700 Subject: [PATCH 2/6] chore: formatting --- src/durable-objects/chainhooks-do.ts | 202 +++++++++++++-------------- 1 file changed, 101 insertions(+), 101 deletions(-) diff --git a/src/durable-objects/chainhooks-do.ts b/src/durable-objects/chainhooks-do.ts index 6ea56f0..8e60668 100644 --- a/src/durable-objects/chainhooks-do.ts +++ b/src/durable-objects/chainhooks-do.ts @@ -7,105 +7,105 @@ import { ErrorCode } from '../utils/error-catalog-util'; import { Logger } from '../utils/logger-util'; export class ChainhooksDO extends DurableObject { - // Configuration constants - private readonly BASE_PATH: string = '/chainhooks'; - private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', ''); - private readonly SUPPORTED_ENDPOINTS: string[] = ['/post_event']; - - constructor(ctx: DurableObjectState, env: Env) { - super(ctx, env); - this.ctx = ctx; - this.env = env; - - // Initialize AppConfig with environment - const config = AppConfig.getInstance(env).getConfig(); - } - - async fetch(request: Request): Promise { - const url = new URL(request.url); - const path = url.pathname; - const method = request.method; - - return handleRequest( - async () => { - if (!path.startsWith(this.BASE_PATH)) { - throw new ApiError(ErrorCode.NOT_FOUND, { resource: path }); - } - - // Remove base path to get the endpoint - const endpoint = path.replace(this.BASE_PATH, ''); - - // Handle root path - if (endpoint === '' || endpoint === '/') { - return { - message: `Supported endpoints: ${this.SUPPORTED_ENDPOINTS.join(', ')}`, - }; - } - - // Handle post_event endpoint - if (endpoint === '/post_event') { - if (method !== 'POST') { - throw new ApiError(ErrorCode.INVALID_REQUEST, { - reason: `Method ${method} not allowed for this endpoint. Use POST.`, - }); - } - - return await this.handlePostEvent(request); - } - - // If we get here, the endpoint is not supported - throw new ApiError(ErrorCode.NOT_FOUND, { - resource: endpoint, - supportedEndpoints: this.SUPPORTED_ENDPOINTS, - }); - }, - this.env, - { - path, - method, - } - ); - } - - private async handlePostEvent(request: Request): Promise { - const logger = Logger.getInstance(this.env); - - try { - // Clone the request to read the body - const clonedRequest = request.clone(); - - // Try to parse as JSON first - let body; - try { - body = await clonedRequest.json(); - } catch (e) { - // If JSON parsing fails, get the body as text - body = await request.text(); - } - - // Log the received event - logger.info('Received chainhook event', { - body, - headers: Object.fromEntries(request.headers.entries()), - }); - - // Store the event in Durable Object storage for later analysis - const eventId = crypto.randomUUID(); - await this.ctx.storage.put(`event_${eventId}`, { - timestamp: new Date().toISOString(), - body, - headers: Object.fromEntries(request.headers.entries()), - }); - - return { - message: 'Event received and logged successfully', - eventId, - }; - } catch (error) { - logger.error('Error processing chainhook event', error instanceof Error ? error : new Error(String(error))); - throw new ApiError(ErrorCode.INTERNAL_ERROR, { - reason: 'Failed to process chainhook event', - }); - } - } + // Configuration constants + private readonly BASE_PATH: string = '/chainhooks'; + private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', ''); + private readonly SUPPORTED_ENDPOINTS: string[] = ['/post_event']; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.ctx = ctx; + this.env = env; + + // Initialize AppConfig with environment + const config = AppConfig.getInstance(env).getConfig(); + } + + async fetch(request: Request): Promise { + const url = new URL(request.url); + const path = url.pathname; + const method = request.method; + + return handleRequest( + async () => { + if (!path.startsWith(this.BASE_PATH)) { + throw new ApiError(ErrorCode.NOT_FOUND, { resource: path }); + } + + // Remove base path to get the endpoint + const endpoint = path.replace(this.BASE_PATH, ''); + + // Handle root path + if (endpoint === '' || endpoint === '/') { + return { + message: `Supported endpoints: ${this.SUPPORTED_ENDPOINTS.join(', ')}`, + }; + } + + // Handle post_event endpoint + if (endpoint === '/post_event') { + if (method !== 'POST') { + throw new ApiError(ErrorCode.INVALID_REQUEST, { + reason: `Method ${method} not allowed for this endpoint. Use POST.`, + }); + } + + return await this.handlePostEvent(request); + } + + // If we get here, the endpoint is not supported + throw new ApiError(ErrorCode.NOT_FOUND, { + resource: endpoint, + supportedEndpoints: this.SUPPORTED_ENDPOINTS, + }); + }, + this.env, + { + path, + method, + } + ); + } + + private async handlePostEvent(request: Request): Promise { + const logger = Logger.getInstance(this.env); + + try { + // Clone the request to read the body + const clonedRequest = request.clone(); + + // Try to parse as JSON first + let body; + try { + body = await clonedRequest.json(); + } catch (e) { + // If JSON parsing fails, get the body as text + body = await request.text(); + } + + // Log the received event + logger.info('Received chainhook event', { + body, + headers: Object.fromEntries(request.headers.entries()), + }); + + // Store the event in Durable Object storage for later analysis + const eventId = crypto.randomUUID(); + await this.ctx.storage.put(`event_${eventId}`, { + timestamp: new Date().toISOString(), + body, + headers: Object.fromEntries(request.headers.entries()), + }); + + return { + message: 'Event received and logged successfully', + eventId, + }; + } catch (error) { + logger.error('Error processing chainhook event', error instanceof Error ? error : new Error(String(error))); + throw new ApiError(ErrorCode.INTERNAL_ERROR, { + reason: 'Failed to process chainhook event', + }); + } + } } From 51fffb3a2ec3c649042c1d007d5e682e9c071757 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Thu, 17 Apr 2025 10:58:39 -0700 Subject: [PATCH 3/6] refactor: Use hyphen instead of underscore in /post-event endpoint --- src/durable-objects/chainhooks-do.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/durable-objects/chainhooks-do.ts b/src/durable-objects/chainhooks-do.ts index 8e60668..6920c35 100644 --- a/src/durable-objects/chainhooks-do.ts +++ b/src/durable-objects/chainhooks-do.ts @@ -10,7 +10,7 @@ export class ChainhooksDO extends DurableObject { // Configuration constants private readonly BASE_PATH: string = '/chainhooks'; private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', ''); - private readonly SUPPORTED_ENDPOINTS: string[] = ['/post_event']; + private readonly SUPPORTED_ENDPOINTS: string[] = ['/post-event']; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); @@ -42,8 +42,8 @@ export class ChainhooksDO extends DurableObject { }; } - // Handle post_event endpoint - if (endpoint === '/post_event') { + // Handle post-event endpoint + if (endpoint === '/post-event') { if (method !== 'POST') { throw new ApiError(ErrorCode.INVALID_REQUEST, { reason: `Method ${method} not allowed for this endpoint. Use POST.`, From 235537e3cac1942bb1f143c4e293785ad3f9136d Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Thu, 17 Apr 2025 11:00:22 -0700 Subject: [PATCH 4/6] feat: Add endpoints to get a specific event or all events --- src/durable-objects/chainhooks-do.ts | 81 +++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/src/durable-objects/chainhooks-do.ts b/src/durable-objects/chainhooks-do.ts index 6920c35..6372225 100644 --- a/src/durable-objects/chainhooks-do.ts +++ b/src/durable-objects/chainhooks-do.ts @@ -10,7 +10,7 @@ export class ChainhooksDO extends DurableObject { // Configuration constants private readonly BASE_PATH: string = '/chainhooks'; private readonly CACHE_PREFIX: string = this.BASE_PATH.replaceAll('/', ''); - private readonly SUPPORTED_ENDPOINTS: string[] = ['/post-event']; + private readonly SUPPORTED_ENDPOINTS: string[] = ['/post-event', '/events', '/events/:id']; constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); @@ -53,6 +53,29 @@ export class ChainhooksDO extends DurableObject { return await this.handlePostEvent(request); } + // Handle get all events endpoint + if (endpoint === '/events') { + if (method !== 'GET') { + throw new ApiError(ErrorCode.INVALID_REQUEST, { + reason: `Method ${method} not allowed for this endpoint. Use GET.`, + }); + } + + return await this.handleGetAllEvents(); + } + + // Handle get specific event endpoint + if (endpoint.startsWith('/events/')) { + if (method !== 'GET') { + throw new ApiError(ErrorCode.INVALID_REQUEST, { + reason: `Method ${method} not allowed for this endpoint. Use GET.`, + }); + } + + const eventId = endpoint.replace('/events/', ''); + return await this.handleGetEvent(eventId); + } + // If we get here, the endpoint is not supported throw new ApiError(ErrorCode.NOT_FOUND, { resource: endpoint, @@ -108,4 +131,60 @@ export class ChainhooksDO extends DurableObject { }); } } + + private async handleGetEvent(eventId: string): Promise { + const logger = Logger.getInstance(this.env); + + try { + // Retrieve the event from storage + const event = await this.ctx.storage.get(`event_${eventId}`); + + if (!event) { + throw new ApiError(ErrorCode.NOT_FOUND, { + resource: `Event with ID ${eventId}`, + }); + } + + return { + event, + }; + } catch (error) { + if (error instanceof ApiError) { + throw error; + } + + logger.error(`Error retrieving event ${eventId}`, error instanceof Error ? error : new Error(String(error))); + throw new ApiError(ErrorCode.INTERNAL_ERROR, { + reason: `Failed to retrieve event ${eventId}`, + }); + } + } + + private async handleGetAllEvents(): Promise { + const logger = Logger.getInstance(this.env); + + try { + // Get all keys that start with "event_" + const eventKeys = await this.ctx.storage.list({ prefix: 'event_' }); + + // Create an array to hold all events + const events: Record = {}; + + // Retrieve each event and add it to the array + for (const [key, value] of eventKeys) { + const eventId = key.replace('event_', ''); + events[eventId] = value; + } + + return { + events, + count: Object.keys(events).length, + }; + } catch (error) { + logger.error('Error retrieving all events', error instanceof Error ? error : new Error(String(error))); + throw new ApiError(ErrorCode.INTERNAL_ERROR, { + reason: 'Failed to retrieve events', + }); + } + } } From 4bdb2fccdc9ec36fa78a02fd5bdfd6a4e5691a19 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Thu, 17 Apr 2025 11:02:39 -0700 Subject: [PATCH 5/6] feat: Implement Bearer token authentication for /post-event endpoint --- src/durable-objects/chainhooks-do.ts | 29 ++++++++++++++++++++++++++++ worker-configuration.d.ts | 1 + 2 files changed, 30 insertions(+) diff --git a/src/durable-objects/chainhooks-do.ts b/src/durable-objects/chainhooks-do.ts index 6372225..738cd00 100644 --- a/src/durable-objects/chainhooks-do.ts +++ b/src/durable-objects/chainhooks-do.ts @@ -50,6 +50,13 @@ export class ChainhooksDO extends DurableObject { }); } + // Check authentication + if (!this.validateAuthToken(request)) { + throw new ApiError(ErrorCode.UNAUTHORIZED, { + reason: 'Invalid or missing authentication token', + }); + } + return await this.handlePostEvent(request); } @@ -187,4 +194,26 @@ export class ChainhooksDO extends DurableObject { }); } } + + /** + * Validates the authentication token from the request + * + * @param request - The incoming request + * @returns boolean indicating if the token is valid + */ + private validateAuthToken(request: Request): boolean { + // Extract the Authorization header + const authHeader = request.headers.get('Authorization'); + + // Check if the header exists and has the correct format + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return false; + } + + // Extract the token + const token = authHeader.replace('Bearer ', ''); + + // Compare with the stored token + return token === this.env.CHAINHOOKS_AUTH_TOKEN; + } } diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 35ddbb9..f7637ee 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -5,6 +5,7 @@ export interface Env { HIRO_API_KEY: string; SUPABASE_URL: string; SUPABASE_SERVICE_KEY: string; + CHAINHOOKS_AUTH_TOKEN: string; // Auth token for chainhooks POST endpoint BNS_API_DO: DurableObjectNamespace; HIRO_API_DO: DurableObjectNamespace; STX_CITY_DO: DurableObjectNamespace; From 221565ce507f35731288dd4807c08e4b9eee1663 Mon Sep 17 00:00:00 2001 From: "Jason Schrader (aider)" Date: Thu, 17 Apr 2025 11:07:48 -0700 Subject: [PATCH 6/6] feat: Add tests for Chainhooks Durable Object endpoints --- tests/run_tests.sh | 2 + tests/test_chainhooks.sh | 95 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 tests/test_chainhooks.sh diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 3e821ff..2e6ad79 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -15,6 +15,7 @@ source "$SCRIPT_DIR/test_stx_city.sh" source "$SCRIPT_DIR/test_supabase.sh" source "$SCRIPT_DIR/test_bns.sh" source "$SCRIPT_DIR/test_contract_calls.sh" +source "$SCRIPT_DIR/test_chainhooks.sh" # If sleep flag is true, wait 10 seconds before starting tests if [ "$SLEEP_BEFORE_START" = true ]; then @@ -31,6 +32,7 @@ test_index #test_supabase (deprecated) #test_bns (deprecated) test_contract_calls +test_chainhooks echo "====================" echo "Test Summary" diff --git a/tests/test_chainhooks.sh b/tests/test_chainhooks.sh new file mode 100644 index 0000000..3879c32 --- /dev/null +++ b/tests/test_chainhooks.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +# Set default API URL from argument if provided +export API_URL=${1:-"http://localhost:8787"} + +source "$(dirname "$0")/utils.sh" + +test_chainhooks() { + echo "====================" + echo "ChainhooksDO Tests" + echo "====================" + + # Test base endpoint + test_cors "/chainhooks" "Base endpoint CORS" + test_endpoint "/chainhooks" 200 "Base endpoint" + + # Test events endpoint (GET all events) + test_cors "/chainhooks/events" "Events endpoint CORS" + test_endpoint "/chainhooks/events" 200 "Get all events" + + # Test post-event endpoint without auth (should fail with 401) + echo "Testing post-event without auth (should fail)..." + local post_url="${API_URL}/chainhooks/post-event" + local payload='{"test":"data"}' + + # Test CORS for post-event endpoint + test_cors "/chainhooks/post-event" "Post event CORS" + + # Test unauthorized post (should return 401) + local unauth_response=$(curl -s -X POST -H "Content-Type: application/json" -d "$payload" "$post_url") + local unauth_status=$(echo "$unauth_response" | jq -r '.success // false') + + if [ "$unauth_status" == "false" ]; then + echo -e "${GREEN}✓${NC} Unauthorized post correctly rejected" + ((TOTAL_TESTS++)) + else + echo -e "${RED}✗${NC} Unauthorized post should have been rejected: $unauth_response" + ((TOTAL_TESTS++)) + ((FAILED_TESTS++)) + fi + + # Test post-event with auth token (if available in environment) + if [ -n "$CHAINHOOKS_AUTH_TOKEN" ]; then + echo "Testing post-event with auth..." + local auth_response=$(curl -s -X POST -H "Content-Type: application/json" -H "Authorization: Bearer $CHAINHOOKS_AUTH_TOKEN" -d "$payload" "$post_url") + local auth_status=$(echo "$auth_response" | jq -r '.success // false') + local event_id=$(echo "$auth_response" | jq -r '.data.eventId // ""') + + if [ "$auth_status" == "true" ] && [ -n "$event_id" ]; then + echo -e "${GREEN}✓${NC} Authorized post successful" + ((TOTAL_TESTS++)) + + # Now test retrieving the specific event we just created + echo "Testing get specific event..." + test_endpoint "/chainhooks/events/$event_id" 200 "Get specific event" + else + echo -e "${RED}✗${NC} Authorized post failed: $auth_response" + ((TOTAL_TESTS++)) + ((FAILED_TESTS++)) + fi + else + echo "Skipping authorized post test (CHAINHOOKS_AUTH_TOKEN not set)" + fi + + # Test invalid event ID + test_endpoint "/chainhooks/events/invalid-id" 404 "Invalid event ID" + + # Test invalid endpoints + test_cors "/chainhooks/invalid" "Invalid endpoint CORS" + test_endpoint "/chainhooks/invalid" 404 "Invalid endpoint" +} + +# Allow running just this test file +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + export FAILED_TESTS=0 + export TOTAL_TESTS=0 + + echo -e "\nTesting Chainhooks API at: $API_URL" + test_chainhooks + + echo "====================" + echo "Test Summary" + echo "====================" + echo "Passed tests: $((TOTAL_TESTS - FAILED_TESTS))" + echo "Failed tests: $FAILED_TESTS" + echo "Total tests: $TOTAL_TESTS" + + if [ $FAILED_TESTS -eq 0 ]; then + echo -e "${GREEN}All tests passed!${NC}" + exit 0 + else + echo -e "${RED}Some tests failed!${NC}" + exit 1 + fi +fi