b0ab0dd5dd
Create/update operations were sending empty request bodies to Paperless,
producing two visible failure modes:
1. POST creates (correspondents, tags, document types, storage paths,
custom fields) returned `400 {"name":["This field is required."]}`
even when the caller passed a valid name.
2. PATCH updates (documents, correspondents, tags, etc.) returned 200
and bumped the row's `modified` timestamp, but no fields actually
changed — server-side this looked like a successful no-op PATCH.
3. Bulk edit operations (`api/documents/bulk_edit/`,
`api/bulk_edit_objects/`) failed because the inner `parameters` field
went out as `{}`, so add_tag/remove_tag/etc. arrived without a tag id.
Reproducer: `correspondents.create(name="ACME Corp")` against a real
Paperless instance returns 400 with the body above.
Root causes are two related serialization patterns where the
compile-time type of the value to serialize is `object`:
- `PostWithResultAsync` / `PatchWithResultAsync` accept the request as
`object` and pass it to `PostAsJsonAsync<TValue>` /
`JsonContent.Create<T>(value, ...)`. The generic `T` is inferred as
`object`, and through the configured DI HttpClient pipeline
(DelegatingHandler + Polly retry policy) the body that reaches the
wire is empty, even though the same JsonContent's
`ReadAsStringAsync()` returns the expected JSON.
- `BulkEditDocumentsAsync` / `BulkEditObjectsAsync` wrap the call args
in an anonymous type whose `parameters` field has compile-time type
`object?`. System.Text.Json serializes that property against
`object`, producing `"parameters":{}` regardless of the runtime value.
Fix: serialize against the runtime type explicitly and materialize the
JSON into a `StringContent` before the request leaves this client.
For bulk edits, build the body via `JsonObject` and serialize the
`parameters` payload against its runtime type.
Adds wire-format pinning tests for create / update / bulk_edit /
bulk_edit_objects that capture the outbound request body and assert the
expected payload shape. These do not reproduce the original bug under
MockHttp (the test helper bypasses the DI handler chain), but they pin
the new serialization for future refactors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
626 lines
20 KiB
C#
626 lines
20 KiB
C#
using System.Net;
|
|
using FluentAssertions;
|
|
using PaperlessMCP.Models.Correspondents;
|
|
using RichardSzalay.MockHttp;
|
|
using Xunit;
|
|
using PaperlessMCP.Models.CustomFields;
|
|
using PaperlessMCP.Models.DocumentTypes;
|
|
using PaperlessMCP.Models.StoragePaths;
|
|
using PaperlessMCP.Models.Documents;
|
|
using PaperlessMCP.Models.Tags;
|
|
using PaperlessMCP.Tests.Fixtures;
|
|
|
|
namespace PaperlessMCP.Tests.Client;
|
|
|
|
public class PaperlessClientTests : IDisposable
|
|
{
|
|
private readonly MockHttpClientFactory _factory;
|
|
|
|
public PaperlessClientTests()
|
|
{
|
|
_factory = new MockHttpClientFactory();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_factory.Dispose();
|
|
}
|
|
|
|
#region Ping Tests
|
|
|
|
[Fact]
|
|
public async Task PingAsync_WhenSuccessful_ReturnsSuccess()
|
|
{
|
|
// Arrange
|
|
_factory.SetupGet("api/status/", """{"pngx_version": "2.0.0", "server_os": "Linux"}""");
|
|
|
|
// Act
|
|
var (success, version, error) = await _factory.Client.PingAsync();
|
|
|
|
// Assert
|
|
success.Should().BeTrue();
|
|
version.Should().Be("2.0.0");
|
|
error.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task PingAsync_WhenUnauthorized_ReturnsFailure()
|
|
{
|
|
// Arrange
|
|
_factory.SetupGetWithStatus("api/status/", HttpStatusCode.Unauthorized);
|
|
|
|
// Act
|
|
var (success, version, error) = await _factory.Client.PingAsync();
|
|
|
|
// Assert
|
|
success.Should().BeFalse();
|
|
version.Should().BeNull();
|
|
error.Should().Contain("401");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Document Tests
|
|
|
|
[Fact]
|
|
public async Task SearchDocumentsAsync_WithQuery_ReturnsResults()
|
|
{
|
|
// Arrange
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Get, "https://paperless.example.com/api/documents/*")
|
|
.Respond("application/json", TestFixtures.Documents.CreateSearchResultsJson(5));
|
|
|
|
// Act
|
|
var result = await _factory.Client.SearchDocumentsAsync(query: "test");
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Count.Should().Be(5);
|
|
result.Results.Should().HaveCount(5);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SearchDocumentsAsync_WithFilters_ReturnsFilteredResults()
|
|
{
|
|
// Arrange
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Get, "https://paperless.example.com/api/documents/*")
|
|
.Respond("application/json", TestFixtures.Documents.CreateSearchResultsJson(2));
|
|
|
|
// Act
|
|
var result = await _factory.Client.SearchDocumentsAsync(
|
|
query: "invoice",
|
|
tags: [1, 2],
|
|
correspondent: 3,
|
|
documentType: 4);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Count.Should().Be(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDocumentAsync_WhenExists_ReturnsDocument()
|
|
{
|
|
// Arrange
|
|
_factory.SetupGet("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "My Document"));
|
|
|
|
// Act
|
|
var result = await _factory.Client.GetDocumentAsync(1);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result!.Id.Should().Be(1);
|
|
result.Title.Should().Be("My Document");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDocumentAsync_WhenNotFound_ReturnsNull()
|
|
{
|
|
// Arrange
|
|
_factory.SetupGetWithStatus("api/documents/999/", HttpStatusCode.NotFound);
|
|
|
|
// Act
|
|
var result = await _factory.Client.GetDocumentAsync(999);
|
|
|
|
// Assert
|
|
result.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteDocumentAsync_WhenSuccessful_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
_factory.SetupDelete("api/documents/1/", HttpStatusCode.NoContent);
|
|
|
|
// Act
|
|
var result = await _factory.Client.DeleteDocumentAsync(1);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteDocumentAsync_WhenNotFound_ReturnsFalse()
|
|
{
|
|
// Arrange
|
|
_factory.SetupDelete("api/documents/999/", HttpStatusCode.NotFound);
|
|
|
|
// Act
|
|
var result = await _factory.Client.DeleteDocumentAsync(999);
|
|
|
|
// Assert
|
|
result.Should().BeFalse();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetDocumentDownloadInfo_ReturnsCorrectUrls()
|
|
{
|
|
// Act
|
|
var result = _factory.Client.GetDocumentDownloadInfo(1, "Test Doc", "test.pdf");
|
|
|
|
// Assert
|
|
result.Id.Should().Be(1);
|
|
result.Title.Should().Be("Test Doc");
|
|
result.OriginalFileName.Should().Be("test.pdf");
|
|
result.DownloadUrl.Should().Be("https://paperless.example.com/api/documents/1/download/");
|
|
result.PreviewUrl.Should().Be("https://paperless.example.com/api/documents/1/preview/");
|
|
result.ThumbnailUrl.Should().Be("https://paperless.example.com/api/documents/1/thumb/");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateDocumentWithResultAsync_WhenSuccessful_ReturnsSuccess()
|
|
{
|
|
// Arrange
|
|
_factory.SetupPatch("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Updated Title"));
|
|
|
|
// Act
|
|
var result = await _factory.Client.UpdateDocumentWithResultAsync(1, new DocumentUpdateRequest { Title = "Updated Title" });
|
|
|
|
// Assert
|
|
result.IsSuccess.Should().BeTrue();
|
|
result.Value.Should().NotBeNull();
|
|
result.Value!.Title.Should().Be("Updated Title");
|
|
result.Error.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateDocumentWithResultAsync_WhenNotFound_ReturnsFailureWithDetails()
|
|
{
|
|
// Arrange
|
|
var errorBody = """{"detail": "Not found."}""";
|
|
_factory.SetupPatchWithError("api/documents/999/", HttpStatusCode.NotFound, errorBody);
|
|
|
|
// Act
|
|
var result = await _factory.Client.UpdateDocumentWithResultAsync(999, new DocumentUpdateRequest { Title = "New Title" });
|
|
|
|
// Assert
|
|
result.IsSuccess.Should().BeFalse();
|
|
result.Value.Should().BeNull();
|
|
result.Error.Should().NotBeNull();
|
|
result.Error!.StatusCode.Should().Be(HttpStatusCode.NotFound);
|
|
result.Error.ResponseBody.Should().Be(errorBody);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateDocumentWithResultAsync_WhenBadRequest_ReturnsFailureWithDetails()
|
|
{
|
|
// Arrange
|
|
var errorBody = """{"title": ["This field may not be blank."]}""";
|
|
_factory.SetupPatchWithError("api/documents/1/", HttpStatusCode.BadRequest, errorBody);
|
|
|
|
// Act
|
|
var result = await _factory.Client.UpdateDocumentWithResultAsync(1, new DocumentUpdateRequest { Title = "" });
|
|
|
|
// Assert
|
|
result.IsSuccess.Should().BeFalse();
|
|
result.Error.Should().NotBeNull();
|
|
result.Error!.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
result.Error.ResponseBody.Should().Contain("This field may not be blank");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Tag Tests
|
|
|
|
[Fact]
|
|
public async Task GetTagsAsync_ReturnsTagList()
|
|
{
|
|
// Arrange
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Get, "https://paperless.example.com/api/tags/*")
|
|
.Respond("application/json", TestFixtures.Tags.CreateTagListJson(5));
|
|
|
|
// Act
|
|
var result = await _factory.Client.GetTagsAsync();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Count.Should().Be(5);
|
|
result.Results.Should().HaveCount(5);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetTagAsync_WhenExists_ReturnsTag()
|
|
{
|
|
// Arrange
|
|
_factory.SetupGet("api/tags/1/", TestFixtures.Tags.CreateTagJson(1, "Important"));
|
|
|
|
// Act
|
|
var result = await _factory.Client.GetTagAsync(1);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result!.Id.Should().Be(1);
|
|
result.Name.Should().Be("Important");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateTagAsync_WhenSuccessful_ReturnsTag()
|
|
{
|
|
// Arrange
|
|
_factory.SetupPost("api/tags/", TestFixtures.Tags.CreateTagJson(5, "New Tag"));
|
|
|
|
// Act
|
|
var result = await _factory.Client.CreateTagAsync(new TagCreateRequest { Name = "New Tag" });
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result!.Id.Should().Be(5);
|
|
result.Name.Should().Be("New Tag");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task DeleteTagAsync_WhenSuccessful_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
_factory.SetupDelete("api/tags/1/", HttpStatusCode.NoContent);
|
|
|
|
// Act
|
|
var result = await _factory.Client.DeleteTagAsync(1);
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateTagWithResultAsync_WhenSuccessful_ReturnsSuccess()
|
|
{
|
|
// Arrange
|
|
_factory.SetupPost("api/tags/", TestFixtures.Tags.CreateTagJson(5, "New Tag"));
|
|
|
|
// Act
|
|
var result = await _factory.Client.CreateTagWithResultAsync(new TagCreateRequest { Name = "New Tag" });
|
|
|
|
// Assert
|
|
result.IsSuccess.Should().BeTrue();
|
|
result.Value.Should().NotBeNull();
|
|
result.Value!.Name.Should().Be("New Tag");
|
|
result.Error.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateTagWithResultAsync_WhenDuplicate_ReturnsFailureWithDetails()
|
|
{
|
|
// Arrange
|
|
var errorBody = """{"name": ["tag with this name already exists."]}""";
|
|
_factory.SetupPostWithError("api/tags/", HttpStatusCode.BadRequest, errorBody);
|
|
|
|
// Act
|
|
var result = await _factory.Client.CreateTagWithResultAsync(new TagCreateRequest { Name = "Existing Tag" });
|
|
|
|
// Assert
|
|
result.IsSuccess.Should().BeFalse();
|
|
result.Error.Should().NotBeNull();
|
|
result.Error!.StatusCode.Should().Be(HttpStatusCode.BadRequest);
|
|
result.Error.ResponseBody.Should().Contain("already exists");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateTagWithResultAsync_WhenUnauthorized_ReturnsFailureWithDetails()
|
|
{
|
|
// Arrange
|
|
var errorBody = """{"detail": "Authentication credentials were not provided."}""";
|
|
_factory.SetupPostWithError("api/tags/", HttpStatusCode.Unauthorized, errorBody);
|
|
|
|
// Act
|
|
var result = await _factory.Client.CreateTagWithResultAsync(new TagCreateRequest { Name = "Test" });
|
|
|
|
// Assert
|
|
result.IsSuccess.Should().BeFalse();
|
|
result.Error.Should().NotBeNull();
|
|
result.Error!.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
|
|
result.Error.ResponseBody.Should().Contain("Authentication credentials");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Correspondent Tests
|
|
|
|
[Fact]
|
|
public async Task GetCorrespondentsAsync_ReturnsCorrespondentList()
|
|
{
|
|
// Arrange
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Get, "https://paperless.example.com/api/correspondents/*")
|
|
.Respond("application/json", TestFixtures.Correspondents.CreateCorrespondentListJson(3));
|
|
|
|
// Act
|
|
var result = await _factory.Client.GetCorrespondentsAsync();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Count.Should().Be(3);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateCorrespondentAsync_WhenSuccessful_ReturnsCorrespondent()
|
|
{
|
|
// Arrange
|
|
_factory.SetupPost("api/correspondents/", TestFixtures.Correspondents.CreateCorrespondentJson(1, "ACME Corp"));
|
|
|
|
// Act
|
|
var result = await _factory.Client.CreateCorrespondentAsync(new CorrespondentCreateRequest { Name = "ACME Corp" });
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result!.Name.Should().Be("ACME Corp");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Document Type Tests
|
|
|
|
[Fact]
|
|
public async Task GetDocumentTypesAsync_ReturnsDocumentTypeList()
|
|
{
|
|
// Arrange
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Get, "https://paperless.example.com/api/document_types/*")
|
|
.Respond("application/json", TestFixtures.DocumentTypes.CreateDocumentTypeListJson(4));
|
|
|
|
// Act
|
|
var result = await _factory.Client.GetDocumentTypesAsync();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Count.Should().Be(4);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateDocumentTypeAsync_WhenSuccessful_ReturnsDocumentType()
|
|
{
|
|
// Arrange
|
|
_factory.SetupPost("api/document_types/", TestFixtures.DocumentTypes.CreateDocumentTypeJson(1, "Invoice"));
|
|
|
|
// Act
|
|
var result = await _factory.Client.CreateDocumentTypeAsync(new DocumentTypeCreateRequest { Name = "Invoice" });
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result!.Name.Should().Be("Invoice");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Storage Path Tests
|
|
|
|
[Fact]
|
|
public async Task GetStoragePathsAsync_ReturnsStoragePathList()
|
|
{
|
|
// Arrange
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Get, "https://paperless.example.com/api/storage_paths/*")
|
|
.Respond("application/json", TestFixtures.StoragePaths.CreateStoragePathListJson(2));
|
|
|
|
// Act
|
|
var result = await _factory.Client.GetStoragePathsAsync();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Count.Should().Be(2);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateStoragePathAsync_WhenSuccessful_ReturnsStoragePath()
|
|
{
|
|
// Arrange
|
|
_factory.SetupPost("api/storage_paths/", TestFixtures.StoragePaths.CreateStoragePathJson(1, "Archive"));
|
|
|
|
// Act
|
|
var result = await _factory.Client.CreateStoragePathAsync(new StoragePathCreateRequest
|
|
{
|
|
Name = "Archive",
|
|
Path = "{correspondent}/{year}"
|
|
});
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result!.Name.Should().Be("Archive");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Custom Field Tests
|
|
|
|
[Fact]
|
|
public async Task GetCustomFieldsAsync_ReturnsCustomFieldList()
|
|
{
|
|
// Arrange
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Get, "https://paperless.example.com/api/custom_fields/*")
|
|
.Respond("application/json", TestFixtures.CustomFields.CreateCustomFieldListJson(3));
|
|
|
|
// Act
|
|
var result = await _factory.Client.GetCustomFieldsAsync();
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Count.Should().Be(3);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task CreateCustomFieldAsync_WhenSuccessful_ReturnsCustomField()
|
|
{
|
|
// Arrange
|
|
_factory.SetupPost("api/custom_fields/", TestFixtures.CustomFields.CreateCustomFieldJson(1, "Invoice Number"));
|
|
|
|
// Act
|
|
var result = await _factory.Client.CreateCustomFieldAsync(new CustomFieldCreateRequest
|
|
{
|
|
Name = "Invoice Number",
|
|
DataType = "string"
|
|
});
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result!.Name.Should().Be("Invoice Number");
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Bulk Operations Tests
|
|
|
|
[Fact]
|
|
public async Task BulkEditDocumentsAsync_WhenSuccessful_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
_factory.SetupPost("api/documents/bulk_edit/", "{}");
|
|
|
|
// Act
|
|
var result = await _factory.Client.BulkEditDocumentsAsync([1, 2, 3], "add_tag", new { tag = 5 });
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BulkEditObjectsAsync_WhenSuccessful_ReturnsTrue()
|
|
{
|
|
// Arrange
|
|
_factory.SetupPost("api/bulk_edit_objects/", "{}");
|
|
|
|
// Act
|
|
var result = await _factory.Client.BulkEditObjectsAsync([1, 2], "tags", "delete");
|
|
|
|
// Assert
|
|
result.Should().BeTrue();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Request Body Serialization Tests
|
|
|
|
// Wire-format documentation tests. These capture the outbound request body and
|
|
// assert the expected payload reaches the HTTP layer. They DO NOT reproduce the
|
|
// production bug that motivated the fix — that bug only manifested through the DI
|
|
// HttpClient pipeline (DelegatingHandler + Polly retry policy), and these tests
|
|
// wire MockHttp directly into the HttpClient, bypassing that chain. Treat these as
|
|
// pinning tests for the new code path. A higher-fidelity regression test would
|
|
// need to register PaperlessClient through ServiceCollection with the same handler
|
|
// pipeline used in Program.cs.
|
|
|
|
[Fact]
|
|
public async Task CreateCorrespondentAsync_SendsNameInRequestBody()
|
|
{
|
|
// Arrange — capture the actual outbound body
|
|
string? capturedBody = null;
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Post, "https://paperless.example.com/api/correspondents/")
|
|
.With(req =>
|
|
{
|
|
capturedBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult();
|
|
return true;
|
|
})
|
|
.Respond("application/json", TestFixtures.Correspondents.CreateCorrespondentJson(1, "ACME Corp"));
|
|
|
|
// Act
|
|
await _factory.Client.CreateCorrespondentAsync(new CorrespondentCreateRequest { Name = "ACME Corp" });
|
|
|
|
// Assert — the body must include the name on the wire.
|
|
capturedBody.Should().NotBeNullOrEmpty();
|
|
capturedBody.Should().Contain("\"name\":\"ACME Corp\"");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task UpdateDocumentAsync_SendsUpdatedFieldsInRequestBody()
|
|
{
|
|
// Arrange
|
|
string? capturedBody = null;
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Patch, "https://paperless.example.com/api/documents/42/")
|
|
.With(req =>
|
|
{
|
|
capturedBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult();
|
|
return true;
|
|
})
|
|
.Respond("application/json", "{\"id\":42}");
|
|
|
|
// Act
|
|
await _factory.Client.UpdateDocumentAsync(42, new DocumentUpdateRequest
|
|
{
|
|
Title = "New Title",
|
|
Correspondent = 7,
|
|
Tags = new List<int> { 1, 2 }
|
|
});
|
|
|
|
// Assert — the PATCH body must contain the updated fields on the wire.
|
|
capturedBody.Should().NotBeNullOrEmpty();
|
|
capturedBody.Should().Contain("\"title\":\"New Title\"");
|
|
capturedBody.Should().Contain("\"correspondent\":7");
|
|
capturedBody.Should().Contain("\"tags\":[1,2]");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BulkEditDocumentsAsync_SerializesInnerParametersWithRuntimeType()
|
|
{
|
|
// Arrange
|
|
string? capturedBody = null;
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Post, "https://paperless.example.com/api/documents/bulk_edit/")
|
|
.With(req =>
|
|
{
|
|
capturedBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult();
|
|
return true;
|
|
})
|
|
.Respond("application/json", "{}");
|
|
|
|
// Act
|
|
await _factory.Client.BulkEditDocumentsAsync([1, 2, 3], "add_tag", new { tag = 5 });
|
|
|
|
// Assert — the inner `parameters` field must contain the actual tag, not `{}`.
|
|
capturedBody.Should().NotBeNullOrEmpty();
|
|
capturedBody.Should().Contain("\"documents\":[1,2,3]");
|
|
capturedBody.Should().Contain("\"method\":\"add_tag\"");
|
|
capturedBody.Should().Contain("\"parameters\":{\"tag\":5}");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task BulkEditObjectsAsync_SerializesInnerParametersWithRuntimeType()
|
|
{
|
|
// Arrange
|
|
string? capturedBody = null;
|
|
_factory.MockHandler
|
|
.When(HttpMethod.Post, "https://paperless.example.com/api/bulk_edit_objects/")
|
|
.With(req =>
|
|
{
|
|
capturedBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult();
|
|
return true;
|
|
})
|
|
.Respond("application/json", "{}");
|
|
|
|
// Act
|
|
await _factory.Client.BulkEditObjectsAsync([10, 20], "tags", "set_permissions",
|
|
new { owner = 3, set_permissions = new { view = new { users = new[] { 1 } } } });
|
|
|
|
// Assert
|
|
capturedBody.Should().NotBeNullOrEmpty();
|
|
capturedBody.Should().Contain("\"objects\":[10,20]");
|
|
capturedBody.Should().Contain("\"object_type\":\"tags\"");
|
|
capturedBody.Should().Contain("\"operation\":\"set_permissions\"");
|
|
capturedBody.Should().Contain("\"owner\":3");
|
|
}
|
|
|
|
#endregion
|
|
}
|