diff --git a/AccessGridTest/ConsoleServiceTests.cs b/AccessGridTest/ConsoleServiceTests.cs index cd792e8..b729f2e 100644 --- a/AccessGridTest/ConsoleServiceTests.cs +++ b/AccessGridTest/ConsoleServiceTests.cs @@ -865,6 +865,21 @@ public async Task WebhooksDeleteAsync_ShouldDeleteWebhook() )), Times.Once); } + [Test] + public async Task WebhooksDeleteAsync_IncludesSigPayloadQueryParam() + { + // DELETE requests have no body, so the server looks for `sig_payload` in + // the query string and verifies the signature against that. + StubHttpResponse("{}"); + + await _client.Console.Webhooks.DeleteAsync("wh_123"); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Delete && + req.RequestUri!.ToString().Contains("sig_payload=") + )), Times.Once); + } + #endregion #region HIDOrgsService diff --git a/src/AccessGrid/AccessGridClient.cs b/src/AccessGrid/AccessGridClient.cs index 8c4923e..af34b10 100644 --- a/src/AccessGrid/AccessGridClient.cs +++ b/src/AccessGrid/AccessGridClient.cs @@ -163,7 +163,7 @@ private async Task MakeRequestAsync(HttpMethod method, string endpoint, ob { // Extract resource ID from the endpoint if needed for signature string resourceId = null; - if (method == HttpMethod.Get || (method == HttpMethod.Post && data == null)) + if (method == HttpMethod.Get || method == HttpMethod.Delete || (method == HttpMethod.Post && data == null)) { // Extract the ID from the endpoint - patterns like /resource/{id} or /resource/{id}/action var parts = endpoint.Trim('/').Split('/'); @@ -186,8 +186,9 @@ private async Task MakeRequestAsync(HttpMethod method, string endpoint, ob // Special handling for requests with no payload: // 1. POST requests with empty body (like unlink/suspend/resume) // 2. GET requests + // 3. DELETE requests (always have no body) string payload; - if ((method == HttpMethod.Post && data == null) || method == HttpMethod.Get) + if ((method == HttpMethod.Post && data == null) || method == HttpMethod.Get || method == HttpMethod.Delete) { // For listing cards endpoint (like what the Python list.py script does) if (method == HttpMethod.Get && endpoint == "/v1/key-cards") @@ -228,7 +229,7 @@ private async Task MakeRequestAsync(HttpMethod method, string endpoint, ob // For GET requests or POST requests with empty bodies that need the sig_payload parameter // Note: We've already added sig_payload for /v1/key-cards endpoint above - if ((method == HttpMethod.Get || (method == HttpMethod.Post && data == null)) && !finalQueryParams.ContainsKey("sig_payload")) + if ((method == HttpMethod.Get || method == HttpMethod.Delete || (method == HttpMethod.Post && data == null)) && !finalQueryParams.ContainsKey("sig_payload")) { if (!string.IsNullOrEmpty(resourceId) && resourceId != "key-cards" && !resourceId.Contains("templates")) { @@ -298,6 +299,12 @@ private async Task MakeRequestAsync(HttpMethod method, string endpoint, ob return (T)(object)responseContent; } + // Empty response bodies (e.g. DELETE 204) have nothing to deserialize. + if (string.IsNullOrWhiteSpace(responseContent)) + { + return default; + } + // Deserialize the response try {