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

namespace NewLife.Studio.Store.Tests;

public class StoreServiceTests : IDisposable
{
    private readonly string _tempDir;
    private readonly StoreService _store;

    public StoreServiceTests()
    {
        _tempDir = Path.Combine(Path.GetTempPath(), $"StoreTests_{Guid.NewGuid():N}");
        _store = new StoreService(_tempDir, new SecretProtection());
    }

    public void Dispose()
    {
        try
        {
            if (Directory.Exists(_tempDir))
                Directory.Delete(_tempDir, recursive: true);
        }
        catch
        {
            // Best effort cleanup
        }
    }

    // ========== 连接管理 ==========

    [Fact]
    public async Task SaveConnection_Then_ListConnections()
    {
        var conn = new ConnectionInfo
        {
            Id = "conn-001",
            Name = "Test DB",
            ConnectionString = "Server=localhost;Database=test;",
            ProviderType = "mysql",
            Group = "Production"
        };

        await _store.SaveConnectionAsync(conn);
        var connections = await _store.ListConnectionsAsync();

        Assert.Single(connections);

        var listed = connections[0];
        Assert.Equal(conn.Id, listed.Id);
        Assert.Equal(conn.Name, listed.Name);
        Assert.Equal(conn.ConnectionString, listed.ConnectionString);
        Assert.Equal(conn.ProviderType, listed.ProviderType);
        Assert.Equal(conn.Group, listed.Group);
    }

    [Fact]
    public async Task SaveConnection_ConnectionStringIsEncryptedInFile()
    {
        var conn = new ConnectionInfo
        {
            Id = "conn-enc-test",
            Name = "Encrypted DB",
            ConnectionString = "Server=prod-server;Pwd=super-secret;"
        };

        await _store.SaveConnectionAsync(conn);

        // 从文件直接读取,验证连接字符串已加密
        var json = await File.ReadAllTextAsync(
            Path.Combine(_tempDir, "connections.json"));
        Assert.DoesNotContain("super-secret", json);
        Assert.DoesNotContain("prod-server", json);
    }

    [Fact]
    public async Task SaveConnection_UpdateExisting()
    {
        var conn = new ConnectionInfo
        {
            Id = "conn-update",
            Name = "Original Name",
            ConnectionString = "Server=old;",
            ProviderType = "sqlite"
        };

        await _store.SaveConnectionAsync(conn);

        conn.Name = "Updated Name";
        conn.ConnectionString = "Server=new;";
        await _store.SaveConnectionAsync(conn);

        var connections = await _store.ListConnectionsAsync();

        Assert.Single(connections);
        Assert.Equal("Updated Name", connections[0].Name);
        Assert.Equal("Server=new;", connections[0].ConnectionString);
    }

    [Fact]
    public async Task ListConnections_WhenEmpty_ReturnsEmptyList()
    {
        var connections = await _store.ListConnectionsAsync();

        Assert.NotNull(connections);
        Assert.Empty(connections);
    }

    [Fact]
    public async Task DeleteConnection_RemovesConnection()
    {
        var conn = new ConnectionInfo { Id = "conn-del", Name = "To Delete" };
        await _store.SaveConnectionAsync(conn);

        await _store.DeleteConnectionAsync("conn-del");

        var connections = await _store.ListConnectionsAsync();
        Assert.Empty(connections);
    }

    [Fact]
    public async Task DeleteConnection_NonExistent_DoesNotThrow()
    {
        var conn = new ConnectionInfo { Id = "conn-keep", Name = "Keep Me" };
        await _store.SaveConnectionAsync(conn);

        await _store.DeleteConnectionAsync("non-existent-id");

        var connections = await _store.ListConnectionsAsync();
        Assert.Single(connections);
    }

    [Fact]
    public async Task SaveMultipleConnections_Then_ListAll()
    {
        var conn1 = new ConnectionInfo { Id = "c1", Name = "DB-1", Group = "Prod" };
        var conn2 = new ConnectionInfo { Id = "c2", Name = "DB-2", Group = "Dev" };
        var conn3 = new ConnectionInfo { Id = "c3", Name = "DB-3", Group = "Prod" };

        await _store.SaveConnectionAsync(conn1);
        await _store.SaveConnectionAsync(conn2);
        await _store.SaveConnectionAsync(conn3);

        var connections = await _store.ListConnectionsAsync();

        Assert.Equal(3, connections.Count);
        Assert.Contains(connections, c => c.Name == "DB-1");
        Assert.Contains(connections, c => c.Name == "DB-2");
        Assert.Contains(connections, c => c.Name == "DB-3");
    }

    // ========== 查询历史 ==========

    [Fact]
    public async Task AddQueryHistory_Then_GetRecentQueries()
    {
        var entry = new QueryHistoryEntry
        {
            Id = "q1",
            Sql = "SELECT * FROM users",
            ConnectionName = "Test DB",
            ExecutedAt = DateTime.Parse("2025-01-01T10:00:00Z"),
            ElapsedMs = 100,
            RowCount = 42
        };

        await _store.AddQueryHistoryAsync(entry);
        var queries = await _store.GetRecentQueriesAsync();

        Assert.Single(queries);
        Assert.Equal(entry.Sql, queries[0].Sql);
        Assert.Equal(entry.ConnectionName, queries[0].ConnectionName);
        Assert.Equal(entry.RowCount, queries[0].RowCount);
        Assert.Equal(entry.ElapsedMs, queries[0].ElapsedMs);
    }

    [Fact]
    public async Task GetRecentQueries_NewestFirst()
    {
        var older = new QueryHistoryEntry
        {
            Id = "old",
            Sql = "SELECT 1",
            ExecutedAt = DateTime.Parse("2025-01-01T10:00:00Z")
        };
        var newer = new QueryHistoryEntry
        {
            Id = "new",
            Sql = "SELECT 2",
            ExecutedAt = DateTime.Parse("2025-02-01T10:00:00Z")
        };

        await _store.AddQueryHistoryAsync(older);
        await _store.AddQueryHistoryAsync(newer);

        var queries = await _store.GetRecentQueriesAsync();

        Assert.Equal(2, queries.Count);
        Assert.Equal("SELECT 2", queries[0].Sql);
        Assert.Equal("SELECT 1", queries[1].Sql);
    }

    [Fact]
    public async Task GetRecentQueries_RespectsLimit()
    {
        for (var i = 0; i < 100; i++)
        {
            await _store.AddQueryHistoryAsync(new QueryHistoryEntry
            {
                Id = $"q{i}",
                Sql = $"SELECT {i}",
                ExecutedAt = DateTime.Now.AddMinutes(-i)
            });
        }

        var queries = await _store.GetRecentQueriesAsync(10);

        Assert.Equal(10, queries.Count);
    }

    [Fact]
    public async Task GetRecentQueries_WhenNoHistory_ReturnsEmptyList()
    {
        var queries = await _store.GetRecentQueriesAsync();

        Assert.NotNull(queries);
        Assert.Empty(queries);
    }

    [Fact]
    public async Task GetRecentQueries_DefaultLimitIs50()
    {
        for (var i = 0; i < 60; i++)
        {
            await _store.AddQueryHistoryAsync(new QueryHistoryEntry
            {
                Id = $"q{i}",
                Sql = $"SELECT {i}",
                ExecutedAt = DateTime.Now.AddMinutes(-i)
            });
        }

        var queries = await _store.GetRecentQueriesAsync();

        Assert.Equal(50, queries.Count);
    }

    // ========== AI 配置 ==========

    [Fact]
    public async Task SaveAiProfile_Then_GetAiProfile()
    {
        var original = new AiProfile
        {
            ProviderType = "azure",
            Endpoint = "https://my-azure.openai.com",
            ApiKey = "sk-abc123-secret-key",
            Model = "gpt-4"
        };

        await _store.SaveAiProfileAsync(original);
        var retrieved = await _store.GetAiProfileAsync();

        Assert.NotNull(retrieved);
        Assert.Equal(original.ProviderType, retrieved.ProviderType);
        Assert.Equal(original.Endpoint, retrieved.Endpoint);
        Assert.Equal(original.ApiKey, retrieved.ApiKey);
        Assert.Equal(original.Model, retrieved.Model);
    }

    [Fact]
    public async Task SaveAiProfile_ApiKeyIsEncryptedInFile()
    {
        var profile = new AiProfile
        {
            ProviderType = "openai",
            ApiKey = "sk-top-secret-key-12345"
        };

        await _store.SaveAiProfileAsync(profile);

        var json = await File.ReadAllTextAsync(
            Path.Combine(_tempDir, "ai_profile.json"));
        Assert.DoesNotContain("sk-top-secret-key-12345", json);
    }

    [Fact]
    public async Task GetAiProfile_WhenNoFile_ReturnsNull()
    {
        var profile = await _store.GetAiProfileAsync();

        Assert.Null(profile);
    }

    [Fact]
    public async Task SaveAiProfile_UpdateExisting()
    {
        var original = new AiProfile
        {
            ProviderType = "openai",
            ApiKey = "old-key"
        };
        await _store.SaveAiProfileAsync(original);

        var updated = new AiProfile
        {
            ProviderType = "azure",
            ApiKey = "new-key",
            Model = "gpt-4-turbo"
        };
        await _store.SaveAiProfileAsync(updated);

        var retrieved = await _store.GetAiProfileAsync();

        Assert.NotNull(retrieved);
        Assert.Equal("azure", retrieved.ProviderType);
        Assert.Equal("new-key", retrieved.ApiKey);
        Assert.Equal("gpt-4-turbo", retrieved.Model);
    }

    // ========== 应用偏好 ==========

    [Fact]
    public async Task SavePreferences_Then_GetPreferences()
    {
        var pref = new AppPreference
        {
            MaxRows = 500,
            DefaultExportPath = "/home/user/exports",
            Theme = "Dark",
            Language = "en-US"
        };

        await _store.SavePreferencesAsync(pref);
        var retrieved = await _store.GetPreferencesAsync();

        Assert.Equal(pref.MaxRows, retrieved.MaxRows);
        Assert.Equal(pref.DefaultExportPath, retrieved.DefaultExportPath);
        Assert.Equal(pref.Theme, retrieved.Theme);
        Assert.Equal(pref.Language, retrieved.Language);
    }

    [Fact]
    public async Task GetPreferences_WhenNoFile_ReturnsDefaults()
    {
        var pref = await _store.GetPreferencesAsync();

        Assert.NotNull(pref);
        Assert.Equal(1000, pref.MaxRows);
        Assert.Equal("", pref.DefaultExportPath);
        Assert.Equal("Light", pref.Theme);
        Assert.Equal("zh-CN", pref.Language);
    }

    [Fact]
    public async Task SavePreferences_UpdateExisting()
    {
        var pref = new AppPreference
        {
            MaxRows = 200,
            Theme = "Light"
        };
        await _store.SavePreferencesAsync(pref);

        pref.MaxRows = 5000;
        pref.Theme = "Dark";
        pref.Language = "ja-JP";
        await _store.SavePreferencesAsync(pref);

        var retrieved = await _store.GetPreferencesAsync();

        Assert.Equal(5000, retrieved.MaxRows);
        Assert.Equal("Dark", retrieved.Theme);
        Assert.Equal("ja-JP", retrieved.Language);
    }
}