Skip to content
Closed
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
74 changes: 74 additions & 0 deletions HydrantCAProxy.Tests/RequestManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,80 @@ public void GetSansRequest_AllTypes_Populated()
Assert.Single(result.Upn);
}

// ---------------------------------------------------------------------
// GetDomainsToValidate
// ---------------------------------------------------------------------

[Fact]
public void GetDomainsToValidate_CnOnly_ReturnsSingleDomain()
{
var result = _sut.GetDomainsToValidate(SampleCsr, null);

Assert.Single(result);
Assert.Equal("unit.test.hydrantid.local", result[0]);
}

[Fact]
public void GetDomainsToValidate_CnPlusDnsSans_ReturnsDeduped()
{
var sans = new Dictionary<string, string[]>
{
["dnsname"] = new[] { "unit.test.hydrantid.local", "www.example.com" }
};

var result = _sut.GetDomainsToValidate(SampleCsr, sans);

Assert.Equal(2, result.Count);
Assert.Contains("unit.test.hydrantid.local", result);
Assert.Contains("www.example.com", result);
}

[Fact]
public void GetDomainsToValidate_SansCaseVariant_DedupedAgainstCn()
{
var sans = new Dictionary<string, string[]>
{
["dnsname"] = new[] { "UNIT.TEST.HYDRANTID.LOCAL" }
};

var result = _sut.GetDomainsToValidate(SampleCsr, sans);

Assert.Single(result);
}

[Fact]
public void GetDomainsToValidate_NullCsr_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _sut.GetDomainsToValidate(null, null));
}

// ---------------------------------------------------------------------
// GetCreateDomainValidationRequest
// ---------------------------------------------------------------------

[Fact]
public void GetCreateDomainValidationRequest_Valid_SetsDnsMethodAndOmitsAccountId()
{
var result = _sut.GetCreateDomainValidationRequest("example.com", "validator-1");

Assert.Equal("example.com", result.DomainName);
Assert.Equal("validator-1", result.Validator);
Assert.Equal(ValidationMethod.Dns, result.Method);
Assert.Null(result.AccountId);
}

[Fact]
public void GetCreateDomainValidationRequest_NullDomain_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _sut.GetCreateDomainValidationRequest(null, "validator-1"));
}

[Fact]
public void GetCreateDomainValidationRequest_NullValidatorId_ThrowsArgumentNullException()
{
Assert.Throws<ArgumentNullException>(() => _sut.GetCreateDomainValidationRequest("example.com", null));
}

// ---------------------------------------------------------------------
// GetCertificatesListRequest
// ---------------------------------------------------------------------
Expand Down
131 changes: 131 additions & 0 deletions HydrantCAProxy/Client/HydrantIdClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,137 @@ public async Task<List<Policy>> GetPolicyList()



public async Task<List<Domain>> GetDomainListAsync()
{
Log.MethodEntry();
var apiEndpoint = "/api/v2/domains/";
var fullUrl = BaseUrl + apiEndpoint;
Log.LogTrace("GetDomainListAsync: API Url={Url}", fullUrl);

var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };

try
{
var restClient = ConfigureRestClient("get", fullUrl);
using var resp = await restClient.GetAsync(apiEndpoint);
var responseContent = await resp.Content.ReadAsStringAsync();

Log.LogTrace("GetDomainListAsync: HTTP status={StatusCode}, response length={Len}",
resp.StatusCode, responseContent?.Length ?? 0);

if (!resp.IsSuccessStatusCode)
{
Log.LogError("GetDomainListAsync: request failed with status {StatusCode}: {Response}", resp.StatusCode, responseContent);
throw new HttpRequestException($"GetDomainListAsync failed with HTTP {resp.StatusCode}: {responseContent}");
}

var domains = JsonConvert.DeserializeObject<List<Domain>>(responseContent, settings);

if (domains == null)
{
Log.LogWarning("GetDomainListAsync: deserialized domain list is null");
return new List<Domain>();
}

Log.LogTrace("GetDomainListAsync: returned {Count} domains", domains.Count);
return domains;
}
catch (Exception e)
{
Log.LogError(e, "GetDomainListAsync: exception: {Message}", e.Message);
throw;
}
}



public async Task<Domain> GetSubmitCreateDomainValidationAsync(CreateDomainValidationPayload payload)
{
Log.MethodEntry();
Log.LogTrace("GetSubmitCreateDomainValidationAsync: payload is {Null}", payload == null ? "NULL" : "present");

if (payload == null)
throw new ArgumentNullException(nameof(payload), "payload cannot be null.");

var apiEndpoint = "/api/v2/domains/";
var fullUrl = BaseUrl + apiEndpoint;
Log.LogTrace("GetSubmitCreateDomainValidationAsync: API Url={Url}", fullUrl);

var json = JsonConvert.SerializeObject(payload);
Log.LogTrace("GetSubmitCreateDomainValidationAsync: request JSON: {Json}", json);

var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };

try
{
var restClient = ConfigureRestClient("post", fullUrl);
using var resp = await restClient.PostAsync(apiEndpoint, new StringContent(json, Encoding.UTF8, "application/json"));
var responseContent = await resp.Content.ReadAsStringAsync();

Log.LogTrace("GetSubmitCreateDomainValidationAsync: HTTP status={StatusCode}, response length={Len}",
resp.StatusCode, responseContent?.Length ?? 0);

if (!resp.IsSuccessStatusCode)
{
Log.LogError("GetSubmitCreateDomainValidationAsync: request failed with status {StatusCode}: {Response}", resp.StatusCode, responseContent);
throw new HttpRequestException($"GetSubmitCreateDomainValidationAsync failed with HTTP {resp.StatusCode}: {responseContent}");
}

var domain = JsonConvert.DeserializeObject<Domain>(responseContent, settings);
Log.LogTrace("GetSubmitCreateDomainValidationAsync: response JSON: {Json}", JsonConvert.SerializeObject(domain));
return domain;
}
catch (Exception e)
{
Log.LogError(e, "GetSubmitCreateDomainValidationAsync: exception: {Message}", e.Message);
throw;
}
}



public async Task<Domain> GetSubmitCheckDomainValidationAsync(string domainId)
{
Log.MethodEntry();
Log.LogTrace("GetSubmitCheckDomainValidationAsync: domainId='{DomainId}'", domainId ?? "(null)");

if (string.IsNullOrEmpty(domainId))
throw new ArgumentNullException(nameof(domainId), "domainId cannot be null or empty.");

var apiEndpoint = $"/api/v2/domains/{domainId}/validate";
var fullUrl = BaseUrl + apiEndpoint;
Log.LogTrace("GetSubmitCheckDomainValidationAsync: API Url={Url}", fullUrl);

var settings = new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore };

try
{
var restClient = ConfigureRestClient("get", fullUrl);
using var resp = await restClient.GetAsync(apiEndpoint);
var responseContent = await resp.Content.ReadAsStringAsync();

Log.LogTrace("GetSubmitCheckDomainValidationAsync: HTTP status={StatusCode}, response length={Len}",
resp.StatusCode, responseContent?.Length ?? 0);

if (!resp.IsSuccessStatusCode)
{
Log.LogError("GetSubmitCheckDomainValidationAsync: request failed with status {StatusCode}: {Response}", resp.StatusCode, responseContent);
throw new HttpRequestException($"GetSubmitCheckDomainValidationAsync failed with HTTP {resp.StatusCode}: {responseContent}");
}

var domain = JsonConvert.DeserializeObject<Domain>(responseContent, settings);
Log.LogTrace("GetSubmitCheckDomainValidationAsync: response JSON: {Json}", JsonConvert.SerializeObject(domain));
return domain;
}
catch (Exception e)
{
Log.LogError(e, "GetSubmitCheckDomainValidationAsync: exception: {Message}", e.Message);
throw;
}
}



public async Task<Certificate> GetSubmitGetCertificateAsync(string certificateId)
{
Log.MethodEntry();
Expand Down
34 changes: 34 additions & 0 deletions HydrantCAProxy/Client/Models/CreateDomainValidationPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2025 Keyfactor
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain a
// copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
// required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
// OR CONDITIONS OF ANY KIND, either express or implied. See the License for
// thespecific language governing permissions and limitations under the
// License.
using Keyfactor.HydrantId.Client.Models.Enums;
using Keyfactor.HydrantId.Interfaces;
using Newtonsoft.Json;

namespace Keyfactor.HydrantId.Client.Models
{
public class CreateDomainValidationPayload : ICreateDomainValidationPayload
{
[JsonProperty("accountId", NullValueHandling = NullValueHandling.Ignore)]
public string AccountId { get;set; }

[JsonProperty("domain", NullValueHandling = NullValueHandling.Ignore)]
public string DomainName { get;set; }

[JsonProperty("validator", NullValueHandling = NullValueHandling.Ignore)]
public string Validator { get;set; }

[JsonProperty("method", NullValueHandling = NullValueHandling.Ignore)]
public ValidationMethod? Method { get;set; }

[JsonProperty("payload", NullValueHandling = NullValueHandling.Ignore)]
public object Payload { get;set; }

}
}
64 changes: 64 additions & 0 deletions HydrantCAProxy/Client/Models/Domain.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// Copyright 2025 Keyfactor
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain a
// copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
// required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
// OR CONDITIONS OF ANY KIND, either express or implied. See the License for
// thespecific language governing permissions and limitations under the
// License.
using Keyfactor.HydrantId.Client.Models.Enums;
using Keyfactor.HydrantId.Interfaces;
using Newtonsoft.Json;

namespace Keyfactor.HydrantId.Client.Models
{
public class Domain : IDomain
{
[JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)]
public string Id { get;set; }

[JsonProperty("validator", NullValueHandling = NullValueHandling.Ignore)]
public string Validator { get;set; }

[JsonProperty("accountId", NullValueHandling = NullValueHandling.Ignore)]
public string AccountId { get;set; }

[JsonProperty("organizationIds", NullValueHandling = NullValueHandling.Ignore)]
public string OrganizationIds { get;set; }

[JsonProperty("domain", NullValueHandling = NullValueHandling.Ignore)]
public string DomainName { get;set; }

[JsonProperty("method", NullValueHandling = NullValueHandling.Ignore)]
public ValidationMethod? Method { get;set; }

[JsonProperty("code", NullValueHandling = NullValueHandling.Ignore)]
public string Code { get;set; }

[JsonProperty("codeInstructions", NullValueHandling = NullValueHandling.Ignore)]
public string CodeInstructions { get;set; }

[JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)]
public string Message { get;set; }

[JsonProperty("payload", NullValueHandling = NullValueHandling.Ignore)]
public object Payload { get;set; }

[JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)]
public DomainStatusEnum? Status { get;set; }

[JsonProperty("domainValidUntil", NullValueHandling = NullValueHandling.Ignore)]
public string DomainValidUntil { get;set; }

[JsonProperty("codeValidUntil", NullValueHandling = NullValueHandling.Ignore)]
public string CodeValidUntil { get;set; }

[JsonProperty("createdAt", NullValueHandling = NullValueHandling.Ignore)]
public string CreatedAt { get;set; }

[JsonProperty("updatedAt", NullValueHandling = NullValueHandling.Ignore)]
public string UpdatedAt { get;set; }

}
}
23 changes: 23 additions & 0 deletions HydrantCAProxy/Client/Models/Enums/DomainStatusEnum.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2025 Keyfactor
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain a
// copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
// required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
// OR CONDITIONS OF ANY KIND, either express or implied. See the License for
// thespecific language governing permissions and limitations under the
// License.
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Keyfactor.HydrantId.Client.Models.Enums
{
[JsonConverter(typeof(StringEnumConverter))]
public enum DomainStatusEnum
{
[EnumMember(Value = "PENDING")] Pending = 1,
[EnumMember(Value = "VALIDATED")] Validated = 2,
[EnumMember(Value = "EXPIRED")] Expired = 3
}
}
24 changes: 24 additions & 0 deletions HydrantCAProxy/Client/Models/Enums/ValidationMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2025 Keyfactor
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain a
// copy of the License at http://www.apache.org/licenses/LICENSE-2.0. Unless
// required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES
// OR CONDITIONS OF ANY KIND, either express or implied. See the License for
// thespecific language governing permissions and limitations under the
// License.
using System.Runtime.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;

namespace Keyfactor.HydrantId.Client.Models.Enums
{
[JsonConverter(typeof(StringEnumConverter))]
public enum ValidationMethod
{
[EnumMember(Value = "DNS")] Dns = 1,
[EnumMember(Value = "WELLKNOWN")] WellKnown = 2,
[EnumMember(Value = "IMPORT")] Import = 3,
[EnumMember(Value = "PERSISTENTDNSTXT")] PersistentDnsTxt = 4
}
}
3 changes: 3 additions & 0 deletions HydrantCAProxy/Client/Models/PolicyDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,8 @@ public class PolicyDetails : IPolicyDetails
[JsonProperty("customExtensions", NullValueHandling = NullValueHandling.Ignore)]
public List<PolicyDetailsCustomExtensions> CustomExtensions { get;set; }

[JsonProperty("validator", NullValueHandling = NullValueHandling.Ignore)]
public string Validator { get;set; }

}
}
Loading
Loading