diff --git a/Directory.Build.props b/Directory.Build.props index 1d33b07..1dd918a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -37,6 +37,17 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + stylecop.json diff --git a/Directory.Packages.props b/Directory.Packages.props index 5e16f7f..d161b1c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -31,7 +31,7 @@ - + diff --git a/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/ServiceCollectionExtensions.cs b/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/ServiceCollectionExtensions.cs index 7b7458d..bfcf0ac 100644 --- a/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/ServiceCollectionExtensions.cs +++ b/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/ServiceCollectionExtensions.cs @@ -14,6 +14,7 @@ using VMelnalksnis.PaperlessDotNet.Correspondents; using VMelnalksnis.PaperlessDotNet.Documents; using VMelnalksnis.PaperlessDotNet.Serialization; +using VMelnalksnis.PaperlessDotNet.Tasks; #if NET6_0_OR_GREATER using System.Net.Mime; @@ -53,19 +54,26 @@ static ServiceCollectionExtensions() return serviceCollection .AddSingleton() - .AddTransient() - .AddTransient(provider => + .AddScoped() + .AddScoped(provider => { var httpClient = provider.GetRequiredService().CreateClient(PaperlessOptions.Name); var options = provider.GetRequiredService(); return new(httpClient, options); }) - .AddTransient(provider => + .AddScoped(provider => { var httpClient = provider.GetRequiredService().CreateClient(PaperlessOptions.Name); var options = provider.GetRequiredService(); return new(httpClient, options); }) + .AddScoped(provider => + { + var httpClient = provider.GetRequiredService().CreateClient(PaperlessOptions.Name); + var options = provider.GetRequiredService(); + var taskClient = provider.GetRequiredService(); + return new(httpClient, options, taskClient); + }) .AddHttpClient(PaperlessOptions.Name, (provider, client) => { var options = provider.GetRequiredService>().CurrentValue; diff --git a/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/VMelnalksnis.PaperlessDotNet.DependencyInjection.csproj b/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/VMelnalksnis.PaperlessDotNet.DependencyInjection.csproj index cbc1598..9db2f99 100644 --- a/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/VMelnalksnis.PaperlessDotNet.DependencyInjection.csproj +++ b/source/VMelnalksnis.PaperlessDotNet.DependencyInjection/VMelnalksnis.PaperlessDotNet.DependencyInjection.csproj @@ -26,15 +26,4 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - diff --git a/source/VMelnalksnis.PaperlessDotNet/Correspondents/CorrespondentClient.cs b/source/VMelnalksnis.PaperlessDotNet/Correspondents/CorrespondentClient.cs index 79ff495..6aae5f5 100644 --- a/source/VMelnalksnis.PaperlessDotNet/Correspondents/CorrespondentClient.cs +++ b/source/VMelnalksnis.PaperlessDotNet/Correspondents/CorrespondentClient.cs @@ -66,7 +66,7 @@ public async Task Create(CorrespondentCreation correspondent) var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync("/api/correspondents/", content).ConfigureAwait(false); - await EnsureSuccessStatusCode(response).ConfigureAwait(false); + await response.EnsureSuccessStatusCodeAsync().ConfigureAwait(false); return (await response.Content.ReadFromJsonAsync(_context.Correspondent).ConfigureAwait(false))!; } @@ -74,17 +74,6 @@ public async Task Create(CorrespondentCreation correspondent) public async Task Delete(int id) { var response = await _httpClient.DeleteAsync($"/api/correspondents/{id}/").ConfigureAwait(false); - await EnsureSuccessStatusCode(response).ConfigureAwait(false); - } - - private static async Task EnsureSuccessStatusCode(HttpResponseMessage response) - { - if (response.IsSuccessStatusCode) - { - return; - } - - var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - throw new HttpRequestException(message); + await response.EnsureSuccessStatusCodeAsync().ConfigureAwait(false); } } diff --git a/source/VMelnalksnis.PaperlessDotNet/Documents/Document.cs b/source/VMelnalksnis.PaperlessDotNet/Documents/Document.cs index 4369666..eb9ddd1 100644 --- a/source/VMelnalksnis.PaperlessDotNet/Documents/Document.cs +++ b/source/VMelnalksnis.PaperlessDotNet/Documents/Document.cs @@ -18,9 +18,9 @@ public sealed class Document /// Gets or sets the archive serial number. [JsonPropertyName("archive_serial_number")] - public int? ArchiveSerialNumber { get; set; } + public uint? ArchiveSerialNumber { get; set; } - /// Gets or sets the correspondent id. + /// Gets or sets the id. [JsonPropertyName("correspondent")] public int? CorrespondentId { get; set; } diff --git a/source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs b/source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs index 50fa6ee..804cbbd 100644 --- a/source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs +++ b/source/VMelnalksnis.PaperlessDotNet/Documents/DocumentClient.cs @@ -2,29 +2,39 @@ // Licensed under the Apache License 2.0. // See LICENSE file in the project root for full license information. +using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; +using NodaTime.Text; + using VMelnalksnis.PaperlessDotNet.Serialization; +using VMelnalksnis.PaperlessDotNet.Tasks; namespace VMelnalksnis.PaperlessDotNet.Documents; /// public sealed class DocumentClient : IDocumentClient { + private static readonly Version _documentIdVersion = new(1, 9, 2); + private readonly HttpClient _httpClient; private readonly PaperlessJsonSerializerContext _context; + private readonly ITaskClient _taskClient; /// Initializes a new instance of the class. /// Http client configured for making requests to the Paperless API. /// Paperless specific instance of . - public DocumentClient(HttpClient httpClient, PaperlessJsonSerializerOptions serializerOptions) + /// Paperless task API client. + public DocumentClient(HttpClient httpClient, PaperlessJsonSerializerOptions serializerOptions, ITaskClient taskClient) { _httpClient = httpClient; + _taskClient = taskClient; _context = serializerOptions.Context; } @@ -54,4 +64,79 @@ public IAsyncEnumerable GetAll(int pageSize, CancellationToken cancell _context.Document, cancellationToken); } + + /// + public async Task Create(DocumentCreation document) + { + var content = new MultipartFormDataContent(); + content.Add(new StreamContent(document.Document), "document", document.FileName); + + if (document.Title is { } title) + { + content.Add(new StringContent(title), "title"); + } + + if (document.Created is { } created) + { + content.Add(new StringContent(InstantPattern.General.Format(created)), "created"); + } + + if (document.CorrespondentId is { } correspondent) + { + content.Add(new StringContent(correspondent.ToString()), "correspondent"); + } + + if (document.DocumentTypeId is { } documentType) + { + content.Add(new StringContent(documentType.ToString()), "document_type"); + } + + if (document.StoragePathId is { } storagePath) + { + content.Add(new StringContent(storagePath.ToString()), "storage_path"); + } + + foreach (var tag in document.TagIds) + { + content.Add(new StringContent(tag.ToString()), "tags"); + } + + if (document.ArchiveSerialNumber is { } archiveSerialNumber) + { + content.Add(new StringContent(archiveSerialNumber.ToString()), "archive_serial_number"); + } + + var response = await _httpClient.PostAsync("/api/documents/post_document/", content).ConfigureAwait(false); + await response.EnsureSuccessStatusCodeAsync().ConfigureAwait(false); + + // Until v1.9.2 paperless did not return the document import task id, + // so it is not possible to get the document id + var versionHeader = response.Headers.GetValues("x-version").SingleOrDefault(); + if (versionHeader is null || !Version.TryParse(versionHeader, out var version) || version <= _documentIdVersion) + { + return new ImportStarted(); + } + + var id = await response.Content.ReadFromJsonAsync(_context.Guid).ConfigureAwait(false); + var task = await _taskClient.Get(id).ConfigureAwait(false); + + while (task is not null && !task.Status.IsCompleted) + { + await Task.Delay(100).ConfigureAwait(false); + task = await _taskClient.Get(id).ConfigureAwait(false); + } + + return task switch + { + null => new ImportFailed($"Could not find the import task by the given id {id}"), + + _ when task.RelatedDocument is { } documentId => new DocumentCreated(documentId), + + _ when task.Status == PaperlessTaskStatus.Success => new ImportFailed( + $"Task status is {PaperlessTaskStatus.Success.Name}, but document id was not given"), + _ when task.Status == PaperlessTaskStatus.Failure => new ImportFailed(task.Result), + + _ => throw new ArgumentOutOfRangeException(nameof(task.Status), task.Status, "Unexpected task result"), + }; + } } diff --git a/source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreation.cs b/source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreation.cs new file mode 100644 index 0000000..706027e --- /dev/null +++ b/source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreation.cs @@ -0,0 +1,54 @@ +// Copyright 2022 Valters Melnalksnis +// Licensed under the Apache License 2.0. +// See LICENSE file in the project root for full license information. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +using NodaTime; + +namespace VMelnalksnis.PaperlessDotNet.Documents; + +/// Information needed to create a new . +[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Global", Justification = "Required endpoints for testing not implemented")] +public sealed class DocumentCreation +{ + /// Initializes a new instance of the class. + /// The document content. + /// The name of the file. + public DocumentCreation(Stream document, string fileName) + { + Document = document; + FileName = fileName; + + TagIds = Array.Empty(); + } + + /// Gets the content of the document. + public Stream Document { get; } + + /// + public string FileName { get; } + + /// + public Instant? Created { get; init; } + + /// + public string? Title { get; init; } + + /// + public int? CorrespondentId { get; init; } + + /// + public int? DocumentTypeId { get; init; } + + /// Gets the id of the storage path. + public int? StoragePathId { get; init; } + + /// + public int[] TagIds { get; init; } + + /// + public uint? ArchiveSerialNumber { get; init; } +} diff --git a/source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs b/source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs new file mode 100644 index 0000000..c8393a1 --- /dev/null +++ b/source/VMelnalksnis.PaperlessDotNet/Documents/DocumentCreationResult.cs @@ -0,0 +1,32 @@ +// Copyright 2022 Valters Melnalksnis +// Licensed under the Apache License 2.0. +// See LICENSE file in the project root for full license information. + +namespace VMelnalksnis.PaperlessDotNet.Documents; +#pragma warning disable SA1402 + +/// Base class for possible results of creating a new document. +/// +/// +/// +public abstract class DocumentCreationResult; + +/// Document was successfully created. +/// The id of the created document. +public sealed class DocumentCreated(int id) : DocumentCreationResult +{ + /// Gets the id of the created document. + public int Id { get; } = id; +} + +/// Document was successfully submitted and import was started. +/// This is only returned for version below or equal to 1.9.2. +public sealed class ImportStarted : DocumentCreationResult; + +/// Document import process failed. +/// The result message returned by paperless. +public sealed class ImportFailed(string? result) : DocumentCreationResult +{ + /// Gets the result message returned by paperless. + public string? Result { get; } = result; +} diff --git a/source/VMelnalksnis.PaperlessDotNet/Documents/IDocumentClient.cs b/source/VMelnalksnis.PaperlessDotNet/Documents/IDocumentClient.cs index 4ee3cec..7a0d129 100644 --- a/source/VMelnalksnis.PaperlessDotNet/Documents/IDocumentClient.cs +++ b/source/VMelnalksnis.PaperlessDotNet/Documents/IDocumentClient.cs @@ -27,4 +27,9 @@ public interface IDocumentClient /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. /// The document with the specified id if it exists; otherwise . Task Get(int id, CancellationToken cancellationToken = default); + + /// Creates a new document. + /// The document to create. + /// Result of creating the document. + Task Create(DocumentCreation document); } diff --git a/source/VMelnalksnis.PaperlessDotNet/Serialization/HttpClientExtensions.cs b/source/VMelnalksnis.PaperlessDotNet/Serialization/HttpClientExtensions.cs index d364fd2..308ed7b 100644 --- a/source/VMelnalksnis.PaperlessDotNet/Serialization/HttpClientExtensions.cs +++ b/source/VMelnalksnis.PaperlessDotNet/Serialization/HttpClientExtensions.cs @@ -8,6 +8,7 @@ using System.Runtime.CompilerServices; using System.Text.Json.Serialization.Metadata; using System.Threading; +using System.Threading.Tasks; namespace VMelnalksnis.PaperlessDotNet.Serialization; @@ -37,4 +38,15 @@ internal static class HttpClientExtensions next = paginatedList.Next?.PathAndQuery; } } + + internal static async Task EnsureSuccessStatusCodeAsync(this HttpResponseMessage response) + { + if (response.IsSuccessStatusCode) + { + return; + } + + var message = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + throw new HttpRequestException(message); + } } diff --git a/source/VMelnalksnis.PaperlessDotNet/Serialization/PaperlessJsonSerializerContext.cs b/source/VMelnalksnis.PaperlessDotNet/Serialization/PaperlessJsonSerializerContext.cs index ff0dc5b..36557bd 100644 --- a/source/VMelnalksnis.PaperlessDotNet/Serialization/PaperlessJsonSerializerContext.cs +++ b/source/VMelnalksnis.PaperlessDotNet/Serialization/PaperlessJsonSerializerContext.cs @@ -2,10 +2,12 @@ // Licensed under the Apache License 2.0. // See LICENSE file in the project root for full license information. +using System.Collections.Generic; using System.Text.Json.Serialization; using VMelnalksnis.PaperlessDotNet.Correspondents; using VMelnalksnis.PaperlessDotNet.Documents; +using VMelnalksnis.PaperlessDotNet.Tasks; namespace VMelnalksnis.PaperlessDotNet.Serialization; @@ -16,4 +18,5 @@ namespace VMelnalksnis.PaperlessDotNet.Serialization; [JsonSerializable(typeof(CorrespondentCreation))] [JsonSerializable(typeof(PaginatedList))] [JsonSerializable(typeof(Document))] +[JsonSerializable(typeof(List))] internal partial class PaperlessJsonSerializerContext : JsonSerializerContext; diff --git a/source/VMelnalksnis.PaperlessDotNet/Serialization/PaperlessJsonSerializerOptions.cs b/source/VMelnalksnis.PaperlessDotNet/Serialization/PaperlessJsonSerializerOptions.cs index a950320..36b1a5a 100644 --- a/source/VMelnalksnis.PaperlessDotNet/Serialization/PaperlessJsonSerializerOptions.cs +++ b/source/VMelnalksnis.PaperlessDotNet/Serialization/PaperlessJsonSerializerOptions.cs @@ -12,6 +12,7 @@ using NodaTime.Serialization.SystemTextJson; using VMelnalksnis.PaperlessDotNet.Correspondents; +using VMelnalksnis.PaperlessDotNet.Tasks; namespace VMelnalksnis.PaperlessDotNet.Serialization; @@ -26,6 +27,8 @@ public PaperlessJsonSerializerOptions(IDateTimeZoneProvider dateTimeZoneProvider .ConfigureForNodaTime(dateTimeZoneProvider); options.Converters.Add(new SmartEnumValueConverter()); + options.Converters.Add(new SmartEnumNameConverter()); + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; Context = new(options); diff --git a/source/VMelnalksnis.PaperlessDotNet/Serialization/YearMonthDayCalendar.cs b/source/VMelnalksnis.PaperlessDotNet/Serialization/YearMonthDayCalendar.cs new file mode 100644 index 0000000..0fcfe5c --- /dev/null +++ b/source/VMelnalksnis.PaperlessDotNet/Serialization/YearMonthDayCalendar.cs @@ -0,0 +1,9 @@ +// Copyright 2022 Valters Melnalksnis +// Licensed under the Apache License 2.0. +// See LICENSE file in the project root for full license information. + +// ReSharper disable once CheckNamespace +namespace NodaTime; + +[System.Obsolete("Proxy type for System.Text.Json to work around https://github.com/dotnet/runtime/issues/66679#issuecomment-1189027602")] +internal readonly struct YearMonthDayCalendar; diff --git a/source/VMelnalksnis.PaperlessDotNet/Tasks/ITaskClient.cs b/source/VMelnalksnis.PaperlessDotNet/Tasks/ITaskClient.cs new file mode 100644 index 0000000..845b921 --- /dev/null +++ b/source/VMelnalksnis.PaperlessDotNet/Tasks/ITaskClient.cs @@ -0,0 +1,25 @@ +// Copyright 2022 Valters Melnalksnis +// Licensed under the Apache License 2.0. +// See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace VMelnalksnis.PaperlessDotNet.Tasks; + +/// Paperless API client for working with tasks. +public interface ITaskClient +{ + /// Gets all tasks. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A collection of all tasks. + Task> GetAll(CancellationToken cancellationToken = default); + + /// Gets the task with the specific id. + /// The id of the task. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// The task with the specified id if it exists; otherwise null. + Task Get(Guid taskId, CancellationToken cancellationToken = default); +} diff --git a/source/VMelnalksnis.PaperlessDotNet/Tasks/PaperlessTask.cs b/source/VMelnalksnis.PaperlessDotNet/Tasks/PaperlessTask.cs new file mode 100644 index 0000000..14fb418 --- /dev/null +++ b/source/VMelnalksnis.PaperlessDotNet/Tasks/PaperlessTask.cs @@ -0,0 +1,52 @@ +// Copyright 2022 Valters Melnalksnis +// Licensed under the Apache License 2.0. +// See LICENSE file in the project root for full license information. + +using System; +using System.Text.Json.Serialization; + +using NodaTime; + +using VMelnalksnis.PaperlessDotNet.Documents; + +namespace VMelnalksnis.PaperlessDotNet.Tasks; + +/// A long running/background task managed by paperless. +public sealed class PaperlessTask +{ + /// Gets or sets the sequential id of the task. + public int Id { get; set; } + + /// Gets or sets the unique id of the task. + [JsonPropertyName("task_id")] + public Guid TaskId { get; set; } + + /// Gets or sets the name of the file related to this task. + [JsonPropertyName("task_file_name")] + public string? TaskFileName { get; set; } + + /// Gets or sets the datetime when the task was created. + [JsonPropertyName("date_created")] + public OffsetDateTime DateCreated { get; set; } + + /// Gets or sets the datetime when the task was completed. + [JsonPropertyName("date_done")] + public OffsetDateTime? DateDone { get; set; } + + /// Gets or sets the type of the task. + public string? Type { get; set; } + + /// Gets or sets the status of the task. + public PaperlessTaskStatus Status { get; set; } = null!; + + /// Gets or sets detailed message about the result of the task. + public string? Result { get; set; } + + /// Gets or sets a value indicating whether a user has acknowledged the result of the task. + public bool Acknowledged { get; set; } + + /// Gets or sets the id of the related to this task. + [JsonPropertyName("related_document")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public int? RelatedDocument { get; set; } +} diff --git a/source/VMelnalksnis.PaperlessDotNet/Tasks/PaperlessTaskStatus.cs b/source/VMelnalksnis.PaperlessDotNet/Tasks/PaperlessTaskStatus.cs new file mode 100644 index 0000000..26db5c3 --- /dev/null +++ b/source/VMelnalksnis.PaperlessDotNet/Tasks/PaperlessTaskStatus.cs @@ -0,0 +1,31 @@ +// Copyright 2022 Valters Melnalksnis +// Licensed under the Apache License 2.0. +// See LICENSE file in the project root for full license information. + +using Ardalis.SmartEnum; + +namespace VMelnalksnis.PaperlessDotNet.Tasks; + +/// Paperless background task status. +public sealed class PaperlessTaskStatus : SmartEnum +{ + /// Task is queued and has not been started yet. + public static readonly PaperlessTaskStatus Pending = new("PENDING", 1); + + /// Task has been started and is currently running. + public static readonly PaperlessTaskStatus Started = new("STARTED", 2); + + /// Task completed successfully. + public static readonly PaperlessTaskStatus Success = new("SUCCESS", 3); + + /// Task completed unsuccessfully. + public static readonly PaperlessTaskStatus Failure = new("FAILURE", 4); + + private PaperlessTaskStatus(string name, int value) + : base(name, value) + { + } + + /// Gets a value indicating whether this status represents a completed task. + public bool IsCompleted => Equals(Success) || Equals(Failure); +} diff --git a/source/VMelnalksnis.PaperlessDotNet/Tasks/TaskClient.cs b/source/VMelnalksnis.PaperlessDotNet/Tasks/TaskClient.cs new file mode 100644 index 0000000..3114bc6 --- /dev/null +++ b/source/VMelnalksnis.PaperlessDotNet/Tasks/TaskClient.cs @@ -0,0 +1,48 @@ +// Copyright 2022 Valters Melnalksnis +// Licensed under the Apache License 2.0. +// See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +using VMelnalksnis.PaperlessDotNet.Serialization; + +namespace VMelnalksnis.PaperlessDotNet.Tasks; + +/// +public sealed class TaskClient : ITaskClient +{ + private readonly HttpClient _httpClient; + private readonly PaperlessJsonSerializerContext _context; + + /// Initializes a new instance of the class. + /// Http client configured for making requests to the Paperless API. + /// Paperless specific instance of . + public TaskClient(HttpClient httpClient, PaperlessJsonSerializerOptions serializerOptions) + { + _httpClient = httpClient; + _context = serializerOptions.Context; + } + + /// + public Task> GetAll(CancellationToken cancellationToken = default) + { + return _httpClient.GetFromJsonAsync("/api/tasks/", _context.ListPaperlessTask, cancellationToken)!; + } + + /// + public async Task Get(Guid taskId, CancellationToken cancellationToken = default) + { + var tasks = await _httpClient + .GetFromJsonAsync($"/api/tasks/?task_id={taskId}", _context.ListPaperlessTask, cancellationToken) + .ConfigureAwait(false); + + return tasks?.SingleOrDefault(); + } +} diff --git a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/Documents/DocumentClientTests.cs b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/Documents/DocumentClientTests.cs index 8064389..433e66a 100644 --- a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/Documents/DocumentClientTests.cs +++ b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/Documents/DocumentClientTests.cs @@ -2,9 +2,16 @@ // Licensed under the Apache License 2.0. // See LICENSE file in the project root for full license information. +#if !NET6_0_OR_GREATER +using System; +#endif using System.Linq; using System.Threading.Tasks; +using NodaTime; + +using VMelnalksnis.PaperlessDotNet.Documents; + using Xunit.Abstractions; namespace VMelnalksnis.PaperlessDotNet.Tests.Integration.Documents; @@ -13,10 +20,12 @@ namespace VMelnalksnis.PaperlessDotNet.Tests.Integration.Documents; public sealed class DocumentClientTests { private readonly IPaperlessClient _paperlessClient; + private readonly IClock _clock; public DocumentClientTests(ITestOutputHelper testOutputHelper, PaperlessFixture paperlessFixture) { _paperlessClient = paperlessFixture.GetPaperlessClient(testOutputHelper); + _clock = paperlessFixture.Clock; } [Fact] @@ -35,4 +44,46 @@ public async Task GetAll_PageSizeShouldNotChangeResult() documents.Should().BeEquivalentTo(pageSizeDocuments); } + + [Fact] + public async Task Create() + { + const string documentName = "Lorem Ipsum.txt"; + + var correspondent = await _paperlessClient.Correspondents.Create(new("Foo")); + await using var documentStream = typeof(DocumentClientTests).GetResource(documentName); + var documentCreation = new DocumentCreation(documentStream, documentName) + { + Created = _clock.GetCurrentInstant(), + Title = "Lorem Ipsum", + CorrespondentId = correspondent.Id, + ArchiveSerialNumber = 1, + }; + + var result = await _paperlessClient.Documents.Create(documentCreation); + + var id = result.Should().BeOfType().Subject.Id; + var document = (await _paperlessClient.Documents.Get(id))!; + + using var scope = new AssertionScope(); + + var currentTime = SystemClock.Instance.GetCurrentInstant(); + var content = await typeof(DocumentClientTests).ReadResource(documentName); + + document.Should().NotBeNull(); + document.OriginalFileName.Should().Be(documentName); + document.Created.ToInstant().Should().Be(documentCreation.Created.Value); + document.Added.ToInstant().Should().BeInRange(currentTime - Duration.FromSeconds(10), currentTime); + document.Modified.ToInstant().Should().BeInRange(currentTime - Duration.FromSeconds(10), currentTime); + document.Title.Should().Be(documentCreation.Title); + document.ArchiveSerialNumber.Should().Be(documentCreation.ArchiveSerialNumber); + document.CorrespondentId.Should().Be(documentCreation.CorrespondentId); +#if NET6_0_OR_GREATER + document.Content.ReplaceLineEndings().Should().BeEquivalentTo(content); +#else + document.Content.Replace("\n", Environment.NewLine).Replace("\r\n", Environment.NewLine).Should().Be(content); +#endif + + await _paperlessClient.Correspondents.Delete(correspondent.Id); + } } diff --git a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/Documents/Lorem Ipsum.txt b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/Documents/Lorem Ipsum.txt new file mode 100644 index 0000000..0b4e621 --- /dev/null +++ b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/Documents/Lorem Ipsum.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi interdum libero at augue sagittis, ut aliquam arcu fringilla. Aenean elit nibh, blandit eu lacinia id, fermentum vitae sapien. Nunc efficitur cursus tempus. Phasellus consequat diam non ante pulvinar dapibus. Pellentesque ullamcorper nulla quis purus laoreet fringilla. Pellentesque vitae elit dolor. Nam elementum a leo non elementum. Cras non ante efficitur, molestie neque a, ultrices leo. Duis mollis ex fringilla elit lacinia semper. Nulla orci tellus, tristique sed iaculis non, sagittis a sem. Aenean scelerisque laoreet erat, a congue urna faucibus mollis. + +Vivamus tempus mattis tristique. Cras volutpat odio ac ligula gravida porta. Sed lobortis purus et eros imperdiet luctus. Aenean mi nulla, sodales at tincidunt sed, sagittis lobortis lorem. Aliquam at tincidunt metus. Nam viverra suscipit ultrices. Etiam vel bibendum massa, dictum tincidunt magna. Aliquam in auctor metus, id semper lorem. + +Ut ultrices volutpat bibendum. Sed lobortis nunc quis facilisis tempor. Vestibulum massa sem, lacinia et eros sed, pellentesque aliquam libero. Maecenas mattis magna et augue egestas, et sodales nibh consequat. Aliquam ultricies leo sit amet urna scelerisque feugiat. Nulla malesuada fringilla lorem eu pulvinar. Ut eu sagittis turpis, eget tincidunt magna. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aliquam suscipit est non urna ullamcorper rhoncus. Curabitur massa tellus, lacinia cursus nibh at, aliquam iaculis neque. + +Sed vulputate dolor ac diam scelerisque sagittis. Duis at gravida dolor, non tempus turpis. Sed convallis dignissim orci at aliquam. Integer euismod nisl a mi interdum porttitor. Donec rhoncus condimentum mi. Nulla sagittis rhoncus ultricies. In non risus vitae neque rhoncus elementum non vel justo. Nulla et congue mi, vitae viverra ex. Aenean nec nisi nisi. Sed ornare vehicula lorem eget convallis. Sed suscipit leo ligula. Nunc sollicitudin turpis eu turpis pulvinar ultricies. Curabitur in euismod tortor. Morbi orci purus, convallis non pellentesque ac, efficitur ac leo. Nullam sollicitudin justo sit amet tristique dapibus. + +Sed eu vulputate dolor, id fermentum ante. Nulla blandit lobortis porttitor. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Curabitur semper finibus elit eu porttitor. Fusce luctus elementum elit, quis sodales libero faucibus sed. Proin sit amet elementum odio. Donec in vehicula ligula. Duis facilisis urna mi, id mollis odio lacinia eleifend. Sed sagittis tellus non lobortis tristique. Sed gravida aliquet arcu, eget venenatis nunc vehicula eget. Aenean sed fermentum tellus. Donec commodo vitae orci eget consequat. Integer tempor diam non odio porta, id porta elit commodo. Nunc fringilla commodo orci, nec consequat velit posuere vel. diff --git a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/MinimalExampleTests.cs b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/MinimalExampleTests.cs index fc9726b..fc4e6ef 100644 --- a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/MinimalExampleTests.cs +++ b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/MinimalExampleTests.cs @@ -12,6 +12,7 @@ using VMelnalksnis.PaperlessDotNet.Correspondents; using VMelnalksnis.PaperlessDotNet.Documents; using VMelnalksnis.PaperlessDotNet.Serialization; +using VMelnalksnis.PaperlessDotNet.Tasks; namespace VMelnalksnis.PaperlessDotNet.Tests.Integration; @@ -29,8 +30,9 @@ public MinimalExampleTests(PaperlessFixture paperlessFixture) httpClient.DefaultRequestHeaders.Authorization = new("Token", options.Token); var serializerOptions = new PaperlessJsonSerializerOptions(DateTimeZoneProviders.Tzdb); + var taskClient = new TaskClient(httpClient, serializerOptions); var correspondentClient = new CorrespondentClient(httpClient, serializerOptions); - var documentClient = new DocumentClient(httpClient, serializerOptions); + var documentClient = new DocumentClient(httpClient, serializerOptions, taskClient); _paperlessClient = new PaperlessClient(correspondentClient, documentClient); } diff --git a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/PaperlessFixture.cs b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/PaperlessFixture.cs index 80cd72b..1d702cb 100644 --- a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/PaperlessFixture.cs +++ b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/PaperlessFixture.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.DependencyInjection; using NodaTime; +using NodaTime.Testing; using Serilog; @@ -48,6 +49,7 @@ public PaperlessFixture() .Build(); _paperless = new PaperlessBuilder() + .WithImage($"{PaperlessBuilder.PaperlessImage}:2.3.3") .WithNetwork(_network) .DependsOn(_redis) .WithRedis($"redis://{redis}:{RedisBuilder.RedisPort}") @@ -56,6 +58,8 @@ public PaperlessFixture() internal PaperlessOptions Options { get; private set; } = null!; + internal IClock Clock { get; } = new FakeClock(Instant.FromUtc(2024, 01, 17, 18, 8, 23), Duration.Zero); + /// public async Task InitializeAsync() { diff --git a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/TypeExtensions.cs b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/TypeExtensions.cs new file mode 100644 index 0000000..1fefaf8 --- /dev/null +++ b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/TypeExtensions.cs @@ -0,0 +1,31 @@ +// Copyright 2022 Valters Melnalksnis +// Licensed under the Apache License 2.0. +// See LICENSE file in the project root for full license information. + +using System; +using System.IO; +using System.Resources; +using System.Threading.Tasks; + +namespace VMelnalksnis.PaperlessDotNet.Tests.Integration; + +internal static class TypeExtensions +{ + internal static Stream GetResource(this Type type, string resourceName) + { + var stream = type.Assembly.GetManifestResourceStream(type, resourceName); + if (stream is not null) + { + return stream; + } + + throw new MissingManifestResourceException($"Could not find {resourceName} is namespace {type.Namespace}"); + } + + internal static async Task ReadResource(this Type type, string resourceName) + { + await using var stream = type.GetResource(resourceName); + using var streamReader = new StreamReader(stream); + return await streamReader.ReadToEndAsync(); + } +} diff --git a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/VMelnalksnis.PaperlessDotNet.Tests.Integration.csproj b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/VMelnalksnis.PaperlessDotNet.Tests.Integration.csproj index efa2e1d..a4f9f7f 100644 --- a/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/VMelnalksnis.PaperlessDotNet.Tests.Integration.csproj +++ b/tests/VMelnalksnis.PaperlessDotNet.Tests.Integration/VMelnalksnis.PaperlessDotNet.Tests.Integration.csproj @@ -19,4 +19,8 @@ + + + +