XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text;
using Xunit;

namespace XFESystem.Documents.Tests;

public sealed class DocumentServiceTests
{
    [Fact]
    public async Task OpenEditAndSaveUsesTheFilesystemAdapterAndTracksRevision()
    {
        var store = new MemoryStore("# Original");
        var service = new DocumentService(store);
        var session = await service.OpenAsync(new("X:\\Users\\Default\\Documents\\Notes.md"));
        Assert.Equal("markdown", session.HighlightLanguage);
        Assert.False(session.IsModified);
        session.Text = "# Updated 中文";
        Assert.True(session.IsModified);
        await service.SaveAsync(session);
        Assert.Equal("# Updated 中文", Encoding.UTF8.GetString(store.Content));
        Assert.Equal("2", session.Revision);
        Assert.False(session.IsModified);
    }

    [Fact]
    public async Task StaleDocumentSaveDoesNotOverwriteAnotherEditorsChange()
    {
        var store = new MemoryStore("Original");
        var service = new DocumentService(store);
        var first = await service.OpenAsync(new("X:\\Users\\Default\\Documents\\Notes.txt"));
        var second = await service.OpenAsync(first.Location);
        first.Text = "First editor";
        await service.SaveAsync(first);
        second.Text = "Second editor";
        await Assert.ThrowsAsync<DocumentConflictException>(async () => await service.SaveAsync(second));
        Assert.True(second.IsModified);
        Assert.Equal("First editor", Encoding.UTF8.GetString(store.Content));
    }

    [Fact]
    public async Task EditsDuringSaveRemainDirty()
    {
        var store = new MemoryStore("Original");
        var service = new DocumentService(store);
        var session = await service.OpenAsync(new("X:\\Users\\Default\\Documents\\Notes.txt"));
        session.Text = "Snapshot";
        store.DuringSave = () => session.Text = "New edit";
        await service.SaveAsync(session);
        Assert.Equal("Snapshot", Encoding.UTF8.GetString(store.Content));
        Assert.Equal("New edit", session.Text);
        Assert.True(session.IsModified);
    }

    [Fact]
    public async Task ReadOnlyFilesRejectSaveWithoutWriting()
    {
        var store = new MemoryStore("Read only") { ReadOnly = true };
        var service = new DocumentService(store);
        var session = await service.OpenAsync(new("X:\\System\\Welcome.txt"));
        session.Text = "Change";
        await Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await service.SaveAsync(session));
        Assert.Equal(0, store.Writes);
    }

    [Fact]
    public async Task InvalidUtf8AndBinaryFilesAreRejected()
    {
        var store = new MemoryStore("Text") { Content = [0xC3, 0x28] };
        var service = new DocumentService(store);
        await Assert.ThrowsAsync<DecoderFallbackException>(async () => await service.OpenAsync(new("X:\\Users\\Default\\Documents\\Bad.txt")));
        store.Content = [65, 0, 66];
        await Assert.ThrowsAsync<IOException>(async () => await service.OpenAsync(new("X:\\Users\\Default\\Documents\\Binary.txt")));
    }

    [Fact]
    public async Task Utf8BomIsRecognizedWithoutBecomingDocumentText()
    {
        var store = new MemoryStore("Text") { Content = [0xEF, 0xBB, 0xBF, 65, 66] };
        Assert.Equal("AB", (await new DocumentService(store).OpenAsync(new("X:\\Users\\Default\\Documents\\Bom.txt"))).Text);
    }

    [Fact]
    public async Task ReadAndSaveLimitsCountUtf8Bytes()
    {
        var store = new MemoryStore("Text");
        var service = new DocumentService(store, 5);
        var session = await service.OpenAsync(new("X:\\Users\\Default\\Documents\\Limit.txt"));
        session.Text = "中文";
        await Assert.ThrowsAsync<IOException>(async () => await service.SaveAsync(session));
        Assert.Equal(0, store.Writes);
        store.Content = new byte[6];
        await Assert.ThrowsAsync<IOException>(async () => await service.OpenAsync(session.Location));
    }

    [Theory]
    [InlineData("C:/Windows/Secret.txt")]
    [InlineData("/etc/passwd")]
    [InlineData("X:\\Users\\..\\System\\Secret.txt")]
    [InlineData("X:\\Users\\Secret.")]
    [InlineData("X:\\Users\\Secret.txt:Stream")]
    [InlineData("X:\\Users/Secret.txt")]
    [InlineData("X:\\Users\\Default\\Documents\\Secret\n.txt")]
    public void PathsCannotAddressHostFilesOrTraverseDirectories(string path) =>
        Assert.Throws<ArgumentException>(() => new DocumentLocation(path));

    private sealed class MemoryStore(string text) : IDocumentStorage
    {
        public byte[] Content { get; set; } = Encoding.UTF8.GetBytes(text);
        public bool ReadOnly { get; init; }
        public int Writes { get; private set; }
        public Action? DuringSave { get; set; }
        private int _revision = 1;

        public ValueTask<StoredDocument> ReadAsync(DocumentLocation location, int maximumBytes, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            return ValueTask.FromResult(new StoredDocument(Content.ToArray(), _revision.ToString(), ReadOnly));
        }

        public ValueTask<string> ReplaceAsync(DocumentLocation location, ReadOnlyMemory<byte> content,
            string expectedRevision, CancellationToken cancellationToken)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (expectedRevision != _revision.ToString()) throw new DocumentConflictException("File changed.");
            DuringSave?.Invoke();
            Content = content.ToArray();
            Writes++;
            return ValueTask.FromResult((++_revision).ToString());
        }
    }
}