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);
}
}
|