Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 47 additions & 7 deletions AccessGridTest/ConsoleServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,21 @@
// Same fixture as Ruby console_spec.rb #list_pass_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
}
],
Expand All @@ -73,9 +75,11 @@

var first = result.PassTemplatePairs[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"));
Expand All @@ -96,7 +100,7 @@
{
var json = """
{
"pass_template_pairs": [],
"card_template_pairs": [],
"pagination": { "current_page": 2, "per_page": 10, "total_pages": 3, "total_count": 25 }
}
""";
Expand All @@ -106,6 +110,7 @@

_mockHttpClient.Verify(x => x.SendAsync(It.Is<HttpRequestMessage>(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);
Expand All @@ -119,7 +124,7 @@
{
var json = """
{
"pass_template_pairs": [],
"card_template_pairs": [],
"pagination": { "current_page": 1, "per_page": 50, "total_pages": 0, "total_count": 0 }
}
""";
Expand All @@ -136,7 +141,7 @@
{
var json = """
{
"pass_template_pairs": [],
"card_template_pairs": [],
"pagination": { "current_page": 1, "per_page": 50, "total_pages": 0, "total_count": 0 }
}
""";
Expand All @@ -151,6 +156,41 @@
)), Times.Once);
}

[Test]
public async Task CreatePassTemplatePairAsync_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.CreatePassTemplatePairAsync(new CreatePassTemplatePairRequest
{
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<HttpRequestMessage>(req =>
req.Method == HttpMethod.Post &&
req.RequestUri!.ToString().Contains("/v1/console/card-template-pairs")
)), Times.Once);
}

#region CreateTemplateAsync

[Test]
Expand Down Expand Up @@ -224,7 +264,7 @@
}
""";

string capturedBody = null;

Check warning on line 267 in AccessGridTest/ConsoleServiceTests.cs

View workflow job for this annotation

GitHub Actions / test (8.0.x)

Converting null literal or possible null value to non-nullable type.
_mockHttpClient
.Setup(x => x.SendAsync(It.IsAny<HttpRequestMessage>()))
.Returns<HttpRequestMessage>(async req =>
Expand Down Expand Up @@ -309,7 +349,7 @@
{
var json = """{ "id": "tmpl-123", "name": "Test" }""";

string capturedBody = null;

Check warning on line 352 in AccessGridTest/ConsoleServiceTests.cs

View workflow job for this annotation

GitHub Actions / test (8.0.x)

Converting null literal or possible null value to non-nullable type.
_mockHttpClient
.Setup(x => x.SendAsync(It.IsAny<HttpRequestMessage>()))
.Returns<HttpRequestMessage>(async req =>
Expand Down
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -381,6 +381,32 @@ public async Task ListPassTemplatePairsAsync()
}
```

### Creating a Pass Template Pair

```csharp
using AccessGrid;
using System;
using System.Threading.Tasks;

public async Task CreatePassTemplatePairAsync()
{
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.CreatePassTemplatePairAsync(new CreatePassTemplatePairRequest
{
Name = "Employee Badge Pair",
AppleCardTemplateId = "0xapplet3mp14t3",
GoogleCardTemplateId = "0xgoogl3t3mp14t3"
});

Console.WriteLine($"Created pair: {pair.Name} ({pair.Id})");
}
```

### Getting Ledger Items

```csharp
Expand Down Expand Up @@ -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.ListPassTemplatePairsAsync()` | Y |
| POST /v1/console/card-template-pairs | `Console.CreatePassTemplatePairAsync()` | 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 |
Expand Down
2 changes: 1 addition & 1 deletion example/Example.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/AccessGrid.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
<PackageId>accessgrid</PackageId>
<Version>1.4.0</Version>
<Version>1.5.0</Version>
<Authors>AccessGrid</Authors>
<Company>AccessGrid</Company>
<Description>Official C# SDK for the AccessGrid API</Description>
Expand Down
4 changes: 2 additions & 2 deletions src/AccessGrid/AccessCardsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ public async Task<AccessCard> UpdateAsync(string cardId, UpdateCardRequest reque
public async Task<List<AccessCard>> ListAsync(ListKeysRequest request)
{
var queryParams = new Dictionary<string, string>();

if (!string.IsNullOrEmpty(request.TemplateId))
queryParams.Add("template_id", request.TemplateId);

if (!string.IsNullOrEmpty(request.State))
queryParams.Add("state", request.State);

Expand Down
16 changes: 8 additions & 8 deletions src/AccessGrid/AccessGridClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -222,10 +222,10 @@ private async Task<T> MakeRequestAsync<T>(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"))
Expand Down Expand Up @@ -279,12 +279,12 @@ private async Task<T> MakeRequestAsync<T>(HttpMethod method, string endpoint, ob
{
try
{
var errorData = !string.IsNullOrEmpty(responseContent)
? JsonSerializer.Deserialize<Dictionary<string, object>>(responseContent, _jsonOptions)
var errorData = !string.IsNullOrEmpty(responseContent)
? JsonSerializer.Deserialize<Dictionary<string, object>>(responseContent, _jsonOptions)
: null;
var errorMessage = responseContent;

var errorMessage = responseContent;

throw new AccessGridException($"API request failed: {errorMessage}");
}
catch (JsonException)
Expand Down
22 changes: 17 additions & 5 deletions src/AccessGrid/ConsoleService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,18 @@ public async Task<Template> ReadTemplateAsync(string templateId)
public async Task<List<EventLogEntry>> EventLogAsync(string templateId, EventLogFilters filters = null)
{
var queryParams = new Dictionary<string, string>();

if (filters != null)
{
if (!string.IsNullOrEmpty(filters.Device))
queryParams.Add("device", filters.Device);

if (filters.StartDate.HasValue)
queryParams.Add("start_date", filters.StartDate.Value.ToString("o"));

if (filters.EndDate.HasValue)
queryParams.Add("end_date", filters.EndDate.Value.ToString("o"));

if (!string.IsNullOrEmpty(filters.EventType))
queryParams.Add("event_type", filters.EventType);
}
Expand All @@ -111,10 +111,22 @@ public async Task<PassTemplatePairsResponse> ListPassTemplatePairsAsync(int? pag
if (perPage.HasValue)
queryParams.Add("per_page", perPage.Value.ToString());

var response = await _apiService.GetAsync<PassTemplatePairsResponse>("/v1/console/pass-template-pairs", queryParams);
var response = await _apiService.GetAsync<PassTemplatePairsResponse>("/v1/console/card-template-pairs", queryParams);
return response ?? new PassTemplatePairsResponse();
}

/// <summary>
/// Creates a pass template pair linking an Apple (iOS) and Google (Android) card template.
/// Both templates must be published (status: ready) and use the same protocol.
/// </summary>
/// <param name="request">Pair creation parameters</param>
/// <returns>The created pass template pair</returns>
public async Task<PassTemplatePair> CreatePassTemplatePairAsync(CreatePassTemplatePairRequest request)
{
var response = await _apiService.PostAsync<PassTemplatePair>("/v1/console/card-template-pairs", request);
return response;
}

/// <summary>
/// Gets ledger/billing items (enterprise only)
/// </summary>
Expand Down
35 changes: 29 additions & 6 deletions src/AccessGrid/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ public AccessCard()

[JsonPropertyName("id")]
public string Id { get; private set; }

[JsonPropertyName("install_url")]
public string Url { get; private set; }

[JsonPropertyName("state")]
public AccessPassState? State { get; private set; }
public AccessPassState? State { get; private set; }

/// <summary>
/// Unique identifier for the card template to use
Expand Down Expand Up @@ -220,7 +220,7 @@ public AccessCard()
/// False by default. Set to true if you'd like to enable the NFC keys issued using this template to exist on multiple devices
/// </summary>
[JsonPropertyName("allow_on_multiple_devices")]
public bool? AllowOnMultipleDevices { get; private set;}
public bool? AllowOnMultipleDevices { get; private set; }

[JsonPropertyName("details")]
public IReadOnlyList<AccessCard> Details { get; set; }
Expand Down Expand Up @@ -658,6 +658,9 @@ public class PassTemplatePairInfo
[JsonPropertyName("id")]
public string Id { get; set; }

[JsonPropertyName("ex_id")]
public string ExId { get; set; }

[JsonPropertyName("name")]
public string Name { get; set; }

Expand All @@ -673,6 +676,9 @@ public class PassTemplatePair
[JsonPropertyName("id")]
public string Id { get; set; }

[JsonPropertyName("ex_id")]
public string ExId { get; set; }

[JsonPropertyName("name")]
public string Name { get; set; }

Expand All @@ -687,17 +693,34 @@ public class PassTemplatePair
}

/// <summary>
/// Response wrapper for listing pass template pairs
/// Response wrapper for listing pass template pairs.
/// Maps the upstream "card_template_pairs" JSON key; the C# type/property
/// names are preserved for backward compatibility.
/// </summary>
public class PassTemplatePairsResponse
{
[JsonPropertyName("pass_template_pairs")]
[JsonPropertyName("card_template_pairs")]
public List<PassTemplatePair> PassTemplatePairs { get; set; } = new List<PassTemplatePair>();

[JsonPropertyName("pagination")]
public PaginationInfo Pagination { get; set; }
}

/// <summary>
/// Request to create a pass template pair
/// </summary>
public class CreatePassTemplatePairRequest
{
[JsonPropertyName("name")]
public string Name { get; set; }

[JsonPropertyName("apple_card_template_id")]
public string AppleCardTemplateId { get; set; }

[JsonPropertyName("google_card_template_id")]
public string GoogleCardTemplateId { get; set; }
}

/// <summary>
/// A pass template reference within a ledger item's access pass
/// </summary>
Expand Down Expand Up @@ -1058,7 +1081,7 @@ public class CredentialProfileEvent
/// <summary>
/// The CloudEvents data of a card template webhook event
/// </summary>
public class CardTemplateEvent
public class CardTemplateEvent
{
[JsonPropertyName("card_template_id")]
public string Id { get; set; }
Expand Down
Loading