Files
PaperlessMCP/PaperlessMCP.Tests/Client/PaperlessClientTests.cs
T
Barry Walker c67781bac5 feat: add proper error handling with full API error details
- Add ApiResult<T> type to carry success/failure with error details
- Add UpdateDocumentWithResultAsync and CreateTagWithResultAsync methods
- Update DocumentTools.Update and TagTools.Create to return actual
  HTTP status codes and response bodies in error responses
- Add comprehensive tests for error handling (18 new tests)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 18:59:16 -05:00

512 lines
15 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
}