Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,463 @@
|
||||
using HfSqlOleDbAudit;
|
||||
|
||||
namespace HfSqlOleDbAudit.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// Tests unitaires du mode <c>connection-test</c> (sans serveur HFSQL réel).
|
||||
/// Couvre : parsing des 3 sections, rejet de section inconnue, non-affichage des
|
||||
/// secrets, absence de chaîne de connexion complète, validateur d'identifiant,
|
||||
/// requête SELECT lecture seule, absence d'écriture de fichier, codes de sortie,
|
||||
/// et préservation du mode historique.
|
||||
/// </summary>
|
||||
public sealed class ConnectionTestTests
|
||||
{
|
||||
// ================================================================
|
||||
// 1-3. Parsing des sections
|
||||
// ================================================================
|
||||
|
||||
[Fact]
|
||||
public void LoadConfig_Hfsql_ParsesAllFields()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"Hfsql": {
|
||||
"Server": "SRV-OMIKA",
|
||||
"Port": 4900,
|
||||
"DatabaseName": "Omika",
|
||||
"Login": "compte_tech",
|
||||
"Password": "p4ss",
|
||||
"FilePasswords": { "ProtectionStd": "std", "ProtectionPlus": "plus" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
WithTempJson(json, path =>
|
||||
{
|
||||
Assert.True(ConnectionTest.TryLoadConfig(path, ConnectionTest.Section.Hfsql, out var config, out _));
|
||||
Assert.NotNull(config);
|
||||
Assert.Equal("SRV-OMIKA", config.Server);
|
||||
Assert.Equal(4900, config.Port);
|
||||
Assert.Equal("Omika", config.DatabaseName);
|
||||
Assert.Equal("compte_tech", config.Login);
|
||||
Assert.True(config.DatabasePasswordPresent);
|
||||
Assert.True(config.ProtectionStdPresent);
|
||||
Assert.True(config.ProtectionPlusPresent);
|
||||
|
||||
// Parité Production : la vue OLE DB porte les mêmes valeurs.
|
||||
Assert.Equal("SRV-OMIKA", config.ConnectionOptions.Server);
|
||||
Assert.Equal(4900, config.ConnectionOptions.Port);
|
||||
Assert.Equal("Omika", config.ConnectionOptions.Database);
|
||||
Assert.Equal("compte_tech", config.ConnectionOptions.User);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadConfig_LicenseConnection_ParsesAllFields()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"LicenseConnection": {
|
||||
"Server": "SRV-OMIKA",
|
||||
"Port": 4900,
|
||||
"DatabaseName": "Licence",
|
||||
"Login": "licence_user",
|
||||
"Password": "licpass",
|
||||
"FilePasswords": { "ProtectionStd": "std", "ProtectionPlus": "plus" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
WithTempJson(json, path =>
|
||||
{
|
||||
Assert.True(ConnectionTest.TryLoadConfig(path, ConnectionTest.Section.LicenseConnection, out var config, out _));
|
||||
Assert.NotNull(config);
|
||||
Assert.Equal("SRV-OMIKA", config.Server);
|
||||
Assert.Equal(4900, config.Port);
|
||||
Assert.Equal("Licence", config.DatabaseName);
|
||||
Assert.Equal("licence_user", config.Login);
|
||||
Assert.True(config.DatabasePasswordPresent);
|
||||
Assert.True(config.ProtectionStdPresent);
|
||||
Assert.True(config.ProtectionPlusPresent);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadConfig_ServiceConnection_ParsesAllFields_WithOptionalPassword()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"ServiceConnection": {
|
||||
"Server": "SRV-OMIKA",
|
||||
"Port": 4900,
|
||||
"DatabaseName": "Service",
|
||||
"Login": "service_user",
|
||||
"Password": "",
|
||||
"FilePasswords": { "ProtectionStd": "", "ProtectionPlus": "" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
WithTempJson(json, path =>
|
||||
{
|
||||
Assert.True(ConnectionTest.TryLoadConfig(path, ConnectionTest.Section.ServiceConnection, out var config, out _));
|
||||
Assert.NotNull(config);
|
||||
Assert.Equal("SRV-OMIKA", config.Server);
|
||||
Assert.Equal(4900, config.Port);
|
||||
Assert.Equal("Service", config.DatabaseName);
|
||||
Assert.Equal("service_user", config.Login);
|
||||
// Mot de passe Service optionnel : absent ici.
|
||||
Assert.False(config.DatabasePasswordPresent);
|
||||
Assert.False(config.ProtectionStdPresent);
|
||||
Assert.False(config.ProtectionPlusPresent);
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 4. Section inconnue rejetée
|
||||
// ================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("Batik")]
|
||||
[InlineData("SyncConnection")]
|
||||
[InlineData("")]
|
||||
[InlineData("hfsqlx")]
|
||||
public void TryParseSection_UnknownSection_ReturnsFalse(string section)
|
||||
{
|
||||
Assert.False(ConnectionTest.TryParseSection(section, out _));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Hfsql")]
|
||||
[InlineData("hfsql")]
|
||||
[InlineData("LicenseConnection")]
|
||||
[InlineData("ServiceConnection")]
|
||||
public void TryParseSection_SupportedSection_ReturnsTrue(string section)
|
||||
{
|
||||
Assert.True(ConnectionTest.TryParseSection(section, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Run_UnknownSection_ReturnsExit2()
|
||||
{
|
||||
using var sw = new StringWriter();
|
||||
int code = ConnectionTest.Run(new[] { "connection-test", "C:\\whatever.json", "Bogus" }, sw);
|
||||
Assert.Equal(2, code);
|
||||
Assert.Contains("UNSUPPORTED_SECTION", sw.ToString());
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 5-8. Aucun secret / aucune chaîne de connexion affichés
|
||||
// ================================================================
|
||||
|
||||
[Fact]
|
||||
public void BuildSummaryLines_NeverPrintsSecrets()
|
||||
{
|
||||
const string password = "SUP3R-S3CR3T-DBP4SS";
|
||||
const string std = "STD-SECRET";
|
||||
const string plus = "PLUS-SECRET";
|
||||
|
||||
string json = $$"""
|
||||
{
|
||||
"Hfsql": {
|
||||
"Server": "SRV-OMIKA", "Port": 4900, "DatabaseName": "Omika", "Login": "user",
|
||||
"Password": "{{password}}",
|
||||
"FilePasswords": { "ProtectionStd": "{{std}}", "ProtectionPlus": "{{plus}}" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
WithTempJson(json, path =>
|
||||
{
|
||||
Assert.True(ConnectionTest.TryLoadConfig(path, ConnectionTest.Section.Hfsql, out var config, out _));
|
||||
string all = string.Join('\n', ConnectionTest.BuildSummaryLines(config!));
|
||||
|
||||
Assert.DoesNotContain(password, all);
|
||||
Assert.DoesNotContain(std, all);
|
||||
Assert.DoesNotContain(plus, all);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildSummaryLines_NeverPrintsFullConnectionString()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"Hfsql": {
|
||||
"Server": "SRV-OMIKA", "Port": 4900, "DatabaseName": "Omika", "Login": "user",
|
||||
"Password": "secret-db",
|
||||
"FilePasswords": { "ProtectionStd": "secret-std", "ProtectionPlus": "secret-plus" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
WithTempJson(json, path =>
|
||||
{
|
||||
Assert.True(ConnectionTest.TryLoadConfig(path, ConnectionTest.Section.Hfsql, out var config, out _));
|
||||
var lines = ConnectionTest.BuildSummaryLines(config!);
|
||||
|
||||
foreach (string line in lines)
|
||||
{
|
||||
Assert.DoesNotContain(";", line); // séparateur de chaîne de connexion
|
||||
Assert.DoesNotContain("Password=", line);
|
||||
Assert.DoesNotContain("Data Source", line);
|
||||
Assert.DoesNotContain("Initial Catalog", line);
|
||||
Assert.DoesNotContain("Extended Properties", line);
|
||||
Assert.DoesNotContain("User ID", line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildSummaryLines_ShowsPresenceFlagsOnly()
|
||||
{
|
||||
const string json = """
|
||||
{
|
||||
"Hfsql": {
|
||||
"Server": "SRV-OMIKA", "Port": 4900, "DatabaseName": "Omika", "Login": "user",
|
||||
"Password": "x",
|
||||
"FilePasswords": { "ProtectionStd": "y", "ProtectionPlus": "z" }
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
WithTempJson(json, path =>
|
||||
{
|
||||
Assert.True(ConnectionTest.TryLoadConfig(path, ConnectionTest.Section.Hfsql, out var config, out _));
|
||||
var lines = ConnectionTest.BuildSummaryLines(config!);
|
||||
|
||||
Assert.Contains("DatabasePasswordPresent=YES", lines);
|
||||
Assert.Contains("ProtectionStdPresent=YES", lines);
|
||||
Assert.Contains("ProtectionPlusPresent=YES", lines);
|
||||
Assert.Contains("Provider=PCSOFT.HFSQL", lines);
|
||||
Assert.Contains("Section=Hfsql", lines);
|
||||
});
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 9. Identifiant de table
|
||||
// ================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData("ParametreMetier")]
|
||||
[InlineData("ParametreMetierEntete")]
|
||||
[InlineData("ParametreSysteme")]
|
||||
[InlineData("TOSRV")]
|
||||
[InlineData("table_1")]
|
||||
[InlineData("A1_b2")]
|
||||
public void IsValidTableIdentifier_Valid(string name)
|
||||
{
|
||||
Assert.True(ConnectionTest.IsValidTableIdentifier(name));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("")]
|
||||
[InlineData("ParametreMetier; DROP TABLE")]
|
||||
[InlineData("foo bar")]
|
||||
[InlineData("foo-bar")]
|
||||
[InlineData("foo.bar")]
|
||||
[InlineData("foo/bar")]
|
||||
[InlineData("foo\\bar")]
|
||||
public void IsValidTableIdentifier_Invalid(string name)
|
||||
{
|
||||
Assert.False(ConnectionTest.IsValidTableIdentifier(name));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Run_InvalidTable_ReturnsExit2()
|
||||
{
|
||||
using var sw = new StringWriter();
|
||||
int code = ConnectionTest.Run(new[] { "connection-test", "C:\\whatever.json", "Hfsql", "Bad;Table" }, sw);
|
||||
Assert.Equal(2, code);
|
||||
Assert.Contains("INVALID_TABLE_IDENTIFIER", sw.ToString());
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 10. Aucune commande SQL d'écriture
|
||||
// ================================================================
|
||||
|
||||
[Fact]
|
||||
public void BuildSelectSql_IsReadOnlySelect()
|
||||
{
|
||||
Assert.Equal("SELECT TOP 1 * FROM ParametreMetier", ConnectionTest.BuildSelectSql("ParametreMetier"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ConnectionTestSource_HasNoWriteSql()
|
||||
{
|
||||
string source = File.ReadAllText(RepoFile("tools", "HfSqlOleDbAudit", "ConnectionTest.cs"));
|
||||
|
||||
string[] forbidden =
|
||||
{
|
||||
"ExecuteNonQuery",
|
||||
"INSERT",
|
||||
"UPDATE",
|
||||
"DELETE",
|
||||
"MERGE",
|
||||
"TRUNCATE",
|
||||
"CREATE TABLE",
|
||||
"DROP TABLE",
|
||||
"ALTER TABLE",
|
||||
"CREATE DATABASE",
|
||||
"DROP DATABASE"
|
||||
};
|
||||
|
||||
foreach (string token in forbidden)
|
||||
{
|
||||
Assert.DoesNotContain(token, source, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 11. Aucune écriture de fichier dans connection-test
|
||||
// ================================================================
|
||||
|
||||
[Fact]
|
||||
public void ConnectionTestSource_HasNoFileWrite()
|
||||
{
|
||||
string source = File.ReadAllText(RepoFile("tools", "HfSqlOleDbAudit", "ConnectionTest.cs"));
|
||||
|
||||
string[] forbidden =
|
||||
{
|
||||
"File.Write",
|
||||
"File.Append",
|
||||
"File.Copy",
|
||||
"File.Move",
|
||||
"File.Create",
|
||||
"File.Delete",
|
||||
"File.OpenWrite",
|
||||
"WriteAllText",
|
||||
"WriteAllLines",
|
||||
"WriteAllBytes",
|
||||
"AppendAllText",
|
||||
"AppendAllLines",
|
||||
"Directory.CreateDirectory",
|
||||
"FileStream",
|
||||
"StreamWriter"
|
||||
};
|
||||
|
||||
foreach (string token in forbidden)
|
||||
{
|
||||
Assert.DoesNotContain(token, source, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 12-13. Codes de sortie
|
||||
// ================================================================
|
||||
|
||||
[Fact]
|
||||
public void Run_MissingConfigFile_ReturnsExit2()
|
||||
{
|
||||
using var sw = new StringWriter();
|
||||
string missing = Path.Combine(Path.GetTempPath(), "hfsql_audit_missing_" + Guid.NewGuid().ToString("N") + ".json");
|
||||
int code = ConnectionTest.Run(new[] { "connection-test", missing, "Hfsql" }, sw);
|
||||
Assert.Equal(2, code);
|
||||
Assert.Contains("CONFIG_FILE_NOT_FOUND", sw.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Run_MissingSection_ReturnsExit2()
|
||||
{
|
||||
const string json = """{ "Unrelated": { "x": 1 } }""";
|
||||
|
||||
WithTempJson(json, path =>
|
||||
{
|
||||
using var sw = new StringWriter();
|
||||
int code = ConnectionTest.Run(new[] { "connection-test", path, "Hfsql" }, sw);
|
||||
Assert.Equal(2, code);
|
||||
Assert.Contains("SECTION_NOT_FOUND", sw.ToString());
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Run_MissingArguments_ReturnsExit2()
|
||||
{
|
||||
using var sw = new StringWriter();
|
||||
Assert.Equal(2, ConnectionTest.Run(new[] { "connection-test" }, sw));
|
||||
Assert.Equal(2, ConnectionTest.Run(new[] { "connection-test", "C:\\x.json" }, sw));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Run_InvalidConfigPath_DoesNotEchoExceptionMessage()
|
||||
{
|
||||
using var sw = new StringWriter();
|
||||
int code = ConnectionTest.Run(new[] { "connection-test", "\u0000bad\0path.json", "Hfsql" }, sw);
|
||||
Assert.Equal(2, code);
|
||||
// Aucun message d'exception brut (peut contenir des fragments sensibles).
|
||||
Assert.DoesNotContain("at System.", sw.ToString());
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Sanitization / erreurs
|
||||
// ================================================================
|
||||
|
||||
[Fact]
|
||||
public void ExtractHfSqlErrorCode_ReadsNumericCodeOnly()
|
||||
{
|
||||
Assert.Equal("70001", ConnectionTest.ExtractHfSqlErrorCode(new Exception("Échec : Code erreur : 70001 dans l'accès")));
|
||||
Assert.Equal("42", ConnectionTest.ExtractHfSqlErrorCode(new Exception("Error code: 42")));
|
||||
Assert.Null(ConnectionTest.ExtractHfSqlErrorCode(new Exception("aucun code ici")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SanitizeErrorType_ReturnsTypeNameOnly()
|
||||
{
|
||||
Assert.Equal("InvalidOperationException", ConnectionTest.SanitizeErrorType(new InvalidOperationException("secret-message")));
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 14. Mode historique inchangé
|
||||
// ================================================================
|
||||
|
||||
[Fact]
|
||||
public void HistoricalAuditMode_IsUnchanged()
|
||||
{
|
||||
string source = File.ReadAllText(RepoFile("tools", "HfSqlOleDbAudit", "Program.cs"));
|
||||
|
||||
// Marqueurs du mode historique toujours présents.
|
||||
Assert.Contains("=== SUMMARY ===", source);
|
||||
Assert.Contains("CSV: CREATED", source);
|
||||
Assert.Contains("MD: CREATED", source);
|
||||
Assert.Contains("LoadTables", source);
|
||||
Assert.Contains("Usage: HfSqlOleDbAudit <configPath> <Batik|Omika>", source);
|
||||
|
||||
// Le dispatch connection-test est le seul ajout en tête de Main.
|
||||
Assert.Contains(ConnectionTest.CommandName, source);
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Helpers
|
||||
// ================================================================
|
||||
|
||||
private static void WithTempJson(string json, Action<string> action)
|
||||
{
|
||||
string path = Path.Combine(Path.GetTempPath(), "hfsql_audit_ct_" + Guid.NewGuid().ToString("N") + ".json");
|
||||
try
|
||||
{
|
||||
File.WriteAllText(path, json);
|
||||
action(path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
private static string RepoFile(params string[] parts)
|
||||
{
|
||||
string relative = Path.Combine(parts);
|
||||
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (dir is not null)
|
||||
{
|
||||
string candidate = Path.Combine(dir.FullName, relative);
|
||||
if (File.Exists(candidate))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
|
||||
dir = dir.Parent;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Fichier introuvable depuis la racine du dépôt : " + relative);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Tests du mode "connection-test" de HfSqlOleDbAudit SANS serveur HFSQL réel :
|
||||
parsing de configuration, rejet de section inconnue, non-affichage des secrets,
|
||||
validateur d'identifiant de table, requête SELECT lecture seule, absence
|
||||
d'écriture de fichier, codes de sortie et préservation du mode historique.
|
||||
-->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\HfSqlOleDbAudit.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user