a37630aeac
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>
207 lines
6.9 KiB
C#
207 lines
6.9 KiB
C#
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");
|
|
}
|
|
}
|