diff --git a/.editorconfig b/.editorconfig index 122cc175..758aaeaf 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,10 +3,50 @@ root = true [*] charset = utf-8 indent_style = tab +insert_final_newline = true -[*.{cs,js}] +[*.{cs,cshtml,js}] trim_trailing_whitespace = true insert_final_newline = true -[*.csproj] -indent_style = space \ No newline at end of file +dotnet_sort_system_directives_first = true : warning +dotnet_style_predefined_type_for_locals_parameters_members = true : warning +dotnet_style_object_initializer = true : warning +dotnet_style_collection_initializer = true : warning +dotnet_style_explicit_tuple_names = true : error +dotnet_style_coalesce_expression = true : warning +dotnet_style_null_propagation = true : warning + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = false : suggestion +csharp_style_var_when_type_is_apparent = true : suggestion +csharp_style_var_elsewhere = false : none + +# Prefer method-like constructs to have a block body +csharp_style_expression_bodied_methods = false : none +csharp_style_expression_bodied_constructors = false : none +csharp_style_expression_bodied_operators = false : none + +# Prefer property-like constructs to have an expression-body +csharp_style_expression_bodied_properties = true : none +csharp_style_expression_bodied_indexers = true : none +csharp_style_expression_bodied_accessors = true : none + +# Suggest more modern language features when available +csharp_style_pattern_matching_over_is_with_cast_check = true : suggestion +csharp_style_pattern_matching_over_as_with_null_check = true : suggestion +csharp_style_inlined_variable_declaration = true : suggestion +csharp_style_throw_expression = true : suggestion +csharp_style_conditional_delegate_call = true : suggestion + +# Newline settings +csharp_new_line_before_open_brace = types, methods, properties, indexers, events, event_accessors, anonymous_types, object_collections, array_initializers, local_functions +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +[*.{props,targets,config,nuspec,csproj}] +indent_style = tab +indent_size = 2 diff --git a/.gitattributes b/.gitattributes index 4eb59c0a..48d3a289 100644 --- a/.gitattributes +++ b/.gitattributes @@ -16,9 +16,6 @@ *.edmx text *.resx text -*.ncrunchproject text -*.ncrunchsolution text - # Custom for Visual Studio *.sln text eol=crlf merge=union *.csproj text diff --git a/.github/workflows/publish_nuget_package.yml b/.github/workflows/publish_nuget_package.yml index dc459d1c..6ab85e6a 100644 --- a/.github/workflows/publish_nuget_package.yml +++ b/.github/workflows/publish_nuget_package.yml @@ -15,7 +15,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v5 with: - dotnet-version: '8.x' + dotnet-version: '10.x' - name: Check for Tag on Current Commit run: | @@ -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 }} diff --git a/.gitignore b/.gitignore index 7494d73c..0e70063f 100644 --- a/.gitignore +++ b/.gitignore @@ -104,11 +104,6 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover -# NCrunch -_NCrunch_* -.*crunch*.local.xml -*.v3.ncrunchsolution.user - # MightyMoose *.mm.* AutoTest.Net/ diff --git a/README.md b/README.md index 6da273b1..b655563e 100644 --- a/README.md +++ b/README.md @@ -15,15 +15,15 @@ For full API documentation, please visit [evidos.github.io](https://evidos.githu ### Example code The following code is an example of how to create and start a sign transaction with two documents. ```c# -var settings = new SignHostApiClientSettings( +var settings = new SignhostApiClientSettings( "AppName appkey", - "apikey or usertoken")); + "apikey or usertoken"); -var client = new SignHostApiClient(settings); +var client = new SignhostApiClient(settings); -var transaction = await client.CreateTransactionAsync(new Transaction { - Signers = new List { - new Signer { +var transaction = await client.CreateTransactionAsync(new CreateTransactionRequest { + Signers = new List { + new CreateSignerRequest { Email = "john.doe@example.com", SignRequestMessage = "Could you please sign this document?", SendSignRequest = true, diff --git a/src/SignhostAPIClient.IntegrationTests/README.md b/src/SignhostAPIClient.IntegrationTests/README.md new file mode 100644 index 00000000..2caaae02 --- /dev/null +++ b/src/SignhostAPIClient.IntegrationTests/README.md @@ -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 diff --git a/src/SignhostAPIClient.IntegrationTests/SignhostAPIClient.IntegrationTests.csproj b/src/SignhostAPIClient.IntegrationTests/SignhostAPIClient.IntegrationTests.csproj new file mode 100644 index 00000000..1d17d43f --- /dev/null +++ b/src/SignhostAPIClient.IntegrationTests/SignhostAPIClient.IntegrationTests.csproj @@ -0,0 +1,31 @@ + + + net10.0 + false + true + ../signhost.ruleset + signhost-api-client-integration-tests + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + diff --git a/src/SignhostAPIClient.IntegrationTests/TestConfiguration.cs b/src/SignhostAPIClient.IntegrationTests/TestConfiguration.cs new file mode 100644 index 00000000..53a7e106 --- /dev/null +++ b/src/SignhostAPIClient.IntegrationTests/TestConfiguration.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.Extensions.Configuration; + +namespace Signhost.APIClient.Rest.IntegrationTests; + +/// +/// Configuration for integration tests loaded from user secrets only. +/// No appsettings.json is used to prevent accidental credential commits. +/// +public class TestConfiguration +{ + private static readonly Lazy LazyInstance = + new(() => new TestConfiguration()); + + private TestConfiguration() + { + var builder = new ConfigurationBuilder() + .AddUserSecrets(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); +} diff --git a/src/SignhostAPIClient.IntegrationTests/TestFiles/small-example-pdf-file.pdf b/src/SignhostAPIClient.IntegrationTests/TestFiles/small-example-pdf-file.pdf new file mode 100644 index 00000000..fd46350c Binary files /dev/null and b/src/SignhostAPIClient.IntegrationTests/TestFiles/small-example-pdf-file.pdf differ diff --git a/src/SignhostAPIClient.IntegrationTests/TransactionTests.cs b/src/SignhostAPIClient.IntegrationTests/TransactionTests.cs new file mode 100644 index 00000000..1d39224e --- /dev/null +++ b/src/SignhostAPIClient.IntegrationTests/TransactionTests.cs @@ -0,0 +1,450 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +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."); + } + + SignhostApiClientSettings settings = new(config.AppKey, config.UserToken) { + Endpoint = config.ApiBaseUrl, + }; + + client = new(settings); + } + + [Fact] + public async Task Given_complex_transaction_When_created_and_started_Then_all_properties_are_correctly_persisted() + { + // Arrange + string testReference = $"IntegrationTest-{DateTime.UtcNow:yyyyMMddHHmmss}"; + string testPostbackUrl = "https://example.com/postback"; + string signerEmail = "john.doe@example.com"; + string signerReference = "SIGNER-001"; + string signerIntroText = "Please review and sign this document carefully."; + var signerExpires = DateTimeOffset.UtcNow.AddDays(15); + string receiverEmail = "receiver@example.com"; + string receiverName = "Jane Receiver"; + string receiverReference = "RECEIVER-001"; + + CreateTransactionRequest transaction = new() { + Seal = false, + Reference = testReference, + PostbackUrl = testPostbackUrl, + DaysToExpire = 30, + SendEmailNotifications = false, + SignRequestMode = 2, + Language = "en-US", + Context = new { + TestContext = "integration-test", + }, + Signers = [ + new() { + 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() { + Name = receiverName, + Email = receiverEmail, + Language = "en-US", + Message = "The document has been signed.", + Subject = "Signed Document", + Reference = receiverReference, + Context = new { + ReceiverContext = "test-receiver", + } + } + ] + }; + + string 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() + .BeCloseTo(DateTimeOffset.UtcNow, TimeSpan.FromMinutes(1)); + createdTransaction.CanceledDateTime.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().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().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); + } + + [Fact] + public async Task Given_complex_file_metadata_When_added_to_transaction_Then_all_properties_are_accepted() + { + // Arrange - Create a simple transaction + string testReference = $"MetadataTest-{DateTime.UtcNow:yyyyMMddHHmmss}"; + CreateTransactionRequest transaction = new() { + Reference = testReference, + SendEmailNotifications = false, + Signers = [ + new() { + Id = "signer1", + Email = "test@example.com", + SendSignRequest = false, + }, + new() { + Id = "signer2", + Email = "test2@example.com", + SendSignRequest = false, + } + ] + }; + + var createdTransaction = await client.CreateTransactionAsync(transaction); + createdTransaction.Should().NotBeNull(); + + // Arrange - Create a very complex FileMeta + FileMeta fileMeta = new() { + DisplayOrder = 1, + DisplayName = "Complex Test Document", + Description = "This is a test document with complex metadata", + SetParaph = true, + Signers = new Dictionary { + ["signer1"] = new() { + FormSets = ["FormSet1", "FormSet2"], + }, + ["signer2"] = new() { + FormSets = ["FormSet2", "FormSet3"], + }, + }, + FormSets = new Dictionary> { + ["FormSet1"] = new Dictionary { + // Test all FileFieldType enum values + ["SealField"] = new Field { + Type = FileFieldType.Seal, + Value = null, + Location = new Location { + Search = "seal_placeholder", + Occurence = 1, + PageNumber = 1, + }, + }, + ["SignatureField"] = new Field { + Type = FileFieldType.Signature, + Value = null, + Location = new Location { + Top = 100, + Left = 50, + Width = 200, + Height = 60, + PageNumber = 1, + }, + }, + ["CheckField"] = new Field { + Type = FileFieldType.Check, + Value = "I agree to the terms", + Location = new Location { + Search = "checkbox_location", + Occurence = 1, + }, + }, + }, + ["FormSet2"] = new Dictionary { + // Test different value types: string, number, boolean, null + ["RadioFieldString"] = new Field { + Type = FileFieldType.Radio, + Value = "Option A", + Location = new Location { + Top = 200, + Left = 100, + Right = 300, + Bottom = 220, + PageNumber = 2, + }, + }, + ["SingleLineFieldString"] = new Field { + Type = FileFieldType.SingleLine, + Value = "John Doe", + Location = new Location { + Search = "name_field", + Width = 150, + Height = 20, + } + }, + ["NumberFieldInteger"] = new Field { + Type = FileFieldType.Number, + Value = 42, + Location = new Location { + Top = 300, + Left = 50, + PageNumber = 2, + }, + }, + ["NumberFieldDecimal"] = new Field { + Type = FileFieldType.Number, + Value = 123.45, + Location = new Location { + Top = 320, + Left = 50, + Width = 100, + Height = 20, + PageNumber = 2, + }, + }, + ["DateField"] = new Field { + Type = FileFieldType.Date, + Value = "2025-11-28", + Location = new Location { + Search = "date_placeholder", + Occurence = 2, + PageNumber = 3, + }, + }, + }, + ["FormSet3"] = new Dictionary { + // Test boolean values and all Location properties + ["CheckFieldTrue"] = new Field { + Type = FileFieldType.Check, + Value = true, + Location = new Location { + Top = 400, + Right = 200, + Bottom = 420, + Left = 50, + PageNumber = 3, + }, + }, + ["CheckFieldFalse"] = new Field { + Type = FileFieldType.Check, + Value = false, + Location = new Location { + Top = 450, + Right = 200, + Bottom = 470, + Left = 50, + Width = 150, + Height = 20, + PageNumber = 3, + }, + }, + ["SingleLineFieldNull"] = new Field { + Type = FileFieldType.SingleLine, + Value = null, + Location = new Location { + Search = "optional_field", + Occurence = 1, + Top = 500, + Left = 50, + Width = 200, + Height = 25, + PageNumber = 4, + }, + }, + ["RadioFieldNumber"] = new Field { + Type = FileFieldType.Radio, + Value = 1, + Location = new Location { + PageNumber = 4, + Top = 550, + Left = 50, + }, + }, + }, + }, + }; + + // Act + Func act = () => client.AddOrReplaceFileMetaToTransactionAsync( + fileMeta, + createdTransaction.Id, + "test-document.pdf"); + + // Assert + await act.Should().NotThrowAsync(); + } + + public void Dispose() + { + client?.Dispose(); + GC.SuppressFinalize(this); + } +} diff --git a/src/SignhostAPIClient.Tests/APIResponses.Designer.cs b/src/SignhostAPIClient.Tests/APIResponses.Designer.cs deleted file mode 100644 index 9a092696..00000000 --- a/src/SignhostAPIClient.Tests/APIResponses.Designer.cs +++ /dev/null @@ -1,186 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Signhost.APIClient.Rest.Tests { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class APIResponses { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal APIResponses() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Signhost.APIClient.Rest.Tests.APIResponses", typeof(APIResponses).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to { - /// "Id": "c487be92-0255-40c7-bd7d-20805a65e7d9", - /// "Status": 20, - /// "File": { - /// "Id": "d4bba0df-f9e5-44c8-89db-f2bb46632d7b", - /// "Name": "contract.pdf" - /// }, - /// "Seal": true, - /// "Signers": [ - /// { - /// "Id": "a2932c07-ca93-4011-96f5-a77d2cd1ec32", - /// "Expires": null, - /// "Email": "user@example.com", - /// "Mobile": "+31612345678", - /// "Iban": null, - /// "BSN": null, - /// "RequireScribbleName": false, - /// "RequireScribble": true, - /// "RequireEmailVerification": true, - /// "R [rest of string was truncated]";. - /// - internal static string AddTransaction { - get { - return ResourceManager.GetString("AddTransaction", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "Id": "496bec4d-4ac7-428f-894a-b3e9bade725d", - /// "Status": 20, - /// "File": { - /// "Id": "3149bf06-d01e-4f0d-9aa1-77e100e19772", - /// "Name": "contract.pdf" - /// }, - /// "Seal": true, - /// "Signers": [ - /// { - /// "Id": "4813e178-68a4-4105-b007-5ce9a3630867", - /// "Expires": null, - /// "Email": "user@example.com", - /// "Mobile": "+31612345678", - /// "Iban": null, - /// "BSN": null, - /// "RequireScribbleName": false, - /// "RequireScribble": true, - /// "RequireEmailVerification": true, - /// "R [rest of string was truncated]";. - /// - internal static string DeleteTransaction { - get { - return ResourceManager.GetString("DeleteTransaction", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "Id": "c487be92-0255-40c7-bd7d-20805a65e7d9", - /// "Status": 20, - /// "File": { - /// "Id": "d4bba0df-f9e5-44c8-89db-f2bb46632d7b", - /// "Name": "contract.pdf" - /// }, - /// "Seal": true, - /// "Signers": [ - /// { - /// "Id": "a2932c07-ca93-4011-96f5-a77d2cd1ec32", - /// "Expires": null, - /// "Email": "user@example.com", - /// "Mobile": "+31612345678", - /// "Iban": null, - /// "BSN": null, - /// "RequireScribbleName": false, - /// "RequireScribble": true, - /// "RequireEmailVerification": true, - /// "R [rest of string was truncated]";. - /// - internal static string GetTransaction { - get { - return ResourceManager.GetString("GetTransaction", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "Id": "c487be92-0255-40c7-bd7d-20805a65e7d9", - /// "Status": 20, - /// "Seal": true, - /// "Signers": [ - /// { - /// "Id": "a2932c07-ca93-4011-96f5-a77d2cd1ec32", - /// "Expires": null, - /// "Email": "user@example.com", - /// "Mobile": "+31612345678", - /// "Iban": null, - /// "BSN": null, - /// "SendSignRequest": true, - /// "SendSignConfirmation": null, - /// "SignRequestMessage": "Hello, could you please sign this document? Best regards, John Doe", - /// "DaysToRemind": 15, - /// "Language": "en-US", - /// "Reference": "Client #123", - /// "ReturnUrl": "h [rest of string was truncated]";. - /// - internal static string GetTransactionCustomVerificationType { - get { - return ResourceManager.GetString("GetTransactionCustomVerificationType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "Id": "50262c3f-9744-45bf-a4c6-8a3whatever", - /// "Status": 5 - ///}. - /// - internal static string MinimalTransactionResponse { - get { - return ResourceManager.GetString("MinimalTransactionResponse", resourceCulture); - } - } - } -} diff --git a/src/SignhostAPIClient.Tests/APIResponses.resx b/src/SignhostAPIClient.Tests/APIResponses.resx deleted file mode 100644 index 197489d0..00000000 --- a/src/SignhostAPIClient.Tests/APIResponses.resx +++ /dev/null @@ -1,443 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - { - "Id": "c487be92-0255-40c7-bd7d-20805a65e7d9", - "Status": 20, - "File": { - "Id": "d4bba0df-f9e5-44c8-89db-f2bb46632d7b", - "Name": "contract.pdf" - }, - "Seal": true, - "Signers": [ - { - "Id": "a2932c07-ca93-4011-96f5-a77d2cd1ec32", - "Expires": null, - "Email": "user@example.com", - "Mobile": "+31612345678", - "Iban": null, - "BSN": null, - "RequireScribbleName": false, - "RequireScribble": true, - "RequireEmailVerification": true, - "RequireSmsVerification": true, - "RequireIdealVerification": false, - "RequireDigidVerification": false, - "RequireKennisnetVerification": false, - "RequireSurfnetVerification": false, - "SendSignRequest": true, - "SendSignConfirmation": null, - "SignRequestMessage": "Hello, could you please sign this document? Best regards, John Doe", - "DaysToRemind": 15, - "Language": "en-US", - "ScribbleName": "John Doe", - "ScribbleNameFixed": false, - "Reference": "Client #123", - "ReturnUrl": "http://signhost.com", - "Activities": [ - { - "Id": "0b47eb5c-e800-4fe3-9795-09d380dff1f9", - "Code": 103, - "Activity": "Opened", - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00" - }, - { - "Id": "761f8678-116c-4e86-a47a-c8312681d285", - "Code": 203, - "Activity": "Signed", - "CreatedDateTime": "2016-03-17T21:13:55.1349315+01:00" - } - ], - "RejectReason": null, - "SignUrl": "http://ui.signhost.com/sign/93dc596f-ab81-4d31-87aa-50352c4c237e", - "SignedDateTime": null, - "RejectDateTime": null, - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "ModifiedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "Context": null - } - ], - "Receivers": [ - { - "Id": "2fe3dddf-4b50-49d1-a3d2-45b7d175fb97", - "Name": "John Doe", - "Email": "user@example.com", - "Language": "en-US", - "Message": "Hello, please find enclosed the digital signed document. Best regards, John Doe", - "Reference": null, - "Activities": null, - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "ModifiedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "Context": null - } - ], - "Reference": "Contract #123", - "PostbackUrl": "http://example.com/postback.php", - "SignRequestMode": 2, - "DaysToExpire": 30, - "SendEmailNotifications": true, - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "ModifiedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "CanceledDateTime": null, - "Context": null -} - - - { - "Id": "496bec4d-4ac7-428f-894a-b3e9bade725d", - "Status": 20, - "File": { - "Id": "3149bf06-d01e-4f0d-9aa1-77e100e19772", - "Name": "contract.pdf" - }, - "Seal": true, - "Signers": [ - { - "Id": "4813e178-68a4-4105-b007-5ce9a3630867", - "Expires": null, - "Email": "user@example.com", - "Mobile": "+31612345678", - "Iban": null, - "BSN": null, - "RequireScribbleName": false, - "RequireScribble": true, - "RequireEmailVerification": true, - "RequireSmsVerification": true, - "RequireIdealVerification": false, - "RequireDigidVerification": false, - "RequireKennisnetVerification": false, - "RequireSurfnetVerification": false, - "SendSignRequest": true, - "SendSignConfirmation": null, - "SignRequestMessage": "Hello, could you please sign this document? Best regards, John Doe", - "DaysToRemind": 15, - "Language": "en-US", - "ScribbleName": "John Doe", - "ScribbleNameFixed": false, - "Reference": "Client #123", - "ReturnUrl": "http://signhost.com", - "Activities": [ - { - "Id": "866183ae-0a3c-4441-a589-6fe3e1a0f1a1", - "Code": 103, - "Activity": "Opened", - "CreatedDateTime": "2016-03-31T21:11:42.0267461+02:00" - }, - { - "Id": "9b7c4de9-6b8d-4d22-9b88-b7d2d0f084b9", - "Code": 203, - "Activity": "Signed", - "CreatedDateTime": "2016-03-31T21:16:42.0267461+02:00" - } - ], - "RejectReason": null, - "SignUrl": "http://ui.signhost.com/sign/2eeaa5b9-9d4d-4418-b79f-4a33810e7147", - "SignedDateTime": null, - "RejectDateTime": null, - "CreatedDateTime": "2016-03-31T21:11:42.0267461+02:00", - "ModifiedDateTime": "2016-03-31T21:11:42.0267461+02:00", - "Context": null - } - ], - "Receivers": [ - { - "Id": "af07aaec-b612-4f7c-bb1b-c32603c9c6a2", - "Name": "John Doe", - "Email": "user@example.com", - "Language": "en-US", - "Message": "Hello, please find enclosed the digital signed document. Best regards, John Doe", - "Reference": null, - "Activities": null, - "CreatedDateTime": "2016-03-31T21:11:42.0267461+02:00", - "ModifiedDateTime": "2016-03-31T21:11:42.0267461+02:00", - "Context": null - } - ], - "Reference": "Contract #123", - "PostbackUrl": "http://example.com/postback.php", - "SignRequestMode": 2, - "DaysToExpire": 30, - "SendEmailNotifications": true, - "CreatedDateTime": "2016-03-31T21:11:42.0267461+02:00", - "ModifiedDateTime": "2016-03-31T21:11:42.0267461+02:00", - "CanceledDateTime": null, - "Context": null -} - - - { - "Id": "c487be92-0255-40c7-bd7d-20805a65e7d9", - "Status": 20, - "File": { - "Id": "d4bba0df-f9e5-44c8-89db-f2bb46632d7b", - "Name": "contract.pdf" - }, - "Seal": true, - "Signers": [ - { - "Id": "a2932c07-ca93-4011-96f5-a77d2cd1ec32", - "Expires": null, - "Email": "user@example.com", - "Mobile": "+31612345678", - "Iban": null, - "BSN": null, - "RequireScribbleName": false, - "RequireScribble": true, - "RequireEmailVerification": true, - "RequireSmsVerification": true, - "RequireIdealVerification": false, - "RequireDigidVerification": false, - "RequireKennisnetVerification": false, - "RequireSurfnetVerification": false, - "SendSignRequest": true, - "SendSignConfirmation": null, - "SignRequestMessage": "Hello, could you please sign this document? Best regards, John Doe", - "DaysToRemind": 15, - "Language": "en-US", - "ScribbleName": "John Doe", - "ScribbleNameFixed": false, - "Reference": "Client #123", - "ReturnUrl": "http://signhost.com", - "Activities": [ - { - "Id": "0b47eb5c-e800-4fe3-9795-09d380dff1f9", - "Code": 103, - "Activity": "Opened", - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00" - }, - { - "Id": "761f8678-116c-4e86-a47a-c8312681d285", - "Code": 203, - "Activity": "Signed", - "CreatedDateTime": "2016-03-17T21:13:55.1349315+01:00" - } - ], - "RejectReason": null, - "SignUrl": "http://ui.signhost.com/sign/93dc596f-ab81-4d31-87aa-50352c4c237e", - "SignedDateTime": null, - "RejectDateTime": null, - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "ModifiedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "Context": null - } - ], - "Receivers": [ - { - "Id": "2fe3dddf-4b50-49d1-a3d2-45b7d175fb97", - "Name": "John Doe", - "Email": "user@example.com", - "Language": "en-US", - "Message": "Hello, please find enclosed the digital signed document. Best regards, John Doe", - "Reference": null, - "Activities": null, - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "ModifiedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "Context": null - } - ], - "Reference": "Contract #123", - "PostbackUrl": "http://example.com/postback.php", - "SignRequestMode": 2, - "DaysToExpire": 30, - "SendEmailNotifications": true, - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "ModifiedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "CanceledDateTime": null, - "Context": null -} - - - { - "Id": "c487be92-0255-40c7-bd7d-20805a65e7d9", - "Status": 20, - "Seal": true, - "Signers": [ - { - "Id": "a2932c07-ca93-4011-96f5-a77d2cd1ec32", - "Expires": null, - "Email": "user@example.com", - "Mobile": "+31612345678", - "Iban": null, - "BSN": null, - "SendSignRequest": true, - "SendSignConfirmation": null, - "SignRequestMessage": "Hello, could you please sign this document? Best regards, John Doe", - "DaysToRemind": 15, - "Language": "en-US", - "Reference": "Client #123", - "ReturnUrl": "http://signhost.com", - "Verifications": [ - { - "Type": "CustomVerificationType" - }, - { - "Type": "IPAddress", - "IPAddress": "127.0.0.33" - }, - { - "Type": "PhoneNumber", - "Number": "123" - } - ], - "Activities": [ - { - "Id": "0b47eb5c-e800-4fe3-9795-09d380dff1f9", - "Code": 103, - "Activity": "Opened", - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00" - }, - { - "Id": "761f8678-116c-4e86-a47a-c8312681d285", - "Code": 203, - "Activity": "Signed", - "CreatedDateTime": "2016-03-17T21:13:55.1349315+01:00" - } - ], - "SignUrl": "http://ui.signhost.com/sign/93dc596f-ab81-4d31-87aa-50352c4c237e", - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "ModifiedDateTime": "2016-03-17T21:08:55.1349315+01:00" - } - ], - "Receivers": [ - { - "Id": "2fe3dddf-4b50-49d1-a3d2-45b7d175fb97", - "Name": "John Doe", - "Email": "user@example.com", - "Language": "en-US", - "Message": "Hello, please find enclosed the digital signed document. Best regards, John Doe", - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "ModifiedDateTime": "2016-03-17T21:08:55.1349315+01:00", - } - ], - "Reference": "Contract #123", - "SignRequestMode": 2, - "DaysToExpire": 30, - "SendEmailNotifications": true, - "CreatedDateTime": "2016-03-17T21:08:55.1349315+01:00", - "ModifiedDateTime": "2016-03-17T21:08:55.1349315+01:00" -} - - - { - "Id": "50262c3f-9744-45bf-a4c6-8a3whatever", - "Status": 5 -} - - \ No newline at end of file diff --git a/src/SignhostAPIClient.Tests/FieldValueTests.cs b/src/SignhostAPIClient.Tests/FieldValueTests.cs new file mode 100644 index 00000000..f7040381 --- /dev/null +++ b/src/SignhostAPIClient.Tests/FieldValueTests.cs @@ -0,0 +1,274 @@ +using System.Text.Json; +using FluentAssertions; +using Signhost.APIClient.Rest.DataObjects; +using Xunit; + +namespace Signhost.APIClient.Rest.Tests; + +public class FieldValueTests +{ + [Fact] + public void Given_a_field_with_string_value_When_serialized_Then_value_is_json_string() + { + // Arrange + Field field = new() { + Type = FileFieldType.SingleLine, + Value = "John Smith", + Location = new() { PageNumber = 1 }, + }; + + // Act + string json = JsonSerializer.Serialize(field); + + // Assert + json.Should().Contain("\"Value\":\"John Smith\""); + } + + [Fact] + public void Given_a_field_with_numeric_integer_value_When_serialized_Then_value_is_json_number() + { + // Arrange + Field field = new() { + Type = FileFieldType.Number, + Value = 42, + Location = new() { PageNumber = 1 }, + }; + + // Act + string json = JsonSerializer.Serialize(field); + + // Assert + json.Should().Contain("\"Value\":42"); + json.Should().NotContain("\"Value\":\"42\""); + } + + [Fact] + public void Given_a_field_with_numeric_double_value_When_serialized_Then_value_is_json_number() + { + // Arrange + Field field = new() { + Type = FileFieldType.Number, + Value = 3.14, + Location = new() { PageNumber = 1 }, + }; + + // Act + string json = JsonSerializer.Serialize(field); + + // Assert + json.Should().Contain("\"Value\":3.14"); + json.Should().NotContain("\"Value\":\"3.14\""); + } + + [Fact] + public void Given_a_field_with_boolean_true_value_When_serialized_Then_value_is_json_boolean() + { + // Arrange + Field field = new() { + Type = FileFieldType.Check, + Value = true, + Location = new() { PageNumber = 1 }, + }; + + // Act + string json = JsonSerializer.Serialize(field); + + // Assert + json.Should().Contain("\"Value\":true"); + json.Should().NotContain("\"Value\":\"true\""); + } + + [Fact] + public void Given_a_field_with_boolean_false_value_When_serialized_Then_value_is_json_boolean() + { + // Arrange + Field field = new() { + Type = FileFieldType.Check, + Value = false, + Location = new() { PageNumber = 1 }, + }; + + // Act + string json = JsonSerializer.Serialize(field); + + // Assert + json.Should().Contain("\"Value\":false"); + json.Should().NotContain("\"Value\":\"false\""); + } + + [Fact] + public void Given_a_field_with_null_value_When_serialized_Then_value_is_json_null() + { + // Arrange + Field field = new() { + Type = FileFieldType.Signature, + Value = null, + Location = new() { PageNumber = 1 }, + }; + + // Act + string json = JsonSerializer.Serialize(field); + + // Assert + json.Should().Contain("\"Value\":null"); + } + + [Fact] + public void Given_a_field_with_invalid_object_value_When_serialized_Then_throws_json_exception() + { + // Arrange + Field field = new() { + Type = FileFieldType.SingleLine, + Value = new { Name = "Test" }, + Location = new() { PageNumber = 1 }, + }; + + // Act + var act = () => JsonSerializer.Serialize(field); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Given_json_with_string_value_When_deserialized_Then_field_value_is_string() + { + // Arrange + string json = @"{ + ""Type"": ""SingleLine"", + ""Value"": ""Test Name"", + ""Location"": { ""PageNumber"": 1 } + }"; + + // Act + var field = JsonSerializer.Deserialize(json); + + // Assert + field.Should().NotBeNull(); + field!.Value.Should().BeOfType().Which.Should().Be("Test Name"); + } + + [Fact] + public void Given_json_with_number_integer_value_When_deserialized_Then_field_value_is_numeric() + { + // Arrange + string json = @"{ + ""Type"": ""Number"", + ""Value"": 123, + ""Location"": { ""PageNumber"": 1 } + }"; + + // Act + var field = JsonSerializer.Deserialize(json); + + // Assert + field.Should().NotBeNull(); + field!.Value.Should().BeOneOf(123L, 123.0); + } + + [Fact] + public void Given_json_with_number_decimal_value_When_deserialized_Then_field_value_is_double() + { + // Arrange + string json = @"{ + ""Type"": ""Number"", + ""Value"": 45.67, + ""Location"": { ""PageNumber"": 1 } + }"; + + // Act + var field = JsonSerializer.Deserialize(json); + + // Assert + field.Should().NotBeNull(); + field!.Value.Should().BeOfType().Which.Should().Be(45.67); + } + + [Fact] + public void Given_json_with_boolean_true_value_When_deserialized_Then_field_value_is_boolean_true() + { + // Arrange + string json = @"{ + ""Type"": ""Check"", + ""Value"": true, + ""Location"": { ""PageNumber"": 1 } + }"; + + // Act + var field = JsonSerializer.Deserialize(json); + + // Assert + field.Should().NotBeNull(); + field!.Value.Should().BeOfType().Which.Should().BeTrue(); + } + + [Fact] + public void Given_json_with_boolean_false_value_When_deserialized_Then_field_value_is_boolean_false() + { + // Arrange + string json = @"{ + ""Type"": ""Check"", + ""Value"": false, + ""Location"": { ""PageNumber"": 1 } + }"; + + // Act + var field = JsonSerializer.Deserialize(json); + + // Assert + field.Should().NotBeNull(); + field!.Value.Should().BeOfType().Which.Should().BeFalse(); + } + + [Fact] + public void Given_json_with_null_value_When_deserialized_Then_field_value_is_null() + { + // Arrange + string json = @"{ + ""Type"": ""Signature"", + ""Value"": null, + ""Location"": { ""PageNumber"": 1 } + }"; + + // Act + var field = JsonSerializer.Deserialize(json); + + // Assert + field.Should().NotBeNull(); + field!.Value.Should().BeNull(); + } + + [Fact] + public void Given_json_with_object_value_When_deserialized_Then_throws_json_exception() + { + // Arrange + string json = @"{ + ""Type"": ""SingleLine"", + ""Value"": { ""nested"": ""object"" }, + ""Location"": { ""PageNumber"": 1 } + }"; + + // Act + var act = () => JsonSerializer.Deserialize(json); + + // Assert + act.Should().Throw(); + } + + [Fact] + public void Given_json_with_array_value_When_deserialized_Then_throws_json_exception() + { + // Arrange + string json = @"{ + ""Type"": ""SingleLine"", + ""Value"": [1, 2, 3], + ""Location"": { ""PageNumber"": 1 } + }"; + + // Act + var act = () => JsonSerializer.Deserialize(json); + + // Assert + act.Should().Throw(); + } +} diff --git a/src/SignhostAPIClient.Tests/FileFieldTypeTests.cs b/src/SignhostAPIClient.Tests/FileFieldTypeTests.cs new file mode 100644 index 00000000..76934659 --- /dev/null +++ b/src/SignhostAPIClient.Tests/FileFieldTypeTests.cs @@ -0,0 +1,55 @@ +using System.Text.Json; +using FluentAssertions; +using Signhost.APIClient.Rest.DataObjects; +using Xunit; + +namespace Signhost.APIClient.Rest.Tests; + +public class FileFieldTypeTests +{ + [Theory] + [InlineData(FileFieldType.Seal, "Seal")] + [InlineData(FileFieldType.Signature, "Signature")] + [InlineData(FileFieldType.Check, "Check")] + [InlineData(FileFieldType.Radio, "Radio")] + [InlineData(FileFieldType.SingleLine, "SingleLine")] + [InlineData(FileFieldType.Number, "Number")] + [InlineData(FileFieldType.Date, "Date")] + public void Given_a_field_with_specific_type_When_serialized_to_json_Then_type_is_string_not_numeric( + FileFieldType fieldType, + string expectedString + ) + { + // Arrange + Field field = new() { + Type = fieldType, + Value = "test", + Location = new() { PageNumber = 1 }, + }; + + // Act + string json = JsonSerializer.Serialize(field); + + // Assert + json.Should().Contain($"\"Type\":\"{expectedString}\""); + json.Should().NotContain($"\"Type\":{(int)fieldType}"); + } + + [Fact] + public void Given_json_with_string_field_type_When_deserialized_Then_field_type_enum_is_correctly_parsed() + { + // Arrange + string json = @"{ + ""Type"": ""Signature"", + ""Value"": ""test"", + ""Location"": { ""PageNumber"": 1 } + }"; + + // Act + var field = JsonSerializer.Deserialize(json); + + // Assert + field.Should().NotBeNull(); + field!.Type.Should().Be(FileFieldType.Signature); + } +} diff --git a/src/SignhostAPIClient.Tests/JSON/JsonResources.cs b/src/SignhostAPIClient.Tests/JSON/JsonResources.cs new file mode 100644 index 00000000..f0d4f241 --- /dev/null +++ b/src/SignhostAPIClient.Tests/JSON/JsonResources.cs @@ -0,0 +1,31 @@ +using System.IO; +using System.Reflection; + +namespace SignhostAPIClient.Tests.JSON; + +public static class JsonResources +{ + // Request JSONs + public static string AddOrReplaceFileMetaToTransaction { get; } = GetJson("Requests.AddOrReplaceFileMetaToTransaction"); + + // Response JSONs + public static string TransactionSingleSignerJson { get; } = GetJson("Responses.TransactionSingleSignerJson"); + public static string AddTransaction { get; } = GetJson("Responses.AddTransaction"); + public static string DeleteTransaction { get; } = GetJson("Responses.DeleteTransaction"); + public static string GetTransaction { get; } = GetJson("Responses.GetTransaction"); + public static string MinimalTransactionResponse { get; } = GetJson("Responses.MinimalTransactionResponse"); + public static string MockPostbackInvalid { get; } = GetJson("Responses.MockPostbackInvalid"); + public static string MockPostbackValid { get; } = GetJson("Responses.MockPostbackValid"); + + private static string GetJson(string fileName) + { + var assembly = Assembly.GetExecutingAssembly(); + string resourceName = $"Signhost.APIClient.Rest.Tests.JSON.{fileName}.json"; + + using var stream = assembly.GetManifestResourceStream(resourceName) + ?? throw new FileNotFoundException($"File not found: {fileName}"); + + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } +} diff --git a/src/SignhostAPIClient.Tests/JSON/Requests/AddOrReplaceFileMetaToTransaction.json b/src/SignhostAPIClient.Tests/JSON/Requests/AddOrReplaceFileMetaToTransaction.json new file mode 100644 index 00000000..1fc00000 --- /dev/null +++ b/src/SignhostAPIClient.Tests/JSON/Requests/AddOrReplaceFileMetaToTransaction.json @@ -0,0 +1 @@ +{"Signers":{"someSignerId":{"FormSets":["SampleFormSet"]}},"FormSets":{"SampleFormSet":{"SampleCheck":{"Type":"Check","Value":"I agree","Location":{"Search":"test"}}}}} \ No newline at end of file diff --git a/src/SignhostAPIClient.Tests/JSON/Responses/AddTransaction.json b/src/SignhostAPIClient.Tests/JSON/Responses/AddTransaction.json new file mode 100644 index 00000000..ea0bbe9f --- /dev/null +++ b/src/SignhostAPIClient.Tests/JSON/Responses/AddTransaction.json @@ -0,0 +1,80 @@ +{ + "Id":"c487be92-0255-40c7-bd7d-20805a65e7d9", + "Status":20, + "File":{ + "Id":"d4bba0df-f9e5-44c8-89db-f2bb46632d7b", + "Name":"contract.pdf" + }, + "Seal":true, + "Signers":[ + { + "Id":"a2932c07-ca93-4011-96f5-a77d2cd1ec32", + "Expires":null, + "Email":"user@example.com", + "Mobile":"+31612345678", + "Iban":null, + "BSN":null, + "RequireScribbleName":false, + "RequireScribble":true, + "RequireEmailVerification":true, + "RequireSmsVerification":true, + "RequireIdealVerification":false, + "RequireDigidVerification":false, + "RequireKennisnetVerification":false, + "RequireSurfnetVerification":false, + "SendSignRequest":true, + "SendSignConfirmation":null, + "SignRequestMessage":"Hello, could you please sign this document? Best regards, John Doe", + "DaysToRemind":15, + "Language":"en-US", + "ScribbleName":"John Doe", + "ScribbleNameFixed":false, + "Reference":"Client #123", + "ReturnUrl":"http://signhost.com", + "Activities":[ + { + "Id":"0b47eb5c-e800-4fe3-9795-09d380dff1f9", + "Code":103, + "Activity":"Opened", + "CreatedDateTime":"2016-03-17T21:08:55.1349315+01:00" + }, + { + "Id":"761f8678-116c-4e86-a47a-c8312681d285", + "Code":203, + "Activity":"Signed", + "CreatedDateTime":"2016-03-17T21:13:55.1349315+01:00" + } + ], + "RejectReason":null, + "SignUrl":"http://ui.signhost.com/sign/93dc596f-ab81-4d31-87aa-50352c4c237e", + "SignedDateTime":null, + "RejectDateTime":null, + "CreatedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "ModifiedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "Context":null + } + ], + "Receivers":[ + { + "Id":"2fe3dddf-4b50-49d1-a3d2-45b7d175fb97", + "Name":"John Doe", + "Email":"user@example.com", + "Language":"en-US", + "Message":"Hello, please find enclosed the digital signed document. Best regards, John Doe", + "Reference":null, + "Activities":null, + "CreatedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "ModifiedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "Context":null + } + ], + "Reference":"Contract #123", + "PostbackUrl":"http://example.com/postback.php", + "SignRequestMode":2, + "DaysToExpire":30, + "SendEmailNotifications":true, + "CreatedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "ModifiedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "CanceledDateTime":null, + "Context":null +} diff --git a/src/SignhostAPIClient.Tests/JSON/Responses/DeleteTransaction.json b/src/SignhostAPIClient.Tests/JSON/Responses/DeleteTransaction.json new file mode 100644 index 00000000..d87b4271 --- /dev/null +++ b/src/SignhostAPIClient.Tests/JSON/Responses/DeleteTransaction.json @@ -0,0 +1,80 @@ +{ + "Id":"496bec4d-4ac7-428f-894a-b3e9bade725d", + "Status":20, + "File":{ + "Id":"3149bf06-d01e-4f0d-9aa1-77e100e19772", + "Name":"contract.pdf" + }, + "Seal":true, + "Signers":[ + { + "Id":"4813e178-68a4-4105-b007-5ce9a3630867", + "Expires":null, + "Email":"user@example.com", + "Mobile":"+31612345678", + "Iban":null, + "BSN":null, + "RequireScribbleName":false, + "RequireScribble":true, + "RequireEmailVerification":true, + "RequireSmsVerification":true, + "RequireIdealVerification":false, + "RequireDigidVerification":false, + "RequireKennisnetVerification":false, + "RequireSurfnetVerification":false, + "SendSignRequest":true, + "SendSignConfirmation":null, + "SignRequestMessage":"Hello, could you please sign this document? Best regards, John Doe", + "DaysToRemind":15, + "Language":"en-US", + "ScribbleName":"John Doe", + "ScribbleNameFixed":false, + "Reference":"Client #123", + "ReturnUrl":"http://signhost.com", + "Activities":[ + { + "Id":"866183ae-0a3c-4441-a589-6fe3e1a0f1a1", + "Code":103, + "Activity":"Opened", + "CreatedDateTime":"2016-03-31T21:11:42.0267461+02:00" + }, + { + "Id":"9b7c4de9-6b8d-4d22-9b88-b7d2d0f084b9", + "Code":203, + "Activity":"Signed", + "CreatedDateTime":"2016-03-31T21:16:42.0267461+02:00" + } + ], + "RejectReason":null, + "SignUrl":"http://ui.signhost.com/sign/2eeaa5b9-9d4d-4418-b79f-4a33810e7147", + "SignedDateTime":null, + "RejectDateTime":null, + "CreatedDateTime":"2016-03-31T21:11:42.0267461+02:00", + "ModifiedDateTime":"2016-03-31T21:11:42.0267461+02:00", + "Context":null + } + ], + "Receivers":[ + { + "Id":"af07aaec-b612-4f7c-bb1b-c32603c9c6a2", + "Name":"John Doe", + "Email":"user@example.com", + "Language":"en-US", + "Message":"Hello, please find enclosed the digital signed document. Best regards, John Doe", + "Reference":null, + "Activities":null, + "CreatedDateTime":"2016-03-31T21:11:42.0267461+02:00", + "ModifiedDateTime":"2016-03-31T21:11:42.0267461+02:00", + "Context":null + } + ], + "Reference":"Contract #123", + "PostbackUrl":"http://example.com/postback.php", + "SignRequestMode":2, + "DaysToExpire":30, + "SendEmailNotifications":true, + "CreatedDateTime":"2016-03-31T21:11:42.0267461+02:00", + "ModifiedDateTime":"2016-03-31T21:11:42.0267461+02:00", + "CanceledDateTime":null, + "Context":null +} diff --git a/src/SignhostAPIClient.Tests/JSON/Responses/GetTransaction.json b/src/SignhostAPIClient.Tests/JSON/Responses/GetTransaction.json new file mode 100644 index 00000000..ea0bbe9f --- /dev/null +++ b/src/SignhostAPIClient.Tests/JSON/Responses/GetTransaction.json @@ -0,0 +1,80 @@ +{ + "Id":"c487be92-0255-40c7-bd7d-20805a65e7d9", + "Status":20, + "File":{ + "Id":"d4bba0df-f9e5-44c8-89db-f2bb46632d7b", + "Name":"contract.pdf" + }, + "Seal":true, + "Signers":[ + { + "Id":"a2932c07-ca93-4011-96f5-a77d2cd1ec32", + "Expires":null, + "Email":"user@example.com", + "Mobile":"+31612345678", + "Iban":null, + "BSN":null, + "RequireScribbleName":false, + "RequireScribble":true, + "RequireEmailVerification":true, + "RequireSmsVerification":true, + "RequireIdealVerification":false, + "RequireDigidVerification":false, + "RequireKennisnetVerification":false, + "RequireSurfnetVerification":false, + "SendSignRequest":true, + "SendSignConfirmation":null, + "SignRequestMessage":"Hello, could you please sign this document? Best regards, John Doe", + "DaysToRemind":15, + "Language":"en-US", + "ScribbleName":"John Doe", + "ScribbleNameFixed":false, + "Reference":"Client #123", + "ReturnUrl":"http://signhost.com", + "Activities":[ + { + "Id":"0b47eb5c-e800-4fe3-9795-09d380dff1f9", + "Code":103, + "Activity":"Opened", + "CreatedDateTime":"2016-03-17T21:08:55.1349315+01:00" + }, + { + "Id":"761f8678-116c-4e86-a47a-c8312681d285", + "Code":203, + "Activity":"Signed", + "CreatedDateTime":"2016-03-17T21:13:55.1349315+01:00" + } + ], + "RejectReason":null, + "SignUrl":"http://ui.signhost.com/sign/93dc596f-ab81-4d31-87aa-50352c4c237e", + "SignedDateTime":null, + "RejectDateTime":null, + "CreatedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "ModifiedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "Context":null + } + ], + "Receivers":[ + { + "Id":"2fe3dddf-4b50-49d1-a3d2-45b7d175fb97", + "Name":"John Doe", + "Email":"user@example.com", + "Language":"en-US", + "Message":"Hello, please find enclosed the digital signed document. Best regards, John Doe", + "Reference":null, + "Activities":null, + "CreatedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "ModifiedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "Context":null + } + ], + "Reference":"Contract #123", + "PostbackUrl":"http://example.com/postback.php", + "SignRequestMode":2, + "DaysToExpire":30, + "SendEmailNotifications":true, + "CreatedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "ModifiedDateTime":"2016-03-17T21:08:55.1349315+01:00", + "CanceledDateTime":null, + "Context":null +} diff --git a/src/SignhostAPIClient.Tests/JSON/Responses/MinimalTransactionResponse.json b/src/SignhostAPIClient.Tests/JSON/Responses/MinimalTransactionResponse.json new file mode 100644 index 00000000..99016264 --- /dev/null +++ b/src/SignhostAPIClient.Tests/JSON/Responses/MinimalTransactionResponse.json @@ -0,0 +1,4 @@ +{ + "Id":"50262c3f-9744-45bf-a4c6-8a3whatever", + "Status":5 +} diff --git a/src/SignhostAPIClient.Tests/JSON/Responses/MockPostbackInvalid.json b/src/SignhostAPIClient.Tests/JSON/Responses/MockPostbackInvalid.json new file mode 100644 index 00000000..ce1c2c4e --- /dev/null +++ b/src/SignhostAPIClient.Tests/JSON/Responses/MockPostbackInvalid.json @@ -0,0 +1,76 @@ +{ + "Id":"b10ae331-af78-4e79-a39e-5b64693b6b68", + "Status":20, + "Seal":true, + "Signers":[ + { + "Id":"fa95495d-6c59-48e0-962a-a4552f8d6b85", + "Expires":null, + "Email":"user@example.com", + "Mobile":"+31612345678", + "Iban":null, + "BSN":null, + "RequireScribbleName":false, + "RequireScribble":true, + "RequireEmailVerification":true, + "RequireSmsVerification":true, + "RequireIdealVerification":false, + "RequireDigidVerification":false, + "RequireKennisnetVerification":false, + "RequireSurfnetVerification":false, + "SendSignRequest":true, + "SendSignConfirmation":null, + "SignRequestMessage":"Hello, could you please sign this document? Best regards, John Doe", + "DaysToRemind":15, + "Language":"en-US", + "ScribbleName":"John Doe", + "ScribbleNameFixed":false, + "Reference":"Client #123", + "ReturnUrl":"https://signhost.com", + "Activities":[ + { + "Id":"bcba44a9-c201-4494-9920-2c1f7baebcf0", + "Code":103, + "Activity":"Opened", + "CreatedDateTime":"2016-06-15T23:33:04.1965465+02:00" + }, + { + "Id":"de94cf6e-e1a3-4c33-93bf-2013b036daaf", + "Code":203, + "Activity":"Signed", + "CreatedDateTime":"2016-06-15T23:38:04.1965465+02:00" + } + ], + "RejectReason":null, + "SignUrl":"https://view.signhost.com/sign/d3c93bd6-f1ce-48e7-8c9c-c2babfdd4034", + "SignedDateTime":null, + "RejectDateTime":null, + "CreatedDateTime":"2016-06-15T23:33:04.1965465+02:00", + "ModifiedDateTime":"2016-06-15T23:33:04.1965465+02:00", + "Context":null + } + ], + "Receivers":[ + { + "Id":"97ed6b54-b6d1-46ed-88c1-79779c3b47b1", + "Name":"John Doe", + "Email":"user@example.com", + "Language":"en-US", + "Message":"Hello, please find enclosed the digital signed document. Best regards, John Doe", + "Reference":null, + "Activities":null, + "CreatedDateTime":"2016-06-15T23:33:04.1965465+02:00", + "ModifiedDateTime":"2016-06-15T23:33:04.1965465+02:00", + "Context":null + } + ], + "Reference":"Contract #123", + "PostbackUrl":"https://example.com/postback.php", + "SignRequestMode":2, + "DaysToExpire":30, + "SendEmailNotifications":true, + "CreatedDateTime":"2016-08-31T21:22:56.2467731+02:00", + "ModifiedDateTime":"2016-08-31T21:22:56.2467731+02:00", + "CanceledDateTime":null, + "Context":null +} diff --git a/src/SignhostAPIClient.Tests/JSON/Responses/MockPostbackValid.json b/src/SignhostAPIClient.Tests/JSON/Responses/MockPostbackValid.json new file mode 100644 index 00000000..62ab673a --- /dev/null +++ b/src/SignhostAPIClient.Tests/JSON/Responses/MockPostbackValid.json @@ -0,0 +1,112 @@ +{ + "Id":"b10ae331-af78-4e79-a39e-5b64693b6b68", + "Status":20, + "Files":{ + "file1":{ + "Links":[ + { + "Rel":"file", + "Type":"application/pdf", + "Link":"https://api.signhost.com/api/transaction/b10ae331-af78-4e79-a39e-5b64693b6b68/file/file1" + } + ], + "DisplayName":"Sample File" + } + }, + "Seal":true, + "Signers":[ + { + "Id":"fa95495d-6c59-48e0-962a-a4552f8d6b85", + "Expires":null, + "Email":"user@example.com", + "Verifications":[ + { + "Type":"PhoneNumber", + "Number":"+31612345678" + }, + { + "Type":"Scribble", + "RequireHandsignature":false, + "ScribbleNameFixed":false, + "ScribbleName":"John Doe" + }, + { + "Type":"IPAddress", + "IPAddress":"1.2.3.4" + } + ], + "Mobile":"+31612345678", + "Iban":null, + "BSN":null, + "RequireScribbleName":false, + "RequireScribble":true, + "RequireEmailVerification":true, + "RequireSmsVerification":true, + "RequireIdealVerification":false, + "RequireDigidVerification":false, + "RequireKennisnetVerification":false, + "RequireSurfnetVerification":false, + "SendSignRequest":true, + "SendSignConfirmation":null, + "SignRequestMessage":"Hello, could you please sign this document? Best regards, John Doe", + "DaysToRemind":15, + "Language":"en-US", + "ScribbleName":"John Doe", + "ScribbleNameFixed":false, + "Reference":"Client #123", + "ReturnUrl":"https://signhost.com", + "Activities":[ + { + "Id":"bcba44a9-c201-4494-9920-2c1f7baebcf0", + "Code":103, + "Activity":"Opened", + "CreatedDateTime":"2016-06-15T23:33:04.1965465+02:00" + }, + { + "Id":"7aacf96a-5c2f-475d-98a5-726e41bfc5d3", + "Code":105, + "Activity":"DocumentOpened", + "Info":"file1", + "CreatedDateTime":"2020-01-30T16:31:05.6679583+01:00" + }, + { + "Id":"de94cf6e-e1a3-4c33-93bf-2013b036daaf", + "Code":203, + "Activity":"Signed", + "CreatedDateTime":"2016-06-15T23:38:04.1965465+02:00" + } + ], + "RejectReason":null, + "SignUrl":"https://view.signhost.com/sign/d3c93bd6-f1ce-48e7-8c9c-c2babfdd4034", + "SignedDateTime":null, + "RejectDateTime":null, + "CreatedDateTime":"2016-06-15T23:33:04.1965465+02:00", + "ModifiedDateTime":"2016-06-15T23:33:04.1965465+02:00", + "Context":null + } + ], + "Receivers":[ + { + "Id":"97ed6b54-b6d1-46ed-88c1-79779c3b47b1", + "Name":"John Doe", + "Email":"user@example.com", + "Language":"en-US", + "Message":"Hello, please find enclosed the digital signed document. Best regards, John Doe", + "Reference":null, + "Activities":null, + "CreatedDateTime":"2016-06-15T23:33:04.1965465+02:00", + "ModifiedDateTime":"2016-06-15T23:33:04.1965465+02:00", + "Context":null + } + ], + "Reference":"Contract #123", + "PostbackUrl":"https://example.com/postback.php", + "SignRequestMode":2, + "DaysToExpire":30, + "SendEmailNotifications":true, + "CreatedDateTime":"2016-08-31T21:22:56.2467731+02:00", + "ModifiedDateTime":"2016-08-31T21:22:56.2467731+02:00", + "CanceledDateTime":null, + "Context":null, + "Checksum":"cdc09eee2ed6df2846dcc193aedfef59f2834f8d" +} diff --git a/src/SignhostAPIClient.Tests/JSON/Responses/TransactionSingleSignerJson.json b/src/SignhostAPIClient.Tests/JSON/Responses/TransactionSingleSignerJson.json new file mode 100644 index 00000000..07d2fdb9 --- /dev/null +++ b/src/SignhostAPIClient.Tests/JSON/Responses/TransactionSingleSignerJson.json @@ -0,0 +1,46 @@ +{ + "Id":"50262c3f-9744-45bf-a4c6-8a3whatever", + "Status":5, + "CanceledDateTime":"2017-01-01T15:00:00.0000000+01:00", + "Files":{}, + "Seal":false, + "Signers":[ + { + "Id":"Signer1", + "Email":"test1@example.com", + "Verifications":[ + { + "Type":"PhoneNumber", + "Number":"+31615123456" + } + ], + "Mobile":"+31615087075", + "SignRequestMessage":"Hello 1st signer", + "Language":"nl-NL", + "Activities":[ + { + "Id":"Activity1", + "Code":103, + "CreatedDateTime":"2017-05-31T22:15:17.6409005+02:00" + }, + { + "Id":"Activity2", + "Code":105, + "Info":"592f2448347cd", + "CreatedDateTime":"2017-05-31T22:15:20.3284659+02:00" + }, + { + "Id":"25dd4131-f1c4-4e4c-a407-c4164cfe4096", + "Code":105, + "Info":"592f244834807", + "CreatedDateTime":"2017-05-31T22:15:24.4379773+02:00" + } + ] + } + ], + "Receivers":[], + "Reference":"Contract #123", + "SignRequestMode":2, + "DaysToExpire":14, + "Context":null +} diff --git a/src/SignhostAPIClient.Tests/LevelEnumConverterTests.cs b/src/SignhostAPIClient.Tests/LevelEnumConverterTests.cs index 67e491d1..175ab3f7 100644 --- a/src/SignhostAPIClient.Tests/LevelEnumConverterTests.cs +++ b/src/SignhostAPIClient.Tests/LevelEnumConverterTests.cs @@ -1,79 +1,89 @@ -using FluentAssertions; -using Newtonsoft.Json; -using Signhost.APIClient.Rest.DataObjects; using System; using System.Collections; using System.Collections.Generic; +using System.Text.Json; +using FluentAssertions; +using Signhost.APIClient.Rest.DataObjects; using Xunit; -namespace Signhost.APIClient.Rest.Tests +namespace Signhost.APIClient.Rest.Tests; + +public class LevelEnumConverterTests { - public class LevelEnumConverterTests + [Fact] + public void When_Level_is_null_should_deserialize_to_null() { - [Fact] - public void when_Level_is_null_should_deserialize_to_null() - { - // Arrange - const string json = "{\"Type\":\"eIDAS Login\",\"Level\":null}"; + // Arrange + const string json = "{\"Type\":\"eIDAS Login\",\"Level\":null}"; - // Act - var eidasLogin = JsonConvert.DeserializeObject(json); + // Act + var eidasLogin = JsonSerializer.Deserialize( + json, + SignhostJsonSerializerOptions.Default + ); - // Assert - eidasLogin.Level.Should().Be(null); - } + // Assert + eidasLogin.Level.Should().Be(null); + } - [Fact] - public void when_Level_is_not_supplied_should_deserialize_to_null() - { - // Arrange - const string json = "{\"Type\":\"eIDAS Login\"}"; + [Fact] + public void When_Level_is_not_supplied_should_deserialize_to_null() + { + // Arrange + const string json = "{\"Type\":\"eIDAS Login\"}"; - // Act - var eidasLogin = JsonConvert.DeserializeObject(json); + // Act + var eidasLogin = JsonSerializer.Deserialize( + json, + SignhostJsonSerializerOptions.Default + ); - // Assert - eidasLogin.Level.Should().Be(null); - } + // Assert + eidasLogin.Level.Should().Be(null); + } - [Fact] - public void when_Level_is_unknown_should_deserialize_to_Unknown_Level() - { - // Arrange - const string json = "{\"Type\":\"eIDAS Login\",\"Level\":\"foobar\"}"; + [Fact] + public void When_Level_is_unknown_should_deserialize_to_Unknown_Level() + { + // Arrange + const string json = "{\"Type\":\"eIDAS Login\",\"Level\":\"foobar\"}"; - // Act - var eidasLogin = JsonConvert.DeserializeObject(json); + // Act + var eidasLogin = JsonSerializer.Deserialize( + json, + SignhostJsonSerializerOptions.Default + ); - // Assert - eidasLogin.Level.Should().Be(Level.Unknown); - } + // Assert + eidasLogin.Level.Should().Be(Level.Unknown); + } - [Theory] - [ClassData(typeof(LevelTestData))] - public void when_Level_is_valid_should_deserialize_to_correct_value(Level level) - { - // Arrange - string json = $"{{\"Type\":\"eIDAS Login\",\"Level\":\"{level}\"}}"; + [Theory] + [ClassData(typeof(LevelTestData))] + public void When_Level_is_valid_should_deserialize_to_correct_value(Level level) + { + // Arrange + string json = $"{{\"Type\":\"eIDAS Login\",\"Level\":\"{level}\"}}"; - // Act - var eidasLogin = JsonConvert.DeserializeObject(json); + // Act + var eidasLogin = JsonSerializer.Deserialize( + json, + SignhostJsonSerializerOptions.Default); - // Assert - eidasLogin.Level.Should().Be(level); - } + // Assert + eidasLogin.Level.Should().Be(level); + } - private class LevelTestData - : IEnumerable + private class LevelTestData + : IEnumerable + { + public IEnumerator GetEnumerator() { - public IEnumerator GetEnumerator() - { - foreach (var value in Enum.GetValues(typeof(Level))) { - yield return new[] { value }; - } + foreach (object value in Enum.GetValues()) { + yield return new[] { value }; } - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } diff --git a/src/SignhostAPIClient.Tests/PostbackTests.cs b/src/SignhostAPIClient.Tests/PostbackTests.cs index a4724723..1e08e4df 100644 --- a/src/SignhostAPIClient.Tests/PostbackTests.cs +++ b/src/SignhostAPIClient.Tests/PostbackTests.cs @@ -1,119 +1,114 @@ using System; using System.Linq; +using System.Text.Json; using FluentAssertions; -using Newtonsoft.Json; using Signhost.APIClient.Rest.DataObjects; +using SignhostAPIClient.Tests.JSON; using Xunit; -namespace Signhost.APIClient.Rest.Tests +namespace Signhost.APIClient.Rest.Tests; + +public class PostbackTests { - public class PostbackTests + [Fact] + public void PostbackTransaction_should_get_serialized_correctly() { - [Fact] - public void PostbackTransaction_should_get_serialized_correctly() - { - string json = RequestBodies.MockPostbackValid; - var postbackTransaction = JsonConvert.DeserializeObject(json); - - postbackTransaction.Id .Should().Be("b10ae331-af78-4e79-a39e-5b64693b6b68"); - postbackTransaction.Status .Should().Be(TransactionStatus.InProgress); - postbackTransaction.Seal .Should().BeTrue(); - postbackTransaction.Reference .Should().Be("Contract #123"); - postbackTransaction.PostbackUrl .Should().Be("https://example.com/postback.php"); - postbackTransaction.SignRequestMode .Should().Be(2); - postbackTransaction.DaysToExpire .Should().Be(30); - postbackTransaction.SendEmailNotifications.Should().BeTrue(); - postbackTransaction.CreatedDateTime .Should().Be(DateTimeOffset.Parse("2016-08-31T21:22:56.2467731+02:00")); - postbackTransaction.CancelledDateTime .Should().BeNull(); - (postbackTransaction.Context is null) .Should().BeTrue(); - postbackTransaction.Checksum .Should().Be("cdc09eee2ed6df2846dcc193aedfef59f2834f8d"); - - var signers = postbackTransaction.Signers; - signers.Should().HaveCount(1); - - var signer = signers.Single(); - signer.Id .Should().Be("fa95495d-6c59-48e0-962a-a4552f8d6b85"); - signer.Expires .Should().BeNull(); - signer.Email .Should().Be("user@example.com"); - signer.SendSignRequest .Should().BeTrue(); - signer.SendSignConfirmation.Should().BeNull(); - signer.SignRequestMessage .Should().Be("Hello, could you please sign this document? Best regards, John Doe"); - signer.DaysToRemind .Should().Be(15); - signer.Language .Should().Be("en-US"); - signer.ScribbleName .Should().Be("John Doe"); - signer.ScribbleNameFixed .Should().BeFalse(); - signer.Reference .Should().Be("Client #123"); - signer.ReturnUrl .Should().Be("https://signhost.com"); - signer.RejectReason .Should().BeNull(); - signer.SignUrl .Should().Be("https://view.signhost.com/sign/d3c93bd6-f1ce-48e7-8c9c-c2babfdd4034"); - (signer.Context is null) .Should().BeTrue(); - - var verifications = signer.Verifications; - verifications.Should().HaveCount(3); - - var phoneNumberVerification = verifications[0] as PhoneNumberVerification; - phoneNumberVerification .Should().NotBeNull(); - phoneNumberVerification.Type .Should().Be("PhoneNumber"); - phoneNumberVerification.Number.Should().Be("+31612345678"); - - var scribbleVerification = verifications[1] as ScribbleVerification; - scribbleVerification .Should().NotBeNull(); - scribbleVerification.Type .Should().Be("Scribble"); - scribbleVerification.RequireHandsignature.Should().BeFalse(); - scribbleVerification.ScribbleNameFixed .Should().BeFalse(); - scribbleVerification.ScribbleName .Should().Be("John Doe"); - - var ipAddressVerification = verifications[2] as IPAddressVerification; - ipAddressVerification .Should().NotBeNull(); - ipAddressVerification.Type .Should().Be("IPAddress"); - ipAddressVerification.IPAddress.Should().Be("1.2.3.4"); - - var activities = signer.Activities; - activities.Should().HaveCount(3); - - var openedActivity = activities[0]; - openedActivity.Id .Should().Be("bcba44a9-c201-4494-9920-2c1f7baebcf0"); - openedActivity.Code .Should().Be(ActivityType.Opened); - openedActivity.Info .Should().BeNull(); - openedActivity.CreatedDateTime.Should().Be(DateTimeOffset.Parse("2016-06-15T23:33:04.1965465+02:00")); - - var documentOpenedActivity = activities[1]; - documentOpenedActivity.Id .Should().Be("7aacf96a-5c2f-475d-98a5-726e41bfc5d3"); - documentOpenedActivity.Code .Should().Be(ActivityType.DocumentOpened); - documentOpenedActivity.Info .Should().Be("file1"); - documentOpenedActivity.CreatedDateTime.Should().Be(DateTimeOffset.Parse("2020-01-30T16:31:05.6679583+01:00")); - - var signedActivity = activities[2]; - signedActivity.Id .Should().Be("de94cf6e-e1a3-4c33-93bf-2013b036daaf"); - signedActivity.Code .Should().Be(ActivityType.Signed); - signedActivity.Info .Should().BeNull(); - signedActivity.CreatedDateTime.Should().Be(DateTimeOffset.Parse("2016-06-15T23:38:04.1965465+02:00")); - - var receivers = postbackTransaction.Receivers; - receivers.Should().HaveCount(1); - - var receiver = receivers.Single(); - receiver.Name .Should().Be("John Doe"); - receiver.Email .Should().Be("user@example.com"); - receiver.Language .Should().Be("en-US"); - receiver.Message .Should().Be("Hello, please find enclosed the digital signed document. Best regards, John Doe"); - receiver.Reference .Should().BeNull(); - receiver.Activities .Should().BeNull(); - (receiver.Context is null).Should().BeTrue(); - - var files = postbackTransaction.Files; - files.Should().HaveCount(1); - - var file = files["file1"]; - file.DisplayName.Should().Be("Sample File"); - - var links = file.Links; - links.Should().HaveCount(1); - - var link = links.Single(); - link.Rel .Should().Be("file"); - link.Type.Should().Be("application/pdf"); - link.Link.Should().Be("https://api.signhost.com/api/transaction/b10ae331-af78-4e79-a39e-5b64693b6b68/file/file1"); - } + string json = JsonResources.MockPostbackValid; + var postbackTransaction = JsonSerializer.Deserialize(json, SignhostJsonSerializerOptions.Default); + + postbackTransaction.Id .Should().Be("b10ae331-af78-4e79-a39e-5b64693b6b68"); + postbackTransaction.Status .Should().Be(TransactionStatus.InProgress); + postbackTransaction.Seal .Should().BeTrue(); + postbackTransaction.Reference .Should().Be("Contract #123"); + postbackTransaction.PostbackUrl .Should().Be("https://example.com/postback.php"); + postbackTransaction.SignRequestMode .Should().Be(2); + postbackTransaction.DaysToExpire .Should().Be(30); + postbackTransaction.SendEmailNotifications.Should().BeTrue(); + postbackTransaction.CreatedDateTime .Should().Be(DateTimeOffset.Parse("2016-08-31T21:22:56.2467731+02:00")); + postbackTransaction.CanceledDateTime .Should().BeNull(); + (postbackTransaction.Context is null) .Should().BeTrue(); + postbackTransaction.Checksum .Should().Be("cdc09eee2ed6df2846dcc193aedfef59f2834f8d"); + + var signers = postbackTransaction.Signers; + signers.Should().HaveCount(1); + + var signer = signers.Single(); + signer.Id .Should().Be("fa95495d-6c59-48e0-962a-a4552f8d6b85"); + signer.Expires .Should().BeNull(); + signer.Email .Should().Be("user@example.com"); + signer.SendSignRequest .Should().BeTrue(); + signer.SendSignConfirmation.Should().BeNull(); + signer.SignRequestMessage .Should().Be("Hello, could you please sign this document? Best regards, John Doe"); + signer.DaysToRemind .Should().Be(15); + signer.Language .Should().Be("en-US"); + signer.Reference .Should().Be("Client #123"); + signer.ReturnUrl .Should().Be("https://signhost.com"); + signer.RejectReason .Should().BeNull(); + signer.SignUrl .Should().Be("https://view.signhost.com/sign/d3c93bd6-f1ce-48e7-8c9c-c2babfdd4034"); + (signer.Context is null) .Should().BeTrue(); + + var verifications = signer.Verifications; + verifications.Should().HaveCount(3); + + var phoneNumberVerification = verifications[0] as PhoneNumberVerification; + phoneNumberVerification .Should().NotBeNull(); + phoneNumberVerification.Number.Should().Be("+31612345678"); + + var scribbleVerification = verifications[1] as ScribbleVerification; + scribbleVerification .Should().NotBeNull(); + scribbleVerification.RequireHandsignature.Should().BeFalse(); + scribbleVerification.ScribbleNameFixed .Should().BeFalse(); + scribbleVerification.ScribbleName .Should().Be("John Doe"); + + var ipAddressVerification = verifications[2] as IPAddressVerification; + ipAddressVerification .Should().NotBeNull(); + ipAddressVerification.IPAddress.Should().Be("1.2.3.4"); + + var activities = signer.Activities; + activities.Should().HaveCount(3); + + var openedActivity = activities[0]; + openedActivity.Id .Should().Be("bcba44a9-c201-4494-9920-2c1f7baebcf0"); + openedActivity.Code .Should().Be(ActivityType.Opened); + openedActivity.Info .Should().BeNull(); + openedActivity.CreatedDateTime.Should().Be(DateTimeOffset.Parse("2016-06-15T23:33:04.1965465+02:00")); + + var documentOpenedActivity = activities[1]; + documentOpenedActivity.Id .Should().Be("7aacf96a-5c2f-475d-98a5-726e41bfc5d3"); + documentOpenedActivity.Code .Should().Be(ActivityType.DocumentOpened); + documentOpenedActivity.Info .Should().Be("file1"); + documentOpenedActivity.CreatedDateTime.Should().Be(DateTimeOffset.Parse("2020-01-30T16:31:05.6679583+01:00")); + + var signedActivity = activities[2]; + signedActivity.Id .Should().Be("de94cf6e-e1a3-4c33-93bf-2013b036daaf"); + signedActivity.Code .Should().Be(ActivityType.Signed); + signedActivity.Info .Should().BeNull(); + signedActivity.CreatedDateTime.Should().Be(DateTimeOffset.Parse("2016-06-15T23:38:04.1965465+02:00")); + + var receivers = postbackTransaction.Receivers; + receivers.Should().HaveCount(1); + + var receiver = receivers.Single(); + receiver.Name .Should().Be("John Doe"); + receiver.Email .Should().Be("user@example.com"); + receiver.Language .Should().Be("en-US"); + receiver.Message .Should().Be("Hello, please find enclosed the digital signed document. Best regards, John Doe"); + receiver.Reference .Should().BeNull(); + receiver.Activities .Should().BeNull(); + (receiver.Context is null).Should().BeTrue(); + + var files = postbackTransaction.Files; + files.Should().HaveCount(1); + + var file = files["file1"]; + file.DisplayName.Should().Be("Sample File"); + + var links = file.Links; + links.Should().HaveCount(1); + + var link = links.Single(); + link.Rel.Should().Be("file"); + link.Type.Should().Be("application/pdf"); + link.Link.Should().Be("https://api.signhost.com/api/transaction/b10ae331-af78-4e79-a39e-5b64693b6b68/file/file1"); } } diff --git a/src/SignhostAPIClient.Tests/RequestBodies.Designer.cs b/src/SignhostAPIClient.Tests/RequestBodies.Designer.cs deleted file mode 100644 index 55227f02..00000000 --- a/src/SignhostAPIClient.Tests/RequestBodies.Designer.cs +++ /dev/null @@ -1,114 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Signhost.APIClient.Rest.Tests { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class RequestBodies { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal RequestBodies() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Signhost.APIClient.Rest.Tests.RequestBodies", typeof(RequestBodies).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to {"DisplayOrder":null,"DisplayName":null,"Description":null,"Signers":{"someSignerId":{"FormSets":["SampleFormSet"]}},"FormSets":{"SampleFormSet":{"SampleCheck":{"Type":"Check","Value":"I agree","Location":{"Search":"test","Occurence":null,"Top":null,"Right":null,"Bottom":null,"Left":null,"Width":null,"Height":null,"PageNumber":null}}}}}. - /// - internal static string AddOrReplaceFileMetaToTransaction { - get { - return ResourceManager.GetString("AddOrReplaceFileMetaToTransaction", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to { - /// "Id": "50262c3f-9744-45bf-a4c6-8a3whatever", - /// "Status": 5, - /// "CanceledDateTime": "2017-01-01 15:00", - /// "Files": {}, - /// "Seal": false, - /// "Signers": [ - /// { - /// "Id": "Signer1", - /// "Email": "test1@example.com", - /// "Verifications": [ - /// { - /// "Type": "PhoneNumber", - /// "Number": "+31615123456" - /// } - /// ], - /// "Mobile": "+31615087075", - /// "SignRequestMessage": "Hello 1st signer", - /// "Language": "nl-NL", - /// "Activities": [ - /// { - /// "I [rest of string was truncated]";. - /// - internal static string TransactionSingleSignerJson { - get { - return ResourceManager.GetString("TransactionSingleSignerJson", resourceCulture); - } - } - - internal static string MockPostbackValid { - get { - return ResourceManager.GetString("MockPostbackValid", resourceCulture); - } - } - - internal static string MockPostbackInvalid { - get { - return ResourceManager.GetString("MockPostbackInvalid", resourceCulture); - } - } - } -} diff --git a/src/SignhostAPIClient.Tests/RequestBodies.resx b/src/SignhostAPIClient.Tests/RequestBodies.resx deleted file mode 100644 index 62d2094a..00000000 --- a/src/SignhostAPIClient.Tests/RequestBodies.resx +++ /dev/null @@ -1,355 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - text/microsoft-resx - - - 2.0 - - - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - {"DisplayOrder":null,"DisplayName":null,"Description":null,"Signers":{"someSignerId":{"FormSets":["SampleFormSet"]}},"FormSets":{"SampleFormSet":{"SampleCheck":{"Type":"Check","Value":"I agree","Location":{"Search":"test","Occurence":null,"Top":null,"Right":null,"Bottom":null,"Left":null,"Width":null,"Height":null,"PageNumber":null}}}}} - - - { - "Id": "50262c3f-9744-45bf-a4c6-8a3whatever", - "Status": 5, - "CanceledDateTime": "2017-01-01 15:00", - "Files": {}, - "Seal": false, - "Signers": [ - { - "Id": "Signer1", - "Email": "test1@example.com", - "Verifications": [ - { - "Type": "PhoneNumber", - "Number": "+31615123456" - } - ], - "Mobile": "+31615087075", - "SignRequestMessage": "Hello 1st signer", - "Language": "nl-NL", - "Activities": [ - { - "Id": "Activity1", - "Code": 103, - "CreatedDateTime": "2017-05-31T22:15:17.6409005+02:00" - }, - { - "Id": "Activity2", - "Code": 105, - "Info": "592f2448347cd", - "CreatedDateTime": "2017-05-31T22:15:20.3284659+02:00" - }, - { - "Id": "25dd4131-f1c4-4e4c-a407-c4164cfe4096", - "Code": 105, - "Info": "592f244834807", - "CreatedDateTime": "2017-05-31T22:15:24.4379773+02:00" - } - ] - } - ], - "Receivers": [], - "Reference": "Contract #123", - "SignRequestMode": 2, - "DaysToExpire": 14, - "Context": null -} - - - -{ - "Id": "b10ae331-af78-4e79-a39e-5b64693b6b68", - "Status": 20, - "Files": { - "file1": { - "Links": [ - { - "Rel": "file", - "Type": "application/pdf", - "Link": "https://api.signhost.com/api/transaction/b10ae331-af78-4e79-a39e-5b64693b6b68/file/file1" - } - ], - "DisplayName": "Sample File" - } - }, - "Seal": true, - "Signers": - [{ - "Id": "fa95495d-6c59-48e0-962a-a4552f8d6b85", - "Expires": null, - "Email": "user@example.com", - "Verifications": [ - { - "Type": "PhoneNumber", - "Number": "+31612345678" - }, - { - "Type": "Scribble", - "RequireHandsignature": false, - "ScribbleNameFixed": false, - "ScribbleName": "John Doe" - }, - { - "Type": "IPAddress", - "IPAddress": "1.2.3.4" - } - ], - "Mobile": "+31612345678", - "Iban": null, - "BSN": null, - "RequireScribbleName": false, - "RequireScribble": true, - "RequireEmailVerification": true, - "RequireSmsVerification": true, - "RequireIdealVerification": false, - "RequireDigidVerification": false, - "RequireKennisnetVerification": false, - "RequireSurfnetVerification": false, - "SendSignRequest": true, - "SendSignConfirmation": null, - "SignRequestMessage": "Hello, could you please sign this document? Best regards, John Doe", - "DaysToRemind": 15, - "Language": "en-US", - "ScribbleName": "John Doe", - "ScribbleNameFixed": false, - "Reference": "Client #123", - "ReturnUrl": "https://signhost.com", - "Activities": - [{ - "Id": "bcba44a9-c201-4494-9920-2c1f7baebcf0", - "Code": 103, - "Activity": "Opened", - "CreatedDateTime": "2016-06-15T23:33:04.1965465+02:00"}, - { - "Id": "7aacf96a-5c2f-475d-98a5-726e41bfc5d3", - "Code": 105, - "Activity": "DocumentOpened", - "Info": "file1", - "CreatedDateTime": "2020-01-30T16:31:05.6679583+01:00" - }, - { - "Id": "de94cf6e-e1a3-4c33-93bf-2013b036daaf", - "Code": 203,"Activity": "Signed", - "CreatedDateTime": "2016-06-15T23:38:04.1965465+02:00" - }], - "RejectReason": null, - "SignUrl": "https://view.signhost.com/sign/d3c93bd6-f1ce-48e7-8c9c-c2babfdd4034", - "SignedDateTime": null, - "RejectDateTime": null, - "CreatedDateTime": "2016-06-15T23:33:04.1965465+02:00", - "ModifiedDateTime": "2016-06-15T23:33:04.1965465+02:00", - "Context": null}], - "Receivers": - [{ - "Id": "97ed6b54-b6d1-46ed-88c1-79779c3b47b1", - "Name": "John Doe", - "Email": "user@example.com", - "Language": "en-US", - "Message": "Hello, please find enclosed the digital signed document. Best regards, John Doe", - "Reference": null, - "Activities": null, - "CreatedDateTime": "2016-06-15T23:33:04.1965465+02:00", - "ModifiedDateTime": "2016-06-15T23:33:04.1965465+02:00", - "Context": null - }], - "Reference": "Contract #123", - "PostbackUrl": "https://example.com/postback.php", - "SignRequestMode": 2, - "DaysToExpire": 30, - "SendEmailNotifications": true, - "CreatedDateTime": "2016-08-31T21:22:56.2467731+02:00", - "ModifiedDateTime": "2016-08-31T21:22:56.2467731+02:00", - "CanceledDateTime": null, - "Context": null, - "Checksum": "cdc09eee2ed6df2846dcc193aedfef59f2834f8d" -} - - - - -{ - "Id": "b10ae331-af78-4e79-a39e-5b64693b6b68", - "Status": 20, - "Seal": true, - "Signers": - [{ - "Id": "fa95495d-6c59-48e0-962a-a4552f8d6b85", - "Expires": null, - "Email": "user@example.com", - "Mobile": "+31612345678", - "Iban": null, - "BSN": null, - "RequireScribbleName": false, - "RequireScribble": true, - "RequireEmailVerification": true, - "RequireSmsVerification": true, - "RequireIdealVerification": false, - "RequireDigidVerification": false, - "RequireKennisnetVerification": false, - "RequireSurfnetVerification": false, - "SendSignRequest": true, - "SendSignConfirmation": null, - "SignRequestMessage": "Hello, could you please sign this document? Best regards, John Doe", - "DaysToRemind": 15, - "Language": "en-US", - "ScribbleName": "John Doe", - "ScribbleNameFixed": false, - "Reference": "Client #123", - "ReturnUrl": "https://signhost.com", - "Activities": - [{ - "Id": "bcba44a9-c201-4494-9920-2c1f7baebcf0", - "Code": 103, - "Activity": "Opened", - "CreatedDateTime": "2016-06-15T23:33:04.1965465+02:00"}, - { - "Id": "de94cf6e-e1a3-4c33-93bf-2013b036daaf", - "Code": 203,"Activity": "Signed", - "CreatedDateTime": "2016-06-15T23:38:04.1965465+02:00" - }], - "RejectReason": null, - "SignUrl": "https://view.signhost.com/sign/d3c93bd6-f1ce-48e7-8c9c-c2babfdd4034", - "SignedDateTime": null, - "RejectDateTime": null, - "CreatedDateTime": "2016-06-15T23:33:04.1965465+02:00", - "ModifiedDateTime": "2016-06-15T23:33:04.1965465+02:00", - "Context": null}], - "Receivers": - [{ - "Id": "97ed6b54-b6d1-46ed-88c1-79779c3b47b1", - "Name": "John Doe", - "Email": "user@example.com", - "Language": "en-US", - "Message": "Hello, please find enclosed the digital signed document. Best regards, John Doe", - "Reference": null, - "Activities": null, - "CreatedDateTime": "2016-06-15T23:33:04.1965465+02:00", - "ModifiedDateTime": "2016-06-15T23:33:04.1965465+02:00", - "Context": null - }], - "Reference": "Contract #123", - "PostbackUrl": "https://example.com/postback.php", - "SignRequestMode": 2, - "DaysToExpire": 30, - "SendEmailNotifications": true, - "CreatedDateTime": "2016-08-31T21:22:56.2467731+02:00", - "ModifiedDateTime": "2016-08-31T21:22:56.2467731+02:00", - "CanceledDateTime": null, - "Context": null, -} - - - diff --git a/src/SignhostAPIClient.Tests/SignhostAPIClient.Tests.csproj b/src/SignhostAPIClient.Tests/SignhostAPIClient.Tests.csproj index cc06f7b3..d4bf700d 100644 --- a/src/SignhostAPIClient.Tests/SignhostAPIClient.Tests.csproj +++ b/src/SignhostAPIClient.Tests/SignhostAPIClient.Tests.csproj @@ -1,6 +1,6 @@ - net8.0 + net10.0 ../signhost.ruleset test @@ -10,7 +10,6 @@ - @@ -22,23 +21,11 @@ - - Signhost.APIClient.Rest.Tests - - - - ResXFileCodeGenerator - - - ResXFileCodeGenerator - RequestBodies.Designer.cs - + - - - RequestBodies.resx - - + + Signhost.APIClient.Rest.Tests + diff --git a/src/SignhostAPIClient.Tests/SignhostAPIClient.Tests.v2.ncrunchproject b/src/SignhostAPIClient.Tests/SignhostAPIClient.Tests.v2.ncrunchproject deleted file mode 100644 index 1f00ab7c..00000000 --- a/src/SignhostAPIClient.Tests/SignhostAPIClient.Tests.v2.ncrunchproject +++ /dev/null @@ -1,26 +0,0 @@ - - true - 1000 - false - false - false - true - false - false - false - false - false - true - true - false - true - true - true - 60000 - - - - AutoDetect - STA - x86 - \ No newline at end of file diff --git a/src/SignhostAPIClient.Tests/SignhostApiClientTests.cs b/src/SignhostAPIClient.Tests/SignhostApiClientTests.cs index 041a40f4..bd3d1dcd 100644 --- a/src/SignhostAPIClient.Tests/SignhostApiClientTests.cs +++ b/src/SignhostAPIClient.Tests/SignhostApiClientTests.cs @@ -1,669 +1,664 @@ using System; -using System.Threading.Tasks; -using System.Net.Http; +using System.Collections.Generic; using System.IO; -using Xunit; -using Signhost.APIClient.Rest.DataObjects; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using FluentAssertions; -using System.Collections.Generic; using RichardSzalay.MockHttp; -using System.Net; +using Signhost.APIClient.Rest.DataObjects; +using Signhost.APIClient.Rest.ErrorHandling; +using SignhostAPIClient.Tests.JSON; +using Xunit; + +namespace Signhost.APIClient.Rest.Tests; -namespace Signhost.APIClient.Rest.Tests +public class SignhostApiClientTests { - public class SignHostApiClientTests + private readonly SignhostApiClientSettings settings = new("AppKey", "Usertoken") { + Endpoint = "http://localhost/api/", + }; + + private readonly SignhostApiClientSettings oauthSettings = new("AppKey") { + Endpoint = "http://localhost/api/", + }; + + [Fact] + public async Task When_AddOrReplaceFileMetaToTransaction_is_called_Then_the_request_body_should_contain_the_serialized_file_meta() { - private readonly SignHostApiClientSettings settings = new("AppKey", "Usertoken") { - Endpoint = "http://localhost/api/" + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Put, "http://localhost/api/transaction/transactionId/file/fileId") + .WithContent(JsonResources.AddOrReplaceFileMetaToTransaction) + .Respond(HttpStatusCode.OK); + + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); + + var fileSignerMeta = new FileSignerMeta { + FormSets = ["SampleFormSet"] }; - private readonly SignHostApiClientSettings oauthSettings = new("AppKey") { - Endpoint = "http://localhost/api/" + var field = new Field { + Type = FileFieldType.Check, + Value = "I agree", + Location = new Location { Search = "test" }, }; - [Fact] - public async Task when_AddOrReplaceFileMetaToTransaction_is_called_then_the_request_body_should_contain_the_serialized_file_meta() - { - var mockHttp = new MockHttpMessageHandler(); - - mockHttp.Expect(HttpMethod.Put, "http://localhost/api/transaction/transactionId/file/fileId") - .WithContent(RequestBodies.AddOrReplaceFileMetaToTransaction) - .Respond(HttpStatusCode.OK); - - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); - - var fileSignerMeta = new FileSignerMeta - { - FormSets = new string[] { "SampleFormSet" } - }; - - var field = new Field - { - Type = "Check", - Value = "I agree", - Location = new Location - { - Search = "test" - } - }; - - FileMeta fileMeta = new FileMeta - { - Signers = new Dictionary - { - { "someSignerId", fileSignerMeta } - }, - FormSets = new Dictionary> - { - { "SampleFormSet", new Dictionary - { - { "SampleCheck", field } - } - } - } - }; + FileMeta fileMeta = new FileMeta { + Signers = new Dictionary { + ["someSignerId"] = fileSignerMeta, + }, + FormSets = new Dictionary> { + ["SampleFormSet"] = new Dictionary { + ["SampleCheck"] = field, + }, + }, + }; - await signhostApiClient.AddOrReplaceFileMetaToTransactionAsync(fileMeta, "transactionId", "fileId"); - } + await signhostApiClient.AddOrReplaceFileMetaToTransactionAsync( + fileMeta, + "transactionId", + "fileId" + ); - mockHttp.VerifyNoOutstandingExpectation(); - } + mockHttp.VerifyNoOutstandingExpectation(); + } - [Fact] - public async Task when_a_GetTransaction_is_called_then_we_should_have_called_the_transaction_get_once() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/transaction Id") - .Respond(HttpStatusCode.OK, new StringContent(APIResponses.GetTransaction)); + [Fact] + public async Task When_a_GetTransaction_is_called_Then_we_should_have_called_the_transaction_get_once() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/transactionId") + .Respond(HttpStatusCode.OK, new StringContent(JsonResources.GetTransaction)); - using (var httpClient = mockHttp.ToHttpClient()) { + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - var signhostApiClient = new SignHostApiClient(settings, httpClient); + var result = await signhostApiClient.GetTransactionAsync("transactionId"); + result.Id.Should().Be("c487be92-0255-40c7-bd7d-20805a65e7d9"); - var result = await signhostApiClient.GetTransactionAsync("transaction Id"); - result.Id.Should().Be("c487be92-0255-40c7-bd7d-20805a65e7d9"); + mockHttp.VerifyNoOutstandingExpectation(); + } + + [Fact] + public async Task When_GetTransaction_is_called_and_the_authorization_is_bad_Then_we_should_get_a_BadAuthorizationException() + { + MockHttpMessageHandler mockHttp = new(); + const string expectedResponseBody = """ + { + "message": "unauthorized" } + """; + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/transactionId") + .Respond(HttpStatusCode.Unauthorized, new StringContent(expectedResponseBody)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); - [Fact] - public async Task when_GetTransaction_is_called_and_the_authorization_is_bad_then_we_should_get_a_BadAuthorizationException() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/transaction Id") - .Respond(HttpStatusCode.Unauthorized, new StringContent("{'message': 'unauthorized' }")); + SignhostApiClient signhostApiClient = new(settings, httpClient); - using (var httpClient = mockHttp.ToHttpClient()) { + Func getTransaction = () => signhostApiClient.GetTransactionAsync("transactionId"); + var exception = await getTransaction.Should().ThrowAsync(); + exception.Which.ResponseBody.Should().Be(expectedResponseBody); - var signhostApiClient = new SignHostApiClient(settings, httpClient); + mockHttp.VerifyNoOutstandingExpectation(); + } - Func getTransaction = () => signhostApiClient.GetTransactionAsync("transaction Id"); - await getTransaction.Should().ThrowAsync(); + [Fact] + public async Task When_GetTransaction_is_called_and_request_is_bad_Then_we_should_get_a_BadRequestException() + { + MockHttpMessageHandler mockHttp = new(); + const string expectedResponseBody = """ + { + "message": "Bad Request" } + """; + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/transactionId") + .Respond(HttpStatusCode.BadRequest, new StringContent(expectedResponseBody)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); - [Fact] - public async Task when_GetTransaction_is_called_and_request_is_bad_then_we_should_get_a_BadRequestException() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/transaction Id") - .Respond(HttpStatusCode.BadRequest, new StringContent("{ 'message': 'Bad Request' }")); + SignhostApiClient signhostApiClient = new(settings, httpClient); - using (var httpClient = mockHttp.ToHttpClient()) { + Func getTransaction = () => signhostApiClient.GetTransactionAsync("transactionId"); + var exception = await getTransaction.Should().ThrowAsync(); + exception.Which.ResponseBody.Should().Be(expectedResponseBody); - var signhostApiClient = new SignHostApiClient(settings, httpClient); + mockHttp.VerifyNoOutstandingExpectation(); + } - Func getTransaction = () => signhostApiClient.GetTransactionAsync("transaction Id"); - await getTransaction.Should().ThrowAsync(); + [Fact] + public async Task When_GetTransaction_is_called_and_credits_have_run_out_Then_we_should_get_a_OutOfCreditsException() + { + MockHttpMessageHandler mockHttp = new(); + const string expectedResponseBody = """ + { + "type": "https://api.signhost.com/problem/subscription/out-of-credits" } + """; + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/transactionId") + .Respond(HttpStatusCode.PaymentRequired, new StringContent(expectedResponseBody)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); - [Fact] - public async Task when_GetTransaction_is_called_and_credits_have_run_out_then_we_should_get_a_OutOfCreditsException() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/transaction Id") - .Respond(HttpStatusCode.PaymentRequired, new StringContent("{ 'type': 'https://api.signhost.com/problem/subscription/out-of-credits' }")); + SignhostApiClient signhostApiClient = new(settings, httpClient); - using (var httpClient = mockHttp.ToHttpClient()) { + Func getTransaction = () => signhostApiClient.GetTransactionAsync("transactionId"); + var exception = await getTransaction.Should().ThrowAsync(); + exception.Which.ResponseBody.Should().Be(expectedResponseBody); - var signhostApiClient = new SignHostApiClient(settings, httpClient); + mockHttp.VerifyNoOutstandingExpectation(); + } - Func getTransaction = () => signhostApiClient.GetTransactionAsync("transaction Id"); - await getTransaction.Should().ThrowAsync(); + [Fact] + public async Task When_GetTransaction_is_called_and_not_found_Then_we_should_get_a_NotFoundException() + { + MockHttpMessageHandler mockHttp = new(); + const string expectedResponseBody = """ + { + "message": "Not Found" } + """; + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/transactionId") + .Respond(HttpStatusCode.NotFound, new StringContent(expectedResponseBody)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); - [Fact] - public async Task when_GetTransaction_is_called_and_not_found_then_we_should_get_a_NotFoundException() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/transaction Id") - .Respond(HttpStatusCode.NotFound, new StringContent("{ 'Message': 'Not Found' }")); + SignhostApiClient signhostApiClient = new(settings, httpClient); - using (var httpClient = mockHttp.ToHttpClient()) { + Func getTransaction = () => signhostApiClient.GetTransactionAsync("transactionId"); - var signhostApiClient = new SignHostApiClient(settings, httpClient); + var exception = await getTransaction.Should().ThrowAsync(); + exception.Which.Message.Should().Be("Not Found"); + exception.Which.ResponseBody.Should().Be(expectedResponseBody); - Func getTransaction = () => signhostApiClient.GetTransactionAsync("transaction Id"); + mockHttp.VerifyNoOutstandingExpectation(); + } - await getTransaction.Should().ThrowAsync().WithMessage("Not Found"); + [Fact] + public async Task When_GetTransaction_is_called_and_unkownerror_like_418_occures_Then_we_should_get_a_SignhostException() + { + MockHttpMessageHandler mockHttp = new(); + const string expectedResponseBody = """ + { + "message": "418 I'm a teapot" } + """; + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/transactionId") + .Respond((HttpStatusCode)418, new StringContent(expectedResponseBody)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); - [Fact] - public async Task when_GetTransaction_is_called_and_unkownerror_like_418_occures_then_we_should_get_a_SignhostException() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/transaction Id") - .Respond((HttpStatusCode)418, new StringContent("{ 'message': '418 I\\'m a teapot' }")); + SignhostApiClient signhostApiClient = new(settings, httpClient); - using (var httpClient = mockHttp.ToHttpClient()) { + Func getTransaction = () => signhostApiClient.GetTransactionAsync("transactionId"); + var exception = await getTransaction.Should().ThrowAsync(); + exception.Which.Message.Should().ContainAll("418"); + exception.Which.ResponseBody.Should().Be(expectedResponseBody); - var signhostApiClient = new SignHostApiClient(settings, httpClient); + mockHttp.VerifyNoOutstandingExpectation(); + } - Func getTransaction = () => signhostApiClient.GetTransactionAsync("transaction Id"); - await getTransaction.Should().ThrowAsync() - .WithMessage("*418*"); + [Fact] + public async Task When_GetTransaction_is_called_and_there_is_an_InternalServerError_Then_we_should_get_a_InternalServerErrorException() + { + MockHttpMessageHandler mockHttp = new(); + const string expectedResponseBody = """ + { + "message": "Internal Server Error" } + """; + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/transactionId") + .Respond(HttpStatusCode.InternalServerError, new StringContent(expectedResponseBody)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); - [Fact] - public async Task when_GetTransaction_is_called_and_there_is_an_InternalServerError_then_we_should_get_a_InternalServerErrorException() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/transaction Id") - .Respond(HttpStatusCode.InternalServerError, new StringContent("{ 'message': 'Internal Server Error' }")); + SignhostApiClient signhostApiClient = new(settings, httpClient); - using (var httpClient = mockHttp.ToHttpClient()) { + Func getTransaction = () => signhostApiClient.GetTransactionAsync("transactionId"); + var exception = await getTransaction.Should().ThrowAsync(); + exception.Which.ResponseBody.Should().Be(expectedResponseBody); - var signhostApiClient = new SignHostApiClient(settings, httpClient); + mockHttp.VerifyNoOutstandingExpectation(); + } - Func getTransaction = () => signhostApiClient.GetTransactionAsync("transaction Id"); - await getTransaction.Should().ThrowAsync(); - } + [Fact] + public async Task When_GetTransaction_is_called_on_gone_transaction_we_shoud_get_a_GoneException() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/transactionId") + .Respond(HttpStatusCode.Gone, new StringContent(JsonResources.GetTransaction)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - [Fact] - public async Task When_GetTransaction_is_called_on_gone_transaction_we_shoud_get_a_GoneException() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/transaction Id") - .Respond(HttpStatusCode.Gone, new StringContent(APIResponses.GetTransaction)); + Func getTransaction = () => signhostApiClient.GetTransactionAsync("transactionId"); + var exception = await getTransaction.Should().ThrowAsync>(); + exception.Which.ResponseBody.Should().Be(JsonResources.GetTransaction); - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); + mockHttp.VerifyNoOutstandingExpectation(); + } - Func getTransaction = () => signhostApiClient.GetTransactionAsync("transaction Id"); - await getTransaction.Should().ThrowAsync>(); - } + [Fact] + public async Task When_GetTransaction_is_called_and_gone_is_expected_we_should_get_a_transaction() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/transactionId") + .Respond(HttpStatusCode.Gone, new StringContent(JsonResources.GetTransaction)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - [Fact] - public async Task When_GetTransaction_is_called_and_gone_is_expected_we_should_get_a_transaction() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/transaction Id") - .Respond(HttpStatusCode.Gone, new StringContent(APIResponses.GetTransaction)); + Func getTransaction = () => signhostApiClient.GetTransactionResponseAsync("transactionId"); + await getTransaction.Should().NotThrowAsync(); - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); + mockHttp.VerifyNoOutstandingExpectation(); + } - Func getTransaction = () => signhostApiClient.GetTransactionResponseAsync("transaction Id"); - await getTransaction.Should().NotThrowAsync(); - } + [Fact] + public async Task When_a_CreateTransaction_is_called_Then_we_should_have_called_the_transaction_Post_once() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Post, "http://localhost/api/transaction") + .Respond(HttpStatusCode.OK, new StringContent(JsonResources.AddTransaction)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - [Fact] - public async Task when_a_CreateTransaction_is_called_then_we_should_have_called_the_transaction_Post_once() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Post, "http://localhost/api/transaction") - .Respond(HttpStatusCode.OK, new StringContent(APIResponses.AddTransaction)); + CreateSignerRequest testSigner = new() { + Email = "firstname.lastname@gmail.com", + }; - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); + CreateTransactionRequest testTransaction = new(); + testTransaction.Signers.Add(testSigner); - Signer testSigner = new Signer(); - testSigner.Email = "firstname.lastname@gmail.com"; + var result = await signhostApiClient.CreateTransactionAsync(testTransaction); + result.Id.Should().Be("c487be92-0255-40c7-bd7d-20805a65e7d9"); - Transaction testTransaction = new Transaction(); - testTransaction.Signers.Add(testSigner); + mockHttp.VerifyNoOutstandingExpectation(); + } - var result = await signhostApiClient.CreateTransactionAsync(testTransaction); - result.Id.Should().Be("c487be92-0255-40c7-bd7d-20805a65e7d9"); - } + [Fact] + public async Task When_a_CreateTransaction_is_called_we_can_add_custom_http_headers() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Post, "http://localhost/api/transaction") + .WithHeaders("X-Forwarded-For", "localhost") + .With(matcher => matcher.Headers.UserAgent.ToString().Contains("SignhostClientLibrary")) + .Respond(HttpStatusCode.OK, new StringContent(JsonResources.AddTransaction)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); + settings.AddHeader = ah => ah("X-Forwarded-For", "localhost"); - [Fact] - public async Task when_a_CreateTransaction_is_called_we_can_add_custom_http_headers() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Post, "http://localhost/api/transaction") - .WithHeaders("X-Forwarded-For", "localhost") - .With(matcher => matcher.Headers.UserAgent.ToString().Contains("SignhostClientLibrary")) - .Respond(HttpStatusCode.OK, new StringContent(APIResponses.AddTransaction)); + SignhostApiClient signhostApiClient = new(settings, httpClient); - using (var httpClient = mockHttp.ToHttpClient()) { - settings.AddHeader = (AddHeaders a) => a("X-Forwarded-For", "localhost"); + CreateTransactionRequest testTransaction = new(); - var signhostApiClient = new SignHostApiClient(settings, httpClient); + var result = await signhostApiClient.CreateTransactionAsync(testTransaction); - Transaction testTransaction = new Transaction(); + mockHttp.VerifyNoOutstandingExpectation(); + } - var result = await signhostApiClient.CreateTransactionAsync(testTransaction); + [Fact] + public async Task When_CreateTransaction_is_called_with_invalid_email_Then_we_should_get_a_BadRequestException() + { + MockHttpMessageHandler mockHttp = new(); + const string expectedResponseBody = """ + { + "message": "Bad Request" } + """; + mockHttp + .Expect(HttpMethod.Post, "http://localhost/api/transaction") + .WithHeaders("Content-Type", "application/json") + .Respond(HttpStatusCode.BadRequest, new StringContent(expectedResponseBody)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - [Fact] - public async Task when_CreateTransaction_is_called_with_invalid_email_then_we_should_get_a_BadRequestException() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Post, "http://localhost/api/transaction") - .WithHeaders("Content-Type", "application/json") - .Respond(HttpStatusCode.BadRequest, new StringContent(" { 'message': 'Bad Request' }")); + CreateSignerRequest testSigner = new() { + Email = "firstname.lastnamegmail.com", + }; - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); + CreateTransactionRequest testTransaction = new(); + testTransaction.Signers.Add(testSigner); - Signer testSigner = new Signer(); - testSigner.Email = "firstname.lastnamegmail.com"; + Func getTransaction = () => signhostApiClient.CreateTransactionAsync(testTransaction); + var exception = await getTransaction.Should().ThrowAsync(); + exception.Which.ResponseBody.Should().Be(expectedResponseBody); - Transaction testTransaction = new Transaction(); - testTransaction.Signers.Add(testSigner); + mockHttp.VerifyNoOutstandingExpectation(); + } - Func getTransaction = () => signhostApiClient.CreateTransactionAsync(testTransaction); - await getTransaction.Should().ThrowAsync(); + [Fact] + public async Task When_a_function_is_called_with_a_wrong_endpoint_we_should_get_a_SignhostRestApiClientException() + { + MockHttpMessageHandler mockHttp = new(); + const string expectedResponseBody = """ + { + "message": "Bad Gateway" } + """; + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/transactionId") + .Respond(HttpStatusCode.BadGateway, new StringContent(expectedResponseBody)); - mockHttp.VerifyNoOutstandingExpectation(); - } + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - [Fact] - public async Task when_a_function_is_called_with_a_wrong_endpoint_we_should_get_a_SignhostRestApiClientException() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/transaction Id") - .Respond(HttpStatusCode.BadGateway, new StringContent(" { 'Message': 'Bad Gateway' }")); - - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); + Func getTransaction = () => signhostApiClient.GetTransactionAsync("transactionId"); + var exception = await getTransaction.Should().ThrowAsync(); + exception.Which.Message.Should().Be("Bad Gateway"); + exception.Which.ResponseBody.Should().Be(expectedResponseBody); - Func getTransaction = () => signhostApiClient.GetTransactionAsync("transaction Id"); - await getTransaction.Should().ThrowAsync() - .WithMessage("Bad Gateway"); - } + mockHttp.VerifyNoOutstandingExpectation(); + } - mockHttp.VerifyNoOutstandingExpectation(); - } + [Fact] + public async Task When_a_DeleteTransaction_is_called_Then_we_should_have_called_the_transaction_delete_once() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Delete, "http://localhost/api/transaction/transactionId") + .Respond(HttpStatusCode.OK, new StringContent(JsonResources.DeleteTransaction)); - [Fact] - public async Task when_a_DeleteTransaction_is_called_then_we_should_have_called_the_transaction_delete_once() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Delete, "http://localhost/api/transaction/transaction Id") - .Respond(HttpStatusCode.OK, new StringContent(APIResponses.DeleteTransaction)); + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); + await signhostApiClient.DeleteTransactionAsync("transactionId"); - await signhostApiClient.DeleteTransactionAsync("transaction Id"); - } + mockHttp.VerifyNoOutstandingExpectation(); + } - mockHttp.VerifyNoOutstandingExpectation(); - } + [Fact] + public async Task When_a_DeleteTransaction_with_notification_is_called_Then_we_should_have_called_the_transaction_delete_once() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Delete, "http://localhost/api/transaction/transactionId") + .WithHeaders("Content-Type", "application/json") + //.With(matcher => matcher.Content.ToString().Contains("'SendNotifications': true")) + .Respond(HttpStatusCode.OK, new StringContent(JsonResources.DeleteTransaction)); - [Fact] - public async Task when_a_DeleteTransaction_with_notification_is_called_then_we_should_have_called_the_transaction_delete_once() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Delete, "http://localhost/api/transaction/transaction Id") - .WithHeaders("Content-Type", "application/json") - //.With(matcher => matcher.Content.ToString().Contains("'SendNotifications': true")) - .Respond(HttpStatusCode.OK, new StringContent(APIResponses.DeleteTransaction)); - - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); - - await signhostApiClient.DeleteTransactionAsync( - "transaction Id", - new DeleteTransactionOptions { SendNotifications = true }); - } + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - mockHttp.VerifyNoOutstandingExpectation(); - } + await signhostApiClient.DeleteTransactionAsync( + "transactionId", + new DeleteTransactionOptions { SendNotifications = true }); - [Fact] - public async Task when_AddOrReplaceFileToTransaction_is_called_then_we_should_have_called_the_file_put_once() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Put, "http://localhost/api/transaction/transaction Id/file/file Id") - .WithHeaders("Content-Type", "application/pdf") - .WithHeaders("Digest", "SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") - .Respond(HttpStatusCode.OK); - - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); - - // Create a 0 sized file - using (Stream file = System.IO.File.Create("unittestdocument.pdf")) { - await signhostApiClient.AddOrReplaceFileToTransaction(file, "transaction Id", "file Id"); - } - } + mockHttp.VerifyNoOutstandingExpectation(); + } - mockHttp.VerifyNoOutstandingExpectation(); - } + [Fact] + public async Task When_AddOrReplaceFileToTransaction_is_called_Then_we_should_have_called_the_file_put_once() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Put, "http://localhost/api/transaction/transactionId/file/fileId") + .WithHeaders("Content-Type", "application/pdf") + .WithHeaders("Digest", "SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") + .Respond(HttpStatusCode.OK); + + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); + + // Create a 0 sized file + using Stream file = File.Create("unittestdocument.pdf"); + await signhostApiClient.AddOrReplaceFileToTransactionAsync( + file, + "transactionId", + "fileId"); + + mockHttp.VerifyNoOutstandingExpectation(); + } - [Fact] - public async Task when_AddOrReplaceFileToTransaction_is_called_default_digest_is_sha256() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Put, "http://localhost/api/transaction/transaction Id/file/file Id") - .WithHeaders("Digest", "SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") - .Respond(HttpStatusCode.OK); + [Fact] + public async Task When_AddOrReplaceFileToTransaction_is_called_default_digest_is_sha256() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Put, "http://localhost/api/transaction/transactionId/file/fileId") + .WithHeaders("Digest", "SHA-256=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=") + .Respond(HttpStatusCode.OK); - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - await signhostApiClient.AddOrReplaceFileToTransaction(new MemoryStream(), "transaction Id", "file Id"); - } + await signhostApiClient.AddOrReplaceFileToTransactionAsync( + new MemoryStream(), + "transactionId", + "fileId"); - mockHttp.VerifyNoOutstandingExpectation(); - } + mockHttp.VerifyNoOutstandingExpectation(); + } - [Fact] - public async Task when_AddOrReplaceFileToTransaction_with_sha512_is_called_default_digest_is_sha512() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Put, "http://localhost/api/transaction/transaction Id/file/file Id") - .WithHeaders("Digest", "SHA-512=z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==") - .Respond(HttpStatusCode.OK); - - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); - - await signhostApiClient.AddOrReplaceFileToTransactionAsync( - new MemoryStream(), - "transaction Id", - "file Id", - new FileUploadOptions{ - DigestOptions = new FileDigestOptions - { - DigestHashAlgorithm = "SHA-512" - } - }); + [Fact] + public async Task When_AddOrReplaceFileToTransaction_with_sha512_is_called_default_digest_is_sha512() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Put, "http://localhost/api/transaction/transactionId/file/fileId") + .WithHeaders( + "Digest", + "SHA-512=z4PhNX7vuL3xVChQ1m2AB9Yg5AULVxXcg/SpIdNs6c5H0NE8XYXysP+DGNKHfuwvY7kxvUdBeoGlODJ6+SfaPg==" + ) + .Respond(HttpStatusCode.OK); + + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); + + await signhostApiClient.AddOrReplaceFileToTransactionAsync( + new MemoryStream(), + "transactionId", + "fileId", + new FileUploadOptions { + DigestOptions = new FileDigestOptions { + DigestHashAlgorithm = DigestHashAlgorithm.SHA512, + }, } + ); - mockHttp.VerifyNoOutstandingExpectation(); - } + mockHttp.VerifyNoOutstandingExpectation(); + } - [Fact] - public async Task when_AddOrReplaceFileToTransaction_with_digest_value_is_used_as_is() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Put, "http://localhost/api/transaction/transaction Id/file/file Id") - .WithHeaders("Digest", "SHA-1=AAEC") - .Respond(HttpStatusCode.OK); - - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); - - await signhostApiClient.AddOrReplaceFileToTransactionAsync( - new MemoryStream(), - "transaction Id", - "file Id", - new FileUploadOptions - { - DigestOptions = new FileDigestOptions - { - DigestHashAlgorithm = "SHA-1", - DigestHashValue = new byte[] { 0x00, 0x01, 0x02 } - } - }); + [Fact] + public async Task When_AddOrReplaceFileToTransaction_with_digest_value_is_used_as_is() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Put, "http://localhost/api/transaction/transactionId/file/fileId") + .WithHeaders("Digest", "SHA-256=AAEC") + .Respond(HttpStatusCode.OK); + + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); + + await signhostApiClient.AddOrReplaceFileToTransactionAsync( + new MemoryStream(), + "transactionId", + "fileId", + new FileUploadOptions { + DigestOptions = new FileDigestOptions { + DigestHashAlgorithm = DigestHashAlgorithm.SHA256, + DigestHashValue = [0x00, 0x01, 0x02], + }, } + ); - mockHttp.VerifyNoOutstandingExpectation(); - } + mockHttp.VerifyNoOutstandingExpectation(); + } - [Fact] - public async Task when_StartTransaction_is_called_then_we_should_have_called_the_transaction_put_once() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp.Expect("http://localhost/api/transaction/transaction Id/start") - .Respond(HttpStatusCode.NoContent); + [Fact] + public async Task When_StartTransaction_is_called_Then_we_should_have_called_the_transaction_put_once() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp.Expect("http://localhost/api/transaction/transactionId/start") + .Respond(HttpStatusCode.NoContent); - using (var httpClient = mockHttp.ToHttpClient()) { + using var httpClient = mockHttp.ToHttpClient(); - var signhostApiClient = new SignHostApiClient(settings, httpClient); + SignhostApiClient signhostApiClient = new(settings, httpClient); - await signhostApiClient.StartTransactionAsync("transaction Id"); - } + await signhostApiClient.StartTransactionAsync("transactionId"); - mockHttp.VerifyNoOutstandingExpectation(); - } + mockHttp.VerifyNoOutstandingExpectation(); + } - [Fact] - public async Task when_GetReceipt_is_called_then_we_should_have_called_the_filereceipt_get_once() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp.Expect("http://localhost/api/file/receipt/transaction ID") - .Respond(HttpStatusCode.OK); + [Fact] + public async Task When_GetReceipt_is_called_Then_we_should_have_called_the_filereceipt_get_once() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp.Expect("http://localhost/api/file/receipt/transactionId") + .Respond(HttpStatusCode.OK); - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - var receipt = await signhostApiClient.GetReceiptAsync("transaction ID"); - } + var receipt = await signhostApiClient.GetReceiptAsync("transactionId"); - mockHttp.VerifyNoOutstandingExpectation(); - } + mockHttp.VerifyNoOutstandingExpectation(); + } - [Fact] - public async Task when_GetDocument_is_called_then_we_should_have_called_the_file_get_once() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp.Expect(HttpMethod.Get, "http://localhost/api/transaction/*/file/file Id") - .Respond(HttpStatusCode.OK, new StringContent(string.Empty)); + [Fact] + public async Task When_GetDocument_is_called_Then_we_should_have_called_the_file_get_once() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp.Expect(HttpMethod.Get, "http://localhost/api/transaction/*/file/fileId") + .Respond(HttpStatusCode.OK, new StringContent(string.Empty)); - using (var httpClient = mockHttp.ToHttpClient()) { + using var httpClient = mockHttp.ToHttpClient(); - var signhostApiClient = new SignHostApiClient(settings, httpClient); + SignhostApiClient signhostApiClient = new(settings, httpClient); - var document = await signhostApiClient.GetDocumentAsync("transaction Id", "file Id"); - } + var document = await signhostApiClient.GetDocumentAsync("transactionId", "fileId"); - mockHttp.VerifyNoOutstandingExpectation(); - } + mockHttp.VerifyNoOutstandingExpectation(); + } - [Fact] - public async Task When_a_transaction_json_is_returned_it_is_deserialized_correctly() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp.Expect(HttpMethod.Post, "http://localhost/api/transaction") - .Respond(HttpStatusCode.OK, new StringContent(RequestBodies.TransactionSingleSignerJson)); - - using (var httpClient = mockHttp.ToHttpClient()) { - - var signhostApiClient = new SignHostApiClient(settings, httpClient); - - var result = await signhostApiClient.CreateTransactionAsync(new Transaction - { - Signers = new List{ - new Signer - { - Verifications = new List - { - new PhoneNumberVerification - { - Number = "31615087075" - } + [Fact] + public async Task When_a_transaction_json_is_returned_it_is_deserialized_correctly() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Post, "http://localhost/api/transaction") + .Respond( + HttpStatusCode.OK, + new StringContent(JsonResources.TransactionSingleSignerJson) + ); + + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); + + var result = await signhostApiClient.CreateTransactionAsync(new() { + Signers = [ + new() { + Email = "test@example.com", + Verifications = [ + new PhoneNumberVerification { + Number = "31615087075" } - } + ] } - }); - - result.Id.Should().Be("50262c3f-9744-45bf-a4c6-8a3whatever"); - result.CancelledDateTime.Should().HaveYear(2017); - result.Status.Should().Be(TransactionStatus.WaitingForDocument); - result.Signers.Should().HaveCount(1); - result.Receivers.Should().HaveCount(0); - result.Reference.Should().Be("Contract #123"); - result.SignRequestMode.Should().Be(2); - result.DaysToExpire.Should().Be(14); - result.Signers[0].Id.Should().Be("Signer1"); - result.Signers[0].Email.Should().Be("test1@example.com"); - result.Signers[0].Verifications.Should().HaveCount(1); - result.Signers[0].Verifications[0].Should().BeOfType() - .And.Subject.Should().BeEquivalentTo(new PhoneNumberVerification { - Number = "+31615123456" - }); - result.Signers[0].Activities.Should().HaveCount(3); - result.Signers[0].Activities[0].Should().BeEquivalentTo(new Activity - { - Id = "Activity1", - Code = ActivityType.Opened, - CreatedDateTime = DateTimeOffset.Parse("2017-05-31T22:15:17.6409005+02:00") - }); - } - - mockHttp.VerifyNoOutstandingExpectation(); - } + ] + }); + + result.Id.Should().Be("50262c3f-9744-45bf-a4c6-8a3whatever"); + result.CanceledDateTime.Should().HaveYear(2017); + result.Status.Should().Be(TransactionStatus.WaitingForDocument); + result.Signers.Should().HaveCount(1); + result.Receivers.Should().HaveCount(0); + result.Reference.Should().Be("Contract #123"); + result.SignRequestMode.Should().Be(2); + result.DaysToExpire.Should().Be(14); + result.Signers[0].Id.Should().Be("Signer1"); + result.Signers[0].Email.Should().Be("test1@example.com"); + result.Signers[0].Verifications.Should().HaveCount(1); + result.Signers[0].Verifications[0].Should().BeOfType() + .And.Subject.Should().BeEquivalentTo(new PhoneNumberVerification { + Number = "+31615123456" + }); + result.Signers[0].Activities.Should().HaveCount(3); + result.Signers[0].Activities[0].Should().BeEquivalentTo(new Activity { + Id = "Activity1", + Code = ActivityType.Opened, + CreatedDateTime = DateTimeOffset.Parse("2017-05-31T22:15:17.6409005+02:00") + }); + + mockHttp.VerifyNoOutstandingExpectation(); + } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task When_a_complete_transaction_flow_is_created_headers_are_not_set_multiple_times( - bool isOauth) + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task When_a_complete_transaction_flow_is_created_headers_are_not_set_multiple_times( + bool isOauth) + { + MockedRequest AddHeaders(MockedRequest request) { - MockedRequest AddHeaders(MockedRequest request) - { - if (!isOauth) { - request = request.WithHeaders("Authorization", "APIKey Usertoken"); - } - - return request - .WithHeaders("Application", "APPKey AppKey") - .WithHeaders("X-Custom", "test"); - } - - var mockHttp = new MockHttpMessageHandler(); - AddHeaders(mockHttp.Expect(HttpMethod.Post, "http://localhost/api/transaction")) - .Respond(new StringContent(RequestBodies.TransactionSingleSignerJson)); - AddHeaders(mockHttp.Expect(HttpMethod.Put, "http://localhost/api/transaction/*/file/somefileid")) - .Respond(HttpStatusCode.Accepted, new StringContent(RequestBodies.AddOrReplaceFileMetaToTransaction)); - AddHeaders(mockHttp.Expect(HttpMethod.Put, "http://localhost/api/transaction/*/file/somefileid")) - .Respond(HttpStatusCode.Created); - AddHeaders(mockHttp.Expect(HttpMethod.Put, "http://localhost/api/transaction/*/start")) - .Respond(HttpStatusCode.NoContent); - - using (var httpClient = mockHttp.ToHttpClient()) { - var clientSettings = isOauth ? oauthSettings : settings; - clientSettings.AddHeader = add => add("X-Custom", "test"); - var signhostApiClient = new SignHostApiClient(clientSettings, httpClient); - - var result = await signhostApiClient.CreateTransactionAsync(new Transaction()); - await signhostApiClient.AddOrReplaceFileMetaToTransactionAsync(new FileMeta(), result.Id, "somefileid"); - using (Stream file = System.IO.File.Create("unittestdocument.pdf")) { - await signhostApiClient.AddOrReplaceFileToTransaction(file, result.Id, "somefileid"); - } - await signhostApiClient.StartTransactionAsync(result.Id); + if (!isOauth) { + request = request.WithHeaders("Authorization", "APIKey Usertoken"); } - mockHttp.VerifyNoOutstandingExpectation(); - mockHttp.VerifyNoOutstandingRequest(); - } - - [Fact] - public async Task When_a_custom_verificationtype_is_provided_it_is_deserialized_correctly() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/c487be92-0255-40c7-bd7d-20805a65e7d9") - .Respond(new StringContent(APIResponses.GetTransactionCustomVerificationType)); - - SignHostApiClient.RegisterVerification(); - - using (var httpClient = mockHttp.ToHttpClient()) { - var signhostApiClient = new SignHostApiClient(settings, httpClient); - - var result = await signhostApiClient.GetTransactionAsync("c487be92-0255-40c7-bd7d-20805a65e7d9"); - - result.Signers[0].Verifications.Should().HaveCount(3); - result.Signers[0].Verifications[0].Should().BeOfType(); - result.Signers[0].Verifications[1].Should().BeOfType() - .Which.IPAddress.Should().Be("127.0.0.33"); - result.Signers[0].Verifications[2].Should().BeOfType() - .Which.Number.Should().Be("123"); - } + return request + .WithHeaders("Application", "APPKey AppKey") + .WithHeaders("X-Custom", "test"); } - [Fact] - public async Task When_a_minimal_response_is_retrieved_list_and_dictionaries_are_not_null() - { - var mockHttp = new MockHttpMessageHandler(); - mockHttp - .Expect(HttpMethod.Get, "http://localhost/api/transaction/c487be92-0255-40c7-bd7d-20805a65e7d9") - .Respond(new StringContent(APIResponses.MinimalTransactionResponse)); - - SignHostApiClient.RegisterVerification(); + MockHttpMessageHandler mockHttp = new(); + AddHeaders(mockHttp.Expect(HttpMethod.Post, "http://localhost/api/transaction")) + .Respond(new StringContent(JsonResources.TransactionSingleSignerJson)); + AddHeaders(mockHttp.Expect(HttpMethod.Put, "http://localhost/api/transaction/*/file/somefileid")) + .Respond(HttpStatusCode.Accepted, new StringContent(JsonResources.AddOrReplaceFileMetaToTransaction)); + AddHeaders(mockHttp.Expect(HttpMethod.Put, "http://localhost/api/transaction/*/file/somefileid")) + .Respond(HttpStatusCode.Created); + AddHeaders(mockHttp.Expect(HttpMethod.Put, "http://localhost/api/transaction/*/start")) + .Respond(HttpStatusCode.NoContent); + + using var httpClient = mockHttp.ToHttpClient(); + var clientSettings = isOauth ? oauthSettings : settings; + clientSettings.AddHeader = add => add("X-Custom", "test"); + SignhostApiClient signhostApiClient = new(clientSettings, httpClient); + + var result = await signhostApiClient + .CreateTransactionAsync(new CreateTransactionRequest()); + await signhostApiClient + .AddOrReplaceFileMetaToTransactionAsync(new FileMeta(), result.Id, "somefileid"); + + using Stream file = File.Create("unittestdocument.pdf"); + await signhostApiClient.AddOrReplaceFileToTransactionAsync(file, result.Id, "somefileid"); + await signhostApiClient.StartTransactionAsync(result.Id); + + mockHttp.VerifyNoOutstandingExpectation(); + mockHttp.VerifyNoOutstandingRequest(); + } - using (var httpClient = mockHttp.ToHttpClient()) - { - var signhostApiClient = new SignHostApiClient(settings, httpClient); + [Fact] + public async Task When_a_minimal_response_is_retrieved_list_and_dictionaries_are_not_null() + { + MockHttpMessageHandler mockHttp = new(); + mockHttp + .Expect(HttpMethod.Get, "http://localhost/api/transaction/c487be92-0255-40c7-bd7d-20805a65e7d9") + .Respond(new StringContent(JsonResources.MinimalTransactionResponse)); - var result = await signhostApiClient.GetTransactionAsync("c487be92-0255-40c7-bd7d-20805a65e7d9"); + using var httpClient = mockHttp.ToHttpClient(); + SignhostApiClient signhostApiClient = new(settings, httpClient); - result.Signers.Should().BeEmpty(); - result.Receivers.Should().BeEmpty(); - result.Files.Should().BeEmpty(); - } - } + var result = await signhostApiClient.GetTransactionAsync("c487be92-0255-40c7-bd7d-20805a65e7d9"); - public class CustomVerification - : IVerification - { - public string Type => "CustomVerificationType"; - } + result.Signers.Should().BeEmpty(); + result.Receivers.Should().BeEmpty(); + result.Files.Should().BeEmpty(); } } diff --git a/src/SignhostAPIClient.Tests/SignhostApiReceiverTests.cs b/src/SignhostAPIClient.Tests/SignhostApiReceiverTests.cs index 79099cc4..81b6bcb4 100644 --- a/src/SignhostAPIClient.Tests/SignhostApiReceiverTests.cs +++ b/src/SignhostAPIClient.Tests/SignhostApiReceiverTests.cs @@ -1,84 +1,90 @@ -using System; -using System.Threading.Tasks; -using System.Net.Http; -using System.IO; -using Xunit; +using Xunit; using Signhost.APIClient.Rest.DataObjects; using FluentAssertions; using System.Collections.Generic; -using RichardSzalay.MockHttp; -using System.Net; +using SignhostAPIClient.Tests.JSON; -namespace Signhost.APIClient.Rest.Tests +namespace Signhost.APIClient.Rest.Tests; + +public class SignhostApiReceiverTests { - public class SignhostApiReceiverTests + private readonly SignhostApiReceiverSettings receiverSettings = + new("SharedSecret"); + + [Fact] + public void When_IsPostbackChecksumValid_is_called_with_valid_postback_in_body_Then_true_is_returned() + { + // Arrange + var headers = new Dictionary { + ["Content-Type"] = ["application/json"] + }; + + string body = JsonResources.MockPostbackValid; + + // Act + SignhostApiReceiver signhostApiReceiver = new(receiverSettings); + bool result = signhostApiReceiver + .IsPostbackChecksumValid(headers, body, out Transaction _); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void When_IsPostbackChecksumValid_is_called_with_invalid_postback_in_body_Then_false_is_returned() + { + // Arrange + var headers = new Dictionary { + ["Content-Type"] = ["application/json"] + }; + + string body = JsonResources.MockPostbackInvalid; + + // Act + SignhostApiReceiver signhostApiReceiver = new(receiverSettings); + bool result = signhostApiReceiver + .IsPostbackChecksumValid(headers, body, out Transaction _); + + // Assert + result.Should().BeFalse(); + } + + [Fact] + public void When_IsPostbackChecksumValid_is_called_with_valid_postback_in_header_Then_true_is_returned() { - private SignhostApiReceiverSettings receiverSettings = new SignhostApiReceiverSettings("SharedSecret"); - - [Fact] - public void when_IsPostbackChecksumValid_is_called_with_valid_postback_in_body_then_true_is_returned() - { - // Arrange - IDictionary headers = new Dictionary { { "Content-Type", new[] { "application/json" } } }; - string body = RequestBodies.MockPostbackValid; - - // Act - SignhostApiReceiver signhostApiReceiver = new SignhostApiReceiver(receiverSettings); - bool result = signhostApiReceiver.IsPostbackChecksumValid(headers, body, out Transaction transaction); - - // Assert - result.Should().BeTrue(); - } - - [Fact] - public void when_IsPostbackChecksumValid_is_called_with_invalid_postback_in_body_then_false_is_returned() - { - // Arrange - IDictionary headers = new Dictionary { { "Content-Type", new[] { "application/json" } } }; - string body = RequestBodies.MockPostbackInvalid; - - // Act - SignhostApiReceiver signhostApiReceiver = new SignhostApiReceiver(receiverSettings); - bool result = signhostApiReceiver.IsPostbackChecksumValid(headers, body, out Transaction transaction); - - // Assert - result.Should().BeFalse(); - } - - [Fact] - public void when_IsPostbackChecksumValid_is_called_with_valid_postback_in_header_then_true_is_returned() - { - // Arrange - IDictionary headers = new Dictionary { - { "Content-Type", new[] { "application/json" }}, - {"Checksum", new[] {"cdc09eee2ed6df2846dcc193aedfef59f2834f8d"}} - }; - string body = RequestBodies.MockPostbackValid; - - // Act - SignhostApiReceiver signhostApiReceiver = new SignhostApiReceiver(receiverSettings); - bool result = signhostApiReceiver.IsPostbackChecksumValid(headers, body, out Transaction transaction); - - // Assert - result.Should().BeTrue(); - } - - [Fact] - public void when_IsPostbackChecksumValid_is_called_with_invalid_postback_in_header_then_false_is_returned() - { - // Arrange - IDictionary headers = new Dictionary { - { "Content-Type", new[] { "application/json" }}, - {"Checksum", new[] {"70dda90616f744797972c0d2f787f86643a60c83"}} - }; - string body = RequestBodies.MockPostbackValid; - - // Act - SignhostApiReceiver signhostApiReceiver = new SignhostApiReceiver(receiverSettings); - bool result = signhostApiReceiver.IsPostbackChecksumValid(headers, body, out Transaction transaction); - - // Assert - result.Should().BeFalse(); - } + // Arrange + var headers = new Dictionary { + ["Content-Type"] = ["application/json"], + ["Checksum"] = ["cdc09eee2ed6df2846dcc193aedfef59f2834f8d"] + }; + + string body = JsonResources.MockPostbackValid; + + // Act + SignhostApiReceiver signhostApiReceiver = new(receiverSettings); + bool result = signhostApiReceiver + .IsPostbackChecksumValid(headers, body, out Transaction _); + + // Assert + result.Should().BeTrue(); + } + + [Fact] + public void When_IsPostbackChecksumValid_is_called_with_invalid_postback_in_header_Then_false_is_returned() + { + // Arrange + var headers = new Dictionary { + ["Content-Type"] = ["application/json"], + ["Checksum"] = ["70dda90616f744797972c0d2f787f86643a60c83"] + }; + string body = JsonResources.MockPostbackValid; + + // Act + SignhostApiReceiver signhostApiReceiver = new(receiverSettings); + bool result = signhostApiReceiver + .IsPostbackChecksumValid(headers, body, out Transaction _); + + // Assert + result.Should().BeFalse(); } } diff --git a/src/SignhostAPIClient.Tests/app.config b/src/SignhostAPIClient.Tests/app.config deleted file mode 100644 index de5386a4..00000000 --- a/src/SignhostAPIClient.Tests/app.config +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/src/SignhostAPIClient.sln b/src/SignhostAPIClient.sln index 1dc1d2d9..fdd4c330 100644 --- a/src/SignhostAPIClient.sln +++ b/src/SignhostAPIClient.sln @@ -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 @@ -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 diff --git a/src/SignhostAPIClient.v2.ncrunchsolution b/src/SignhostAPIClient.v2.ncrunchsolution deleted file mode 100644 index b98737f1..00000000 --- a/src/SignhostAPIClient.v2.ncrunchsolution +++ /dev/null @@ -1,14 +0,0 @@ - - 1 - false - false - true - UseDynamicAnalysis - UseStaticAnalysis - UseStaticAnalysis - UseStaticAnalysis - UseDynamicAnalysis - - - - \ No newline at end of file diff --git a/src/SignhostAPIClient.v3.ncrunchsolution b/src/SignhostAPIClient.v3.ncrunchsolution deleted file mode 100644 index ff7af7e3..00000000 --- a/src/SignhostAPIClient.v3.ncrunchsolution +++ /dev/null @@ -1,6 +0,0 @@ - - - True - True - - diff --git a/src/SignhostAPIClient/NotNullWhenAttribute.cs b/src/SignhostAPIClient/NotNullWhenAttribute.cs new file mode 100644 index 00000000..c222fdbf --- /dev/null +++ b/src/SignhostAPIClient/NotNullWhenAttribute.cs @@ -0,0 +1,18 @@ +#if NETFRAMEWORK || NETSTANDARD2_0 +using System; + +namespace Signhost.APIClient.Rest; + +public sealed class NotNullWhenAttribute + : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} +#endif diff --git a/src/SignhostAPIClient/Rest/ApiResponse.cs b/src/SignhostAPIClient/Rest/ApiResponse.cs index f6c6c128..0752cb3f 100644 --- a/src/SignhostAPIClient/Rest/ApiResponse.cs +++ b/src/SignhostAPIClient/Rest/ApiResponse.cs @@ -1,32 +1,41 @@ -using System; -using System.Collections.Generic; -using System.Net; +using System.Net; using System.Net.Http; -using System.Text; +using System.Threading; +using System.Threading.Tasks; -namespace Signhost.APIClient.Rest +namespace Signhost.APIClient.Rest; + +public class ApiResponse { - public class ApiResponse + private readonly HttpResponseMessage httpResponse; + + public ApiResponse(HttpResponseMessage httpResponse, TValue? value) { - private readonly HttpResponseMessage httpResponse; + httpResponse.ThrowIfNullOrEmpty(nameof(httpResponse)); - public ApiResponse(HttpResponseMessage httpResponse, TValue value) - { - this.httpResponse = httpResponse; - this.Value = value; - } + this.httpResponse = httpResponse; + Value = value; + } - public TValue Value { get; private set; } + public TValue? Value { get; private set; } - public HttpStatusCode HttpStatusCode => httpResponse.StatusCode; + public HttpStatusCode HttpStatusCode => httpResponse.StatusCode; - public void EnsureAvailableStatusCode() - { - if (HttpStatusCode == HttpStatusCode.Gone) { - throw new ErrorHandling.GoneException( - httpResponse.ReasonPhrase, - Value); - } + public async Task EnsureAvailableStatusCodeAsync( + CancellationToken cancellationToken = default) + { + if (HttpStatusCode == HttpStatusCode.Gone) { + throw new ErrorHandling.GoneException( + httpResponse.ReasonPhrase ?? "No reason phrase provided", + Value) { + ResponseBody = await httpResponse.Content +#if NETFRAMEWORK || NETSTANDARD2_0 + .ReadAsStringAsync() +#else + .ReadAsStringAsync(cancellationToken) +#endif + .ConfigureAwait(false), + }; } } } diff --git a/src/SignhostAPIClient/Rest/DataObjects/Activity.cs b/src/SignhostAPIClient/Rest/DataObjects/Activity.cs index dccf8caa..86f4453f 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/Activity.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/Activity.cs @@ -1,15 +1,18 @@ -using System; +using System; +using System.Text.Json.Serialization; -namespace Signhost.APIClient.Rest.DataObjects +namespace Signhost.APIClient.Rest.DataObjects; + +public class Activity { - public class Activity - { - public string Id { get; set; } + public string Id { get; set; } = default!; + + public ActivityType Code { get; set; } - public ActivityType Code { get; set; } + [JsonPropertyName("Activity")] + public string? ActivityValue { get; set; } - public string Info { get; set; } + public string? Info { get; set; } - public DateTimeOffset CreatedDateTime { get; set; } - } + public DateTimeOffset CreatedDateTime { get; set; } } diff --git a/src/SignhostAPIClient/Rest/DataObjects/ActivityType.cs b/src/SignhostAPIClient/Rest/DataObjects/ActivityType.cs index e954356a..2ef5d2a7 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/ActivityType.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/ActivityType.cs @@ -1,139 +1,79 @@ -using System; -using System.Collections.Generic; -using System.Text; +namespace Signhost.APIClient.Rest.DataObjects; -namespace Signhost.APIClient.Rest.DataObjects +/// +/// type codes as defined in the Signhost API. +/// +public enum ActivityType { /// - /// type. + /// Invitation sent (code 101). /// - public enum ActivityType - { - /// - /// The invitation mail was sent. - /// - InvitationSent = 101, + InvitationSent = 101, - /// - /// The invitation mail was received. - /// - InvitationReceived = 102, - - /// - /// The sign url was opened. - /// - Opened = 103, - - /// - /// An invitation reminder mail was sent. - /// - InvitationReminderResent = 104, - - /// - /// The document was opened. - /// The contains the fileId of the opened - /// document. - /// - DocumentOpened = 105, - - /// - /// Consumer Signing identity approved. - /// - IdentityApproved = 110, - - /// - /// Consumer Signing identity failed. - /// - IdentityFailed = 111, - - /// - /// Cancelled. - /// - Cancelled = 201, - - /// - /// The signer rejected the sign request. - /// - Rejected = 202, - - /// - /// The signer signed the documents. - /// - Signed = 203, - - /// - /// The signer delegated signing to a different signer. - /// - SignerDelegated = 204, - - /// - /// Signed document sent. - /// - SignedDocumentSent = 301, - - /// - /// Signed document opened. - /// - SignedDocumentOpened = 302, + /// + /// Sign URL was opened (code 103). + /// + Opened = 103, - /// - /// Signed document downloaded. - /// - SignedDocumentDownloaded = 303, + /// + /// Invitation reminder sent (code 104). + /// + InvitationReminderResent = 104, - /// - /// Receipt sent. - /// - ReceiptSent = 401, + /// + /// Document was opened. + /// The property contains the file ID of + /// the opened document (code 105). + /// + DocumentOpened = 105, - /// - /// Receipt opened. - /// - ReceiptOpened = 402, + /// + /// The signer rejected the sign request (code 202). + /// + Rejected = 202, - /// - /// Receipt downloaded. - /// - ReceiptDownloaded = 403, + /// + /// The signer signed the documents (code 203). + /// + Signed = 203, - /// - /// Finished. - /// - Finished = 500, + /// + /// The signer delegated signing to a different signer (code 204). + /// + SignerDelegated = 204, - /// - /// Deleted. - /// - Deleted = 600, + /// + /// Signed document sent (code 301). + /// + SignedDocumentSent = 301, - /// - /// Expired. - /// - Expired = 700, + /// + /// Signed document opened (code 302). + /// + SignedDocumentOpened = 302, - /// - /// Email bounce - hard. - /// - EmailBounceHard = 901, + /// + /// Signed document downloaded (code 303). + /// + SignedDocumentDownloaded = 303, - /// - /// Email bounce - soft. - /// - EmailBounceSoft = 902, + /// + /// Receipt sent (code 401). + /// + ReceiptSent = 401, - /// - /// Email bounce - blocked. - /// - EmailBounceBlocked = 903, + /// + /// Receipt opened (code 402). + /// + ReceiptOpened = 402, - /// - /// Email bounce - undetermined. - /// - EmailBounceUndetermined = 904, + /// + /// Receipt downloaded (code 403). + /// + ReceiptDownloaded = 403, - /// - /// Operation failed. - /// - Failed = 999, - } + /// + /// Transaction failed due to this entity, e.g. email bounce (code 999). + /// + Failed = 999, } diff --git a/src/SignhostAPIClient/Rest/DataObjects/ConsentVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/ConsentVerification.cs deleted file mode 100644 index 18c1cfd0..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/ConsentVerification.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Signhost.APIClient.Rest.DataObjects -{ - /// - /// Adds a consent verification screen - /// - public class ConsentVerification - : IVerification - { - /// - /// Gets the . - /// - public string Type { get; } = "Consent"; - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/CreateReceiverRequest.cs b/src/SignhostAPIClient/Rest/DataObjects/CreateReceiverRequest.cs new file mode 100644 index 00000000..59a82f0c --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/CreateReceiverRequest.cs @@ -0,0 +1,47 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// Request object for creating a receiver in a transaction. +/// Receiver configuration for getting copies of signed documents. +/// +public class CreateReceiverRequest +{ + /// + /// Gets or sets the receiver's name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the receiver's email address (required). + /// + public string Email { get; set; } = default!; + + /// + /// Gets or sets the language for receiver communications. + /// Supported values: de-DE, en-US, es-ES, fr-FR, it-IT, pl-PL, nl-NL. + /// Default is nl-NL. + /// + public string? Language { get; set; } + + /// + /// Gets or sets the custom subject for notification email. + /// Maximum of 64 characters allowed. Omitting this parameter will enable the default subject. + /// + public string? Subject { get; set; } + + /// + /// Gets or sets the custom message for notification email (required). + /// Newlines can be created by including a \n in the message. HTML is not allowed. + /// + public string Message { get; set; } = default!; + + /// + /// Gets or sets the custom reference for this receiver. + /// + public string? Reference { get; set; } + + /// + /// Gets or sets the custom receiver data (JSON object only). + /// + public dynamic? Context { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/CreateSignerRequest.cs b/src/SignhostAPIClient/Rest/DataObjects/CreateSignerRequest.cs new file mode 100644 index 00000000..20c2f1a2 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/CreateSignerRequest.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; + +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// Request object for creating a new signer in a transaction. Defines the signer's identity, +/// authentication/verification requirements, notification preferences, and signing behavior. +/// +/// +/// Key Requirements: +/// +/// Email address is mandatory +/// Either Authentications or Verifications must be provided (or both) +/// Signers with AllowDelegation enabled cannot have Authentications +/// SendSignRequest determines if sign request emails are sent automatically +/// +/// +public class CreateSignerRequest +{ + /// + /// Gets or sets the signer identifier (will be generated if not provided). + /// + public string? Id { get; set; } + + /// + /// Gets or sets when the signer's access expires. + /// + public DateTimeOffset? Expires { get; set; } + + /// + /// Gets or sets the signer's email address (required). + /// + public string Email { get; set; } = default!; + + /// + /// Gets or sets the list of authentication methods that the signer has to authenticate with. + /// The order in which the authentications are provided determine in which order the signer will have to perform the specified method. + /// Authentications must be performed before the document(s) can be viewed. + /// + public IList? Authentications { get; set; } + + /// + /// Gets or sets the list of verification methods that the signer has to verify with. + /// The order in which the verifications are provided determine in which order the signer will have to perform the specified method. + /// Verifications must be performed before the document(s) can be signed. + /// + /// + /// Critical Requirement: You must use one of the following verification methods as the last verification in the list: + /// + /// Consent + /// PhoneNumber + /// Scribble + /// CSC Qualified* + /// + /// Important Notes: + /// + /// CSC Qualified must always be the final verification if used + /// The other three methods (Consent, PhoneNumber, Scribble) can be succeeded by other verification methods + /// + /// + public IList? Verifications { get; set; } + + /// + /// Gets or sets a value indicating whether to send sign request to this signer's email address. + /// Default is true. + /// + public bool SendSignRequest { get; set; } = true; + + /// + /// Gets or sets a value indicating whether to send a confirmation email to the signer after signing. + /// Default value is the value of . + /// + public bool? SendSignConfirmation { get; set; } + + /// + /// Gets or sets the subject of the sign request email in plain text. + /// Maximum of 64 characters allowed. If omitted, the default subject will be used. + /// + public string? SignRequestSubject { get; set; } + + /// + /// Gets or sets the message of the sign request email in plain text. HTML is not allowed. + /// Newlines can be created by including a \n. + /// Required if is true. + /// + public string? SignRequestMessage { get; set; } + + /// + /// Gets or sets the number of days between automatic reminder emails sent to this signer. + /// Set to -1 to disable reminders entirely for this signer. + /// Set to 0 to use your organization's default reminder interval. + /// Set to a positive number (e.g., 3, 7) to send reminders every N days. + /// Default is 7. + /// + /// + /// Reminders are only sent if is true and the signer hasn't completed signing yet. + /// + public int? DaysToRemind { get; set; } + + /// + /// Gets or sets the language for signer interface and emails. + /// Supported values: de-DE, en-US, es-ES, fr-FR, it-IT, pl-PL, nl-NL. + /// Default is nl-NL. + /// + public string? Language { get; set; } + + /// + /// Gets or sets the custom reference for this signer. + /// + public string? Reference { get; set; } + + /// + /// Gets or sets the custom introduction text shown to the signer during the signing process. + /// This will be shown on the first screen to the signer and supports limited markdown markup. + /// + /// + /// The following markup is supported: + /// + /// # Headings + /// *Emphasis* / _Emphasis_ + /// **Strong** / __Strong__ + /// 1. Ordered and - Unordered lists + /// + /// + public string? IntroText { get; set; } + + /// + /// Gets or sets the URL to redirect signer after signing. + /// Default is https://signhost.com. + /// + public string? ReturnUrl { get; set; } + + /// + /// Gets or sets a value indicating whether this signer can delegate signing to another person. + /// Cannot be used together with . + /// Default is false. + /// + public bool AllowDelegation { get; set; } + + /// + /// Gets or sets the custom signer data (dynamic JSON object). + /// + public dynamic? Context { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/CreateTransactionRequest.cs b/src/SignhostAPIClient/Rest/DataObjects/CreateTransactionRequest.cs new file mode 100644 index 00000000..6d0d57ea --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/CreateTransactionRequest.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; + +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// Request object for creating a new transaction. +/// Transaction creation data including signers, receivers, files, and configuration. +/// +public class CreateTransactionRequest +{ + /// + /// Gets or sets a value indicating whether to seal the transaction (no signers required). + /// When true, the transaction is automatically completed without requiring signatures. + /// + public bool Seal { get; set; } + + /// + /// Gets or sets the custom reference identifier for the transaction. + /// + public string? Reference { get; set; } + + /// + /// Gets or sets the URL to receive status notifications about the transaction. + /// + public string? PostbackUrl { get; set; } + + /// + /// Gets or sets the number of days until the transaction expires. + /// If 0, uses organization default. Maximum value depends on organization settings. + /// + public int DaysToExpire { get; set; } + + /// + /// Gets or sets a value indicating whether to send email notifications to signers and receivers. + /// + public bool SendEmailNotifications { get; set; } + + /// + /// Gets or sets the mode for sign request delivery. + /// 0: No sign requests, 1: Send immediately, 2: Send when ready (default when signers have SendSignRequest enabled). + /// + public int SignRequestMode { get; set; } + + /// + /// Gets or sets the language code for transaction interface and emails. + /// Supported values: de-DE, en-US, es-ES, fr-FR, it-IT, pl-PL, nl-NL. + /// + public string? Language { get; set; } + + /// + /// Gets or sets the custom JSON object for additional transaction data. + /// Only JSON objects are allowed (no arrays or primitives). + /// + public dynamic? Context { get; set; } + + /// + /// Gets or sets the list of signers for the transaction. + /// + public IList Signers { get; set; } = new List(); + + /// + /// Gets or sets the list of receivers who get copies of completed documents. + /// + public IList Receivers { get; set; } = new List(); +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/DeleteTransactionOptions.cs b/src/SignhostAPIClient/Rest/DataObjects/DeleteTransactionOptions.cs index 19ad295d..f2ec106b 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/DeleteTransactionOptions.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/DeleteTransactionOptions.cs @@ -1,16 +1,15 @@ -namespace Signhost.APIClient.Rest.DataObjects +namespace Signhost.APIClient.Rest.DataObjects; + +public class DeleteTransactionOptions { - public class DeleteTransactionOptions - { - /// - /// Gets or sets a value indicating whether - /// e-mail notifications should be send to the awaiting signers. - /// - public bool SendNotifications { get; set; } + /// + /// Gets or sets a value indicating whether + /// e-mail notifications should be send to the awaiting signers. + /// + public bool SendNotifications { get; set; } - /// - /// Gets or sets the reason of cancellation. - /// - public string Reason { get; set; } - } + /// + /// Gets or sets the reason of cancellation. + /// + public string? Reason { get; set; } } diff --git a/src/SignhostAPIClient/Rest/DataObjects/DigidVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/DigidVerification.cs deleted file mode 100644 index b4034816..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/DigidVerification.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - public class DigidVerification - : IVerification - { - public string Type => "DigiD"; - - public string Bsn { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/EidasLoginVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/EidasLoginVerification.cs deleted file mode 100644 index 72deeac5..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/EidasLoginVerification.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Signhost.APIClient.Rest.DataObjects -{ - /// - /// Verification object for eIDAS. - /// - public class EidasLoginVerification - : IVerification - { - /// - /// Gets the . - /// - public string Type { get; } = "eIDAS Login"; - - /// - /// Gets or sets the uid. - /// - public string Uid { get; set; } - - /// - /// Gets or sets the level. - /// - public Level? Level { get; set; } - - /// - /// Gets or sets the first name. - /// - public string FirstName { get; set; } - - /// - /// Gets or sets the last name. - /// - public string LastName { get; set; } - - /// - /// Gets or sets the date of birth. - /// - public DateTime? DateOfBirth { get; set; } - - /// - /// Gets or sets the eIDAS attributes. - /// - public IDictionary Attributes { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Field.cs b/src/SignhostAPIClient/Rest/DataObjects/Field.cs deleted file mode 100644 index 2c2482bc..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/Field.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - public class Field - { - public string Type { get; set; } - - public string Value { get; set; } - - public Location Location { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/FileEntry.cs b/src/SignhostAPIClient/Rest/DataObjects/FileEntry.cs index 5a3cc1bf..333ad673 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/FileEntry.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/FileEntry.cs @@ -1,11 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; -namespace Signhost.APIClient.Rest.DataObjects +namespace Signhost.APIClient.Rest.DataObjects; + +public class FileEntry { - public class FileEntry - { - public IList Links { get; set; } + public IList Links { get; set; } = default!; - public string DisplayName { get; set; } - } + public string? DisplayName { get; set; } } diff --git a/src/SignhostAPIClient/Rest/DataObjects/FileLink.cs b/src/SignhostAPIClient/Rest/DataObjects/FileLink.cs index d1fb0323..8dced5fc 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/FileLink.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/FileLink.cs @@ -1,11 +1,10 @@ -namespace Signhost.APIClient.Rest.DataObjects +namespace Signhost.APIClient.Rest.DataObjects; + +public class FileLink { - public class FileLink - { - public string Rel { get; set; } + public string Rel { get; set; } = default!; - public string Type { get; set; } + public string Type { get; set; } = default!; - public string Link { get; set; } - } + public string Link { get; set; } = default!; } diff --git a/src/SignhostAPIClient/Rest/DataObjects/FileMeta.cs b/src/SignhostAPIClient/Rest/DataObjects/FileMeta.cs deleted file mode 100644 index 4c0f2eba..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/FileMeta.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace Signhost.APIClient.Rest.DataObjects -{ - public class FileMeta - { - public int? DisplayOrder { get; set; } - - public string DisplayName { get; set; } - - public string Description { get; set; } - - public IDictionary Signers { get; set; } - - public IDictionary> FormSets { get; set; } - - /// - /// Gets or sets whether to use the scribble signature as a paraph - /// on each non-signed page. - /// Don't use this setting unless you are really sure this is what you - /// want and know the side-effects. - /// - [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] - public bool? SetParaph { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/Field.cs b/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/Field.cs new file mode 100644 index 00000000..62ed0ac4 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/Field.cs @@ -0,0 +1,17 @@ +using System.Text.Json.Serialization; +using Signhost.APIClient.Rest.JsonConverters; + +namespace Signhost.APIClient.Rest.DataObjects; + +public class Field +{ + public FileFieldType Type { get; set; } + + /// + /// The value content for the field. Can be a string, number, or boolean. + /// + [JsonConverter(typeof(JsonObjectConverter))] + public object? Value { get; set; } + + public Location Location { get; set; } = default!; +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/FileFieldType.cs b/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/FileFieldType.cs new file mode 100644 index 00000000..c5812cb1 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/FileFieldType.cs @@ -0,0 +1,15 @@ +using System.Text.Json.Serialization; + +namespace Signhost.APIClient.Rest.DataObjects; + +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum FileFieldType +{ + Seal, + Signature, + Check, + Radio, + SingleLine, + Number, + Date, +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/FileMeta.cs b/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/FileMeta.cs new file mode 100644 index 00000000..4212292e --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/FileMeta.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Signhost.APIClient.Rest.DataObjects; + +public class FileMeta +{ + public int? DisplayOrder { get; set; } + + public string? DisplayName { get; set; } + + public string? Description { get; set; } + + public IDictionary Signers { get; set; } = + new Dictionary(); + + public IDictionary> FormSets { get; set; } = + new Dictionary>(); + + /// + /// Gets or sets whether to use the scribble signature as a paraph + /// on each non-signed page. + /// Don't use this setting unless you are really sure this is what you + /// want and know the side-effects. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? SetParaph { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/FileSignerMeta.cs b/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/FileSignerMeta.cs new file mode 100644 index 00000000..89b8a6c0 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/FileSignerMeta.cs @@ -0,0 +1,6 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +public class FileSignerMeta +{ + public string[] FormSets { get; set; } = default!; +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/Location.cs b/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/Location.cs new file mode 100644 index 00000000..997bc211 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/FileMetaData/Location.cs @@ -0,0 +1,22 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +public class Location +{ + public string? Search { get; set; } + + public int? Occurence { get; set; } + + public int? Top { get; set; } + + public int? Right { get; set; } + + public int? Bottom { get; set; } + + public int? Left { get; set; } + + public int? Width { get; set; } + + public int? Height { get; set; } + + public int? PageNumber { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/FileSignerMeta.cs b/src/SignhostAPIClient/Rest/DataObjects/FileSignerMeta.cs deleted file mode 100644 index 1c39aa83..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/FileSignerMeta.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - public class FileSignerMeta - { - public string[] FormSets { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/IPAddressVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/IPAddressVerification.cs deleted file mode 100644 index ea2f0eb8..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/IPAddressVerification.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Newtonsoft.Json; - -namespace Signhost.APIClient.Rest.DataObjects -{ - /// - /// Adds a consent verification screen - /// - public class IPAddressVerification - : IVerification - { - /// - /// Initializes a new instance of the class. - /// Do not use! - /// - [Obsolete("This constructor is for internal usage only!")] - public IPAddressVerification() - { - } - - /// - /// Gets the . - /// - public string Type { get; } = "IPAddress"; - - /// - /// Gets or sets the IP Address used by the signer while signing the documents. - /// - public string IPAddress { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/IVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/IVerification.cs deleted file mode 100644 index fe76ce2c..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/IVerification.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Newtonsoft.Json; -using Signhost.APIClient.Rest.JsonConverters; - -namespace Signhost.APIClient.Rest.DataObjects -{ - [JsonConverter(typeof(JsonVerificationConverter))] - public interface IVerification - { - string Type { get; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/IdealVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/IdealVerification.cs deleted file mode 100644 index 459e159b..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/IdealVerification.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - public class IdealVerification - : IVerification - { - public string Type => "iDeal"; - - public string Iban { get; set; } - - public string AccountHolderName { get; set; } - - public string AccountHolderCity { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/IdinVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/IdinVerification.cs deleted file mode 100644 index 684438c6..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/IdinVerification.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Signhost.APIClient.Rest.DataObjects -{ - public class IdinVerification - : IVerification - { - public string Type { get; } = "iDIN"; - - public string AccountHolderName { get; set; } - - public string AccountHolderAddress1 { get; set; } - - public string AccountHolderAddress2 { get; set; } - - public DateTime AccountHolderDateOfBirth { get; set; } - - public IDictionary Attributes { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/ItsmeIdentificationVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/ItsmeIdentificationVerification.cs deleted file mode 100644 index c2c069e6..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/ItsmeIdentificationVerification.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - /// - /// Verification object for itsme Identification. - /// - public class ItsmeIdentificationVerification - : IVerification - { - /// - /// Gets the . - /// - public string Type => "itsme Identification"; - - /// - /// Gets or sets the phonenumber. - /// - public string PhoneNumber { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/ItsmeSignVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/ItsmeSignVerification.cs deleted file mode 100644 index 20699888..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/ItsmeSignVerification.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - /// - /// Verification object for itsme sign. - /// - public class ItsmeSignVerification - : IVerification - { - /// - /// Gets the . - /// - public string Type => "itsme sign"; - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/KennisnetVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/KennisnetVerification.cs deleted file mode 100644 index 72546591..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/KennisnetVerification.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Signhost.APIClient.Rest.DataObjects -{ - [Obsolete("This verification is no longer supported and will be removed in SemVer 4.")] - public class KennisnetVerification - : IVerification - { - public string Type => "Kennisnet"; - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Level.cs b/src/SignhostAPIClient/Rest/DataObjects/Level.cs deleted file mode 100644 index 40b016c1..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/Level.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Newtonsoft.Json; -using Signhost.APIClient.Rest.JsonConverters; - -namespace Signhost.APIClient.Rest.DataObjects -{ - /// - /// Level of Assurance. - /// - [JsonConverter(typeof(LevelEnumConverter))] - public enum Level - { - /// - /// Unknown. - /// - Unknown = 0, - - /// - /// Low. - /// - Low, - - /// - /// Substantial. - /// - Substantial, - - /// - /// High. - /// - High, - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Location.cs b/src/SignhostAPIClient/Rest/DataObjects/Location.cs deleted file mode 100644 index d7288d99..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/Location.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - public class Location - { - public string Search { get; set; } - - public int? Occurence { get; set; } - - public int? Top { get; set; } - - public int? Right { get; set; } - - public int? Bottom { get; set; } - - public int? Left { get; set; } - - public int? Width { get; set; } - - public int? Height { get; set; } - - public int? PageNumber { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/PhoneNumberVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/PhoneNumberVerification.cs deleted file mode 100644 index 8503fbfb..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/PhoneNumberVerification.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - public class PhoneNumberVerification - : IVerification - { - public string Type => "PhoneNumber"; - - public string Number { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/PostbackTransaction.cs b/src/SignhostAPIClient/Rest/DataObjects/PostbackTransaction.cs index 4c5db856..5ae74e50 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/PostbackTransaction.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/PostbackTransaction.cs @@ -1,27 +1,10 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json; +using System.Text.Json.Serialization; -namespace Signhost.APIClient.Rest.DataObjects -{ - public class PostbackTransaction - : Transaction - { - public PostbackTransaction() - { - } - - [JsonConstructor] - private PostbackTransaction( - IReadOnlyDictionary files, - DateTimeOffset? createdDateTime, - DateTimeOffset? canceledDateTime, - string cancelationReason) - : base(files, createdDateTime, canceledDateTime, cancelationReason) - { - } +namespace Signhost.APIClient.Rest.DataObjects; - [JsonProperty("Checksum")] - public string Checksum { get; set; } - } +public class PostbackTransaction + : Transaction +{ + [JsonPropertyName("Checksum")] + public string Checksum { get; set; } = default!; } diff --git a/src/SignhostAPIClient/Rest/DataObjects/Receiver.cs b/src/SignhostAPIClient/Rest/DataObjects/Receiver.cs index 8fc286e2..32937d69 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/Receiver.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/Receiver.cs @@ -1,35 +1,29 @@ -using System.Collections.Generic; -using Newtonsoft.Json; +using System; +using System.Collections.Generic; -namespace Signhost.APIClient.Rest.DataObjects +namespace Signhost.APIClient.Rest.DataObjects; + +public class Receiver { - public class Receiver - { - public Receiver() - { - } + public string? Id { get; set; } + + public string? Name { get; set; } - [JsonConstructor] - private Receiver(IReadOnlyList activities) - { - Activities = activities; - } + public string? Email { get; set; } - public string Name { get; set; } + public string? Language { get; set; } - public string Email { get; set; } + public string? Subject { get; set; } - public string Language { get; set; } + public string? Message { get; set; } - public string Subject { get; set; } + public string? Reference { get; set; } - public string Message { get; set; } + public DateTimeOffset? CreatedDateTime { get; set; } - public string Reference { get; set; } + public DateTimeOffset? ModifiedDateTime { get; set; } - public IReadOnlyList Activities { get; set; } = - new List().AsReadOnly(); + public IList? Activities { get; set; } - public dynamic Context { get; set; } - } + public dynamic? Context { get; set; } } diff --git a/src/SignhostAPIClient/Rest/DataObjects/ScribbleVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/ScribbleVerification.cs deleted file mode 100644 index 64f18a75..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/ScribbleVerification.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - public class ScribbleVerification - : IVerification - { - public string Type => "Scribble"; - - public bool RequireHandsignature { get; set; } - - public bool ScribbleNameFixed { get; set; } - - public string ScribbleName { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Signer.cs b/src/SignhostAPIClient/Rest/DataObjects/Signer.cs index 77cada56..f445f730 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/Signer.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/Signer.cs @@ -1,72 +1,67 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; -namespace Signhost.APIClient.Rest.DataObjects +namespace Signhost.APIClient.Rest.DataObjects; + +public class Signer { - public class Signer - { - public Signer() - { - } + public string? Id { get; set; } + + public DateTimeOffset? Expires { get; set; } + + public string Email { get; set; } = default!; + + public string? IntroText { get; set; } - [JsonConstructor] - private Signer(IReadOnlyList activities) - { - Activities = activities; - } + public string? SignRequestSubject { get; set; } - public string Id { get; set; } + public string? SignRequestMessage { get; set; } - public DateTimeOffset? Expires { get; set; } + public IList Authentications { get; set; } = default!; - public string Email { get; set; } + public IList Verifications { get; set; } = default!; - public string IntroText { get; set; } + public bool SendSignRequest { get; set; } - public string SignRequestSubject { get; set; } + public bool? SendSignConfirmation { get; set; } - public string SignRequestMessage { get; set; } + public int? DaysToRemind { get; set; } - public IList Authentications { get; set; } - = new List(); + public string? Language { get; set; } - public IList Verifications { get; set; } - = new List(); + public string? Reference { get; set; } - public bool SendSignRequest { get; set; } + public string? ReturnUrl { get; set; } - public bool? SendSignConfirmation { get; set; } + public string? RejectReason { get; set; } - public int? DaysToRemind { get; set; } + public string SignUrl { get; set; } = default!; - public string Language { get; set; } + public bool AllowDelegation { get; set; } - public string ScribbleName { get; set; } + public string? DelegateSignUrl { get; set; } - public bool ScribbleNameFixed { get; set; } + public string? DelegateReason { get; set; } - public string Reference { get; set; } + public string? DelegateSignerEmail { get; set; } - public string ReturnUrl { get; set; } + public string? DelegateSignerName { get; set; } - public string RejectReason { get; set; } + public DateTimeOffset? SignedDateTime { get; set; } - public string SignUrl { get; set; } + public DateTimeOffset? RejectDateTime { get; set; } - public bool AllowDelegation { get; set; } + public DateTimeOffset? CreatedDateTime { get; set; } - public string DelegateSignUrl { get; set; } + public DateTimeOffset? SignerDelegationDateTime { get; set; } - public string DelegateReason { get; set; } + public DateTimeOffset? ModifiedDateTime { get; set; } - public string DelegateSignerEmail { get; set; } + public string ShowUrl { get; set; } = default!; - public string DelegateSignerName { get; set; } + public string ReceiptUrl { get; set; } = default!; - public IReadOnlyList Activities { get; private set; } = - new List().AsReadOnly(); + public IList Activities { get; set; } = new List(); - public dynamic Context { get; set; } - } + public dynamic? Context { get; set; } } diff --git a/src/SignhostAPIClient/Rest/DataObjects/SigningCertificateVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/SigningCertificateVerification.cs deleted file mode 100644 index 98673fd3..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/SigningCertificateVerification.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - /// - /// Represents a verification method using a signers signing certificate - /// for example a qualified certificate. - /// - public class SigningCertificateVerification - : IVerification - { - /// - public string Type => "SigningCertificate"; - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/SurfnetVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/SurfnetVerification.cs deleted file mode 100644 index 466551c8..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/SurfnetVerification.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - public class SurfnetVerification - : IVerification - { - public string Type => "SURFnet"; - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Transaction.cs b/src/SignhostAPIClient/Rest/DataObjects/Transaction.cs index 534d31e6..089958d1 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/Transaction.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/Transaction.cs @@ -1,68 +1,49 @@ using System; using System.Collections.Generic; -using Newtonsoft.Json; -namespace Signhost.APIClient.Rest.DataObjects -{ - public class Transaction - { - public Transaction() - { - } - - [JsonConstructor] - protected Transaction( - IReadOnlyDictionary files, - DateTimeOffset? createdDateTime, - DateTimeOffset? canceledDateTime, - string cancelationReason) - { - Files = files ?? new Dictionary(); - CreatedDateTime = createdDateTime; - CancelledDateTime = canceledDateTime; - CancellationReason = cancelationReason; - } +namespace Signhost.APIClient.Rest.DataObjects; - public string Id { get; set; } +public class Transaction +{ + public string Id { get; set; } = default!; - /// - /// Gets the when the was created. - /// - public DateTimeOffset? CreatedDateTime { get; } + /// + /// Gets the when the was created. + /// + public DateTimeOffset CreatedDateTime { get; set; } - /// - /// Gets the when the was cancelled. - /// Returns null if the transaction was not cancelled. - /// - public DateTimeOffset? CancelledDateTime { get; } + /// + /// Gets the when the was cancelled. + /// Returns null if the transaction was not cancelled. + /// + public DateTimeOffset? CanceledDateTime { get; set; } - /// - /// Gets the cancellation reason when the was cancelled. - /// - public string CancellationReason { get; } + /// + /// Gets the cancellation reason when the was cancelled. + /// + public string? CancellationReason { get; set; } - public IReadOnlyDictionary Files { get; private set; } + public IDictionary Files { get; set; } = new Dictionary(); - public TransactionStatus Status { get; set; } + public TransactionStatus Status { get; set; } - public bool Seal { get; set; } + public bool Seal { get; set; } - public IList Signers { get; set; } = new List(); + public IList Signers { get; set; } = new List(); - public IList Receivers { get; set; } = new List(); + public IList Receivers { get; set; } = new List(); - public string Reference { get; set; } + public string? Reference { get; set; } - public string PostbackUrl { get; set; } + public string? PostbackUrl { get; set; } - public int SignRequestMode { get; set; } + public int SignRequestMode { get; set; } - public int DaysToExpire { get; set; } + public int DaysToExpire { get; set; } - public string Language { get; set; } + public string? Language { get; set; } - public bool SendEmailNotifications { get; set; } + public bool SendEmailNotifications { get; set; } - public dynamic Context { get; set; } - } + public dynamic? Context { get; set; } } diff --git a/src/SignhostAPIClient/Rest/DataObjects/TransactionStatus.cs b/src/SignhostAPIClient/Rest/DataObjects/TransactionStatus.cs index 2c9b80a9..a4b62d79 100644 --- a/src/SignhostAPIClient/Rest/DataObjects/TransactionStatus.cs +++ b/src/SignhostAPIClient/Rest/DataObjects/TransactionStatus.cs @@ -1,47 +1,46 @@ -namespace Signhost.APIClient.Rest.DataObjects +namespace Signhost.APIClient.Rest.DataObjects; + +public enum TransactionStatus { - public enum TransactionStatus - { - /// - /// Transaction has not yet been started and is waiting for its - /// documents. - /// - WaitingForDocument = 5, - - /// - /// The transaction was started and is waiting for one or more signers - /// to sign the transaction. - /// - WaitingForSigner = 10, - - /// - /// In progress - /// - InProgress = 20, - - /// - /// The transaction was succesfully completed. - /// - Signed = 30, - - /// - /// The transaction was rejected by one or more of the signers. - /// - Rejected = 40, - - /// - /// The transaction was not signed before it expired. - /// - Expired = 50, - - /// - /// The transaction was cancelled by the sender. - /// - Cancelled = 60, - - /// - /// The transaction could not be completed. - /// - Failed = 70, - } + /// + /// Transaction has not yet been started and is waiting for its + /// documents. + /// + WaitingForDocument = 5, + + /// + /// The transaction was started and is waiting for one or more signers + /// to sign the transaction. + /// + WaitingForSigner = 10, + + /// + /// In progress + /// + InProgress = 20, + + /// + /// The transaction was succesfully completed. + /// + Signed = 30, + + /// + /// The transaction was rejected by one or more of the signers. + /// + Rejected = 40, + + /// + /// The transaction was not signed before it expired. + /// + Expired = 50, + + /// + /// The transaction was cancelled by the sender. + /// + Cancelled = 60, + + /// + /// The transaction could not be completed. + /// + Failed = 70, } diff --git a/src/SignhostAPIClient/Rest/DataObjects/UnknownVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/UnknownVerification.cs deleted file mode 100644 index 69c17ba3..00000000 --- a/src/SignhostAPIClient/Rest/DataObjects/UnknownVerification.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Signhost.APIClient.Rest.DataObjects -{ - public class UnknownVerification - : IVerification - { - public string Type { get; set; } - } -} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/ConsentVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/ConsentVerification.cs new file mode 100644 index 00000000..b43c7cef --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/ConsentVerification.cs @@ -0,0 +1,9 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// Adds a consent verification screen +/// +public class ConsentVerification + : IVerification +{ +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/CscVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/CscVerification.cs new file mode 100644 index 00000000..6a17c85d --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/CscVerification.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// Cloud Signature Consortium (CSC) verification. +/// +public class CscVerification + : IVerification +{ + /// + /// Gets or sets the provider identifier. + /// + public string? Provider { get; set; } + + /// + /// Gets or sets the certificate issuer. + /// + public string? Issuer { get; set; } + + /// + /// Gets or sets the certificate subject. + /// + public string? Subject { get; set; } + + /// + /// Gets or sets the certificate thumbprint. + /// + public string? Thumbprint { get; set; } + + /// + /// Gets or sets additional user data. + /// + public Dictionary? AdditionalUserData { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/DigidVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/DigidVerification.cs new file mode 100644 index 00000000..2fb6dd2c --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/DigidVerification.cs @@ -0,0 +1,11 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +public class DigidVerification + : IVerification +{ + public string? Bsn { get; set; } + + public string? Betrouwbaarheidsniveau { get; set; } + + public bool? SecureDownload { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/EherkenningVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/EherkenningVerification.cs new file mode 100644 index 00000000..b5799217 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/EherkenningVerification.cs @@ -0,0 +1,15 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +public class EherkenningVerification + : IVerification +{ + /// + /// Gets or sets the Uid. + /// + public string? Uid { get; set; } + + /// + /// Gets or sets the entity concern ID / KVK number. + /// + public string? EntityConcernIdKvkNr { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/EidasLoginVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/EidasLoginVerification.cs new file mode 100644 index 00000000..13dd4d5e --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/EidasLoginVerification.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Signhost.APIClient.Rest.JsonConverters; + +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// Verification object for eIDAS. +/// +public class EidasLoginVerification + : IVerification +{ + /// + /// Gets or sets the uid. + /// + public string? Uid { get; set; } + + /// + /// Gets or sets the level. + /// + [JsonConverter(typeof(LevelEnumConverter))] + public Level? Level { get; set; } + + /// + /// Gets or sets the first name. + /// + public string? FirstName { get; set; } + + /// + /// Gets or sets the last name. + /// + public string? LastName { get; set; } + + /// + /// Gets or sets the date of birth. + /// + public DateTime? DateOfBirth { get; set; } + + /// + /// Gets or sets the eIDAS attributes. + /// + public IDictionary? Attributes { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/IPAddressVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/IPAddressVerification.cs new file mode 100644 index 00000000..f3141b4b --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/IPAddressVerification.cs @@ -0,0 +1,13 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// Adds a consent verification screen +/// +public class IPAddressVerification + : IVerification +{ + /// + /// Gets or sets the IP Address used by the signer while signing the documents. + /// + public string? IPAddress { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/IVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/IVerification.cs new file mode 100644 index 00000000..9c586474 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/IVerification.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace Signhost.APIClient.Rest.DataObjects; + +[JsonPolymorphic(TypeDiscriminatorPropertyName = "Type")] +[JsonDerivedType(typeof(ConsentVerification), "Consent")] +[JsonDerivedType(typeof(DigidVerification), "DigiD")] +[JsonDerivedType(typeof(EidasLoginVerification), "eIDAS Login")] +[JsonDerivedType(typeof(IdealVerification), "iDeal")] +[JsonDerivedType(typeof(IdinVerification), "iDIN")] +[JsonDerivedType(typeof(IPAddressVerification), "IPAddress")] +[JsonDerivedType(typeof(ItsmeIdentificationVerification), "itsme Identification")] +[JsonDerivedType(typeof(PhoneNumberVerification), "PhoneNumber")] +[JsonDerivedType(typeof(ScribbleVerification), "Scribble")] +[JsonDerivedType(typeof(SurfnetVerification), "SURFnet")] +[JsonDerivedType(typeof(CscVerification), "CSC Qualified")] +[JsonDerivedType(typeof(EherkenningVerification), "eHerkenning")] +[JsonDerivedType(typeof(OidcVerification), "OpenID Providers")] +[JsonDerivedType(typeof(OnfidoVerification), "Onfido")] +public interface IVerification +{ +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/IdealVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/IdealVerification.cs new file mode 100644 index 00000000..d13a96ce --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/IdealVerification.cs @@ -0,0 +1,11 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +public class IdealVerification + : IVerification +{ + public string Iban { get; set; } = default!; + + public string? AccountHolderName { get; set; } + + public string? AccountHolderCity { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/IdinVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/IdinVerification.cs new file mode 100644 index 00000000..c0f360ba --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/IdinVerification.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Signhost.APIClient.Rest.DataObjects; + +public class IdinVerification + : IVerification +{ + public string? AccountHolderName { get; set; } + + public string? AccountHolderAddress1 { get; set; } + + public string? AccountHolderAddress2 { get; set; } + + public string? AccountHolderDateOfBirth { get; set; } + + public IDictionary? Attributes { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/ItsmeIdentificationVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/ItsmeIdentificationVerification.cs new file mode 100644 index 00000000..76bd5821 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/ItsmeIdentificationVerification.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// Verification object for itsme Identification. +/// +public class ItsmeIdentificationVerification + : IVerification +{ + /// + /// Gets or sets the phonenumber. + /// + public string PhoneNumber { get; set; } = default!; + + public IDictionary? Attributes { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/Level.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/Level.cs new file mode 100644 index 00000000..cf4dcd9e --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/Level.cs @@ -0,0 +1,27 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// Level of Assurance of eIDAS Login verification. +/// +public enum Level +{ + /// + /// Unknown. + /// + Unknown = 0, + + /// + /// Low. + /// + Low, + + /// + /// Substantial. + /// + Substantial, + + /// + /// High. + /// + High, +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/OidcVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/OidcVerification.cs new file mode 100644 index 00000000..eea7aece --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/OidcVerification.cs @@ -0,0 +1,13 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// OpenID Connect identification. +/// +public class OidcVerification + : IVerification +{ + /// + /// Gets or sets the OIDC provider name. + /// + public string? ProviderName { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/OnfidoVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/OnfidoVerification.cs new file mode 100644 index 00000000..c980c25b --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/OnfidoVerification.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace Signhost.APIClient.Rest.DataObjects; + +/// +/// Onfido identity verification. +/// +public class OnfidoVerification + : IVerification +{ + /// + /// Gets or sets the Onfido workflow identifier. + /// + public Guid? WorkflowId { get; set; } + + /// + /// Gets or sets the Onfido workflow run identifier. + /// + public Guid? WorkflowRunId { get; set; } + + /// + /// Gets or sets the Onfido API version. + /// + public int? Version { get; set; } + + /// + /// Gets or sets raw Onfido attributes (availability not guaranteed). + /// + public Dictionary? Attributes { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/PhoneNumberVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/PhoneNumberVerification.cs new file mode 100644 index 00000000..84e15c75 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/PhoneNumberVerification.cs @@ -0,0 +1,9 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +public class PhoneNumberVerification + : IVerification +{ + public string Number { get; set; } = default!; + + public bool? SecureDownload { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/ScribbleVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/ScribbleVerification.cs new file mode 100644 index 00000000..8ba484d2 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/ScribbleVerification.cs @@ -0,0 +1,11 @@ +namespace Signhost.APIClient.Rest.DataObjects; + +public class ScribbleVerification + : IVerification +{ + public bool RequireHandsignature { get; set; } + + public bool ScribbleNameFixed { get; set; } + + public string? ScribbleName { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DataObjects/Verifications/SurfnetVerification.cs b/src/SignhostAPIClient/Rest/DataObjects/Verifications/SurfnetVerification.cs new file mode 100644 index 00000000..45db66a3 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DataObjects/Verifications/SurfnetVerification.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Signhost.APIClient.Rest.DataObjects; + +public class SurfnetVerification + : IVerification +{ + public string? Uid { get; set; } + + public IDictionary? Attributes { get; set; } +} diff --git a/src/SignhostAPIClient/Rest/DigestHashAlgorithmNames.cs b/src/SignhostAPIClient/Rest/DigestHashAlgorithmNames.cs new file mode 100644 index 00000000..c41d87e9 --- /dev/null +++ b/src/SignhostAPIClient/Rest/DigestHashAlgorithmNames.cs @@ -0,0 +1,30 @@ +namespace Signhost.APIClient.Rest; + +/// +/// Provides constants for hash algorithm names used in HTTP Digest headers, +/// following the naming conventions specified in RFC 3230 (Instance Digests in HTTP) +/// and RFC 5843 (Additional Hash Algorithms for HTTP Instance Digests). +/// +/// These names are in accordance with the Digest header in HTTP requests, +/// where the header specifies the algorithm used to create the digest of the resource. +/// +/// For more information: +/// https://evidos.github.io/endpoints/##/paths//api/transaction/%7BtransactionId%7D/file/%7BfileId%7D/put +/// +public enum DigestHashAlgorithm +{ + /// + /// Use no digest. + /// + None = 0, + + /// + /// SHA-256 hash algorithm, as specified in RFC 5843. + /// + SHA256, + + /// + /// SHA-512 hash algorithm, as specified in RFC 5843. + /// + SHA512, +} diff --git a/src/SignhostAPIClient/Rest/ErrorHandling/BadAuthorizationException.cs b/src/SignhostAPIClient/Rest/ErrorHandling/BadAuthorizationException.cs index 04bc32b1..5beab9e8 100644 --- a/src/SignhostAPIClient/Rest/ErrorHandling/BadAuthorizationException.cs +++ b/src/SignhostAPIClient/Rest/ErrorHandling/BadAuthorizationException.cs @@ -1,41 +1,35 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; -namespace Signhost.APIClient.Rest.ErrorHandling +namespace Signhost.APIClient.Rest.ErrorHandling; + +[Serializable] +public class BadAuthorizationException + : SignhostRestApiClientException { - [Serializable] - public class BadAuthorizationException - : SignhostRestApiClientException + public BadAuthorizationException() + : base("API call returned a 401 error code. Please check your request headers.") { - public BadAuthorizationException() - : base("API call returned a 401 error code. Please check your request headers.") - { - HelpLink = "https://api.signhost.com/Help"; - } + HelpLink = "https://api.signhost.com/Help"; + } - public BadAuthorizationException(string message) - : base(message) - { - } + public BadAuthorizationException(string message) + : base(message) + { + } - public BadAuthorizationException( - string message, - Exception innerException) - : base(message, innerException) - { - } + public BadAuthorizationException( + string message, + Exception innerException) + : base(message, innerException) + { + } #if SERIALIZABLE - protected BadAuthorizationException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } -#endif + protected BadAuthorizationException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { } +#endif } diff --git a/src/SignhostAPIClient/Rest/ErrorHandling/BadRequestException.cs b/src/SignhostAPIClient/Rest/ErrorHandling/BadRequestException.cs index 8fb0d528..d449b02b 100644 --- a/src/SignhostAPIClient/Rest/ErrorHandling/BadRequestException.cs +++ b/src/SignhostAPIClient/Rest/ErrorHandling/BadRequestException.cs @@ -1,37 +1,35 @@ using System; -using System.Runtime.Serialization; -namespace Signhost.APIClient.Rest.ErrorHandling +namespace Signhost.APIClient.Rest.ErrorHandling; + +[Serializable] +public class BadRequestException + : SignhostRestApiClientException { - [Serializable] - public class BadRequestException - : SignhostRestApiClientException + public BadRequestException() + : base() { - public BadRequestException() - : base() - { - } + } - public BadRequestException(string message) - : base(message) - { - } + public BadRequestException(string message) + : base(message) + { + } - public BadRequestException( - string message, - Exception innerException) - : base(message, innerException) - { - HelpLink = "https://api.signhost.com/Help"; - } + public BadRequestException( + string message, + Exception innerException) + : base(message, innerException) + { + HelpLink = "https://api.signhost.com/Help"; + } #if SERIALIZABLE - protected BadRequestException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } -#endif + protected BadRequestException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { } +#endif } diff --git a/src/SignhostAPIClient/Rest/ErrorHandling/DefaultSignhostException.cs b/src/SignhostAPIClient/Rest/ErrorHandling/DefaultSignhostException.cs index ca6fb125..b7db21ab 100644 --- a/src/SignhostAPIClient/Rest/ErrorHandling/DefaultSignhostException.cs +++ b/src/SignhostAPIClient/Rest/ErrorHandling/DefaultSignhostException.cs @@ -1,31 +1,29 @@ using System; -using System.Runtime.Serialization; -namespace Signhost.APIClient.Rest.ErrorHandling +namespace Signhost.APIClient.Rest.ErrorHandling; + +[Serializable] +public class DefaultSignhostException : Exception { - [Serializable] - public class DefaultSignhostException : Exception + public DefaultSignhostException(string message) + : base(message) { - public DefaultSignhostException(string message) - : base(message) - { - HelpLink = "https://api.signhost.com/Help"; - } + HelpLink = "https://api.signhost.com/Help"; + } - public DefaultSignhostException( - string message, - Exception innerException) - : base(message, innerException) - { - } + public DefaultSignhostException( + string message, + Exception innerException) + : base(message, innerException) + { + } #if SERIALIZABLE - protected DefaultSignhostException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } -#endif + protected DefaultSignhostException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { } +#endif } diff --git a/src/SignhostAPIClient/Rest/ErrorHandling/GoneException.cs b/src/SignhostAPIClient/Rest/ErrorHandling/GoneException.cs index b902fe62..077a34dc 100644 --- a/src/SignhostAPIClient/Rest/ErrorHandling/GoneException.cs +++ b/src/SignhostAPIClient/Rest/ErrorHandling/GoneException.cs @@ -1,63 +1,61 @@ using System; -using System.Runtime.Serialization; -namespace Signhost.APIClient.Rest.ErrorHandling +namespace Signhost.APIClient.Rest.ErrorHandling; + +/// +/// Thrown when a transaction is deleted / cancelled. +/// +[Serializable] +public class GoneException + : SignhostRestApiClientException { /// - /// Thrown when a transaction is deleted / cancelled. + /// Initializes a new instance of the class. /// - [Serializable] - public class GoneException - : SignhostRestApiClientException + public GoneException() + : base() { - /// - /// Initializes a new instance of the class. - /// - public GoneException() - : base() - { - } + } - /// - /// Initializes a new instance of the class. - /// - /// Additional information - public GoneException(string message) - : base(message) - { - } + /// + /// Initializes a new instance of the class. + /// + /// Additional information + public GoneException(string message) + : base(message) + { + } - public GoneException(string message, TResult result) - : base(message) - { - Result = result; - } + public GoneException(string message, TResult? result) + : base(message) + { + Result = result; + } - /// - /// Initializes a new instance of the class. - /// - /// Additional information - /// Inner exception - public GoneException(string message, Exception innerException) - : base(message, innerException) - { - HelpLink = "https://api.signhost.com/Help"; - } + /// + /// Initializes a new instance of the class. + /// + /// Additional information + /// Inner exception + public GoneException(string message, Exception innerException) + : base(message, innerException) + { + HelpLink = "https://api.signhost.com/Help"; + } #if SERIALIZABLE - protected GoneException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } + protected GoneException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { + } #endif - /// - /// Gets the api / transaction details which are still available. - /// Please note that this no longer contains the full transaction - /// details as most data is removed. - /// - public TResult Result { get; private set; } - } + /// + /// Gets the api / transaction details which are still available. + /// Please note that this no longer contains the full transaction + /// details as most data is removed. + /// + public TResult? Result { get; private set; } } diff --git a/src/SignhostAPIClient/Rest/ErrorHandling/HttpResponseMessageErrorHandlingExtensions.cs b/src/SignhostAPIClient/Rest/ErrorHandling/HttpResponseMessageErrorHandlingExtensions.cs index 745b17c9..491c9d4c 100644 --- a/src/SignhostAPIClient/Rest/ErrorHandling/HttpResponseMessageErrorHandlingExtensions.cs +++ b/src/SignhostAPIClient/Rest/ErrorHandling/HttpResponseMessageErrorHandlingExtensions.cs @@ -2,102 +2,101 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; -using Newtonsoft.Json; -namespace Signhost.APIClient.Rest.ErrorHandling +namespace Signhost.APIClient.Rest.ErrorHandling; + +/// +/// Error handling around s. +/// +public static class HttpResponseMessageErrorHandlingExtensions { + private const string OutOfCreditsApiProblemType = + "https://api.signhost.com/problem/subscription/out-of-credits"; + /// - /// Error handling around s. + /// Throws an exception if the + /// has an error code. /// - public static class HttpResponseMessageErrorHandlingExtensions + /// + /// Returns if the call is succesful. + /// List of which should + /// not be handled as an error. + /// + /// When the api authentication failed. + /// + /// + /// When the API request was an invalid request for your account. + /// + /// + /// When your organisation has run out of credits. + /// + /// + /// When the request resource (ie transaction id or file id) was not found. + /// + /// + /// When the API was unable to proces the request at the moment, + /// a RetryAfter property is set if available. + /// + /// + /// An other unknown API error occured. + /// + public static async Task EnsureSignhostSuccessStatusCodeAsync( + this Task responseTask, + params HttpStatusCode[] expectedStatusCodes) { - /// - /// Throws an exception if the - /// has an error code. - /// - /// - /// Returns if the call is succesful. - /// List of which should - /// not be handled as an error. - /// - /// When the api authentication failed. - /// - /// - /// When the API request was an invalid request for your account. - /// - /// - /// When your organisation has run out of credits. - /// - /// - /// When the request resource (ie transaction id or file id) was not found. - /// - /// - /// When the API was unable to proces the request at the moment, - /// a RetryAfter property is set if available. - /// - /// - /// An other unknown API error occured. - /// - public static async Task EnsureSignhostSuccessStatusCodeAsync( - this Task responseTask, - params HttpStatusCode[] expectedStatusCodes) - { - var response = await responseTask.ConfigureAwait(false); - - if (response.IsSuccessStatusCode) { - return response; - } - - if (expectedStatusCodes.Contains(response.StatusCode)) { - return response; - } - - string errorType = string.Empty; - string errorMessage = "Unknown Signhost error"; - - if (response.Content != null) { - string responsejson = await response.Content.ReadAsStringAsync() - .ConfigureAwait(false); - - var error = JsonConvert.DeserializeAnonymousType( - responsejson, - new { - Type = string.Empty, - Message = string.Empty, - }); - - errorType = error.Type; - errorMessage = error.Message; - } - - switch (response.StatusCode) { - case HttpStatusCode.Unauthorized: - throw new System.UnauthorizedAccessException( - errorMessage); - case HttpStatusCode.BadRequest: - throw new BadRequestException( - errorMessage); - case HttpStatusCode.PaymentRequired - when errorType == "https://api.signhost.com/problem/subscription/out-of-credits": - if (string.IsNullOrEmpty(errorMessage)) { - errorMessage = "The credit bundle has been exceeded."; - } - - throw new OutOfCreditsException( - errorMessage); - case HttpStatusCode.NotFound: - throw new NotFoundException( - errorMessage); - case HttpStatusCode.InternalServerError: - throw new InternalServerErrorException( - errorMessage, response.Headers.RetryAfter); - default: - throw new SignhostRestApiClientException( - errorMessage); - } - - System.Diagnostics.Debug.Fail("Should not be reached"); + var response = await responseTask.ConfigureAwait(false); + + if (response.IsSuccessStatusCode) { + return response; + } + + if (expectedStatusCodes.Contains(response.StatusCode)) { + return response; + } + + string errorType = string.Empty; + string errorMessage = "Unknown Signhost error"; + string responseBody = string.Empty; + + if (response.Content is not null) { + responseBody = await response.Content.ReadAsStringAsync() + .ConfigureAwait(false); + + var error = JsonSerializer.Deserialize(responseBody); + + errorType = error?.Type ?? string.Empty; + errorMessage = error?.Message ?? "Unknown Signhost error"; } + + SignhostRestApiClientException exception = response.StatusCode switch { + HttpStatusCode.Unauthorized => new BadAuthorizationException(errorMessage), + HttpStatusCode.BadRequest => new BadRequestException(errorMessage), + HttpStatusCode.NotFound => new NotFoundException(errorMessage), + + HttpStatusCode.InternalServerError => + new InternalServerErrorException(errorMessage, response.Headers.RetryAfter), + + HttpStatusCode.PaymentRequired + when errorType == OutOfCreditsApiProblemType => + new OutOfCreditsException(errorMessage), + + _ => new SignhostRestApiClientException(errorMessage), + }; + + exception.ResponseBody = responseBody; + + throw exception; + } + + private class ErrorResponse + { + [JsonPropertyName("type")] + public string Type { get; set; } = string.Empty; + + [JsonPropertyName("message")] + public string Message { get; set; } = string.Empty; } } diff --git a/src/SignhostAPIClient/Rest/ErrorHandling/InternalServerErrorException.cs b/src/SignhostAPIClient/Rest/ErrorHandling/InternalServerErrorException.cs index 927c49f2..ff54e52d 100644 --- a/src/SignhostAPIClient/Rest/ErrorHandling/InternalServerErrorException.cs +++ b/src/SignhostAPIClient/Rest/ErrorHandling/InternalServerErrorException.cs @@ -1,49 +1,47 @@ using System; using System.Net.Http.Headers; -using System.Runtime.Serialization; -namespace Signhost.APIClient.Rest.ErrorHandling +namespace Signhost.APIClient.Rest.ErrorHandling; + +[Serializable] +public class InternalServerErrorException + : SignhostRestApiClientException { - [Serializable] - public class InternalServerErrorException - : SignhostRestApiClientException + public InternalServerErrorException() + : base() { - public InternalServerErrorException() - : base() - { - } + } - public InternalServerErrorException(string message) - : base(message) - { - } + public InternalServerErrorException(string message) + : base(message) + { + } - public InternalServerErrorException( - string message, RetryConditionHeaderValue retryAfter) - : base(message) - { - HelpLink = "https://api.signhost.com/Help"; + public InternalServerErrorException( + string message, RetryConditionHeaderValue? retryAfter) + : base(message) + { + HelpLink = "https://api.signhost.com/Help"; - if (retryAfter != null) { - if (retryAfter.Date != null) { - RetryAfter = retryAfter.Date; - } + if (retryAfter is not null) { + if (retryAfter.Date is not null) { + RetryAfter = retryAfter.Date; + } - if (retryAfter.Delta != null) { - RetryAfter = DateTime.Now + retryAfter.Delta; - } + if (retryAfter.Delta is not null) { + RetryAfter = DateTimeOffset.Now + retryAfter.Delta; } } + } #if SERIALIZABLE - protected InternalServerErrorException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } + protected InternalServerErrorException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { + } #endif - private DateTimeOffset? RetryAfter { get; set; } - } + private DateTimeOffset? RetryAfter { get; set; } } diff --git a/src/SignhostAPIClient/Rest/ErrorHandling/NotFoundException.cs b/src/SignhostAPIClient/Rest/ErrorHandling/NotFoundException.cs index ff21de45..138cc568 100644 --- a/src/SignhostAPIClient/Rest/ErrorHandling/NotFoundException.cs +++ b/src/SignhostAPIClient/Rest/ErrorHandling/NotFoundException.cs @@ -1,35 +1,33 @@ using System; -using System.Runtime.Serialization; -namespace Signhost.APIClient.Rest.ErrorHandling +namespace Signhost.APIClient.Rest.ErrorHandling; + +[Serializable] +public class NotFoundException + : SignhostRestApiClientException { - [Serializable] - public class NotFoundException - : SignhostRestApiClientException + public NotFoundException() + : base() { - public NotFoundException() - : base() - { - } + } - public NotFoundException(string message) - : base(message) - { - } + public NotFoundException(string message) + : base(message) + { + } - public NotFoundException(string message, Exception innerException) - : base(message, innerException) - { - HelpLink = "https://api.signhost.com/Help"; - } + public NotFoundException(string message, Exception innerException) + : base(message, innerException) + { + HelpLink = "https://api.signhost.com/Help"; + } #if SERIALIZABLE - protected NotFoundException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } -#endif + protected NotFoundException( + SerializationInfo info, + StreamingContext context) + : base(info, context) + { } +#endif } diff --git a/src/SignhostAPIClient/Rest/ErrorHandling/OutOfCreditsException.cs b/src/SignhostAPIClient/Rest/ErrorHandling/OutOfCreditsException.cs index a5cf42e7..7d8c89e6 100644 --- a/src/SignhostAPIClient/Rest/ErrorHandling/OutOfCreditsException.cs +++ b/src/SignhostAPIClient/Rest/ErrorHandling/OutOfCreditsException.cs @@ -1,21 +1,20 @@ using System; -namespace Signhost.APIClient.Rest.ErrorHandling +namespace Signhost.APIClient.Rest.ErrorHandling; + +/// +/// An exception which indicates payment is required when an API action is called. +/// +[Serializable] +public class OutOfCreditsException + : SignhostRestApiClientException { /// - /// An exception which indicates payment is required when an API action is called. + /// Initializes a new instance of the class. /// - [Serializable] - public class OutOfCreditsException - : SignhostRestApiClientException + /// The exception message. + public OutOfCreditsException(string message) + : base(message) { - /// - /// Initializes a new instance of the class. - /// - /// The exception message. - public OutOfCreditsException(string message) - : base(message) - { - } } } diff --git a/src/SignhostAPIClient/Rest/ErrorHandling/SignhostException.cs b/src/SignhostAPIClient/Rest/ErrorHandling/SignhostException.cs deleted file mode 100644 index fd06b202..00000000 --- a/src/SignhostAPIClient/Rest/ErrorHandling/SignhostException.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace Signhost.APIClient.Rest.ErrorHandling -{ - [Serializable] - [Obsolete("Unused will be removed")] - public class SignhostException - : SignhostRestApiClientException - { - public SignhostException() - : base() - { - } - - public SignhostException(string message) - : base(message) - { - } - - public SignhostException(string message, Exception innerException) - : base(message, innerException) - { - HelpLink = "https://api.signhost.com/Help"; - } - -#if SERIALIZABLE - protected SignhostException( - SerializationInfo info, - StreamingContext context) - : base(info, context) - { - } -#endif - } -} diff --git a/src/SignhostAPIClient/Rest/ErrorHandling/SignhostRestApiClientException.cs b/src/SignhostAPIClient/Rest/ErrorHandling/SignhostRestApiClientException.cs index 600a3c53..3b4ef903 100644 --- a/src/SignhostAPIClient/Rest/ErrorHandling/SignhostRestApiClientException.cs +++ b/src/SignhostAPIClient/Rest/ErrorHandling/SignhostRestApiClientException.cs @@ -1,29 +1,28 @@ using System; -using System.Runtime.Serialization; -namespace Signhost.APIClient.Rest.ErrorHandling +namespace Signhost.APIClient.Rest.ErrorHandling; + +[Serializable] +public class SignhostRestApiClientException + : Exception { - [Serializable] - public class SignhostRestApiClientException - : Exception + public SignhostRestApiClientException() + : base() { - public SignhostRestApiClientException() - : base() - { - } + } - public SignhostRestApiClientException(string message) - : base(message) - { - } + public SignhostRestApiClientException(string message) + : base(message) + { + } - public SignhostRestApiClientException( - string message, - Exception innerException) - : base(message, innerException) - { - HelpLink = "https://api.signhost.com/Help"; - } + public SignhostRestApiClientException( + string message, + Exception innerException) + : base(message, innerException) + { + HelpLink = "https://api.signhost.com/Help"; + } #if SERIALIZABLE protected SignhostRestApiClientException( @@ -33,5 +32,9 @@ protected SignhostRestApiClientException( { } #endif - } + + /// + /// Gets or sets the response body returned from the Signhost REST API. + /// + public string? ResponseBody { get; set; } } diff --git a/src/SignhostAPIClient/Rest/FileDigestOptions.cs b/src/SignhostAPIClient/Rest/FileDigestOptions.cs index 02d42487..9493d526 100644 --- a/src/SignhostAPIClient/Rest/FileDigestOptions.cs +++ b/src/SignhostAPIClient/Rest/FileDigestOptions.cs @@ -1,31 +1,26 @@ -using System; -using System.IO; -using System.Security.Cryptography; +namespace Signhost.APIClient.Rest; -namespace Signhost.APIClient.Rest +/// +/// File digest options for file uploads. +/// +public class FileDigestOptions { /// - /// File digest options for file uploads + /// Gets or sets whether to use the Digest header with a checksum of + /// the uploaded file. /// - public class FileDigestOptions - { - /// - /// Gets or sets whether to use the Digest header with a checksum of - /// the uploaded file. - /// - public bool UseFileDigesting { get; set; } = true; + public bool UseFileDigesting { get; set; } = true; - /// - /// Gets or sets the digest algorithm to use when calculating - /// the hash value or the digest algorithm that is used - /// to set the . - /// - public string DigestHashAlgorithm { get; set; } = "SHA-256"; + /// + /// Gets or sets the digest algorithm to use when calculating + /// the hash value or the digest algorithm that is used + /// to set the . + /// + public DigestHashAlgorithm DigestHashAlgorithm { get; set; } = DigestHashAlgorithm.SHA256; - /// - /// Gets or sets the hash digest value, you can set this yourself - /// if you know the digest value in advance. - /// - public byte[] DigestHashValue { get; set; } - } + /// + /// Gets or sets the hash digest value, you can set this yourself + /// if you know the digest value in advance. + /// + public byte[]? DigestHashValue { get; set; } } diff --git a/src/SignhostAPIClient/Rest/FileUploadOptions.cs b/src/SignhostAPIClient/Rest/FileUploadOptions.cs index 340ad1ec..09ff94c5 100644 --- a/src/SignhostAPIClient/Rest/FileUploadOptions.cs +++ b/src/SignhostAPIClient/Rest/FileUploadOptions.cs @@ -1,14 +1,12 @@ -namespace Signhost.APIClient.Rest +namespace Signhost.APIClient.Rest; + +/// +/// Options to be used during a file upload +/// +public class FileUploadOptions { /// - /// Options to be used during a file upload + /// Gets or sets the . /// - public class FileUploadOptions - { - /// - /// Gets or sets the . - /// - public FileDigestOptions DigestOptions { get; set; } - = new FileDigestOptions(); - } + public FileDigestOptions DigestOptions { get; set; } = new FileDigestOptions(); } diff --git a/src/SignhostAPIClient/Rest/HttpContentJsonExtensions.cs b/src/SignhostAPIClient/Rest/HttpContentJsonExtensions.cs index e205e41a..94911871 100644 --- a/src/SignhostAPIClient/Rest/HttpContentJsonExtensions.cs +++ b/src/SignhostAPIClient/Rest/HttpContentJsonExtensions.cs @@ -1,31 +1,28 @@ using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; -using Newtonsoft.Json; -namespace Signhost.APIClient.Rest +namespace Signhost.APIClient.Rest; + +/// +/// Extension methods around JSON Deserialization. +/// +internal static class HttpContentJsonExtensions { /// - /// Extension methods around JSON Deserialization. + /// Reads the JSON content and returns the deserialized value. /// - internal static class HttpContentJsonExtensions + /// Type to deserialize to + /// to read. + /// A deserialized value of + /// or default(T) if no content is available. + internal static async Task FromJsonAsync(this HttpContent httpContent) { - /// - /// Reads the JSON content and returns the deserialized value. - /// - /// Type to deserialize to - /// to read. - /// A deserialized value of - /// or default(T) if no content is available. - internal static async Task FromJsonAsync( - this HttpContent httpContent) - { - if (httpContent == null) { - return default(T); - } - - var json = await httpContent.ReadAsStringAsync() - .ConfigureAwait(false); - return JsonConvert.DeserializeObject(json); + if (httpContent is null) { + return default; } + + string json = await httpContent.ReadAsStringAsync().ConfigureAwait(false); + return JsonSerializer.Deserialize(json, SignhostJsonSerializerOptions.Default); } } diff --git a/src/SignhostAPIClient/Rest/ISignHostApiClient.cs b/src/SignhostAPIClient/Rest/ISignHostApiClient.cs index f4546fd8..ceb27d0d 100644 --- a/src/SignhostAPIClient/Rest/ISignHostApiClient.cs +++ b/src/SignhostAPIClient/Rest/ISignHostApiClient.cs @@ -3,248 +3,137 @@ using System.Threading.Tasks; using Signhost.APIClient.Rest.DataObjects; -namespace Signhost.APIClient.Rest +namespace Signhost.APIClient.Rest; + +/// +/// Interface abstracting the available Signhost API calls. +/// +public interface ISignhostApiClient { /// - /// Interface abstracting the available Signhost API calls. + /// Creates a new transaction. /// - public interface ISignHostApiClient - { - /// - /// Creates a new transaction. - /// - /// A transaction model. - /// A transaction object. - Task CreateTransactionAsync(Transaction transaction); - - /// - /// Creates a new transaction. - /// - /// A transaction model. - /// A cancellation token. - /// A transaction object. - Task CreateTransactionAsync( - Transaction transaction, - CancellationToken cancellationToken = default); - - /// - /// Adds meta data for a file to an existing transaction by providing a - /// file location and a transaction id. - /// - /// Meta data for the file. - /// A valid transaction Id of an existing transaction. - /// An Id for the file. Should be the same as the fileId in the . - /// A task. - /// Make sure to call this method before - /// . - Task AddOrReplaceFileMetaToTransactionAsync( - FileMeta fileMeta, - string transactionId, - string fileId); - - /// - /// Adds meta data for a file to an existing transaction by providing a - /// file location and a transaction id. - /// - /// Meta data for the file. - /// A valid transaction Id of an existing transaction. - /// An Id for the file. Should be the same as the fileId in the . - /// A cancellation token. - /// A task. - /// Make sure to call this method before - /// . - Task AddOrReplaceFileMetaToTransactionAsync( - FileMeta fileMeta, - string transactionId, - string fileId, - CancellationToken cancellationToken = default); - - /// - /// Add a file to a existing transaction by providing a file location - /// and a transaction id. - /// - /// A Stream containing the file to upload. - /// A valid transaction Id of an existing transaction. - /// A Id for the file. Using the file name is recommended. - /// If a file with the same fileId allready exists the file wil be replaced. - /// . - /// A Task. - Task AddOrReplaceFileToTransactionAsync( - Stream fileStream, - string transactionId, - string fileId, - FileUploadOptions uploadOptions); - - /// - /// Add a file to a existing transaction by providing a file location - /// and a transaction id. - /// - /// A Stream containing the file to upload. - /// A valid transaction Id of an existing transaction. - /// A Id for the file. Using the file name is recommended. - /// If a file with the same fileId allready exists the file wil be replaced. - /// . - /// A cancellation token. - /// A Task. - Task AddOrReplaceFileToTransactionAsync( - Stream fileStream, - string transactionId, - string fileId, - FileUploadOptions uploadOptions, - CancellationToken cancellationToken = default); - - /// - /// Add a file to a existing transaction by providing a file location - /// and a transaction id. - /// - /// A string representation of the file path. - /// A valid transaction Id of an existing transaction. - /// A Id for the file. Using the file name is recommended. - /// If a file with the same fileId allready exists the file wil be replaced. - /// Optional . - /// A Task. - Task AddOrReplaceFileToTransactionAsync( - string filePath, - string transactionId, - string fileId, - FileUploadOptions uploadOptions); - - /// - /// Add a file to a existing transaction by providing a file location - /// and a transaction id. - /// - /// A string representation of the file path. - /// A valid transaction Id of an existing transaction. - /// A Id for the file. Using the file name is recommended. - /// If a file with the same fileId allready exists the file wil be replaced. - /// Optional . - /// A cancellation token. - /// A Task. - Task AddOrReplaceFileToTransactionAsync( - string filePath, - string transactionId, - string fileId, - FileUploadOptions uploadOptions, - CancellationToken cancellationToken = default); + /// A transaction creation request. + /// A cancellation token. + /// A transaction object. + Task CreateTransactionAsync( + CreateTransactionRequest request, + CancellationToken cancellationToken = default); - /// - /// start a existing transaction by providing transaction id. - /// - /// A valid transaction Id of an existing transaction. - /// A Task. - Task StartTransactionAsync(string transactionId); - - /// - /// start a existing transaction by providing transaction id. - /// - /// A valid transaction Id of an existing transaction. - /// A cancellation token. - /// A Task. - Task StartTransactionAsync( - string transactionId, - CancellationToken cancellationToken = default); - - /// - /// Gets an exisiting transaction by providing a transaction id. - /// - /// A valid transaction id for an existing transaction. - /// A object. - Task GetTransactionAsync(string transactionId); - - /// - /// Gets an exisiting transaction by providing a transaction id. - /// - /// A valid transaction id for an existing transaction. - /// A cancellation token. - /// A object. - Task GetTransactionAsync( - string transactionId, - CancellationToken cancellationToken = default); - - /// - /// Gets a existing transaction by providing a transaction id. - /// - /// A valid transaction Id of an existing transaction. - /// A object. - Task> GetTransactionResponseAsync(string transactionId); + /// + /// Adds meta data for a file to an existing transaction by providing a + /// file location and a transaction id. + /// + /// Meta data for the file. + /// A valid transaction Id of an existing transaction. + /// An Id for the file. Should be the same as the fileId in the . + /// A cancellation token. + /// A task. + /// Make sure to call this method before + /// . + Task AddOrReplaceFileMetaToTransactionAsync( + FileMeta fileMeta, + string transactionId, + string fileId, + CancellationToken cancellationToken = default); - /// - /// Gets a existing transaction by providing a transaction id. - /// - /// A valid transaction Id of an existing transaction. - /// A cancellation token. - /// A object. - Task> GetTransactionResponseAsync( - string transactionId, - CancellationToken cancellationToken = default); + /// + /// Add a file to a existing transaction by providing a file location + /// and a transaction id. + /// + /// A Stream containing the file to upload. + /// A valid transaction Id of an existing transaction. + /// A Id for the file. Using the file name is recommended. + /// If a file with the same fileId allready exists the file wil be replaced. + /// . + /// A cancellation token. + /// A Task. + Task AddOrReplaceFileToTransactionAsync( + Stream fileStream, + string transactionId, + string fileId, + FileUploadOptions? uploadOptions = default, + CancellationToken cancellationToken = default); - /// - /// Deletes a existing transaction by providing a transaction id. - /// - /// A valid transaction Id of an existing transaction. - /// A cancellation token. - /// A Task. - Task DeleteTransactionAsync( - string transactionId, - CancellationToken cancellationToken = default); + /// + /// Add a file to a existing transaction by providing a file location + /// and a transaction id. + /// + /// A string representation of the file path. + /// A valid transaction Id of an existing transaction. + /// A Id for the file. Using the file name is recommended. + /// If a file with the same fileId allready exists the file wil be replaced. + /// Optional . + /// A cancellation token. + /// A Task. + Task AddOrReplaceFileToTransactionAsync( + string filePath, + string transactionId, + string fileId, + FileUploadOptions? uploadOptions = default, + CancellationToken cancellationToken = default); - /// - /// Deletes a existing transaction by providing a transaction id. - /// - /// A valid transaction Id of an existing transaction. - /// Optional . - /// A Task. - Task DeleteTransactionAsync( - string transactionId, - DeleteTransactionOptions options); + /// + /// start a existing transaction by providing transaction id. + /// + /// A valid transaction Id of an existing transaction. + /// A cancellation token. + /// A Task. + Task StartTransactionAsync( + string transactionId, + CancellationToken cancellationToken = default); - /// - /// Deletes a existing transaction by providing a transaction id. - /// - /// A valid transaction Id of an existing transaction. - /// Optional . - /// A cancellation token. - /// A Task. - Task DeleteTransactionAsync( - string transactionId, - DeleteTransactionOptions options = default, - CancellationToken cancellationToken = default); + /// + /// Gets an exisiting transaction by providing a transaction id. + /// + /// A valid transaction id for an existing transaction. + /// A cancellation token. + /// A object. + Task GetTransactionAsync( + string transactionId, + CancellationToken cancellationToken = default); - /// - /// Gets the signed document of a finished transaction by providing transaction id. - /// - /// A valid transaction Id of an existing transaction. - /// A valid file Id of a signed document. - /// Returns a stream containing the signed document data. - Task GetDocumentAsync(string transactionId, string fileId); + /// + /// Gets a existing transaction by providing a transaction id. + /// + /// A valid transaction Id of an existing transaction. + /// A cancellation token. + /// A object. + Task> GetTransactionResponseAsync( + string transactionId, + CancellationToken cancellationToken = default); - /// - /// Gets the signed document of a finished transaction by providing transaction id. - /// - /// A valid transaction Id of an existing transaction. - /// A valid file Id of a signed document. - /// A cancellation token. - /// Returns a stream containing the signed document data. - Task GetDocumentAsync( - string transactionId, - string fileId, - CancellationToken cancellationToken = default); + /// + /// Deletes a existing transaction by providing a transaction id. + /// + /// A valid transaction Id of an existing transaction. + /// Optional . + /// A cancellation token. + /// A Task. + Task DeleteTransactionAsync( + string transactionId, + DeleteTransactionOptions? options = default, + CancellationToken cancellationToken = default); - /// - /// Gets the receipt of a finished transaction by providing transaction id. - /// - /// A valid transaction Id of an finnished transaction. - /// Returns a stream containing the receipt data. - Task GetReceiptAsync(string transactionId); + /// + /// Gets the signed document of a finished transaction by providing transaction id. + /// + /// A valid transaction Id of an existing transaction. + /// A valid file Id of a signed document. + /// A cancellation token. + /// Returns a stream containing the signed document data. + Task GetDocumentAsync( + string transactionId, + string fileId, + CancellationToken cancellationToken = default); - /// - /// Gets the receipt of a finished transaction by providing transaction id. - /// - /// A valid transaction Id of an finnished transaction. - /// A cancellation token. - /// Returns a stream containing the receipt data. - Task GetReceiptAsync( - string transactionId, - CancellationToken cancellationToken = default); - } + /// + /// Gets the receipt of a finished transaction by providing transaction id. + /// + /// A valid transaction Id of an finnished transaction. + /// A cancellation token. + /// Returns a stream containing the receipt data. + Task GetReceiptAsync( + string transactionId, + CancellationToken cancellationToken = default); } diff --git a/src/SignhostAPIClient/Rest/ISignhostApiClientSettings.cs b/src/SignhostAPIClient/Rest/ISignhostApiClientSettings.cs index 439dc127..5b423968 100644 --- a/src/SignhostAPIClient/Rest/ISignhostApiClientSettings.cs +++ b/src/SignhostAPIClient/Rest/ISignhostApiClientSettings.cs @@ -1,26 +1,25 @@ -using System; +using System; -namespace Signhost.APIClient.Rest -{ - public delegate void AddHeaders(string name, string value); +namespace Signhost.APIClient.Rest; + +public delegate void AddHeaders(string name, string value); - public interface ISignHostApiClientSettings - { - /// - /// Gets the usertoken identifying an authorized user. - /// - string UserToken { get; } +public interface ISignhostApiClientSettings +{ + /// + /// Gets the usertoken identifying an authorized user. + /// + string? UserToken { get; } - /// - /// Gets the app key of your applications. - /// - string APPKey { get; } + /// + /// Gets the app key of your applications. + /// + string APPKey { get; } - /// - /// Gets the signhost API endpoint. - /// - string Endpoint { get; } + /// + /// Gets the signhost API endpoint. + /// + string Endpoint { get; } - Action AddHeader { get; } - } + Action? AddHeader { get; } } diff --git a/src/SignhostAPIClient/Rest/ISignhostApiReceiver.cs b/src/SignhostAPIClient/Rest/ISignhostApiReceiver.cs index ac8441a7..8b297523 100644 --- a/src/SignhostAPIClient/Rest/ISignhostApiReceiver.cs +++ b/src/SignhostAPIClient/Rest/ISignhostApiReceiver.cs @@ -1,26 +1,23 @@ -using System; using System.Collections.Generic; -using System.Threading.Tasks; -using Signhost.APIClient.Rest; +using System.Diagnostics.CodeAnalysis; using Signhost.APIClient.Rest.DataObjects; -namespace Signhost.APIClient.Rest +namespace Signhost.APIClient.Rest; + +/// +/// Interface abstracting the available Signhost API responses. +/// +public interface ISignhostApiReceiver { /// - /// Interface abstracting the available Signhost API responses. + /// Checks the validity of the postback checksum. /// - public interface ISignhostApiReceiver - { - /// - /// Checks the validity of the postback checksum. - /// - /// true, if postback checksum valid was validated, false otherwise. - /// HTTP response headers. - /// HTTP response body. - /// A transaction object. - bool IsPostbackChecksumValid( - IDictionary headers, - string body, - out Transaction postbackTransaction); - } + /// true, if postback checksum valid was validated, false otherwise. + /// HTTP response headers. + /// HTTP response body. + /// A transaction object. + bool IsPostbackChecksumValid( + IDictionary headers, + string body, + [NotNullWhen(true)] out Transaction? postbackTransaction); } diff --git a/src/SignhostAPIClient/Rest/JsonContent.cs b/src/SignhostAPIClient/Rest/JsonContent.cs index 54e72b4c..bb29567a 100644 --- a/src/SignhostAPIClient/Rest/JsonContent.cs +++ b/src/SignhostAPIClient/Rest/JsonContent.cs @@ -1,46 +1,45 @@ -using System.Net.Http; +using System.Net.Http; using System.Net.Http.Headers; -using Newtonsoft.Json; +using System.Text.Json; -namespace Signhost.APIClient.Rest +namespace Signhost.APIClient.Rest; + +/// +/// See the helper class. +/// +internal static class JsonContent { /// - /// Helper class + /// Creates a new . /// - internal static class JsonContent + /// Type to serialize. + /// Value to serialize. + /// . + internal static JsonContent From(T value) { - /// - /// Creates a new . - /// - /// Type to serialize - /// Value to serialize - /// - internal static JsonContent From(T value) - { - return new JsonContent(value); - } + return new JsonContent(value); } +} +/// +/// A class for application/json. +/// +/// The type to serialize +internal class JsonContent + : StringContent +{ /// - /// A class for application/json. + /// Initializes a new instance of the class. /// - /// The type to serialize - internal class JsonContent - : StringContent + /// Value to serialize. + public JsonContent(T value) + : base(ToJson(value)) { - /// - /// Initializes a new instance of the class. - /// - /// Value to serialize - public JsonContent(T value) - : base(ToJson(value)) - { - Headers.ContentType = new MediaTypeHeaderValue("application/json"); - } + Headers.ContentType = new MediaTypeHeaderValue("application/json"); + } - private static string ToJson(T value) - { - return JsonConvert.SerializeObject(value); - } + private static string ToJson(T value) + { + return JsonSerializer.Serialize(value, SignhostJsonSerializerOptions.Default); } } diff --git a/src/SignhostAPIClient/Rest/JsonConverters/JsonBaseConverter.cs b/src/SignhostAPIClient/Rest/JsonConverters/JsonBaseConverter.cs deleted file mode 100644 index cc3f3e53..00000000 --- a/src/SignhostAPIClient/Rest/JsonConverters/JsonBaseConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Signhost.APIClient.Rest.JsonConverters -{ - public abstract class JsonBaseConverter - : JsonConverter - { -#if TYPEINFO - public override bool CanConvert(Type objectType) - => typeof(T).GetTypeInfo().IsAssignableFrom(objectType.GetTypeInfo()); -#else - public override bool CanConvert(Type objectType) - => typeof(T).IsAssignableFrom(objectType); -#endif - - public override object ReadJson( - JsonReader reader, - Type objectType, - object existingValue, - JsonSerializer serializer) - { - var jsonObject = JObject.Load(reader); - var target = Create(objectType, jsonObject); - serializer.Populate(jsonObject.CreateReader(), target); - return target; - } - - protected abstract T Create(Type objectType, JObject jsonObject); - } -} diff --git a/src/SignhostAPIClient/Rest/JsonConverters/JsonObjectConverter.cs b/src/SignhostAPIClient/Rest/JsonConverters/JsonObjectConverter.cs new file mode 100644 index 00000000..a604e698 --- /dev/null +++ b/src/SignhostAPIClient/Rest/JsonConverters/JsonObjectConverter.cs @@ -0,0 +1,56 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Signhost.APIClient.Rest.JsonConverters; + +/// +/// Converts JSON values of specific types (string, number, boolean) to/from a C# object. +/// Allows flexible deserialization of fields that can contain string, numeric, or boolean JSON values. +/// +public class JsonObjectConverter + : JsonConverter +{ + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.String => reader.GetString(), + JsonTokenType.Number => reader.TryGetInt64(out long value) + ? value + : reader.GetDouble(), + JsonTokenType.True or JsonTokenType.False => reader.GetBoolean(), + JsonTokenType.Null => null, + _ => throw new JsonException($"Unexpected token {reader.TokenType} when parsing field value. Only string, number, boolean, or null are allowed."), + }; + } + + public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) + { + if (value is null) { + writer.WriteNullValue(); + } + else if (value is string s) { + writer.WriteStringValue(s); + } + else if (value is bool b) { + writer.WriteBooleanValue(b); + } + else if (value is int i) { + writer.WriteNumberValue(i); + } + else if (value is long l) { + writer.WriteNumberValue(l); + } + else if (value is double d) { + writer.WriteNumberValue(d); + } + else if (value is decimal dec) { + writer.WriteNumberValue(dec); + } + else { + throw new JsonException( + $"Field value must be string, number, or boolean, but got {value.GetType().Name}"); + } + } +} diff --git a/src/SignhostAPIClient/Rest/JsonConverters/JsonVerificationConverter.cs b/src/SignhostAPIClient/Rest/JsonConverters/JsonVerificationConverter.cs deleted file mode 100644 index 3019809e..00000000 --- a/src/SignhostAPIClient/Rest/JsonConverters/JsonVerificationConverter.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Signhost.APIClient.Rest.DataObjects; - -namespace Signhost.APIClient.Rest.JsonConverters -{ - internal class JsonVerificationConverter - : JsonBaseConverter - { - private static readonly IDictionary VerificationTypes = - CreateVerificationTypeMap(); - - public override bool CanWrite - => false; - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - => new NotImplementedException(); - - /// - /// Adds an additional verification type to the - /// map. - /// - /// - internal static void RegisterVerification() - where T : IVerification - { - var verification = (IVerification)Activator.CreateInstance(typeof(T)); - - VerificationTypes[verification.Type] = typeof(T).GetTypeInfo(); - } - - protected override IVerification Create( - Type objectType, - JObject jsonObject) - { - var typeName = jsonObject["Type"]?.ToString(); - - if (VerificationTypes.TryGetValue(typeName, out var verificationType)) { - return (IVerification)Activator.CreateInstance(verificationType.AsType()); - } - - return new UnknownVerification(); - } - - private static IDictionary CreateVerificationTypeMap() - { - return typeof(JsonVerificationConverter).GetTypeInfo().Assembly.ExportedTypes - .Select(t => t.GetTypeInfo()) - .Where(t => typeof(IVerification).GetTypeInfo().IsAssignableFrom(t)) - .Where(t => !t.IsInterface && !t.IsAbstract) -#pragma warning disable SA1008 // Opening parenthesis must be spaced correctly - .Select(t => ( - typeInfo: t, - instance: (IVerification)Activator.CreateInstance(t.AsType()))) -#pragma warning restore SA1008 // Opening parenthesis must be spaced correctly - .Where(t => t.instance.Type != null) - .ToDictionary(t => t.instance.Type, t => t.typeInfo); - } - } -} diff --git a/src/SignhostAPIClient/Rest/JsonConverters/LevelEnumConverter.cs b/src/SignhostAPIClient/Rest/JsonConverters/LevelEnumConverter.cs index c088635a..043ceb03 100644 --- a/src/SignhostAPIClient/Rest/JsonConverters/LevelEnumConverter.cs +++ b/src/SignhostAPIClient/Rest/JsonConverters/LevelEnumConverter.cs @@ -1,57 +1,55 @@ using System; -using System.Reflection; -using Newtonsoft.Json; +using System.Text.Json; +using System.Text.Json.Serialization; using Signhost.APIClient.Rest.DataObjects; -namespace Signhost.APIClient.Rest.JsonConverters +namespace Signhost.APIClient.Rest.JsonConverters; + +/// +/// JSON converter factory for converting the enum. +/// Invalid values are mapped to . +/// +internal class LevelEnumConverter + : JsonConverter { - /// - /// JSON converter for converting the enum. - /// Invalid values are mapped to . - /// - internal class LevelEnumConverter - : JsonConverter + public override Level? Read( + ref Utf8JsonReader reader, + Type typeToConvert, + JsonSerializerOptions options) { - /// - public override bool CanWrite => false; - - /// - public override bool CanConvert(Type objectType) - => IsLevelEnum(GetUnderlyingType(objectType)); - - /// - public override object ReadJson( - JsonReader reader, - Type objectType, - object existingValue, - JsonSerializer serializer) - { - var value = reader.Value as string; - - if (value != null) { - if (Enum.TryParse(value, out Level level)) { - return level; - } + if (reader.TokenType == JsonTokenType.Null) { + return null; + } - return Level.Unknown; + if (reader.TokenType == JsonTokenType.String) { + string value = reader.GetString() ?? string.Empty; + if (Enum.TryParse(value, out var level)) { + return level; } - return null; + return Level.Unknown; } - /// - public override void WriteJson( - JsonWriter writer, - object value, - JsonSerializer serializer) - => throw new NotImplementedException(); + if (reader.TokenType == JsonTokenType.Number) { + int value = reader.GetInt32(); + if (Enum.IsDefined(typeof(Level), value)) { + return (Level)value; + } + } - private static Type GetUnderlyingType(Type type) - => type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) - ? Nullable.GetUnderlyingType(type) - : type; + return Level.Unknown; + } - private static bool IsLevelEnum(Type type) - => type.GetTypeInfo().IsEnum && type == typeof(Level); + public override void Write( + Utf8JsonWriter writer, + Level? value, + JsonSerializerOptions options) + { + if (value is null) { + writer.WriteNullValue(); + } + else { + writer.WriteStringValue(value.ToString()); + } } } diff --git a/src/SignhostAPIClient/Rest/SignHostApiClient.cs b/src/SignhostAPIClient/Rest/SignHostApiClient.cs index 093632b8..eba12797 100644 --- a/src/SignhostAPIClient/Rest/SignHostApiClient.cs +++ b/src/SignhostAPIClient/Rest/SignHostApiClient.cs @@ -9,482 +9,317 @@ using Signhost.APIClient.Rest.DataObjects; using Signhost.APIClient.Rest.ErrorHandling; -namespace Signhost.APIClient.Rest +namespace Signhost.APIClient.Rest; + +/// +/// Implements the interface which provides +/// an signhost api client implementation. +/// +public class SignhostApiClient + : ISignhostApiClient + , IDisposable { + private const string ApiVersion = "v1"; + + private static readonly string Version = GetVersion() + ?? throw new InvalidOperationException("Unknown assembly version."); + + private readonly ISignhostApiClientSettings settings; + private readonly HttpClient client; + /// - /// Implements the interface which provides - /// an signhost api client implementation. + /// Initializes a new instance of the class. + /// Set your usertoken and APPKey by creating a . /// - public class SignHostApiClient - : ISignHostApiClient - , IDisposable + /// . + public SignhostApiClient(ISignhostApiClientSettings settings) + : this(settings, new HttpClient()) { - private const string ApiVersion = "v1"; + } - private static readonly string Version = typeof(SignHostApiClient) - .GetTypeInfo() - .Assembly.GetCustomAttribute() - .Version; - - private readonly ISignHostApiClientSettings settings; - private readonly HttpClient client; - - /// - /// Initializes a new instance of the class. - /// Set your usertoken and APPKey by creating a . - /// - /// - public SignHostApiClient(ISignHostApiClientSettings settings) - : this(settings, new HttpClient()) - { + /// + /// Initializes a new instance of the class. + /// Set your usertoken and APPKey by creating a . + /// + /// . + /// to use for all http calls. + public SignhostApiClient( + ISignhostApiClientSettings settings, + HttpClient httpClient) + { + settings.ThrowIfNullOrEmpty(nameof(settings)); + httpClient.ThrowIfNullOrEmpty(nameof(httpClient)); + + this.settings = settings; + client = httpClient; + client.BaseAddress = new Uri( + settings.Endpoint + (settings.Endpoint.EndsWith("/") ? string.Empty : "/")); + client.DefaultRequestHeaders.UserAgent.Add( + new ProductInfoHeaderValue("SignhostClientLibrary", Version)); + client.DefaultRequestHeaders.Add("Application", ApplicationHeader); + + if (!string.IsNullOrWhiteSpace(settings.UserToken)) { + client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader); } - /// - /// Initializes a new instance of the class. - /// Set your usertoken and APPKey by creating a . - /// - /// - /// to use for all http calls. - public SignHostApiClient( - ISignHostApiClientSettings settings, - HttpClient httpClient) - { - this.settings = settings; - this.client = httpClient; - this.client.BaseAddress = new Uri( - settings.Endpoint + (settings.Endpoint.EndsWith("/") ? string.Empty : "/")); - this.client.DefaultRequestHeaders.UserAgent.Add( - new System.Net.Http.Headers.ProductInfoHeaderValue( - "SignhostClientLibrary", - Version)); - this.client.DefaultRequestHeaders.Add("Application", ApplicationHeader); - - if (!string.IsNullOrWhiteSpace(settings.UserToken)) { - this.client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader); - } - - this.client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse($"application/vnd.signhost.{ApiVersion}+json")); - settings.AddHeader?.Invoke(this.client.DefaultRequestHeaders.Add); - } + client.DefaultRequestHeaders.Accept.Add( + MediaTypeWithQualityHeaderValue.Parse($"application/vnd.signhost.{ApiVersion}+json")); + settings.AddHeader?.Invoke(client.DefaultRequestHeaders.Add); + } - private string ApplicationHeader - => $"APPKey {settings.APPKey}"; + private string ApplicationHeader => $"APPKey {settings.APPKey}"; - private string AuthorizationHeader - => $"APIKey {settings.UserToken}"; + private string AuthorizationHeader => $"APIKey {settings.UserToken}"; - /// - /// Globally register an additional verification type. - /// - /// to - public static void RegisterVerification() - where T : IVerification - { - JsonConverters.JsonVerificationConverter.RegisterVerification(); - } + /// + public async Task CreateTransactionAsync( + CreateTransactionRequest request, + CancellationToken cancellationToken = default) + { + request.ThrowIfNullOrEmpty(nameof(request)); - /// - public async Task CreateTransactionAsync( - Transaction transaction) - => await CreateTransactionAsync(transaction, default) - .ConfigureAwait(false); - - /// - public async Task CreateTransactionAsync( - Transaction transaction, - CancellationToken cancellationToken = default) - { - if (transaction == null) { - throw new ArgumentNullException(nameof(transaction)); - } - - var result = await client - .PostAsync( - "transaction", - JsonContent.From(transaction), - cancellationToken) - .EnsureSignhostSuccessStatusCodeAsync() - .ConfigureAwait(false); - - return await result.Content.FromJsonAsync() - .ConfigureAwait(false); - } + var result = await client + .PostAsync("transaction", JsonContent.From(request), cancellationToken) + .EnsureSignhostSuccessStatusCodeAsync() + .ConfigureAwait(false); - /// - public async Task> GetTransactionResponseAsync( - string transactionId) - => await GetTransactionResponseAsync(transactionId, default) - .ConfigureAwait(false); - - /// - public async Task> GetTransactionResponseAsync( - string transactionId, - CancellationToken cancellationToken = default) - { - if (transactionId == null) { - throw new ArgumentNullException(nameof(transactionId)); - } - - if (string.IsNullOrWhiteSpace(transactionId)) { - throw new ArgumentException("Cannot be empty or contain only whitespaces.", nameof(transactionId)); - } - - var result = await client - .GetAsync( - "transaction".JoinPaths(transactionId), - cancellationToken) - .EnsureSignhostSuccessStatusCodeAsync(HttpStatusCode.Gone) - .ConfigureAwait(false); - var transaction = await result.Content.FromJsonAsync() - .ConfigureAwait(false); - - return new ApiResponse(result, transaction); - } + return await result.Content.FromJsonAsync().ConfigureAwait(false) + ?? throw new InvalidOperationException("Failed to deserialize the transaction."); + } - /// - public async Task GetTransactionAsync(string transactionId) - => await GetTransactionAsync(transactionId, default) - .ConfigureAwait(false); - - /// - public async Task GetTransactionAsync( - string transactionId, - CancellationToken cancellationToken = default) - { - var response = await GetTransactionResponseAsync( - transactionId, - cancellationToken) - .ConfigureAwait(false); + /// + public async Task> GetTransactionResponseAsync( + string transactionId, + CancellationToken cancellationToken = default) + { + transactionId.ThrowIfNullOrEmpty(nameof(transactionId)); - response.EnsureAvailableStatusCode(); + var result = await client + .GetAsync("transaction".JoinPaths(transactionId), cancellationToken) + .EnsureSignhostSuccessStatusCodeAsync(HttpStatusCode.Gone) + .ConfigureAwait(false); + var transaction = await result.Content.FromJsonAsync().ConfigureAwait(false); - return response.Value; - } + return new ApiResponse(result, transaction); + } - public async Task DeleteTransactionAsync( - string transactionId, - CancellationToken cancellationToken = default) - => await DeleteTransactionAsync( - transactionId, - default, - cancellationToken).ConfigureAwait(false); - - /// - public async Task DeleteTransactionAsync( - string transactionId, - DeleteTransactionOptions options) - => await DeleteTransactionAsync( - transactionId, - options, - default).ConfigureAwait(false); - - /// - public async Task DeleteTransactionAsync( - string transactionId, - DeleteTransactionOptions options, - CancellationToken cancellationToken = default) - { - if (transactionId == null) { - throw new ArgumentNullException(nameof(transactionId)); - } - - if (string.IsNullOrWhiteSpace(transactionId)) { - throw new ArgumentException("Cannot be empty or contain only whitespaces.", nameof(transactionId)); - } - - if (options == null) { - options = new DeleteTransactionOptions(); - } - - var request = new HttpRequestMessage(HttpMethod.Delete, "transaction".JoinPaths(transactionId)); - request.Content = JsonContent.From(options); - await client - .SendAsync( - request, - cancellationToken) - .EnsureSignhostSuccessStatusCodeAsync() - .ConfigureAwait(false); - } + /// + public async Task GetTransactionAsync( + string transactionId, + CancellationToken cancellationToken = default) + { + var response = await GetTransactionResponseAsync(transactionId, cancellationToken) + .ConfigureAwait(false); - /// - public async Task AddOrReplaceFileMetaToTransactionAsync( - FileMeta fileMeta, - string transactionId, - string fileId) - => await AddOrReplaceFileMetaToTransactionAsync( - fileMeta, - transactionId, - fileId, - default).ConfigureAwait(false); - - /// - public async Task AddOrReplaceFileMetaToTransactionAsync( - FileMeta fileMeta, - string transactionId, - string fileId, - CancellationToken cancellationToken = default) - { - if (fileMeta == null) { - throw new ArgumentNullException("fileMeta"); - } - - if (transactionId == null) { - throw new ArgumentNullException(nameof(transactionId)); - } - - if (string.IsNullOrWhiteSpace(transactionId)) { - throw new ArgumentException("Cannot be empty or contain only whitespaces.", nameof(transactionId)); - } - - if (fileId == null) { - throw new ArgumentNullException(nameof(fileId)); - } - - if (string.IsNullOrWhiteSpace(fileId)) { - throw new ArgumentException("Cannot be empty or contain only whitespaces.", nameof(fileId)); - } - - await client - .PutAsync( - "transaction".JoinPaths(transactionId, "file", fileId), - JsonContent.From(fileMeta), - cancellationToken) - .EnsureSignhostSuccessStatusCodeAsync() - .ConfigureAwait(false); - } + await response.EnsureAvailableStatusCodeAsync(cancellationToken).ConfigureAwait(false); + + return response.Value + ?? throw new InvalidOperationException("Failed to deserialize the transaction."); + } + + /// + public async Task DeleteTransactionAsync( + string transactionId, + DeleteTransactionOptions? options = default, + CancellationToken cancellationToken = default) + { + transactionId.ThrowIfNullOrEmpty(nameof(transactionId)); + options ??= new DeleteTransactionOptions(); + + var request = new HttpRequestMessage( + HttpMethod.Delete, + "transaction".JoinPaths(transactionId)) { + Content = JsonContent.From(options), + }; + + await client + .SendAsync(request, cancellationToken) + .EnsureSignhostSuccessStatusCodeAsync() + .ConfigureAwait(false); + } - /// - public async Task AddOrReplaceFileToTransactionAsync( + /// + public async Task AddOrReplaceFileMetaToTransactionAsync( + FileMeta fileMeta, + string transactionId, + string fileId, + CancellationToken cancellationToken = default) + { + fileMeta.ThrowIfNullOrEmpty(nameof(fileMeta)); + transactionId.ThrowIfNullOrEmpty(nameof(transactionId)); + fileId.ThrowIfNullOrEmpty(nameof(fileId)); + + await client + .PutAsync( + "transaction".JoinPaths(transactionId, "file", fileId), + JsonContent.From(fileMeta), + cancellationToken) + .EnsureSignhostSuccessStatusCodeAsync() + .ConfigureAwait(false); + } + + /// + public async Task AddOrReplaceFileToTransactionAsync( Stream fileStream, string transactionId, string fileId, - FileUploadOptions uploadOptions) - => await AddOrReplaceFileToTransactionAsync( + FileUploadOptions? uploadOptions = default, + CancellationToken cancellationToken = default) + { + fileStream.ThrowIfNullOrEmpty(nameof(fileStream)); + transactionId.ThrowIfNullOrEmpty(nameof(transactionId)); + fileId.ThrowIfNullOrEmpty(nameof(fileId)); + + uploadOptions ??= new FileUploadOptions(); + + var content = new StreamContent(fileStream).WithDigest( + fileStream, + uploadOptions.DigestOptions); + content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); + + await client + .PutAsync( + "transaction".JoinPaths(transactionId, "file", fileId), + content, + cancellationToken) + .EnsureSignhostSuccessStatusCodeAsync() + .ConfigureAwait(false); + } + + /// + public async Task AddOrReplaceFileToTransactionAsync( + string filePath, + string transactionId, + string fileId, + FileUploadOptions? uploadOptions = default, + CancellationToken cancellationToken = default) + { + filePath.ThrowIfNullOrEmpty(nameof(filePath)); + + using Stream fileStream = File.Open( + filePath, + FileMode.Open, + FileAccess.Read, + FileShare.Delete | FileShare.Read); + + await AddOrReplaceFileToTransactionAsync( fileStream, transactionId, fileId, uploadOptions, - default).ConfigureAwait(false); - - /// - public async Task AddOrReplaceFileToTransactionAsync( - Stream fileStream, - string transactionId, - string fileId, - FileUploadOptions uploadOptions, - CancellationToken cancellationToken = default) - { - if (fileStream == null) { - throw new ArgumentNullException(nameof(fileStream)); - } - - if (transactionId == null) { - throw new ArgumentNullException(nameof(transactionId)); - } - - if (string.IsNullOrWhiteSpace(transactionId)) { - throw new ArgumentException("Cannot be empty or contain only whitespaces.", nameof(transactionId)); - } - - if (fileId == null) { - throw new ArgumentNullException(nameof(fileId)); - } - - if (string.IsNullOrWhiteSpace(fileId)) { - throw new ArgumentException("Cannot be empty or contain only whitespaces.", nameof(fileId)); - } - - if (uploadOptions == null) { - uploadOptions = new FileUploadOptions(); - } - - var content = new StreamContent(fileStream) - .WithDigest(fileStream, uploadOptions.DigestOptions); - content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); - - await client - .PutAsync( - "transaction".JoinPaths(transactionId, "file", fileId), - content, - cancellationToken) - .EnsureSignhostSuccessStatusCodeAsync() - .ConfigureAwait(false); - } + cancellationToken) + .ConfigureAwait(false); + } - /// - public Task AddOrReplaceFileToTransaction( - Stream fileStream, - string transactionId, - string fileId) - { - return AddOrReplaceFileToTransactionAsync( - fileStream, - transactionId, - fileId, - null); - } + /// + public async Task StartTransactionAsync( + string transactionId, + CancellationToken cancellationToken = default) + { + transactionId.ThrowIfNullOrEmpty(nameof(transactionId)); - /// - public async Task AddOrReplaceFileToTransactionAsync( - string filePath, - string transactionId, - string fileId, - FileUploadOptions uploadOptions) - => await AddOrReplaceFileToTransactionAsync( - filePath, - transactionId, - fileId, - uploadOptions, - default).ConfigureAwait(false); - - /// - public async Task AddOrReplaceFileToTransactionAsync( - string filePath, - string transactionId, - string fileId, - FileUploadOptions uploadOptions, - CancellationToken cancellationToken = default) - { - if (filePath == null) { - throw new ArgumentNullException(nameof(filePath)); - } - - using (Stream fileStream = System.IO.File.Open( - filePath, - FileMode.Open, - FileAccess.Read, - FileShare.Delete | FileShare.Read)) - { - await AddOrReplaceFileToTransactionAsync( - fileStream, - transactionId, - fileId, - uploadOptions, - cancellationToken) - .ConfigureAwait(false); - } + if (string.IsNullOrWhiteSpace(transactionId)) { + throw new ArgumentException( + "Cannot be empty or contain only whitespaces.", + nameof(transactionId)); } - /// - public Task AddOrReplaceFileToTransaction( - string filePath, - string transactionId, - string fileId) - { - return AddOrReplaceFileToTransactionAsync( - filePath, - transactionId, - fileId, - null); - } + await client + .PutAsync("transaction".JoinPaths(transactionId, "start"), null, cancellationToken) + .EnsureSignhostSuccessStatusCodeAsync() + .ConfigureAwait(false); + } - /// - public async Task StartTransactionAsync( - string transactionId) - => await StartTransactionAsync(transactionId, default) - .ConfigureAwait(false); - - /// - public async Task StartTransactionAsync( - string transactionId, - CancellationToken cancellationToken = default) - { - if (transactionId == null) { - throw new ArgumentNullException(nameof(transactionId)); - } - - if (string.IsNullOrWhiteSpace(transactionId)) { - throw new ArgumentException("Cannot be empty or contain only whitespaces.", nameof(transactionId)); - } - - await client - .PutAsync( - "transaction".JoinPaths(transactionId, "start"), - null, - cancellationToken) - .EnsureSignhostSuccessStatusCodeAsync() - .ConfigureAwait(false); - } + /// + public async Task GetReceiptAsync( + string transactionId, + CancellationToken cancellationToken = default) + { + transactionId.ThrowIfNullOrEmpty(nameof(transactionId)); - /// - public async Task GetReceiptAsync(string transactionId) - => await GetReceiptAsync(transactionId, default) - .ConfigureAwait(false); - - /// - public async Task GetReceiptAsync( - string transactionId, - CancellationToken cancellationToken = default) - { - if (transactionId == null) { - throw new ArgumentNullException(nameof(transactionId)); - } - - if (string.IsNullOrWhiteSpace(transactionId)) { - throw new ArgumentException("Cannot be empty or contain only whitespaces.", nameof(transactionId)); - } - - var result = await client - .GetStreamAsync( - "file".JoinPaths("receipt", transactionId)) - .ConfigureAwait(false); - - return result; + if (string.IsNullOrWhiteSpace(transactionId)) { + throw new ArgumentException( + "Cannot be empty or contain only whitespaces.", + nameof(transactionId)); } - /// - public async Task GetDocumentAsync( - string transactionId, - string fileId) - => await GetDocumentAsync(transactionId, fileId, default) - .ConfigureAwait(false); - - /// - public async Task GetDocumentAsync( - string transactionId, - string fileId, - CancellationToken cancellationToken = default) - { - if (transactionId == null) { - throw new ArgumentNullException(nameof(transactionId)); - } - - if (string.IsNullOrWhiteSpace(transactionId)) { - throw new ArgumentException("Cannot be empty or contain only whitespaces.", nameof(transactionId)); - } - - if (fileId == null) { - throw new ArgumentNullException(nameof(fileId)); - } - - if (string.IsNullOrWhiteSpace(fileId)) { - throw new ArgumentException("Cannot be empty or contain only whitespaces.", nameof(fileId)); - } - - var result = await client - .GetStreamAsync( - "transaction".JoinPaths(transactionId, "file", fileId)) - .ConfigureAwait(false); - - return result; + var response = await client + .GetAsync( + "file".JoinPaths("receipt", transactionId), + cancellationToken) + .EnsureSignhostSuccessStatusCodeAsync() + .ConfigureAwait(false); + + return await response.Content +#if NET8_0_OR_GREATER + .ReadAsStreamAsync(cancellationToken) +#else + .ReadAsStreamAsync() +#endif + .ConfigureAwait(false); + } + + /// + public async Task GetDocumentAsync( + string transactionId, + string fileId, + CancellationToken cancellationToken = default) + { + transactionId.ThrowIfNullOrEmpty(nameof(transactionId)); + + if (string.IsNullOrWhiteSpace(transactionId)) { + throw new ArgumentException( + "Cannot be empty or contain only whitespaces.", + nameof(transactionId)); } - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); + fileId.ThrowIfNullOrEmpty(nameof(fileId)); + + if (string.IsNullOrWhiteSpace(fileId)) { + throw new ArgumentException( + "Cannot be empty or contain only whitespaces.", + nameof(fileId)); } - /// - /// Disposes the instance. - /// - /// Is callled. - protected virtual void Dispose(bool disposing) - { - if (disposing) { - client?.Dispose(); - } + var response = await client + .GetAsync( + "transaction".JoinPaths(transactionId, "file", fileId), + cancellationToken) + .EnsureSignhostSuccessStatusCodeAsync() + .ConfigureAwait(false); + + return await response.Content +#if NET8_0_OR_GREATER + .ReadAsStreamAsync(cancellationToken) +#else + .ReadAsStreamAsync() +#endif + .ConfigureAwait(false); + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes the instance. + /// + /// Is callled. + protected virtual void Dispose(bool disposing) + { + if (disposing) { + client?.Dispose(); } } + + private static string? GetVersion() + => typeof(SignhostApiClient) +#if TYPEINFO + .GetTypeInfo() +#endif + ?.Assembly?.GetCustomAttribute() + ?.Version; } diff --git a/src/SignhostAPIClient/Rest/SignHostApiClientSettings.cs b/src/SignhostAPIClient/Rest/SignHostApiClientSettings.cs index f93a6c9b..9512f2b8 100644 --- a/src/SignhostAPIClient/Rest/SignHostApiClientSettings.cs +++ b/src/SignhostAPIClient/Rest/SignHostApiClientSettings.cs @@ -1,29 +1,33 @@ -using System; +using System; -namespace Signhost.APIClient.Rest +namespace Signhost.APIClient.Rest; + +public class SignhostApiClientSettings + : ISignhostApiClientSettings { - public class SignHostApiClientSettings - : ISignHostApiClientSettings + public const string DefaultEndpoint = "https://api.signhost.com/api/"; + + public SignhostApiClientSettings(string appkey, string userToken) { - public const string DefaultEndpoint = "https://api.signhost.com/api/"; + appkey.ThrowIfNullOrEmpty(nameof(appkey)); + userToken.ThrowIfNullOrEmpty(nameof(userToken)); - public SignHostApiClientSettings(string appkey, string userToken) - { - APPKey = appkey; - UserToken = userToken; - } + APPKey = appkey; + UserToken = userToken; + } - public SignHostApiClientSettings(string appkey) - { - APPKey = appkey; - } + public SignhostApiClientSettings(string appkey) + { + appkey.ThrowIfNullOrEmpty(nameof(appkey)); - public string UserToken { get; set; } + APPKey = appkey; + } - public string APPKey { get; private set; } + public string? UserToken { get; set; } - public string Endpoint { get; set; } = DefaultEndpoint; + public string APPKey { get; private set; } - public Action AddHeader { get; set; } - } + public string Endpoint { get; set; } = DefaultEndpoint; + + public Action? AddHeader { get; set; } } diff --git a/src/SignhostAPIClient/Rest/SignhostApiReceiver.cs b/src/SignhostAPIClient/Rest/SignhostApiReceiver.cs index a3bfde6a..4a935e4a 100644 --- a/src/SignhostAPIClient/Rest/SignhostApiReceiver.cs +++ b/src/SignhostAPIClient/Rest/SignhostApiReceiver.cs @@ -1,90 +1,102 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security.Cryptography; using System.Text; -using Newtonsoft.Json; +using System.Text.Json; using Signhost.APIClient.Rest; using Signhost.APIClient.Rest.DataObjects; -namespace Signhost.APIClient +namespace Signhost.APIClient; + +/// +/// Implements the interface which provides +/// a Signhost API receiver implementation. +/// +public class SignhostApiReceiver + : ISignhostApiReceiver { + private readonly SignhostApiReceiverSettings settings; + /// - /// Implements the interface which provides - /// a Signhost API receiver implementation. + /// Initializes a new instance of the class. + /// Set your SharedSecret by creating a . /// - public class SignhostApiReceiver - : ISignhostApiReceiver + /// + /// Settings for the receiver. + /// + public SignhostApiReceiver(SignhostApiReceiverSettings receiverSettings) { - private readonly SignhostApiReceiverSettings settings; + receiverSettings.ThrowIfNullOrEmpty(nameof(receiverSettings)); - /// - /// Initializes a new instance of the class. - /// Set your SharedSecret by creating a . - /// - /// - public SignhostApiReceiver(SignhostApiReceiverSettings receiverSettings) - { - this.settings = receiverSettings; - } + settings = receiverSettings; + } - /// - public bool IsPostbackChecksumValid( - IDictionary headers, - string body, - out Transaction postbackTransaction) - { - postbackTransaction = null; - string postbackChecksum; - string calculatedChecksum; - PostbackTransaction postback; + /// + public bool IsPostbackChecksumValid( + IDictionary headers, + string body, + [NotNullWhen(true)] out Transaction? postbackTransaction) + { + headers.ThrowIfNullOrEmpty(nameof(headers)); + body.ThrowIfNullOrEmpty(nameof(body)); - postback = DeserializeToPostbackTransaction(body); - postbackChecksum = GetChecksumFromHeadersOrPostback(headers, postback); - bool parametersAreValid = HasValidChecksumProperties(postbackChecksum, postback); + postbackTransaction = null; + var postback = DeserializeToPostbackTransaction(body); + if (postback is null) { + return false; + } - if (parametersAreValid) { - calculatedChecksum = CalculateChecksumFromPostback(postback); - postbackTransaction = postback; - } else { - return false; - } + string postbackChecksum = GetChecksumFromHeadersOrPostback(headers, postback); + bool parametersAreValid = HasValidChecksumProperties(postbackChecksum, postback); - return Equals(calculatedChecksum, postbackChecksum); + string calculatedChecksum; + if (parametersAreValid) { + calculatedChecksum = CalculateChecksumFromPostback(postback); + postbackTransaction = postback; } - - private string CalculateChecksumFromPostback(PostbackTransaction postback) - { - using (var sha1 = SHA1.Create()) { - var checksumBytes = sha1.ComputeHash(Encoding.UTF8.GetBytes( - $"{postback.Id}||{(int)postback.Status}|{settings.SharedSecret}")); - return BitConverter.ToString(checksumBytes) - .Replace("-", string.Empty) - .ToLower(); - } + else { + return false; } - private PostbackTransaction DeserializeToPostbackTransaction(string body) - { - return JsonConvert.DeserializeObject(body); - } + return Equals(calculatedChecksum, postbackChecksum); + } - private string GetChecksumFromHeadersOrPostback( - IDictionary headers, - PostbackTransaction postback) - { - string[] postbackChecksumArray; - if (headers.TryGetValue("Checksum", out postbackChecksumArray)) { - return postbackChecksumArray.First(); - } - else { - return postback.Checksum; - } - } + private static PostbackTransaction? DeserializeToPostbackTransaction(string body) + { + return JsonSerializer.Deserialize( + body, + SignhostJsonSerializerOptions.Default); + } - private bool HasValidChecksumProperties(string postbackChecksum, PostbackTransaction postback) - { - return !string.IsNullOrWhiteSpace(postbackChecksum) && !string.IsNullOrWhiteSpace(postback.Id); + private static string GetChecksumFromHeadersOrPostback( + IDictionary headers, + PostbackTransaction postback) + { + if ( + headers.TryGetValue("Checksum", out string[]? postbackChecksumArray) && + postbackChecksumArray is not null + ) { + return postbackChecksumArray.First(); } + else { + return postback.Checksum; + } + } + + private static bool HasValidChecksumProperties(string postbackChecksum, PostbackTransaction postback) + { + return !string.IsNullOrWhiteSpace(postbackChecksum) && !string.IsNullOrWhiteSpace(postback.Id); + } + + private string CalculateChecksumFromPostback(PostbackTransaction postback) + { + using var sha1 = SHA1.Create(); + byte[] checksumBytes = sha1.ComputeHash(Encoding.UTF8.GetBytes( + $"{postback.Id}||{(int)postback.Status}|{settings.SharedSecret}")); + return BitConverter.ToString(checksumBytes) + .Replace("-", string.Empty) + .ToLower(); } } diff --git a/src/SignhostAPIClient/Rest/SignhostApiReceiverSettings.cs b/src/SignhostAPIClient/Rest/SignhostApiReceiverSettings.cs index cb8fd629..a8f8f97d 100644 --- a/src/SignhostAPIClient/Rest/SignhostApiReceiverSettings.cs +++ b/src/SignhostAPIClient/Rest/SignhostApiReceiverSettings.cs @@ -1,21 +1,20 @@ -using System; +namespace Signhost.APIClient.Rest; -namespace Signhost.APIClient.Rest +/// +/// Registers the necessary settings for the class. +/// +public class SignhostApiReceiverSettings { - /// - /// Registers the necessary settings for the class. - /// - public class SignhostApiReceiverSettings + public SignhostApiReceiverSettings(string sharedsecret) { - public SignhostApiReceiverSettings(string sharedsecret) - { - SharedSecret = sharedsecret; - } + sharedsecret.ThrowIfNullOrEmpty(nameof(sharedsecret)); - /// - /// Gets the shared secret. - /// - /// The shared secret key issued by Signhost.com. - public string SharedSecret { get; private set; } + SharedSecret = sharedsecret; } + + /// + /// Gets the shared secret. + /// + /// The shared secret key issued by Signhost.com. + public string SharedSecret { get; private set; } } diff --git a/src/SignhostAPIClient/Rest/SignhostJsonSerializerOptions.cs b/src/SignhostAPIClient/Rest/SignhostJsonSerializerOptions.cs new file mode 100644 index 00000000..7c7040cf --- /dev/null +++ b/src/SignhostAPIClient/Rest/SignhostJsonSerializerOptions.cs @@ -0,0 +1,20 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Signhost.APIClient.Rest; + +/// +/// Centralized JSON serialization options for Signhost API. +/// +public static class SignhostJsonSerializerOptions +{ + /// + /// Gets the default JSON serializer options. + /// + public static JsonSerializerOptions Default { get; } = + new JsonSerializerOptions { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + Converters = { new JsonStringEnumConverter() }, + }; +} diff --git a/src/SignhostAPIClient/Rest/StreamContentDigestOptionsExtensions.cs b/src/SignhostAPIClient/Rest/StreamContentDigestOptionsExtensions.cs index 2aad9008..09d2e9d7 100644 --- a/src/SignhostAPIClient/Rest/StreamContentDigestOptionsExtensions.cs +++ b/src/SignhostAPIClient/Rest/StreamContentDigestOptionsExtensions.cs @@ -3,105 +3,94 @@ using System.Net.Http; using System.Security.Cryptography; -namespace Signhost.APIClient.Rest +namespace Signhost.APIClient.Rest; + +/// +/// digest extensions. +/// +public static class StreamContentDigestOptionsExtensions { /// - /// digest extensions. + /// Digest extension method on the . /// - public static class StreamContentDigestOptionsExtensions + /// + /// of the filestream. + /// No digest is calculated if the stream is not . + /// digest options to use. + /// . + public static StreamContent WithDigest( + this StreamContent content, + Stream fileStream, + FileDigestOptions options) { - /// - /// Digest extension method on the . - /// - /// - /// of the filestream. - /// No digest is calculated if the stream is not . - /// digest options to use. - /// . - public static StreamContent WithDigest( - this StreamContent content, - Stream fileStream, - FileDigestOptions options) - { - if (!options.UseFileDigesting || options.DigestHashAlgorithm == null) { - return content; - } - - SetHashValue(fileStream, options); - - string base64Digest = Convert.ToBase64String(options.DigestHashValue); - - content.Headers.Add("Digest", $"{options.DigestHashAlgorithm}={base64Digest}"); + content.ThrowIfNullOrEmpty(nameof(content)); + fileStream.ThrowIfNullOrEmpty(nameof(fileStream)); + options.ThrowIfNullOrEmpty(nameof(options)); + if ( + !options.UseFileDigesting || + options.DigestHashAlgorithm == DigestHashAlgorithm.None + ) { return content; } - private static void SetHashValue( - Stream fileStream, - FileDigestOptions options) - { - if (options.DigestHashValue != null) { - return; - } + SetHashValue(fileStream, options); + if (options.DigestHashValue is null) { + throw new InvalidOperationException( + "Digest hash value is not set after calculating it."); + } - if (!fileStream.CanSeek) { - return; - } + string base64Digest = Convert.ToBase64String(options.DigestHashValue); - long position = fileStream.Position; + content.Headers.Add("Digest", $"{GetDigestHashAlgorithmName(options)}={base64Digest}"); - using (var algo = HashAlgorithmCreate(options)) { - options.DigestHashValue = algo.ComputeHash(fileStream); - } + return content; + } + + private static string GetDigestHashAlgorithmName(FileDigestOptions options) + { + return options.DigestHashAlgorithm switch { + DigestHashAlgorithm.SHA256 => "SHA-256", + DigestHashAlgorithm.SHA512 => "SHA-512", - fileStream.Position = position; + _ => throw new InvalidOperationException( + $"No hash algorithm name for '{options.DigestHashAlgorithm}'."), + }; + } + + private static void SetHashValue( + Stream fileStream, + FileDigestOptions options) + { + if (options.DigestHashValue is not null) { + return; } - private static HashAlgorithm HashAlgorithmCreate( - FileDigestOptions options) - { - string algorithmName = options.DigestHashAlgorithm; - HashAlgorithm algorithm = null; + if (!fileStream.CanSeek) { + return; + } -#if NETSTANDARD1_4 || NETSTANDARD2_0 - switch (algorithmName) { - case "SHA1": - case "SHA-1": - algorithm = SHA1.Create(); - break; - case "SHA256": - case "SHA-256": - algorithm = SHA256.Create(); - break; - case "SHA384": - case "SHA-384": - algorithm = SHA384.Create(); - break; - case "SHA512": - case "SHA-512": - algorithm = SHA512.Create(); - break; - } -#else - algorithm = HashAlgorithm.Create(algorithmName); -#endif - if (algorithm == null && options.DigestHashValue == null) { - algorithm = DefaultHashAlgorithm(); - options.DigestHashAlgorithm = algorithm.GetType().Name; - } + long position = fileStream.Position; - if (algorithm == null) { - throw new InvalidOperationException($"No hash algorithm for '{algorithmName}'"); - } + using var algo = HashAlgorithmCreate(options); + options.DigestHashValue = algo.ComputeHash(fileStream); - return algorithm; - } + fileStream.Position = position; + } - private static HashAlgorithm DefaultHashAlgorithm() => -#if NETSTANDARD1_4 || NETSTANDARD2_0 - SHA256.Create(); + private static HashAlgorithm HashAlgorithmCreate( + FileDigestOptions options) + { + return options.DigestHashAlgorithm switch { +#if NET462 + DigestHashAlgorithm.SHA256 => HashAlgorithm.Create("SHA256"), + DigestHashAlgorithm.SHA512 => HashAlgorithm.Create("SHA512"), #else - HashAlgorithm.Create(); + DigestHashAlgorithm.SHA256 => SHA256.Create(), + DigestHashAlgorithm.SHA512 => SHA512.Create(), #endif + _ => throw new InvalidOperationException( + $"No hash algorithm for '{options.DigestHashAlgorithm}'."), + }; } } diff --git a/src/SignhostAPIClient/Rest/ThrowIfNullExtensions.cs b/src/SignhostAPIClient/Rest/ThrowIfNullExtensions.cs new file mode 100644 index 00000000..ea627ad8 --- /dev/null +++ b/src/SignhostAPIClient/Rest/ThrowIfNullExtensions.cs @@ -0,0 +1,26 @@ +using System; + +namespace Signhost.APIClient.Rest; + +/// +/// Polyfill extensions for ArgumentNullException.ThrowIfNull that works across all target frameworks. +/// +internal static class ThrowIfNullExtensions +{ + /// + /// Throws an if the specified argument is null. + /// + /// The value to check for null. + /// The name of the parameter. + /// is null. + internal static void ThrowIfNullOrEmpty(this object? value, string? paramName = null) + { + if (value is null) { + throw new ArgumentNullException(paramName); + } + + if (value is string str && string.IsNullOrWhiteSpace(str)) { + throw new ArgumentException("Argument cannot be empty or whitespace.", paramName); + } + } +} diff --git a/src/SignhostAPIClient/Rest/UriPathExtensions.cs b/src/SignhostAPIClient/Rest/UriPathExtensions.cs index 39fb97fb..9b5639b7 100644 --- a/src/SignhostAPIClient/Rest/UriPathExtensions.cs +++ b/src/SignhostAPIClient/Rest/UriPathExtensions.cs @@ -1,25 +1,17 @@ using System; using System.Linq; -namespace Signhost.APIClient.Rest +namespace Signhost.APIClient.Rest; + +internal static class UriPathExtensions { - internal static class UriPathExtensions + internal static Uri JoinPaths( + this string url, + params string[] segments) { - internal static UriBuilder AppendPathSegment(this string url) - { - var builder = new UriBuilder(url); - - return builder; - } - - internal static Uri JoinPaths( - this string url, - params string[] segments) - { - var segmentList = segments.ToList(); - segmentList.Insert(0, url); - var escaped = segmentList.Select(seg => Uri.EscapeDataString(seg)); - return new Uri(string.Join("/", escaped), UriKind.Relative); - } + var segmentList = segments.ToList(); + segmentList.Insert(0, url); + var escaped = segmentList.Select(seg => Uri.EscapeDataString(seg)); + return new Uri(string.Join("/", escaped), UriKind.Relative); } } diff --git a/src/SignhostAPIClient/SignhostAPIClient.csproj b/src/SignhostAPIClient/SignhostAPIClient.csproj index f8acfff0..677e13d7 100644 --- a/src/SignhostAPIClient/SignhostAPIClient.csproj +++ b/src/SignhostAPIClient/SignhostAPIClient.csproj @@ -1,12 +1,11 @@ - netstandard2.0;netstandard1.4;net462 + net10.0;net9.0;net8.0;netstandard2.0;net462 + 10 ../signhost.ruleset - SERIALIZABLE - TYPEINFO TYPEINFO - SERIALIZABLE Signhost.APIClient + enable @@ -44,7 +43,7 @@ - + @@ -53,9 +52,4 @@ - - - - - diff --git a/src/SignhostAPIClient/SignhostAPIClient.v2.ncrunchproject b/src/SignhostAPIClient/SignhostAPIClient.v2.ncrunchproject deleted file mode 100644 index 3f48b966..00000000 --- a/src/SignhostAPIClient/SignhostAPIClient.v2.ncrunchproject +++ /dev/null @@ -1,29 +0,0 @@ - - true - 1000 - false - false - false - true - false - false - false - false - false - true - true - false - true - true - true - 60000 - - - - AutoDetect - STA - x86 - - - - \ No newline at end of file diff --git a/src/SignhostAPIClient/System/SerializableAttribute.cs b/src/SignhostAPIClient/System/SerializableAttribute.cs deleted file mode 100644 index 568c0bad..00000000 --- a/src/SignhostAPIClient/System/SerializableAttribute.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace System -{ -#if !SERIALIZABLE - internal sealed class SerializableAttribute - : Attribute - { - } -#endif -} diff --git a/src/signhost.ruleset b/src/signhost.ruleset index 6d703aa7..d2259345 100644 --- a/src/signhost.ruleset +++ b/src/signhost.ruleset @@ -6,8 +6,19 @@ + + + + + + + + + + +