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
1 change: 1 addition & 0 deletions .github/workflows/publish_nuget_package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ jobs:

- name: Test with Coverage
run: dotnet test --no-build src/SignhostAPIClient.Tests/SignhostAPIClient.Tests.csproj --collect:"XPlat Code Coverage" -c Release
# Note: Integration tests are excluded from CI/CD as they require live credentials

- name: Pack
run: dotnet pack src/SignhostAPIClient/SignhostAPIClient.csproj /p:Version=${{ env.LATEST_VERSION }}
Expand Down
27 changes: 27 additions & 0 deletions src/SignhostAPIClient.IntegrationTests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Integration Tests

This project contains integration tests that require live API credentials to run.

## Configuration

The tests require Signhost API credentials configured via .NET User Secrets:

```bash
cd src/SignhostAPIClient.IntegrationTests
dotnet user-secrets set "Signhost:AppKey" "your-app-key-here"
dotnet user-secrets set "Signhost:UserToken" "your-user-token-here"
dotnet user-secrets set "Signhost:ApiBaseUrl" "https://api.signhost.com/api"
```

## Running Tests

```bash
dotnet test src/SignhostAPIClient.IntegrationTests/SignhostAPIClient.IntegrationTests.csproj
```

## Important Notes

- These tests are **excluded from CI/CD** pipelines
- These tests are **not packaged** in the NuGet package
- Tests will fail if credentials are not configured
- Tests create real transactions in your Signhost account
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="Build">
<TargetFrameworks>net8.0</TargetFrameworks>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<CodeAnalysisRuleSet>../signhost.ruleset</CodeAnalysisRuleSet>
<UserSecretsId>signhost-api-client-integration-tests</UserSecretsId>
</PropertyGroup>

<ItemGroup Label="Build">
<AdditionalFiles Include="../stylecop.json" />
</ItemGroup>

<ItemGroup Label="Package References">
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="FluentAssertions" Version="7.2.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.1" />
</ItemGroup>

<ItemGroup Label="Project References">
<ProjectReference Include="../SignhostAPIClient/SignhostAPIClient.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="TestFiles/*.pdf">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
38 changes: 38 additions & 0 deletions src/SignhostAPIClient.IntegrationTests/TestConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using Microsoft.Extensions.Configuration;

namespace Signhost.APIClient.Rest.IntegrationTests;

/// <summary>
/// Configuration for integration tests loaded from user secrets only.
/// No appsettings.json is used to prevent accidental credential commits.
/// </summary>
public class TestConfiguration
{
private static readonly Lazy<TestConfiguration> LazyInstance =
new(() => new TestConfiguration());

private TestConfiguration()
{
var builder = new ConfigurationBuilder()
.AddUserSecrets<TestConfiguration>(optional: false);

IConfiguration configuration = builder.Build();
AppKey = configuration["Signhost:AppKey"];
UserToken = configuration["Signhost:UserToken"];
ApiBaseUrl = configuration["Signhost:ApiBaseUrl"];
}

public static TestConfiguration Instance => LazyInstance.Value;

public string AppKey { get; }

public string UserToken { get; }

public string ApiBaseUrl { get; }

public bool IsConfigured =>
!string.IsNullOrWhiteSpace(AppKey) &&
!string.IsNullOrWhiteSpace(UserToken) &&
!string.IsNullOrWhiteSpace(ApiBaseUrl);
}
Binary file not shown.
261 changes: 261 additions & 0 deletions src/SignhostAPIClient.IntegrationTests/TransactionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
using System;
using System.IO;
using System.Threading.Tasks;
using FluentAssertions;
using Signhost.APIClient.Rest.DataObjects;
using Xunit;

namespace Signhost.APIClient.Rest.IntegrationTests;

public class TransactionTests
: IDisposable
{
private readonly SignHostApiClient client;
private readonly TestConfiguration config;

public TransactionTests()
{
config = TestConfiguration.Instance;

if (!config.IsConfigured) {
throw new InvalidOperationException(
"Integration tests are not configured");
}

var settings = new SignHostApiClientSettings(config.AppKey, config.UserToken) {
Endpoint = config.ApiBaseUrl
};

client = new SignHostApiClient(settings);
}

[Fact]
public async Task Given_complex_transaction_When_created_and_started_Then_all_properties_are_correctly_persisted()
{
// Arrange
var testReference = $"IntegrationTest-{DateTime.UtcNow:yyyyMMddHHmmss}";
var testPostbackUrl = "https://example.com/postback";
var signerEmail = "john.doe@example.com";
var signerReference = "SIGNER-001";
var signerIntroText = "Please review and sign this document carefully.";
var signerExpires = DateTimeOffset.UtcNow.AddDays(15);
var receiverEmail = "receiver@example.com";
var receiverName = "Jane Receiver";
var receiverReference = "RECEIVER-001";

var transaction = new Transaction {
Seal = false,
Reference = testReference,
PostbackUrl = testPostbackUrl,
DaysToExpire = 30,
SendEmailNotifications = false,
SignRequestMode = 2,
Language = "en-US",
Context = new {
TestContext = "integration-test",
},
Signers = [
new Signer {
Id = "signer1",
Email = signerEmail,
Reference = signerReference,
IntroText = signerIntroText,
Expires = signerExpires,
SendSignRequest = false,
SendSignConfirmation = false,
DaysToRemind = 7,
Language = "en-US",
SignRequestMessage = "Please sign this document.",
SignRequestSubject = "Document for Signature",
ReturnUrl = "https://example.com/return",
AllowDelegation = false,
Context = new {
SignerContext = "test-signer",
},
Verifications = [
new ScribbleVerification {
RequireHandsignature = true,
ScribbleName = "John Doe",
ScribbleNameFixed = true
}
],
Authentications = [
new PhoneNumberVerification {
Number = "+31612345678",
SecureDownload = true,
}
]
}
],
Receivers = [
new Receiver {
Name = receiverName,
Email = receiverEmail,
Language = "en-US",
Message = "The document has been signed.",
Subject = "Signed Document",
Reference = receiverReference,
Context = new {
ReceiverContext = "test-receiver",
}
}
]
};

var pdfPath = Path.Combine("TestFiles", "small-example-pdf-file.pdf");
if (!File.Exists(pdfPath)) {
throw new FileNotFoundException($"Test PDF file not found at: {pdfPath}");
}

// Act - Create transaction
var createdTransaction = await client.CreateTransactionAsync(transaction);

// Assert - Creation properties
createdTransaction.Should().NotBeNull();
createdTransaction.Id.Should().NotBeNullOrEmpty();
createdTransaction.Status.Should().Be(TransactionStatus.WaitingForDocument);
createdTransaction.Seal.Should().BeFalse();
createdTransaction.Reference.Should().Be(testReference);
createdTransaction.PostbackUrl.Should().Be(testPostbackUrl);
createdTransaction.DaysToExpire.Should().Be(30);
createdTransaction.SendEmailNotifications.Should().BeFalse();
createdTransaction.SignRequestMode.Should().Be(2);
createdTransaction.Language.Should().Be("en-US");
createdTransaction.CreatedDateTime.Should().HaveValue();
createdTransaction.CreatedDateTime.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromMinutes(1));
createdTransaction.CancelledDateTime.Should().BeNull();
createdTransaction.CancellationReason.Should().BeNull();

// Assert - Context
((object)createdTransaction.Context).Should().NotBeNull();
string transactionContextJson = createdTransaction.Context.ToString();
transactionContextJson.Should().Contain("integration-test");

// Assert - Signers
createdTransaction.Signers.Should().HaveCount(1);
var createdSigner = createdTransaction.Signers[0];
createdSigner.Id.Should().Be("signer1");
createdSigner.Email.Should().Be(signerEmail);
createdSigner.Reference.Should().Be(signerReference);
createdSigner.IntroText.Should().Be(signerIntroText);
createdSigner.Expires.Should().HaveValue();
createdSigner.Expires.Should().BeCloseTo(signerExpires, TimeSpan.FromMinutes(1));
createdSigner.SendSignRequest.Should().BeFalse();
createdSigner.SendSignConfirmation.Should().BeFalse();
createdSigner.DaysToRemind.Should().Be(7);
createdSigner.Language.Should().Be("en-US");
createdSigner.SignRequestMessage.Should().Be("Please sign this document.");
createdSigner.SignRequestSubject.Should().Be("Document for Signature");
createdSigner.ReturnUrl.Should().Be("https://example.com/return");
createdSigner.AllowDelegation.Should().BeFalse();
createdSigner.CreatedDateTime.Should().HaveValue();
createdSigner.ModifiedDateTime.Should().HaveValue();
createdSigner.SignedDateTime.Should().BeNull();
createdSigner.RejectDateTime.Should().BeNull();
createdSigner.SignerDelegationDateTime.Should().BeNull();
createdSigner.RejectReason.Should().BeNull();
createdSigner.Activities.Should().NotBeNull();

// Assert - Signer Context
((object)createdSigner.Context).Should().NotBeNull();
string signerContextJson = createdSigner.Context.ToString();
signerContextJson.Should().Contain("test-signer");

// Assert - Signer Verifications
createdSigner.Verifications.Should().HaveCount(1);
var verification = createdSigner.Verifications[0].Should().BeOfType<ScribbleVerification>().Subject;
verification.ScribbleName.Should().Be("John Doe");
verification.RequireHandsignature.Should().BeTrue();
verification.ScribbleNameFixed.Should().BeTrue();

// Assert - Signer Authentications
createdSigner.Authentications.Should().HaveCount(1);
var authentication = createdSigner.Authentications[0].Should().BeOfType<PhoneNumberVerification>().Subject;
authentication.Number.Should().Be("+31612345678");
authentication.SecureDownload.Should().BeTrue();

// Assert - Receivers
createdTransaction.Receivers.Should().HaveCount(1);
var createdReceiver = createdTransaction.Receivers[0];
createdReceiver.Name.Should().Be(receiverName);
createdReceiver.Email.Should().Be(receiverEmail);
createdReceiver.Language.Should().Be("en-US");
createdReceiver.Message.Should().Be("The document has been signed.");
createdReceiver.Subject.Should().Be("Signed Document");
createdReceiver.Reference.Should().Be(receiverReference);
createdReceiver.Id.Should().NotBeNullOrEmpty();
createdReceiver.CreatedDateTime.Should().HaveValue();
createdReceiver.ModifiedDateTime.Should().HaveValue();
createdReceiver.Activities.Should().BeNull(
because: "actual API inconsistency - Receiver Activities are null rather than an empty list");

// Assert - Receiver Context
((object)createdReceiver.Context).Should().NotBeNull();
string receiverContextJson = createdReceiver.Context.ToString();
receiverContextJson.Should().Contain("test-receiver");

// Act - Upload file
await using var fileStream = File.OpenRead(pdfPath);
await client.AddOrReplaceFileToTransactionAsync(
fileStream,
createdTransaction.Id,
"test-document.pdf",
new FileUploadOptions());

// Act - Start transaction
await client.StartTransactionAsync(createdTransaction.Id);

// Act - Retrieve final state
var finalTransaction = await client.GetTransactionAsync(createdTransaction.Id);

// Assert - Final transaction state
finalTransaction.Should().NotBeNull();
finalTransaction.Id.Should().Be(createdTransaction.Id);
finalTransaction.Status.Should().BeOneOf(
TransactionStatus.WaitingForSigner,
TransactionStatus.InProgress);
finalTransaction.Reference.Should().Be(testReference);
finalTransaction.PostbackUrl.Should().Be(testPostbackUrl);
finalTransaction.DaysToExpire.Should().Be(30);
finalTransaction.SendEmailNotifications.Should().BeFalse();
finalTransaction.Language.Should().Be("en-US");

// Assert - Files
finalTransaction.Files.Should().NotBeNull();
finalTransaction.Files.Should().ContainKey("test-document.pdf");
var fileEntry = finalTransaction.Files["test-document.pdf"];
fileEntry.Should().NotBeNull();
fileEntry.DisplayName.Should().Be("test-document.pdf");
fileEntry.Links.Should().NotBeNull().And.NotBeEmpty();

// Assert - Signer in final state
finalTransaction.Signers.Should().HaveCount(1);
var finalSigner = finalTransaction.Signers[0];
finalSigner.Id.Should().Be("signer1");
finalSigner.Email.Should().Be(signerEmail);
finalSigner.Reference.Should().Be(signerReference);
finalSigner.ModifiedDateTime.Should().HaveValue();
finalSigner.ModifiedDateTime.Should().BeOnOrAfter(finalSigner.CreatedDateTime.Value);
finalSigner.Expires.Should().HaveValue();
finalSigner.SignUrl.Should().NotBeNullOrEmpty();
finalSigner.ShowUrl.Should().NotBeNullOrEmpty();
finalSigner.ReceiptUrl.Should().NotBeNullOrEmpty();
finalSigner.DelegateSignUrl.Should().BeNullOrEmpty();
finalSigner.DelegateReason.Should().BeNullOrEmpty();
finalSigner.DelegateSignerEmail.Should().BeNullOrEmpty();
finalSigner.DelegateSignerName.Should().BeNullOrEmpty();

// Assert - Receiver in final state
finalTransaction.Receivers.Should().HaveCount(1);
var finalReceiver = finalTransaction.Receivers[0];
finalReceiver.Email.Should().Be(receiverEmail);
finalReceiver.Name.Should().Be(receiverName);
finalReceiver.Reference.Should().Be(receiverReference);
}

public void Dispose()
{
client?.Dispose();
GC.SuppressFinalize(this);
}
}
6 changes: 6 additions & 0 deletions src/SignhostAPIClient.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignhostAPIClient", "Signho
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignhostAPIClient.Tests", "SignhostAPIClient.Tests\SignhostAPIClient.Tests.csproj", "{0A2CF5DE-060C-4C92-8F15-7AA26268511D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignhostAPIClient.IntegrationTests", "SignhostAPIClient.IntegrationTests\SignhostAPIClient.IntegrationTests.csproj", "{B8F3E9A1-3C4D-4E5F-9A2B-1D3E4F5A6B7C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -21,6 +23,10 @@ Global
{0A2CF5DE-060C-4C92-8F15-7AA26268511D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A2CF5DE-060C-4C92-8F15-7AA26268511D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A2CF5DE-060C-4C92-8F15-7AA26268511D}.Release|Any CPU.Build.0 = Release|Any CPU
{B8F3E9A1-3C4D-4E5F-9A2B-1D3E4F5A6B7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8F3E9A1-3C4D-4E5F-9A2B-1D3E4F5A6B7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8F3E9A1-3C4D-4E5F-9A2B-1D3E4F5A6B7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8F3E9A1-3C4D-4E5F-9A2B-1D3E4F5A6B7C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Loading
Loading