Testing

Learn how to test applications that use the .NET Custody SDK with built-in mock clients, test data builders, and testing best practices.

Mock Mode

The SDK includes a built-in mock mode that returns realistic test data without making actual API calls. Perfect for unit tests, integration tests, and development.

Enable Mock Mode in Configuration

using CoxAuto.Vecu.CustodySdk.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCustodyClient(options =>
{
    options.Environment = CustodyEnvironment.Sandbox;
    options.UseMockData = true; // Enable mock mode
});

When mock mode is enabled:

  • ✅ No authentication required
  • ✅ No network calls made
  • ✅ Deterministic, predictable responses
  • ✅ In-memory authorization storage
  • ✅ Fast test execution

MockCustodyServiceClient

Use the built-in MockCustodyServiceClient for complete control over mock behavior:

using CoxAuto.Vecu.CustodySdk.Mock;
using CoxAuto.Vecu.CustodySdk.Models.Requests;

// Basic usage with default success behavior
var mockClient = new MockCustodyServiceClient();

var request = new CreateAuthorizationRequest
{
    Vin = "1TEST5678MOCK1234",
    Origin = "500 Commerce Rd, Richmond, VA 23234",
    Destination = "750 Industrial Pkwy, Dallas, TX 75247",
    PersonIdentityKey = "DL-109-283-745",
    MakeModel = "Toyota Camry",
    AuthorizedBy = "TEST_SYSTEM"
};

var response = await mockClient.CreateAuthorizationAsync(request);

Console.WriteLine($"Authorization created: {response.AuthorizationId}");
Console.WriteLine($"Status: {response.Status}"); // PENDING

Configuring Mock Behavior

Use MockResponseConfiguration to customize mock responses:

using CoxAuto.Vecu.CustodySdk.Mock;

// Configure custom behaviors
var config = new MockResponseConfiguration
{
    DefaultBehavior = MockBehavior.Success,
    ThrowOnError = true,
    SimulatedDelay = TimeSpan.FromMilliseconds(50) // Simulate network latency
};

// Configure specific authorization behaviors
config.AuthorizationBehaviors["auth-404"] = MockBehavior.NotFound;
config.AuthorizationBehaviors["auth-expired"] = MockBehavior.Expired;
config.AuthorizationBehaviors["auth-duplicate"] = MockBehavior.Duplicate;

var mockClient = new MockCustodyServiceClient(config);

// This will throw AuthorizationNotFoundException
try
{
    var auth = await mockClient.GetAuthorizationAsync("auth-404");
}
catch (AuthorizationNotFoundException ex)
{
    Console.WriteLine($"Expected: {ex.Message}");
}

MockBehavior Options

Configure different error scenarios for testing:

SuccessMockBehavior

Return successful response with valid mock data

NotFoundMockBehavior

Throw AuthorizationNotFoundException (404)

ValidationErrorMockBehavior

Throw ValidationException (400, 422)

DuplicateMockBehavior

Throw DuplicateAuthorizationException (409)

NotReleasableMockBehavior

Throw VehicleNotReleasableException (409)

RateLimitMockBehavior

Throw RateLimitException (429)

NetworkErrorMockBehavior

Throw NetworkException (5xx)

ExpiredMockBehavior

Return authorization with Expired status

AlreadyCancelledMockBehavior

Throw ValidationException for already cancelled authorization

MockResponseConfiguration Fluent API

Chain configuration methods for cleaner setup:

var config = new MockResponseConfiguration()
    .WithBehavior("auth-404", MockBehavior.NotFound)
    .WithBehavior("auth-expired", MockBehavior.Expired)
    .WithBehavior("auth-duplicate", MockBehavior.Duplicate)
    .WithDelay(TimeSpan.FromMilliseconds(100));

var mockClient = new MockCustodyServiceClient(config);

TestDataBuilder

Use TestDataBuilder for fluent test data creation with sensible defaults:

using CoxAuto.Vecu.CustodySdk.Testing;
using CoxAuto.Vecu.CustodySdk.Models.Enums;

// Create authorization request with defaults
var request = TestDataBuilder
    .CreateAuthorizationRequest()
    .WithVin("1TEST5678MOCK1234")
    .WithOrigin("500 Commerce Rd, Richmond, VA 23234")
    .WithDestination("750 Industrial Pkwy, Dallas, TX 75247")
    .WithValidUntil(DateTimeOffset.UtcNow.AddHours(48))
    .Build();

// Create authorization response
var response = TestDataBuilder
    .AuthorizationResponse()
    .WithAuthorizationId("AUTH-TEST-123")
    .WithStatus(AuthorizationStatus.RELEASED)
    .WithExpiresAt(DateTimeOffset.UtcNow.AddHours(24))
    .Build();

// Create authorization details
var details = TestDataBuilder
    .AuthorizationDetails()
    .WithAuthorizationId("AUTH-TEST-123")
    .WithStatus(AuthorizationStatus.PENDING)
    .WithRole(AuthorizationRole.DRIVER)
    .Build();

All builders provide sensible defaults, so you only need to customize what matters for your test:

// Minimal test data - uses all defaults
var request = TestDataBuilder
    .CreateAuthorizationRequest()
    .Build();

Console.WriteLine(request.Vin); // Auto-generated valid VIN
Console.WriteLine(request.Origin); // "500 Commerce Rd, Richmond, VA 23234"
Console.WriteLine(request.Destination); // "750 Industrial Pkwy, Dallas, TX 75247"

xUnit Testing Examples

Basic Unit Test

using Xunit;
using CoxAuto.Vecu.CustodySdk.Mock;
using CoxAuto.Vecu.CustodySdk.Testing;
using CoxAuto.Vecu.CustodySdk.Models.Enums;

public class AuthorizationServiceTests
{
    [Fact]
    public async Task CreateAuthorization_ValidRequest_ReturnsAuthorization()
    {
        // Arrange
        var mockClient = new MockCustodyServiceClient();
        var service = new AuthorizationService(mockClient);

        var request = TestDataBuilder
            .CreateAuthorizationRequest()
            .WithVin("1TEST5678MOCK1234")
            .Build();

        // Act
        var result = await service.CreateAuthorizationAsync(request);

        // Assert
        Assert.NotNull(result);
        Assert.NotNull(result.AuthorizationId);
        Assert.Equal(AuthorizationStatus.PENDING, result.Status);
        Assert.Equal("1TEST5678MOCK1234", result.Vin);
    }

    [Fact]
    public async Task CreateAuthorization_DuplicateVin_ThrowsDuplicateException()
    {
        // Arrange
        var config = new MockResponseConfiguration()
            .WithBehavior("auth-duplicate", MockBehavior.Duplicate);

        config.DefaultBehavior = MockBehavior.Duplicate;

        var mockClient = new MockCustodyServiceClient(config);
        var service = new AuthorizationService(mockClient);

        var request = TestDataBuilder
            .CreateAuthorizationRequest()
            .Build();

        // Act & Assert
        var ex = await Assert.ThrowsAsync<DuplicateAuthorizationException>(
            () => service.CreateAuthorizationAsync(request)
        );

        Assert.NotNull(ex.ExistingAuthorizationId);
    }

    [Fact]
    public async Task GetAuthorization_NotFound_ThrowsNotFoundException()
    {
        // Arrange
        var config = new MockResponseConfiguration()
            .WithBehavior("auth-404", MockBehavior.NotFound);

        var mockClient = new MockCustodyServiceClient(config);

        // Act & Assert
        await Assert.ThrowsAsync<AuthorizationNotFoundException>(
            () => mockClient.GetAuthorizationAsync("auth-404")
        );
    }

    [Fact]
    public async Task WaitForAuthorization_CompletesSuccessfully_ReturnsCompletedStatus()
    {
        // Arrange
        var mockClient = new MockCustodyServiceClient();

        var request = TestDataBuilder
            .CreateAuthorizationRequest()
            .Build();

        var createResponse = await mockClient.CreateAuthorizationAsync(request);

        // Act
        var result = await mockClient.WaitForAuthorizationAsync(
            createResponse.AuthorizationId,
            new WaitOptions
            {
                TimeoutSeconds = 10,
                PollingIntervalSeconds = 1
            }
        );

        // Assert
        Assert.Equal(AuthorizationStatus.RELEASED, result.Status);
        Assert.NotNull(result.ExpiresAt);
    }
}

Testing with Dependency Injection

using Microsoft.Extensions.DependencyInjection;
using Xunit;

public class IntegrationTests : IDisposable
{
    private readonly ServiceProvider _serviceProvider;
    private readonly ICustodyServiceClient _client;

    public IntegrationTests()
    {
        var services = new ServiceCollection();

        // Register mock client
        services.AddCustodyClient(options =>
        {
            options.Environment = CustodyEnvironment.Sandbox;
            options.UseMockData = true;
        });

        services.AddSingleton<AuthorizationService>();

        _serviceProvider = services.BuildServiceProvider();
        _client = _serviceProvider.GetRequiredService<ICustodyServiceClient>();
    }

    [Fact]
    public async Task ServiceIntegration_CreateAndRetrieve_Success()
    {
        // Arrange
        var service = _serviceProvider.GetRequiredService<AuthorizationService>();

        // Act
        var authId = await service.CreateAndGetIdAsync(
            "1TEST5678MOCK1234",
            "500 Commerce Rd, Richmond, VA 23234",
            "750 Industrial Pkwy, Dallas, TX 75247"
        );

        var details = await service.GetDetailsAsync(authId);

        // Assert
        Assert.NotNull(details);
        Assert.Equal(authId, details.AuthorizationId);
    }

    public void Dispose()
    {
        _serviceProvider.Dispose();
    }
}

Testing Error Scenarios

public class ErrorHandlingTests
{
    [Fact]
    public async Task CreateAuthorization_ValidationError_HandlesGracefully()
    {
        // Arrange
        var config = new MockResponseConfiguration
        {
            DefaultBehavior = MockBehavior.ValidationError
        };

        var mockClient = new MockCustodyServiceClient(config);

        var request = TestDataBuilder
            .CreateAuthorizationRequest()
            .Build();

        // Act & Assert
        var ex = await Assert.ThrowsAsync<ValidationException>(
            () => mockClient.CreateAuthorizationAsync(request)
        );

        Assert.NotNull(ex.ValidationErrors);
        Assert.Contains(ex.ValidationErrors, kvp => kvp.Value.Length > 0);
    }

    [Fact]
    public async Task GetAuthorization_RateLimited_ThrowsRateLimitException()
    {
        // Arrange
        var config = new MockResponseConfiguration()
            .WithBehavior("auth-ratelimit", MockBehavior.RateLimit);

        var mockClient = new MockCustodyServiceClient(config);

        // Act & Assert
        var ex = await Assert.ThrowsAsync<RateLimitException>(
            () => mockClient.GetAuthorizationAsync("auth-ratelimit")
        );

        Assert.NotNull(ex.RetryAfter);
        Assert.True(ex.RetryAfter.Value.TotalSeconds > 0);
    }

    [Theory]
    [InlineData("auth-404", MockBehavior.NotFound)]
    [InlineData("auth-expired", MockBehavior.Expired)]
    [InlineData("auth-network-error", MockBehavior.NetworkError)]
    public async Task GetAuthorization_VariousErrors_ThrowsExpectedException(
        string authId,
        MockBehavior behavior)
    {
        // Arrange
        var config = new MockResponseConfiguration()
            .WithBehavior(authId, behavior);

        var mockClient = new MockCustodyServiceClient(config);

        // Act & Assert
        var exceptionThrown = false;
        try
        {
            await mockClient.GetAuthorizationAsync(authId);
        }
        catch (CustodyException)
        {
            exceptionThrown = true;
        }

        Assert.True(exceptionThrown);
    }

}

Mocking with Moq

For more advanced mocking scenarios, use Moq with the ICustodyServiceClient interface:

using Moq;
using Xunit;
using CoxAuto.Vecu.CustodySdk.Client;
using CoxAuto.Vecu.CustodySdk.Testing;

public class ServiceWithMoqTests
{
    [Fact]
    public async Task ProcessAuthorization_Success_CallsGetAndCancel()
    {
        // Arrange
        var mockClient = new Mock<ICustodyServiceClient>();

        var authDetails = TestDataBuilder
            .AuthorizationDetails()
            .WithAuthorizationId("AUTH-TEST-123")
            .WithStatus(AuthorizationStatus.PENDING)
            .Build();

        var cancelResponse = new CancelAuthorizationResponse
        {
            AuthorizationId = "AUTH-TEST-123",
            Vin = "1TEST5678MOCK1234",
            Status = AuthorizationStatus.CANCELLED,
            CancelledAt = DateTimeOffset.UtcNow,
            CancelledBy = "TEST_SYSTEM",
            CancellationReason = "Cancelled for testing - driver reassigned"
        };

        mockClient
            .Setup(c => c.GetAuthorizationAsync(
                It.IsAny<string>(),
                It.IsAny<CancellationToken>()
            ))
            .ReturnsAsync(authDetails);

        mockClient
            .Setup(c => c.CancelAuthorizationAsync(
                It.IsAny<string>(),
                It.IsAny<CancelAuthorizationRequest>(),
                It.IsAny<CancellationToken>()
            ))
            .ReturnsAsync(cancelResponse);

        var service = new AuthorizationService(mockClient.Object);

        // Act
        await service.ProcessAndCancelAsync("AUTH-TEST-123");

        // Assert
        mockClient.Verify(
            c => c.GetAuthorizationAsync("AUTH-TEST-123", It.IsAny<CancellationToken>()),
            Times.Once
        );

        mockClient.Verify(
            c => c.CancelAuthorizationAsync(
                "AUTH-TEST-123",
                It.IsAny<CancelAuthorizationRequest>(),
                It.IsAny<CancellationToken>()
            ),
            Times.Once
        );
    }

    [Fact]
    public async Task GetAuthorization_ThrowsException_HandlesGracefully()
    {
        // Arrange
        var mockClient = new Mock<ICustodyServiceClient>();

        mockClient
            .Setup(c => c.GetAuthorizationAsync(
                It.IsAny<string>(),
                It.IsAny<CancellationToken>()
            ))
            .ThrowsAsync(new AuthorizationNotFoundException("AUTH-404"));

        var service = new AuthorizationService(mockClient.Object);

        // Act & Assert
        await Assert.ThrowsAsync<AuthorizationNotFoundException>(
            () => service.GetDetailsAsync("AUTH-404")
        );
    }
}

Integration Testing with Sandbox

Test against the real Sandbox environment for end-to-end validation:

using Xunit;
using Microsoft.Extensions.Configuration;

public class SandboxIntegrationTests : IDisposable
{
    private readonly ICustodyServiceClient _client;
    private readonly IConfiguration _configuration;

    public SandboxIntegrationTests()
    {
        // Load configuration from appsettings.Test.json
        _configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.Test.json")
            .AddEnvironmentVariables()
            .Build();

        var services = new ServiceCollection();

        services.AddCustodyClient(options =>
        {
            options.Environment = CustodyEnvironment.Sandbox;
            options.TokenProvider = async (ct) =>
            {
                // Use your auth system to get a bearer token
                return _configuration["CustodyClient:TestToken"]
                    ?? throw new InvalidOperationException("Test token not configured");
            };
            options.TimeoutSeconds = 60;
        });

        var serviceProvider = services.BuildServiceProvider();
        _client = serviceProvider.GetRequiredService<ICustodyServiceClient>();
    }

    [Fact(Skip = "Integration test - run manually")]
    public async Task CreateAuthorization_RealSandbox_Success()
    {
        // Arrange
        var request = TestDataBuilder
            .CreateAuthorizationRequest()
            .WithVin("1TEST5678SANDBOX1")
            .WithOrigin("100 Sandbox Dr, Phoenix, AZ 85001")
            .WithDestination("250 Test Blvd, Denver, CO 80202")
            .Build();

        // Act
        var response = await _client.CreateAuthorizationAsync(request);

        // Assert
        Assert.NotNull(response);
        Assert.NotEmpty(response.AuthorizationId);
        Assert.Equal(AuthorizationStatus.PENDING, response.Status);

        // Cleanup
        await _client.CancelAuthorizationAsync(
            response.AuthorizationId,
            new CancelAuthorizationRequest
            {
                CancellationReason = "Test cleanup - sandbox integration test complete"
            }
        );
    }

    public void Dispose()
    {
        // Cleanup resources
    }
}

appsettings.Test.json

{
  "CustodyClient": {
    "TestToken": "your-sandbox-test-token-here"
  }
}

Mark integration tests with [Fact(Skip = "...")] or a custom trait to prevent running them in CI/CD pipelines unless explicitly enabled.

Best Practices

1. Use Mock Mode for Unit Tests

Mock mode is faster and doesn't require network access:

// ✅ GOOD: Fast, deterministic, no network
builder.Services.AddCustodyClient(options =>
{
    options.UseMockData = true;
});

// ❌ AVOID: Slow, requires token provider, network dependency
builder.Services.AddCustodyClient(options =>
{
    options.Environment = CustodyEnvironment.Sandbox;
    options.TokenProvider = async (ct) => await GetRealTokenAsync(ct);
});

2. Test Error Scenarios

Always test how your code handles exceptions:

[Fact]
public async Task Service_HandlesNotFound_ReturnsNull()
{
    var config = new MockResponseConfiguration()
        .WithBehavior("not-found", MockBehavior.NotFound);

    var mockClient = new MockCustodyServiceClient(config);
    var service = new AuthorizationService(mockClient);

    var result = await service.TryGetAuthorizationAsync("not-found");

    Assert.Null(result);
}

3. Use TestDataBuilder for Consistency

Create test data with sensible defaults:

// ✅ GOOD: Readable, maintainable, consistent
var request = TestDataBuilder
    .CreateAuthorizationRequest()
    .WithVin("1TEST5678MOCK1234")
    .Build();

// ❌ AVOID: Verbose, error-prone, inconsistent
var request = new CreateAuthorizationRequest
{
    Vin = "1TEST5678MOCK1234",
    Origin = "500 Commerce Rd, Richmond, VA 23234",
    Destination = "750 Industrial Pkwy, Dallas, TX 75247",
    PersonIdentityKey = "DL-109-283-745",
    MakeModel = "Toyota Camry",
    AuthorizedBy = "SYSTEM"
};

4. Test Async Patterns Properly

Use async/await correctly in tests:

// ✅ GOOD: Proper async test
[Fact]
public async Task CreateAuthorization_Success()
{
    var result = await mockClient.CreateAuthorizationAsync(request);
    Assert.NotNull(result);
}

// ❌ WRONG: Missing async/await
[Fact]
public void CreateAuthorization_Success()
{
    var result = mockClient.CreateAuthorizationAsync(request).Result; // Blocks!
    Assert.NotNull(result);
}

5. Dispose Resources Properly

Use IDisposable pattern for cleanup:

public class MyTests : IDisposable
{
    private readonly MockCustodyServiceClient _mockClient;

    public MyTests()
    {
        _mockClient = new MockCustodyServiceClient();
    }

    [Fact]
    public async Task MyTest()
    {
        // Test using _mockClient
    }

    public void Dispose()
    {
        _mockClient.Dispose();
    }
}

Next Steps