using System.Net; using System.Text.Json; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using SQLInsight.Core.Data; using SQLInsight.Core.DynamicsAx; using SQLInsight.Core.Instances; using SQLInsight.Repository; using SQLInsight.Web; namespace SQLInsight.IntegrationTests; /// The Dynamics AX pages in SQLInsight Web and the same data in API v1, with sensitive values redacted. [Collection(SqlServerTestGroup.Name)] [Trait("Category", "Integration")] public class DynamicsAxWebTests(SqlServerFixture sql) : IAsyncLifetime { private readonly string _database = "SQLInsightAxWeb_" + Guid.NewGuid().ToString("N")[..8]; private WebApplicationFactory _factory = null!; private const string Name = @"AX01\AX30"; public async Task InitializeAsync() { await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];"); var repository = new InsightRepository(sql.DatabaseConnectionString(_database)); await repository.MigrateAsync(); var now = DateTimeOffset.UtcNow; var id = await repository.UpsertInstanceAsync(Name, new InstanceInfo { ServerName = "AX01", Version = SqlServerVersion.Parse("16.0.4135.4"), ProductLevel = "RTM", Edition = "Standard Edition", EngineEdition = EngineEdition.Standard, }, "SqlClient", false, now); // AX 3.0 recognised from the customer's real schema, without read access; AX 2012 with it. var ax30 = AxDetection.Recognise("AX30", DynamicsAxTests.Schema("schema-ax30").DetectionRows(readable: false), ["AX30"])! with { AgentLogin = @"KUNDE\svc-sqlinsight$" }; var ax2012 = AxDetection.Recognise("AX2012", DynamicsAxTests.Schema("schema-ax2012").DetectionRows(readable: true, "svc"), ["AX2012", "AX2012_model"])! with { AgentLogin = @"KUNDE\svc-sqlinsight$", Companies = [new AxCompany("dk01", "Danmark", false)] }; await repository.SaveCollectedAsync(id, DynamicsAxView.DatabasesKind, new AxDatabaseSnapshot { CapturedAt = now, Databases = [ax30, ax2012], AgentLogin = ax30.AgentLogin }); var job = AxSchemaAdapter.Ax2012.Interpret(new AxBatchJobRow(5637144576, null, "Lagerafslutning", "dk01", null, 1, now.UtcDateTime.AddHours(-2), null, null, null, null, null, 0), new AxBatchHistory(), "AX2012", null); await repository.SaveCollectedAsync(id, DynamicsAxView.BatchKind, new AxBatchSnapshot { CapturedAt = now, Thresholds = new(), Databases = [ new AxBatchDatabase { Database = "AX30", Version = AxVersion.Axapta30, NotMeasurable = AxSchemaAdapter.Ax30.BatchJobs(ax30).NotMeasurable }, new AxBatchDatabase { Database = "AX2012", Version = AxVersion.Ax2012R2, Jobs = [job], StatusCounts = new Dictionary { [1] = 1 }, NewRuns = [new AxBatchRun(job.Key, "H1", "Lagerafslutning", "dk01", now.UtcDateTime.AddDays(-1), now.UtcDateTime.AddDays(-1).AddMinutes(12), AxBatchStatus.Finished)], }, ], }); await repository.SaveCollectedAsync(id, DynamicsAxView.SequencesKind, new AxSequenceSnapshot { CapturedAt = now, Thresholds = new(), Databases = [ new AxSequenceDatabase { Database = "AX2012", Version = AxVersion.Ax2012R2, NumberSequences = [AxSchemaAdapter.Ax2012.Interpret(new AxNumberSequenceRow(2001, "Ordre", "dk01", "Salgsordrer", 1, 1_000_000, 990_000, "######", false, false, false, false, true, null, null, null, 0), [], now.UtcDateTime, 30)], RecIds = [AxSchemaAdapter.Interpret(new AxRecIdRow(77, null, 5_637_150_000, null, "CustTable"), false, [], now.UtcDateTime, 30)], }, ], }); await repository.SaveCollectedAsync(id, "blocking", new BlockingSnapshot { CapturedAt = now, Sessions = [ new SessionInfo { SessionId = 61, Status = "sleeping", DatabaseName = "AX2012", HostName = "AOS01", ProgramName = "Microsoft Dynamics AX", LoginName = "AXSERVICE", ContextInfo = Convert.ToHexString("anne 17"u8), SqlText = "UPDATE INVENTSUM SET …", }, new SessionInfo { SessionId = 62, BlockingSessionId = 61, DatabaseName = "AX2012", WaitTimeMs = 60_000, WaitType = "LCK_M_U" }, ], }); _factory = new WebApplicationFactory().WithWebHostBuilder(b => { b.UseEnvironment("Development"); b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(_database)); b.UseSetting("SQLInsight:Web:Authentication", "None"); }); } public async Task DisposeAsync() { await _factory.DisposeAsync(); await sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];"); } private async Task GetAsync(string path) { var response = await _factory.CreateClient().GetAsync(path); var text = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, $"{path}: {(int)response.StatusCode} {text}"); return text; } [Fact] public async Task The_dynamics_ax_pages_show_versions_the_exact_grant_jobs_sequences_recid_and_ax_users() { var databases = await GetAsync("/dynamics-ax/databaser"); var batch = await GetAsync("/dynamics-ax/batch"); var numbers = await GetAsync("/dynamics-ax/nummerserier"); var recid = await GetAsync("/dynamics-ax/recid"); var blocking = await GetAsync("/dynamics-ax/blokering"); Assert.Contains("Axapta 3.0 (eller 2.5)", databases, StringComparison.Ordinal); Assert.Contains(@"GRANT SELECT ON [bmssa].[BATCH] TO [KUNDE\svc-sqlinsight$];", WebUtility.HtmlDecode(databases), StringComparison.Ordinal); Assert.Contains("ikke verificeret mod kundedata", databases, StringComparison.Ordinal); Assert.Contains("AX2012_model", databases, StringComparison.Ordinal); Assert.Contains("Ikke målbar", batch, StringComparison.Ordinal); Assert.Contains("Lagerafslutning", batch, StringComparison.Ordinal); Assert.Contains("Salgsordrer", numbers, StringComparison.Ordinal); Assert.Contains("CustTable", recid, StringComparison.Ordinal); Assert.Contains("anne", blocking, StringComparison.Ordinal); // the web UI is for the people logged in: names are shown Assert.Contains("Dynamics AX", await GetAsync("/"), StringComparison.Ordinal); } [Fact] public async Task The_same_is_in_api_v1_with_logins_and_ax_users_redacted() { var databases = JsonDocument.Parse(await GetAsync("/api/v1/dynamics-ax/databases")).RootElement; var grant = JsonDocument.Parse(await GetAsync("/api/v1/dynamics-ax/grant-script?instance=" + Uri.EscapeDataString(Name) + "&database=AX30")).RootElement; var jobs = await GetAsync("/api/v1/dynamics-ax/batch-jobs"); var runs = JsonDocument.Parse(await GetAsync("/api/v1/dynamics-ax/batch-runs?days=7")).RootElement; var numbers = await GetAsync("/api/v1/dynamics-ax/number-sequences?instance=" + Uri.EscapeDataString(Name)); var recids = await GetAsync("/api/v1/dynamics-ax/recids"); var samples = JsonDocument.Parse(await GetAsync("/api/v1/dynamics-ax/sequence-samples?instance=" + Uri.EscapeDataString(Name) + "&kind=N")).RootElement; var blocking = await GetAsync("/api/v1/dynamics-ax/blocking"); Assert.Equal(["AX30", "AX2012"], databases.EnumerateArray().Select(d => d.GetProperty("database").GetString())); var ax30 = databases[0]; Assert.True(ax30.GetProperty("verified").GetBoolean()); Assert.Contains("GRANT SELECT ON [bmssa].[BATCH] TO [DOMÆNE\\svc-sqlinsight$];", ax30.GetProperty("grant").GetString(), StringComparison.Ordinal); Assert.DoesNotContain("KUNDE", databases.GetRawText(), StringComparison.Ordinal); Assert.Contains("SET @DatabaseName = N'AX30';", grant.GetProperty("script").GetString(), StringComparison.Ordinal); Assert.Contains("Lagerafslutning", jobs, StringComparison.Ordinal); Assert.Equal(1, runs.GetArrayLength()); Assert.Contains("\"formatTooShort\":true", numbers, StringComparison.Ordinal); Assert.Contains("\"bits\":64", recids, StringComparison.Ordinal); Assert.Equal("2001", Assert.Single(samples.EnumerateArray()).GetProperty("sequenceKey").GetString()); // AX 2012: keyed by RECID Assert.Contains("[udeladt]", blocking, StringComparison.Ordinal); Assert.Contains("context_info", blocking, StringComparison.Ordinal); Assert.DoesNotContain("anne", blocking, StringComparison.Ordinal); var client = _factory.CreateClient(); Assert.Equal(HttpStatusCode.NotFound, (await client.GetAsync("/api/v1/dynamics-ax/databases?instance=ukendt")).StatusCode); Assert.Equal(HttpStatusCode.BadRequest, (await client.GetAsync("/api/v1/dynamics-ax/grant-script")).StatusCode); Assert.Equal(HttpStatusCode.BadRequest, (await client.GetAsync("/api/v1/dynamics-ax/sequence-samples?instance=" + Uri.EscapeDataString(Name) + "&kind=X")).StatusCode); } }