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
27 changes: 27 additions & 0 deletions .github/workflows/keyfactor-starter-workflow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Keyfactor Bootstrap Workflow

on:
workflow_dispatch:
pull_request:
types: [opened, closed, synchronize, edited, reopened]
push:
create:
branches:
- 'release-*.*'

jobs:
call-starter-workflow:
uses: keyfactor/actions/.github/workflows/starter.yml@v5
with:
command_token_url: ${{ vars.COMMAND_TOKEN_URL }}
command_hostname: ${{ vars.COMMAND_HOSTNAME }}
command_base_api_path: ${{ vars.COMMAND_API_PATH }}
secrets:
token: ${{ secrets.V2BUILDTOKEN}}
gpg_key: ${{ secrets.KF_GPG_PRIVATE_KEY }}
gpg_pass: ${{ secrets.KF_GPG_PASSPHRASE }}
scan_token: ${{ secrets.SAST_TOKEN }}
entra_username: ${{ secrets.DOCTOOL_ENTRA_USERNAME }}
entra_password: ${{ secrets.DOCTOOL_ENTRA_PASSWD }}
command_client_id: ${{ secrets.COMMAND_CLIENT_ID }}
command_client_secret: ${{ secrets.COMMAND_CLIENT_SECRET }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,7 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp

# Claude Code / agent state, and local vendor secrets
.claude/
.secrets/
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
v1.0.0
- Inital Version
35 changes: 35 additions & 0 deletions Keyfactor.DnsProvider.LuaDns.Tests/FakeHttpMessageHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.Net;
using System.Net.Http;

namespace Keyfactor.Extensions.DomainValidator.LuaDns.Tests
{
/// <summary>
/// Routes requests to a caller-supplied responder so LuaDnsProvider can be
/// exercised end-to-end without touching the real LuaDNS API.
/// </summary>
internal class FakeHttpMessageHandler : HttpMessageHandler
{
public List<HttpRequestMessage> Requests { get; } = new();

private readonly Func<HttpRequestMessage, HttpResponseMessage> _responder;

public FakeHttpMessageHandler(Func<HttpRequestMessage, HttpResponseMessage> responder)
{
_responder = responder;
}

protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Requests.Add(request);
return Task.FromResult(_responder(request));
}

public static HttpResponseMessage Json(HttpStatusCode status, string body)
{
return new HttpResponseMessage(status)
{
Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json")
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<IsPackable>false</IsPackable>
<RootNamespace>Keyfactor.Extensions.DomainValidator.LuaDns.Tests</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Keyfactor.DnsProvider.LuaDns\Keyfactor.DnsProvider.LuaDns.csproj" />
</ItemGroup>
</Project>
60 changes: 60 additions & 0 deletions Keyfactor.DnsProvider.LuaDns.Tests/LuaDnsDomainValidatorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using Xunit;

namespace Keyfactor.Extensions.DomainValidator.LuaDns.Tests
{
public class LuaDnsDomainValidatorTests
{
[Fact]
public void GetValidationType_ReturnsDns01()
{
var validator = new LuaDnsDomainValidator();

Assert.Equal("dns-01", validator.GetValidationType());
}

[Fact]
public void GetDomainValidatorAnnotations_DeclaresUsernameAndApiKey()
{
var validator = new LuaDnsDomainValidator();

var annotations = validator.GetDomainValidatorAnnotations();

Assert.True(annotations.ContainsKey("LuaDns_Username"));
Assert.True(annotations.ContainsKey("LuaDns_ApiKey"));
Assert.Equal("Secret", annotations["LuaDns_ApiKey"].Type);
Assert.True(annotations["LuaDns_ApiKey"].Hidden);
Assert.False(annotations["LuaDns_Username"].Hidden);
}

[Fact]
public async Task ValidateConfiguration_ThrowsWhenUsernameMissing()
{
var validator = new LuaDnsDomainValidator();
var config = new Dictionary<string, object> { ["LuaDns_ApiKey"] = "key" };

await Assert.ThrowsAsync<ArgumentException>(() => validator.ValidateConfiguration(config));
}

[Fact]
public async Task ValidateConfiguration_ThrowsWhenApiKeyMissing()
{
var validator = new LuaDnsDomainValidator();
var config = new Dictionary<string, object> { ["LuaDns_Username"] = "user@example.com" };

await Assert.ThrowsAsync<ArgumentException>(() => validator.ValidateConfiguration(config));
}

[Fact]
public async Task ValidateConfiguration_SucceedsWhenBothFieldsPresent()
{
var validator = new LuaDnsDomainValidator();
var config = new Dictionary<string, object>
{
["LuaDns_Username"] = "user@example.com",
["LuaDns_ApiKey"] = "key"
};

await validator.ValidateConfiguration(config);
}
}
}
205 changes: 205 additions & 0 deletions Keyfactor.DnsProvider.LuaDns.Tests/LuaDnsProviderTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
using System.Net;
using Xunit;

namespace Keyfactor.Extensions.DomainValidator.LuaDns.Tests
{
public class LuaDnsProviderTests
{
[Fact]
public void FindBestMatch_PicksLongestMatchingSuffix()
{
var zones = new Dictionary<string, int>
{
["com"] = 1,
["example.com"] = 2,
["other.com"] = 3
};

var match = LuaDnsProvider.FindBestMatch(zones, "_acme-challenge.www.example.com");

Assert.NotNull(match);
Assert.Equal("example.com", match.Value.Key);
Assert.Equal(2, match.Value.Value);
}

[Fact]
public void FindBestMatch_MatchesExactZoneName()
{
var zones = new Dictionary<string, int> { ["example.com"] = 2 };

var match = LuaDnsProvider.FindBestMatch(zones, "example.com");

Assert.NotNull(match);
Assert.Equal("example.com", match.Value.Key);
}

[Fact]
public void FindBestMatch_ReturnsNullWhenNoZoneMatches()
{
var zones = new Dictionary<string, int> { ["example.com"] = 2 };

var match = LuaDnsProvider.FindBestMatch(zones, "unrelated-domain.net");

Assert.Null(match);
}

[Fact]
public void FindBestMatch_DoesNotMatchUnrelatedSuffixSubstring()
{
// "notexample.com" must not match zone "example.com" just because it ends with the same characters.
var zones = new Dictionary<string, int> { ["example.com"] = 2 };

var match = LuaDnsProvider.FindBestMatch(zones, "notexample.com");

Assert.Null(match);
}

[Fact]
public async Task CreateRecordAsync_PostsTxtRecordToResolvedZone()
{
HttpRequestMessage postRequest = null;

var handler = new FakeHttpMessageHandler(req =>
{
if (req.Method == HttpMethod.Get && req.RequestUri.PathAndQuery.EndsWith("zones"))
{
return FakeHttpMessageHandler.Json(HttpStatusCode.OK,
"[{\"id\":42,\"name\":\"example.com.\"}]");
}

if (req.Method == HttpMethod.Post)
{
postRequest = req;
return FakeHttpMessageHandler.Json(HttpStatusCode.OK,
"{\"id\":1,\"zone_id\":42,\"name\":\"_acme-challenge.example.com.\",\"type\":\"TXT\",\"content\":\"abc123\",\"ttl\":300}");
}

throw new InvalidOperationException($"Unexpected request: {req.Method} {req.RequestUri}");
});

var provider = new LuaDnsProvider("user@example.com", "apikey", handler);

var result = await provider.CreateRecordAsync("_acme-challenge.example.com", "abc123", "TXT");

Assert.True(result);
Assert.NotNull(postRequest);
Assert.EndsWith("zones/42/records", postRequest.RequestUri.PathAndQuery);

var body = await postRequest.Content.ReadAsStringAsync();
Assert.Contains("\"name\":\"_acme-challenge.example.com.\"", body);
Assert.Contains("\"type\":\"TXT\"", body);
Assert.Contains("\"content\":\"abc123\"", body);
}

[Fact]
public async Task CreateRecordAsync_ThrowsWithApiDetailsWhenZoneApiRejects()
{
var handler = new FakeHttpMessageHandler(req =>
{
if (req.Method == HttpMethod.Get && req.RequestUri.PathAndQuery.EndsWith("zones"))
{
return FakeHttpMessageHandler.Json(HttpStatusCode.OK,
"[{\"id\":42,\"name\":\"example.com.\"}]");
}

return FakeHttpMessageHandler.Json(HttpStatusCode.BadRequest, "{\"error\":\"invalid content\"}");
});

var provider = new LuaDnsProvider("user@example.com", "apikey", handler);

var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => provider.CreateRecordAsync("_acme-challenge.example.com", "abc123", "TXT"));

Assert.Contains("400", ex.Message);
Assert.Contains("example.com", ex.Message);
}

[Fact]
public async Task CreateRecordAsync_ThrowsWhenNoZoneMatches()
{
var handler = new FakeHttpMessageHandler(req =>
FakeHttpMessageHandler.Json(HttpStatusCode.OK, "[{\"id\":42,\"name\":\"other.com.\"}]"));

var provider = new LuaDnsProvider("user@example.com", "apikey", handler);

var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => provider.CreateRecordAsync("_acme-challenge.example.com", "abc123", "TXT"));

Assert.Contains("No LuaDNS zone found", ex.Message);
}

[Fact]
public async Task DeleteRecordAsync_DeletesMatchingRecord()
{
HttpRequestMessage deleteRequest = null;

var handler = new FakeHttpMessageHandler(req =>
{
if (req.Method == HttpMethod.Get && req.RequestUri.PathAndQuery.EndsWith("zones"))
{
return FakeHttpMessageHandler.Json(HttpStatusCode.OK,
"[{\"id\":42,\"name\":\"example.com.\"}]");
}

if (req.Method == HttpMethod.Get && req.RequestUri.PathAndQuery.Contains("records"))
{
return FakeHttpMessageHandler.Json(HttpStatusCode.OK,
"[{\"id\":7,\"zone_id\":42,\"name\":\"_acme-challenge.example.com.\",\"type\":\"TXT\",\"content\":\"abc123\",\"ttl\":300}]");
}

if (req.Method == HttpMethod.Delete)
{
deleteRequest = req;
return FakeHttpMessageHandler.Json(HttpStatusCode.OK, "{}");
}

throw new InvalidOperationException($"Unexpected request: {req.Method} {req.RequestUri}");
});

var provider = new LuaDnsProvider("user@example.com", "apikey", handler);

var result = await provider.DeleteRecordAsync("_acme-challenge.example.com", "TXT");

Assert.True(result);
Assert.NotNull(deleteRequest);
Assert.EndsWith("zones/42/records/7", deleteRequest.RequestUri.PathAndQuery);
}

[Fact]
public async Task DeleteRecordAsync_IsIdempotentWhenRecordAlreadyGone()
{
var handler = new FakeHttpMessageHandler(req =>
{
if (req.Method == HttpMethod.Get && req.RequestUri.PathAndQuery.EndsWith("zones"))
{
return FakeHttpMessageHandler.Json(HttpStatusCode.OK,
"[{\"id\":42,\"name\":\"example.com.\"}]");
}

if (req.Method == HttpMethod.Get && req.RequestUri.PathAndQuery.Contains("records"))
{
return FakeHttpMessageHandler.Json(HttpStatusCode.OK, "[]");
}

throw new InvalidOperationException($"Unexpected request: {req.Method} {req.RequestUri}");
});

var provider = new LuaDnsProvider("user@example.com", "apikey", handler);

var result = await provider.DeleteRecordAsync("_acme-challenge.example.com", "TXT");

Assert.True(result);
}

[Theory]
[InlineData(null, "apikey")]
[InlineData("", "apikey")]
[InlineData("user@example.com", null)]
[InlineData("user@example.com", "")]
public void Constructor_ThrowsOnMissingCredentials(string username, string apiKey)
{
Assert.Throws<ArgumentException>(() => new LuaDnsProvider(username, apiKey, new FakeHttpMessageHandler(_ =>
throw new InvalidOperationException("Should not make HTTP calls"))));
}
}
}
4 changes: 4 additions & 0 deletions Keyfactor.DnsProvider.LuaDns.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<Solution>
<Project Path="Keyfactor.DnsProvider.LuaDns/Keyfactor.DnsProvider.LuaDns.csproj" />
<Project Path="Keyfactor.DnsProvider.LuaDns.Tests/Keyfactor.DnsProvider.LuaDns.Tests.csproj" />
</Solution>
3 changes: 3 additions & 0 deletions Keyfactor.DnsProvider.LuaDns/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Keyfactor.DnsProvider.LuaDns.Tests")]
19 changes: 19 additions & 0 deletions Keyfactor.DnsProvider.LuaDns/Keyfactor.DnsProvider.LuaDns.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>disable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<RootNamespace>Keyfactor.Extensions.DomainValidator.LuaDns</RootNamespace>
<AssemblyName>LuaDnsDomainValidator</AssemblyName>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Keyfactor.AnyGateway.IAnyCAPlugin" Version="3.3.0" />
<PackageReference Include="Keyfactor.Logging" Version="1.3.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<None Update="manifest.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
Loading
Loading