Initial commit: Paperless-ngx MCP Server
A Model Context Protocol (MCP) server for Paperless-ngx document management. Features: - Full CRUD operations for documents, tags, correspondents, document types, storage paths, and custom fields - Document upload with retry logic (base64 and file path) - Bulk operations with dry-run support - Search with full-text and metadata filtering - Pagination support across all list operations - Proper error handling with McpResponse wrapper Built with .NET 10 and the official MCP SDK. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using PaperlessMCP.Tests.Fixtures;
|
||||
using RichardSzalay.MockHttp;
|
||||
using PaperlessMCP.Tools;
|
||||
using Xunit;
|
||||
|
||||
namespace PaperlessMCP.Tests.Tools;
|
||||
|
||||
public class CorrespondentToolsTests : IDisposable
|
||||
{
|
||||
private readonly MockHttpClientFactory _factory;
|
||||
|
||||
public CorrespondentToolsTests()
|
||||
{
|
||||
_factory = new MockHttpClientFactory();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_factory.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_ReturnsCorrespondentList()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/correspondents/*")
|
||||
.Respond("application/json", TestFixtures.Correspondents.CreateCorrespondentListJson(5));
|
||||
|
||||
// Act
|
||||
var result = await CorrespondentTools.List(_factory.Client);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetArrayLength().Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenExists_ReturnsCorrespondent()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/correspondents/1/", TestFixtures.Correspondents.CreateCorrespondentJson(1, "ACME Corp"));
|
||||
|
||||
// Act
|
||||
var result = await CorrespondentTools.Get(_factory.Client, 1);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("ACME Corp");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenNotFound_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGetWithStatus("api/correspondents/999/", HttpStatusCode.NotFound);
|
||||
|
||||
// Act
|
||||
var result = await CorrespondentTools.Get(_factory.Client, 999);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WhenSuccessful_ReturnsCreatedCorrespondent()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/correspondents/", TestFixtures.Correspondents.CreateCorrespondentJson(1, "New Company"));
|
||||
|
||||
// Act
|
||||
var result = await CorrespondentTools.Create(_factory.Client, "New Company");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("New Company");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WhenSuccessful_ReturnsUpdatedCorrespondent()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPatch("api/correspondents/1/", TestFixtures.Correspondents.CreateCorrespondentJson(1, "Updated Company"));
|
||||
|
||||
// Act
|
||||
var result = await CorrespondentTools.Update(_factory.Client, 1, name: "Updated Company");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Updated Company");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithoutConfirmation_ReturnsDryRun()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/correspondents/1/", TestFixtures.Correspondents.CreateCorrespondentJson(1, "To Delete"));
|
||||
|
||||
// Act
|
||||
var result = await CorrespondentTools.Delete(_factory.Client, 1, confirm: false);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("CONFIRMATION_REQUIRED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithConfirmation_DeletesCorrespondent()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupDelete("api/correspondents/1/", HttpStatusCode.NoContent);
|
||||
|
||||
// Act
|
||||
var result = await CorrespondentTools.Delete(_factory.Client, 1, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("deleted").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDelete_WithDryRun_ReturnsPreview()
|
||||
{
|
||||
// Act
|
||||
var result = await CorrespondentTools.BulkDelete(_factory.Client, "1,2", dryRun: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("executed").GetBoolean().Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDelete_WithConfirmation_ExecutesDeletion()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/bulk_edit_objects/", "{}");
|
||||
|
||||
// Act
|
||||
var result = await CorrespondentTools.BulkDelete(_factory.Client, "1,2", dryRun: false, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("executed").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using PaperlessMCP.Tests.Fixtures;
|
||||
using RichardSzalay.MockHttp;
|
||||
using PaperlessMCP.Tools;
|
||||
using Xunit;
|
||||
|
||||
namespace PaperlessMCP.Tests.Tools;
|
||||
|
||||
public class CustomFieldToolsTests : IDisposable
|
||||
{
|
||||
private readonly MockHttpClientFactory _factory;
|
||||
|
||||
public CustomFieldToolsTests()
|
||||
{
|
||||
_factory = new MockHttpClientFactory();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_factory.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_ReturnsCustomFieldList()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/custom_fields/*")
|
||||
.Respond("application/json", TestFixtures.CustomFields.CreateCustomFieldListJson(4));
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.List(_factory.Client);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetArrayLength().Should().Be(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenExists_ReturnsCustomField()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/custom_fields/1/", TestFixtures.CustomFields.CreateCustomFieldJson(1, "Invoice Number"));
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.Get(_factory.Client, 1);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Invoice Number");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenNotFound_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGetWithStatus("api/custom_fields/999/", HttpStatusCode.NotFound);
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.Get(_factory.Client, 999);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_StringField_ReturnsCreatedField()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/custom_fields/", TestFixtures.CustomFields.CreateCustomFieldJson(1, "Reference Number"));
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.Create(_factory.Client, "Reference Number", "string");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Reference Number");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_SelectField_IncludesOptions()
|
||||
{
|
||||
// Arrange
|
||||
var selectField = TestFixtures.CustomFields.CreateCustomField(1, "Status", "select");
|
||||
_factory.SetupPost("api/custom_fields/", JsonSerializer.Serialize(selectField));
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.Create(
|
||||
_factory.Client,
|
||||
"Status",
|
||||
"select",
|
||||
selectOptions: "Pending,Approved,Rejected");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WhenSuccessful_ReturnsUpdatedField()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPatch("api/custom_fields/1/", TestFixtures.CustomFields.CreateCustomFieldJson(1, "Updated Field"));
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.Update(_factory.Client, 1, name: "Updated Field");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Updated Field");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithoutConfirmation_ReturnsDryRun()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/custom_fields/1/", TestFixtures.CustomFields.CreateCustomFieldJson(1, "To Delete"));
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.Delete(_factory.Client, 1, confirm: false);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("CONFIRMATION_REQUIRED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithConfirmation_DeletesField()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupDelete("api/custom_fields/1/", HttpStatusCode.NoContent);
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.Delete(_factory.Client, 1, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("deleted").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Assign_WhenDocumentExists_AssignsFieldValue()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Test Doc"));
|
||||
_factory.SetupGet("api/custom_fields/1/", TestFixtures.CustomFields.CreateCustomFieldJson(1, "Invoice Number"));
|
||||
_factory.SetupPatch("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Test Doc"));
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.Assign(_factory.Client, documentId: 1, fieldId: 1, value: "INV-001");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("document_id").GetInt32().Should().Be(1);
|
||||
json.RootElement.GetProperty("result").GetProperty("field_id").GetInt32().Should().Be(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Assign_WhenDocumentNotFound_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGetWithStatus("api/documents/999/", HttpStatusCode.NotFound);
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.Assign(_factory.Client, documentId: 999, fieldId: 1, value: "test");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Assign_WhenFieldNotFound_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Test Doc"));
|
||||
_factory.SetupGetWithStatus("api/custom_fields/999/", HttpStatusCode.NotFound);
|
||||
|
||||
// Act
|
||||
var result = await CustomFieldTools.Assign(_factory.Client, documentId: 1, fieldId: 999, value: "test");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using PaperlessMCP.Tests.Fixtures;
|
||||
using RichardSzalay.MockHttp;
|
||||
using PaperlessMCP.Tools;
|
||||
using Xunit;
|
||||
|
||||
namespace PaperlessMCP.Tests.Tools;
|
||||
|
||||
public class DocumentToolsTests : IDisposable
|
||||
{
|
||||
private readonly MockHttpClientFactory _factory;
|
||||
|
||||
public DocumentToolsTests()
|
||||
{
|
||||
_factory = new MockHttpClientFactory();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_factory.Dispose();
|
||||
}
|
||||
|
||||
#region Search Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Search_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 DocumentTools.Search(_factory.Client, query: "invoice");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetArrayLength().Should().Be(5);
|
||||
json.RootElement.GetProperty("meta").GetProperty("total").GetInt32().Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_WithPagination_IncludesMetadata()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/documents/*")
|
||||
.Respond("application/json", TestFixtures.Documents.CreateSearchResultsJson(50));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Search(_factory.Client, page: 2, pageSize: 10);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
var meta = json.RootElement.GetProperty("meta");
|
||||
meta.GetProperty("page").GetInt32().Should().Be(2);
|
||||
meta.GetProperty("page_size").GetInt32().Should().Be(10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_WithFilters_PassesFiltersCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/documents/*")
|
||||
.Respond("application/json", TestFixtures.Documents.CreateSearchResultsJson(2));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Search(
|
||||
_factory.Client,
|
||||
query: "test",
|
||||
tags: "1,2",
|
||||
correspondent: 3,
|
||||
documentType: 4,
|
||||
createdAfter: "2024-01-01",
|
||||
createdBefore: "2024-12-31"
|
||||
);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_ByDefault_ExcludesContent()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/documents/*")
|
||||
.Respond("application/json", TestFixtures.Documents.CreateSearchResultsJson(2));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Search(_factory.Client, query: "test");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
|
||||
var results = json.RootElement.GetProperty("result");
|
||||
results.GetArrayLength().Should().Be(2);
|
||||
|
||||
// Content should be null when includeContent is false (default)
|
||||
foreach (var doc in results.EnumerateArray())
|
||||
{
|
||||
doc.GetProperty("content").ValueKind.Should().Be(JsonValueKind.Null);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_WithIncludeContent_ReturnsContent()
|
||||
{
|
||||
// Arrange
|
||||
var longContent = TestFixtures.Documents.CreateLongContent(1000);
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/documents/*")
|
||||
.Respond("application/json", TestFixtures.Documents.CreateSearchResultsJson(2, longContent));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Search(
|
||||
_factory.Client,
|
||||
query: "test",
|
||||
includeContent: true,
|
||||
contentMaxLength: 0); // Unlimited
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
var results = json.RootElement.GetProperty("result");
|
||||
|
||||
foreach (var doc in results.EnumerateArray())
|
||||
{
|
||||
var content = doc.GetProperty("content").GetString();
|
||||
content.Should().NotBeNullOrEmpty();
|
||||
content.Should().Be(longContent);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_WithContentMaxLength_TruncatesContent()
|
||||
{
|
||||
// Arrange
|
||||
var longContent = TestFixtures.Documents.CreateLongContent(1000);
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/documents/*")
|
||||
.Respond("application/json", TestFixtures.Documents.CreateSearchResultsJson(2, longContent));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Search(
|
||||
_factory.Client,
|
||||
query: "test",
|
||||
includeContent: true,
|
||||
contentMaxLength: 100);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
var results = json.RootElement.GetProperty("result");
|
||||
|
||||
foreach (var doc in results.EnumerateArray())
|
||||
{
|
||||
var content = doc.GetProperty("content").GetString();
|
||||
content.Should().NotBeNullOrEmpty();
|
||||
content!.Length.Should().Be(103); // 100 chars + "..."
|
||||
content.Should().EndWith("...");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Search_ReturnsDocumentSummaryFields()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/documents/*")
|
||||
.Respond("application/json", TestFixtures.Documents.CreateSearchResultsJson(1));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Search(_factory.Client, query: "test");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
var doc = json.RootElement.GetProperty("result")[0];
|
||||
|
||||
// DocumentSummary fields should be present
|
||||
doc.GetProperty("id").GetInt32().Should().BeGreaterThan(0);
|
||||
doc.GetProperty("title").GetString().Should().NotBeNullOrEmpty();
|
||||
doc.GetProperty("correspondent").ValueKind.Should().NotBe(JsonValueKind.Undefined);
|
||||
doc.GetProperty("document_type").ValueKind.Should().NotBe(JsonValueKind.Undefined);
|
||||
doc.GetProperty("tags").GetArrayLength().Should().BeGreaterThanOrEqualTo(0);
|
||||
doc.GetProperty("created").ValueKind.Should().NotBe(JsonValueKind.Undefined);
|
||||
|
||||
// SearchHit should be present
|
||||
doc.GetProperty("__search_hit__").GetProperty("score").GetDouble().Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Get Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenDocumentExists_ReturnsDocument()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Test Invoice"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Get(_factory.Client, 1);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("id").GetInt32().Should().Be(1);
|
||||
json.RootElement.GetProperty("result").GetProperty("title").GetString().Should().Be("Test Invoice");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenDocumentNotFound_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGetWithStatus("api/documents/999/", HttpStatusCode.NotFound);
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Get(_factory.Client, 999);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Download Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Download_WhenDocumentExists_ReturnsDownloadUrls()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Test Doc"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Download(_factory.Client, 1);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
|
||||
var downloadResult = json.RootElement.GetProperty("result");
|
||||
downloadResult.GetProperty("download_url").GetString().Should().Contain("/api/documents/1/download/");
|
||||
downloadResult.GetProperty("preview_url").GetString().Should().Contain("/api/documents/1/preview/");
|
||||
downloadResult.GetProperty("thumbnail_url").GetString().Should().Contain("/api/documents/1/thumb/");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Preview_WhenDocumentExists_ReturnsPreviewUrl()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Test Doc"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Preview(_factory.Client, 1);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("preview_url").GetString()
|
||||
.Should().Contain("/api/documents/1/preview/");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Thumbnail_WhenDocumentExists_ReturnsThumbnailUrl()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Test Doc"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Thumbnail(_factory.Client, 1);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("thumbnail_url").GetString()
|
||||
.Should().Contain("/api/documents/1/thumb/");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Upload Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Upload_WithValidBase64_ReturnsTaskId()
|
||||
{
|
||||
// Arrange
|
||||
var fileContent = Convert.ToBase64String("Test file content"u8.ToArray());
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Post, "https://paperless.example.com/api/documents/post_document/")
|
||||
.Respond("application/json", "\"task-uuid-12345\"");
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Upload(
|
||||
_factory.Client,
|
||||
fileContent,
|
||||
"test.pdf",
|
||||
title: "Test Upload");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("task_id").GetString().Should().Be("task-uuid-12345");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Upload_WithInvalidBase64_ReturnsValidationError()
|
||||
{
|
||||
// Act
|
||||
var result = await DocumentTools.Upload(
|
||||
_factory.Client,
|
||||
"not-valid-base64!!!",
|
||||
"test.pdf");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("VALIDATION");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadFromPath_WhenFileNotFound_ReturnsError()
|
||||
{
|
||||
// Act
|
||||
var result = await DocumentTools.UploadFromPath(
|
||||
_factory.Client,
|
||||
"/nonexistent/path/to/file.pdf");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadFromPath_WithRelativePath_ReturnsValidationError()
|
||||
{
|
||||
// Act
|
||||
var result = await DocumentTools.UploadFromPath(
|
||||
_factory.Client,
|
||||
"relative/path/to/file.pdf");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("VALIDATION");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadFromPath_WithValidFile_ReturnsTaskId()
|
||||
{
|
||||
// Arrange
|
||||
var tempFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(tempFile, "Test file content for upload");
|
||||
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Post, "https://paperless.example.com/api/documents/post_document/")
|
||||
.Respond("application/json", "\"task-uuid-from-path-12345\"");
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.UploadFromPath(
|
||||
_factory.Client,
|
||||
tempFile,
|
||||
title: "Test Path Upload");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("task_id").GetString().Should().Be("task-uuid-from-path-12345");
|
||||
json.RootElement.GetProperty("result").GetProperty("file_name").GetString().Should().NotBeNullOrEmpty();
|
||||
json.RootElement.GetProperty("result").GetProperty("file_size").GetInt64().Should().BeGreaterThan(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UploadFromPath_ExpandsTildeToHome()
|
||||
{
|
||||
// This test verifies tilde expansion happens (even if file doesn't exist)
|
||||
// Act
|
||||
var result = await DocumentTools.UploadFromPath(
|
||||
_factory.Client,
|
||||
"~/nonexistent_test_file_12345.pdf");
|
||||
|
||||
// Assert - Should try to find the file (and fail with NOT_FOUND, not VALIDATION)
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Update Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WhenSuccessful_ReturnsUpdatedDocument()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPatch("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Updated Title"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Update(_factory.Client, 1, title: "Updated Title");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("title").GetString().Should().Be("Updated Title");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WhenNotFound_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPatchWithStatus("api/documents/999/", HttpStatusCode.NotFound);
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Update(_factory.Client, 999, title: "New Title");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Delete Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithoutConfirmation_ReturnsDryRun()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Doc to Delete"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Delete(_factory.Client, 1, confirm: false);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("CONFIRMATION_REQUIRED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithConfirmation_DeletesDocument()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupDelete("api/documents/1/", HttpStatusCode.NoContent);
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Delete(_factory.Client, 1, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("deleted").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Bulk Update Tests
|
||||
|
||||
[Fact]
|
||||
public async Task BulkUpdate_WithDryRun_ReturnsPreview()
|
||||
{
|
||||
// Act
|
||||
var result = await DocumentTools.BulkUpdate(
|
||||
_factory.Client,
|
||||
documentIds: "1,2,3",
|
||||
operation: "add_tag",
|
||||
value: 5,
|
||||
dryRun: true,
|
||||
confirm: false);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("executed").GetBoolean().Should().BeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkUpdate_WithConfirmation_ExecutesOperation()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/documents/bulk_edit/", "{}");
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.BulkUpdate(
|
||||
_factory.Client,
|
||||
documentIds: "1,2,3",
|
||||
operation: "add_tag",
|
||||
value: 5,
|
||||
dryRun: false,
|
||||
confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("executed").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkUpdate_WithInvalidOperation_ReturnsValidationError()
|
||||
{
|
||||
// Act
|
||||
var result = await DocumentTools.BulkUpdate(
|
||||
_factory.Client,
|
||||
documentIds: "1,2,3",
|
||||
operation: "invalid_operation",
|
||||
dryRun: false,
|
||||
confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("VALIDATION");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkUpdate_WithEmptyIds_ReturnsValidationError()
|
||||
{
|
||||
// Act
|
||||
var result = await DocumentTools.BulkUpdate(
|
||||
_factory.Client,
|
||||
documentIds: "",
|
||||
operation: "add_tag",
|
||||
dryRun: false,
|
||||
confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("VALIDATION");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Reprocess Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Reprocess_WithoutConfirmation_ReturnsDryRun()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Doc to Reprocess"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Reprocess(_factory.Client, 1, confirm: false);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("CONFIRMATION_REQUIRED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reprocess_WithConfirmation_QueuesReprocessing()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/documents/bulk_edit/", "{}");
|
||||
|
||||
// Act
|
||||
var result = await DocumentTools.Reprocess(_factory.Client, 1, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("status").GetString().Should().Be("queued");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using PaperlessMCP.Tests.Fixtures;
|
||||
using RichardSzalay.MockHttp;
|
||||
using PaperlessMCP.Tools;
|
||||
using Xunit;
|
||||
|
||||
namespace PaperlessMCP.Tests.Tools;
|
||||
|
||||
public class DocumentTypeToolsTests : IDisposable
|
||||
{
|
||||
private readonly MockHttpClientFactory _factory;
|
||||
|
||||
public DocumentTypeToolsTests()
|
||||
{
|
||||
_factory = new MockHttpClientFactory();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_factory.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_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 DocumentTypeTools.List(_factory.Client);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetArrayLength().Should().Be(4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenExists_ReturnsDocumentType()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/document_types/1/", TestFixtures.DocumentTypes.CreateDocumentTypeJson(1, "Invoice"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTypeTools.Get(_factory.Client, 1);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Invoice");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenNotFound_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGetWithStatus("api/document_types/999/", HttpStatusCode.NotFound);
|
||||
|
||||
// Act
|
||||
var result = await DocumentTypeTools.Get(_factory.Client, 999);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WhenSuccessful_ReturnsCreatedType()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/document_types/", TestFixtures.DocumentTypes.CreateDocumentTypeJson(1, "Contract"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTypeTools.Create(_factory.Client, "Contract");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Contract");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WhenSuccessful_ReturnsUpdatedType()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPatch("api/document_types/1/", TestFixtures.DocumentTypes.CreateDocumentTypeJson(1, "Updated Type"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTypeTools.Update(_factory.Client, 1, name: "Updated Type");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Updated Type");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithoutConfirmation_ReturnsDryRun()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/document_types/1/", TestFixtures.DocumentTypes.CreateDocumentTypeJson(1, "To Delete"));
|
||||
|
||||
// Act
|
||||
var result = await DocumentTypeTools.Delete(_factory.Client, 1, confirm: false);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("CONFIRMATION_REQUIRED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithConfirmation_DeletesType()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupDelete("api/document_types/1/", HttpStatusCode.NoContent);
|
||||
|
||||
// Act
|
||||
var result = await DocumentTypeTools.Delete(_factory.Client, 1, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("deleted").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDelete_WithConfirmation_ExecutesDeletion()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/bulk_edit_objects/", "{}");
|
||||
|
||||
// Act
|
||||
var result = await DocumentTypeTools.BulkDelete(_factory.Client, "1,2,3", dryRun: false, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("executed").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using PaperlessMCP.Tests.Fixtures;
|
||||
using RichardSzalay.MockHttp;
|
||||
using PaperlessMCP.Tools;
|
||||
using Xunit;
|
||||
|
||||
namespace PaperlessMCP.Tests.Tools;
|
||||
|
||||
public class HealthToolsTests : IDisposable
|
||||
{
|
||||
private readonly MockHttpClientFactory _factory;
|
||||
|
||||
public HealthToolsTests()
|
||||
{
|
||||
_factory = new MockHttpClientFactory();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_factory.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ping_WhenConnected_ReturnsSuccess()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/")
|
||||
.Respond(req =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
||||
response.Headers.Add("X-Version", "2.5.0");
|
||||
return response;
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await HealthTools.Ping(_factory.Client);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("connected").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Ping_WhenConnectionFails_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGetWithStatus("api/", HttpStatusCode.Unauthorized);
|
||||
|
||||
// Act
|
||||
var result = await HealthTools.Ping(_factory.Client);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("UPSTREAM_ERROR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCapabilities_ReturnsCapabilitiesInfo()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/")
|
||||
.Respond(req =>
|
||||
{
|
||||
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
||||
response.Headers.Add("X-Version", "2.5.0");
|
||||
return response;
|
||||
});
|
||||
|
||||
_factory.SetupGet("api/status/", "{}");
|
||||
|
||||
// Act
|
||||
var result = await HealthTools.GetCapabilities(_factory.Client);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
|
||||
var capabilities = json.RootElement.GetProperty("result");
|
||||
capabilities.GetProperty("connected").GetBoolean().Should().BeTrue();
|
||||
capabilities.GetProperty("endpoints").Should().NotBeNull();
|
||||
capabilities.GetProperty("bulk_edit_methods").GetArrayLength().Should().BeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetCapabilities_IncludesAllEndpointCategories()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/")
|
||||
.Respond(HttpStatusCode.OK);
|
||||
|
||||
_factory.SetupGet("api/status/", "{}");
|
||||
|
||||
// Act
|
||||
var result = await HealthTools.GetCapabilities(_factory.Client);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
var endpoints = json.RootElement.GetProperty("result").GetProperty("endpoints");
|
||||
|
||||
endpoints.GetProperty("documents").Should().NotBeNull();
|
||||
endpoints.GetProperty("tags").Should().NotBeNull();
|
||||
endpoints.GetProperty("correspondents").Should().NotBeNull();
|
||||
endpoints.GetProperty("document_types").Should().NotBeNull();
|
||||
endpoints.GetProperty("storage_paths").Should().NotBeNull();
|
||||
endpoints.GetProperty("custom_fields").Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using PaperlessMCP.Tests.Fixtures;
|
||||
using RichardSzalay.MockHttp;
|
||||
using PaperlessMCP.Tools;
|
||||
using Xunit;
|
||||
|
||||
namespace PaperlessMCP.Tests.Tools;
|
||||
|
||||
public class StoragePathToolsTests : IDisposable
|
||||
{
|
||||
private readonly MockHttpClientFactory _factory;
|
||||
|
||||
public StoragePathToolsTests()
|
||||
{
|
||||
_factory = new MockHttpClientFactory();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_factory.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_ReturnsStoragePathList()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/storage_paths/*")
|
||||
.Respond("application/json", TestFixtures.StoragePaths.CreateStoragePathListJson(3));
|
||||
|
||||
// Act
|
||||
var result = await StoragePathTools.List(_factory.Client);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetArrayLength().Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenExists_ReturnsStoragePath()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/storage_paths/1/", TestFixtures.StoragePaths.CreateStoragePathJson(1, "Archive"));
|
||||
|
||||
// Act
|
||||
var result = await StoragePathTools.Get(_factory.Client, 1);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Archive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenNotFound_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGetWithStatus("api/storage_paths/999/", HttpStatusCode.NotFound);
|
||||
|
||||
// Act
|
||||
var result = await StoragePathTools.Get(_factory.Client, 999);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WhenSuccessful_ReturnsCreatedPath()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/storage_paths/", TestFixtures.StoragePaths.CreateStoragePathJson(1, "New Archive"));
|
||||
|
||||
// Act
|
||||
var result = await StoragePathTools.Create(_factory.Client, "New Archive", "{correspondent}/{year}");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("New Archive");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WhenSuccessful_ReturnsUpdatedPath()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPatch("api/storage_paths/1/", TestFixtures.StoragePaths.CreateStoragePathJson(1, "Updated Path"));
|
||||
|
||||
// Act
|
||||
var result = await StoragePathTools.Update(_factory.Client, 1, name: "Updated Path");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Updated Path");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithoutConfirmation_ReturnsDryRun()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/storage_paths/1/", TestFixtures.StoragePaths.CreateStoragePathJson(1, "To Delete"));
|
||||
|
||||
// Act
|
||||
var result = await StoragePathTools.Delete(_factory.Client, 1, confirm: false);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("CONFIRMATION_REQUIRED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithConfirmation_DeletesPath()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupDelete("api/storage_paths/1/", HttpStatusCode.NoContent);
|
||||
|
||||
// Act
|
||||
var result = await StoragePathTools.Delete(_factory.Client, 1, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("deleted").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDelete_WithConfirmation_ExecutesDeletion()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/bulk_edit_objects/", "{}");
|
||||
|
||||
// Act
|
||||
var result = await StoragePathTools.BulkDelete(_factory.Client, "1,2", dryRun: false, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("executed").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using PaperlessMCP.Tests.Fixtures;
|
||||
using RichardSzalay.MockHttp;
|
||||
using PaperlessMCP.Tools;
|
||||
using Xunit;
|
||||
|
||||
namespace PaperlessMCP.Tests.Tools;
|
||||
|
||||
public class TagToolsTests : IDisposable
|
||||
{
|
||||
private readonly MockHttpClientFactory _factory;
|
||||
|
||||
public TagToolsTests()
|
||||
{
|
||||
_factory = new MockHttpClientFactory();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_factory.Dispose();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_ReturnsTagList()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/tags/*")
|
||||
.Respond("application/json", TestFixtures.Tags.CreateTagListJson(5));
|
||||
|
||||
// Act
|
||||
var result = await TagTools.List(_factory.Client);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetArrayLength().Should().Be(5);
|
||||
json.RootElement.GetProperty("meta").GetProperty("total").GetInt32().Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task List_WithPagination_IncludesMetadata()
|
||||
{
|
||||
// Arrange
|
||||
_factory.MockHandler
|
||||
.When(HttpMethod.Get, "https://paperless.example.com/api/tags/*")
|
||||
.Respond("application/json", TestFixtures.Tags.CreateTagListJson(10));
|
||||
|
||||
// Act
|
||||
var result = await TagTools.List(_factory.Client, page: 2, pageSize: 5);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
var meta = json.RootElement.GetProperty("meta");
|
||||
meta.GetProperty("page").GetInt32().Should().Be(2);
|
||||
meta.GetProperty("page_size").GetInt32().Should().Be(5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenTagExists_ReturnsTag()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/tags/1/", TestFixtures.Tags.CreateTagJson(1, "Important"));
|
||||
|
||||
// Act
|
||||
var result = await TagTools.Get(_factory.Client, 1);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("id").GetInt32().Should().Be(1);
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Important");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Get_WhenTagNotFound_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGetWithStatus("api/tags/999/", HttpStatusCode.NotFound);
|
||||
|
||||
// Act
|
||||
var result = await TagTools.Get(_factory.Client, 999);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WhenSuccessful_ReturnsCreatedTag()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/tags/", TestFixtures.Tags.CreateTagJson(5, "New Tag"));
|
||||
|
||||
// Act
|
||||
var result = await TagTools.Create(_factory.Client, "New Tag", color: "#00ff00");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("New Tag");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_WhenFails_ReturnsError()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPostWithStatus("api/tags/", HttpStatusCode.BadRequest);
|
||||
|
||||
// Act
|
||||
var result = await TagTools.Create(_factory.Client, "Bad Tag");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("UPSTREAM_ERROR");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Update_WhenSuccessful_ReturnsUpdatedTag()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPatch("api/tags/1/", TestFixtures.Tags.CreateTagJson(1, "Updated Name"));
|
||||
|
||||
// Act
|
||||
var result = await TagTools.Update(_factory.Client, 1, name: "Updated Name");
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("name").GetString().Should().Be("Updated Name");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithoutConfirmation_ReturnsDryRun()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupGet("api/tags/1/", TestFixtures.Tags.CreateTagJson(1, "Tag to Delete"));
|
||||
|
||||
// Act
|
||||
var result = await TagTools.Delete(_factory.Client, 1, confirm: false);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("CONFIRMATION_REQUIRED");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WithConfirmation_DeletesTag()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupDelete("api/tags/1/", HttpStatusCode.NoContent);
|
||||
|
||||
// Act
|
||||
var result = await TagTools.Delete(_factory.Client, 1, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("deleted").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDelete_WithDryRun_ReturnsPreview()
|
||||
{
|
||||
// Act
|
||||
var result = await TagTools.BulkDelete(_factory.Client, "1,2,3", dryRun: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("executed").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("result").GetProperty("affected_ids").GetArrayLength().Should().Be(3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDelete_WithConfirmation_ExecutesDeletion()
|
||||
{
|
||||
// Arrange
|
||||
_factory.SetupPost("api/bulk_edit_objects/", "{}");
|
||||
|
||||
// Act
|
||||
var result = await TagTools.BulkDelete(_factory.Client, "1,2,3", dryRun: false, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeTrue();
|
||||
json.RootElement.GetProperty("result").GetProperty("executed").GetBoolean().Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task BulkDelete_WithEmptyIds_ReturnsValidationError()
|
||||
{
|
||||
// Act
|
||||
var result = await TagTools.BulkDelete(_factory.Client, "", dryRun: false, confirm: true);
|
||||
|
||||
// Assert
|
||||
var json = JsonDocument.Parse(result);
|
||||
json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse();
|
||||
json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("VALIDATION");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user