464 lines
16 KiB
C#
464 lines
16 KiB
C#
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);
|
|
}
|
|
}
|