From c6ad5c276c91cfd0194ca85cb71e5598d827c76d Mon Sep 17 00:00:00 2001 From: Auston Bunsen Date: Wed, 15 Apr 2026 14:13:47 -0400 Subject: [PATCH 1/2] feat: Rename pass-template-pairs to card-template-pairs, add create endpoint - Rename URL from /v1/console/pass-template-pairs to /v1/console/card-template-pairs to match Rails API rename in PR #1396 (AGENG-776). - Rename types: PassTemplatePair{,Info,sResponse} to CardTemplatePair{,Info,sResponse}. - Rename method ListPassTemplatePairsAsync to ListCardTemplatePairsAsync. - Add ex_id fields to pair + nested template response models. - Add CreateCardTemplatePairAsync + CreateCardTemplatePairRequest for POST /v1/console/card-template-pairs. - Update README example + feature matrix. - Add tests for new create endpoint; update existing list tests for renamed response key (card_template_pairs) and ex_id. - Bump package version to 1.5.0. - dotnet format pass picked up some pre-existing whitespace drift. --- AccessGridTest/ConsoleServiceTests.cs | 80 ++++++++++++++++++++------- README.md | 41 +++++++++++--- example/Example.cs | 2 +- src/AccessGrid.csproj | 2 +- src/AccessGrid/AccessCardsService.cs | 4 +- src/AccessGrid/AccessGridClient.cs | 16 +++--- src/AccessGrid/ConsoleService.cs | 30 +++++++--- src/AccessGrid/Models.cs | 49 +++++++++++----- 8 files changed, 162 insertions(+), 62 deletions(-) diff --git a/AccessGridTest/ConsoleServiceTests.cs b/AccessGridTest/ConsoleServiceTests.cs index 8f56aa2..451e697 100644 --- a/AccessGridTest/ConsoleServiceTests.cs +++ b/AccessGridTest/ConsoleServiceTests.cs @@ -36,24 +36,26 @@ private void StubHttpResponse(string json, HttpStatusCode status = HttpStatusCod } [Test] - public async Task ListPassTemplatePairsAsync_ReturnsPassTemplatePairs() + public async Task ListCardTemplatePairsAsync_ReturnsCardTemplatePairs() { - // Same fixture as Ruby console_spec.rb #list_pass_template_pairs + // Same fixture as Ruby console_spec.rb #list_card_template_pairs var json = """ { - "pass_template_pairs": [ + "card_template_pairs": [ { "id": "pair_1", + "ex_id": "pair_1", "name": "Employee Badge Pair", "created_at": "2025-01-01T00:00:00Z", - "ios_template": { "id": "tmpl_ios_1", "name": "iOS Badge", "platform": "apple" }, - "android_template": { "id": "tmpl_android_1", "name": "Android Badge", "platform": "android" } + "ios_template": { "id": "tmpl_ios_1", "ex_id": "tmpl_ios_1", "name": "iOS Badge", "platform": "apple" }, + "android_template": { "id": "tmpl_android_1", "ex_id": "tmpl_android_1", "name": "Android Badge", "platform": "android" } }, { "id": "pair_2", + "ex_id": "pair_2", "name": "Contractor Badge Pair", "created_at": "2025-01-02T00:00:00Z", - "ios_template": { "id": "tmpl_ios_2", "name": "iOS Contractor", "platform": "apple" }, + "ios_template": { "id": "tmpl_ios_2", "ex_id": "tmpl_ios_2", "name": "iOS Contractor", "platform": "apple" }, "android_template": null } ], @@ -67,20 +69,22 @@ public async Task ListPassTemplatePairsAsync_ReturnsPassTemplatePairs() """; StubHttpResponse(json); - var result = await _client.Console.ListPassTemplatePairsAsync(); + var result = await _client.Console.ListCardTemplatePairsAsync(); - Assert.That(result.PassTemplatePairs, Has.Count.EqualTo(2)); + Assert.That(result.CardTemplatePairs, Has.Count.EqualTo(2)); - var first = result.PassTemplatePairs[0]; + var first = result.CardTemplatePairs[0]; Assert.That(first.Id, Is.EqualTo("pair_1")); + Assert.That(first.ExId, Is.EqualTo("pair_1")); Assert.That(first.Name, Is.EqualTo("Employee Badge Pair")); Assert.That(first.CreatedAt, Is.EqualTo(new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc))); Assert.That(first.IosTemplate.Id, Is.EqualTo("tmpl_ios_1")); + Assert.That(first.IosTemplate.ExId, Is.EqualTo("tmpl_ios_1")); Assert.That(first.IosTemplate.Platform, Is.EqualTo("apple")); Assert.That(first.AndroidTemplate.Id, Is.EqualTo("tmpl_android_1")); Assert.That(first.AndroidTemplate.Platform, Is.EqualTo("android")); - var second = result.PassTemplatePairs[1]; + var second = result.CardTemplatePairs[1]; Assert.That(second.Id, Is.EqualTo("pair_2")); Assert.That(second.AndroidTemplate, Is.Null); Assert.That(second.IosTemplate, Is.Not.Null); @@ -92,20 +96,21 @@ public async Task ListPassTemplatePairsAsync_ReturnsPassTemplatePairs() } [Test] - public async Task ListPassTemplatePairsAsync_PassesPaginationParams() + public async Task ListCardTemplatePairsAsync_PassesPaginationParams() { var json = """ { - "pass_template_pairs": [], + "card_template_pairs": [], "pagination": { "current_page": 2, "per_page": 10, "total_pages": 3, "total_count": 25 } } """; StubHttpResponse(json); - var result = await _client.Console.ListPassTemplatePairsAsync(page: 2, perPage: 10); + var result = await _client.Console.ListCardTemplatePairsAsync(page: 2, perPage: 10); _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => req.Method == HttpMethod.Get && + req.RequestUri!.ToString().Contains("/v1/console/card-template-pairs") && req.RequestUri!.ToString().Contains("page=2") && req.RequestUri!.ToString().Contains("per_page=10") )), Times.Once); @@ -115,34 +120,34 @@ public async Task ListPassTemplatePairsAsync_PassesPaginationParams() } [Test] - public async Task ListPassTemplatePairsAsync_HandlesEmptyResponse() + public async Task ListCardTemplatePairsAsync_HandlesEmptyResponse() { var json = """ { - "pass_template_pairs": [], + "card_template_pairs": [], "pagination": { "current_page": 1, "per_page": 50, "total_pages": 0, "total_count": 0 } } """; StubHttpResponse(json); - var result = await _client.Console.ListPassTemplatePairsAsync(); + var result = await _client.Console.ListCardTemplatePairsAsync(); - Assert.That(result.PassTemplatePairs, Is.Empty); + Assert.That(result.CardTemplatePairs, Is.Empty); Assert.That(result.Pagination.TotalCount, Is.EqualTo(0)); } [Test] - public async Task ListPassTemplatePairsAsync_SetsAuthHeaders() + public async Task ListCardTemplatePairsAsync_SetsAuthHeaders() { var json = """ { - "pass_template_pairs": [], + "card_template_pairs": [], "pagination": { "current_page": 1, "per_page": 50, "total_pages": 0, "total_count": 0 } } """; StubHttpResponse(json); - await _client.Console.ListPassTemplatePairsAsync(); + await _client.Console.ListCardTemplatePairsAsync(); _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => req.Headers.Contains("X-ACCT-ID") && @@ -151,6 +156,41 @@ public async Task ListPassTemplatePairsAsync_SetsAuthHeaders() )), Times.Once); } + [Test] + public async Task CreateCardTemplatePairAsync_PostsAndReturnsPair() + { + var json = """ + { + "id": "pair_new", + "ex_id": "pair_new", + "name": "New Badge Pair", + "created_at": "2025-04-15T12:00:00Z", + "ios_template": { "id": "tmpl_ios", "ex_id": "tmpl_ios", "name": "iOS Badge", "platform": "apple" }, + "android_template": { "id": "tmpl_android", "ex_id": "tmpl_android", "name": "Android Badge", "platform": "android" } + } + """; + StubHttpResponse(json, HttpStatusCode.Created); + + var result = await _client.Console.CreateCardTemplatePairAsync(new CreateCardTemplatePairRequest + { + Name = "New Badge Pair", + AppleCardTemplateId = "tmpl_ios", + GoogleCardTemplateId = "tmpl_android" + }); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Id, Is.EqualTo("pair_new")); + Assert.That(result.ExId, Is.EqualTo("pair_new")); + Assert.That(result.Name, Is.EqualTo("New Badge Pair")); + Assert.That(result.IosTemplate.Platform, Is.EqualTo("apple")); + Assert.That(result.AndroidTemplate.Platform, Is.EqualTo("android")); + + _mockHttpClient.Verify(x => x.SendAsync(It.Is(req => + req.Method == HttpMethod.Post && + req.RequestUri!.ToString().Contains("/v1/console/card-template-pairs") + )), Times.Once); + } + #region CreateTemplateAsync [Test] diff --git a/README.md b/README.md index 4188eac..37585e2 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Official C# SDK for interacting with the AccessGrid API. ## Installation ``` -Install-Package accessgrid -Version 1.4.0 +Install-Package accessgrid -Version 1.5.0 ``` ## Authentication @@ -350,14 +350,14 @@ public async Task GetEventLogAsync() } ``` -### Listing Pass Template Pairs +### Listing Card Template Pairs ```csharp using AccessGrid; using System; using System.Threading.Tasks; -public async Task ListPassTemplatePairsAsync() +public async Task ListCardTemplatePairsAsync() { var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); @@ -365,9 +365,9 @@ public async Task ListPassTemplatePairsAsync() using var client = new AccessGridClient(accountId, secretKey); // List first page with default page size (50) - var result = await client.Console.ListPassTemplatePairsAsync(); + var result = await client.Console.ListCardTemplatePairsAsync(); - foreach (var pair in result.PassTemplatePairs) + foreach (var pair in result.CardTemplatePairs) { Console.WriteLine($"Pair: {pair.Name} ({pair.Id})"); Console.WriteLine($" iOS: {pair.IosTemplate?.Name ?? "none"}"); @@ -377,7 +377,33 @@ public async Task ListPassTemplatePairsAsync() Console.WriteLine($"Page {result.Pagination.CurrentPage} of {result.Pagination.TotalPages}"); // Or with pagination - var page2 = await client.Console.ListPassTemplatePairsAsync(page: 2, perPage: 10); + var page2 = await client.Console.ListCardTemplatePairsAsync(page: 2, perPage: 10); +} +``` + +### Creating a Card Template Pair + +```csharp +using AccessGrid; +using System; +using System.Threading.Tasks; + +public async Task CreateCardTemplatePairAsync() +{ + var accountId = Environment.GetEnvironmentVariable("ACCOUNT_ID"); + var secretKey = Environment.GetEnvironmentVariable("SECRET_KEY"); + + using var client = new AccessGridClient(accountId, secretKey); + + // Both templates must be published (status: ready) and use the same protocol. + var pair = await client.Console.CreateCardTemplatePairAsync(new CreateCardTemplatePairRequest + { + Name = "Employee Badge Pair", + AppleCardTemplateId = "0xapplet3mp14t3", + GoogleCardTemplateId = "0xgoogl3t3mp14t3" + }); + + Console.WriteLine($"Created pair: {pair.Name} ({pair.Id})"); } ``` @@ -1043,7 +1069,8 @@ public class AccessCardsApiTests | PUT /v1/console/card-templates/{id} | `Console.UpdateTemplateAsync()` | Y | | GET /v1/console/card-templates/{id} | `Console.ReadTemplateAsync()` | Y | | GET /v1/console/card-templates/{id}/logs | `Console.EventLogAsync()` | Y | -| GET /v1/console/pass-template-pairs | `Console.ListPassTemplatePairsAsync()` | Y | +| GET /v1/console/card-template-pairs | `Console.ListCardTemplatePairsAsync()` | Y | +| POST /v1/console/card-template-pairs | `Console.CreateCardTemplatePairAsync()` | Y | | POST /v1/console/card-templates/{id}/ios_preflight | `Console.IosPreflightAsync()` | Y | | GET /v1/console/ledger-items | `Console.GetLedgerItemsAsync()` | Y | | GET /v1/console/landing-pages | `Console.ListLandingPagesAsync()` | Y | diff --git a/example/Example.cs b/example/Example.cs index c0eeee2..ce19772 100644 --- a/example/Example.cs +++ b/example/Example.cs @@ -28,7 +28,7 @@ static async Task Main() Console.WriteLine("API Credentials:"); Console.WriteLine($" Account ID: {accountId}"); Console.WriteLine($" Secret Key: {secretKey.Substring(0, 3)}...{secretKey.Substring(secretKey.Length - 3)}"); - + // Example 1: List cards Console.WriteLine("\nListing access cards..."); var cards = await client.AccessCards.ListAsync(new ListKeysRequest diff --git a/src/AccessGrid.csproj b/src/AccessGrid.csproj index 361cac5..dcb0afe 100644 --- a/src/AccessGrid.csproj +++ b/src/AccessGrid.csproj @@ -4,7 +4,7 @@ netstandard2.0 8.0 accessgrid - 1.4.0 + 1.5.0 AccessGrid AccessGrid Official C# SDK for the AccessGrid API diff --git a/src/AccessGrid/AccessCardsService.cs b/src/AccessGrid/AccessCardsService.cs index f5221fa..32bd1ba 100644 --- a/src/AccessGrid/AccessCardsService.cs +++ b/src/AccessGrid/AccessCardsService.cs @@ -67,10 +67,10 @@ public async Task UpdateAsync(string cardId, UpdateCardRequest reque public async Task> ListAsync(ListKeysRequest request) { var queryParams = new Dictionary(); - + if (!string.IsNullOrEmpty(request.TemplateId)) queryParams.Add("template_id", request.TemplateId); - + if (!string.IsNullOrEmpty(request.State)) queryParams.Add("state", request.State); diff --git a/src/AccessGrid/AccessGridClient.cs b/src/AccessGrid/AccessGridClient.cs index 1afe9b8..8c4923e 100644 --- a/src/AccessGrid/AccessGridClient.cs +++ b/src/AccessGrid/AccessGridClient.cs @@ -43,7 +43,7 @@ public AccessGridClient(string accountId, string secretKey, IHttpClientWrapper h { if (string.IsNullOrEmpty(accountId)) throw new ArgumentException("Account ID is required", nameof(accountId)); - + if (string.IsNullOrEmpty(secretKey)) throw new ArgumentException("Secret Key is required", nameof(secretKey)); @@ -222,10 +222,10 @@ private async Task MakeRequestAsync(HttpMethod method, string endpoint, ob { finalQueryParams["sig_payload"] = payload; } - + // Generate signature string signature = GenerateSignature(payload); - + // 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")) @@ -279,12 +279,12 @@ private async Task MakeRequestAsync(HttpMethod method, string endpoint, ob { try { - var errorData = !string.IsNullOrEmpty(responseContent) - ? JsonSerializer.Deserialize>(responseContent, _jsonOptions) + var errorData = !string.IsNullOrEmpty(responseContent) + ? JsonSerializer.Deserialize>(responseContent, _jsonOptions) : null; - - var errorMessage = responseContent; - + + var errorMessage = responseContent; + throw new AccessGridException($"API request failed: {errorMessage}"); } catch (JsonException) diff --git a/src/AccessGrid/ConsoleService.cs b/src/AccessGrid/ConsoleService.cs index 36643f9..bacbb7c 100644 --- a/src/AccessGrid/ConsoleService.cs +++ b/src/AccessGrid/ConsoleService.cs @@ -75,18 +75,18 @@ public async Task