using Centron.BusinessLogic.Telemetry; using Centron.DAO; using Centron.DAO.Mappings.Telemetry; using Centron.Data.Entities.Telemetry; using FluentNHibernate.Cfg; using FluentNHibernate.Cfg.Db; using JetBrains.Annotations; using NHibernate; using NHibernate.Tool.hbm2ddl; namespace Centron.Tests.BL.Telemetry; [TestSubject(typeof(TelemetryBL))] public class TelemetryBLTest { private static readonly DateTime _bucket = new(2026, 5, 2, 12, 15, 0, DateTimeKind.Utc); [Fact] public void UpsertMcpToolUsageBatch_EmptyInput_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.UpsertMcpToolUsageBatch(Array.Empty()); Assert.True(result.IsSuccess); } [Fact] public void UpsertMcpToolUsageBatch_NullInput_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.UpsertMcpToolUsageBatch(null); Assert.True(result.IsSuccess); } [Fact] public void UpsertApiCallBatch_EmptyInput_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.UpsertApiCallBatch(Array.Empty()); Assert.True(result.IsSuccess); } [Fact] public void UpsertApiCallBatch_NullInput_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.UpsertApiCallBatch(null); Assert.True(result.IsSuccess); } [Fact] public void UpsertArtificialIntelligenceToolUsageBatch_EmptyInput_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.UpsertArtificialIntelligenceToolUsageBatch(Array.Empty()); Assert.True(result.IsSuccess); } [Fact] public void UpsertArtificialIntelligenceToolUsageBatch_NullInput_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.UpsertArtificialIntelligenceToolUsageBatch(null); Assert.True(result.IsSuccess); } [Fact] public void GetCompletedPendingMcpToolUsage_ReturnsAllRowsBeforeCutoffAndUnuploaded() { using var session = CreateSession(); var cutoff = _bucket; for (int i = 0; i < 5; i++) session.Save(new McpToolUsageTelemetry { UserID = i + 1, ToolNameI3D = 100 + i, ToolMode = McpToolMode.User, BucketStartUtc = cutoff.AddMinutes(-15 * (i + 1)), Count = 1 }); session.Save(new McpToolUsageTelemetry { UserID = 99, ToolNameI3D = 99, ToolMode = McpToolMode.User, BucketStartUtc = cutoff.AddMinutes(15), Count = 1 }); session.Save(new McpToolUsageTelemetry { UserID = 100, ToolNameI3D = 100, ToolMode = McpToolMode.User, BucketStartUtc = cutoff.AddMinutes(-15), Count = 1, UploadedDate = DateTime.UtcNow }); session.Flush(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.GetCompletedPendingMcpToolUsage(cutoff); Assert.True(result.IsSuccess); Assert.Equal(5, result.Data.Count); Assert.All(result.Data, r => Assert.True(r.BucketStartUtc < cutoff)); Assert.All(result.Data, r => Assert.Null(r.UploadedDate)); } [Fact] public void GetCompletedPendingArtificialIntelligenceToolUsage_ReturnsAllRowsBeforeCutoffAndUnuploaded() { using var session = CreateSession(); var cutoff = _bucket; for (int i = 0; i < 5; i++) session.Save(new ArtificialIntelligenceToolUsageTelemetry { UserID = i + 1, ToolNameI3D = 100 + i, HardwareIDI3D = 200 + i, BucketStartUtc = cutoff.AddMinutes(-15 * (i + 1)), Count = 1 }); session.Save(new ArtificialIntelligenceToolUsageTelemetry { UserID = 99, ToolNameI3D = 99, HardwareIDI3D = 99, BucketStartUtc = cutoff.AddMinutes(15), Count = 1 }); session.Save(new ArtificialIntelligenceToolUsageTelemetry { UserID = 100, ToolNameI3D = 100, HardwareIDI3D = 100, BucketStartUtc = cutoff.AddMinutes(-15), Count = 1, UploadedDate = DateTime.UtcNow }); session.Flush(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.GetCompletedPendingArtificialIntelligenceToolUsage(cutoff); Assert.True(result.IsSuccess); Assert.Equal(5, result.Data.Count); Assert.All(result.Data, r => Assert.True(r.BucketStartUtc < cutoff)); Assert.All(result.Data, r => Assert.Null(r.UploadedDate)); } [Fact] public void GetCompletedPendingApiCalls_ReturnsAllRowsBeforeCutoffAndUnuploaded() { using var session = CreateSession(); var cutoff = _bucket; for (int i = 0; i < 5; i++) session.Save(new ApiCallTelemetry { UserID = i + 1, UserKind = TelemetryUserKind.User, LicenseKind = TelemetryLicenseKind.Centron, MethodNameI3D = 100 + i, BucketStartUtc = cutoff.AddMinutes(-15 * (i + 1)), Count = 1 }); session.Save(new ApiCallTelemetry { UserID = 99, UserKind = TelemetryUserKind.User, LicenseKind = TelemetryLicenseKind.Centron, MethodNameI3D = 99, BucketStartUtc = cutoff.AddMinutes(15), Count = 1 }); session.Save(new ApiCallTelemetry { UserID = 100, UserKind = TelemetryUserKind.User, LicenseKind = TelemetryLicenseKind.Centron, MethodNameI3D = 100, BucketStartUtc = cutoff.AddMinutes(-15), Count = 1, UploadedDate = DateTime.UtcNow }); session.Flush(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.GetCompletedPendingApiCalls(cutoff); Assert.True(result.IsSuccess); Assert.Equal(5, result.Data.Count); Assert.All(result.Data, r => Assert.True(r.BucketStartUtc < cutoff)); Assert.All(result.Data, r => Assert.Null(r.UploadedDate)); } [Fact] public void LoadAllMcpToolNames_ReturnsAllRows() { using var session = CreateSession(); session.Save(new McpToolNameLookup { Name = "tool_a" }); session.Save(new McpToolNameLookup { Name = "tool_b" }); session.Flush(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.LoadAllMcpToolNames(); Assert.True(result.IsSuccess); Assert.Equal(2, result.Data.Count); Assert.Contains(result.Data, r => r.Name == "tool_a"); Assert.Contains(result.Data, r => r.Name == "tool_b"); } [Fact] public void LoadAllApiMethodNames_ReturnsAllRows() { using var session = CreateSession(); session.Save(new ApiMethodNameLookup { Name = "GetCustomer" }); session.Save(new ApiMethodNameLookup { Name = "SaveReceipt" }); session.Flush(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.LoadAllApiMethodNames(); Assert.True(result.IsSuccess); Assert.Equal(2, result.Data.Count); } // MarkMcpToolUsageUploaded / MarkApiCallsUploaded "happy path" with row updates is // not unit-tested on SQLite: the raw SQL uses "dbo." schema prefix which SQLite rejects. // Same reason for ResolveMcp*/Api* and the Upsert*Batch methods (raw MERGE statements). // Coverage is provided by the EndToEnd tests against SQL Server. [Fact] public void MarkMcpToolUsageUploaded_EmptyIds_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.MarkMcpToolUsageUploaded(Array.Empty(), DateTime.UtcNow); Assert.True(result.IsSuccess); } [Fact] public void MarkApiCallsUploaded_EmptyIds_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.MarkApiCallsUploaded(Array.Empty(), DateTime.UtcNow); Assert.True(result.IsSuccess); } [Fact] public void MarkArtificialIntelligenceToolUsageUploaded_EmptyIds_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.MarkArtificialIntelligenceToolUsageUploaded(Array.Empty(), DateTime.UtcNow); Assert.True(result.IsSuccess); } [Fact] public void RecordArtificialIntelligenceToolUsage_InvalidInput_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); Assert.True(bl.RecordArtificialIntelligenceToolUsage(0, "tool", "hardware", DateTime.UtcNow).IsSuccess); Assert.True(bl.RecordArtificialIntelligenceToolUsage(1, null, "hardware", DateTime.UtcNow).IsSuccess); Assert.True(bl.RecordArtificialIntelligenceToolUsage(1, string.Empty, "hardware", DateTime.UtcNow).IsSuccess); } [Fact] public void RecordArtificialIntelligenceToolUsageBatch_InvalidInput_ReturnsSuccess() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); Assert.True(bl.RecordArtificialIntelligenceToolUsageBatch(0, new[] { "tool" }, "hardware", DateTime.UtcNow).IsSuccess); Assert.True(bl.RecordArtificialIntelligenceToolUsageBatch(1, null, "hardware", DateTime.UtcNow).IsSuccess); Assert.True(bl.RecordArtificialIntelligenceToolUsageBatch(1, Array.Empty(), "hardware", DateTime.UtcNow).IsSuccess); Assert.True(bl.RecordArtificialIntelligenceToolUsageBatch(1, new[] { null, string.Empty }, "hardware", DateTime.UtcNow).IsSuccess); } [Fact] public void ResolveMcpToolNameI3Ds_EmptyInput_ReturnsEmptyDictionary() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.ResolveMcpToolNameI3Ds(Array.Empty()); Assert.True(result.IsSuccess); Assert.Empty(result.Data); } [Fact] public void ResolveApiMethodNameI3Ds_EmptyInput_ReturnsEmptyDictionary() { using var session = CreateSession(); var bl = new TelemetryBL(new DAOSession(session)); var result = bl.ResolveApiMethodNameI3Ds(Array.Empty()); Assert.True(result.IsSuccess); Assert.Empty(result.Data); } private static ISession CreateSession() { var fluentConfig = Fluently.Configure() .Database(SQLiteConfiguration.Standard.InMemory()) .Mappings(m => { m.FluentMappings.Add(); m.FluentMappings.Add(); m.FluentMappings.Add(); m.FluentMappings.Add(); m.FluentMappings.Add(); m.FluentMappings.Add(); }); var sessionFactory = fluentConfig.BuildSessionFactory(); var session = sessionFactory.OpenSession(); new SchemaExport(fluentConfig.BuildConfiguration()) .Execute(false, true, false, session.Connection, null); return session; } }