Skip to content

Commit 360edab

Browse files
committed
Add integration tests
1 parent 1cab4fb commit 360edab

7 files changed

Lines changed: 365 additions & 0 deletions

File tree

.github/workflows/publish_nuget_package.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ jobs:
5353

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

5758
- name: Pack
5859
run: dotnet pack src/SignhostAPIClient/SignhostAPIClient.csproj /p:Version=${{ env.LATEST_VERSION }}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Integration Tests
2+
3+
This project contains integration tests that require live API credentials to run.
4+
5+
## Configuration
6+
7+
The tests require Signhost API credentials configured via .NET User Secrets:
8+
9+
```bash
10+
cd src/SignhostAPIClient.IntegrationTests
11+
dotnet user-secrets set "Signhost:AppKey" "your-app-key-here"
12+
dotnet user-secrets set "Signhost:UserToken" "your-user-token-here"
13+
dotnet user-secrets set "Signhost:ApiBaseUrl" "https://api.signhost.com/api"
14+
```
15+
16+
## Running Tests
17+
18+
```bash
19+
dotnet test src/SignhostAPIClient.IntegrationTests/SignhostAPIClient.IntegrationTests.csproj
20+
```
21+
22+
## Important Notes
23+
24+
- These tests are **excluded from CI/CD** pipelines
25+
- These tests are **not packaged** in the NuGet package
26+
- Tests will fail if credentials are not configured
27+
- Tests create real transactions in your Signhost account
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup Label="Build">
3+
<TargetFrameworks>net8.0</TargetFrameworks>
4+
<IsPackable>false</IsPackable>
5+
<IsTestProject>true</IsTestProject>
6+
<CodeAnalysisRuleSet>../signhost.ruleset</CodeAnalysisRuleSet>
7+
<UserSecretsId>signhost-api-client-integration-tests</UserSecretsId>
8+
</PropertyGroup>
9+
10+
<ItemGroup Label="Build">
11+
<AdditionalFiles Include="../stylecop.json" />
12+
</ItemGroup>
13+
14+
<ItemGroup Label="Package References">
15+
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
16+
<PackageReference Include="FluentAssertions" Version="7.2.0" />
17+
<PackageReference Include="xunit" Version="2.9.3" />
18+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
19+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
20+
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.1" />
21+
</ItemGroup>
22+
23+
<ItemGroup Label="Project References">
24+
<ProjectReference Include="../SignhostAPIClient/SignhostAPIClient.csproj" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<None Update="TestFiles/*.pdf">
29+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
30+
</None>
31+
</ItemGroup>
32+
</Project>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System;
2+
using Microsoft.Extensions.Configuration;
3+
4+
namespace Signhost.APIClient.Rest.IntegrationTests;
5+
6+
/// <summary>
7+
/// Configuration for integration tests loaded from user secrets only.
8+
/// No appsettings.json is used to prevent accidental credential commits.
9+
/// </summary>
10+
public class TestConfiguration
11+
{
12+
private static readonly Lazy<TestConfiguration> LazyInstance =
13+
new(() => new TestConfiguration());
14+
15+
private TestConfiguration()
16+
{
17+
var builder = new ConfigurationBuilder()
18+
.AddUserSecrets<TestConfiguration>(optional: false);
19+
20+
IConfiguration configuration = builder.Build();
21+
AppKey = configuration["Signhost:AppKey"];
22+
UserToken = configuration["Signhost:UserToken"];
23+
ApiBaseUrl = configuration["Signhost:ApiBaseUrl"];
24+
}
25+
26+
public static TestConfiguration Instance => LazyInstance.Value;
27+
28+
public string AppKey { get; }
29+
30+
public string UserToken { get; }
31+
32+
public string ApiBaseUrl { get; }
33+
34+
public bool IsConfigured =>
35+
!string.IsNullOrWhiteSpace(AppKey) &&
36+
!string.IsNullOrWhiteSpace(UserToken) &&
37+
!string.IsNullOrWhiteSpace(ApiBaseUrl);
38+
}
Binary file not shown.
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
using System;
2+
using System.IO;
3+
using System.Threading.Tasks;
4+
using FluentAssertions;
5+
using Signhost.APIClient.Rest.DataObjects;
6+
using Xunit;
7+
8+
namespace Signhost.APIClient.Rest.IntegrationTests;
9+
10+
public class TransactionTests
11+
: IDisposable
12+
{
13+
private readonly SignHostApiClient client;
14+
private readonly TestConfiguration config;
15+
16+
public TransactionTests()
17+
{
18+
config = TestConfiguration.Instance;
19+
20+
if (!config.IsConfigured) {
21+
throw new InvalidOperationException(
22+
"Integration tests are not configured");
23+
}
24+
25+
var settings = new SignHostApiClientSettings(config.AppKey, config.UserToken) {
26+
Endpoint = config.ApiBaseUrl
27+
};
28+
29+
client = new SignHostApiClient(settings);
30+
}
31+
32+
[Fact]
33+
public async Task Given_complex_transaction_When_created_and_started_Then_all_properties_are_correctly_persisted()
34+
{
35+
// Arrange
36+
var testReference = $"IntegrationTest-{DateTime.UtcNow:yyyyMMddHHmmss}";
37+
var testPostbackUrl = "https://example.com/postback";
38+
var signerEmail = "john.doe@example.com";
39+
var signerReference = "SIGNER-001";
40+
var signerIntroText = "Please review and sign this document carefully.";
41+
var signerExpires = DateTimeOffset.UtcNow.AddDays(15);
42+
var receiverEmail = "receiver@example.com";
43+
var receiverName = "Jane Receiver";
44+
var receiverReference = "RECEIVER-001";
45+
46+
var transaction = new Transaction {
47+
Seal = false,
48+
Reference = testReference,
49+
PostbackUrl = testPostbackUrl,
50+
DaysToExpire = 30,
51+
SendEmailNotifications = false,
52+
SignRequestMode = 2,
53+
Language = "en-US",
54+
Context = new {
55+
TestContext = "integration-test",
56+
},
57+
Signers = [
58+
new Signer {
59+
Id = "signer1",
60+
Email = signerEmail,
61+
Reference = signerReference,
62+
IntroText = signerIntroText,
63+
Expires = signerExpires,
64+
SendSignRequest = false,
65+
SendSignConfirmation = false,
66+
DaysToRemind = 7,
67+
Language = "en-US",
68+
SignRequestMessage = "Please sign this document.",
69+
SignRequestSubject = "Document for Signature",
70+
ReturnUrl = "https://example.com/return",
71+
AllowDelegation = false,
72+
Context = new {
73+
SignerContext = "test-signer",
74+
},
75+
Verifications = [
76+
new ScribbleVerification {
77+
RequireHandsignature = true,
78+
ScribbleName = "John Doe",
79+
ScribbleNameFixed = true
80+
}
81+
],
82+
Authentications = [
83+
new PhoneNumberVerification {
84+
Number = "+31612345678",
85+
SecureDownload = true,
86+
}
87+
]
88+
}
89+
],
90+
Receivers = [
91+
new Receiver {
92+
Name = receiverName,
93+
Email = receiverEmail,
94+
Language = "en-US",
95+
Message = "The document has been signed.",
96+
Subject = "Signed Document",
97+
Reference = receiverReference,
98+
Context = new {
99+
ReceiverContext = "test-receiver",
100+
}
101+
}
102+
]
103+
};
104+
105+
var pdfPath = Path.Combine("TestFiles", "small-example-pdf-file.pdf");
106+
if (!File.Exists(pdfPath)) {
107+
throw new FileNotFoundException($"Test PDF file not found at: {pdfPath}");
108+
}
109+
110+
// Act - Create transaction
111+
var createdTransaction = await client.CreateTransactionAsync(transaction);
112+
113+
// Assert - Creation properties
114+
createdTransaction.Should().NotBeNull();
115+
createdTransaction.Id.Should().NotBeNullOrEmpty();
116+
createdTransaction.Status.Should().Be(TransactionStatus.WaitingForDocument);
117+
createdTransaction.Seal.Should().BeFalse();
118+
createdTransaction.Reference.Should().Be(testReference);
119+
createdTransaction.PostbackUrl.Should().Be(testPostbackUrl);
120+
createdTransaction.DaysToExpire.Should().Be(30);
121+
createdTransaction.SendEmailNotifications.Should().BeFalse();
122+
createdTransaction.SignRequestMode.Should().Be(2);
123+
createdTransaction.Language.Should().Be("en-US");
124+
createdTransaction.CreatedDateTime.Should().HaveValue();
125+
createdTransaction.CreatedDateTime.Should().BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromMinutes(1));
126+
createdTransaction.CancelledDateTime.Should().BeNull();
127+
createdTransaction.CancellationReason.Should().BeNull();
128+
129+
// Assert - Context
130+
((object)createdTransaction.Context).Should().NotBeNull();
131+
string transactionContextJson = createdTransaction.Context.ToString();
132+
transactionContextJson.Should().Contain("integration-test");
133+
134+
// Assert - Signers
135+
createdTransaction.Signers.Should().HaveCount(1);
136+
var createdSigner = createdTransaction.Signers[0];
137+
createdSigner.Id.Should().Be("signer1");
138+
createdSigner.Email.Should().Be(signerEmail);
139+
createdSigner.Reference.Should().Be(signerReference);
140+
createdSigner.IntroText.Should().Be(signerIntroText);
141+
createdSigner.Expires.Should().HaveValue();
142+
createdSigner.Expires.Should().BeCloseTo(signerExpires, TimeSpan.FromMinutes(1));
143+
createdSigner.SendSignRequest.Should().BeFalse();
144+
createdSigner.SendSignConfirmation.Should().BeFalse();
145+
createdSigner.DaysToRemind.Should().Be(7);
146+
createdSigner.Language.Should().Be("en-US");
147+
createdSigner.SignRequestMessage.Should().Be("Please sign this document.");
148+
createdSigner.SignRequestSubject.Should().Be("Document for Signature");
149+
createdSigner.ReturnUrl.Should().Be("https://example.com/return");
150+
createdSigner.AllowDelegation.Should().BeFalse();
151+
createdSigner.CreatedDateTime.Should().HaveValue();
152+
createdSigner.ModifiedDateTime.Should().HaveValue();
153+
createdSigner.SignedDateTime.Should().BeNull();
154+
createdSigner.RejectDateTime.Should().BeNull();
155+
createdSigner.SignerDelegationDateTime.Should().BeNull();
156+
createdSigner.RejectReason.Should().BeNull();
157+
createdSigner.Activities.Should().NotBeNull();
158+
159+
// Assert - Signer Context
160+
((object)createdSigner.Context).Should().NotBeNull();
161+
string signerContextJson = createdSigner.Context.ToString();
162+
signerContextJson.Should().Contain("test-signer");
163+
164+
// Assert - Signer Verifications
165+
createdSigner.Verifications.Should().HaveCount(1);
166+
var verification = createdSigner.Verifications[0].Should().BeOfType<ScribbleVerification>().Subject;
167+
verification.ScribbleName.Should().Be("John Doe");
168+
verification.RequireHandsignature.Should().BeTrue();
169+
verification.ScribbleNameFixed.Should().BeTrue();
170+
171+
// Assert - Signer Authentications
172+
createdSigner.Authentications.Should().HaveCount(1);
173+
var authentication = createdSigner.Authentications[0].Should().BeOfType<PhoneNumberVerification>().Subject;
174+
authentication.Number.Should().Be("+31612345678");
175+
authentication.SecureDownload.Should().BeTrue();
176+
177+
// Assert - Receivers
178+
createdTransaction.Receivers.Should().HaveCount(1);
179+
var createdReceiver = createdTransaction.Receivers[0];
180+
createdReceiver.Name.Should().Be(receiverName);
181+
createdReceiver.Email.Should().Be(receiverEmail);
182+
createdReceiver.Language.Should().Be("en-US");
183+
createdReceiver.Message.Should().Be("The document has been signed.");
184+
createdReceiver.Subject.Should().Be("Signed Document");
185+
createdReceiver.Reference.Should().Be(receiverReference);
186+
createdReceiver.Id.Should().NotBeNullOrEmpty();
187+
createdReceiver.CreatedDateTime.Should().HaveValue();
188+
createdReceiver.ModifiedDateTime.Should().HaveValue();
189+
createdReceiver.Activities.Should().BeNull(
190+
because: "actual API inconsistency - Receiver Activities are null rather than an empty list");
191+
192+
// Assert - Receiver Context
193+
((object)createdReceiver.Context).Should().NotBeNull();
194+
string receiverContextJson = createdReceiver.Context.ToString();
195+
receiverContextJson.Should().Contain("test-receiver");
196+
197+
// Act - Upload file
198+
await using var fileStream = File.OpenRead(pdfPath);
199+
await client.AddOrReplaceFileToTransactionAsync(
200+
fileStream,
201+
createdTransaction.Id,
202+
"test-document.pdf",
203+
new FileUploadOptions());
204+
205+
// Act - Start transaction
206+
await client.StartTransactionAsync(createdTransaction.Id);
207+
208+
// Act - Retrieve final state
209+
var finalTransaction = await client.GetTransactionAsync(createdTransaction.Id);
210+
211+
// Assert - Final transaction state
212+
finalTransaction.Should().NotBeNull();
213+
finalTransaction.Id.Should().Be(createdTransaction.Id);
214+
finalTransaction.Status.Should().BeOneOf(
215+
TransactionStatus.WaitingForSigner,
216+
TransactionStatus.InProgress);
217+
finalTransaction.Reference.Should().Be(testReference);
218+
finalTransaction.PostbackUrl.Should().Be(testPostbackUrl);
219+
finalTransaction.DaysToExpire.Should().Be(30);
220+
finalTransaction.SendEmailNotifications.Should().BeFalse();
221+
finalTransaction.Language.Should().Be("en-US");
222+
223+
// Assert - Files
224+
finalTransaction.Files.Should().NotBeNull();
225+
finalTransaction.Files.Should().ContainKey("test-document.pdf");
226+
var fileEntry = finalTransaction.Files["test-document.pdf"];
227+
fileEntry.Should().NotBeNull();
228+
fileEntry.DisplayName.Should().Be("test-document.pdf");
229+
fileEntry.Links.Should().NotBeNull().And.NotBeEmpty();
230+
231+
// Assert - Signer in final state
232+
finalTransaction.Signers.Should().HaveCount(1);
233+
var finalSigner = finalTransaction.Signers[0];
234+
finalSigner.Id.Should().Be("signer1");
235+
finalSigner.Email.Should().Be(signerEmail);
236+
finalSigner.Reference.Should().Be(signerReference);
237+
finalSigner.ModifiedDateTime.Should().HaveValue();
238+
finalSigner.ModifiedDateTime.Should().BeOnOrAfter(finalSigner.CreatedDateTime.Value);
239+
finalSigner.Expires.Should().HaveValue();
240+
finalSigner.SignUrl.Should().NotBeNullOrEmpty();
241+
finalSigner.ShowUrl.Should().NotBeNullOrEmpty();
242+
finalSigner.ReceiptUrl.Should().NotBeNullOrEmpty();
243+
finalSigner.DelegateSignUrl.Should().BeNullOrEmpty();
244+
finalSigner.DelegateReason.Should().BeNullOrEmpty();
245+
finalSigner.DelegateSignerEmail.Should().BeNullOrEmpty();
246+
finalSigner.DelegateSignerName.Should().BeNullOrEmpty();
247+
248+
// Assert - Receiver in final state
249+
finalTransaction.Receivers.Should().HaveCount(1);
250+
var finalReceiver = finalTransaction.Receivers[0];
251+
finalReceiver.Email.Should().Be(receiverEmail);
252+
finalReceiver.Name.Should().Be(receiverName);
253+
finalReceiver.Reference.Should().Be(receiverReference);
254+
}
255+
256+
public void Dispose()
257+
{
258+
client?.Dispose();
259+
GC.SuppressFinalize(this);
260+
}
261+
}

src/SignhostAPIClient.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignhostAPIClient", "Signho
77
EndProject
88
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignhostAPIClient.Tests", "SignhostAPIClient.Tests\SignhostAPIClient.Tests.csproj", "{0A2CF5DE-060C-4C92-8F15-7AA26268511D}"
99
EndProject
10+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SignhostAPIClient.IntegrationTests", "SignhostAPIClient.IntegrationTests\SignhostAPIClient.IntegrationTests.csproj", "{B8F3E9A1-3C4D-4E5F-9A2B-1D3E4F5A6B7C}"
11+
EndProject
1012
Global
1113
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1214
Debug|Any CPU = Debug|Any CPU
@@ -21,6 +23,10 @@ Global
2123
{0A2CF5DE-060C-4C92-8F15-7AA26268511D}.Debug|Any CPU.Build.0 = Debug|Any CPU
2224
{0A2CF5DE-060C-4C92-8F15-7AA26268511D}.Release|Any CPU.ActiveCfg = Release|Any CPU
2325
{0A2CF5DE-060C-4C92-8F15-7AA26268511D}.Release|Any CPU.Build.0 = Release|Any CPU
26+
{B8F3E9A1-3C4D-4E5F-9A2B-1D3E4F5A6B7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27+
{B8F3E9A1-3C4D-4E5F-9A2B-1D3E4F5A6B7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
28+
{B8F3E9A1-3C4D-4E5F-9A2B-1D3E4F5A6B7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
29+
{B8F3E9A1-3C4D-4E5F-9A2B-1D3E4F5A6B7C}.Release|Any CPU.Build.0 = Release|Any CPU
2430
EndGlobalSection
2531
GlobalSection(SolutionProperties) = preSolution
2632
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)