feat: 初始化NewLife Studio项目,完成基础框架与数据管理模块
何炳宏 authored at 2026-05-26 12:09:09
7.72 KiB
NewLife.Studio
using Moq;
using NewLife.Studio.Core.DTOs;
using NewLife.Studio.Data;
using NewLife.Studio.Modules.DataStudio.ViewModels;
using NewLife.Studio.Store;
using Xunit;

namespace NewLife.Studio.Modules.DataStudio.Tests;

public class SqlEditorViewModelTests
{
    private readonly Mock<IStoreService> _storeServiceMock;
    private readonly SqlEditorViewModel _vm;

    public SqlEditorViewModelTests()
    {
        _storeServiceMock = new Mock<IStoreService>();
        _vm = new SqlEditorViewModel(_storeServiceMock.Object);
    }

    [Fact]
    public void InitialState_HasEmptyTabs()
    {
        Assert.NotNull(_vm.Tabs);
        Assert.Empty(_vm.Tabs);
    }

    [Fact]
    public void InitialState_ActiveTab_IsNull()
    {
        Assert.Null(_vm.ActiveTab);
    }

    [Fact]
    public void NewTabCommand_CreatesNewTab()
    {
        _vm.NewTabCommand.Execute(null);

        Assert.Single(_vm.Tabs);
        Assert.NotNull(_vm.Tabs[0]);
    }

    [Fact]
    public void NewTabCommand_SetsActiveTab()
    {
        _vm.NewTabCommand.Execute(null);

        Assert.NotNull(_vm.ActiveTab);
        Assert.Same(_vm.Tabs[0], _vm.ActiveTab);
    }

    [Fact]
    public void NewTabCommand_FirstTab_HasDefaultTitle()
    {
        _vm.NewTabCommand.Execute(null);

        Assert.Equal("查询 1", _vm.Tabs[0].Title);
    }

    [Fact]
    public void NewTabCommand_SecondTab_HasIncrementedTitle()
    {
        _vm.NewTabCommand.Execute(null); // "查询 1"
        _vm.NewTabCommand.Execute(null); // "查询 2"

        Assert.Equal(2, _vm.Tabs.Count);
        Assert.Equal("查询 1", _vm.Tabs[0].Title);
        Assert.Equal("查询 2", _vm.Tabs[1].Title);
    }

    [Fact]
    public void NewTabCommand_ThirdTab_HasIncrementedTitle()
    {
        _vm.NewTabCommand.Execute(null);
        _vm.NewTabCommand.Execute(null);
        _vm.NewTabCommand.Execute(null);

        Assert.Equal(3, _vm.Tabs.Count);
        Assert.Equal("查询 3", _vm.Tabs[2].Title);
    }

    [Fact]
    public void NewTabCommand_EachTabHasIndependentViewModel()
    {
        _vm.NewTabCommand.Execute(null);
        _vm.NewTabCommand.Execute(null);

        Assert.NotSame(_vm.Tabs[0].ResultGrid, _vm.Tabs[1].ResultGrid);
    }

    [Fact]
    public void NewTabCommand_ActiveTabIsLastCreated()
    {
        _vm.NewTabCommand.Execute(null);
        var first = _vm.ActiveTab;
        _vm.NewTabCommand.Execute(null);
        var second = _vm.ActiveTab;

        Assert.NotSame(first, second);
        Assert.Same(_vm.Tabs[1], _vm.ActiveTab);
    }

    [Fact]
    public void CloseTabCommand_RemovesTab()
    {
        _vm.NewTabCommand.Execute(null);
        var tab = _vm.Tabs[0];

        _vm.CloseTabCommand.Execute(tab);

        Assert.Empty(_vm.Tabs);
    }

    [Fact]
    public void CloseTabCommand_WithNull_DoesNothing()
    {
        _vm.NewTabCommand.Execute(null);

        _vm.CloseTabCommand.Execute(null);

        Assert.Single(_vm.Tabs);
    }

    [Fact]
    public void CloseTabCommand_RemovesMiddleTab_KeepsOthers()
    {
        _vm.NewTabCommand.Execute(null); // tabs[0]
        _vm.NewTabCommand.Execute(null); // tabs[1]
        _vm.NewTabCommand.Execute(null); // tabs[2]

        _vm.CloseTabCommand.Execute(_vm.Tabs[1]);

        Assert.Equal(2, _vm.Tabs.Count);
        Assert.Equal("查询 1", _vm.Tabs[0].Title);
        Assert.Equal("查询 3", _vm.Tabs[1].Title);
    }

    [Fact]
    public async Task ExecuteAsync_WithNullActiveTab_DoesNothing()
    {
        var mockSession = new Mock<IDbSession>();
        _vm.SetSession(mockSession.Object);

        await _vm.ExecuteCommand.ExecuteAsync(null);

        mockSession.Verify(
            s => s.ExecuteQueryAsync(It.IsAny<QueryRequest>(), It.IsAny<CancellationToken>()),
            Times.Never);
    }

    [Fact]
    public async Task ExecuteAsync_WithNullSession_DoesNothing()
    {
        _vm.NewTabCommand.Execute(null);

        await _vm.ExecuteCommand.ExecuteAsync(null);

        // No session, no store calls either
        _storeServiceMock.Verify(
            s => s.AddQueryHistoryAsync(It.IsAny<QueryHistoryEntry>()),
            Times.Never);
    }

    [Fact]
    public async Task ExecuteAsync_ExecutesQuery_AndSetsResult()
    {
        _vm.NewTabCommand.Execute(null);
        _vm.ActiveTab!.Sql = "SELECT * FROM users";

        var mockSession = new Mock<IDbSession>();
        var result = new QueryResult
        {
            Columns = new[] { new ColumnInfo { Name = "Id", DataType = "INTEGER" } },
            Rows = new List<object?[]> { new object?[] { 1 } },
            RowCount = 1,
            ElapsedMs = 10
        };
        mockSession
            .Setup(s => s.ExecuteQueryAsync(It.IsAny<QueryRequest>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(result);
        mockSession
            .Setup(s => s.Connection)
            .Returns(new ConnectionInfo { Name = "TestDB" });
        _storeServiceMock
            .Setup(s => s.AddQueryHistoryAsync(It.IsAny<QueryHistoryEntry>()))
            .Returns(Task.CompletedTask);

        _vm.SetSession(mockSession.Object);

        await _vm.ExecuteCommand.ExecuteAsync(null);

        Assert.NotNull(_vm.ActiveTab.Result);
        Assert.Equal(1, _vm.ActiveTab.Result!.RowCount);
        mockSession.Verify(
            s => s.ExecuteQueryAsync(
                It.Is<QueryRequest>(r => r.Sql == "SELECT * FROM users"),
                It.IsAny<CancellationToken>()),
            Times.Once);
    }

    [Fact]
    public async Task ExecuteAsync_AddsQueryHistory()
    {
        _vm.NewTabCommand.Execute(null);
        _vm.ActiveTab!.Sql = "SELECT 1";

        var mockSession = new Mock<IDbSession>();
        mockSession
            .Setup(s => s.ExecuteQueryAsync(It.IsAny<QueryRequest>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new QueryResult
            {
                Columns = [],
                Rows = [],
                RowCount = 0,
                ElapsedMs = 5
            });
        mockSession
            .Setup(s => s.Connection)
            .Returns(new ConnectionInfo { Name = "HistoryDB" });
        _storeServiceMock
            .Setup(s => s.AddQueryHistoryAsync(It.IsAny<QueryHistoryEntry>()))
            .Returns(Task.CompletedTask);

        _vm.SetSession(mockSession.Object);

        await _vm.ExecuteCommand.ExecuteAsync(null);

        _storeServiceMock.Verify(
            s => s.AddQueryHistoryAsync(It.Is<QueryHistoryEntry>(
                h => h.Sql == "SELECT 1" && h.ConnectionName == "HistoryDB" && h.ElapsedMs == 5)),
            Times.Once);
    }

    [Fact]
    public async Task SetSession_StoresSessionInternally()
    {
        var mockSession = new Mock<IDbSession>();
        _vm.SetSession(mockSession.Object);

        // Can verify by running ExecuteAsync successfully
        _vm.NewTabCommand.Execute(null);
        mockSession
            .Setup(s => s.ExecuteQueryAsync(It.IsAny<QueryRequest>(), It.IsAny<CancellationToken>()))
            .ReturnsAsync(new QueryResult { Columns = [], Rows = [] });
        mockSession
            .Setup(s => s.Connection)
            .Returns(new ConnectionInfo { Name = "Test" });
        _storeServiceMock
            .Setup(s => s.AddQueryHistoryAsync(It.IsAny<QueryHistoryEntry>()))
            .Returns(Task.CompletedTask);

        // Should not throw
        var exception = await Record.ExceptionAsync(() => _vm.ExecuteCommand.ExecuteAsync(null));
        Assert.Null(exception);
    }

    [Fact]
    public void Tabs_Collection_CanBeObservedExternally()
    {
        var addedTabs = new List<QueryTab>();
        _vm.Tabs.CollectionChanged += (_, args) =>
        {
            if (args.NewItems != null)
                foreach (QueryTab tab in args.NewItems)
                    addedTabs.Add(tab);
        };

        _vm.NewTabCommand.Execute(null);

        Assert.Single(addedTabs);
    }
}