Skip to content

Commit c32bcd0

Browse files
Merge ee9828c into f8c8f22
2 parents f8c8f22 + ee9828c commit c32bcd0

19 files changed

Lines changed: 1663 additions & 190 deletions

.github/workflows/keyfactor-bootstrap-workflow.yml

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,9 @@ on:
1111

1212
jobs:
1313
call-starter-workflow:
14-
uses: keyfactor/actions/.github/workflows/starter.yml@v4
15-
permissions:
16-
contents: write
17-
with:
18-
command_token_url: ${{ vars.COMMAND_TOKEN_URL }}
19-
command_hostname: ${{ vars.COMMAND_HOSTNAME }}
20-
command_base_api_path: ${{ vars.COMMAND_API_PATH }}
14+
uses: keyfactor/actions/.github/workflows/starter.yml@v3
2115
secrets:
22-
token: ${{ github.token }}
16+
token: ${{ secrets.V2BUILDTOKEN}}
17+
APPROVE_README_PUSH: ${{ secrets.APPROVE_README_PUSH}}
2318
gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
2419
gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
25-
scan_token: ${{ secrets.SAST_TOKEN }}
26-
entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }}
27-
entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }}
28-
command_client_id: ${{ secrets.COMMAND_CLIENT_ID }}
29-
command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,4 @@ healthchecksdb
347347
/cert.pem
348348
/cert.csr
349349
.config/dotnet-tools.json
350+
/.claude/agents

CHANGELOG.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,11 @@
1-
## 1.0.0
2-
* Initial release
1+
### 1.1.0
2+
* **Procedures as ProductIDs**`GetProductIds()` now returns Nexus CA token procedure names dynamically from the `/procedures` endpoint rather than a single hardcoded value. Each certificate template in Command should be configured with a procedure name as its ProductID.
3+
* **Pagination**`Synchronize` and `GetCertificateList` now page through results in batches of 500, resolving failures that occurred when the max returned records limit (that defaults to 500) was reached.
4+
* **Conditional synchronization**`Synchronize` throws `NotSupportedException` with a clear explanation when `SyncProcedureField` is not configured. When configured, sync reads the specified `ExtendedCertSearch` field from each certificate to resolve its ProductID. See documentation for CA-side requirements.
5+
* **`GetSingleRecord` fix**`ProductID` is now resolved from the configured `ExtendedCertSearch` field instead of incorrectly using `CertId`.
6+
* **`ValidateProductInfo`** — now validates that `ProductID` is non-empty.
7+
* **Fixed deadlock risk** — replaced `.Result` with `.GetAwaiter().GetResult()` in `GetProductIds()`.
8+
* **General Cleanup** — corrected "retreived" / "retreive" in log messages throughout.
9+
10+
### 1.0.0
11+
* Initial release
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
// Copyright 2025 Keyfactor
2+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
3+
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
4+
// Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS,
5+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
6+
// and limitations under the License.
7+
8+
using System.Collections.Generic;
9+
using FluentAssertions;
10+
using Keyfactor.Extensions.CAPlugin.NexusCertManager.models;
11+
using Keyfactor.PKI.Enums.EJBCA;
12+
using Xunit;
13+
14+
namespace Keyfactor.Extensions.CAPlugin.NexusCertManager.Tests
15+
{
16+
/// <summary>
17+
/// Tests for <see cref="Helpers.GetStatusCodeFromNexusCADescription"/>.
18+
///
19+
/// Covers all known Nexus status string mappings and the unknown-status fallback.
20+
/// </summary>
21+
public class StatusMappingTests
22+
{
23+
[Theory]
24+
[InlineData("issued")]
25+
[InlineData("approved")]
26+
[InlineData("expired")]
27+
[InlineData("active")]
28+
public void GetStatusCode_ActiveStatuses_ReturnGenerated(string status)
29+
{
30+
Helpers.GetStatusCodeFromNexusCADescription(status)
31+
.Should().Be((int)EndEntityStatus.GENERATED);
32+
}
33+
34+
[Theory]
35+
[InlineData("processing")]
36+
[InlineData("reissue_pending")]
37+
[InlineData("pending")]
38+
[InlineData("waiting_pickup")]
39+
[InlineData("needs_approval")]
40+
public void GetStatusCode_PendingStatuses_ReturnExternalValidation(string status)
41+
{
42+
Helpers.GetStatusCodeFromNexusCADescription(status)
43+
.Should().Be((int)EndEntityStatus.EXTERNALVALIDATION);
44+
}
45+
46+
[Theory]
47+
[InlineData("denied")]
48+
[InlineData("rejected")]
49+
[InlineData("canceled")]
50+
public void GetStatusCode_FailureStatuses_ReturnFailed(string status)
51+
{
52+
Helpers.GetStatusCodeFromNexusCADescription(status)
53+
.Should().Be((int)EndEntityStatus.FAILED);
54+
}
55+
56+
[Fact]
57+
public void GetStatusCode_Revoked_ReturnRevoked()
58+
{
59+
Helpers.GetStatusCodeFromNexusCADescription("revoked")
60+
.Should().Be((int)EndEntityStatus.REVOKED);
61+
}
62+
63+
[Theory]
64+
[InlineData("unknown_value")]
65+
[InlineData("")]
66+
[InlineData(null)]
67+
public void GetStatusCode_UnknownOrEmptyStatus_ReturnNew(string status)
68+
{
69+
Helpers.GetStatusCodeFromNexusCADescription(status)
70+
.Should().Be((int)EndEntityStatus.NEW,
71+
because: "unknown statuses should default to NEW for manual triage");
72+
}
73+
}
74+
75+
/// <summary>
76+
/// Tests for <see cref="Helpers.GetRevocationReasonCodeFromNexusCADescription"/>.
77+
///
78+
/// Covers all known Nexus revocation reason strings and the unspecified fallback.
79+
/// </summary>
80+
public class RevocationReasonMappingTests
81+
{
82+
[Theory]
83+
[InlineData("key compromise", RevocationReason.KeyCompromise)]
84+
[InlineData("affiliation changed", RevocationReason.AffiliationChanged)]
85+
[InlineData("superseded", RevocationReason.Superseded)]
86+
[InlineData("cessation of operation", RevocationReason.CessationOfOperation)]
87+
[InlineData("certificate hold", RevocationReason.CertificateHold)]
88+
[InlineData("privilege withdrawn", RevocationReason.PrivilegeWithdrawn)]
89+
public void GetRevocationReason_KnownReasons_ReturnCorrectCode(string reason, int expected)
90+
{
91+
Helpers.GetRevocationReasonCodeFromNexusCADescription(reason)
92+
.Should().Be(expected);
93+
}
94+
95+
[Theory]
96+
[InlineData("KEY COMPROMISE")] // case insensitivity
97+
[InlineData("Key Compromise")]
98+
public void GetRevocationReason_CaseInsensitive(string reason)
99+
{
100+
Helpers.GetRevocationReasonCodeFromNexusCADescription(reason)
101+
.Should().Be((int)RevocationReason.KeyCompromise);
102+
}
103+
104+
[Theory]
105+
[InlineData("unknown reason")]
106+
[InlineData("")]
107+
[InlineData(null)]
108+
public void GetRevocationReason_UnknownOrEmpty_ReturnUnspecified(string reason)
109+
{
110+
Helpers.GetRevocationReasonCodeFromNexusCADescription(reason)
111+
.Should().Be(RevocationReason.Unspecified);
112+
}
113+
}
114+
115+
/// <summary>
116+
/// Tests for <see cref="Helpers.ParseSubject"/>.
117+
///
118+
/// Covers CN extraction from various subject formats.
119+
/// </summary>
120+
public class ParseSubjectTests
121+
{
122+
[Fact]
123+
public void ParseSubject_SimpleCN_ExtractedCorrectly()
124+
{
125+
Helpers.ParseSubject("CN=test.example.com, O=Acme", "CN=")
126+
.Should().Be("test.example.com");
127+
}
128+
129+
[Fact]
130+
public void ParseSubject_CNWithEscapedComma_PreservesComma()
131+
{
132+
// Escaped commas in the CN value should survive round-trip
133+
Helpers.ParseSubject(@"CN=Last\, First, O=Acme", "CN=")
134+
.Should().Be("Last, First");
135+
}
136+
137+
[Fact]
138+
public void ParseSubject_MissingRdn_ThrowsException()
139+
{
140+
var act = () => Helpers.ParseSubject("O=Acme, C=US", "CN=");
141+
act.Should().Throw<System.Exception>().WithMessage("*CN=*");
142+
}
143+
144+
[Fact]
145+
public void ParseSubject_ExtractsOrgUnit()
146+
{
147+
Helpers.ParseSubject("CN=test, OU=Engineering, O=Acme", "OU=")
148+
.Should().Be("Engineering");
149+
}
150+
}
151+
152+
/// <summary>
153+
/// Tests for <see cref="CmApiException"/>.
154+
///
155+
/// Covers error code descriptions and exception message plumbing.
156+
/// </summary>
157+
public class CmApiExceptionTests
158+
{
159+
[Theory]
160+
[InlineData(0, "Success")]
161+
[InlineData(-1, "General error")]
162+
[InlineData(-7, "Missing field")]
163+
[InlineData(-8, "Encoding error")]
164+
[InlineData(-12, "Not initialized")]
165+
[InlineData(-14, "Bad field value")]
166+
[InlineData(-15, "Privilege error")]
167+
[InlineData(-17, "Bad signature")]
168+
[InlineData(-18, "Connection error")]
169+
[InlineData(-19, "Signature required")]
170+
[InlineData(-40, "Too many requests")]
171+
[InlineData(-999, "Unknown error")]
172+
public void GetErrorDescription_KnownCodes_ReturnExpectedDescription(int code, string expected)
173+
{
174+
var ex = new CmApiException(code, "test");
175+
ex.GetErrorDescription().Should().Be(expected);
176+
}
177+
178+
[Fact]
179+
public void CmApiException_MessageIsPreserved()
180+
{
181+
var ex = new CmApiException(-1, "Something went wrong");
182+
ex.Message.Should().Be("Something went wrong");
183+
ex.ErrorCode.Should().Be(-1);
184+
}
185+
}
186+
187+
/// <summary>
188+
/// Tests for <see cref="ListCertificatesRequest"/> — the query parameter model
189+
/// used to paginate and filter certificate list requests.
190+
///
191+
/// Covers that key pagination fields are set correctly when constructing requests.
192+
/// </summary>
193+
public class ListCertificatesRequestTests
194+
{
195+
[Fact]
196+
public void ListCertificatesRequest_PaginationFields_SetCorrectly()
197+
{
198+
var req = new ListCertificatesRequest
199+
{
200+
SearchLimit = 500,
201+
SearchOffset = 1000
202+
};
203+
204+
req.SearchLimit.Should().Be(500);
205+
req.SearchOffset.Should().Be(1000);
206+
}
207+
208+
[Fact]
209+
public void ListCertificatesRequest_ExtendedSearchFields_SetCorrectly()
210+
{
211+
var req = new ListCertificatesRequest { Field1 = "ProcA", Field3 = "Something" };
212+
213+
req.Field1.Should().Be("ProcA");
214+
req.Field2.Should().BeNull();
215+
req.Field3.Should().Be("Something");
216+
}
217+
218+
[Fact]
219+
public void ListCertificatesRequest_AllFieldsNullByDefault()
220+
{
221+
var req = new ListCertificatesRequest();
222+
223+
req.SearchLimit.Should().BeNull();
224+
req.SearchOffset.Should().BeNull();
225+
req.Field1.Should().BeNull();
226+
req.OrderBy.Should().BeNull();
227+
}
228+
}
229+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<RootNamespace>Keyfactor.Extensions.CAPlugin.NexusCertManager.Tests</RootNamespace>
6+
<AssemblyName>NexusCertManagerCAPlugin.Tests</AssemblyName>
7+
<ImplicitUsings>disable</ImplicitUsings>
8+
<Nullable>disable</Nullable>
9+
<IsPackable>false</IsPackable>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<!-- Test framework -->
14+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
15+
<PackageReference Include="xunit" Version="2.7.0" />
16+
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
17+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
18+
<PrivateAssets>all</PrivateAssets>
19+
</PackageReference>
20+
<!-- Mocking -->
21+
<PackageReference Include="Moq" Version="4.20.70" />
22+
<!-- HTTP mocking for NexusCertManagerClient tests -->
23+
<PackageReference Include="RichardSzalay.MockHttp" Version="7.0.0" />
24+
<!-- Assertions -->
25+
<PackageReference Include="FluentAssertions" Version="6.12.0" />
26+
<!-- Match main project dependencies -->
27+
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
28+
<PackageReference Include="RestSharp" Version="112.1.0" />
29+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
30+
</ItemGroup>
31+
32+
<ItemGroup>
33+
<ProjectReference Include="..\nexus-certificate-manager-caplugin\NexusCertManagerCAPlugin.csproj" />
34+
</ItemGroup>
35+
36+
</Project>

0 commit comments

Comments
 (0)