From be2bc81054bf01014ccf3b65e2132a88ac71976d Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Mon, 2 Feb 2026 15:28:22 -0700 Subject: [PATCH 1/3] feat: create/delete/enable/disable API keys --- CHANGELOG.md | 8 + .../ServicesTests/ApiKeyServiceTest.cs | 65 +++++-- .../net/api_key_service/lifecycle.json | 184 ++++++++++++++++++ EasyPost.nuspec | 2 +- EasyPost/Models/API/ApiKey.cs | 12 ++ EasyPost/Properties/VersionInfo.cs | 6 +- EasyPost/Services/ApiKeyService.cs | 60 +++++- 7 files changed, 305 insertions(+), 32 deletions(-) create mode 100644 EasyPost.Tests/cassettes/net/api_key_service/lifecycle.json diff --git a/CHANGELOG.md b/CHANGELOG.md index bbc1982f0..0852055fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## v7.5.0 (2026-02-03) + +- Adds the following functions usable by child and referral customer users: + - `ApiKey.Create` + - `ApiKey.Delete` + - `ApiKey.Enable` + - `ApiKey.Disable` + ## v7.4.0 (2025-11-24) - Adds .NET 10 support diff --git a/EasyPost.Tests/ServicesTests/ApiKeyServiceTest.cs b/EasyPost.Tests/ServicesTests/ApiKeyServiceTest.cs index 57e77248e..d9b1d1fe0 100644 --- a/EasyPost.Tests/ServicesTests/ApiKeyServiceTest.cs +++ b/EasyPost.Tests/ServicesTests/ApiKeyServiceTest.cs @@ -20,6 +20,33 @@ public ApiKeyServiceTests() : base("api_key_service", TestUtils.ApiKey.Productio #region Test CRUD Operations + [Fact] + [CrudOperations.Read] + public async Task TestRetrieveApiKeys() + { + UseVCR("retrieve_api_keys"); + + User user = await Client.User.RetrieveMe(); + + List apiKeys = await Client.ApiKey.RetrieveApiKeysForUser(user.Id); + + Assert.IsType>(apiKeys); + } + + [Fact] + [CrudOperations.Read] + public async Task TestRetrieveApiKeysChild() + { + UseVCR("retrieve_api_keys_child"); + + const string fakeChildId = "user_123456789"; + + // Test suite user has no child users, so this should throw a FilteringError + Exception? possibleException = await Record.ExceptionAsync(async () => await Client.ApiKey.RetrieveApiKeysForUser(fakeChildId)); + + Assert.IsType(possibleException); + } + [Fact] [CrudOperations.Read] [Testing.Function] @@ -43,30 +70,30 @@ public async Task TestAll() } [Fact] - [CrudOperations.Read] - public async Task TestRetrieveApiKeys() + [CrudOperations.Create] + [CrudOperations.Update] + [CrudOperations.Delete] + public async Task TestApiKeyLifecycle() { - UseVCR("retrieve_api_keys"); + string? referralApiKey = Environment.GetEnvironmentVariable("REFERRAL_CUSTOMER_PROD_API_KEY"); + UseVCR("lifecycle", referralApiKey); - User user = await Client.User.RetrieveMe(); + // Create an API key + ApiKey apiKey = await Client.ApiKey.Create("production"); + Assert.NotNull(apiKey); + Assert.StartsWith("ak_", apiKey.Id); + Assert.Equal("production", apiKey.Mode); - List apiKeys = await Client.ApiKey.RetrieveApiKeysForUser(user.Id); + // Disable the API key + apiKey = await Client.ApiKey.Disable(apiKey.Id); + Assert.False(apiKey.Active); - Assert.IsType>(apiKeys); - } + // Enable the API key + apiKey = await Client.ApiKey.Enable(apiKey.Id); + Assert.True(apiKey.Active); - [Fact] - [CrudOperations.Read] - public async Task TestRetrieveApiKeysChild() - { - UseVCR("retrieve_api_keys_child"); - - const string fakeChildId = "user_123456789"; - - // Test suite user has no child users, so this should throw a FilteringError - Exception? possibleException = await Record.ExceptionAsync(async () => await Client.ApiKey.RetrieveApiKeysForUser(fakeChildId)); - - Assert.IsType(possibleException); + // Delete the API key + await Client.ApiKey.Delete(apiKey.Id); } #endregion diff --git a/EasyPost.Tests/cassettes/net/api_key_service/lifecycle.json b/EasyPost.Tests/cassettes/net/api_key_service/lifecycle.json new file mode 100644 index 000000000..1dedb44ae --- /dev/null +++ b/EasyPost.Tests/cassettes/net/api_key_service/lifecycle.json @@ -0,0 +1,184 @@ +[ + { + "Duration": 295, + "RecordedAt": "2026-02-02T20:32:38.000000Z", + "Request": { + "Body": "{\"mode\":\"production\"}", + "BodyContentType": "Json", + "ContentHeaders": { + "Content-Type": "application/json" + }, + "Method": "POST", + "RequestHeaders": { + "Authorization": "", + "User-Agent": "" + }, + "Uri": "https://api.easypost.com/v2/api_keys" + }, + "Response": { + "Body": "{\"object\":\"ApiKey\",\"key\":\"\",\"mode\":\"production\",\"created_at\":\"2026-02-02T20:32:38Z\",\"active\":true,\"id\":\"ak_2df46e55f8434c3882b40f7c99c8b64f\"}", + "BodyContentType": "Json", + "ContentHeaders": { + "Expires": "0", + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "199" + }, + "HttpVersion": "1.1", + "ResponseHeaders": { + "x-frame-options": "SAMEORIGIN", + "x-xss-protection": "1; mode=block", + "x-content-type-options": "nosniff", + "x-download-options": "noopen", + "x-permitted-cross-domain-policies": "none", + "referrer-policy": "strict-origin-when-cross-origin", + "x-ep-request-uuid": "cfd5bd42698109e6e2b988370270e6e6", + "cache-control": "private, no-cache, no-store", + "pragma": "no-cache", + "x-runtime": "0.119875", + "x-node": "bigweb66nuq", + "x-version-label": "easypost-202602021944-8c5966c6f9-master", + "x-backend": "easypost", + "x-proxied": "intlb5nuq d9379ca146,extlb1nuq cbbd141214", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload" + }, + "Status": { + "Code": 201, + "Message": "Created" + } + } + }, + { + "Duration": 258, + "RecordedAt": "2026-02-02T20:32:38.000000Z", + "Request": { + "Body": "", + "BodyContentType": "Text", + "ContentHeaders": {}, + "Method": "POST", + "RequestHeaders": { + "Authorization": "", + "User-Agent": "" + }, + "Uri": "https://api.easypost.com/v2/api_keys/ak_2df46e55f8434c3882b40f7c99c8b64f/disable" + }, + "Response": { + "Body": "{\"object\":\"ApiKey\",\"key\":\"\",\"mode\":\"production\",\"created_at\":\"2026-02-02T20:32:38Z\",\"active\":false,\"id\":\"ak_2df46e55f8434c3882b40f7c99c8b64f\"}", + "BodyContentType": "Json", + "ContentHeaders": { + "Expires": "0", + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "200" + }, + "HttpVersion": "1.1", + "ResponseHeaders": { + "x-frame-options": "SAMEORIGIN", + "x-xss-protection": "1; mode=block", + "x-content-type-options": "nosniff", + "x-download-options": "noopen", + "x-permitted-cross-domain-policies": "none", + "referrer-policy": "strict-origin-when-cross-origin", + "x-ep-request-uuid": "cfd5bd41698109e6e2b988380270e748", + "cache-control": "private, no-cache, no-store", + "pragma": "no-cache", + "x-runtime": "0.143682", + "x-node": "bigweb33nuq", + "x-version-label": "easypost-202602021944-8c5966c6f9-master", + "x-backend": "easypost", + "x-proxied": "intlb4nuq d9379ca146,extlb1nuq cbbd141214", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload" + }, + "Status": { + "Code": 200, + "Message": "OK" + } + } + }, + { + "Duration": 226, + "RecordedAt": "2026-02-02T20:32:38.000000Z", + "Request": { + "Body": "", + "BodyContentType": "Text", + "ContentHeaders": {}, + "Method": "POST", + "RequestHeaders": { + "Authorization": "", + "User-Agent": "" + }, + "Uri": "https://api.easypost.com/v2/api_keys/ak_2df46e55f8434c3882b40f7c99c8b64f/enable" + }, + "Response": { + "Body": "{\"object\":\"ApiKey\",\"key\":\"\",\"mode\":\"production\",\"created_at\":\"2026-02-02T20:32:38Z\",\"active\":true,\"id\":\"ak_2df46e55f8434c3882b40f7c99c8b64f\"}", + "BodyContentType": "Json", + "ContentHeaders": { + "Expires": "0", + "Content-Type": "application/json; charset=utf-8", + "Content-Length": "199" + }, + "HttpVersion": "1.1", + "ResponseHeaders": { + "x-frame-options": "SAMEORIGIN", + "x-xss-protection": "1; mode=block", + "x-content-type-options": "nosniff", + "x-download-options": "noopen", + "x-permitted-cross-domain-policies": "none", + "referrer-policy": "strict-origin-when-cross-origin", + "x-ep-request-uuid": "cfd5bd41698109e6e2b988390270e7ae", + "cache-control": "private, no-cache, no-store", + "pragma": "no-cache", + "x-runtime": "0.116097", + "x-node": "bigweb42nuq", + "x-version-label": "easypost-202602021944-8c5966c6f9-master", + "x-backend": "easypost", + "x-proxied": "intlb5nuq d9379ca146,extlb1nuq cbbd141214", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload" + }, + "Status": { + "Code": 200, + "Message": "OK" + } + } + }, + { + "Duration": 359, + "RecordedAt": "2026-02-02T20:32:38.000000Z", + "Request": { + "Body": "", + "BodyContentType": "Text", + "ContentHeaders": {}, + "Method": "DELETE", + "RequestHeaders": { + "Authorization": "", + "User-Agent": "" + }, + "Uri": "https://api.easypost.com/v2/api_keys/ak_2df46e55f8434c3882b40f7c99c8b64f" + }, + "Response": { + "Body": "", + "BodyContentType": "Text", + "ContentHeaders": {}, + "HttpVersion": "1.1", + "ResponseHeaders": { + "x-frame-options": "SAMEORIGIN", + "x-xss-protection": "1; mode=block", + "x-content-type-options": "nosniff", + "x-download-options": "noopen", + "x-permitted-cross-domain-policies": "none", + "referrer-policy": "strict-origin-when-cross-origin", + "x-ep-request-uuid": "cfd5bd44698109e6e2b9883a0270e816", + "cache-control": "private, no-cache, no-store", + "pragma": "no-cache", + "x-runtime": "0.259319", + "x-node": "bigweb39nuq", + "x-version-label": "easypost-202602021944-8c5966c6f9-master", + "x-backend": "easypost", + "x-proxied": "intlb3nuq d9379ca146,extlb1nuq cbbd141214", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload" + }, + "Status": { + "Code": 204, + "Message": "No Content" + } + } + } +] diff --git a/EasyPost.nuspec b/EasyPost.nuspec index 6a5b5a1d2..bf74264a4 100644 --- a/EasyPost.nuspec +++ b/EasyPost.nuspec @@ -3,7 +3,7 @@ EasyPost-Official EasyPost (Official) - 7.4.0 + 7.5.0 EasyPost EasyPost https://www.easypost.com diff --git a/EasyPost/Models/API/ApiKey.cs b/EasyPost/Models/API/ApiKey.cs index bec334821..2cf475ee8 100644 --- a/EasyPost/Models/API/ApiKey.cs +++ b/EasyPost/Models/API/ApiKey.cs @@ -11,12 +11,24 @@ public class ApiKey : EasyPostObject { #region JSON Properties + /// + /// Whether the API key is active. + /// + [JsonProperty("active")] + public bool? Active { get; set; } + /// /// The actual key value to use for authentication. /// [JsonProperty("key")] public string? Key { get; set; } + /// + /// The mode of the API key (e.g., "production" or "test"). + /// + [JsonProperty("mode")] + public new string? Mode { get; set; } + #endregion } diff --git a/EasyPost/Properties/VersionInfo.cs b/EasyPost/Properties/VersionInfo.cs index 0b69e10cf..8295288fc 100644 --- a/EasyPost/Properties/VersionInfo.cs +++ b/EasyPost/Properties/VersionInfo.cs @@ -2,6 +2,6 @@ // Version information for an assembly must follow semantic versioning // When releasing a release candidate, append a 4th digit being the number of the release candidate -[assembly: AssemblyVersion("7.4.0")] -[assembly: AssemblyFileVersion("7.4.0")] -[assembly: AssemblyInformationalVersion("7.4.0")] +[assembly: AssemblyVersion("7.5.0")] +[assembly: AssemblyFileVersion("7.5.0")] +[assembly: AssemblyInformationalVersion("7.5.0")] diff --git a/EasyPost/Services/ApiKeyService.cs b/EasyPost/Services/ApiKeyService.cs index ce26e5e73..180d35bee 100644 --- a/EasyPost/Services/ApiKeyService.cs +++ b/EasyPost/Services/ApiKeyService.cs @@ -28,15 +28,6 @@ internal ApiKeyService(EasyPostClient client) #region CRUD Operations - /// - /// Get a list of all s. - /// Related API documentation. - /// - /// to use for the HTTP request. - /// A object. - [CrudOperations.Read] - public async Task All(CancellationToken cancellationToken = default) => await RequestAsync(Method.Get, "api_keys", cancellationToken); - /// /// Retrieve the s for a specific . /// @@ -66,6 +57,57 @@ internal ApiKeyService(EasyPostClient client) throw new FilteringError(string.Format(CultureInfo.InvariantCulture, Constants.ErrorMessages.NoObjectFound, "child")); } + /// + /// Get a list of all s. + /// Related API documentation. + /// + /// to use for the HTTP request. + /// A object. + [CrudOperations.Read] + public async Task All(CancellationToken cancellationToken = default) => await RequestAsync(Method.Get, "api_keys", cancellationToken); + + /// + /// Create an API key for a child or referral customer user. + /// + /// The mode for the API key (e.g., "production" or "test"). + /// to use for the HTTP request. + /// The created object. + [CrudOperations.Create] + public async Task Create(string mode, CancellationToken cancellationToken = default) + { + Dictionary parameters = new() { { "mode", mode } }; + return await RequestAsync(Method.Post, "api_keys", cancellationToken, parameters); + } + + /// + /// Delete an API key for a child or referral customer user. + /// + /// The ID of the to delete. + /// to use for the HTTP request. + [CrudOperations.Delete] + public async Task Delete(string id, CancellationToken cancellationToken = default) + { + await RequestAsync(Method.Delete, $"api_keys/{id}", cancellationToken); + } + + /// + /// Enable a child or referral customer API key. + /// + /// The ID of the to enable. + /// to use for the HTTP request. + /// The updated object. + [CrudOperations.Update] + public async Task Enable(string id, CancellationToken cancellationToken = default) => await RequestAsync(Method.Post, $"api_keys/{id}/enable", cancellationToken); + + /// + /// Disable a child or referral customer API key. + /// + /// The ID of the to disable. + /// to use for the HTTP request. + /// The updated object. + [CrudOperations.Update] + public async Task Disable(string id, CancellationToken cancellationToken = default) => await RequestAsync(Method.Post, $"api_keys/{id}/disable", cancellationToken); + #endregion } } From 2d4111e4ca107adcbc1a69252b9122a8a4d3cbf4 Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:53:58 -0700 Subject: [PATCH 2/3] chore: trigger GitHub Actions From 65c2194b3a3df6290f9d9d6e6ae3f4cccb3427fa Mon Sep 17 00:00:00 2001 From: Justintime50 <39606064+Justintime50@users.noreply.github.com> Date: Tue, 3 Feb 2026 09:58:51 -0700 Subject: [PATCH 3/3] fix: lint --- EasyPost/Services/ApiKeyService.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/EasyPost/Services/ApiKeyService.cs b/EasyPost/Services/ApiKeyService.cs index 180d35bee..4b4b2a1a5 100644 --- a/EasyPost/Services/ApiKeyService.cs +++ b/EasyPost/Services/ApiKeyService.cs @@ -84,6 +84,7 @@ public async Task Create(string mode, CancellationToken cancellationToke /// /// The ID of the to delete. /// to use for the HTTP request. + /// A representing the asynchronous delete operation. [CrudOperations.Delete] public async Task Delete(string id, CancellationToken cancellationToken = default) {