diff --git a/src/Auth0.ManagementApi/Clients/CustomDomainsClient.cs b/src/Auth0.ManagementApi/Clients/CustomDomainsClient.cs index 054de74b9..5d6e4bf6c 100644 --- a/src/Auth0.ManagementApi/Clients/CustomDomainsClient.cs +++ b/src/Auth0.ManagementApi/Clients/CustomDomainsClient.cs @@ -4,6 +4,8 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Auth0.ManagementApi.Paging; +using Newtonsoft.Json; namespace Auth0.ManagementApi.Clients; @@ -12,6 +14,8 @@ namespace Auth0.ManagementApi.Clients; /// public class CustomDomainsClient : BaseClient, ICustomDomainsClient { + private readonly JsonConverter[] checkpointConverters = [new CheckpointPagedListConverter("custom_domains")]; + /// /// Initializes a new instance of . /// @@ -56,6 +60,28 @@ public Task> GetAllAsync(CancellationToken cancellationToken return Connection.GetAsync>(BuildUri("custom-domains"), DefaultHeaders, cancellationToken: cancellationToken); } + /// + public Task> GetAllAsync( + CustomDomainsGetAllRequest request, + CheckpointPaginationInfo? checkpointPaginationInfo, + CancellationToken cancellationToken = default) + { + request.ThrowIfNull(); + + var queryStrings = new Dictionary(); + queryStrings.AddIfNotEmpty("q", request.Query!); + queryStrings.AddIfNotEmpty("fields", request.Fields!); + queryStrings.AddIfNotEmpty("include_fields", request.IncludeFields.ToString().ToLower()); + queryStrings.AddIfNotEmpty("sort", request.Sort!); + + if (checkpointPaginationInfo != null) + { + queryStrings["from"] = checkpointPaginationInfo.From?.ToString(); + queryStrings["take"] = checkpointPaginationInfo.Take.ToString(); + } + return Connection.GetAsync>(BuildUri("custom-domains", queryStrings), DefaultHeaders, checkpointConverters , cancellationToken: cancellationToken); + } + /// /// Retrieves a custom domain status by its ID /// diff --git a/src/Auth0.ManagementApi/Clients/ICustomDomainsClient.cs b/src/Auth0.ManagementApi/Clients/ICustomDomainsClient.cs index ae5b2ff1a..91457dfa9 100644 --- a/src/Auth0.ManagementApi/Clients/ICustomDomainsClient.cs +++ b/src/Auth0.ManagementApi/Clients/ICustomDomainsClient.cs @@ -1,3 +1,5 @@ +using Auth0.ManagementApi.Paging; + namespace Auth0.ManagementApi.Clients; using System.Collections.Generic; @@ -30,6 +32,15 @@ public interface ICustomDomainsClient /// /// A containing the details of every custom domain. Task> GetAllAsync(CancellationToken cancellationToken = default); + + /// + /// Retrieve details on custom domains . + /// + /// + /// + /// + /// + Task> GetAllAsync(CustomDomainsGetAllRequest request, CheckpointPaginationInfo? checkpointPaginationInfo, CancellationToken cancellationToken = default); /// /// Retrieves a custom domain status by its ID diff --git a/src/Auth0.ManagementApi/Models/CustomDomain/CustomDomain.cs b/src/Auth0.ManagementApi/Models/CustomDomain/CustomDomain.cs index e0d9ab8de..24ee3cdd4 100644 --- a/src/Auth0.ManagementApi/Models/CustomDomain/CustomDomain.cs +++ b/src/Auth0.ManagementApi/Models/CustomDomain/CustomDomain.cs @@ -1,8 +1,16 @@ -namespace Auth0.ManagementApi.Models; +using Newtonsoft.Json; + +namespace Auth0.ManagementApi.Models; /// /// Represents a Custom Domain /// public class CustomDomain : CustomDomainBase { + /// + /// Indicates whether this is the default custom domain. + /// There can only be one default custom domain per tenant. + /// + [JsonProperty("is_default")] + public bool? IsDefault { get; set; } } \ No newline at end of file diff --git a/src/Auth0.ManagementApi/Models/CustomDomain/CustomDomainsGetAllRequest.cs b/src/Auth0.ManagementApi/Models/CustomDomain/CustomDomainsGetAllRequest.cs new file mode 100644 index 000000000..d63d72d5c --- /dev/null +++ b/src/Auth0.ManagementApi/Models/CustomDomain/CustomDomainsGetAllRequest.cs @@ -0,0 +1,30 @@ +namespace Auth0.ManagementApi.Models; + +/// +/// Represents a request to retrieve all custom domains with optional filtering, field selection, and sorting. +/// +public class CustomDomainsGetAllRequest +{ + /// + /// Query in Lucene query string syntax. + /// + public string? Query { get; set; } + + /// + /// Comma-separated list of fields to include or exclude + /// (based on value provided for include_fields) in the result. + /// Leave empty to retrieve all fields. + /// + public string? Fields { get; set; } + + /// + /// Whether specified fields are to be included (true) or excluded (false). + /// + public bool? IncludeFields { get; set; } = null; + + /// + /// Field to sort by. + /// Only domain:1 (ascending order by domain) is supported at this time. + /// + public string? Sort { get; set; } +} \ No newline at end of file diff --git a/tests/Auth0.ManagementApi.IntegrationTests/CustomDomainsTests.cs b/tests/Auth0.ManagementApi.IntegrationTests/CustomDomainsTests.cs index 94dbeac5b..553dd2cdc 100644 --- a/tests/Auth0.ManagementApi.IntegrationTests/CustomDomainsTests.cs +++ b/tests/Auth0.ManagementApi.IntegrationTests/CustomDomainsTests.cs @@ -1,13 +1,18 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Auth0.Core.Exceptions; using Auth0.IntegrationTests.Shared.CleanUp; +using Auth0.ManagementApi.Clients; using Auth0.ManagementApi.IntegrationTests.Testing; using Auth0.ManagementApi.Models; using Auth0.ManagementApi.Paging; + using FluentAssertions; +using Moq; +using Newtonsoft.Json; using Xunit; namespace Auth0.ManagementApi.IntegrationTests; @@ -27,11 +32,18 @@ public override async Task DisposeAsync() public class CustomDomainsTests : IClassFixture { + private readonly Mock _mockConnection; + private readonly CustomDomainsClient _client; private readonly CustomDomainsTestsFixture fixture; public CustomDomainsTests(CustomDomainsTestsFixture fixture) { this.fixture = fixture; + _mockConnection = new Mock(); + _client = new CustomDomainsClient( + _mockConnection.Object, + new Uri("https://test.auth0.com/api/v2/"), + new Dictionary()); } [Fact] public async Task Test_custom_domains() @@ -79,8 +91,155 @@ public async Task Test_custom_domains() await fixture.ApiClient.CustomDomains.DeleteAsync(id); - var afterRunCustomDomains = await fixture.ApiClient.CustomDomains.GetAllAsync(); + var afterRunCustomDomains = + await fixture.ApiClient.CustomDomains.GetAllAsync( + new CustomDomainsGetAllRequest() + { + Sort = "domain:1" + }, new CheckpointPaginationInfo()); + afterRunCustomDomains.Should().NotContain(x => x.CustomDomainId == id); fixture.UnTrackIdentifier(CleanUpType.CustomDomains, id); } + + /// + /// Sets up the mock to capture the URI passed to GetAsync. Returns a holder + /// whose Value is populated after the call under test completes. + /// + private (Func GetUri, Func GetConverters) SetupCapture( + ICheckpointPagedList response = null) + { + Uri capturedUri = null; + JsonConverter[] capturedConverters = null; + + _mockConnection + .Setup(c => c.GetAsync>( + It.IsAny(), + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Callback, JsonConverter[], CancellationToken>( + (uri, _, converters, _) => + { + capturedUri = uri; + capturedConverters = converters; + }) + .ReturnsAsync(response ?? new CheckpointPagedList()); + + return (() => capturedUri, () => capturedConverters); + } + + [Fact] + public async Task GetAllAsync_Throws_When_Request_Is_Null() + { + await Assert.ThrowsAsync(() => + _client.GetAllAsync(null!, null)); + } + + [Fact] + public async Task GetAllAsync_Returns_Result_From_Connection() + { + var expected = new CheckpointPagedList + { + new CustomDomain { IsDefault = true }, + new CustomDomain { IsDefault = false } + }; + SetupCapture(expected); + + var result = await _client.GetAllAsync(new CustomDomainsGetAllRequest(), null); + + result.Should().HaveCount(2); + } + + [Fact] + public async Task GetAllAsync_Calls_Correct_Endpoint() + { + var (getUri, _) = SetupCapture(); + + await _client.GetAllAsync(new CustomDomainsGetAllRequest(), null); + + getUri().AbsolutePath.Should().EndWith("custom-domains"); + } + + [Fact] + public async Task GetAllAsync_Includes_Query_In_QueryString_When_Provided() + { + var (getUri, _) = SetupCapture(); + + await _client.GetAllAsync(new CustomDomainsGetAllRequest { Query = "domain:example.com" }, null); + + getUri().Query.Should().Contain("q=domain%3Aexample.com"); + } + + [Fact] + public async Task GetAllAsync_Includes_Fields_In_QueryString_When_Provided() + { + var (getUri, _) = SetupCapture(); + + await _client.GetAllAsync(new CustomDomainsGetAllRequest { Fields = "domain,status" }, null); + + getUri().Query.Should().Contain("fields=domain%2Cstatus"); + } + + [Theory] + [InlineData(true, "include_fields=true")] + [InlineData(false, "include_fields=false")] + public async Task GetAllAsync_Includes_IncludeFields_In_QueryString_When_Provided(bool includeFields, string expectedParam) + { + var (getUri, _) = SetupCapture(); + + await _client.GetAllAsync(new CustomDomainsGetAllRequest { IncludeFields = includeFields }, null); + + getUri().Query.Should().Contain(expectedParam); + } + + [Fact] + public async Task GetAllAsync_Includes_Sort_In_QueryString_When_Provided() + { + var (getUri, _) = SetupCapture(); + + await _client.GetAllAsync(new CustomDomainsGetAllRequest { Sort = "domain:1" }, null); + + getUri().Query.Should().Contain("sort=domain%3A1"); + } + + [Fact] + public async Task GetAllAsync_Includes_Take_And_From_When_CheckpointPaginationInfo_Provided() + { + var (getUri, _) = SetupCapture(); + + await _client.GetAllAsync( + new CustomDomainsGetAllRequest(), + new CheckpointPaginationInfo(take: 25, from: "cd_abc123")); + + getUri().Query.Should().Contain("take=25"); + getUri().Query.Should().Contain("from=cd_abc123"); + } + + [Fact] + public async Task GetAllAsync_Omits_From_Param_When_CheckpointPaginationInfo_From_Is_Null() + { + var (getUri, _) = SetupCapture(); + + await _client.GetAllAsync( + new CustomDomainsGetAllRequest(), + new CheckpointPaginationInfo(take: 50)); + + getUri().Query.Should().Contain("take=50"); + getUri().Query.Should().NotContain("from="); + } + + [Fact] + public async Task GetAllAsync_Passes_CheckpointConverters_To_Connection() + { + var (_, getConverters) = SetupCapture(); + + await _client.GetAllAsync(new CustomDomainsGetAllRequest(), null); + + getConverters().Should().NotBeNull(); + getConverters().Should().HaveCount(1); + getConverters()[0].Should().BeAssignableTo(); + getConverters()[0].CanConvert(typeof(ICheckpointPagedList)).Should().BeTrue(); + } + } \ No newline at end of file