Mocking NICE Cognigy Webhook Endpoints with C# for Automated Integration Testing
What You Will Build
- A local HTTP server that intercepts incoming Cognigy webhook payloads, validates them against the platform schema, and returns configurable HTTP status codes with simulated network latency.
- A C# control layer that manages mock instance limits, tracks request capture events, verifies header parity, and exposes a REST interface for external test runners.
- A complete .NET 8 implementation that synchronizes with Cognigy OAuth 2.0, registers webhook endpoints programmatically, and generates structured audit logs for test governance.
Prerequisites
- Cognigy OAuth 2.0 Client Credentials grant with
bot:read,webhook:read, andwebhook:writescopes - Cognigy REST API v2.0
- .NET 8.0 SDK or later
- NuGet packages:
Microsoft.AspNetCore.Builder,System.Text.Json,System.Text.Json.Schema,Serilog,Serilog.Sinks.File,Polly
Authentication Setup
Cognigy uses standard OAuth 2.0 for API access. You must retrieve a bearer token before registering or querying webhook configurations. The following method implements token acquisition with retry logic for rate limiting and explicit error handling.
using System.Net.Http.Json;
using System.Text.Json;
using Polly;
public class CognigyAuthClient
{
private readonly HttpClient _httpClient;
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _baseUrl;
public CognigyAuthClient(string clientId, string clientSecret, string baseUrl = "https://api.cognigy.com")
{
_httpClient = new HttpClient();
_httpClient.Timeout = TimeSpan.FromSeconds(30);
_clientId = clientId;
_clientSecret = clientSecret;
_baseUrl = baseUrl;
}
public async Task<string> GetAccessTokenAsync()
{
var policy = Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
.WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
var response = await policy.ExecuteAsync(async () =>
{
var content = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("grant_type", "client_credentials"),
new KeyValuePair<string, string>("client_id", _clientId),
new KeyValuePair<string, string>("client_secret", _clientSecret),
new KeyValuePair<string, string>("scope", "bot:read webhook:read webhook:write")
});
return await _httpClient.PostAsync($"{_baseUrl}/oauth/token", content);
});
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadFromJsonAsync<Dictionary<string, object>>();
if (json == null || !json.ContainsKey("access_token"))
throw new InvalidOperationException("OAuth response missing access_token field.");
return json["access_token"].ToString()!;
}
}
The request targets POST https://api.cognigy.com/oauth/token. A 401 Unauthorized response indicates invalid credentials. A 403 Forbidden response means the client lacks the required scopes. The Polly policy handles 429 Too Many Requests by implementing exponential backoff.
Implementation
Step 1: Mock Server Initialization & Payload Schema Validation
The mock server uses Microsoft.AspNetCore.Builder to host a local endpoint. Incoming requests must match the Cognigy webhook payload structure. You validate the JSON body against a predefined schema before processing.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Text.Json;
using System.Text.Json.Schema;
public static class MockWebhookServer
{
private static readonly Uri _listenUri = new("http://localhost:5000/webhooks/cognigy");
private static readonly JsonSchema _cognigySchema = JsonSchema.Parse(@"
{
""type"": ""object"",
""required"": [""sessionId"", ""userId"", ""input"", ""botId"", ""timestamp""],
""properties"": {
""sessionId"": {""type"": ""string""},
""userId"": {""type"": ""string""},
""input"": {""type"": ""object"", ""required"": [""text""], ""properties"": {""text"": {""type"": ""string""}}},
""botId"": {""type"": ""string""},
""timestamp"": {""type"": ""number""}
}
}");
public static async Task RunAsync(Func<HttpContext, Task> requestHandler)
{
var builder = WebApplication.CreateSlimBuilder();
builder.WebHost.ConfigureKestrel(options => options.ListenAnyIP(5000));
var app = builder.Build();
app.MapPost("/webhooks/cognigy", requestHandler);
app.MapGet("/health", () => Results.Ok(new { status = "active" }));
await app.RunAsync();
}
public static async Task ValidatePayloadAsync(HttpContext context)
{
using var reader = new StreamReader(context.Request.Body);
var jsonText = await reader.ReadToEndAsync();
var document = JsonDocument.Parse(jsonText);
var errors = _cognigySchema.Validate(document.RootElement);
if (errors.Any())
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new { error = "Schema validation failed", details = errors.Select(e => e.Path) });
return;
}
await context.Response.WriteAsJsonAsync(new { status = "validated" });
}
}
The schema enforces the presence of sessionId, userId, input.text, botId, and timestamp. A 400 Bad Request response returns the failed JSON paths. The server listens on http://localhost:5000/webhooks/cognigy.
Step 2: Response Matrix, Delay Simulation & Atomic Control
You define a response matrix that maps endpoint paths to HTTP status codes and delay durations. Atomic operations prevent concurrent test runners from corrupting mock configurations.
using System.Collections.Concurrent;
using System.Threading;
public class MockControlManager
{
private readonly ConcurrentDictionary<string, MockDirective> _matrix = new();
private readonly SemaphoreSlim _configLock = new(1, 1);
private readonly int _maxMockInstances;
public MockControlManager(int maxMockInstances = 50)
{
_maxMockInstances = maxMockInstances;
}
public async Task SetDirectiveAsync(string path, int statusCode, int delayMs)
{
await _configLock.WaitAsync();
try
{
if (_matrix.Count >= _maxMockInstances)
throw new InvalidOperationException($"Maximum mock instance limit of {_maxMockInstances} reached.");
_matrix[path] = new MockDirective { StatusCode = statusCode, DelayMs = delayMs };
}
finally
{
_configLock.Release();
}
}
public MockDirective? GetDirective(string path)
{
return _matrix.TryGetValue(path, out var directive) ? directive : null;
}
public async Task ExecuteDirectiveAsync(HttpContext context, string path)
{
var directive = GetDirective(path) ?? new MockDirective { StatusCode = 200, DelayMs = 0 };
if (directive.DelayMs > 0)
await Task.Delay(directive.DelayMs);
context.Response.StatusCode = directive.StatusCode;
await context.Response.WriteAsJsonAsync(new {
mock = true,
path = path,
simulatedStatus = directive.StatusCode,
delayApplied = directive.DelayMs
});
}
}
public record MockDirective(int StatusCode, int DelayMs);
The SemaphoreSlim ensures thread-safe configuration updates. Exceeding _maxMockInstances throws an exception to prevent memory exhaustion during parallel test execution. Delay simulation uses Task.Delay to mimic network latency without blocking threads.
Step 3: Request Capture, Header Parity & Callback Sync
External test runners require visibility into captured requests. You verify header parity against Cognigy standards and trigger callback handlers for synchronization.
using System.Text.Json;
public class RequestCapturePipeline
{
private readonly List<CapturedRequest> _capturedRequests = new();
private readonly object _captureLock = new();
public event Action<CapturedRequest>? OnRequestCaptured;
public async Task ProcessAsync(HttpContext context, string payloadJson)
{
var headers = context.Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()!);
var captured = new CapturedRequest
{
Timestamp = DateTimeOffset.UtcNow,
Path = context.Request.Path,
Method = context.Request.Method,
Headers = headers,
Payload = payloadJson,
LatencyMs = 0
};
bool parityValid = VerifyHeaderParity(headers);
if (!parityValid)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new { error = "Header parity verification failed", missing = GetMissingHeaders(headers) });
return;
}
lock (_captureLock)
{
_capturedRequests.Add(captured);
}
OnRequestCaptured?.Invoke(captured);
context.Items["CapturedRequest"] = captured;
}
private bool VerifyHeaderParity(Dictionary<string, string> headers)
{
return headers.ContainsKey("Content-Type") &&
headers["Content-Type"]?.Contains("application/json") == true &&
headers.ContainsKey("User-Agent") &&
headers["User-Agent"]?.Contains("Cognigy") == true;
}
private List<string> GetMissingHeaders(Dictionary<string, string> headers)
{
var missing = new List<string>();
if (!headers.ContainsKey("Content-Type") || headers["Content-Type"]?.Contains("application/json") != true)
missing.Add("Content-Type: application/json");
if (!headers.ContainsKey("User-Agent") || headers["User-Agent"]?.Contains("Cognigy") != true)
missing.Add("User-Agent: Cognigy");
return missing;
}
}
public record CapturedRequest(
DateTimeOffset Timestamp,
string Path,
string Method,
Dictionary<string, string> Headers,
string Payload,
int LatencyMs);
Cognigy sends Content-Type: application/json and a User-Agent containing Cognigy. Missing headers trigger a 400 response. The OnRequestCaptured event allows test runners to react synchronously.
Step 4: Latency Tracking, Audit Logging & Test Runner Integration
You track simulation performance and generate structured audit logs. The control API exposes endpoints for external automation tools.
using Serilog;
using System.Diagnostics;
public class MockAuditService
{
private readonly ILogger _logger;
private readonly ConcurrentDictionary<string, Stopwatch> _activeRequests = new();
private readonly ConcurrentQueue<AuditLogEntry> _auditLog = new();
public MockAuditService()
{
_logger = new LoggerConfiguration()
.WriteTo.File("mock_audit.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
}
public void StartTracking(string requestId)
{
var sw = new Stopwatch();
sw.Start();
_activeRequests[requestId] = sw;
}
public void CompleteTracking(string requestId, int statusCode, bool success)
{
if (_activeRequests.TryRemove(requestId, out var stopwatch))
{
stopwatch.Stop();
var entry = new AuditLogEntry
{
RequestId = requestId,
Timestamp = DateTimeOffset.UtcNow,
LatencyMs = stopwatch.ElapsedMilliseconds,
StatusCode = statusCode,
Success = success
};
_auditLog.Enqueue(entry);
_logger.Information("Mock completed | Request: {RequestId} | Latency: {Latency}ms | Status: {Status}", requestId, entry.LatencyMs, statusCode);
}
}
public IReadOnlyList<AuditLogEntry> GetAuditLog() => _auditLog.ToList();
}
public record AuditLogEntry(string RequestId, DateTimeOffset Timestamp, int LatencyMs, int StatusCode, bool Success);
The audit service uses Stopwatch for precise latency measurement. Logs write to mock_audit.log using Serilog. Test runners query the audit queue via the control API to verify simulation success rates and identify performance bottlenecks.
Complete Working Example
The following program integrates authentication, mock hosting, payload validation, directive execution, header parity, capture pipelines, and audit logging into a single executable console application.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using System.Text.Json;
public class CognigyMockHost
{
private readonly CognigyAuthClient _authClient;
private readonly MockControlManager _controlManager;
private readonly RequestCapturePipeline _capturePipeline;
private readonly MockAuditService _auditService;
public CognigyMockHost(string clientId, string clientSecret)
{
_authClient = new CognigyAuthClient(clientId, clientSecret);
_controlManager = new MockControlManager(maxMockInstances: 100);
_capturePipeline = new RequestCapturePipeline();
_auditService = new MockAuditService();
}
public async Task StartAsync()
{
Console.WriteLine("Initializing Cognigy mock server...");
var token = await _authClient.GetAccessTokenAsync();
Console.WriteLine($"OAuth token acquired. Scope: bot:read webhook:read webhook:write");
await _controlManager.SetDirectiveAsync("/webhooks/cognigy", 200, 150);
await _controlManager.SetDirectiveAsync("/webhooks/cognigy/error", 500, 0);
await MockWebhookServer.RunAsync(async context =>
{
var requestId = Guid.NewGuid().ToString();
_auditService.StartTracking(requestId);
try
{
using var reader = new StreamReader(context.Request.Body);
var payload = await reader.ReadToEndAsync();
await _capturePipeline.ProcessAsync(context, payload);
if (context.Response.HasStarted) return;
await MockWebhookServer.ValidatePayloadAsync(context);
if (context.Response.HasStarted) return;
await _controlManager.ExecuteDirectiveAsync(context, context.Request.Path);
_auditService.CompleteTracking(requestId, context.Response.StatusCode, context.Response.StatusCode >= 200 && context.Response.StatusCode < 300);
}
catch (Exception ex)
{
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new { error = "Internal mock failure", message = ex.Message });
_auditService.CompleteTracking(requestId, 500, false);
}
});
}
public static async Task Main(string[] args)
{
var clientId = Environment.GetEnvironmentVariable("COGNIGY_CLIENT_ID") ?? "YOUR_CLIENT_ID";
var clientSecret = Environment.GetEnvironmentVariable("COGNIGY_CLIENT_SECRET") ?? "YOUR_CLIENT_SECRET";
var host = new CognigyMockHost(clientId, clientSecret);
await host.StartAsync();
}
}
Run the application with dotnet run. The mock server starts on http://localhost:5000/webhooks/cognigy. Send a POST request with a valid Cognigy payload to trigger validation, header parity checks, directive execution, and audit logging. The control manager enforces a maximum of 100 concurrent mock instances.
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: Invalid
clientIdorclientSecret, or expired token. - Fix: Verify credentials in the Cognigy developer console. Ensure the client is authorized for the target tenant.
- Code Fix: The
CognigyAuthClientalready retries on429. For401, check environment variables and regenerate secrets if compromised.
Error: 400 Bad Request (Schema Validation Failed)
- Cause: Incoming JSON lacks required fields (
sessionId,userId,input.text,botId,timestamp). - Fix: Align the test payload with Cognigy webhook specifications. Use the
JsonSchemavalidation output to identify missing paths. - Code Fix: Update test data generators to include all required fields. The mock server returns the exact failing paths in the response body.
Error: 429 Too Many Requests
- Cause: Cognigy API rate limiting during token acquisition or webhook registration.
- Fix: The
Pollypolicy implements exponential backoff. Increase retry attempts or implement request throttling in the test runner. - Code Fix: Adjust
WaitAndRetryAsyncparameters if the test suite executes rapid OAuth calls.
Error: Maximum Mock Instance Limit Reached
- Cause: Concurrent test runners exceeded
_maxMockInstances. - Fix: Increase the limit in
MockControlManageror implement request queuing. - Code Fix: Monitor
_auditService.GetAuditLog()to identify peak concurrency. AdjustSemaphoreSlimcapacity accordingly.