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