feat: 初始化NewLife Studio项目,完成基础框架与数据管理模块
何炳宏 authored at 2026-05-26 12:09:09
7.61 KiB
NewLife.Studio
using Microsoft.Data.Sqlite;
using NewLife.Studio.Core.DTOs;
using NewLife.Studio.Data.Providers.SQLite;
using Xunit;

namespace NewLife.Studio.Data.Tests;

public class SQLiteSessionTests : IDisposable
{
    private readonly SqliteConnection _setupConnection;

    public SQLiteSessionTests()
    {
        // shared in-memory DB: tables created via _setupConnection
        // are visible to SQLiteSession connections opened with the same shared URI
        _setupConnection = new SqliteConnection("Data Source=file::memory:?cache=shared");
        _setupConnection.Open();
    }

    public void Dispose()
    {
        _setupConnection?.Dispose();
    }

    private static ConnectionInfo CreateInMemoryConnection() => new()
    {
        Name = "test-session",
        ConnectionString = "Data Source=file::memory:?cache=shared",
        ProviderType = "sqlite"
    };

    private void ExecuteSql(string sql)
    {
        using var cmd = _setupConnection.CreateCommand();
        cmd.CommandText = sql;
        cmd.ExecuteNonQuery();
    }

    [Fact]
    public async Task GetTablesAsync_WithCreatedTable_ReturnsTable()
    {
        ExecuteSql("CREATE TABLE test_users (id INTEGER PRIMARY KEY, name TEXT NOT NULL);");
        var session = new SQLiteSession(CreateInMemoryConnection());

        var tables = await session.GetTablesAsync();

        Assert.NotEmpty(tables);
        Assert.Contains(tables, t => t.Name == "test_users" && t.Schema == "main");
    }

    [Fact]
    public async Task GetTablesAsync_WhenEmpty_Database_ReturnsNoTables()
    {
        // Use a private in-memory connection (not shared) so no tables exist
        var conn = new ConnectionInfo
        {
            Name = "test-empty",
            ConnectionString = "Data Source=:memory:",
            ProviderType = "sqlite"
        };
        var session = new SQLiteSession(conn);

        var tables = await session.GetTablesAsync();

        Assert.Empty(tables);
    }

    [Fact]
    public async Task GetColumnsAsync_ReturnsCorrectColumnInfo()
    {
        ExecuteSql("CREATE TABLE test_products (" +
                   "id INTEGER PRIMARY KEY NOT NULL, " +
                   "name TEXT NOT NULL, " +
                   "price REAL, " +
                   "description TEXT DEFAULT 'N/A');");
        var session = new SQLiteSession(CreateInMemoryConnection());

        var columns = await session.GetColumnsAsync("test_products");

        Assert.Equal(4, columns.Length);

        var idCol = columns.First(c => c.Name == "id");
        Assert.Equal("INTEGER", idCol.DataType);
        Assert.False(idCol.IsNullable);
        Assert.True(idCol.IsPrimaryKey);
        Assert.Equal(0, idCol.Ordinal);

        var nameCol = columns.First(c => c.Name == "name");
        Assert.Equal("TEXT", nameCol.DataType);
        Assert.False(nameCol.IsNullable);

        var priceCol = columns.First(c => c.Name == "price");
        Assert.Equal("REAL", priceCol.DataType);
        Assert.True(priceCol.IsNullable);

        var descCol = columns.First(c => c.Name == "description");
        Assert.Equal("TEXT", descCol.DataType);
        Assert.Equal("'N/A'", descCol.DefaultValue);
    }

    [Fact]
    public async Task ExecuteQueryAsync_WithSelect_ReturnsExpectedRows()
    {
        ExecuteSql("CREATE TABLE test_items (id INTEGER PRIMARY KEY, value TEXT);");
        ExecuteSql("INSERT INTO test_items (value) VALUES ('alpha');");
        ExecuteSql("INSERT INTO test_items (value) VALUES ('beta');");
        ExecuteSql("INSERT INTO test_items (value) VALUES ('gamma');");
        var session = new SQLiteSession(CreateInMemoryConnection());

        var request = new QueryRequest
        {
            Sql = "SELECT id, value FROM test_items ORDER BY id",
            MaxRows = 100
        };
        var result = await session.ExecuteQueryAsync(request);

        Assert.Null(result.Error);
        Assert.Equal(3, result.RowCount);
        Assert.False(result.Truncated);
        Assert.Equal(2, result.Columns.Length);
        Assert.Equal("id", result.Columns[0].Name);
        Assert.Equal("value", result.Columns[1].Name);

        Assert.Equal(3, result.Rows.Count);
        Assert.Equal("alpha", result.Rows[0][1]);
        Assert.Equal("beta", result.Rows[1][1]);
        Assert.Equal("gamma", result.Rows[2][1]);
    }

    [Fact]
    public async Task ExecuteQueryAsync_MaxRowsTruncation_TruncatesResult()
    {
        ExecuteSql("CREATE TABLE test_rows (id INTEGER PRIMARY KEY, num INTEGER);");
        for (int i = 0; i < 10; i++)
            ExecuteSql($"INSERT INTO test_rows (num) VALUES ({i});");
        var session = new SQLiteSession(CreateInMemoryConnection());

        var request = new QueryRequest
        {
            Sql = "SELECT id, num FROM test_rows ORDER BY id",
            MaxRows = 4
        };
        var result = await session.ExecuteQueryAsync(request);

        Assert.True(result.Truncated);
        Assert.Equal(4, result.RowCount);
        Assert.Equal(4, result.Rows.Count);
    }

    [Fact]
    public async Task ExecuteQueryAsync_WithInvalidSql_ReturnsError()
    {
        var session = new SQLiteSession(CreateInMemoryConnection());

        var request = new QueryRequest
        {
            Sql = "SELECTZ * FROM nonexistent_table;"
        };
        var result = await session.ExecuteQueryAsync(request);

        Assert.NotNull(result.Error);
        Assert.NotEmpty(result.Error);
    }

    [Fact]
    public async Task ExecuteQueryAsync_RecordsElapsedTime()
    {
        ExecuteSql("CREATE TABLE test_time (id INTEGER PRIMARY KEY, data TEXT);");
        var session = new SQLiteSession(CreateInMemoryConnection());

        var request = new QueryRequest
        {
            Sql = "SELECT * FROM test_time",
            MaxRows = 100
        };
        var result = await session.ExecuteQueryAsync(request);

        Assert.True(result.ElapsedMs >= 0);
    }

    [Fact]
    public async Task ExecuteQueryAsync_WithDBNull_ReturnsNullInRow()
    {
        ExecuteSql("CREATE TABLE test_nulls (id INTEGER PRIMARY KEY, value TEXT);");
        ExecuteSql("INSERT INTO test_nulls (value) VALUES (NULL);");
        var session = new SQLiteSession(CreateInMemoryConnection());

        var request = new QueryRequest
        {
            Sql = "SELECT id, value FROM test_nulls",
            MaxRows = 100
        };
        var result = await session.ExecuteQueryAsync(request);

        Assert.Equal(1, result.RowCount);
        Assert.Null(result.Rows[0][1]);
    }

    [Fact]
    public async Task CloseAsync_ClosesSession()
    {
        ExecuteSql("CREATE TABLE test_close (id INTEGER PRIMARY KEY);");
        var session = new SQLiteSession(CreateInMemoryConnection());
        // force open
        await session.GetTablesAsync();
        Assert.True(session.IsOpen);

        await session.CloseAsync();

        Assert.False(session.IsOpen);
    }

    [Fact]
    public async Task Dispose_ClosesConnection()
    {
        ExecuteSql("CREATE TABLE test_dispose (id INTEGER PRIMARY KEY);");
        var session = new SQLiteSession(CreateInMemoryConnection());
        await session.GetTablesAsync();
        Assert.True(session.IsOpen);

        session.Dispose();

        Assert.False(session.IsOpen);
    }

    [Fact]
    public async Task CloseAsync_CanBeCalledMultipleTimes()
    {
        var session = new SQLiteSession(CreateInMemoryConnection());
        await session.CloseAsync();
        await session.CloseAsync();

        Assert.False(session.IsOpen);
    }

    [Fact]
    public async Task Dispose_CanBeCalledMultipleTimes()
    {
        var session = new SQLiteSession(CreateInMemoryConnection());

        session.Dispose();
        session.Dispose();

        Assert.False(session.IsOpen);
    }
}