using LiteCharms.Features.MidrandBooks.Payments; using LiteCharms.Features.MidrandBooks.Payments.Models; using LiteCharms.Features.Tests.Common; namespace LiteCharms.Features.MidrandBooks.Tests; public sealed class PayfastServiceFeatureTests(Fixture fixture) : IClassFixture { private readonly PayfastService payfastService = fixture.Services.GetRequiredService(); [IntegrationFact] public async Task WriteLedgerEntryAsync_ShouldReturn_ResultWithGatewayLedgerId() { var request = new CreateGatewayLedgerEntry { OrderId = 1, PaymentId = 1, MerchantPaymentId = "M_REF_TEST_99", PayfastPaymentId = "PF_SYS_ID_10023", CustomerEmail = "buyer@litecharms.co.za", AmountGross = 350.00m, AmountFee = 12.50m, AmountNet = 337.50m, PaymentStatus = "COMPLETE" }; var result = await payfastService.WriteLedgerEntryAsync(request, fixture.CancellationToken); Assert.True(result.IsSuccess); Assert.True(result.Value > 0); } [IntegrationFact] public async Task ValidateReferrerIpAsync_WithValidPayfastHostIp_ShouldReturnTrue() { var addresses = await Dns.GetHostAddressesAsync("sandbox.payfast.co.za", fixture.CancellationToken); string liveTargetIp = addresses.First().ToString(); var result = await payfastService.ValidateReferrerIpAsync(liveTargetIp, true, fixture.CancellationToken); Assert.True(result.IsSuccess); Assert.True(result.Value); } [IntegrationFact] public async Task ValidateReferrerIpAsync_WithUntrustedIp_ShouldReturnFalse() { string rogueIp = "8.8.8.8"; var result = await payfastService.ValidateReferrerIpAsync(rogueIp, true, fixture.CancellationToken); Assert.True(result.IsSuccess); Assert.False(result.Value); } [IntegrationFact] public void ValidatePaymentAmount_WhenWithinAllowableDelta_ShouldReturnTrue() { decimal systemExpectedTotal = 199.99m; string gatewayClearedGross = "200.00"; // Variance is exactly R0.01 var result = payfastService.ValidatePaymentAmount(systemExpectedTotal, gatewayClearedGross); Assert.True(result.IsSuccess); Assert.True(result.Value); } [IntegrationFact] public void ValidatePaymentAmount_WhenVarianceBreachesDeltaBounds_ShouldReturnFalse() { decimal systemExpectedTotal = 199.99m; string gatewayClearedGross = "150.00"; var result = payfastService.ValidatePaymentAmount(systemExpectedTotal, gatewayClearedGross); Assert.True(result.IsSuccess); Assert.False(result.Value); } [IntegrationFact] public async Task ValidateServerConfirmationAsync_WithUnrecognizedPayload_ShouldReturnFalseFromCentralGateway() { // Arrange - Execute against actual Payfast servers using raw mock parameters. // The server handshake will return 200 OK with string payload 'INVALID' string arbitraryParameters = "merchant_id=10000000&payment_status=COMPLETE"; var result = await payfastService.ValidateServerConfirmationAsync(arbitraryParameters, isSandbox: true, fixture.CancellationToken); Assert.True(result.IsSuccess); Assert.False(result.Value); // Handshake data rejected as fraudulent/unrecognized } [IntegrationFact] public void GenerateSignature_WithStandardTelemetryData_ShouldSucceedAndHashString() { var telemetryPayload = new Dictionary { { "merchant_id", "10049307" }, { "merchant_key", "ju6navn0jcbf0" }, { "amount_gross", "250.00" }, { "item_name", "Midrand School Textbook Variant A" } }; string passphrase = "oauth_test_signature_pass"; var result = PayfastService.GenerateSignature(telemetryPayload, passphrase); Assert.True(result.IsSuccess); Assert.False(string.IsNullOrWhiteSpace(result.Value)); Assert.Equal(32, result.Value.Length); // MD5 outputs hex representations totaling 32 characters } }