Codebasis als Dateien ins Arbeitsrepo statt als Gitlink

QuellCode/CentronERP war nur als Gitlink (Submodul-Referenz auf 79c1142)
getrackt, ohne .gitmodules und ohne erreichbares Remote. Der
Untersuchungsgegenstand der Versuchsreihe war damit nicht reproduzierbar
gesichert: Ein Klon haette ein leeres Verzeichnis erhalten, und die Belege
der 3.287 Anforderungen waeren nicht ueberpruefbar gewesen.

Umstellung:
- Historie nach c:\DEV\CentronERP_git_snapshot_79c1142 ausgelagert
  (vollstaendig lesbar, enthaelt 79c1142 und Vorgaenger 89ccfd6)
- Gitlink aus dem Index entfernt
- Dateiinhalt aufgenommen: 24.557 Dateien, rund 333 MB

Die verschachtelte .gitignore der Codebasis gilt weiter, Build-Artefakte
bleiben ausgeschlossen. Details in Versuche/Versuch_01/_Codebasis-Nachweis.md
This commit is contained in:
Christoph Schwörer
2026-08-26 07:43:51 +02:00
parent 18edae75b6
commit f045b99a25
24664 changed files with 5846716 additions and 1 deletions
@@ -0,0 +1,231 @@
using Centron.BusinessLogic.Administration.AccessTokens;
using Centron.BusinessLogic.WebServices.Administration.AccessTokens;
using JetBrains.Annotations;
namespace Centron.Tests.BL.Administration.AccessTokens;
/// <summary>
/// Unit tests for AccessTokenWebServiceBL and AccessTokenBL.
/// These tests focus on logic that can be tested without full database setup.
/// For integration tests with rights checks, see the EndToEnd tests.
/// </summary>
[TestSubject(typeof(AccessTokenWebServiceBL))]
public class AccessTokenWebServiceBLTest
{
#region Token Hash Tests
[Fact]
public void AccessTokenBL_HashToken_ReturnsDeterministicHash()
{
// Arrange
var plainToken = "test-token-value";
// Act
var hash1 = AccessTokenBL.HashToken(plainToken);
var hash2 = AccessTokenBL.HashToken(plainToken);
// Assert
Assert.Equal(hash1, hash2);
}
[Fact]
public void AccessTokenBL_HashToken_ReturnsDifferentHashForDifferentTokens()
{
// Arrange
var token1 = "token-one";
var token2 = "token-two";
// Act
var hash1 = AccessTokenBL.HashToken(token1);
var hash2 = AccessTokenBL.HashToken(token2);
// Assert
Assert.NotEqual(hash1, hash2);
}
[Fact]
public void AccessTokenBL_HashToken_ReturnsNonEmptyString()
{
// Arrange
var plainToken = "test-token";
// Act
var hash = AccessTokenBL.HashToken(plainToken);
// Assert
Assert.NotNull(hash);
Assert.NotEmpty(hash);
// SHA-256 produces 64 hex characters
Assert.Equal(64, hash.Length);
}
[Fact]
public void AccessTokenBL_HashToken_ReturnsUppercaseHex()
{
// Arrange
var plainToken = "test-token";
// Act
var hash = AccessTokenBL.HashToken(plainToken);
// Assert
Assert.Matches("^[A-F0-9]+$", hash);
}
[Fact]
public void AccessTokenBL_HashToken_ConsistentAcrossRuns()
{
// This test verifies that the hash is deterministic and consistent
// Arrange
var plainToken = "consistent-test-token";
// Act
var actualHash = AccessTokenBL.HashToken(plainToken);
// Verify it's consistent across multiple calls
var hash1 = AccessTokenBL.HashToken(plainToken);
var hash2 = AccessTokenBL.HashToken(plainToken);
// Assert
Assert.Equal(hash1, hash2);
Assert.Equal(64, actualHash.Length); // SHA-256 = 256 bits = 32 bytes = 64 hex chars
}
[Theory]
[InlineData("")]
[InlineData("a")]
[InlineData("short")]
[InlineData("this-is-a-longer-token-value-for-testing")]
[InlineData("special!@#$%^&*()characters")]
[InlineData("unicode-äöü-日本語-🔒")]
public void AccessTokenBL_HashToken_HandlesVariousInputs(string plainToken)
{
// Act
var hash = AccessTokenBL.HashToken(plainToken);
// Assert
Assert.NotNull(hash);
Assert.Equal(64, hash.Length);
Assert.Matches("^[A-F0-9]+$", hash);
}
#endregion
#region Token Generation Tests
[Fact]
public void GenerateTestToken_ReturnsNonEmptyString()
{
// Act
var token = GenerateTestToken();
// Assert
Assert.NotNull(token);
Assert.NotEmpty(token);
}
[Fact]
public void GenerateTestToken_ReturnsUniqueTokens()
{
// Act
var token1 = GenerateTestToken();
var token2 = GenerateTestToken();
// Assert
Assert.NotEqual(token1, token2);
}
[Fact]
public void GenerateTestToken_ReturnsUrlSafeString()
{
// Act
var token = GenerateTestToken();
// Assert - URL-safe Base64 uses - and _ instead of + and /
Assert.DoesNotContain("+", token);
Assert.DoesNotContain("/", token);
Assert.DoesNotContain("=", token);
}
[Fact]
public void GenerateTestToken_HasSufficientLength()
{
// Act
var token = GenerateTestToken();
// Assert - 32 bytes encoded in Base64 should be ~43 characters
Assert.True(token.Length >= 32, $"Token length {token.Length} is too short for security");
}
#endregion
#region Hash Verification Tests
[Fact]
public void HashToken_VerificationWorkflow_MatchingToken_Succeeds()
{
// Arrange - Simulate the token creation and verification workflow
var plainToken = GenerateTestToken();
var storedHash = AccessTokenBL.HashToken(plainToken);
// Act - Later, when verifying
var providedHash = AccessTokenBL.HashToken(plainToken);
// Assert
Assert.Equal(storedHash, providedHash);
}
[Fact]
public void HashToken_VerificationWorkflow_WrongToken_Fails()
{
// Arrange - Simulate the token creation and verification workflow
var originalToken = GenerateTestToken();
var storedHash = AccessTokenBL.HashToken(originalToken);
var wrongToken = GenerateTestToken();
// Act - Attacker tries with wrong token
var providedHash = AccessTokenBL.HashToken(wrongToken);
// Assert
Assert.NotEqual(storedHash, providedHash);
}
[Fact]
public void HashToken_SimilarTokens_ProduceDifferentHashes()
{
// This tests that even minor changes produce completely different hashes
// Arrange
var token1 = "test-token-123";
var token2 = "test-token-124"; // One character different
// Act
var hash1 = AccessTokenBL.HashToken(token1);
var hash2 = AccessTokenBL.HashToken(token2);
// Assert - Hashes should be completely different (avalanche effect)
Assert.NotEqual(hash1, hash2);
// Count differing characters - should be significant
var differences = hash1.Zip(hash2, (a, b) => a != b).Count(d => d);
Assert.True(differences > 20, $"Hashes are too similar: only {differences} characters differ");
}
#endregion
#region Helper Methods
/// <summary>
/// Generates a test token similar to the internal AccessTokenBL method.
/// </summary>
private static string GenerateTestToken()
{
var bytes = new byte[32];
using (var rng = System.Security.Cryptography.RandomNumberGenerator.Create())
{
rng.GetBytes(bytes);
}
return Convert.ToBase64String(bytes).Replace("+", "-").Replace("/", "_").TrimEnd('=');
}
#endregion
}
@@ -0,0 +1,391 @@
using System.Reflection;
using Centron.Tests.BL.SetupUtils;
using Centron.BusinessLogic.Administration.Logins.Auth;
using Centron.BusinessLogic.Administration.WebServiceConfiguration;
using Centron.DAO;
using Centron.DAO.Mappings;
using Centron.DAO.Mappings.Administration;
using Centron.DAO.Mappings.Administration.Applications;
using Centron.DAO.Mappings.Administration.Company;
using Centron.DAO.Mappings.Administration.Employees;
using Centron.DAO.Mappings.Administration.Logins;
using Centron.DAO.Mappings.Administration.Settings;
using Centron.DAO.Mappings.CustomerArea.Support;
using Centron.DAO.Mappings.States;
using Centron.DAO.Mappings.TemporaryEntities;
using Centron.DAO.TemporaryEntities;
using Centron.Interfaces.Administration.Employees;
using Centron.Interfaces.Administration.Logins;
using Centron.Interfaces.Administration.Settings;
using Centron.Interfaces.BL;
using Centron.Tests.BL.Fixtures;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.BL.Administration.Logins.Auth;
[TestSubject(typeof(AuthenticatorFactory))]
public class AuthenticatorFactoryTest : IClassFixture<LicenseManagerFixture>
{
private static Type Ba => typeof(BasicAuthenticator);
private static Type Ad => typeof(ActiveDirectoryAuthenticator);
private static Type Oi => typeof(OpenIdConnectAuthenticator);
public static IEnumerable<object?[]> DataAuthenticatorSystemNoneTestSuccess()
{
var data = new List<object?[]>();
// [expectedAuthenticatorType, authObject, method, ActiveDirectoryEnabled, JwtEnabled]
// Basic
data.Add([Ba, new BasicAuthObject(), false, false]);
data.Add([Ba, new BasicAuthObject(), false, true]);
// Active Directory
data.Add([Ad, new BasicAuthObject(), true, false]);
data.Add([Ad, new BasicAuthObject(), true, true]);
// OpenId Connect
data.Add([Oi, new OpenIdConnectAuthObject(), false, true]);
data.Add([Oi, new OpenIdConnectAuthObject(), true, true]);
return data;
}
[Theory]
[MemberData(nameof(DataAuthenticatorSystemNoneTestSuccess))]
internal void GetAuthenticatorSystemNoneSuccess(
Type expectedAuthenticatorType, AuthObject authObject, bool activeDirectoryEnabled, bool jwtEnabled)
{
// Arrange
const SystemAuthenticationMethod systemAuthMethod = SystemAuthenticationMethod.None;
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var session = CreateSession();
var daoSession = new DAOSession(session);
SetupAppSettings(session, systemAuthMethod, activeDirectoryEnabled, jwtEnabled);
var authenticatorFactory = new AuthenticatorFactory(daoSession, licenseManager);
// Act
var result = authenticatorFactory.GetAuthenticator(authObject);
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.IsType(expectedAuthenticatorType, result.Data);
}
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
internal void GetAuthenticatorSystemBasicSuccess(
bool activeDirectoryEnabled, bool jwtEnabled)
{
// Arrange
const SystemAuthenticationMethod systemAuthMethod = SystemAuthenticationMethod.Basic;
var session = CreateSession();
var daoSession = new DAOSession(session);
SetupAppSettings(session, systemAuthMethod, activeDirectoryEnabled, jwtEnabled);
var authenticatorFactory = new AuthenticatorFactory(daoSession);
var authObject = new BasicAuthObject();
// Act
var result = authenticatorFactory.GetAuthenticator(authObject);
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.IsType<BasicAuthenticator>(result.Data);
}
[Theory]
[InlineData(true, false)]
[InlineData(true, true)]
internal void GetAuthenticatorSystemActiveDirectorySuccess(bool activeDirectoryEnabled, bool jwtEnabled)
{
// Arrange
const SystemAuthenticationMethod systemAuthMethod = SystemAuthenticationMethod.ActiveDirectory;
var session = CreateSession();
var daoSession = new DAOSession(session);
SetupAppSettings(session, systemAuthMethod, activeDirectoryEnabled, jwtEnabled);
var authenticatorFactory = new AuthenticatorFactory(daoSession);
var authObject = new BasicAuthObject();
// Act
var result = authenticatorFactory.GetAuthenticator(authObject);
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.IsType<ActiveDirectoryAuthenticator>(result.Data);
}
[Theory]
[InlineData(false, true)]
[InlineData(true, true)]
internal void GetAuthenticatorSystemOpenIdConnectSuccess(bool activeDirectoryEnabled, bool jwtEnabled)
{
// Arrange
const SystemAuthenticationMethod systemAuthMethod = SystemAuthenticationMethod.OpenIdConnect;
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var session = CreateSession();
var daoSession = new DAOSession(session);
SetupAppSettings(session, systemAuthMethod, activeDirectoryEnabled, jwtEnabled);
var authenticatorFactory = new AuthenticatorFactory(daoSession, licenseManager);
var authObject = new OpenIdConnectAuthObject();
// Act
var result = authenticatorFactory.GetAuthenticator(authObject);
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.IsType<OpenIdConnectAuthenticator>(result.Data);
}
[Theory]
[InlineData(SystemAuthenticationMethod.None, false, false)]
[InlineData(SystemAuthenticationMethod.None, false, true)]
[InlineData(SystemAuthenticationMethod.None, true, false)]
[InlineData(SystemAuthenticationMethod.None, true, true)]
[InlineData(SystemAuthenticationMethod.Basic, false, false)]
[InlineData(SystemAuthenticationMethod.Basic, false, true)]
[InlineData(SystemAuthenticationMethod.Basic, true, false)]
[InlineData(SystemAuthenticationMethod.Basic, true, true)]
[InlineData(SystemAuthenticationMethod.ActiveDirectory, false, false)]
[InlineData(SystemAuthenticationMethod.ActiveDirectory, false, true)]
[InlineData(SystemAuthenticationMethod.ActiveDirectory, true, false)]
[InlineData(SystemAuthenticationMethod.ActiveDirectory, true, true)]
[InlineData(SystemAuthenticationMethod.OpenIdConnect, false, false)]
[InlineData(SystemAuthenticationMethod.OpenIdConnect, false, true)]
[InlineData(SystemAuthenticationMethod.OpenIdConnect, true, false)]
[InlineData(SystemAuthenticationMethod.OpenIdConnect, true, true)]
internal void GetAuthenticatorWebAccountSuccess(
SystemAuthenticationMethod method, bool activeDirectoryEnabled, bool jwtEnabled)
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
SetupAppSettings(session, method, activeDirectoryEnabled, jwtEnabled);
var authenticatorFactory = new AuthenticatorFactory(daoSession);
// This always prompts a WebAccountAuthenticator
var webAccountAuthObject = new WebAccountAuthObject();
// Act
var result = authenticatorFactory.GetAuthenticator(webAccountAuthObject);
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.IsType<WebAccountAuthenticator>(result.Data);
}
public static IEnumerable<object?[]> DataAuthenticatorFallbackTestSuccess()
{
var data = new List<object?[]>();
// [expectedMainAuthenticatorType, method, ActiveDirectoryEnabled, JwtEnabled]
data.Add([Ba, SystemAuthenticationMethod.None, false, false]);
data.Add([Ad, SystemAuthenticationMethod.None, true, false]);
data.Add([Ba, SystemAuthenticationMethod.Basic, false, false]);
data.Add([Ba, SystemAuthenticationMethod.Basic, true, false]);
data.Add([Ad, SystemAuthenticationMethod.ActiveDirectory, true, false]);
data.Add([null, SystemAuthenticationMethod.ActiveDirectory, false, false]);
data.Add([null, SystemAuthenticationMethod.OpenIdConnect, false, true]);
data.Add([null, SystemAuthenticationMethod.OpenIdConnect, false, false]);
return data;
}
[Theory]
[MemberData(nameof(DataAuthenticatorFallbackTestSuccess))]
public void GetAuthenticatorFallbackTestSuccess(
Type? expectedMainAuthenticatorType, SystemAuthenticationMethod systemAuthenticationMethod,
bool activeDirectoryEnabled, bool jwtEnabled)
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
SetupAppSettings(session, systemAuthenticationMethod, activeDirectoryEnabled, jwtEnabled);
var authenticatorFactory = new AuthenticatorFactory(daoSession);
const string username = "username";
var appUser = DbSetupUtils.SaveAppUser(session, username, "e@mail.com", 1);
appUser.AuthentificationKind = AuthentificationKind.CentronLogin;
session.Save(appUser);
session.Flush();
var authObject = new BasicAuthObject
{
UserName = username,
};
// Act
var result = authenticatorFactory.GetAuthenticator(authObject);
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.IsType<FallbackAuthenticator>(result.Data);
// assert that the main authenticator is of the expected type
var fallbackAuthenticator = (FallbackAuthenticator)result.Data;
var mainAuthenticator = fallbackAuthenticator
.GetType()
.GetField("<mainAuthenticator>P", BindingFlags.NonPublic | BindingFlags.Instance)?
.GetValue(fallbackAuthenticator);
if (expectedMainAuthenticatorType is null)
{
Assert.Null(mainAuthenticator);
return;
}
Assert.NotNull(mainAuthenticator);
Assert.IsType(expectedMainAuthenticatorType, mainAuthenticator);
}
[Theory]
#pragma warning disable CS0612 // Type or member is obsolete
[InlineData(AuthentificationKind.WindowsAuth)]
[InlineData(AuthentificationKind.OpenIdConnectAuth)]
#pragma warning restore CS0612 // Type or member is obsolete
public void GetAuthenticatorFallbackTestError(AuthentificationKind authKind)
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
SetupAppSettings(session, SystemAuthenticationMethod.None, true, true);
var authenticatorFactory = new AuthenticatorFactory(daoSession);
const string username = "username";
var appUser = DbSetupUtils.SaveAppUser(session, username, "e@mail.com", 1);
appUser.AuthentificationKind = authKind;
session.Save(appUser);
session.Flush();
var authObject = new BasicAuthObject
{
UserName = username,
};
// Act
var result = authenticatorFactory.GetAuthenticator(authObject);
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.IsType<FallbackAuthenticator>(result.Data);
// assert that the main authenticator is of the expected type
var fallbackAuthenticator = (FallbackAuthenticator)result.Data;
var secondaryAuthenticator = fallbackAuthenticator
.GetType()
.GetField("<secondaryAuthenticator>P", BindingFlags.NonPublic | BindingFlags.Instance)?
.GetValue(fallbackAuthenticator);
Assert.NotNull(secondaryAuthenticator);
Assert.IsType<FailingAuthenticator>(secondaryAuthenticator);
}
public static IEnumerable<object?[]> DataAuthenticatorTestError()
{
var data = new List<object?[]>();
// [authObject, method, ActiveDirectoryEnabled, JwtEnabled]
// Basic
// - wrong auth object
data.Add([new OpenIdConnectAuthObject(), SystemAuthenticationMethod.Basic, false, false]);
data.Add([new OpenIdConnectAuthObject(), SystemAuthenticationMethod.Basic, false, true]);
// Active Directory
// - wrong auth object
data.Add([new OpenIdConnectAuthObject(), SystemAuthenticationMethod.ActiveDirectory, false, false]);
data.Add([new OpenIdConnectAuthObject(), SystemAuthenticationMethod.ActiveDirectory, false, true]);
data.Add([new OpenIdConnectAuthObject(), SystemAuthenticationMethod.ActiveDirectory, true, false]);
data.Add([new OpenIdConnectAuthObject(), SystemAuthenticationMethod.ActiveDirectory, true, true]);
// - Active Directory not enabled
data.Add([new BasicAuthObject(), SystemAuthenticationMethod.ActiveDirectory, false, false]);
data.Add([new BasicAuthObject(), SystemAuthenticationMethod.ActiveDirectory, false, true]);
// OpenId Connect
// - wrong auth object
data.Add([new BasicAuthObject(), SystemAuthenticationMethod.OpenIdConnect, false, false]);
data.Add([new BasicAuthObject(), SystemAuthenticationMethod.OpenIdConnect, false, true]);
data.Add([new BasicAuthObject(), SystemAuthenticationMethod.OpenIdConnect, true, false]);
data.Add([new BasicAuthObject(), SystemAuthenticationMethod.OpenIdConnect, true, true]);
// - OIDC not enabled
data.Add([new OpenIdConnectAuthObject(), SystemAuthenticationMethod.OpenIdConnect, false, false]);
return data;
}
[Theory]
[MemberData(nameof(DataAuthenticatorTestError))]
public void GetAuthenticatorError(
AuthObject authObject, SystemAuthenticationMethod method, bool activeDirectoryEnabled, bool jwtEnabled)
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
SetupAppSettings(session, method, activeDirectoryEnabled, jwtEnabled);
var authenticatorFactory = new AuthenticatorFactory(daoSession);
// Act
var result = authenticatorFactory.GetAuthenticator(authObject);
// Assert
Assert.Equal(ResultStatus.Error, result.Status);
}
private static ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<AppUserMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeCompactMaps>())
.Mappings(m => m.FluentMappings.Add<SupportLevelMaps>())
.Mappings(m => m.FluentMappings.Add<MandatorMaps>())
.Mappings(m => m.FluentMappings.Add<CountryMaps>())
.Mappings(m => m.FluentMappings.Add<FederalStateMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserGroupMaps>())
.Mappings(m => m.FluentMappings.Add<AppRightMaps>())
.Mappings(m => m.FluentMappings.Add<ApplicationVersionMaps>())
.Mappings(m => m.FluentMappings.Add<TicketMaps>())
.Mappings(m => m.FluentMappings.Add<AppSettingMaps>())
.Mappings(m => m.FluentMappings.Add<ApplicationSettingMaps>());
var sessionFactory = fluentConfiguration.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection,
Console.Out);
return session;
}
private static void SetupAppSettings(
ISession session, SystemAuthenticationMethod method = SystemAuthenticationMethod.None,
bool activeDirectoryEnabled = false, bool jwtEnabled = false)
{
// WebServiceConfigHelper
WebServiceConfigHelper.Current.ActiveDirectoryAuthEnabled = activeDirectoryEnabled;
// System authentication setting
DbSetupUtils.SaveApplicationSetting(
session,
ApplicationSettingID.SystemAuthenticationMethod,
valueInt: (int)method);
if (jwtEnabled)
{
DbSetupUtils.SaveApplicationSetting(
session,
ApplicationSettingID.JwtAuthority,
valueText: "https://localhost/some/test/uri");
DbSetupUtils.SaveApplicationSetting(
session,
ApplicationSettingID.JwtAudience,
valueText: "audience");
}
session.Flush();
}
}
@@ -0,0 +1,247 @@
using Centron.BusinessLogic.Administration.Licensing;
using Centron.Tests.BL.SetupUtils;
using Centron.BusinessLogic.Administration.Logins;
using Centron.BusinessLogic.Administration.Logins.Auth;
using Centron.DAO;
using Centron.DAO.Mappings;
using Centron.DAO.Mappings.Administration;
using Centron.DAO.Mappings.Administration.Applications;
using Centron.DAO.Mappings.Administration.Company;
using Centron.DAO.Mappings.Administration.Employees;
using Centron.DAO.Mappings.Administration.Logins;
using Centron.DAO.Mappings.CustomerArea.Support;
using Centron.DAO.Mappings.States;
using Centron.Data.Entities.Administration.Logins;
using Centron.Interfaces.Administration.Logins;
using Centron.Interfaces.BL;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.BL.Administration.Logins.Auth;
internal class ConcreteAuthenticator(
DAOSession session, AuthObject authObject, ILicenseManager licenseManager, LoggedInUser? loggedInUser = null)
: Authenticator(session, authObject, licenseManager)
{
protected override Result<LoggedInUser> AuthenticateInternal()
{
// only used for "GetTicket" tests
return loggedInUser ?? Result<LoggedInUser>.AsError("Authentication failed");
}
}
internal class ConcreteAuthObject : AuthObject;
[TestSubject(typeof(ConcreteAuthenticator))]
public class AuthenticatorTest
{
public static AuthObject DummyAuthObject => new ConcreteAuthObject
{
ApplicationName = ApplicationKind.Centron.LicenseGuid.ToString(),
AppVersion = "1.0.0.0",
MachineName = "machine"
};
public static IEnumerable<object?[]> GetDataValidateAppUser()
{
var yesterday = DateTime.Today - TimeSpan.FromDays(1);
var tomorrow = DateTime.Today + TimeSpan.FromDays(1);
return new List<object?[]>
{
new object?[] { ResultStatus.Success, false, null, null, 1 },
new object?[] { ResultStatus.Success, false, yesterday, null, 1 },
new object?[] { ResultStatus.Success, false, null, tomorrow, 1 },
new object?[] { ResultStatus.Success, false, yesterday, tomorrow, 1 },
new object?[] { ResultStatus.Error, true, null, null, 1 },
new object?[] { ResultStatus.Error, false, tomorrow, null, 1 },
new object?[] { ResultStatus.Error, false, null, yesterday, 1 },
new object?[] { ResultStatus.Error, false, tomorrow, yesterday, 1 },
new object?[] { ResultStatus.Error, false, null, yesterday, 1 },
new object?[] { ResultStatus.Error, false, null, null, 0 },
new object?[] { ResultStatus.Error, false, null, null, 2 },
};
}
[Theory]
[MemberData(nameof(GetDataValidateAppUser))]
public void ValidateAppUser(
ResultStatus expectedStatus, bool isAccountDisabled, DateTime? commencementDate, DateTime? leavingDate,
int state)
{
var session = CreateSession();
var daoSession = new DAOSession(session);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var authenticator = new ConcreteAuthenticator(daoSession, DummyAuthObject, licenseManager);
var user = DbSetupUtils.SaveAppUser(
session,
"admin",
"admin@admin.com",
1,
isAccountDisabled: isAccountDisabled,
commencementDate: commencementDate,
leavingDate: leavingDate,
state: state);
var validationResult = authenticator.ValidateAppUser(user);
Assert.Equal(expectedStatus, validationResult.Status);
}
[Fact]
public void AuthenticateAppUserExistingTicket()
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var authenticator = new ConcreteAuthenticator(daoSession, DummyAuthObject, licenseManager);
var user = DbSetupUtils.SaveAppUser(session, "admin", "admin@admin.com", 1);
var loggedInUser = new LoggedInUser(user);
var ticketBl = new TicketBL(daoSession);
ticketBl.CreateNewTicket(ApplicationKind.Centron, ApplicationKind.Centron.LicenseGuid, "machine", loggedInUser.User, null);
session.Flush();
// Act
var authenticationResult =
authenticator.AuthenticateUser(loggedInUser, ApplicationKind.Centron, "machine", "1.0.0.0");
// Assert
Assert.Equal(ResultStatus.Success, authenticationResult.Status);
}
public static IEnumerable<object?[]> GetDataAuthenticateAppUserNoTicket()
{
return new List<object?[]>
{
new object?[] { ResultStatus.Success, Result<Guid>.AsSuccess(LicenseGuids.Centron) },
new object?[] { ResultStatus.Error, Result<Guid>.AsError("") },
};
}
[Theory]
[MemberData(nameof(GetDataAuthenticateAppUserNoTicket))]
public void AuthenticateAppUserNoTicket(ResultStatus expectedStatus, Result<Guid> checkLicenseResult)
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(checkLicenseResult);
var authenticator = new ConcreteAuthenticator(daoSession, DummyAuthObject, licenseManager);
var user = DbSetupUtils.SaveAppUser(session, "admin", "admin@admin.com", 1);
var loggedInUser = new LoggedInUser(user);
// Act
var authenticationResult =
authenticator.AuthenticateUser(loggedInUser, ApplicationKind.Centron, "machine",
"1.0.0.0");
// Assert
Assert.Equal(expectedStatus, authenticationResult.Status);
}
public static IEnumerable<object?[]> GetDataAppUserNoTicketDifferentArgs()
{
return new List<object?[]>
{
new object?[] { ResultStatus.Success, "machine", "1.0.0.0" },
new object?[] { ResultStatus.Warning, "machine", "not-a-real-version" },
new object?[] { ResultStatus.Warning, "machine", "" },
new object?[] { ResultStatus.Warning, "machine", null },
new object?[] { ResultStatus.Success, "", "1.0.0.0" },
new object?[] { ResultStatus.Success, null, "1.0.0.0" },
};
}
[Theory]
[MemberData(nameof(GetDataAppUserNoTicketDifferentArgs))]
public void AuthenticateAppUserNoTicketDifferentArgs(ResultStatus expectedStatus, string machine, string appVersion)
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.Centron);
var authenticator = new ConcreteAuthenticator(daoSession, DummyAuthObject, licenseManager);
var user = DbSetupUtils.SaveAppUser(session, "admin", "admin@admin.com", 1);
var loggedInUser = new LoggedInUser(user);
// Act
var authenticationResult =
authenticator.AuthenticateUser(loggedInUser, ApplicationKind.Centron, machine, appVersion);
// Assert
Assert.Equal(expectedStatus, authenticationResult.Status);
}
[Fact]
public void GetTicketSuccess()
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
const string sub = "valid-sub";
const string email = "admin@admin.com";
var user = DbSetupUtils.SaveAppUser(session, "admin", email, 1, sub);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.Centron);
var authenticator = new ConcreteAuthenticator(daoSession, DummyAuthObject, licenseManager, new LoggedInUser(user)); // return user on authenticate
// Act
var result = authenticator.GetTicket();
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.NotEmpty(result.Data);
var ticketBl = new TicketBL(daoSession);
var ticketResult = ticketBl.GetTicket(result.Data);
Assert.Equal(ResultStatus.Success, ticketResult.Status);
Assert.NotNull(ticketResult.Data);
var ticket = ticketResult.Data;
Assert.Equal(user.I3D, ticket.UserI3D);
}
[Fact]
public void GetTicketFail()
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
var user = DbSetupUtils.SaveAppUser(session, "admin", "admin@admin.com", 1);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var authenticator = new ConcreteAuthenticator(daoSession, DummyAuthObject, licenseManager, null); // return Error on authenticate
// Act
var result = authenticator.GetTicket();
// Assert
Assert.Equal(ResultStatus.Error, result.Status);
}
private static ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<AppUserMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeCompactMaps>())
.Mappings(m => m.FluentMappings.Add<SupportLevelMaps>())
.Mappings(m => m.FluentMappings.Add<MandatorMaps>())
.Mappings(m => m.FluentMappings.Add<CountryMaps>())
.Mappings(m => m.FluentMappings.Add<FederalStateMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserGroupMaps>())
.Mappings(m => m.FluentMappings.Add<AppRightMaps>())
.Mappings(m => m.FluentMappings.Add<ApplicationVersionMaps>())
.Mappings(m => m.FluentMappings.Add<TicketMaps>());
var sessionFactory = fluentConfiguration.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection,
Console.Out);
return session;
}
}
@@ -0,0 +1,119 @@
using Centron.Tests.BL.SetupUtils;
using Centron.BusinessLogic.Administration.Logins.Auth;
using Centron.DAO;
using Centron.DAO.Mappings;
using Centron.DAO.Mappings.Administration;
using Centron.DAO.Mappings.Administration.Applications;
using Centron.DAO.Mappings.Administration.Company;
using Centron.DAO.Mappings.Administration.Employees;
using Centron.DAO.Mappings.Administration.Logins;
using Centron.DAO.Mappings.CustomerArea.Support;
using Centron.DAO.Mappings.States;
using Centron.Interfaces.Administration.Logins;
using Centron.Interfaces.BL;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.BL.Administration.Logins.Auth;
[TestSubject(typeof(BasicAuthenticator))]
public class BasicAuthenticatorTest
{
private const string _correctUsername = "admin";
private const string _correctPassword = "TestPassword123!";
private const string _otherUsername = "other";
private const string _otherPassword = "OtherPassword123!!";
[Fact]
public void AuthenticateSuccess()
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
var user = DbSetupUtils.SaveAppUser(session, _correctUsername, "admin@admin.com", 1,
plainTextPassword: _correctPassword);
var otherUser = DbSetupUtils.SaveAppUser(session, _otherUsername, "different@mail.com", 2,
plainTextPassword: _otherPassword);
var authObject = new BasicAuthObject
{
ApplicationName = ApplicationKind.Centron.Name,
MachineName = "machine",
AppVersion = "1.0.0.0",
UserName = _correctUsername,
Password = _correctPassword
};
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var authenticator = new BasicAuthenticator(daoSession, authObject, licenseManager);
// Act
var result = authenticator.Authenticate();
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.Equal(user.I3D, result.Data.User.I3D);
}
[Theory]
[InlineData("invalid-username", _correctPassword)]
[InlineData(_otherUsername, _correctPassword)]
[InlineData("", _correctPassword)]
[InlineData(null, _correctPassword)]
[InlineData(_correctUsername, "invalid-password")]
[InlineData(_correctUsername, _otherPassword)]
[InlineData(_correctUsername, "")]
[InlineData(_correctUsername, null)]
public void AuthenticateFailOnUsernamePassword(string? username, string? password)
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
var user = DbSetupUtils.SaveAppUser(session, _correctUsername, "admin@admin.com", 1,
plainTextPassword: _correctPassword);
var otherUser = DbSetupUtils.SaveAppUser(session, _otherUsername, "different@mail.com", 2,
plainTextPassword: _otherPassword);
var authObject = new BasicAuthObject
{
ApplicationName = ApplicationKind.Centron.Name,
MachineName = "machine",
AppVersion = "1.0.0.0",
UserName = username,
Password = password
};
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var authenticator = new BasicAuthenticator(daoSession, authObject, licenseManager);
// Act
var result = authenticator.Authenticate();
// Assert
Assert.Equal(ResultStatus.Error, result.Status);
}
private static ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<AppUserMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeCompactMaps>())
.Mappings(m => m.FluentMappings.Add<SupportLevelMaps>())
.Mappings(m => m.FluentMappings.Add<MandatorMaps>())
.Mappings(m => m.FluentMappings.Add<CountryMaps>())
.Mappings(m => m.FluentMappings.Add<FederalStateMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserGroupMaps>())
.Mappings(m => m.FluentMappings.Add<AppRightMaps>())
.Mappings(m => m.FluentMappings.Add<ApplicationVersionMaps>())
.Mappings(m => m.FluentMappings.Add<TicketMaps>());
var sessionFactory = fluentConfiguration.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection,
Console.Out);
return session;
}
}
@@ -0,0 +1,193 @@
using System.Security.Claims;
using Centron.Tests.BL.SetupUtils;
using Centron.BusinessLogic.Administration.Logins;
using Centron.BusinessLogic.Administration.Logins.Auth;
using Centron.DAO;
using Centron.DAO.Mappings;
using Centron.DAO.Mappings.Administration;
using Centron.DAO.Mappings.Administration.Applications;
using Centron.DAO.Mappings.Administration.Company;
using Centron.DAO.Mappings.Administration.Employees;
using Centron.DAO.Mappings.Administration.Logins;
using Centron.DAO.Mappings.CustomerArea.Support;
using Centron.DAO.Mappings.States;
using Centron.Interfaces.Administration.Logins;
using Centron.Interfaces.BL;
using Centron.Tests.BL.Fixtures;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.BL.Administration.Logins.Auth;
[TestSubject(typeof(OpenIdConnectAuthenticator))]
public class OpenIdConnectAuthenticatorTest : IClassFixture<LicenseManagerFixture>
{
[Fact]
public void AuthenticateSuccess()
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
const string sub = "valid-sub";
const string email = "admin@admin.com";
var user = DbSetupUtils.SaveAppUser(session, "admin", email, 1, sub);
var otherUser = DbSetupUtils.SaveAppUser(session, "other", "different@mail.com", 2, "different-sub");
var identity = new ClaimsIdentity(new List<Claim>
{
new(OpenIdConnectAuthObject.SubjectIdentifierKey, sub),
new(OpenIdConnectAuthObject.EmailKey, email)
}, "test");
var authObject = new OpenIdConnectAuthObject
{
Identity = identity,
ApplicationName = ApplicationKind.Centron.Name,
MachineName = "machine",
AppVersion = "1.0.0.0"
};
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var authenticator = new OpenIdConnectAuthenticator(daoSession, authObject, licenseManager);
// Act
var result = authenticator.Authenticate();
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.Equal(user.I3D, result.Data.User.I3D);
}
[Theory]
[InlineData("invalid-sub")]
[InlineData("")]
[InlineData(null)]
public void AuthenticateFailOnSub(string? sub)
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
const string email = "admin@admin.com";
var user = DbSetupUtils.SaveAppUser(session, "admin", email, 1, sub);
var otherUser = DbSetupUtils.SaveAppUser(session, "other", "different@mail.com", 2, "different-sub");
var authObject = new OpenIdConnectAuthObject
{
Identity = new ClaimsIdentity(new List<Claim>
{
new(OpenIdConnectAuthObject.SubjectIdentifierKey, "valid-sub"),
new(OpenIdConnectAuthObject.EmailKey, email)
}, "test"),
ApplicationName = ApplicationKind.Centron.LicenseGuid.ToString(),
MachineName = "machine",
AppVersion = "1.0.0.0"
};
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var authenticator = new OpenIdConnectAuthenticator(daoSession, authObject, licenseManager);
// Act
var result = authenticator.Authenticate();
// Assert
Assert.Equal(ResultStatus.Error, result.Status);
}
[Fact]
public void GetTicketSuccess()
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
const string sub = "valid-sub";
const string email = "admin@admin.com";
var user = DbSetupUtils.SaveAppUser(session, "admin", email, 1, sub);
var identity = new ClaimsIdentity(new List<Claim>
{
new(OpenIdConnectAuthObject.SubjectIdentifierKey, sub),
new(OpenIdConnectAuthObject.EmailKey, email)
}, "test");
var authObject = new OpenIdConnectAuthObject
{
Identity = identity,
ApplicationName = ApplicationKind.Centron.LicenseGuid.ToString(),
MachineName = "machine",
AppVersion = "1.0.0.0"
};
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.Centron, LicenseGuids.OpenIDConnectAuthentication);
var authenticator = new OpenIdConnectAuthenticator(daoSession, authObject, licenseManager);
// Act
var result = authenticator.GetTicket();
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.NotEmpty(result.Data);
var ticketBl = new TicketBL(daoSession);
var ticketResult = ticketBl.GetTicket(result.Data);
Assert.Equal(ResultStatus.Success, ticketResult.Status);
Assert.NotNull(ticketResult.Data);
var ticket = ticketResult.Data;
Assert.Equal(user.I3D, ticket.UserI3D);
}
[Theory]
[InlineData("invalid-sub")]
[InlineData("")]
[InlineData(null)]
public void GetTicketFailOnSub(string? sub)
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
const string email = "admin@admin.com";
var user = DbSetupUtils.SaveAppUser(session, "admin", email, 1, sub);
var otherUser = DbSetupUtils.SaveAppUser(session, "other", "different@mail.com", 2, "different-sub");
var authObject = new OpenIdConnectAuthObject
{
Identity = new ClaimsIdentity(new List<Claim>
{
new(OpenIdConnectAuthObject.SubjectIdentifierKey, "valid-sub"),
new(OpenIdConnectAuthObject.EmailKey, email)
}, "test"),
ApplicationName = ApplicationKind.Centron.LicenseGuid.ToString(),
MachineName = "machine",
AppVersion = "1.0.0.0"
};
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var authenticator = new OpenIdConnectAuthenticator(daoSession, authObject, licenseManager);
// Act
var result = authenticator.GetTicket();
// Assert
Assert.Equal(ResultStatus.Error, result.Status);
}
private static ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<AppUserMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeCompactMaps>())
.Mappings(m => m.FluentMappings.Add<SupportLevelMaps>())
.Mappings(m => m.FluentMappings.Add<MandatorMaps>())
.Mappings(m => m.FluentMappings.Add<CountryMaps>())
.Mappings(m => m.FluentMappings.Add<FederalStateMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserGroupMaps>())
.Mappings(m => m.FluentMappings.Add<AppRightMaps>())
.Mappings(m => m.FluentMappings.Add<ApplicationVersionMaps>())
.Mappings(m => m.FluentMappings.Add<TicketMaps>());
var sessionFactory = fluentConfiguration.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection,
Console.Out);
return session;
}
}
@@ -0,0 +1,190 @@
using Centron.Tests.BL.SetupUtils;
using Centron.Tests.BL.DatabaseMappings;
using Centron.BusinessLogic.Administration.Logins.Auth;
using Centron.DAO;
using Centron.DAO.Mappings;
using Centron.DAO.Mappings.Accounts;
using Centron.DAO.Mappings.Administration;
using Centron.DAO.Mappings.Administration.Applications;
using Centron.DAO.Mappings.Administration.Company;
using Centron.DAO.Mappings.Administration.Employees;
using Centron.DAO.Mappings.Administration.FileManagement;
using Centron.DAO.Mappings.Administration.Logins;
using Centron.DAO.Mappings.Administration.Settings;
using Centron.DAO.Mappings.TemporaryEntities;
using Centron.DAO.Mappings.CustomerArea;
using Centron.DAO.Mappings.CustomerArea.CRM.Activities;
using Centron.DAO.Mappings.CustomerArea.CustomerDetails;
using Centron.DAO.Mappings.CustomerArea.Support;
using Centron.DAO.Mappings.EmployeeArea;
using Centron.DAO.Mappings.Logistics.Warehousing;
using Centron.DAO.Mappings.Merchandise.Articles;
using Centron.DAO.Mappings.Merchandise.Materialgroups;
using Centron.DAO.Mappings.Sales.Customers;
using Centron.DAO.Mappings.States;
using Centron.DAO.Mappings.Warehousing;
using Centron.DAO.Mappings.Warehousing.InventoryManagement;
using Centron.DAO.Mappings.Warehousing.StockManagement;
using Centron.Interfaces.Administration.Logins;
using Centron.Interfaces.BL;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.BL.Administration.Logins.Auth;
[TestSubject(typeof(WebAccountAuthenticator))]
public class WebAccountAuthenticatorTest
{
private const string _correctPassword = "TestPassword123!";
private const string _correctUsername = "admin";
[Fact]
public void AuthenticateSuccess()
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
var responsibleUser = DbSetupUtils.SaveAppUser(session, "responsible-user", "responsible@email.com", 1);
DbSetupUtils.SetupResponsibleEmployeeForWebaccounts(session, responsibleUser.Employee);
var user = DbSetupUtils.SaveWebAccount(session, _correctUsername, "admin@admin.com", 2, plainTextPassword: _correctPassword);
var otherUser = DbSetupUtils.SaveWebAccount(session, "other", "different@mail.com", 3, plainTextPassword: "other-password");
var authObject = new WebAccountAuthObject
{
ApplicationName = ApplicationKind.Centron.Name,
MachineName = "machine",
AppVersion = "1.0.0.0",
UserName = _correctUsername,
Password = _correctPassword
};
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var authenticator = new WebAccountAuthenticator(daoSession, authObject, licenseManager);
// Act
var result = authenticator.Authenticate();
// Assert
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.Equal(user.I3D, result.Data.WebAccount.I3D);
Assert.Equal(responsibleUser.I3D, result.Data.User.I3D);
}
[Theory]
[InlineData("invalid-username", _correctPassword)]
[InlineData("", _correctPassword)]
[InlineData(null, _correctPassword)]
[InlineData(_correctUsername, "invalid-password")]
[InlineData(_correctUsername, "")]
[InlineData(_correctUsername, null)]
public void AuthenticateFailOnUsernamePassword(string? username, string? password)
{
// Arrange
var session = CreateSession();
var daoSession = new DAOSession(session);
var responsibleUser = DbSetupUtils.SaveAppUser(session, "responsible-user", "responsible@email.com", 1);
DbSetupUtils.SetupResponsibleEmployeeForWebaccounts(session, responsibleUser.Employee);
var user = DbSetupUtils.SaveWebAccount(session, _correctUsername, "admin@admin.com", 2, plainTextPassword: _correctPassword);
var otherUser = DbSetupUtils.SaveWebAccount(session, "other", "different@mail.com", 3, plainTextPassword: "other-password");
var authObject = new WebAccountAuthObject
{
ApplicationName = ApplicationKind.Centron.Name,
MachineName = "machine",
AppVersion = "1.0.0.0",
UserName = username,
Password = password
};
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var authenticator = new WebAccountAuthenticator(daoSession, authObject, licenseManager);
// Act
var result = authenticator.Authenticate();
// Assert
Assert.Equal(ResultStatus.Error, result.Status);
}
private static ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<AppUserMaps>())
.Mappings(m => m.FluentMappings.Add<AppSettingMaps>())
.Mappings(m => m.FluentMappings.Add<ApplicationSettingMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeCompactMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeDepartmentMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeArticleMaps>())
.Mappings(m => m.FluentMappings.Add<SupportLevelMaps>())
.Mappings(m => m.FluentMappings.Add<MandatorMaps>())
.Mappings(m => m.FluentMappings.Add<CountryMaps>())
.Mappings(m => m.FluentMappings.Add<FederalStateMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserGroupMaps>())
.Mappings(m => m.FluentMappings.Add<AppRightMaps>())
.Mappings(m => m.FluentMappings.Add<ApplicationVersionMaps>())
.Mappings(m => m.FluentMappings.Add<WebAccountMaps>())
.Mappings(m => m.FluentMappings.Add<WebRightsCategoriesMaps>())
.Mappings(m => m.FluentMappings.Add<WebRightsMaps>())
.Mappings(m => m.FluentMappings.Add<ContactPersonMaps>())
.Mappings(m => m.FluentMappings.Add<AddressMaps>())
.Mappings(m => m.FluentMappings.Add<AccountMaps>())
.Mappings(m => m.FluentMappings.Add<AccountAddressMaps>())
.Mappings(m => m.FluentMappings.Add<AccountAddressContactMaps>())
.Mappings(m => m.FluentMappings.Add<GeoInfoMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerCompactMaps>())
// IMPORTANT: 'CustomerMapsForTest' instead of 'CustomerMaps' due to syntax error on column 'Limit'
.Mappings(m => m.FluentMappings.Add<CustomerMapsForTest>())
.Mappings(m => m.FluentMappings.Add<CustomerDepartmentMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerSalutationMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerBusinessLineMaps>())
.Mappings(m => m.FluentMappings.Add<BusinessLineMaps>())
.Mappings(m => m.FluentMappings.Add<SupplierMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerInterestMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerProductMaps>())
.Mappings(m => m.FluentMappings.Add<ArticleMaps>())
.Mappings(m => m.FluentMappings.Add<ArticleUnitMaps>())
.Mappings(m => m.FluentMappings.Add<ArticleLightMaps>())
.Mappings(m => m.FluentMappings.Add<PartListArticleMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerArticleMaps>())
.Mappings(m => m.FluentMappings.Add<ArticleCompactMaps>())
.Mappings(m => m.FluentMappings.Add<ArticleStockInfoMaps>())
.Mappings(m => m.FluentMappings.Add<SecondaryStockArticleMaps>())
.Mappings(m => m.FluentMappings.Add<ValueAddedTaxMaps>())
.Mappings(m => m.FluentMappings.Add<CostObjectMaps>())
.Mappings(m => m.FluentMappings.Add<StockMaps>())
.Mappings(m => m.FluentMappings.Add<StorageAreaMaps>())
.Mappings(m => m.FluentMappings.Add<SpecialAgreementMaps>())
.Mappings(m => m.FluentMappings.Add<SpecialAgreementArticleMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerSpecialPriceMaps>())
.Mappings(m => m.FluentMappings.Add<InterestMaps>())
.Mappings(m => m.FluentMappings.Add<ProductMaps>())
.Mappings(m => m.FluentMappings.Add<ActivityMaps>())
.Mappings(m => m.FluentMappings.Add<MaterialGroupMaps>())
.Mappings(m => m.FluentMappings.Add<MaterialGroupCompactMaps>())
.Mappings(m => m.FluentMappings.Add<MaterialMarkupMaps>())
.Mappings(m => m.FluentMappings.Add<SecondaryMaterialGroupMaps>())
.Mappings(m => m.FluentMappings.Add<SecondaryMaterialGroupCompactMaps>())
.Mappings(m => m.FluentMappings.Add<SecondaryMaterialMarkupMaps>())
.Mappings(m => m.FluentMappings.Add<AccountMaps>())
.Mappings(m => m.FluentMappings.Add<DirectoryMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerDirectoryMaps>())
.Mappings(m => m.FluentMappings.Add<DocumentMaps>())
.Mappings(m => m.FluentMappings.Add<CostCenterMaps>())
.Mappings(m => m.FluentMappings.Add<TicketMaps>());
var sessionFactory = fluentConfiguration.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection,
Console.Out);
return session;
}
}
@@ -0,0 +1,439 @@
using Centron.DAO;
using Centron.Tests.BL.SetupUtils;
using Centron.DAO.Mappings;
using Centron.DAO.Mappings.Administration;
using Centron.DAO.Mappings.Administration.Company;
using Centron.DAO.Mappings.Administration.Employees;
using Centron.DAO.Mappings.CustomerArea.Support;
using Centron.DAO.Mappings.States;
using Centron.Data.WebServices.Administration;
using Centron.Data.WebServices.Administration.Login;
using Centron.Interfaces.Administration.Logins;
using Microsoft.Graph.Models;
namespace Centron.Tests.BL.Administration.Logins;
using Centron.BusinessLogic.Administration.Logins;
using Centron.Interfaces.BL;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate.Tool.hbm2ddl;
using System.Collections.Generic;
using DAO;
using JetBrains.Annotations;
using NHibernate;
using Xunit;
[TestSubject(typeof(EntraIDUsersBL))]
public class EntraIDUsersBLTest
{
[Fact]
public async Task MatchEntraIDUsersWorksAsExpected()
{
var session = CreateSession();
DbSetupUtils.SaveAppUser(session, "admin", "admin@admin.com", 1, "1-unique");
DbSetupUtils.SaveAppUser(session, "root", "root@admin.com", 10, null);
DbSetupUtils.SaveAppUser(session, "Hans", "Hans@mail.com", 2, "2-matched");
DbSetupUtils.SaveAppUser(session, "Jake", "Jake@mail.com", 3, "3-matched");
DbSetupUtils.SaveAppUser(session, "Maria", "Maria@mail.com", 4, "4-unmatched");
DbSetupUtils.SaveAppUser(session, "Eve", "", 5, "5-matched");
DbSetupUtils.SaveAppUser(session, "Dave", "Dave@mail.com", 7, null);
DbSetupUtils.SaveAppUser(session, "Lia", "Lia@mail.com", 8, "8-true");
await session.FlushAsync();
List<User> entraUsers =
[
CreateUser("Hans@mail.com", "2-matched"),
CreateUser("jake@mail.com", "3-matched"),
CreateUser("Maria@mail.com", "6-unmatched"),
CreateUser("Eve@mail.com", "5-matched"),
CreateUser("Dave@mail.com", "7-notsyncedyet"),
CreateUser("Lia@mail.com", "8-false"),
CreateUser("Lia-false@mail.com", "8-true"),
CreateUser("john@mail.com", "10-unique")
];
var expectedResult =
Result<MatchUsersResultDTO>.AsSuccess(new MatchUsersResultDTO
{
MatchedUsers =
[
new MatchedEntraIDCentronUserDTO
{
EntraIdUser = new EntraIDUserDTO { Email = "Hans@mail.com", SubjectIdentifier = "2-matched" },
AppUser = new AppUserDTO
{ Name = "Hans", EmployeeI3D = 2, I3D = 2, OpenIdConnectSubjectIdentifier = "2-matched" }
},
new MatchedEntraIDCentronUserDTO
{
EntraIdUser = new EntraIDUserDTO { Email = "jake@mail.com", SubjectIdentifier = "3-matched" },
AppUser = new AppUserDTO
{ Name = "Jake", EmployeeI3D = 3, I3D = 3, OpenIdConnectSubjectIdentifier = "3-matched" }
}
],
UnmatchedUsers =
[
new MatchedEntraIDCentronUserDTO
{
EntraIdUser = new EntraIDUserDTO
{ Email = "Dave@mail.com", SubjectIdentifier = "7-notsyncedyet" },
AppUser = new AppUserDTO
{ Name = "Dave", EmployeeI3D = 7, I3D = 7, OpenIdConnectSubjectIdentifier = null }
}
],
MisconfiguredSub =
[
new MatchedEntraIDCentronUserDTO
{
EntraIdUser = null,
AppUser = new AppUserDTO
{ Name = "admin", EmployeeI3D = 1, I3D = 1, OpenIdConnectSubjectIdentifier = "1-unique" }
},
new MatchedEntraIDCentronUserDTO
{
EntraIdUser =
new EntraIDUserDTO { Email = "Maria@mail.com", SubjectIdentifier = "6-unmatched" },
AppUser = new AppUserDTO
{ Name = "Maria", EmployeeI3D = 4, I3D = 4, OpenIdConnectSubjectIdentifier = "4-unmatched" }
},
new MatchedEntraIDCentronUserDTO
{
EntraIdUser = new EntraIDUserDTO { Email = "Eve@mail.com", SubjectIdentifier = "5-matched" },
AppUser = new AppUserDTO
{ Name = "Eve", EmployeeI3D = 5, I3D = 5, OpenIdConnectSubjectIdentifier = "5-matched" }
},
new MatchedEntraIDCentronUserDTO()
{
EntraIdUser = new EntraIDUserDTO { Email = "Lia-false@mail.com", SubjectIdentifier = "8-true" },
AppUser = new AppUserDTO
{ Name = "Lia", EmployeeI3D = 8, I3D = 8, OpenIdConnectSubjectIdentifier = "8-true" }
}
],
UsersNotInCentron =
[
new EntraIDUserDTO { Email = "john@mail.com", SubjectIdentifier = "10-unique" }
],
UsersNotInEntra =
[
new AppUserDTO { Name = "root", EmployeeI3D = 10, I3D = 10, OpenIdConnectSubjectIdentifier = null }
]
});
var daoSession = new DAOSession(session);
var graphServiceClient = MockSetupUtils.PrepareGraphMock(entraUsers);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var entraIDUsersBL = new EntraIDUsersBL(daoSession, graphServiceClient, licenseManager);
var matchEntraIdUsers = await entraIDUsersBL.MatchEntraIDUsers();
Assert.Equivalent(expectedResult.Data, matchEntraIdUsers.Data, true);
}
[Fact]
public async Task MatchEntraIDUsersMatch()
{
var session = CreateSession();
DbSetupUtils.SaveAppUser(session, "user", "user@mail.com", 1, "match");
await session.FlushAsync();
List<User> entraUsers =
[
CreateUser("user@mail.com", "match"),
];
var expectedResult =
Result<MatchUsersResultDTO>.AsSuccess(new MatchUsersResultDTO
{
MatchedUsers =
[
new MatchedEntraIDCentronUserDTO
{
EntraIdUser = new EntraIDUserDTO { Email = "user@mail.com", SubjectIdentifier = "match" },
AppUser = new AppUserDTO
{ Name = "user", EmployeeI3D = 1, I3D = 1, OpenIdConnectSubjectIdentifier = "match" }
}
],
UnmatchedUsers = [],
MisconfiguredSub = [],
UsersNotInCentron = [],
UsersNotInEntra = []
});
var daoSession = new DAOSession(session);
var graphServiceClient = MockSetupUtils.PrepareGraphMock(entraUsers);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var entraIDUsersBL = new EntraIDUsersBL(daoSession, graphServiceClient, licenseManager);
var matchEntraIdUsers = await entraIDUsersBL.MatchEntraIDUsers();
Assert.Equivalent(expectedResult.Data, matchEntraIdUsers.Data, true);
}
[Fact]
public async Task MatchEntraIDUsersUnmatched()
{
var session = CreateSession();
DbSetupUtils.SaveAppUser(session, "user", "user@mail.com", 1, null);
await session.FlushAsync();
List<User> entraUsers =
[
CreateUser("user@mail.com", "not-synced-yet"),
];
var expectedResult =
Result<MatchUsersResultDTO>.AsSuccess(new MatchUsersResultDTO
{
MatchedUsers = [],
UnmatchedUsers =
[
new MatchedEntraIDCentronUserDTO
{
EntraIdUser = new EntraIDUserDTO
{ Email = "user@mail.com", SubjectIdentifier = "not-synced-yet" },
AppUser = new AppUserDTO
{ Name = "user", EmployeeI3D = 1, I3D = 1, OpenIdConnectSubjectIdentifier = null }
}
],
MisconfiguredSub = [],
UsersNotInCentron = [],
UsersNotInEntra = []
});
var daoSession = new DAOSession(session);
var graphServiceClient = MockSetupUtils.PrepareGraphMock(entraUsers);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var entraIDUsersBL = new EntraIDUsersBL(daoSession, graphServiceClient, licenseManager);
var matchEntraIdUsers = await entraIDUsersBL.MatchEntraIDUsers();
Assert.Equivalent(expectedResult.Data, matchEntraIdUsers.Data, true);
}
[Fact]
public async Task MatchEntraIDUsersMisconfiguredWrongEmail()
{
var session = CreateSession();
DbSetupUtils.SaveAppUser(session, "user", "wrong-user@mail.com", 1, "match");
await session.FlushAsync();
List<User> entraUsers =
[
CreateUser("correct-user@mail.com", "match"),
];
var expectedResult =
Result<MatchUsersResultDTO>.AsSuccess(new MatchUsersResultDTO
{
MatchedUsers = [],
UnmatchedUsers = [],
MisconfiguredSub =
[
new MatchedEntraIDCentronUserDTO
{
EntraIdUser = new EntraIDUserDTO
{ Email = "correct-user@mail.com", SubjectIdentifier = "match" },
AppUser = new AppUserDTO
{ Name = "user", EmployeeI3D = 1, I3D = 1, OpenIdConnectSubjectIdentifier = "match" }
}
],
UsersNotInCentron = [],
UsersNotInEntra = []
});
var daoSession = new DAOSession(session);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var graphServiceClient = MockSetupUtils.PrepareGraphMock(entraUsers);
var entraIDUsersBL = new EntraIDUsersBL(daoSession, graphServiceClient, licenseManager);
var matchEntraIdUsers = await entraIDUsersBL.MatchEntraIDUsers();
Assert.Equivalent(expectedResult.Data, matchEntraIdUsers.Data, true);
}
[Fact]
public async Task MatchEntraIDUsersMisconfiguredNoEntraUserWithSub()
{
var session = CreateSession();
DbSetupUtils.SaveAppUser(session, "user", "user@mail.com", 1, "non-existent-sub");
await session.FlushAsync();
var expectedResult =
Result<MatchUsersResultDTO>.AsSuccess(new MatchUsersResultDTO
{
MatchedUsers = [],
UnmatchedUsers = [],
MisconfiguredSub =
[
new MatchedEntraIDCentronUserDTO
{
EntraIdUser = null,
AppUser = new AppUserDTO
{
Name = "user", EmployeeI3D = 1, I3D = 1, OpenIdConnectSubjectIdentifier = "non-existent-sub"
}
}
],
UsersNotInCentron = [],
UsersNotInEntra = []
});
var daoSession = new DAOSession(session);
var graphServiceClient = MockSetupUtils.PrepareGraphMock([]);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var entraIDUsersBL = new EntraIDUsersBL(daoSession, graphServiceClient, licenseManager);
var matchEntraIdUsers = await entraIDUsersBL.MatchEntraIDUsers();
Assert.Equivalent(expectedResult.Data, matchEntraIdUsers.Data, true);
}
[Fact]
public async Task MatchEntraIDUsersNotInCentron()
{
var session = CreateSession();
await session.FlushAsync();
List<User> entraUsers =
[
CreateUser("user@mail.com", "not-synced-yet"),
];
var expectedResult =
Result<MatchUsersResultDTO>.AsSuccess(new MatchUsersResultDTO
{
MatchedUsers = [],
UnmatchedUsers = [],
MisconfiguredSub = [],
UsersNotInCentron =
[
new EntraIDUserDTO { Email = "user@mail.com", SubjectIdentifier = "not-synced-yet" }
],
UsersNotInEntra = []
});
var daoSession = new DAOSession(session);
var graphServiceClient = MockSetupUtils.PrepareGraphMock(entraUsers);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var entraIDUsersBL = new EntraIDUsersBL(daoSession, graphServiceClient, licenseManager);
var matchEntraIdUsers = await entraIDUsersBL.MatchEntraIDUsers();
Assert.Equivalent(expectedResult.Data, matchEntraIdUsers.Data, true);
}
[Fact]
public async Task MatchEntraIDUsersNotInEntra()
{
var session = CreateSession();
DbSetupUtils.SaveAppUser(session, "user", "user@mail.com", 1, null);
await session.FlushAsync();
var expectedResult =
Result<MatchUsersResultDTO>.AsSuccess(new MatchUsersResultDTO
{
MatchedUsers = [],
UnmatchedUsers = [],
MisconfiguredSub = [],
UsersNotInCentron = [],
UsersNotInEntra =
[
new AppUserDTO { Name = "user", EmployeeI3D = 1, I3D = 1, OpenIdConnectSubjectIdentifier = null }
]
});
var daoSession = new DAOSession(session);
var graphServiceClient = MockSetupUtils.PrepareGraphMock([]);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var entraIDUsersBL = new EntraIDUsersBL(daoSession, graphServiceClient, licenseManager);
var matchEntraIdUsers = await entraIDUsersBL.MatchEntraIDUsers();
Assert.Equivalent(expectedResult.Data, matchEntraIdUsers.Data, true);
}
[Fact]
public async Task MatchEntraIDUsersNotActive()
{
var session = CreateSession();
DbSetupUtils.SaveAppUser(session, "disabled", "disabled@mail.com", 1, isAccountDisabled: true);
DbSetupUtils.SaveAppUser(session, "disabled-from-today", "disabled-from-today@mail.com", 2,
disabledFromDate: DateTime.Today);
DbSetupUtils.SaveAppUser(session, "disabled-from-yesterday", "disabled-from-yesterday@mail.com", 3,
disabledFromDate: DateTime.Today - TimeSpan.FromDays(1));
DbSetupUtils.SaveAppUser(session, "disabled-to-today", "disabled-to-today@mail.com", 4,
disabledToDate: DateTime.Today);
DbSetupUtils.SaveAppUser(session, "disabled-to-tomorrow", "disabled-to-tomorrow@mail.com", 5,
disabledToDate: DateTime.Today + TimeSpan.FromDays(1));
DbSetupUtils.SaveAppUser(session, "disabled-in-range", "disabled-in-range@mail.com", 6,
disabledFromDate: DateTime.Today - TimeSpan.FromDays(1), disabledToDate: DateTime.Today + TimeSpan.FromDays(1));
await session.FlushAsync();
List<User> entraUsers =
[
CreateUser("disabled@mail.com", "not-synced-yet-1"),
CreateUser("disabled-from-today@mail.com", "not-synced-yet-2"),
CreateUser("disabled-from-yesterday@mail.com", "not-synced-yet-3"),
CreateUser("disabled-to-today@mail.com", "not-synced-yet-4"),
CreateUser("disabled-to-tomorrow@mail.com", "not-synced-yet-5"),
CreateUser("disabled-in-range@mail.com", "not-synced-yet-6"),
];
var expectedResult =
Result<MatchUsersResultDTO>.AsSuccess(new MatchUsersResultDTO
{
MatchedUsers = [],
UnmatchedUsers = [],
MisconfiguredSub = [],
UsersNotInCentron = [
new EntraIDUserDTO { Email = "disabled@mail.com", SubjectIdentifier = "not-synced-yet-1" },
new EntraIDUserDTO { Email = "disabled-from-today@mail.com", SubjectIdentifier = "not-synced-yet-2" },
new EntraIDUserDTO { Email = "disabled-from-yesterday@mail.com", SubjectIdentifier = "not-synced-yet-3" },
new EntraIDUserDTO { Email = "disabled-to-today@mail.com", SubjectIdentifier = "not-synced-yet-4" },
new EntraIDUserDTO { Email = "disabled-to-tomorrow@mail.com", SubjectIdentifier = "not-synced-yet-5" },
new EntraIDUserDTO { Email = "disabled-in-range@mail.com", SubjectIdentifier = "not-synced-yet-6" }
],
UsersNotInEntra = []
});
var daoSession = new DAOSession(session);
var graphServiceClient = MockSetupUtils.PrepareGraphMock(entraUsers);
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock(LicenseGuids.OpenIDConnectAuthentication);
var entraIDUsersBL = new EntraIDUsersBL(daoSession, graphServiceClient, licenseManager);
var matchEntraIdUsers = await entraIDUsersBL.MatchEntraIDUsers();
Assert.Equivalent(expectedResult.Data, matchEntraIdUsers.Data, true);
}
private User CreateUser(string email, string subjectId)
{
return new User
{
Id = subjectId,
UserPrincipalName = email,
};
}
public ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<AppUserMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeCompactMaps>())
.Mappings(m => m.FluentMappings.Add<SupportLevelMaps>())
.Mappings(m => m.FluentMappings.Add<MandatorMaps>())
.Mappings(m => m.FluentMappings.Add<CountryMaps>())
.Mappings(m => m.FluentMappings.Add<FederalStateMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserGroupMaps>())
.Mappings(m => m.FluentMappings.Add<AppRightMaps>());
ISessionFactory sessionFactory = fluentConfiguration.BuildSessionFactory();
ISession session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection,
Console.Out);
return session;
}
}
@@ -0,0 +1,254 @@
using System.Globalization;
using Centron.BusinessLogic.Administration.Masterdata;
using Centron.DAO;
using Centron.DAO.Mappings;
using Centron.DAO.Mappings.Administration;
using Centron.Data.Entities.Administration.MasterData;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.BL.Administration.Masterdata;
/// <summary>
/// Tests for the AssetConditionBL class, specifically testing the BR-DE-18 Skonto formatting
/// for ZUGFeRD/XRechnung export functionality.
/// </summary>
[TestSubject(typeof(AssetConditionBL))]
public class AssetConditionBLTest
{
[Fact]
public void GetPaymentConditionSkontoInBR_DE_18Format_ReturnsNull_WhenNoConditionExists()
{
// Arrange
using var session = CreateSession();
var daoSession = new DAOSession(session);
var sut = new AssetConditionBL(daoSession);
var nonExistentI3D = 999;
// Act
var result = sut.GetPaymentConditionSkontoInBR_DE_18Format(nonExistentI3D);
// Assert
Assert.Null(result);
}
[Fact]
public void GetPaymentConditionSkontoInBR_DE_18Format_ReturnsNull_WhenNoSkontoData()
{
// Arrange
using var session = CreateSession();
var condition = new AssetCondition
{
ShortName = "NO_SKONTO",
Skonto1OffDay = 0,
Skonto1Percent = 0,
Skonto2OffDay = 0,
Skonto2Percent = 0
};
session.Save(condition);
session.Flush();
var daoSession = new DAOSession(session);
var sut = new AssetConditionBL(daoSession);
// Act
var result = sut.GetPaymentConditionSkontoInBR_DE_18Format(condition.I3D);
// Assert
Assert.Null(result);
}
[Fact]
public void GetPaymentConditionSkontoInBR_DE_18Format_FormatsCorrectly_WhenSingleSkontoLevel()
{
// Arrange
using var session = CreateSession();
var condition = new AssetCondition
{
ShortName = "SKONTO_14_2.5",
Skonto1OffDay = 14,
Skonto1Percent = 2.5,
Skonto2OffDay = 0,
Skonto2Percent = 0
};
session.Save(condition);
session.Flush();
var daoSession = new DAOSession(session);
var sut = new AssetConditionBL(daoSession);
// Act
var result = sut.GetPaymentConditionSkontoInBR_DE_18Format(condition.I3D);
// Assert
Assert.Equal("#SKONTO#TAGE=14#PROZENT=2.50#", result);
}
[Fact]
public void GetPaymentConditionSkontoInBR_DE_18Format_FormatsCorrectly_WhenTwoSkontoLevels()
{
// Arrange
using var session = CreateSession();
var condition = new AssetCondition
{
ShortName = "SKONTO_MULTI",
Skonto1OffDay = 10,
Skonto1Percent = 3.0,
Skonto2OffDay = 30,
Skonto2Percent = 1.5
};
session.Save(condition);
session.Flush();
var daoSession = new DAOSession(session);
var sut = new AssetConditionBL(daoSession);
// Act
var result = sut.GetPaymentConditionSkontoInBR_DE_18Format(condition.I3D);
// Assert
Assert.Equal($"#SKONTO#TAGE=10#PROZENT=3.00#{Environment.NewLine}#SKONTO#TAGE=30#PROZENT=1.50#", result);
}
[Fact]
public void GetPaymentConditionSkontoInBR_DE_18Format_UsesInvariantCulture_WhenGermanCultureIsSet()
{
// Arrange
var originalCulture = Thread.CurrentThread.CurrentCulture;
try
{
Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE");
using var session = CreateSession();
var condition = new AssetCondition
{
ShortName = "SKONTO_DE",
Skonto1OffDay = 14,
Skonto1Percent = 2.75,
Skonto2OffDay = 0,
Skonto2Percent = 0
};
session.Save(condition);
session.Flush();
var daoSession = new DAOSession(session);
var sut = new AssetConditionBL(daoSession);
// Act
var result = sut.GetPaymentConditionSkontoInBR_DE_18Format(condition.I3D);
// Assert
Assert.Equal("#SKONTO#TAGE=14#PROZENT=2.75#", result);
Assert.DoesNotContain(",", result); // Should use dot, not comma
}
finally
{
Thread.CurrentThread.CurrentCulture = originalCulture;
}
}
[Fact]
public void GetPaymentConditionSkontoInBR_DE_18Format_ReturnsCorrectFormat_WhenOnlySkonto2HasData()
{
// Arrange
using var session = CreateSession();
var condition = new AssetCondition
{
ShortName = "SKONTO2_ONLY",
Skonto1OffDay = 0,
Skonto1Percent = 0,
Skonto2OffDay = 21,
Skonto2Percent = 1.25
};
session.Save(condition);
session.Flush();
var daoSession = new DAOSession(session);
var sut = new AssetConditionBL(daoSession);
// Act
var result = sut.GetPaymentConditionSkontoInBR_DE_18Format(condition.I3D);
// Assert
Assert.Equal("#SKONTO#TAGE=21#PROZENT=1.25#", result);
}
[Theory]
[InlineData(14, 0)] // Days without percent
[InlineData(0, 2.5)] // Percent without days
public void GetPaymentConditionSkontoInBR_DE_18Format_ReturnsNull_WhenIncompleteData(int days, double percent)
{
// Arrange
using var session = CreateSession();
var condition = new AssetCondition
{
ShortName = "INCOMPLETE",
Skonto1OffDay = days,
Skonto1Percent = percent,
Skonto2OffDay = 0,
Skonto2Percent = 0
};
session.Save(condition);
session.Flush();
var daoSession = new DAOSession(session);
var sut = new AssetConditionBL(daoSession);
// Act
var result = sut.GetPaymentConditionSkontoInBR_DE_18Format(condition.I3D);
// Assert
Assert.Null(result);
}
[Theory]
[InlineData(7, 0.5, "#SKONTO#TAGE=7#PROZENT=0.50#")]
[InlineData(5, 10.99, "#SKONTO#TAGE=5#PROZENT=10.99#")]
[InlineData(365, 99.99, "#SKONTO#TAGE=365#PROZENT=99.99#")]
public void GetPaymentConditionSkontoInBR_DE_18Format_HandlesEdgeCases_Correctly(int days, double percent, string expected)
{
// Arrange
using var session = CreateSession();
var condition = new AssetCondition
{
ShortName = "EDGE_CASE",
Skonto1OffDay = days,
Skonto1Percent = percent,
Skonto2OffDay = 0,
Skonto2Percent = 0
};
session.Save(condition);
session.Flush();
var daoSession = new DAOSession(session);
var sut = new AssetConditionBL(daoSession);
// Act
var result = sut.GetPaymentConditionSkontoInBR_DE_18Format(condition.I3D);
// Assert
Assert.Equal(expected, result);
}
private static ISession CreateSession()
{
var fluentConfig = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory())
.Mappings(m =>
{
m.FluentMappings.Add<AssetConditionMaps>();
});
var sessionFactory = fluentConfig.BuildSessionFactory();
var session = sessionFactory.OpenSession();
// Create schema in the in-memory database
new SchemaExport(fluentConfig.BuildConfiguration())
.Execute(false, true, false, session.Connection, null);
return session;
}
}
@@ -0,0 +1,42 @@
using System.Text.Json.Nodes;
using Centron.BusinessLogic.ArtificialIntelligence.Chat;
namespace Centron.Tests.BL.ArtificialIntelligence.Chat;
public class GoogleGeminiChatModelClientTest
{
[Fact]
public void CreateFunctionDeclaration_UsesFullJsonSchemaField()
{
const string jsonSchema =
"""
{
"type": "object",
"properties": {
"query": {
"type": ["string", "null"]
}
},
"additionalProperties": false
}
""";
var declaration = GoogleGeminiChatModelClient.CreateFunctionDeclaration(
new AiModelToolDescriptor
{
Name = "search_accounts",
Description = "Find accounts.",
JsonSchema = jsonSchema
});
Assert.Null(declaration["parameters"]);
var parameters = Assert.IsType<JsonObject>(declaration["parametersJsonSchema"]);
Assert.False(parameters["additionalProperties"]!.GetValue<bool>());
var properties = Assert.IsType<JsonObject>(parameters["properties"]);
var query = Assert.IsType<JsonObject>(properties["query"]);
var types = Assert.IsType<JsonArray>(query["type"]);
Assert.Equal("string", types[0]!.GetValue<string>());
Assert.Equal("null", types[1]!.GetValue<string>());
}
}
@@ -0,0 +1,37 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="JetBrains.Annotations" Version="2025.2.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\backend\Centron.BL\Centron.BL.csproj" />
<ProjectReference Include="..\..\..\src\backend\Centron.DAO\Centron.DAO.csproj" />
<ProjectReference Include="..\..\..\src\backend\Centron.Gateway\Centron.Gateway.csproj" />
<ProjectReference Include="..\..\..\src\webservice\Centron.Host\Centron.Host.csproj" />
<ProjectReference Include="..\Centron.Tests.DAO\Centron.Tests.DAO.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Xml.Linq;
using Centron.Data.Entities.DataExchange.BookKeeping.Export;
using Centron.Gateway.DataExchange.BookKeeping.Abacus;
using Centron.Interfaces.DataExchange.BookKeeping;
using NSubstitute;
namespace Centron.Tests.BL.DataExchange.BookKeeping.Abacus;
public class BookKeepingExportAbacusTest
{
[Theory]
[InlineData("2024.00")]
[InlineData("2023.00")]
[InlineData("2020.00")]
public void SupplierReceiptExport_DoesNotWriteTaxTransferTypeForModernKreditorVersions(string kreditorReceiptVersion)
{
var result = CreateExport(kreditorReceiptVersion);
var document = XDocument.Parse((string)result.File);
Assert.Empty(document.Descendants("TaxTransferType"));
Assert.Equal("KRED", document.Root?.Element("Task")?.Element("Parameter")?.Element("Application")?.Value);
Assert.Equal("Document", document.Root?.Element("Task")?.Element("Parameter")?.Element("Id")?.Value);
Assert.Equal(kreditorReceiptVersion, document.Root?.Element("Task")?.Element("Parameter")?.Element("Version")?.Value);
}
[Theory]
[InlineData("2018.00")]
[InlineData("2015.00")]
public void SupplierReceiptExport_WritesTaxTransferTypeForLegacyKreditorVersions(string kreditorReceiptVersion)
{
var result = CreateExport(kreditorReceiptVersion);
var document = XDocument.Parse((string)result.File);
var taxTransferType = Assert.Single(document.Descendants("TaxTransferType"));
Assert.Equal("0", taxTransferType.Value);
Assert.Equal(kreditorReceiptVersion, document.Root?.Element("Task")?.Element("Parameter")?.Element("Version")?.Value);
}
private static IBookKeepingReceiptExportFileGeneratorResult CreateExport(string kreditorReceiptVersion)
{
var sut = new BookKeepingExportAbacus();
var result = sut.GetReceiptsBookingData(
new[] { CreateSupplierReceipt() },
CreateSettings(kreditorReceiptVersion),
CreateUserSettings(),
customInterfaceSettings: null,
customInterfaceColumns: null);
Assert.True(result.IsSuccessful, result.Message);
Assert.Empty(result.FailedList);
return result;
}
private static BookKeepingExportConfiguration CreateSettings(string kreditorReceiptVersion)
{
return new BookKeepingExportConfiguration
{
KreditorReceiptVersion = kreditorReceiptVersion,
ReceiptsExportAllPositions = true
};
}
private static BookKeepingExportUserSettings CreateUserSettings()
{
return new BookKeepingExportUserSettings
{
ExportDateFrom = new DateTime(2026, 1, 1),
ExportDateTo = new DateTime(2026, 1, 31)
};
}
private static IBookKeepingReceipt CreateSupplierReceipt()
{
var receipt = Substitute.For<IBookKeepingReceipt>();
receipt.Number.Returns(4711);
receipt.Date.Returns(new DateTime(2026, 1, 15));
receipt.ExternalReceiptDate.Returns(new DateTime(2026, 1, 14));
receipt.ExternalReceiptNumber.Returns("EXT-4711");
receipt.Type.Returns(BookKeepingReceiptKind.SupplierInvoice);
receipt.AddressBookKeepingNumber.Returns("70001");
receipt.CurrencyISOCode.Returns("CHF");
receipt.PaymentTypeNumber.Returns("30");
receipt.BankNumber.Returns(1);
receipt.NetPriceComplete.Returns(100m);
receipt.TaxPriceComplete.Returns(8.1m);
receipt.NetPriceFCComplete.Returns(100m);
receipt.TaxPriceFCComplete.Returns(8.1m);
var item = CreateReceiptItem();
receipt.Items.Returns(new List<IBookKeepingReceiptItem> { item });
return receipt;
}
private static IBookKeepingReceiptItem CreateReceiptItem()
{
var item = Substitute.For<IBookKeepingReceiptItem>();
item.Position.Returns(1);
item.ProfitAndLossAccount.Returns(4000);
item.NetPriceTotalComplete.Returns(100m);
item.TaxPriceTotalComplete.Returns(8.1m);
item.NetPriceTotalFCComplete.Returns(100m);
item.TaxPriceTotalFCComplete.Returns(8.1m);
item.TaxRate.Returns(8.1m);
item.VATCode.Returns("81");
item.Text.Returns("Test position");
return item;
}
}
@@ -0,0 +1,432 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Centron.Data.Entities.DataExchange.BookKeeping.Export;
using Centron.Gateway.DataExchange.BookKeeping.DatevXMLOnline2020;
using Centron.Interfaces.BL;
using Centron.Interfaces.DataExchange.BookKeeping;
using CentronSoftware.Centron.WebServices.Entities.DataExchange.Exports;
using NSubstitute;
namespace Centron.Tests.BL.DataExchange.BookKeeping.DatevXMLOnline2020;
/// <summary>
/// Verifies that the new DATEV-Belegtransfer ApplicationSetting
/// <c>UseAccountFromDifferentInvoiceAddress</c> (Ticket 160066) is correctly honoured
/// in <see cref="BookKeepingExportDatevXmlOnline_2020.CreateReceivableLedger"/> and
/// has no effect on <see cref="BookKeepingExportDatevXmlOnline_2020.CreatePayableLedger"/>.
/// </summary>
public class BookKeepingExportDatevXmlOnline_2020Test
{
private const string CustomerAccount = "10000";
private const string AlternateInvoiceAccount = "20000";
private const string CustomerNumberAtSupplier = "ZZZ-99";
private const string CustomerName = "Customer GmbH";
private const string AlternateInvoiceName = "Alt Recipient AG";
private const string CustomerCity = "Berlin";
private const string AlternateInvoiceCity = "Hamburg";
// ---------- CreateReceivableLedger: gating of partyId ----------
[Fact]
public void Receivable_FlagTrue_AltSet_UsesAlternateAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(AlternateInvoiceAccount, result.Data.partyId);
}
[Fact]
public void Receivable_FlagFalse_AltSet_UsesCustomerAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: false);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerAccount, result.Data.partyId);
}
[Fact]
public void Receivable_FlagTrue_AltEmpty_FallsBackToCustomerAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: string.Empty);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerAccount, result.Data.partyId);
}
[Fact]
public void Receivable_FlagTrue_AltWhitespaceOnly_FallsBackToCustomerAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: " ");
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerAccount, result.Data.partyId);
}
[Fact]
public void Receivable_FlagFalse_NoAddressBookKeepingNumber_PartyIdIsNull()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: string.Empty,
altInvoiceAccount: null);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: false);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Null(result.Data.partyId);
}
// ---------- CreateReceivableLedger: name / city / bpAccountNo + bookingText follow the alternate recipient ----------
[Fact]
public void Receivable_FlagTrue_AltSet_UsesAlternateNameAndCityAndBpAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
altInvoiceName: AlternateInvoiceName,
altInvoiceCity: AlternateInvoiceCity);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(AlternateInvoiceName, result.Data.customerName);
Assert.Equal(AlternateInvoiceCity, result.Data.customerCity);
Assert.Equal(int.Parse(AlternateInvoiceAccount), result.Data.bpAccountNo);
}
[Fact]
public void Receivable_FlagFalse_AltSet_KeepsPrimaryNameCityBpAccount()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
altInvoiceName: AlternateInvoiceName,
altInvoiceCity: AlternateInvoiceCity);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: false);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerName, result.Data.customerName);
Assert.Equal(CustomerCity, result.Data.customerCity);
Assert.Equal(int.Parse(CustomerAccount), result.Data.bpAccountNo);
}
[Fact]
public void Receivable_FlagTrue_AltAccountEmpty_FallsBackForAllFields()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: string.Empty,
altInvoiceName: AlternateInvoiceName,
altInvoiceCity: AlternateInvoiceCity);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerName, result.Data.customerName);
Assert.Equal(CustomerCity, result.Data.customerCity);
Assert.Equal(int.Parse(CustomerAccount), result.Data.bpAccountNo);
}
[Fact]
public void Receivable_FlagTrue_AltAccountSet_AltNameAndCityEmpty_FallsBackForNameAndCity()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
altInvoiceName: null,
altInvoiceCity: "");
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerName, result.Data.customerName);
Assert.Equal(CustomerCity, result.Data.customerCity);
Assert.Equal(int.Parse(AlternateInvoiceAccount), result.Data.bpAccountNo);
Assert.Equal(AlternateInvoiceAccount, result.Data.partyId);
}
[Fact]
public void Receivable_FlagTrue_BookingTextUsesAlternateName()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
altInvoiceName: AlternateInvoiceName,
altInvoiceCity: AlternateInvoiceCity);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true, useIndividualBookingText: false);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Contains(AlternateInvoiceName, result.Data.bookingText);
Assert.DoesNotContain(CustomerName, result.Data.bookingText);
}
[Fact]
public void Receivable_FlagFalse_BookingTextUsesPrimaryName()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
altInvoiceName: AlternateInvoiceName,
altInvoiceCity: AlternateInvoiceCity);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: false, useIndividualBookingText: false);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Contains(CustomerName, result.Data.bookingText);
Assert.DoesNotContain(AlternateInvoiceName, result.Data.bookingText);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Receivable_EmptyIndividualBookingText_UsesDefaultBookingText(string? bookingText)
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: null);
var settings = CreateSettings(
useAccountFromDifferentInvoiceAddress: false,
useIndividualBookingText: true,
individualBookingText: bookingText);
var result = sut.CreateReceivableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerName + " - 1", result.Data.bookingText);
}
// ---------- CreatePayableLedger: regression — flag must not affect supplier path ----------
[Theory]
[InlineData(true)]
[InlineData(false)]
public void Payable_FlagToggle_DoesNotChangePartyId_AlwaysAddressBookKeepingNumber(bool flag)
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateSupplierReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
customerNumberAtSupplier: null);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: flag);
var result = sut.CreatePayableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerAccount, result.Data.partyId);
}
[Fact]
public void Payable_CustomerNumberAtSupplier_OverridesAddressBookKeepingNumber()
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateSupplierReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: AlternateInvoiceAccount,
customerNumberAtSupplier: CustomerNumberAtSupplier);
var settings = CreateSettings(useAccountFromDifferentInvoiceAddress: true);
var result = sut.CreatePayableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal(CustomerNumberAtSupplier, result.Data.partyId);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Payable_EmptyIndividualBookingText_UsesDefaultBookingText(string? bookingText)
{
var sut = new BookKeepingExportDatevXmlOnline_2020();
var receipt = CreateSupplierReceipt(addressBookKeepingNumber: CustomerAccount,
altInvoiceAccount: null,
customerNumberAtSupplier: null);
var settings = CreateSettings(
useAccountFromDifferentInvoiceAddress: false,
useIndividualBookingText: true,
individualBookingText: bookingText);
var result = sut.CreatePayableLedger(CreateSplit(), splitCostCentre: null, receipt, settings);
Assert.True(result.IsSuccess);
Assert.Equal("Supplier GmbH - EXT-1", result.Data.bookingText);
}
[Fact]
public void Split_UsesItemServicePeriodToAsDeliveryDate_WhenItemServicePeriodIsSingleDate()
{
var (receipt, item) = CreateReceiptForSplit();
item.ServicePeriodFrom.Returns(new DateTime(2026, 6, 2, 10, 0, 0));
item.ServicePeriodTo.Returns(new DateTime(2026, 6, 2, 13, 30, 0));
var split = Assert.Single(GetSplits(receipt));
Assert.Equal(new DateTime(2026, 6, 2), split.DeliveryDate);
}
[Fact]
public void Split_UsesReceiptServicePeriodToAsDeliveryDate_WhenItemServicePeriodIsEmpty()
{
var (receipt, item) = CreateReceiptForSplit();
item.DeliveryDate.Returns((DateTime?)null);
receipt.ServicePeriodFrom.Returns(new DateTime(2026, 6, 3, 8, 0, 0));
receipt.ServicePeriodTo.Returns(new DateTime(2026, 6, 3, 18, 0, 0));
var split = Assert.Single(GetSplits(receipt));
Assert.Equal(new DateTime(2026, 6, 3), split.DeliveryDate);
}
[Fact]
public void Split_UsesItemDeliveryDate_WhenItemServicePeriodIsRange()
{
var (receipt, item) = CreateReceiptForSplit();
item.ServicePeriodFrom.Returns(new DateTime(2026, 6, 2, 10, 0, 0));
item.ServicePeriodTo.Returns(new DateTime(2026, 6, 3, 13, 30, 0));
item.DeliveryDate.Returns(new DateTime(2026, 6, 6));
receipt.ServicePeriodFrom.Returns(new DateTime(2026, 6, 4, 8, 0, 0));
receipt.ServicePeriodTo.Returns(new DateTime(2026, 6, 4, 18, 0, 0));
var split = Assert.Single(GetSplits(receipt));
Assert.Equal(new DateTime(2026, 6, 3), split.DeliveryDate);
}
// ---------- helpers ----------
private static BookKeepingExportAccountSplitDTO CreateSplit() => new()
{
Account = 8400,
Amount = 100m,
TaxAmount = 19m,
AmountFC = 100m,
TaxAmountFC = 19m,
TaxRate = 19m,
TaxId = "9"
};
private static DatevOnlineExportSettingsDTO CreateSettings(bool useAccountFromDifferentInvoiceAddress, bool useIndividualBookingText = true, string? individualBookingText = "Test") => new()
{
UseAccountFromDifferentInvoiceAddress = useAccountFromDifferentInvoiceAddress,
ExportCaption = false,
ExportDeliveryDate = false,
// Use individual booking texts to keep CreateBookingText on the explicit-template branch — safer with mocks.
UseIndividualBookingTextForCustomerReceipts = useIndividualBookingText,
IndividualBookingTextForCustomerReceipts = individualBookingText,
UseIndividualBookingTextForSupplierReceipts = useIndividualBookingText,
IndividualBookingTextForSupplierReceipts = individualBookingText
};
private static IList<BookKeepingExportAccountSplitDTO> GetSplits(IBookKeepingReceipt receipt)
{
var helperType = typeof(BookKeepingExportDatevXmlOnline_2020).Assembly.GetType("Centron.Gateway.DataExchange.BookKeeping.BookKeepingExportHelper");
Assert.NotNull(helperType);
var helper = helperType.GetProperty("Instance", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
Assert.NotNull(helper);
var method = helperType.GetMethod("GetPositionsThroughSplit", BindingFlags.Public | BindingFlags.Instance);
Assert.NotNull(method);
var result = (Result<IList<BookKeepingExportAccountSplitDTO>>)method.Invoke(helper, new object[]
{
receipt,
new BookKeepingExportConfiguration { ExportPerformanceDate = true },
false,
false
})!;
Assert.True(result.IsSuccess, result.Message);
return result.Data;
}
private static (IBookKeepingReceipt Receipt, IBookKeepingReceiptItem Item) CreateReceiptForSplit()
{
var receipt = CreateCustomerReceipt(addressBookKeepingNumber: CustomerAccount, altInvoiceAccount: null);
receipt.NetPriceComplete.Returns(100m);
receipt.TaxPriceComplete.Returns(19m);
receipt.NetPriceFCComplete.Returns(100m);
receipt.TaxPriceFCComplete.Returns(19m);
var item = Substitute.For<IBookKeepingReceiptItem>();
item.ProfitAndLossAccount.Returns(8400);
item.TaxRate.Returns(19m);
item.VATCode.Returns("9");
item.DeliveryDate.Returns(new DateTime(2026, 6, 4));
item.NetPriceTotalComplete.Returns(100m);
item.TaxPriceTotalComplete.Returns(19m);
item.NetPriceTotalFCComplete.Returns(100m);
item.TaxPriceTotalFCComplete.Returns(19m);
receipt.Items.Returns(new List<IBookKeepingReceiptItem> { item });
return (receipt, item);
}
private static IBookKeepingReceipt CreateCustomerReceipt(string addressBookKeepingNumber, string? altInvoiceAccount,
string? altInvoiceName = null, string? altInvoiceCity = null,
string addressCity = CustomerCity)
{
var r = Substitute.For<IBookKeepingReceipt>();
r.Number.Returns(1);
r.Date.Returns(new DateTime(2026, 1, 15));
r.Type.Returns(BookKeepingReceiptKind.Invoice);
r.CurrencyISOCode.Returns("EUR");
r.CurrencyFactor.Returns(1.0);
r.AddressName.Returns(CustomerName);
r.AddressCity.Returns(addressCity);
r.AddressBookKeepingNumber.Returns(addressBookKeepingNumber);
r.BookKeepingNumberDifferentInvoiceAddress.Returns(altInvoiceAccount);
r.AddressNameDifferentInvoiceAddress.Returns(altInvoiceName);
r.AddressCityDifferentInvoiceAddress.Returns(altInvoiceCity);
return r;
}
private static IBookKeepingReceipt CreateSupplierReceipt(string addressBookKeepingNumber, string? altInvoiceAccount, string? customerNumberAtSupplier)
{
var r = Substitute.For<IBookKeepingReceipt>();
r.Number.Returns(1);
r.Date.Returns(new DateTime(2026, 1, 15));
r.ExternalReceiptDate.Returns(new DateTime(2026, 1, 14));
r.ExternalReceiptNumber.Returns("EXT-1");
r.Type.Returns(BookKeepingReceiptKind.SupplierInvoice);
r.CurrencyISOCode.Returns("EUR");
r.CurrencyFactor.Returns(1.0);
r.AddressName.Returns("Supplier GmbH");
r.AddressCity.Returns("Berlin");
r.AddressBookKeepingNumber.Returns(addressBookKeepingNumber);
r.BookKeepingNumberDifferentInvoiceAddress.Returns(altInvoiceAccount);
r.CustomerNumberAtSupplier.Returns(customerNumberAtSupplier);
return r;
}
}
@@ -0,0 +1,291 @@
using System;
using System.Collections.Generic;
using Centron.BusinessLogic.DataExchange.Connectors;
using Centron.DAO;
using Centron.DAO.Mappings.DataExchange.Connectors;
using Centron.Interfaces.BL;
using CentronSoftware.Centron.WebServices.Entities.DataExchange.Connectors;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
using Xunit;
namespace Centron.Tests.BL.DataExchange.Connectors;
/// <summary>
/// Integration-style tests for <see cref="DocBeeTicketTemplateBL"/> using SQLite in-memory and
/// <see cref="DocBeeTicketTemplateMaps"/> only.
/// </summary>
[TestSubject(typeof(DocBeeTicketTemplateBL))]
public class DocBeeTicketTemplateBLTest
{
[Fact]
public void GetAllTemplates_WhenEmpty_ReturnsSuccessWithEmptyList()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var result = bl.GetAllTemplates();
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.Empty(result.Data);
}
[Fact]
public void SyncTemplates_WhenNull_ReturnsError()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var result = bl.SyncTemplates(null!);
Assert.Equal(ResultStatus.Error, result.Status);
Assert.Contains("Templates list is required", result.Message, StringComparison.Ordinal);
}
[Fact]
public void SyncTemplates_WhenCaptionMissing_ReturnsError()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var result = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "t1", Caption = null, Info = null }
});
Assert.Equal(ResultStatus.Error, result.Status);
Assert.Contains("t1", result.Message, StringComparison.Ordinal);
Assert.Contains("Caption is required", result.Message, StringComparison.Ordinal);
}
[Fact]
public void SyncTemplates_InsertsNewRows_AndGetReturnsOrderedByCaption()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var sync = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "id-z", Caption = "Zebra", Info = "i1" },
new() { TicketTemplateID = "id-a", Caption = "Alpha", Info = "i2" }
});
Assert.Equal(ResultStatus.Success, sync.Status);
Assert.Equal(2, sync.Data);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
Assert.Equal(ResultStatus.Success, get.Status);
Assert.NotNull(get.Data);
Assert.Equal(2, get.Data.Count);
Assert.Equal("Alpha", get.Data[0].Caption);
Assert.Equal("id-a", get.Data[0].TicketTemplateID);
Assert.Equal("Zebra", get.Data[1].Caption);
Assert.Equal("id-z", get.Data[1].TicketTemplateID);
}
[Fact]
public void SyncTemplates_UpdatesCaptionAndInfo_WhenIdExists()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "same", Caption = "OldCap", Info = "OldInfo" }
});
session.Flush();
session.Clear();
var sync2 = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "same", Caption = "NewCap", Info = "NewInfo" }
});
Assert.Equal(ResultStatus.Success, sync2.Status);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
var row = Assert.Single(get.Data!);
Assert.Equal("same", row.TicketTemplateID);
Assert.Equal("NewCap", row.Caption);
Assert.Equal("NewInfo", row.Info);
}
[Fact]
public void SyncTemplates_RemovesRowsNotInIncomingList()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "keep", Caption = "K", Info = null },
new() { TicketTemplateID = "drop", Caption = "D", Info = null }
});
session.Flush();
session.Clear();
var sync2 = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "keep", Caption = "K", Info = null }
});
Assert.Equal(ResultStatus.Success, sync2.Status);
Assert.Equal(1, sync2.Data);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
var row = Assert.Single(get.Data!);
Assert.Equal("keep", row.TicketTemplateID);
}
[Fact]
public void SyncTemplates_EmptyList_RemovesAllTemplates()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "a", Caption = "A", Info = null },
new() { TicketTemplateID = "b", Caption = "B", Info = null }
});
session.Flush();
session.Clear();
var sync2 = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>());
Assert.Equal(ResultStatus.Success, sync2.Status);
Assert.Equal(0, sync2.Data);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
Assert.Empty(get.Data!);
}
[Fact]
public void SyncTemplates_ClearsReferencesInArticleAndMaterialGroups_WhenTemplateRemoved()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "keep", Caption = "Keep", Info = null },
new() { TicketTemplateID = "drop", Caption = "Drop", Info = null }
});
session.Flush();
session.Clear();
session.CreateSQLQuery("INSERT INTO ARTIK (I3D, DocBeeTicketTemplateID) VALUES (1, 'drop')").ExecuteUpdate();
session.CreateSQLQuery("INSERT INTO ARTIK (I3D, DocBeeTicketTemplateID) VALUES (2, 'keep')").ExecuteUpdate();
session.CreateSQLQuery("INSERT INTO WAREN (I3D, DocBeeTicketTemplateID) VALUES (1, 'drop')").ExecuteUpdate();
session.CreateSQLQuery("INSERT INTO UNTERWAREN (I3D, DocBeeTicketTemplateID) VALUES (1, 'drop')").ExecuteUpdate();
var sync2 = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "keep", Caption = "Keep", Info = null }
});
Assert.Equal(ResultStatus.Success, sync2.Status);
session.Flush();
session.Clear();
var artikDropRef = session.CreateSQLQuery("SELECT DocBeeTicketTemplateID FROM ARTIK WHERE I3D = 1").UniqueResult();
Assert.Null(artikDropRef);
var artikKeepRef = (string)session.CreateSQLQuery("SELECT DocBeeTicketTemplateID FROM ARTIK WHERE I3D = 2").UniqueResult();
Assert.Equal("keep", artikKeepRef);
var warenRef = session.CreateSQLQuery("SELECT DocBeeTicketTemplateID FROM WAREN WHERE I3D = 1").UniqueResult();
Assert.Null(warenRef);
var unterwarenRef = session.CreateSQLQuery("SELECT DocBeeTicketTemplateID FROM UNTERWAREN WHERE I3D = 1").UniqueResult();
Assert.Null(unterwarenRef);
}
[Fact]
public void SyncTemplates_SkipsNullOrWhitespaceTicketTemplateID()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var sync = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = "valid", Caption = "V", Info = null },
new() { TicketTemplateID = " ", Caption = "X", Info = null },
null!
});
Assert.True(
sync.Status == ResultStatus.Success,
$"Expected Success, got {sync.Status}: {sync.Message}");
Assert.Equal(1, sync.Data);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
var row = Assert.Single(get.Data!);
Assert.Equal("valid", row.TicketTemplateID);
}
[Fact]
public void SyncTemplates_TrimsTicketTemplateIdAndCaption()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var bl = new DocBeeTicketTemplateBL(daoSession);
var sync = bl.SyncTemplates(new List<DocBeeTicketTemplateDTO>
{
new() { TicketTemplateID = " id1 ", Caption = " cap ", Info = " info " }
});
Assert.Equal(ResultStatus.Success, sync.Status);
session.Flush();
session.Clear();
var get = bl.GetAllTemplates();
var row = Assert.Single(get.Data!);
Assert.Equal("id1", row.TicketTemplateID);
Assert.Equal("cap", row.Caption);
Assert.Equal("info", row.Info);
}
private static ISession CreateSession()
{
var fluentConfig = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<DocBeeTicketTemplateMaps>());
var sessionFactory = fluentConfig.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfig.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
session.CreateSQLQuery("CREATE TABLE IF NOT EXISTS ARTIK (I3D INTEGER PRIMARY KEY AUTOINCREMENT, DocBeeTicketTemplateID TEXT)").ExecuteUpdate();
session.CreateSQLQuery("CREATE TABLE IF NOT EXISTS WAREN (I3D INTEGER PRIMARY KEY AUTOINCREMENT, DocBeeTicketTemplateID TEXT)").ExecuteUpdate();
session.CreateSQLQuery("CREATE TABLE IF NOT EXISTS UNTERWAREN (I3D INTEGER PRIMARY KEY AUTOINCREMENT, DocBeeTicketTemplateID TEXT)").ExecuteUpdate();
return session;
}
}
@@ -0,0 +1,78 @@
using System;
using System.Collections.Generic;
using Centron.BusinessLogic.WebServices.DataExchange.Connectors;
using Centron.DAO;
using Centron.DAO.Mappings.DataExchange.Connectors;
using Centron.Data.Entities.Administration.Logins;
using Centron.Interfaces.BL;
using CentronSoftware.Centron.WebServices.Entities.DataExchange.Connectors;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
using Xunit;
namespace Centron.Tests.BL.DataExchange.Connectors;
/// <summary>
/// Guard tests for <see cref="DocBeeTicketTemplateWebServiceBL"/> (no full rights/DB setup for SETTINGS).
/// </summary>
[TestSubject(typeof(DocBeeTicketTemplateWebServiceBL))]
public class DocBeeTicketTemplateWebServiceBLTest
{
[Fact]
public void SyncTemplates_WhenCurrentUserNull_ReturnsError()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var wsbl = new DocBeeTicketTemplateWebServiceBL(daoSession);
var result = wsbl.SyncTemplates(new List<DocBeeTicketTemplateDTO>(), null!);
Assert.Equal(ResultStatus.Error, result.Status);
Assert.Contains("User information is required", result.Message, StringComparison.Ordinal);
}
[Fact]
public void SyncTemplates_WhenUserNull_ReturnsError()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var wsbl = new DocBeeTicketTemplateWebServiceBL(daoSession);
var loggedIn = new LoggedInUser(appUserI3D: null, user: null);
var result = wsbl.SyncTemplates(new List<DocBeeTicketTemplateDTO>(), loggedIn);
Assert.Equal(ResultStatus.Error, result.Status);
Assert.Contains("User does not have permission", result.Message, StringComparison.Ordinal);
}
[Fact]
public void GetAllTemplates_DelegatesToBl_EmptyDatabase()
{
using var session = CreateSession();
var daoSession = new DAOSession(session);
var wsbl = new DocBeeTicketTemplateWebServiceBL(daoSession);
var result = wsbl.GetAllTemplates();
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.Empty(result.Data);
}
private static ISession CreateSession()
{
var fluentConfig = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<DocBeeTicketTemplateMaps>());
var sessionFactory = fluentConfig.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfig.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
return session;
}
}
@@ -0,0 +1,311 @@
using System.Linq;
using Centron.BusinessLogic.DataExchange.EDI;
using Centron.BusinessLogic.DataExchange.EDI.SaleInvoices;
using Centron.Data.Entities.Sales.Receipts.Invoices;
using Centron.Interfaces.DataExchange.BookKeeping;
using JetBrains.Annotations;
namespace Centron.Tests.BL.DataExchange.EDI.SaleInvoices;
/// <summary>
/// Tests for the contract-contingent balance merge in <see cref="InvoiceZugferdBL.MergeBalanceCompensationItems"/>.
/// Service and compensation items share a <see cref="ZugferdExportPositionItem.BalanceID"/>; the merge folds the
/// compensation amount into the service item and removes the compensation line from the export.
/// On invoices the service item is positive and the compensation is negative; credit vouchers store the same
/// pair with inverted signs (Ticket 164110 — without the sign-aware merge the wrong line would be removed
/// and the position total would no longer match the receipt header).
/// </summary>
[TestSubject(typeof(InvoiceZugferdBL))]
public class InvoiceZugferdBalanceMergeTests
{
private const string BalanceA = "BAL-A";
private const string BalanceB = "BAL-B";
private static ZugferdExportPositionItem Service(string balanceId, decimal netTotal, decimal netUnit, decimal quantity)
=> new()
{
BalanceID = balanceId,
NetPriceTotalFC = netTotal,
NetPrice = netUnit,
Quantity = quantity,
TaxPriceTotalFC = 0m,
ArticleCode = "SERPELISTD",
Text = "Servicedienstleistung",
};
private static ZugferdExportPositionItem Compensation(string balanceId, decimal netTotal)
=> new()
{
BalanceID = balanceId,
NetPriceTotalFC = netTotal,
NetPrice = -netTotal,
Quantity = 0m,
TaxPriceTotalFC = 0m,
ArticleCode = "AUSGLEICH",
Text = "Ausgleichsartikel für Kontingente",
};
private static ZugferdExportItem ItemWith(params ZugferdExportPositionItem[] positions)
=> new() { Positions = positions.ToList() };
[Fact]
public void Invoice_FullyCoveredByContingent_ReducesServiceToZeroAndKeepsBasisQuantity()
{
var service = Service(BalanceA, netTotal: 150m, netUnit: 150m, quantity: 1m);
var compensation = Compensation(BalanceA, netTotal: -150m);
var export = ItemWith(service, compensation);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
Assert.Single(export.Positions);
Assert.Same(service, export.Positions[0]);
Assert.Equal(0m, service.NetPriceTotalFC);
Assert.Equal(0m, service.Quantity);
Assert.Equal(1m, service.BasisQuantity);
}
[Fact]
public void Invoice_PartiallyCovered_ReducesServiceProportionally()
{
var service = Service(BalanceA, netTotal: 150m, netUnit: 150m, quantity: 1m);
var compensation = Compensation(BalanceA, netTotal: -90m);
var export = ItemWith(service, compensation);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
Assert.Single(export.Positions);
Assert.Equal(60m, service.NetPriceTotalFC);
Assert.Equal(0.4m, service.Quantity);
Assert.Equal(1m, service.BasisQuantity);
}
[Fact]
public void CreditVoucher_FullyCoveredByContingent_ReducesServiceToZeroAndKeepsRealServiceLine()
{
// Credit voucher: signs are inverted compared to the source invoice
var service = Service(BalanceA, netTotal: -150m, netUnit: -150m, quantity: 1m);
var compensation = Compensation(BalanceA, netTotal: 150m);
var export = ItemWith(service, compensation);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.CreditVoucher);
Assert.Single(export.Positions);
Assert.Same(service, export.Positions[0]);
Assert.Equal("SERPELISTD", export.Positions[0].ArticleCode);
Assert.Equal(0m, service.NetPriceTotalFC);
Assert.Equal(0m, service.Quantity);
Assert.Equal(1m, service.BasisQuantity);
}
[Fact]
public void CreditVoucher_PartiallyCovered_ReducesServiceProportionally()
{
var service = Service(BalanceA, netTotal: -150m, netUnit: -150m, quantity: 1m);
var compensation = Compensation(BalanceA, netTotal: 90m);
var export = ItemWith(service, compensation);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.CreditVoucher);
Assert.Single(export.Positions);
Assert.Equal(-60m, service.NetPriceTotalFC);
Assert.Equal(0.4m, service.Quantity);
Assert.Equal(1m, service.BasisQuantity);
}
[Fact]
public void MultipleBalanceGroups_AreMergedIndependently()
{
var service1 = Service(BalanceA, netTotal: 150m, netUnit: 150m, quantity: 1m);
var compensation1 = Compensation(BalanceA, netTotal: -150m);
var service2 = Service(BalanceB, netTotal: 200m, netUnit: 100m, quantity: 2m);
var compensation2 = Compensation(BalanceB, netTotal: -100m);
var export = ItemWith(service1, compensation1, service2, compensation2);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
Assert.Equal(2, export.Positions.Count);
Assert.Equal(0m, service1.NetPriceTotalFC);
Assert.Equal(0m, service1.Quantity);
Assert.Equal(100m, service2.NetPriceTotalFC);
Assert.Equal(1m, service2.Quantity);
Assert.DoesNotContain(compensation1, export.Positions);
Assert.DoesNotContain(compensation2, export.Positions);
}
[Fact]
public void PositionsWithoutBalanceId_AreLeftUntouched()
{
var standalone = new ZugferdExportPositionItem
{
BalanceID = null,
NetPriceTotalFC = 99m,
NetPrice = 99m,
Quantity = 1m,
ArticleCode = "OTHER",
};
var export = ItemWith(standalone);
InvoiceZugferdBL.MergeBalanceCompensationItems(export, BookKeepingReceiptKind.Invoice);
Assert.Single(export.Positions);
Assert.Same(standalone, export.Positions[0]);
Assert.Equal(99m, standalone.NetPriceTotalFC);
Assert.Equal(1m, standalone.Quantity);
Assert.Null(standalone.BasisQuantity);
}
[Fact]
public void ResolveServicePeriod_ItemPeriodWinsBeforeReceiptHeadAndFallback()
{
var receipt = new ReceiptInvoice
{
ServicePeriodFrom = new System.DateTime(2024, 1, 1),
ServicePeriodTo = new System.DateTime(2024, 1, 31),
};
var item = new ReceiptInvoiceItem
{
I3D = 10,
ServicePeriodFrom = new System.DateTime(2024, 2, 2, 14, 0, 0),
ServicePeriodTo = new System.DateTime(2024, 2, 4, 16, 0, 0),
};
var fallbackCalled = false;
var result = InvoiceZugferdBL.ResolveServicePeriod(
receipt,
item,
BookKeepingReceiptKind.Invoice,
(_, _) =>
{
fallbackCalled = true;
return (new System.DateTime(2024, 3, 1), new System.DateTime(2024, 3, 31));
});
Assert.Equal(new System.DateTime(2024, 2, 2), result.From);
Assert.Equal(new System.DateTime(2024, 2, 4), result.To);
Assert.False(fallbackCalled);
}
[Fact]
public void ResolveServicePeriod_ReceiptHeadWinsBeforeFallback()
{
var receipt = new ReceiptInvoice
{
ServicePeriodFrom = new System.DateTime(2024, 1, 1, 9, 0, 0),
ServicePeriodTo = new System.DateTime(2024, 1, 31, 18, 0, 0),
};
var item = new ReceiptInvoiceItem { I3D = 11 };
var fallbackCalled = false;
var result = InvoiceZugferdBL.ResolveServicePeriod(
receipt,
item,
BookKeepingReceiptKind.Invoice,
(_, _) =>
{
fallbackCalled = true;
return (new System.DateTime(2024, 3, 1), new System.DateTime(2024, 3, 31));
});
Assert.Equal(new System.DateTime(2024, 1, 1), result.From);
Assert.Equal(new System.DateTime(2024, 1, 31), result.To);
Assert.False(fallbackCalled);
}
[Fact]
public void ResolveServicePeriod_UsesContractFallbackWhenItemAndHeadAreEmpty()
{
var receipt = new ReceiptInvoice();
var item = new ReceiptInvoiceItem { I3D = 12 };
var result = InvoiceZugferdBL.ResolveServicePeriod(
receipt,
item,
BookKeepingReceiptKind.Invoice,
(itemI3D, receiptKind) =>
{
Assert.Equal(12, itemI3D);
Assert.Equal(BookKeepingReceiptKind.Invoice, receiptKind);
return (new System.DateTime(2024, 3, 1), new System.DateTime(2024, 3, 31));
});
Assert.Equal(new System.DateTime(2024, 3, 1), result.From);
Assert.Equal(new System.DateTime(2024, 3, 31), result.To);
}
[Fact]
public void ApplyHeaderBillingPeriodFromPositions_UsesMinMaxAndKeepsSameDayPeriod()
{
var export = ItemWith(
new ZugferdExportPositionItem
{
BillingPeriodFrom = new System.DateTime(2024, 2, 15),
BillingPeriodTo = new System.DateTime(2024, 2, 15),
},
new ZugferdExportPositionItem
{
BillingPeriodFrom = new System.DateTime(2024, 1, 1),
BillingPeriodTo = new System.DateTime(2024, 3, 31),
});
InvoiceZugferdBL.ApplyHeaderBillingPeriodFromPositions(export);
Assert.Equal(new System.DateTime(2024, 1, 1), export.BillingPeriodFrom);
Assert.Equal(new System.DateTime(2024, 3, 31), export.BillingPeriodTo);
}
[Fact]
public void IsSingleServicePeriodDate_ReturnsTrue_WhenDatesAreEqualIgnoringTime()
{
var isSingleDate = InvoiceZugferdBL.IsSingleServicePeriodDate(
new System.DateTime(2026, 2, 6, 10, 0, 0),
new System.DateTime(2026, 2, 6, 13, 30, 0));
Assert.True(isSingleDate);
}
[Fact]
public void GetHeaderDeliveryDate_UsesServicePeriodTo_WhenHeaderPeriodIsSingleDate()
{
var exportItem = ItemWith();
exportItem.ReceiptDate = new System.DateTime(2026, 2, 1);
exportItem.DeliveryDate = new System.DateTime(2026, 2, 4);
exportItem.BillingPeriodFrom = new System.DateTime(2026, 2, 6, 10, 0, 0);
exportItem.BillingPeriodTo = new System.DateTime(2026, 2, 6, 13, 30, 0);
var deliveryDate = InvoiceZugferdBL.GetHeaderDeliveryDate(exportItem);
Assert.Equal(new System.DateTime(2026, 2, 6), deliveryDate);
}
[Fact]
public void DoCreateActualDeliverySupplyChainEvent_UsesOnlyDeliveryDateWhenPeriodDatesAreEqual()
{
var document = new System.Xml.XmlDocument();
var node = InvoiceZugferdBL.DoCreateActualDeliverySupplyChainEvent(
new System.DateTime(2026, 2, 6, 13, 30, 0),
document);
var occurrenceDateTimeNode = Assert.Single(node.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "OccurrenceDateTime");
var dateTimeStringNode = Assert.Single(occurrenceDateTimeNode.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "DateTimeString");
Assert.Equal("102", dateTimeStringNode.Attributes?["format"]?.Value);
Assert.Equal("20260206", dateTimeStringNode.InnerText);
}
[Fact]
public void DoCreateBillingSpecifiedPeriod_UsesStartAndEndDateWhenDatesDiffer()
{
var document = new System.Xml.XmlDocument();
var node = InvoiceZugferdBL.DoCreateBillingSpecifiedPeriod(
new System.DateTime(2026, 2, 6, 10, 0, 0),
new System.DateTime(2026, 2, 7, 13, 30, 0),
document);
var startDateTimeNode = Assert.Single(node.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "StartDateTime");
var endDateTimeNode = Assert.Single(node.ChildNodes.Cast<System.Xml.XmlNode>(), f => f.LocalName == "EndDateTime");
Assert.Equal("20260206", startDateTimeNode.InnerText);
Assert.Equal("20260207", endDateTimeNode.InnerText);
}
}
@@ -0,0 +1,40 @@
using Centron.Data.Entities.Accounts;
using FluentNHibernate.Mapping;
namespace Centron.Tests.BL.DatabaseMappings
{
/// <summary>
/// <b>IMPORTANT</b>: This is a replacement class for the real <see cref="Centron.DAO.Mappings.Accounts.AccountSearchItemAccMaps"/> class for running in-memory unit tests.
/// The real mapping maps to a database view (cvw_AccountSearchAcc) which cannot be created in an in-memory SQLite database.
/// This test mapping creates a simple table with only the columns needed for the tests.
/// Make sure to keep this class in sync with the columns needed for your tests.
/// </summary>
public class AccountSearchItemAccMapsForTest : ClassMap<AccountSearchItemAcc>
{
public AccountSearchItemAccMapsForTest()
{
// Mark as read-only like the production mapping
ReadOnly();
// Use a simple table name instead of the view name
Table("AccountSearchItemAccTest");
// Map RowNumber as the ID (same as production mapping)
// Using Assigned generator since we won't be inserting via NHibernate
Id(m => m.RowNumber).Column("RowNumber").GeneratedBy.Assigned();
// Map AccountI3D as I3D (this is how it's queried in SetAccountReferenceProperties)
// This is the key property that the business logic queries by
Map(m => m.I3D).Column("AccountI3D");
// Map the columns used in SetAccountReferenceProperties
Map(m => m.CustomerNumber).Column("CustomerNumber").Nullable();
Map(m => m.SupplierNumber).Column("SupplierNumber").Nullable();
// Additional commonly used columns - add more as needed for tests
Map(m => m.AccountNumber).Column("AccountNumber").Nullable();
Map(m => m.AccountName).Column("AccountName").Nullable();
Map(m => m.IsActive).Column("IsActive").Nullable();
}
}
}
@@ -0,0 +1,28 @@
using Centron.Data.Entities.Accounts;
using FluentNHibernate.Mapping;
namespace Centron.Tests.BL.DatabaseMappings
{
/// <summary>
/// <b>IMPORTANT</b>: This is a replacement class for the real <see cref="Centron.DAO.Mappings.Accounts.AccountSupplierMaps"/> class for running in-memory unit tests.
/// This test mapping includes only the columns needed for the tests.
/// Make sure to keep this class in sync with the columns needed for your tests.
/// </summary>
public class AccountSupplierMapsForTest : ClassMap<AccountSupplier>
{
public AccountSupplierMapsForTest()
{
Table("AccountSuppliers");
Id(m => m.I3D).Column("I3D").GeneratedBy.Assigned();
Map(m => m.Number).Column("Number");
// Basic columns that might be needed for tests
Map(m => m.BookKeepingNumber).Column("BookKeepingNumber").Length(64).Nullable();
Map(m => m.OwnCustomerNumber).Column("OwnCustomerNumber").Length(20).Nullable();
// Note: Other columns and reference mappings are intentionally omitted
// for test purposes to keep the mapping simple
}
}
}
@@ -0,0 +1,27 @@
using Centron.Data.Entities.Accounts;
using FluentNHibernate.Mapping;
namespace Centron.Tests.BL.DatabaseMappings
{
/// <summary>
/// <b>IMPORTANT</b>: This is a replacement class for the real <see cref="Centron.DAO.Mappings.Accounts.AccountTypeToAccountMaps"/> class for running in-memory unit tests.
/// This test mapping includes only the columns needed for the tests, without the reference mappings that would cause issues.
/// Make sure to keep this class in sync with the columns needed for your tests.
/// </summary>
public class AccountTypeToAccountMapsForTest : ClassMap<AccountTypeToAccount>
{
public AccountTypeToAccountMapsForTest()
{
Table("AccountTypeToAccounts");
Id(m => m.I3D).Column("I3D").GeneratedBy.Assigned();
Map(m => m.AccountTypeI3D).Column("AccountTypeI3D");
Map(m => m.AccountI3D).Column("AccountI3D");
Map(m => m.AccountCustomerI3D).Column("AccountCustomerI3D").Nullable();
Map(m => m.AccountSupplierI3D).Column("AccountSupplierI3D").Nullable();
// Note: Reference mappings (AccountType, CustomerData, SupplierData) are intentionally
// omitted for test purposes to avoid dependency on additional entity mappings
}
}
}
@@ -0,0 +1,246 @@
using Centron.DAO.Mappings.CustomerArea;
using Centron.DAO.UserTypes;
using Centron.Data.Entities.CustomerArea;
using Centron.Data.Entities.ObjectTypes;
using Centron.Interfaces.CustomerArea;
using Centron.Interfaces.Sales.CustomerAssets;
namespace Centron.Tests.BL.DatabaseMappings
{
/// <summary>
/// <b>IMPORTANT</b>: This is a replacement class for the real <see cref="CustomerMaps"/> class for running in-memory unit tests.
/// Make sure to keep this class in sync with the real class.
/// </summary>
public class CustomerMapsForTest : CustomerOptimizedMaps<Customer>
{
public CustomerMapsForTest()
{
Id(k => k.I3D).Column("I3D").GeneratedBy.Assigned();
Map(k => k.PaymentConditionOfferI3D).Column("ZahlKondAng").Nullable();
Map(k => k.PaymentConditionOrderI3D).Column("ZahlKondAuf").Nullable();
Map(k => k.PaymentConditionServiceOrderI3D).Column("ZahlKondSer").Nullable();
Map(k => k.PaymentConditionPickupListI3D).Column("ZahlkondAbhol").Nullable();
Map(k => k.PaymentConditionInvoiceI3D).Column("ZahlKondRech").Nullable();
Map(k => k.PaymentConditionCreditVoucherI3D).Column("ZahlKondGut").Nullable();
Map(k => k.DeliveryConditionI3D).Column("ZahlKondLiefBed").Nullable();
// IMPORTANT: This is the problematic column that causes a syntax error in the SQLite in-memory database.
// The original name of the column is 'Limit' which is a reserved keyword in SQLite.
// This is changed here for 'CreditLimit' to avoid the syntax error.
Map(k => k.CreditLimit).Column("CreditLimit");
Map(k => k.CreditLimitAvailable).Column("LimitVerfuegbar");
Map(k => k.CreditLimitCalculationKind).Column("LimitBerechnungsArt").Nullable();
Map(k => k.PurchaseOrderNumberRequiered).Column("BestNrNoetig");
References(k => k.RootDir).Column("RootDirI3D").LazyLoad();
References(k => k.PurchaseOrderDir).Column("BestDirI3D").LazyLoad();
References(k => k.ServiceDir).Column("SerDirI3D").LazyLoad();
References(k => k.DeliveryListDir).Column("LiefDirI3D").LazyLoad();
References(k => k.HelpdeskDir).Column("HlpDirI3D").LazyLoad();
References(k => k.DeviceDir).Column("GerDirI3D").LazyLoad();
References(k => k.OrderDir).Column("AufDirI3D").LazyLoad();
References(k => k.InvoiceDir).Column("RechDirI3D").LazyLoad();
References(k => k.ProjectDir).Column("ProjDirI3D").LazyLoad();
References(k => k.ActivityDir).Column("TaetDirI3D").LazyLoad();
References(k => k.ContractDir).Column("VertragDirI3D").LazyLoad();
References(k => k.OfferDir).Column("AngDirI3D").LazyLoad();
References(k => k.MailDir).Column("MailDirI3D").LazyLoad();
References(k => k.CreditVoucherDir).Column("GutDirI3D").LazyLoad();
References(k => k.PickupListDir).Column("AbholDirI3D").LazyLoad();
Map(k => k.AkqQuantityServer).Column("AkqAnzahlServer");
Map(k => k.AkqQuantityPC).Column("AkqAnzahlPC");
Map(k => k.AkqManufacturerServer).Column("AkqHerstellerServer").Length(80);
Map(k => k.AkqManufacturerPC).Column("AkqHerstellerPC").Length(80);
Map(k => k.AkqBuysFrom).Column("AkqKauftBei").Length(80);
Map(k => k.AkqManufacturerPrinter).Column("AkqHerstellerDrucker").Length(80);
Map(k => k.AkqResubmission).Column("AkqWiedervorlage");
Map(k => k.AkqAkquiseComplete).Column("AkqAkquiseKomplett");
Map(k => k.AkqAdditionalInformation).Column("AkqZusatzinfo").Length(int.MaxValue);
Map(k => k.DefaultAddressI3D).Column("DefaultAnschrift").Nullable();
Map(k => k.PriceList).Column("Preisliste");
Map(k => k.Bonus).Column("Bonus");
Map(k => k.AkqVIP).Column("AkqVIP");
Map(k => k.AdvertsingBan).Column("werbesperre");
References(k => k.DunningContactPerson).Column("MahnPersonI3D").LazyLoad();
Map(k => k.DunningKind).Column("MahnArt").Length(50);
Map(k => k.AlternativeCustomerI3D).Column("AbwKundeI3D");
Map(k => k.AlternativeAddressI3D).Column("AbwAnschriftI3D");
Map(k => k.Comment).Column("Kommentar").Length(int.MaxValue);
Map(k => k.WebPassword).Column("WebKennwort").Length(50);
Map(k => k.LastWebLogin).Column("LastWebLogin").Nullable();
Map(k => k.ClassificationI3D).Column("KlassifizierungI3D");
Map(k => k.ExternalI3D).Column("ExterneI3D").Length(50);
Map(k => k.ToDoAkquiseI3D).Column("ToDoAkquiseI3D");
Map(k => k.VATNotActive).Column("MWStAktiv");
References(k => k.BankCountry).Column("BankLand").LazyLoad();
Map(k => k.BankCity).Column("BankOrt").Length(50);
Map(k => k.BankStreet).Column("BankStrasse").Length(50);
Map(k => k.BankIBAN).Column("BankIBAN").Length(50);
Map(k => k.BankSWIFT).Column("BankSWIFT").Length(50);
Map(k => k.Bank02).Column("Bank02").Length(40);
Map(k => k.BankCode02).Column("BankBLZ02").Length(20);
Map(k => k.BankAccountNumber02).Column("BankKtoNr02").Length(20);
References(k => k.BankCountry02).Column("BankLand02").LazyLoad();
Map(k => k.BankCity02).Column("BankOrt02").Length(50);
Map(k => k.BankStreet02).Column("BankStrasse02").Length(50);
Map(k => k.BankIBAN02).Column("BankIBAN02").Length(50);
Map(k => k.BankSWIFT02).Column("BankSWIFT02").Length(50);
Map(k => k.OfferReportI3D).Column("AngRepI3D");
Map(k => k.OfferReportQuantity).Column("AngRepAnz");
Map(k => k.OrderReportI3D).Column("AufRepI3D");
Map(k => k.OrderReportQuantity).Column("AufRepAnz");
Map(k => k.ServiceReportI3D).Column("SerRepI3D");
Map(k => k.ServiceReportQuantity).Column("SerRepAnz");
Map(k => k.DeliveryListReportI3D).Column("LiefRepI3D");
Map(k => k.DeliveryListReportQuantity).Column("LiefRepAnz");
Map(k => k.PickupListReportI3D).Column("AbhRepI3D");
Map(k => k.PickupListReportQuantity).Column("AbhRepAnz");
Map(k => k.InvoiceReportI3D).Column("RechRepI3D");
Map(k => k.InvoiceReportQuantity).Column("RechRepAnz");
Map(k => k.CreditVoucherReportI3D).Column("GutRepI3D");
Map(k => k.CreditVoucherReportQuantity).Column("GutRepAnz");
Map(k => k.InfoOffer).Column("NeuInfoAngebot").Length(int.MaxValue);
Map(k => k.InfoOrder).Column("NeuInfoAuftrag").Length(int.MaxValue);
Map(k => k.InfoDeliveryList).Column("NeuInfoLieferschein").Length(int.MaxValue);
Map(k => k.InfoPickupList).Column("NeuInfoAbholschein").Length(int.MaxValue);
Map(k => k.InfoInvoice).Column("NeuInfoRechnung").Length(int.MaxValue);
Map(k => k.SalesRegionI3D).Column("VertriebsgebietI3D");
Map(k => k.DeliveryListDuplicateReportI3D).Column("LiefDublRepI3D");
Map(k => k.DeliveryListDuplicateReportQuantity).Column("LiefDublRepAnz");
Map(k => k.InvoiceDuplicateReportI3D).Column("RechDublRepI3D");
Map(k => k.InvoiceDuplicateReportQuantity).Column("RechDublRepAnz");
Map(k => k.DunningAfterDays).Column("MahnungNachTagen");
Map(k => k.SecoundDunningAfterDays).Column("MahnungNachTagen2");
Map(k => k.ThirdDunningAfterDays).Column("MahnungNachTagen3");
Map(k => k.TransferedToAccounting).Column("RWUebergabe");
References(k => k.TransferedToAccountringBy).Column("RWPersonalI3D").Nullable().LazyLoad();
Map(k => k.TransferedToAccountingDate).Column("RWDatum");
Map(k => k.CustomerOriginI3D).Column("KundenHerkunftI3D");
Map(k => k.OfferReportFaxI3D).Column("AngRepI3DFax");
Map(k => k.OfferReportMailI3D).Column("AngRepI3DMail");
Map(k => k.OfferReportPrintI3D).Column("AngRepI3DDruck");
Map(k => k.OrderReportFaxI3D).Column("AufRepI3DFax");
Map(k => k.OrderReportMailI3D).Column("AufRepI3DMail");
Map(k => k.OrderReportPrintI3D).Column("AufRepI3DDruck");
Map(k => k.ServiceReportFaxI3D).Column("SerRepI3DFax");
Map(k => k.ServiceReportMailI3D).Column("SerRepI3DMail");
Map(k => k.ServiceReportPrintI3D).Column("SerRepI3DDruck");
Map(k => k.DeliveryListReportFaxI3D).Column("LiefRepI3DFax");
Map(k => k.DeliveryListMailI3D).Column("LiefRepI3DMail");
Map(k => k.DeliveryListPrintI3D).Column("LiefRepI3DDruck");
Map(k => k.PickupListReportFaxI3D).Column("AbhRepI3DFax");
Map(k => k.PickupListReportMailI3D).Column("AbhRepI3DMail");
Map(k => k.PickupListReportPrintI3D).Column("AbhRepI3DDruck");
Map(k => k.InvoiceReportFaxI3D).Column("RechRepI3DFax");
Map(k => k.InvoiceReportMailI3D).Column("RechRepI3DMail");
Map(k => k.InvoiceReportPrintI3D).Column("RechRepI3DDruck");
Map(k => k.CreditVoucherReportFaxI3D).Column("GutRepI3DFax");
Map(k => k.CreditVoucherReportMailI3D).Column("GutRepI3DMail");
Map(k => k.CreditVoucherReportPrintI3D).Column("GutRepI3DDruck");
Map(k => k.Matchcode).Column("Kurzbezeichnung").Length(30);
Map(k => k.SpecialAgreementI3D).Column("SondervereinbarungI3D");
Map(k => k.AlternateContactPersonI3D).Column("AbwPersonI3D");
Map(k => k.AlternateReceiver).Column("AbwEmpfaenger").Length(500);
Map(k => k.AlternateDeliveryAddressI3D).Column("AbwLiefAnschriftI3D");
Map(k => k.AlternateDeliveryReceiver).Column("AbwLiefEmpfaenger").Length(500);
Map(k => k.AlternateDeliveryCustomerI3D).Column("AbwLiefKundeI3D");
Map(k => k.AlternateDeliveryContactPersonI3D).Column("AbwLiefPersonI3D");
Map(k => k.ProjNrNeeded).Column("ProjNrNoetig");
Map(k => k.AkqQuantityFree).Column("AkqAnzahlFrei");
Map(k => k.OrderLockAfterDunning).Column("AuftragsperreNachMahnung");
Map(k => k.AkqFreeText1).Column("AkqFreiText1").Length(30);
Map(k => k.AkqFreeText2).Column("AkqFreiText2").Length(30);
Map(k => k.AkqFreeText3).Column("AkqFreiText3").Length(30);
Map(k => k.AkqFreeText4).Column("AkqFreiText4").Length(30);
Map(k => k.InfoHelpdesk).Column("NeuInfoHelpdesk").Length(int.MaxValue);
Map(k => k.CustomerNumber).Column("KundenNummer");
Map(k => k.CustomerNumberExt).Column("KundenNummerExt");
Map(k => k.EvaluationOfSupplier).Column("Lieferantenbewertung");
Map(k => k.ISOCertified).Column("ISOzertifiziert");
Map(k => k.CreatedThrough).Column("ErstelltDurch");
Map(k => k.InfoCreditVoucher).Column("NeuInfoGutschrift").Length(int.MaxValue);
Map(k => k.DunningAtAlternateInvoiceAddress).Column("MahnungAnAbwRechAnschrift");
Map(k => k.DontShowRabateText).Column("RabatttextNichtAnzeigen");
Map(k => k.ProductionConfigurationRequiring).Column("FertigungskonfigurationsPflicht");
Map(k => k.PrintProductionConfiguration).Column("FertigungskonfigurationDrucken");
Map(k => k.Stock).Column("LagerI3D").CustomType<DefaultWarehouseI3DCustomType>().Nullable();
Map(k => k.InstructionActive).Column("UnterweisungAktiv");
Map(k => k.ContactI3D).Column("KontakteI3D");
Map(k => k.AkqResubmissionConfiguration).Column("AkqWiedervorlageEinstellung");
Map(k => k.AkqResubmissionKind).Column("AkqWiedervorlageArt");
Map(k => k.AkqResubmissionLength).Column("AkqWiedervorlageDauer");
Map(k => k.DistributorI3D).Column("DistributorI3D");
Map(k => k.FreeText01).Column("Freitext01").Length(255);
Map(k => k.GoodsRecipient).Column("Warenempfaengernummer").Length(50);
Map(k => k.NoCargoArticle).Column("KeinFrachtartikel");
Map(k => k.Kontoinhaber).Column("Kontoinhaber").Length(255);
HasMany(a => a.BusinessLines)
.KeyColumn("KundenI3D")
.Where(a => a.AccountKind == 0)
.LazyLoad();
HasMany(a => a.Interests)
.KeyColumn("KundenI3D")
.Where(a => a.AccountKind == 0)
.LazyLoad();
HasMany(a => a.Products)
.KeyColumn("KundenI3D")
.Where(a => a.AccountKind == 0)
.LazyLoad();
HasMany(a => a.CustomerArticles)
.KeyColumn("KundenI3D")
.LazyLoad();
HasMany(a => a.SpecialPrices)
.KeyColumn("KundenI3D")
.LazyLoad();
HasMany(a => a.Activitys)
.KeyColumn("KundeI3D")
.Where("ObjektArt = " + ((int)ObjectType.ObjectTypeDictionary.Customer).ToString())
.LazyLoad();
Map(f => f.InvoiceSendType)
.Column("RechnungVersandArt")
.Nullable()
.CustomType<AssetSendType>();
References(f => f.AlternateInvoiceReceiver)
.Column("AbwMailRechnungEmpfaengerI3D")
.Nullable()
.NotFound.Ignore()
.Cascade.None();
Map(f => f.BillingInterval)
.Column("BillingInterval")
.Nullable()
.CustomType<BillingInterval>();
Map(f => f.IsCustomerKind1)
.Column("Firmenkunde")
.Nullable();
Map(f => f.IsCustomerKind2)
.Column("Endkunde")
.Nullable();
Map(f => f.IsCustomerKind3)
.Column("Haendler")
.Nullable();
Map(f => f.IsCustomerKind4)
.Column("Interessent")
.Nullable();
Map(f => f.IsCustomerKind5)
.Column("KundenArt5")
.Nullable();
Map(m => m.RiverbirdMsp)
.Column("RiverbirdMsp")
.Length(32)
.Nullable();
this.Map(f => f.HelpdeskClosingDontNotifyCustomer).Column("HelpdeskClosingDontNotifyCustomer").Nullable();
}
}
}
@@ -0,0 +1,131 @@
using Centron.BusinessLogic.EDI.AlsoCH;
using Centron.Data.WebServices.Administration.MasterData;
using CentronSoftware.Centron.WebServices.Entities.Sales.Receipts.ReceiptReceiver;
using CentronSoftware.Centron.WebServices.Entities.Sales.Receipts.SupplierOrders;
using CentronSoftware.Centron.WebServices.Entities.Warehousing;
using JetBrains.Annotations;
namespace Centron.Tests.BL.EDI.AlsoCH;
[TestSubject(typeof(AlsoOrderCH_BL))]
public class AlsoOrderCH_BLTest
{
private const string _orderSender = "L01";
private const string _customerNumberAtSupplier = "10776732";
private readonly List<CountryDTO> _testCountries = CreateTestCountries();
private readonly List<ReceiptSupplierOrderItemDTO> _testReceiptItems = CreateTestReceiptItems();
private readonly List<SpecialAgreementPreviewDTO> _testSpecialAgreements = new();
[Fact]
public void CreateOrderDocument_ShouldMapLicenseReceiverContact_WhenLicenseReceiverHasContactData()
{
// Arrange
var receipt = CreateTestReceipt();
receipt.ReceiptReceiverLicense = new ReceiptReceiverDTO
{
CompanyName = "Licence Company AG",
Email = "licence@example.ch",
Phone = "+41 44 1234567",
Fax = "+41 44 7654321"
};
var orderBL = new AlsoOrderCH_BL(receipt);
// Act
var xmlDoc = orderBL.CreateOrderDocument(
_orderSender,
_customerNumberAtSupplier,
_testReceiptItems,
_testSpecialAgreements,
_testCountries);
// Assert
var communicationInfo = xmlDoc.Descendants()
.FirstOrDefault(x => x.Name.LocalName == "licence_information")
?.Descendants().FirstOrDefault(x => x.Name.LocalName == "reseller_party")
?.Descendants().FirstOrDefault(x => x.Name.LocalName == "communication_info");
Assert.NotNull(communicationInfo);
var email = communicationInfo.Descendants().FirstOrDefault(x => x.Name.LocalName == "PAEA");
Assert.NotNull(email);
Assert.Equal("licence@example.ch", email.Value);
var phone = communicationInfo.Descendants().FirstOrDefault(x => x.Name.LocalName == "PPHN");
Assert.NotNull(phone);
Assert.Equal("+41 44 1234567", phone.Value);
var fax = communicationInfo.Descendants().FirstOrDefault(x => x.Name.LocalName == "PFAN");
Assert.NotNull(fax);
Assert.Equal("+41 44 7654321", fax.Value);
var name = communicationInfo.Descendants().FirstOrDefault(x => x.Name.LocalName == "CCNA");
Assert.NotNull(name);
Assert.Equal("Licence Company AG", name.Value);
}
[Fact]
public void CreateOrderDocument_ShouldNotEmitEmail_WhenLicenseReceiverHasNoEmail()
{
// Arrange
var receipt = CreateTestReceipt();
receipt.ReceiptReceiverLicense = new ReceiptReceiverDTO
{
CompanyName = "Licence Company AG"
};
var orderBL = new AlsoOrderCH_BL(receipt);
// Act
var xmlDoc = orderBL.CreateOrderDocument(
_orderSender,
_customerNumberAtSupplier,
_testReceiptItems,
_testSpecialAgreements,
_testCountries);
// Assert
var email = xmlDoc.Descendants().FirstOrDefault(x => x.Name.LocalName == "PAEA");
Assert.Null(email);
}
#region Test Data Creation Methods
private static ReceiptSupplierOrderDTO CreateTestReceipt()
{
return new ReceiptSupplierOrderDTO
{
Number = 12345,
Version = 1,
Date = new DateTime(2024, 10, 14)
};
}
private static List<CountryDTO> CreateTestCountries()
{
return
[
new CountryDTO { I3D = 1, CountryCode = "CH" }
];
}
private static List<ReceiptSupplierOrderItemDTO> CreateTestReceiptItems()
{
return
[
new ReceiptSupplierOrderItemDTO
{
I3D = 1001,
InternalPosition = 1,
ArticleI3D = 2001,
ArticleCode = "ART-001",
ManufacturerCode = "MFG-001",
QuantityComplete = 3m,
BasePrice = 10.50m
}
];
}
#endregion
}
@@ -0,0 +1,600 @@
using Centron.BusinessLogic.EDI.Opentrans21;
using Centron.Data.WebServices.Accounts;
using Centron.Data.WebServices.Administration.MandatoryArea;
using Centron.Data.WebServices.Administration.MasterData;
using Centron.Data.WebServices.Sales.Receipts;
using Centron.Interfaces.EDI;
using CentronSoftware.Centron.WebServices.Entities.EDI;
using CentronSoftware.Centron.WebServices.Entities.Sales.Receipts.SupplierOrders;
using CentronSoftware.Centron.WebServices.Entities.Warehousing;
using JetBrains.Annotations;
namespace Centron.Tests.BL.EDI.Opentrans21;
[TestSubject(typeof(Opentrans21OrderBL))]
public class Opentrans21OrderBLTest
{
private readonly List<CountryDTO> _testCountries;
private readonly ReceiptSupplierOrderDTO _testReceipt;
private readonly MandatoryDTO _testMandatory;
private readonly AccountSearchItemDTO _testAddress;
private readonly AccountSearchItemDTO _testLicenceAddress;
private readonly AccountSearchItemDTO _testSupplierAccount;
private readonly ReceiptSettingsDTO _testReceiptSettings;
private readonly List<ReceiptSupplierOrderItemDTO> _testReceiptItems;
private readonly List<SpecialAgreementPreviewDTO> _testSpecialAgreements;
private const string _customerNumberAtSupplier = "CUST123";
private const string _supplierID = "SUPP456";
public Opentrans21OrderBLTest()
{
_testCountries = CreateTestCountries();
_testReceipt = CreateTestReceipt();
_testMandatory = CreateTestMandatory();
_testAddress = CreateTestAddress();
_testLicenceAddress = CreateTestLicenceAddress();
_testSupplierAccount = CreateTestSupplierAccount();
_testReceiptSettings = CreateTestReceiptSettings();
_testReceiptItems = CreateTestReceiptItems();
_testSpecialAgreements = CreateTestSpecialAgreements();
}
[Fact]
public async Task CreateOrderDocument_ShouldSetCorrectOrderInfo_WhenValidDataProvided()
{
// Arrange
var orderBL = new Opentrans21OrderBL(
_testReceipt,
_customerNumberAtSupplier,
_supplierID,
(int)EDIOrderSpecificity.none,
_testCountries
);
// Act
var result = await orderBL.CreateOrderDocument(
_testReceiptItems,
_testMandatory,
_testSpecialAgreements,
_testAddress,
_testLicenceAddress,
_testSupplierAccount,
EDIMultidistributors.None,
_testReceiptSettings,
"test@example.com"
);
// Assert
Assert.True(result.IsSuccess);
var xmlDoc = result.Data;
var orderInfo = xmlDoc.Descendants().FirstOrDefault(x => x.Name.LocalName == "ORDER_INFO");
Assert.NotNull(orderInfo);
var orderId = orderInfo.Descendants().FirstOrDefault(x => x.Name.LocalName == "ORDER_ID");
Assert.NotNull(orderId);
Assert.Equal(_testReceipt.Number.ToString(), orderId.Value);
var orderDate = orderInfo.Descendants().FirstOrDefault(x => x.Name.LocalName == "ORDER_DATE");
Assert.NotNull(orderDate);
Assert.Equal(_testReceipt.Date.ToString("yyyy-MM-dd"), orderDate.Value);
}
[Fact]
public async Task CreateOrderDocument_ShouldIncludeCorrectParties_WhenStandardDistributor()
{
// Arrange
var orderBL = new Opentrans21OrderBL(
_testReceipt,
_customerNumberAtSupplier,
_supplierID,
(int)EDIOrderSpecificity.none,
_testCountries
);
// Act
var result = await orderBL.CreateOrderDocument(
_testReceiptItems,
_testMandatory,
_testSpecialAgreements,
_testAddress,
_testLicenceAddress,
_testSupplierAccount,
EDIMultidistributors.None,
_testReceiptSettings,
"test@example.com"
);
// Assert
Assert.True(result.IsSuccess);
var xmlDoc = result.Data;
var parties = xmlDoc.Descendants().Where(x => x.Name.LocalName == "PARTY").ToList();
// Should have buyer, supplier, and delivery parties
Assert.True(parties.Count >= 3);
var partyRoles = parties.SelectMany(p => p.Descendants().Where(x => x.Name.LocalName == "PARTY_ROLE"))
.Select(r => r.Value).ToList();
Assert.Contains("buyer", partyRoles);
Assert.Contains("supplier", partyRoles);
Assert.Contains("delivery", partyRoles);
}
[Fact]
public async Task CreateOrderDocument_ShouldCalculateCorrectTotalAmount_WhenMultipleItems()
{
// Arrange
var receiptItems = new List<ReceiptSupplierOrderItemDTO>
{
CreateTestReceiptItem(1, 2.0m, 10.50m), // 2 * 10.50 = 21.00
CreateTestReceiptItem(2, 3.0m, 15.75m), // 3 * 15.75 = 47.25
CreateTestReceiptItem(3, 1.0m, 5.25m) // 1 * 5.25 = 5.25
};
// Total expected: 73.50
var orderBL = new Opentrans21OrderBL(
_testReceipt,
_customerNumberAtSupplier,
_supplierID,
(int)EDIOrderSpecificity.none,
_testCountries
);
// Act
var result = await orderBL.CreateOrderDocument(
receiptItems,
_testMandatory,
_testSpecialAgreements,
_testAddress,
_testLicenceAddress,
_testSupplierAccount,
EDIMultidistributors.None,
_testReceiptSettings,
"test@example.com"
);
// Assert
Assert.True(result.IsSuccess);
var xmlDoc = result.Data;
var totalAmount = xmlDoc.Descendants().FirstOrDefault(x => x.Name.LocalName == "TOTAL_AMOUNT");
Assert.NotNull(totalAmount);
Assert.Equal("73.50", totalAmount.Value);
var totalItemNum = xmlDoc.Descendants().FirstOrDefault(x => x.Name.LocalName == "TOTAL_ITEM_NUM");
Assert.NotNull(totalItemNum);
Assert.Equal("3", totalItemNum.Value);
}
[Fact]
public async Task CreateOrderDocument_ShouldSetDeliveryDate_WhenDeliveryDateSpecified()
{
// Arrange
var receipt = CreateTestReceipt();
receipt.DeliveryDate = new DateTime(2024, 12, 25);
var orderBL = new Opentrans21OrderBL(
receipt,
_customerNumberAtSupplier,
_supplierID,
(int)EDIOrderSpecificity.none,
_testCountries
);
// Act
var result = await orderBL.CreateOrderDocument(
_testReceiptItems,
_testMandatory,
_testSpecialAgreements,
_testAddress,
_testLicenceAddress,
_testSupplierAccount,
EDIMultidistributors.None,
_testReceiptSettings,
"test@example.com"
);
// Assert
Assert.True(result.IsSuccess);
var xmlDoc = result.Data;
var deliveryStartDate = xmlDoc.Descendants().FirstOrDefault(x => x.Name.LocalName == "DELIVERY_START_DATE");
var deliveryEndDate = xmlDoc.Descendants().FirstOrDefault(x => x.Name.LocalName == "DELIVERY_END_DATE");
Assert.NotNull(deliveryStartDate);
Assert.NotNull(deliveryEndDate);
Assert.Equal("2024-12-25", deliveryStartDate.Value);
Assert.Equal("2024-12-25", deliveryEndDate.Value);
}
[Fact]
public async Task CreateOrderDocument_ShouldHandleTHSSpecificity_WhenTHSOrderSpecificity()
{
// Arrange
var orderBL = new Opentrans21OrderBL(
_testReceipt,
_customerNumberAtSupplier,
_supplierID,
(int)EDIOrderSpecificity.THS,
_testCountries
);
// Act
var result = await orderBL.CreateOrderDocument(
_testReceiptItems,
_testMandatory,
_testSpecialAgreements,
null, // No delivery address for THS
_testLicenceAddress,
_testSupplierAccount,
EDIMultidistributors.None,
_testReceiptSettings,
"test@example.com"
);
// Assert
Assert.True(result.IsSuccess);
var xmlDoc = result.Data;
var orderItems = xmlDoc.Descendants().Where(x => x.Name.LocalName == "ORDER_ITEM").ToList();
foreach (var item in orderItems)
{
var remarks = item.Descendants().Where(x => x.Name.LocalName == "REMARKS").ToList();
Assert.Equal(10, remarks.Count); // THS should have exactly 10 remarks
}
}
[Fact]
public async Task CreateOrderDocument_ShouldReturnError_WhenExceptionOccurs()
{
// Arrange
var orderBL = new Opentrans21OrderBL(
_testReceipt,
_customerNumberAtSupplier,
_supplierID,
(int)EDIOrderSpecificity.none,
_testCountries
);
// Create invalid receipt items that will cause an exception
var invalidReceiptItems = new List<ReceiptSupplierOrderItemDTO?>
{
null // This should cause an exception
};
// Act
var result = await orderBL.CreateOrderDocument(
invalidReceiptItems,
_testMandatory,
_testSpecialAgreements,
_testAddress,
_testLicenceAddress,
_testSupplierAccount,
EDIMultidistributors.None,
_testReceiptSettings,
"test@example.com"
);
// Assert
Assert.False(result.IsSuccess);
Assert.NotNull(result.Error);
}
[Fact]
public async Task CreateOrderDocument_ShouldSetCorrectCurrency_WhenCurrencyFound()
{
// Arrange
var receipt = CreateTestReceipt();
receipt.CurrencyString = "EUR";
var orderBL = new Opentrans21OrderBL(
receipt,
_customerNumberAtSupplier,
_supplierID,
(int)EDIOrderSpecificity.none,
_testCountries
);
// Act
var result = await orderBL.CreateOrderDocument(
_testReceiptItems,
_testMandatory,
_testSpecialAgreements,
_testAddress,
_testLicenceAddress,
_testSupplierAccount,
EDIMultidistributors.None,
_testReceiptSettings,
"test@example.com"
);
// Assert
Assert.True(result.IsSuccess);
var xmlDoc = result.Data;
var currency = xmlDoc.Descendants().FirstOrDefault(x => x.Name.LocalName == "CURRENCY");
Assert.NotNull(currency);
Assert.Equal("EUR", currency.Value);
}
[Fact]
public async Task CreateOrderDocument_ShouldIncludeLicenceAddress_WhenLicenceAddressProvided()
{
// Arrange
var orderBL = new Opentrans21OrderBL(
_testReceipt,
_customerNumberAtSupplier,
_supplierID,
(int)EDIOrderSpecificity.none,
_testCountries
);
// Act
var result = await orderBL.CreateOrderDocument(
_testReceiptItems,
_testMandatory,
_testSpecialAgreements,
_testAddress,
_testLicenceAddress,
_testSupplierAccount,
EDIMultidistributors.None,
_testReceiptSettings,
"test@example.com"
);
// Assert
Assert.True(result.IsSuccess);
var xmlDoc = result.Data;
var parties = xmlDoc.Descendants().Where(x => x.Name.LocalName == "PARTY").ToList();
// Should have buyer, supplier, delivery, and licence parties
Assert.True(parties.Count >= 4);
var partyRoles = parties.SelectMany(p => p.Descendants().Where(x => x.Name.LocalName == "PARTY_ROLE"))
.Select(r => r.Value).ToList();
Assert.Contains("buyer", partyRoles);
Assert.Contains("supplier", partyRoles);
Assert.Contains("delivery", partyRoles);
// Find the licence party
var licenceParty = parties.FirstOrDefault(p =>
p.Descendants().Any(x => x.Name.LocalName == "PARTY_ROLE" && x.Value == "customer"));
Assert.NotNull(licenceParty);
// Verify licence party contains the correct information
var partyName = licenceParty.Descendants().FirstOrDefault(x => x.Name.LocalName == "NAME");
Assert.NotNull(partyName);
Assert.Equal(_testLicenceAddress.AccountName, partyName.Value);
var street = licenceParty.Descendants().FirstOrDefault(x => x.Name.LocalName == "STREET");
Assert.NotNull(street);
Assert.Equal(_testLicenceAddress.AddressStreet, street.Value);
var city = licenceParty.Descendants().FirstOrDefault(x => x.Name.LocalName == "CITY");
Assert.NotNull(city);
Assert.Equal(_testLicenceAddress.AddressCity, city.Value);
var zip = licenceParty.Descendants().FirstOrDefault(x => x.Name.LocalName == "ZIP");
Assert.NotNull(zip);
Assert.Equal(_testLicenceAddress.AddressZip, zip.Value);
}
[Fact]
public async Task CreateOrderDocument_ShouldHandleNullLicenceAddress_WhenLicenceAddressNotProvided()
{
// Arrange
var orderBL = new Opentrans21OrderBL(
_testReceipt,
_customerNumberAtSupplier,
_supplierID,
(int)EDIOrderSpecificity.none,
_testCountries
);
// Act
var result = await orderBL.CreateOrderDocument(
_testReceiptItems,
_testMandatory,
_testSpecialAgreements,
_testAddress,
null, // No licence address
_testSupplierAccount,
EDIMultidistributors.None,
_testReceiptSettings,
"test@example.com"
);
// Assert
Assert.True(result.IsSuccess);
var xmlDoc = result.Data;
var parties = xmlDoc.Descendants().Where(x => x.Name.LocalName == "PARTY").ToList();
// Should have buyer, supplier, and delivery parties (no licence party)
var partyRoles = parties.SelectMany(p => p.Descendants().Where(x => x.Name.LocalName == "PARTY_ROLE"))
.Select(r => r.Value).ToList();
Assert.Contains("buyer", partyRoles);
Assert.Contains("supplier", partyRoles);
Assert.Contains("delivery", partyRoles);
// Ensure no licence party is present
Assert.DoesNotContain("licencee", partyRoles);
}
#region Test Data Creation Methods
private static List<CountryDTO> CreateTestCountries()
{
return
[
new CountryDTO
{
I3D = 1,
CountryCode = "DE",
CurrencySymbol = "EUR",
CurrencyISO = "EUR"
},
new CountryDTO
{
I3D = 2,
CountryCode = "US",
CurrencySymbol = "USD",
CurrencyISO = "USD"
}
];
}
private static ReceiptSupplierOrderDTO CreateTestReceipt()
{
return new ReceiptSupplierOrderDTO
{
Number = 12345,
Version = 1,
Date = new DateTime(2024, 10, 14),
CurrencyString = "EUR",
DeliveryDate = null
};
}
private static MandatoryDTO CreateTestMandatory()
{
return new MandatoryDTO
{
Mandator = "Test Company GmbH",
Street = "Test Street 123",
City = "Test City",
PostalCode = "12345",
Mail = "test@company.com",
Telephone = "+49 123 456789",
Country = new CountryDTO
{
I3D = 1,
CountryCode = "DE"
}
};
}
private static AccountSearchItemDTO CreateTestAddress()
{
return new AccountSearchItemDTO
{
AddressI3D = 1001,
AccountName = "Delivery Company",
AddressStreet = "Delivery Street 456",
AddressCity = "Delivery City",
AddressZip = "54321",
AddressCountryI3D = 1,
AddressDepartment = "Receiving Department",
AddressContactFirstname = "John",
AddressContactLastname = "Doe",
AddressContactTitle = "Mr.",
PrintName = true,
PrintDepartment = true,
PrintAddressContact = true
};
}
private static AccountSearchItemDTO CreateTestLicenceAddress()
{
return new AccountSearchItemDTO
{
AddressI3D = 1002,
AccountName = "Licence Company",
AddressStreet = "Licence Street 789",
AddressCity = "Licence City",
AddressZip = "65432",
AddressCountryI3D = 1,
AddressDepartment = "Licence Department",
AddressContactFirstname = "John",
AddressContactLastname = "Doe",
AddressContactTitle = "Mr.",
PrintName = true,
PrintDepartment = true,
PrintAddressContact = true
};
}
private static AccountSearchItemDTO CreateTestSupplierAccount()
{
return new AccountSearchItemDTO
{
AccountName = "Supplier Company Ltd",
AddressStreet = "Supplier Street 789",
AddressCity = "Supplier City",
AddressZip = "98765",
AddressCountryI3D = 1,
AddressDepartment = "Sales Department",
AddressContactFirstname = "Jane",
AddressContactLastname = "Smith",
AddressContactTitle = "Ms.",
ItScopeSupplierNumber = "12345",
PrintName = true,
PrintAddressContact = true,
PrintDepartment = true,
PrintDepartmentAddressContact = true
};
}
private static ReceiptSettingsDTO CreateTestReceiptSettings()
{
return new ReceiptSettingsDTO
{
ItScopeApiKey = "test-api-key"
};
}
private static List<ReceiptSupplierOrderItemDTO> CreateTestReceiptItems()
{
return
[
CreateTestReceiptItem(1, 2.0m, 25.50m),
CreateTestReceiptItem(2, 1.0m, 15.75m)
];
}
private static ReceiptSupplierOrderItemDTO CreateTestReceiptItem(int position, decimal quantity, decimal basePrice)
{
return new ReceiptSupplierOrderItemDTO
{
I3D = 1000 + position,
InternalPosition = position,
ArticleI3D = 2000 + position,
ArticleCode = $"ART-{position:D3}",
Text = $"Test Article {position}",
EANCode = $"123456789012{position}",
ManufacturerCode = $"MFG-{position:D3}",
SupplierManufacturerCode = $"SUP-MFG-{position:D3}",
QuantityComplete = quantity,
QuantityProcessed = 0,
BasePrice = basePrice,
PurchaseInformations = $"Purchase info for item {position}",
PurchaseOrderNumber = $"PO-{position:D6}",
SpecialAgreementI3D = null
};
}
private static List<SpecialAgreementPreviewDTO> CreateTestSpecialAgreements()
{
return
[
new SpecialAgreementPreviewDTO
{
I3D = 1,
Number = "SA-001",
Text = "Special Agreement 1",
IsProjectPrice = true,
IsInternal = false
}
];
}
#endregion
}
@@ -0,0 +1,399 @@
using Centron.BusinessLogic.EmployeeArea;
using Centron.DAO;
using Centron.DAO.Mappings;
using Centron.DAO.Mappings.Administration;
using Centron.DAO.Mappings.Administration.Company;
using Centron.DAO.Mappings.Administration.Employees;
using Centron.DAO.Mappings.CustomerArea.Support;
using Centron.DAO.Mappings.States;
using Centron.DAO.Mappings.TemporaryEntities;
using Centron.DAO.TemporaryEntities;
using Centron.Data.Entities.Administration;
using Centron.Data.Entities.EmployeeArea;
using Centron.Interfaces.Administration.Settings;
using Centron.Tests.BL.SetupUtils;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.BL.EmployeeArea
{
public class AppUserBLTest
{
[Fact]
public void GetActiveAppUsers_ReturnsOnlyActiveEmployees()
{
// Arrange
var session = CreateSession();
// Create employees
var activeEmployee = new EmployeeCompact { I3D = 1, IsActive = true };
var inactiveEmployee = new EmployeeCompact { I3D = 2, IsActive = false };
// Create AppUsers
var activeAppUser = new AppUser
{
I3D = 1,
Name = "activeUser",
IsAccountDisabled = false,
AccountDisabledFromDate = null,
AccountDisabledToDate = null,
Employee = activeEmployee
};
var inactiveAppUser = new AppUser
{
I3D = 2,
Name = "inactiveUser",
IsAccountDisabled = false,
AccountDisabledFromDate = null,
AccountDisabledToDate = null,
Employee = inactiveEmployee
};
// Add both to session
session.Save(activeEmployee);
session.Save(inactiveEmployee);
session.Save(activeAppUser);
session.Save(inactiveAppUser);
session.Flush();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetActiveAppUsers();
// Assert
Assert.Single(result);
Assert.Equal("activeUser", result[0].Name);
}
[Fact]
public void GetActiveAppUsers_ReturnsEmpty_WhenNoUsersExist()
{
// Arrange
var session = CreateSession();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetActiveAppUsers();
// Assert
Assert.Empty(result);
}
[Fact]
public void GetActiveAppUsers_ReturnsEmpty_WhenAllUsersInactive()
{
// Arrange
var session = CreateSession();
var inactiveEmployee = new EmployeeCompact { I3D = 1, IsActive = false };
var appUser = new AppUser
{
I3D = 1,
Name = "inactiveUser",
IsAccountDisabled = false,
Employee = inactiveEmployee
};
session.Save(inactiveEmployee);
session.Save(appUser);
session.Flush();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetActiveAppUsers();
// Assert
Assert.Empty(result);
}
[Fact]
public void GetActiveAppUsers_ReturnsEmpty_WhenAllAccountsDisabled()
{
// Arrange
var session = CreateSession();
var employee = new EmployeeCompact { I3D = 1, IsActive = true };
var appUser = new AppUser
{
I3D = 1,
Name = "disabledUser",
IsAccountDisabled = true,
Employee = employee
};
session.Save(employee);
session.Save(appUser);
session.Flush();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetActiveAppUsers();
// Assert
Assert.Empty(result);
}
[Fact]
public void GetActiveAppUsers_ReturnsEmpty_WhenAccountDisabledByDateRange()
{
// Arrange
var session = CreateSession();
var employee = new EmployeeCompact { I3D = 1, IsActive = true };
var now = DateTime.UtcNow;
var appUser = new AppUser
{
I3D = 1,
Name = "dateDisabledUser",
IsAccountDisabled = false,
AccountDisabledFromDate = now.AddDays(-1),
AccountDisabledToDate = now.AddDays(1),
Employee = employee
};
session.Save(employee);
session.Save(appUser);
session.Flush();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetActiveAppUsers();
// Assert
Assert.Empty(result);
}
[Fact]
public void GetActiveAppUsers_ReturnsUser_WhenAccountDisabledDateRangeIsInPast()
{
// Arrange
var session = CreateSession();
var employee = new EmployeeCompact { I3D = 1, IsActive = true };
var now = DateTime.UtcNow;
var appUser = new AppUser
{
I3D = 1,
Name = "pastDisabledUser",
IsAccountDisabled = false,
AccountDisabledFromDate = now.AddDays(-10),
AccountDisabledToDate = now.AddDays(-5),
Employee = employee
};
session.Save(employee);
session.Save(appUser);
session.Flush();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetActiveAppUsers();
// Assert
Assert.Single(result);
Assert.Equal("pastDisabledUser", result[0].Name);
}
[Fact]
public void GetActiveAppUsers_ReturnsOnlyActiveAndEnabledUsers()
{
// Arrange
var session = CreateSession();
var activeEmployee = new EmployeeCompact { I3D = 1, IsActive = true };
var inactiveEmployee = new EmployeeCompact { I3D = 2, IsActive = false };
var appUser1 = new AppUser
{
I3D = 1,
Name = "activeUser",
IsAccountDisabled = false,
Employee = activeEmployee
};
var appUser2 = new AppUser
{
I3D = 2,
Name = "inactiveEmployeeUser",
IsAccountDisabled = false,
Employee = inactiveEmployee
};
var appUser3 = new AppUser
{
I3D = 3,
Name = "disabledAccountUser",
IsAccountDisabled = true,
Employee = activeEmployee
};
session.Save(activeEmployee);
session.Save(inactiveEmployee);
session.Save(appUser1);
session.Save(appUser2);
session.Save(appUser3);
session.Flush();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetActiveAppUsers();
// Assert
Assert.Single(result);
Assert.Equal("activeUser", result[0].Name);
}
[Fact]
public void GetActiveAppUsers_ReturnsMultipleActiveUsers()
{
// Arrange
var session = CreateSession();
var emp1 = new EmployeeCompact { I3D = 1, IsActive = true };
var emp2 = new EmployeeCompact { I3D = 2, IsActive = true };
var appUser1 = new AppUser
{
I3D = 1,
Name = "user1",
IsAccountDisabled = false,
Employee = emp1
};
var appUser2 = new AppUser
{
I3D = 2,
Name = "user2",
IsAccountDisabled = false,
Employee = emp2
};
session.Save(emp1);
session.Save(emp2);
session.Save(appUser1);
session.Save(appUser2);
session.Flush();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetActiveAppUsers();
// Assert
Assert.Equal(2, result.Count);
Assert.Contains(result, u => u.Name == "user1");
Assert.Contains(result, u => u.Name == "user2");
}
[Fact]
public void GetAppUserForWebaccounts_ReturnsAppUser_WhenCentronSystemUserIsConfigured()
{
// Arrange
var session = CreateSession();
var employee = new EmployeeCompact { I3D = 1, IsActive = true };
var appUser = new AppUser
{
Name = "centronSystemUser",
IsAccountDisabled = false,
Employee = employee
};
session.Save(employee);
var appUserI3D = (int)session.Save(appUser);
session.Flush();
var applicationSetting = new ApplicationSetting
{
I3D = (int)ApplicationSettingID.CentronSystemUser,
ValueInt = appUserI3D,
Description = "Centron System User"
};
session.Save(applicationSetting);
session.Flush();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetAppUserForWebaccounts();
// Assert
Assert.True(result.IsSuccess, $"Expected success but got: {result.Message}");
Assert.NotNull(result.Data);
Assert.Equal("centronSystemUser", result.Data.Name);
Assert.Equal(appUserI3D, result.Data.I3D);
}
[Fact]
public void GetAppUserForWebaccounts_ReturnsError_WhenCentronSystemUserNotConfigured()
{
// Arrange
var session = CreateSession();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetAppUserForWebaccounts();
// Assert
Assert.False(result.IsSuccess);
Assert.Equal("Es ist kein CentronSystemUser hinterlegt.", result.Message);
}
[Fact]
public void GetAppUserForWebaccounts_ReturnsError_WhenCentronSystemUserNotFound()
{
// Arrange
var session = CreateSession();
var applicationSetting = new ApplicationSetting
{
I3D = (int)ApplicationSettingID.CentronSystemUser,
ValueInt = 999, // Non-existent AppUser ID
Description = "Centron System User"
};
session.Save(applicationSetting);
session.Flush();
var licenseManager = MockSetupUtils.PrepareLicenseManagerMock();
var daoSession = new DAOSession(session);
var bl = new AppUserBL(daoSession, licenseManager);
// Act
var result = bl.GetAppUserForWebaccounts();
// Assert
Assert.False(result.IsSuccess);
Assert.Equal("CentronSystemUser nicht gefunden!", result.Message);
}
// Helper: create in-memory NHibernate session
private ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<AppUserMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeCompactMaps>())
.Mappings(m => m.FluentMappings.Add<MandatorMaps>())
.Mappings(m => m.FluentMappings.Add<CountryMaps>())
.Mappings(m => m.FluentMappings.Add<FederalStateMaps>())
.Mappings(m => m.FluentMappings.Add<SupportLevelMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserGroupMaps>())
.Mappings(m => m.FluentMappings.Add<AppRightMaps>())
.Mappings(m => m.FluentMappings.Add<ApplicationSettingMaps>());
ISessionFactory sessionFactory = fluentConfiguration.BuildSessionFactory();
ISession session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
return session;
}
}
}
@@ -0,0 +1,24 @@
using Centron.BusinessLogic.Administration.Licensing;
using Centron.Interfaces.Administration.Logins;
namespace Centron.Tests.BL.Fixtures;
public class LicenseManagerFixture
{
public LicenseManagerFixture()
{
var licenses = new Dictionary<Guid, string>
{
[LicenseGuids.Centron] = "c-entron.NET"
};
try
{
LicenseManager.Initialize(LicenseManager.SettingsForTests(licenses));
LicenseManager.Instance.LoadLicenses().Wait();
}
catch (Exception)
{
// The LicenseManager is already initialized
}
}
}
@@ -0,0 +1,113 @@
namespace Centron.Tests.BL.MyDay;
using Centron.DAO.Mappings.MyDay;
using Centron.Interfaces.BL;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate.Tool.hbm2ddl;
using System.Collections.Generic;
using Centron.BusinessLogic.MyDay;
using Centron.DAO;
using Centron.Data.Entities.MyDay;
using Centron.Data.WebServices.Tapi;
using Centron.Interfaces;
using JetBrains.Annotations;
using NHibernate;
using Xunit;
[TestSubject(typeof(MyDayBL))]
public class MyDayBLTest
{
[Fact]
public void GetEmployeeSelectionWorksAsExpected()
{
var session = CreateSession();
var myDayEmployeeSelection = new MyDayEmployeeSelection();
myDayEmployeeSelection.EmployeeI3D = 10;
myDayEmployeeSelection.AppUserI3D = 1;
myDayEmployeeSelection.DepartementI3D = 2;
var expectedResult =
Result<IList<MyDayEmployeeSelection>>.AsSuccess(new List<MyDayEmployeeSelection> { myDayEmployeeSelection });
session.Save(myDayEmployeeSelection);
session.Flush();
var daoSession = new DAOSession(session);
var myDayBL = new MyDayBL(daoSession);
var appUserI3D = 1;
var result = myDayBL.GetEmployeeSelection(appUserI3D);
Assert.Equivalent(expectedResult, result);
}
[Fact]
public void CreateGroupCallCaptionListsOtherParticipantsAndOrganizer()
{
var phoneCalls = new List<PhoneCallWithAccountDTO>
{
new()
{
CreatedBy = 1,
CreatorName = "Organizer",
CreatorNumber = "organizer@example.com",
OwnerKind = CentronObjectKindNumeric.EmployeeClass,
OwnerI3D = 2,
CallerName = "Anna",
CallerNumber = "anna@example.com"
},
new()
{
CreatedBy = 1,
CreatorName = "Organizer",
CreatorNumber = "organizer@example.com",
OwnerKind = CentronObjectKindNumeric.EmployeeClass,
OwnerI3D = 3,
CallerName = "Bob",
CallerNumber = "bob@example.com"
}
};
var caption = MyDayBL.CreateGroupCallCaption(phoneCalls, 2);
Assert.Equal("Microsoft Teams-Gruppengespräch mit Bob (bob@example.com), Organizer (organizer@example.com).", caption);
}
[Fact]
public void GetPhoneCallPeriodUsesFullGroupCallDuration()
{
var startTime = new DateTime(2026, 7, 30, 13, 23, 32);
var phoneCalls = new List<PhoneCallWithAccountDTO>
{
new()
{
StartTime = startTime.AddMinutes(2),
EndTime = startTime.AddSeconds(241)
},
new()
{
StartTime = startTime,
EndTime = startTime.AddSeconds(916)
}
};
var period = MyDayBL.GetPhoneCallPeriod(phoneCalls);
Assert.Equal(startTime, period.StartTime);
Assert.Equal(startTime.AddSeconds(916), period.EndTime);
}
public ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<MyDayEmployeeSelectionMaps>());
ISessionFactory sessionFactory = fluentConfiguration.BuildSessionFactory();
ISession session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
return session;
}
}
@@ -0,0 +1,688 @@
using Centron.BusinessLogic.Sales.Receipts.Internal;
using Centron.BusinessLogic.WebServices;
using Centron.DAO;
using Centron.DAO.Mappings;
using Centron.DAO.Mappings.Accounts;
using Centron.DAO.Mappings.Administration;
using Centron.DAO.Mappings.Administration.Company;
using Centron.DAO.Mappings.Administration.Employees;
using Centron.DAO.Mappings.Administration.Settings;
using Centron.DAO.Mappings.CustomerArea;
using Centron.DAO.Mappings.CustomerArea.Support;
using Centron.DAO.Mappings.Sales.Receipts;
using Centron.DAO.Mappings.States;
using Centron.DAO.Mappings.TemporaryEntities;
using Centron.Data.Entities.Accounts;
using Centron.Data.Entities.Administration.MasterData;
using Centron.Data.Entities.BusinessPartner;
using Centron.Data.Entities.CustomerArea;
using Centron.Data.Entities.Sales.Receipts;
using Centron.Data.Entities.Sales.Receipts.Invoices;
using Centron.Data.Entities.Sales.Receipts.Offers;
using Centron.Data.Entities.Sales.Receipts.SupplierInvoices;
using Centron.Tests.BL.DatabaseMappings;
using CentronSoftware.Centron.WebServices.Entities.Sales.Receipts.ReceiptReceiver;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.BL.Sales.Receipts.Internal;
[TestSubject(typeof(ReceiptAddressAndContactPersonHelperBL))]
public class ReceiptAddressAndContactPersonHelperBLTest
{
[Fact]
public void ReceiptCanBeConvertedToStructuredReceiptReceiver()
{
var session = CreateSession();
var country = new Country()
{
Name = "Deutschland"
};
session.Save(country, 1);
var address = new Address()
{
Street = "Main Street 123a",
Zip = "12345",
City = "Munich",
Country = country,
PrintType = true,
PrintContactPerson = true,
PrintDepartmentContact = 1,
PrintDepartment = true,
HasPostOfficeBox = false,
PostOfficeBox = "",
Department = "Department",
Language = country,
Currency = country,
VariableAddress = 0
};
session.Save(address, 2);
var department = new ContactDepartment()
{
Department = "Contact Department"
};
session.Save(department, 3);
var contact = new ContactPerson()
{
FirstName = "John",
LastName = "Doe",
Department = department,
Address = address
};
session.Save(contact, 4);
var customer = new CustomerCompact()
{
Name = "Tree"
};
session.Save(customer, 5);
session.Flush();
var daoSession = new DAOSession(session);
var receiptAddressAndContactPersonHelperBl = new ReceiptAddressAndContactPersonHelperBL(daoSession);
var expectedReceiptReceiver = new ReceiptReceiver()
{
CompanyName = "Tree",
Department = "Department",
ContactName = "John Doe",
ContactDepartment = "Contact Department",
Street = "Main Street",
HouseNumber = "123a",
HasPostOfficeBox = false,
PostOfficeBox = "",
Zip = "12345",
City = "Munich",
Country = "Deutschland",
CountryI3D = 1,
AdditionalAddressSupplement = ""
};
var receipt = "Tree\nDepartment \nJohn Doe\nContact Department\nMain Street 123a\n12345 Munich\n";
var result = receiptAddressAndContactPersonHelperBl.ConvertReceiverToReceiptReceiver(receipt, 2, 4, false);
Assert.Equivalent(expectedReceiptReceiver, result);
}
[Fact]
public void ReceiptReceiverCanBeUpdated()
{
var session = CreateSession();
var country = new Country()
{
Name = "Deutschland"
};
session.Save(country, 1);
var customer = new CustomerCompact()
{
Name = "Tree"
};
session.Save(customer, 5);
var address = new Address()
{
Street = "Main Street 123a",
Zip = "12345",
City = "Munich",
Country = country,
PrintType = true,
PrintContactPerson = true,
PrintDepartmentContact = 1,
PrintDepartment = true,
HasPostOfficeBox = false,
PostOfficeBox = "",
Department = "Department",
Language = country,
Currency = country,
VariableAddress = 0,
CustomerI3D = 5
};
session.Save(address, 2);
var department = new ContactDepartment()
{
Department = "Contact Department"
};
session.Save(department, 3);
var contact = new ContactPerson()
{
FirstName = "John",
LastName = "Doe",
Department = department,
Address = address
};
session.Save(contact, 4);
session.Flush();
var daoSession = new DAOSession(session);
var receiptAddressAndContactPersonHelperBl = new ReceiptAddressAndContactPersonHelperBL(daoSession);
var expectedReceiptReceiver = new ReceiptReceiver()
{
CompanyName = "Tree",
Department = "Department",
ContactName = "John Doe",
ContactDepartment = "Contact Department",
Street = "Main Street",
HouseNumber = "123a",
HasPostOfficeBox = false,
PostOfficeBox = "",
Zip = "12345",
City = "Munich",
Country = "Deutschland",
CountryI3D = 1,
AdditionalAddressSupplement = "",
CustomerNumber = 5,
AccountI3D = null,
SupplierNumber = null,
FederalState = string.Empty
};
var receipt = new ReceiptOffer() { CustomerI3D = 5 };
receiptAddressAndContactPersonHelperBl.UpdateReceiptReceiver(receipt, 5, 2, 4);
Assert.Equivalent(expectedReceiptReceiver, receipt.ReceiptReceiver);
var receiptReceiverDto = receiptAddressAndContactPersonHelperBl.CreateReceiverObject(false, true, 5, "Tree", 2, 4);
var expectedDto = ObjectMapper.Map<ReceiptReceiver, ReceiptReceiverDTO>(expectedReceiptReceiver);
expectedDto.ReceiverText = receiptAddressAndContactPersonHelperBl.CreateReceiverTextFromDTO(expectedDto);
Assert.Equivalent(expectedDto, receiptReceiverDto);
}
[Fact]
public void ReceiptReceiverCanHandleUniCode()
{
var session = CreateSession();
var country = new Country()
{
Name = "China"
};
session.Save(country, 1);
var address = new Address()
{
Street = "香港葵涌大連排金龙工业中心二期12楼L室",
Zip = "12345",
City = "HongKong",
Country = country,
PrintType = true,
PrintContactPerson = true,
PrintDepartment = true,
PrintDepartmentContact = 1,
HasPostOfficeBox = false,
PostOfficeBox = "",
Department = "Foreign",
Language = country,
Currency = country,
VariableAddress = 0
};
session.Save(address, 2);
var department = new ContactDepartment()
{
Department = "Foreign Contact Department"
};
session.Save(department, 3);
var contact = new ContactPerson()
{
FirstName = "John",
LastName = "Doe",
Department = department,
Address = address
};
session.Save(contact, 4);
var customer = new CustomerCompact()
{
Name = "Tree"
};
session.Save(customer, 5);
session.Flush();
var daoSession = new DAOSession(session);
var receiptAddressAndContactPersonHelperBl = new ReceiptAddressAndContactPersonHelperBL(daoSession);
var expectedReceiptReceiver = new ReceiptReceiver()
{
CompanyName = "Tree",
Department = "Foreign",
ContactName = "John Doe",
ContactDepartment = "Foreign Contact Department",
Street = "香港葵涌大連排金龙工业中心二期",
HouseNumber = "12楼L室",
HasPostOfficeBox = false,
PostOfficeBox = "",
Zip = "12345",
City = "HongKong",
Country = "China",
CountryI3D = 1,
AdditionalAddressSupplement = "",
CustomerNumber = 5,
AccountI3D = null,
SupplierNumber = null,
FederalState = string.Empty
};
var receipt = new ReceiptOffer() { CustomerI3D = 5 };
receiptAddressAndContactPersonHelperBl.UpdateReceiptReceiver(receipt, 5, 2, 4);
Assert.Equivalent(expectedReceiptReceiver, receipt.ReceiptReceiver);
}
[Fact]
public void UpdatingReceiptReceiverCanHandleAlternativeAddresses()
{
var session = CreateSession();
var expectedReceiptReceiverDelivery = new ReceiptReceiver()
{
CompanyName = "Tree",
Department = "Department Delivery",
ContactName = "John Doe",
ContactDepartment = "Contact Department",
Street = "Main Street",
HouseNumber = "123a",
HasPostOfficeBox = false,
PostOfficeBox = "",
Zip = "12345",
City = "Munich",
Country = "Deutschland",
CountryI3D = 1,
AdditionalAddressSupplement = "",
FederalState = string.Empty
};
var expectedReceiptReceiverInvoice = new ReceiptReceiver()
{
CompanyName = "Tree",
Department = "Department Invoice",
ContactName = "John Doe",
ContactDepartment = "Contact Department",
Street = "Main Street",
HouseNumber = "123a",
HasPostOfficeBox = false,
PostOfficeBox = "",
Zip = "12345",
City = "Munich",
Country = "Deutschland",
CountryI3D = 1,
AdditionalAddressSupplement = "",
FederalState = string.Empty
};
var country = new Country()
{
Name = "Deutschland"
};
session.Save(country, 1);
var address = new Address()
{
Street = "Main Street 123a",
Zip = "12345",
City = "Munich",
Country = country,
PrintType = true,
PrintContactPerson = true,
PrintDepartmentContact = 1,
PrintDepartment = true,
HasPostOfficeBox = false,
PostOfficeBox = "",
Department = "Department",
Language = country,
Currency = country,
VariableAddress = 0
};
session.Save(address, 2);
var department = new ContactDepartment()
{
Department = "Contact Department"
};
session.Save(department, 3);
var contact = new ContactPerson()
{
FirstName = "John",
LastName = "Doe",
Department = department,
Address = address
};
session.Save(contact, 4);
var customer = new CustomerCompact()
{
Name = "Tree"
};
session.Save(customer, 5);
var accountCustomer = new AccountCustomer
{
Number = 5,
ReceiptReceiverDelivery = expectedReceiptReceiverDelivery,
ReceiptReceiverInvoice = expectedReceiptReceiverInvoice
};
session.Save(accountCustomer, 7);
session.Flush();
var daoSession = new DAOSession(session);
var receiptAddressAndContactPersonHelperBl = new ReceiptAddressAndContactPersonHelperBL(daoSession);
var expectedReceiptReceiver = new ReceiptReceiver()
{
CompanyName = "Tree",
Department = "Department",
ContactName = "John Doe",
ContactDepartment = "Contact Department",
Street = "Main Street",
HouseNumber = "123a",
HasPostOfficeBox = false,
PostOfficeBox = "",
Zip = "12345",
City = "Munich",
Country = "Deutschland",
CountryI3D = 1,
AdditionalAddressSupplement = "",
CustomerNumber = 5,
AccountI3D = null,
SupplierNumber = null,
FederalState = string.Empty
};
var receipt = new ReceiptInvoice()
{
CustomerI3D = 5,
AddressI3D = 2,
ContactPersonI3D = 4,
InvoiceAddressAddressI3D = 1,
InvoiceAddressContactPersonI3D = 1,
DeliveryAddressAddressI3D = 1,
DeliveryAddressContactPersonI3D = 1,
Receiver = "receiver",
InvoiceAddress = "",
DeliveryAddress = "",
ReceiptReceiverInvoice = expectedReceiptReceiverInvoice,
ReceiptReceiverDelivery = expectedReceiptReceiverDelivery
};
receiptAddressAndContactPersonHelperBl.UpdateReceiptReceiver(receipt);
Assert.Equivalent(expectedReceiptReceiver, receipt.ReceiptReceiver);
var receiptWithInvoiceAddressUsed = new ReceiptInvoice()
{
CustomerI3D = 5,
AddressI3D = 1,
ContactPersonI3D = 1,
InvoiceAddressAddressI3D = 2,
InvoiceAddressContactPersonI3D = 4,
DeliveryAddressAddressI3D = 1,
DeliveryAddressContactPersonI3D = 1,
Receiver = "receiver",
InvoiceAddress = "receiver",
DeliveryAddress = "",
ReceiptReceiverInvoice = expectedReceiptReceiverInvoice,
ReceiptReceiverDelivery = expectedReceiptReceiverDelivery
};
receiptAddressAndContactPersonHelperBl.UpdateReceiptReceiver(receiptWithInvoiceAddressUsed);
Assert.Equivalent(expectedReceiptReceiverInvoice, receiptWithInvoiceAddressUsed.ReceiptReceiver);
var receiptWithDeliveryAddressUsed = new ReceiptInvoice()
{
CustomerI3D = 5,
AddressI3D = 1,
ContactPersonI3D = 1,
InvoiceAddressAddressI3D = 1,
InvoiceAddressContactPersonI3D = 1,
DeliveryAddressAddressI3D = 2,
DeliveryAddressContactPersonI3D = 4,
Receiver = "receiver",
InvoiceAddress = "",
DeliveryAddress = "receiver",
ReceiptReceiverInvoice = expectedReceiptReceiverInvoice,
ReceiptReceiverDelivery = expectedReceiptReceiverDelivery
};
receiptAddressAndContactPersonHelperBl.UpdateReceiptReceiver(receiptWithDeliveryAddressUsed);
Assert.Equivalent(expectedReceiptReceiverDelivery, receiptWithDeliveryAddressUsed.ReceiptReceiver);
}
[Fact]
public void HouseNumberCanBeBeforeStreet()
{
var session = CreateSession();
var country = new Country()
{
Name = "Deutschland"
};
session.Save(country, 1);
var address = new Address()
{
Street = "123b Main Street",
Zip = "12345",
City = "Munich",
Country = country,
PrintType = true,
PrintContactPerson = true,
PrintDepartmentContact = 1,
PrintDepartment = true,
HasPostOfficeBox = true,
PostOfficeBox = "Box",
Department = "Department",
Language = country,
Currency = country,
VariableAddress = 0
};
session.Save(address, 2);
var departement = new ContactDepartment()
{
Department = "Contact Department"
};
session.Save(departement, 3);
var contact = new ContactPerson()
{
FirstName = "John",
LastName = "Doe",
Department = departement,
Address = address
};
session.Save(contact, 4);
var supplier = new Supplier()
{
Name = "Tree"
};
session.Save(supplier, 5);
session.Flush();
var daoSession = new DAOSession(session);
var receiptAddressAndContactPersonHelperBl = new ReceiptAddressAndContactPersonHelperBL(daoSession);
var expectedReceiptReceiver = new ReceiptReceiver()
{
CompanyName = "Tree",
Department = "Department",
ContactName = "John Doe",
ContactDepartment = "Contact Department",
Street = "Main Street",
HouseNumber = "123b",
HasPostOfficeBox = true,
PostOfficeBox = "Box",
Zip = "12345",
City = "Munich",
Country = "Deutschland",
CountryI3D = 1,
AdditionalAddressSupplement = "",
CustomerNumber = null,
AccountI3D = null,
SupplierNumber = 5,
FederalState = string.Empty
};
var receipt = new ReceiptSupplierInvoice() { SupplierI3D = 5 };
receiptAddressAndContactPersonHelperBl.UpdateReceiptReceiver(receipt, 5, 2, 4);
Assert.Equivalent(expectedReceiptReceiver, receipt.ReceiptReceiver);
}
[Fact]
public void ContactPersonDepartmentAndCompanyNameCanBeSuppressed()
{
var session = CreateSession();
var country = new Country()
{
Name = "Deutschland"
};
session.Save(country, 1);
var customer = new CustomerCompact()
{
Name = "Tree"
};
session.Save(customer, 5);
var address = new Address()
{
Street = "Main Street 123a",
Zip = "12345",
City = "Munich",
Country = country,
PrintType = false,
PrintContactPerson = false,
PrintDepartment = false,
HasPostOfficeBox = false,
PostOfficeBox = "",
Department = "Department",
Language = country,
Currency = country,
VariableAddress = 0,
CustomerI3D = 5
};
session.Save(address, 2);
var departement = new ContactDepartment()
{
Department = "Contact Department"
};
session.Save(departement, 3);
var contact = new ContactPerson()
{
FirstName = "John",
LastName = "Doe",
Department = departement,
Address = address
};
session.Save(contact, 4);
session.Flush();
var daoSession = new DAOSession(session);
var receiptAddressAndContactPersonHelperBl = new ReceiptAddressAndContactPersonHelperBL(daoSession);
var expectedReceiptReceiver = new ReceiptReceiver()
{
CompanyName = "",
Department = "",
ContactName = "",
ContactDepartment = "",
Street = "Main Street",
HouseNumber = "123a",
HasPostOfficeBox = false,
PostOfficeBox = "",
Zip = "12345",
City = "Munich",
Country = "Deutschland",
CountryI3D = 1,
AdditionalAddressSupplement = "",
CustomerNumber = 5,
AccountI3D = null,
SupplierNumber = null,
FederalState = string.Empty
};
var receipt = new ReceiptInvoice() { CustomerI3D = 5 };
receiptAddressAndContactPersonHelperBl.UpdateReceiptReceiver(receipt, 5, 2, 4);
Assert.Equivalent(expectedReceiptReceiver, receipt.ReceiptReceiver);
var receiptReceiverDto = receiptAddressAndContactPersonHelperBl.CreateReceiverObject(false, true, 5, "", 2, 4);
var expectedDto = ObjectMapper.Map<ReceiptReceiver, ReceiptReceiverDTO>(expectedReceiptReceiver);
expectedDto.ReceiverText = receiptAddressAndContactPersonHelperBl.CreateReceiverTextFromDTO(expectedDto);
Assert.Equivalent(expectedDto, receiptReceiverDto);
}
[Fact]
public void ReceiptReceiverCanHandleMissingValues()
{
var session = CreateSession();
var customer = new CustomerCompact()
{
Name = "Tree"
};
session.Save(customer, 5);
session.Flush();
var daoSession = new DAOSession(session);
var receiptAddressAndContactPersonHelperBl = new ReceiptAddressAndContactPersonHelperBL(daoSession);
var expectedReceiptReceiver = new ReceiptReceiver()
{
CompanyName = "Tree",
Department = "",
ContactName = "",
ContactDepartment = "",
Street = "",
HouseNumber = "",
HasPostOfficeBox = false,
PostOfficeBox = "",
Zip = "",
City = "",
Country = "",
AdditionalAddressSupplement = "",
CustomerNumber = 5,
AccountI3D = null,
SupplierNumber = null,
FederalState = string.Empty
};
var receipt = new ReceiptOffer() { CustomerI3D = 5 };
receiptAddressAndContactPersonHelperBl.UpdateReceiptReceiver(receipt, 5, 2, 4);
Assert.Equivalent(expectedReceiptReceiver, receipt.ReceiptReceiver);
// Test with a different customer number (5 instead of 10000 to match the actual customer)
var receiptReceiverDto = receiptAddressAndContactPersonHelperBl.CreateReceiverObject(false, true, 5, "Tree", 2, 4);
var expectedDto = ObjectMapper.Map<ReceiptReceiver, ReceiptReceiverDTO>(expectedReceiptReceiver);
expectedDto.ReceiverText = receiptAddressAndContactPersonHelperBl.CreateReceiverTextFromDTO(expectedDto);
Assert.Equivalent(expectedDto, receiptReceiverDto);
}
public ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<AddressMaps>())
.Mappings(m => m.FluentMappings.Add<CountryMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptReceiverMaps>())
.Mappings(m => m.FluentMappings.Add<ContactPersonMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeCompactMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserMaps>())
.Mappings(m => m.FluentMappings.Add<SupportLevelMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerDepartmentMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerSalutationMaps>())
.Mappings(m => m.FluentMappings.Add<MandatorMaps>())
.Mappings(m => m.FluentMappings.Add<FederalStateMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserGroupMaps>())
.Mappings(m => m.FluentMappings.Add<AppRightMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerCompactMaps>())
.Mappings(m => m.FluentMappings.Add<SupplierMaps>())
.Mappings(m => m.FluentMappings.Add<AccountCustomerMapsForTest>())
.Mappings(m => m.FluentMappings.Add<AppSettingMaps>())
.Mappings(m => m.FluentMappings.Add<AppSettingDataMaps>())
.Mappings(m => m.FluentMappings.Add<ApplicationSettingMaps>())
.Mappings(m => m.FluentMappings.Add<AccountSearchItemAccMapsForTest>())
.Mappings(m => m.FluentMappings.Add<AccountTypeToAccountMapsForTest>())
.Mappings(m => m.FluentMappings.Add<AccountSupplierMapsForTest>())
.Mappings(m => m.FluentMappings.Add<AccountAddressMaps>())
.Mappings(m => m.FluentMappings.Add<AccountAddressContactMaps>())
.Mappings(m => m.FluentMappings.Add<GeoInfoMaps>())
;
ISessionFactory sessionFactory = fluentConfiguration.BuildSessionFactory();
ISession session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
return session;
}
}
@@ -0,0 +1,28 @@
using Centron.BusinessLogic.Sales.Receipts.Internal;
using Centron.Interfaces;
using Centron.Interfaces.Warehousing;
namespace Centron.Tests.BL.Sales.Receipts.Internal;
public class ReceiptBarcodeBLTest
{
[Theory]
[InlineData(BarcodeState.InDeliveryList, true)]
[InlineData(BarcodeState.InStock, false)]
[InlineData(BarcodeState.LostAtStocktaking, false)]
[InlineData(BarcodeState.ManuallyBookedOut, false)]
public void ShouldUpdateBarcode_ForPickupList_OnlyAllowsBarcodeInDeliveryList(BarcodeState currentState, bool expected)
{
bool result = ReceiptBarcodeBL.ShouldUpdateBarcode(CentronObjectKindNumeric.PickupListClass, currentState);
Assert.Equal(expected, result);
}
[Fact]
public void ShouldUpdateBarcode_ForOtherReceiptKind_DoesNotRestrictBarcodeState()
{
bool result = ReceiptBarcodeBL.ShouldUpdateBarcode(CentronObjectKindNumeric.CreditVoucherClass, BarcodeState.ManuallyBookedOut);
Assert.True(result);
}
}
@@ -0,0 +1,53 @@
using System.Linq.Expressions;
using Centron.BusinessLogic.Sales.Receipts.Internal;
using Centron.Data.Entities.Sales.Receipts.DeliveryLists;
using Centron.Interfaces.Sales.Receipts.DeliveryLists;
namespace Centron.Tests.BL.Sales.Receipts.Internal;
public class ReceiptExpressionHelperTest
{
[Fact]
public void GetExpression_RewritesArrayContainsForReceiptFilters()
{
int[] receiptItemI3Ds = [1, 2];
Expression<Func<ReceiptDeliveryList, bool>> filter =
receipt => receipt.Items.Any(item => receiptItemI3Ds.Contains(item.I3D));
var result = ReceiptExpressionHelper.GetExpression<ReceiptDeliveryList, IReceiptDeliveryList>(filter);
Assert.NotNull(result);
Assert.False(ContainsArrayContainsOrImplicitConversion(result));
}
private static bool ContainsArrayContainsOrImplicitConversion(Expression expression)
{
var visitor = new ArrayContainsOrImplicitConversionFinder();
visitor.Visit(expression);
return visitor.Found;
}
private sealed class ArrayContainsOrImplicitConversionFinder : ExpressionVisitor
{
public bool Found { get; private set; }
protected override Expression VisitMethodCall(MethodCallExpression node)
{
if (node.Method.Name == "op_Implicit")
{
Found = true;
return node;
}
if (node.Method.Name == "Contains" && node.Arguments.Count > 0)
{
var source = node.Object ?? node.Arguments[0];
if (source.Type.IsArray)
Found = true;
}
return base.VisitMethodCall(node);
}
}
}
@@ -0,0 +1,295 @@
using Centron.BusinessLogic.Sales.Receipts.Internal;
using Centron.DAO;
using Centron.DAO.Mappings;
using Centron.DAO.Mappings.Accounts;
using Centron.DAO.Mappings.Administration;
using Centron.DAO.Mappings.Administration.Company;
using Centron.DAO.Mappings.Administration.Employees;
using Centron.DAO.Mappings.CustomerArea;
using Centron.DAO.Mappings.CustomerArea.Support;
using Centron.DAO.Mappings.Sales.Receipts;
using Centron.DAO.Mappings.States;
using Centron.Tests.BL.DatabaseMappings;
using Centron.Data.Entities.Accounts;
using Centron.Data.Entities.CustomerArea;
using Centron.Interfaces.Accounts;
using Centron.Interfaces.BL;
using Centron.BusinessLogic.WebServices.Sales.Receipts.DataAndResults;
using Centron.DAO.Mappings.Administration.Settings;
using Centron.DAO.Mappings.Sales.Receipts.ContractLists;
using Centron.DAO.Mappings.Sales.Receipts.CreditVouchers;
using Centron.DAO.Mappings.Sales.Receipts.DeliveryLists;
using Centron.DAO.Mappings.Sales.Receipts.Invoices;
using Centron.DAO.Mappings.Sales.Receipts.Offers;
using Centron.DAO.Mappings.Sales.Receipts.Orders;
using Centron.DAO.Mappings.Sales.Receipts.PickupLists;
using Centron.DAO.Mappings.TemporaryEntities;
using Centron.Data.Entities.Administration.MasterData;
using Centron.Data.Entities.Sales.Receipts;
using Centron.Data.Entities.Sales.Receipts.Offers;
using Centron.Interfaces.Sales.Receipts;
using Centron.Tests.BL.Fixtures;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
using AccountType = Centron.Data.Entities.Accounts.AccountType;
namespace Centron.Tests.BL.Sales.Receipts.Internal;
[TestSubject(typeof(ReceiptReceiverUpdaterBL))]
public class ReceiptReceiverUpdaterBLTest : LicenseManagerFixture
{
[Fact]
public void FindReceiptReceiverForCustomerAccounts_ReturnsUpdates_ForAlternativeReceivers()
{
var session = CreateSession();
var country = new Country() { Name = "Deutschland" };
session.Save(country, 1);
var address = new AccountAddress()
{
Street = "Main Street 123a",
Zip = "12345",
City = "Munich",
PrintDepartment = true,
PostOfficeBox = string.Empty,
CountryI3D = 1,
};
session.Save(address, 2);
var contact = new AccountAddressContact()
{
Firstname = "John",
Lastname = "Doe",
DepartmentText = "Dept",
};
session.Save(contact, 3);
var account = new Account()
{
I3D = 100,
Name = "Main Account",
Number = 1,
IsActive = true,
IsLocked = false
};
session.Save(account, 100);
var accountType = new AccountType()
{
Name = "Kunde",
I3D = 200,
Kind = AccountTypeKind.Customer
};
session.Save(accountType, 200);
var accountCustomer = new AccountCustomer()
{
I3D = 300,
AlternativeDeliveryReceiver = "Main Account\nDept\nJohn Doe\nMain Street 123a\n12345 Munich",
AlternativeDeliveryAddressI3D = 2,
AlternativeDeliveryAddressContactI3D = 3,
AlternativeDeliveryAccountI3D = 100,
AlternativeInvoiceReceiver = "Dept\nMr John Doe\nMain Street 123a\n12345 Munich",
AlternativeInvoiceAddressI3D = 2,
AlternativeInvoiceAddressContactI3D = 3,
AlternativeInvoiceAccountI3D = 100
};
session.Save(accountCustomer, 300);
var typeBinding = new AccountTypeToAccount()
{
I3D = 400,
AccountI3D = 100,
AccountTypeI3D = 200,
AccountCustomerI3D = 300
};
session.Save(typeBinding, 400);
session.Flush();
var daoSession = new DAOSession(session);
var bl = new ReceiptReceiverUpdaterBL(daoSession);
var result = bl.FindReceiptReceiverForCustomerAccounts();
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.Equal(2, result.Data.Count);
var delivery = result.Data.First(f => f.Type == ReceiptReceiverUpdateInformation.ReceiptReceiverType.Delivery);
Assert.True(delivery.Update);
Assert.Equal("Main Account\nDept\nJohn Doe\nMain Street 123a\n12345 Munich", delivery.Receiver);
Assert.NotNull(delivery.ReceiptReceiver);
Assert.Equal("Main Account", delivery.ReceiptReceiver.CompanyName);
var invoice = result.Data.First(f => f.Type == ReceiptReceiverUpdateInformation.ReceiptReceiverType.Invoice);
Assert.False(invoice.Update);
Assert.Equal("Dept\nMr John Doe\nMain Street 123a\n12345 Munich", invoice.Receiver);
Assert.Null(invoice.ReceiptReceiver);
Assert.Equal("Nicht erkannte Zeilen: " + Environment.NewLine + "Mr John Doe" + Environment.NewLine + "Mögliche Zeilen: " + Environment.NewLine + "John Doe, " + Environment.NewLine + "Deutschland, " + Environment.NewLine + "Main Account", invoice.Message);
}
[Fact]
public void FindReceiptReceiverForReceipts_ReturnsUpdates_ForAlternativeReceivers()
{
var session = CreateSession();
var country = new Country()
{
Name = "Deutschland"
};
session.Save(country, 1);
var address = new Address()
{
Street = "Main Street 123a",
Zip = "12345",
City = "Munich",
Country = country,
PrintType = true,
PrintContactPerson = true,
PrintDepartmentContact = 1,
PrintDepartment = true,
HasPostOfficeBox = false,
PostOfficeBox = "",
Department = "Department",
Language = country,
Currency = country,
VariableAddress = 0
};
session.Save(address, 2);
var department = new ContactDepartment()
{
Department = "Contact Department"
};
session.Save(department, 3);
var contact = new ContactPerson()
{
FirstName = "John",
LastName = "Doe",
Department = department,
Address = address
};
session.Save(contact, 4);
var customer = new CustomerCompact()
{
Name = "Tree"
};
session.Save(customer, 5);
var receipt = new ReceiptOffer()
{
CustomerI3D = 5,
DeliveryAddress = "Tree\nContact Department\nJohn Doe\nMain Street 123a\n12345 Munich",
DeliveryAddressCustomerI3D = 5,
DeliveryAddressAddressI3D = 2,
DeliveryAddressContactPersonI3D = 4,
InvoiceAddress = "Tree\nContact Department\nMr John Doe\nMain Street 123a\n12345 Munich",
InvoiceAddressCustomerI3D = 5,
InvoiceAddressAddressI3D = 2,
InvoiceAddressContactPersonI3D = 4,
Receiver = "Receiver",
Phone = "1234",
Fax = "1234",
Email = "test@test.de",
CreatedThroughApplicationVersion = "2.0.0",
ChangedThroughApplicationVersion = "2.0.0",
CurrencyString = "€",
Street = "Street",
PostOfficeBox = "",
City = "City",
Zip = "12345",
ContactName = "Name",
Information = "",
Number = 1234,
Date = DateTime.Now,
Version = 1,
State = ReceiptState.Active,
BranchOrigin = BranchOrigin.Creator,
CurrencyFactor = 1,
PurchaseOrderNumber = "1234",
};
session.Save(receipt, 7);
var provisionSchema = new ReceiptProvisionSchema
{
Name = "Schema",
};
session.Save(provisionSchema, 8);
session.Flush();
var daoSession = new DAOSession(session);
var bl = new ReceiptReceiverUpdaterBL(daoSession);
var result = bl.FindReceiptReceiverForReceipts();
Assert.Equal(ResultStatus.Success, result.Status);
Assert.NotNull(result.Data);
Assert.Equal(3, result.Data.Count);
var delivery = result.Data.First(f => f.Type == ReceiptReceiverUpdateInformation.ReceiptReceiverType.Delivery);
Assert.True(delivery.Update);
Assert.Equal("Tree\nContact Department\nJohn Doe\nMain Street 123a\n12345 Munich", delivery.Receiver);
Assert.NotNull(delivery.ReceiptReceiver);
Assert.Equal("Tree", delivery.ReceiptReceiver.CompanyName);
var invoice = result.Data.First(f => f.Type == ReceiptReceiverUpdateInformation.ReceiptReceiverType.Invoice);
Assert.False(invoice.Update);
Assert.Equal("Tree\nContact Department\nMr John Doe\nMain Street 123a\n12345 Munich", invoice.Receiver);
Assert.Null(invoice.ReceiptReceiver);
Assert.Equal("Nicht erkannte Zeilen: " + Environment.NewLine + "Mr John Doe" + Environment.NewLine + "Mögliche Zeilen: " + Environment.NewLine + "John Doe, " + Environment.NewLine + "Department, " + Environment.NewLine + "Deutschland" , invoice.Message);
}
public ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<AddressMaps>())
.Mappings(m => m.FluentMappings.Add<CountryMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptReceiverMaps>())
.Mappings(m => m.FluentMappings.Add<ContactPersonMaps>())
.Mappings(m => m.FluentMappings.Add<EmployeeCompactMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserMaps>())
.Mappings(m => m.FluentMappings.Add<SupportLevelMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerDepartmentMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerSalutationMaps>())
.Mappings(m => m.FluentMappings.Add<MandatorMaps>())
.Mappings(m => m.FluentMappings.Add<FederalStateMaps>())
.Mappings(m => m.FluentMappings.Add<AppUserGroupMaps>())
.Mappings(m => m.FluentMappings.Add<AppRightMaps>())
.Mappings(m => m.FluentMappings.Add<CustomerCompactMaps>())
.Mappings(m => m.FluentMappings.Add<SupplierMaps>())
.Mappings(m => m.FluentMappings.Add<AccountAddressMaps>())
.Mappings(m => m.FluentMappings.Add<AccountCustomerMapsForTest>())
.Mappings(m => m.FluentMappings.Add<AccountMaps>())
.Mappings(m => m.FluentMappings.Add<AccountTypeMaps>())
.Mappings(m => m.FluentMappings.Add<AccountTypeToAccountMaps>())
.Mappings(m => m.FluentMappings.Add<GeoInfoMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptOfferMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptOrderMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptOrderItemMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptInvoiceMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptInvoiceItemMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptContractMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptContractItemMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptCreditVoucherMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptCreditVoucherItemMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptProvisionSchemaMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptProvisionSchemaItemMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptProvisionItemEntityMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptOfferItemMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptDeliveryListMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptDeliveryListItemMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptPickupListMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptPickupListItemMaps>())
.Mappings(m => m.FluentMappings.Add<AccountAddressContactMaps>())
.Mappings(m => m.FluentMappings.Add<AppSettingMaps>())
.Mappings(m => m.FluentMappings.Add<ApplicationSettingMaps>())
.Mappings(m => m.FluentMappings.Add<AccountSearchItemAccMaps>());
ISessionFactory sessionFactory = fluentConfiguration.BuildSessionFactory();
ISession session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
return session;
}
}
@@ -0,0 +1,493 @@
using Centron.BusinessLogic.Sales.Support;
using Centron.DAO;
using Centron.DAO.Mappings.Administration;
using Centron.DAO.Mappings.CustomerArea.Support;
using Centron.Data.Entities.Administration;
using Centron.Data.Entities.CustomerArea.Support;
using Centron.Data.Entities.EmployeeArea;
using Centron.Interfaces.BL;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.BL.Sales.Support;
[TestSubject(typeof(HelpdeskTimeRecordingBL))]
public class HelpdeskTimeRecordingBLTest
{
[Fact]
public void FreeRecordingCanBeStarted()
{
var session = CreateSession();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.StartRecording(0, currentUser, false, false);
Assert.Equal(ResultStatus.Success, result.Status);
Assert.Equal(5, result.Data.EmployeeI3D);
Assert.Equal(0, result.Data.HelpdeskI3D);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Single(savedEntities);
var savedEntity = savedEntities.First();
Assert.Equal(5, savedEntity.EmployeeI3D);
Assert.Equal(0, savedEntity.HelpdeskI3D);
Assert.Equal(RecordingKind.Start, savedEntity.Kind);
Assert.InRange(savedEntity.Date, DateTime.Now.AddMinutes(-1), DateTime.Now.AddMinutes(1));
}
[Fact]
public void FreeRecordingCanNotBePausedForOtherUsers()
{
var session = CreateSession();
var recording = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 3,
HelpdeskI3D = 0,
Kind = RecordingKind.Start,
Date = DateTime.Now
};
session.Save(recording);
session.Flush();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.PauseRecording("GUID-123", 0, currentUser, false);
Assert.Equal(ResultStatus.Error, result.Status);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Single(savedEntities);
}
[Fact]
public void FreeRecordingCanNotBeResumedForOtherUsers()
{
var session = CreateSession();
var recording1 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 3,
HelpdeskI3D = 0,
Kind = RecordingKind.Start,
Date = DateTime.Now.AddMinutes(-5),
};
var recording2 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 3,
HelpdeskI3D = 0,
Kind = RecordingKind.Pause,
Date = DateTime.Now,
};
session.Save(recording1);
session.Save(recording2);
session.Flush();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.ResumeRecording("GUID-123", 0, currentUser, false);
Assert.Equal(ResultStatus.Error, result.Status);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Equal(2, savedEntities.Count);
}
[Fact]
public void FreeRecordingCanNotBeStoppedForOtherUsers()
{
var session = CreateSession();
var recording = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 3,
HelpdeskI3D = 0,
Kind = RecordingKind.Start,
Date = DateTime.Now
};
session.Save(recording);
session.Flush();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.StopRecording("GUID-123", 0, currentUser, false);
Assert.Equal(ResultStatus.Error, result.Status);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Single(savedEntities);
}
[Fact]
public void RecordingCanBeStartedAtSpecificDate()
{
var session = CreateSession();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var date = new DateTime(2020, 1, 1);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.StartRecording(0, currentUser, false, false, date);
Assert.Equal(ResultStatus.Success, result.Status);
Assert.Equal(5, result.Data.EmployeeI3D);
Assert.Equal(0, result.Data.HelpdeskI3D);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Single(savedEntities);
var savedEntity = savedEntities.First();
Assert.Equal(5, savedEntity.EmployeeI3D);
Assert.Equal(0, savedEntity.HelpdeskI3D);
Assert.Equal(RecordingKind.Start, savedEntity.Kind);
Assert.Equal(savedEntity.Date, date);
}
[Fact]
public void FreeRecordingCanBeStartedPausedAndResumed()
{
var session = CreateSession();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var recording = helpdeskTimeRecordingBl.StartRecording(0, currentUser, false, false);
helpdeskTimeRecordingBl.PauseRecording(recording.Data.Guid, 0, currentUser);
var resultSecondPause = helpdeskTimeRecordingBl.PauseRecording(recording.Data.Guid, 0, currentUser);
helpdeskTimeRecordingBl.ResumeRecording(recording.Data.Guid, 0, currentUser);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Equal(3, savedEntities.Count);
var savedEntity1 = savedEntities[0];
var savedEntity2 = savedEntities[1];
var savedEntity3 = savedEntities[2];
Assert.Equal(5, savedEntity1.EmployeeI3D);
Assert.Equal(5, savedEntity2.EmployeeI3D);
Assert.Equal(5, savedEntity3.EmployeeI3D);
Assert.Equal(recording.Data.Guid, savedEntity1.Guid);
Assert.Equal(recording.Data.Guid, savedEntity2.Guid);
Assert.Equal(recording.Data.Guid, savedEntity3.Guid);
Assert.Equal(RecordingKind.Start, savedEntity1.Kind);
Assert.Equal(RecordingKind.Pause, savedEntity2.Kind);
Assert.Equal(RecordingKind.Resume, savedEntity3.Kind);
}
[Fact]
public void CommentsToRecordingsCanBeAdded()
{
var date = new DateTime(2020, 1, 1);
var session = CreateSession();
var recording1 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = 0,
Kind = RecordingKind.Start,
Date = date
};
var recording2 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = 0,
Kind = RecordingKind.Start,
Date = date
};
session.Save(recording1);
session.Save(recording2);
session.Flush();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.SaveTimeRecordingComment("GUID-123", 0, currentUser, "some comment");
Assert.Equal(ResultStatus.Success, result.Status);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Equal(2, savedEntities.Count);
var savedEntity1 = savedEntities[0];
var savedEntity2 = savedEntities[1];
Assert.Equal(5, savedEntity1.EmployeeI3D);
Assert.Equal(5, savedEntity2.EmployeeI3D);
Assert.Equal(0, savedEntity1.HelpdeskI3D);
Assert.Equal(0, savedEntity2.HelpdeskI3D);
Assert.Equal(RecordingKind.Start, savedEntity1.Kind);
Assert.Equal(RecordingKind.Start, savedEntity2.Kind);
Assert.Equal(date, savedEntity1.Date);
Assert.Equal(date, savedEntity2.Date);
Assert.Equal("some comment", savedEntity1.Comment);
Assert.Equal("some comment", savedEntity2.Comment);
}
[Fact]
public void LastClosedCanBeSaved()
{
var date = new DateTime(2020, 1, 1);
var dateLastClosed = new DateTime(2022, 1, 1);
var helpdeskId = 1;
var session = CreateSession();
var recording1 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = helpdeskId,
Kind = RecordingKind.Start,
Date = date
};
var recording2 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = helpdeskId,
Kind = RecordingKind.Start,
Date = date
};
session.Save(recording1);
session.Save(recording2);
session.Flush();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.SaveTimeRecordingLastClosed(helpdeskId, currentUser, dateLastClosed);
Assert.Equal(ResultStatus.Success, result.Status);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Equal(2, savedEntities.Count);
var savedEntity1 = savedEntities[0];
var savedEntity2 = savedEntities[1];
Assert.Equal(5, savedEntity1.EmployeeI3D);
Assert.Equal(5, savedEntity2.EmployeeI3D);
Assert.Equal(helpdeskId, savedEntity1.HelpdeskI3D);
Assert.Equal(helpdeskId, savedEntity2.HelpdeskI3D);
Assert.Equal(RecordingKind.Start, savedEntity1.Kind);
Assert.Equal(RecordingKind.Start, savedEntity2.Kind);
Assert.Equal(date, savedEntity1.Date);
Assert.Equal(date, savedEntity2.Date);
Assert.Equal(dateLastClosed, savedEntity1.LastClosed);
Assert.Equal(dateLastClosed, savedEntity2.LastClosed);
}
[Fact]
public void RecordingCanBePaused()
{
var date = new DateTime(2020, 1, 1);
var session = CreateSession();
var recording = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = 1,
Kind = RecordingKind.Start,
Date = date
};
session.Save(recording);
session.Flush();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.PauseRecording("GUID-123", 1, currentUser);
Assert.Equal(ResultStatus.Success, result.Status);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Equal(2, savedEntities.Count);
var savedEntity1 = savedEntities[0];
var savedEntity2 = savedEntities[1];
Assert.Equal(5, savedEntity1.EmployeeI3D);
Assert.Equal(5, savedEntity2.EmployeeI3D);
Assert.Equal(1, savedEntity1.HelpdeskI3D);
Assert.Equal(1, savedEntity2.HelpdeskI3D);
Assert.Equal(RecordingKind.Start, savedEntity1.Kind);
Assert.Equal(RecordingKind.Pause, savedEntity2.Kind);
}
[Fact]
public void RecordingCanBeResumed()
{
var date = new DateTime(2020, 1, 1);
var session = CreateSession();
var recording1 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = 1,
Kind = RecordingKind.Start,
Date = date.AddHours(-1)
};
var recording2 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = 1,
Kind = RecordingKind.Pause,
Date = date
};
session.Save(recording1);
session.Save(recording2);
session.Flush();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.ResumeRecording("GUID-123", 1, currentUser);
Assert.Equal(ResultStatus.Success, result.Status);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Equal(3, savedEntities.Count);
var savedEntity1 = savedEntities[0];
var savedEntity2 = savedEntities[1];
var savedEntity3 = savedEntities[2];
Assert.Equal(5, savedEntity1.EmployeeI3D);
Assert.Equal(5, savedEntity2.EmployeeI3D);
Assert.Equal(5, savedEntity3.EmployeeI3D);
Assert.Equal(1, savedEntity1.HelpdeskI3D);
Assert.Equal(1, savedEntity2.HelpdeskI3D);
Assert.Equal(1, savedEntity3.HelpdeskI3D);
Assert.Equal(RecordingKind.Start, savedEntity1.Kind);
Assert.Equal(RecordingKind.Pause, savedEntity2.Kind);
Assert.Equal(RecordingKind.Resume, savedEntity3.Kind);
}
[Fact]
public void RecordingCanBeStopped()
{
var date = new DateTime(2020, 1, 1);
var session = CreateSession();
var recording1 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = 1,
Kind = RecordingKind.Start,
Date = date.AddHours(-1)
};
var recording2 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = 1,
Kind = RecordingKind.Pause,
Date = date
};
session.Save(recording1);
session.Save(recording2);
session.Flush();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.StopRecording("GUID-123", 0, currentUser);
Assert.Equal(ResultStatus.Success, result.Status);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Empty(savedEntities);
}
[Fact]
public void RecordingCanBeCleared()
{
var date = new DateTime(2020, 1, 1);
var session = CreateSession();
var recording1 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = 1,
Kind = RecordingKind.Start,
Date = date.AddHours(-1)
};
var recording2 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 5,
HelpdeskI3D = 1,
Kind = RecordingKind.Pause,
Date = date
};
session.Save(recording1);
session.Save(recording2);
session.Flush();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.ClearRecording("GUID-123", 0, currentUser);
Assert.Equal(ResultStatus.Success, result.Status);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Empty(savedEntities);
}
[Fact]
public void RecordingsCanBeCleared()
{
var date = new DateTime(2020, 1, 1);
var session = CreateSession();
var recording1 = new HelpdeskTimeRecording {
Guid = "GUID-123",
EmployeeI3D = 1,
HelpdeskI3D = 1,
Kind = RecordingKind.Start,
Date = date.AddHours(-1)
};
var recording2 = new HelpdeskTimeRecording {
Guid = "GUID-345",
EmployeeI3D = 5,
HelpdeskI3D = 2,
Kind = RecordingKind.Start,
Date = date
};
var recording3 = new HelpdeskTimeRecording {
Guid = "GUID-345",
EmployeeI3D = 5,
HelpdeskI3D = 2,
Kind = RecordingKind.Pause,
Date = date
};
session.Save(recording1);
session.Save(recording2);
session.Save(recording3);
session.Flush();
var daoSession = new DAOSession(session);
var helpdeskTimeRecordingBl = new HelpdeskTimeRecordingBL(daoSession);
var currentUser = new AppUser { Employee = new EmployeeCompact {I3D = 5} };
var result = helpdeskTimeRecordingBl.ClearRecordings([new RecordingInformation { HelpdeskI3D = 2, EmployeeI3D = 1}], currentUser);
Assert.Equal(ResultStatus.Success, result.Status);
var savedEntities = session.Query<HelpdeskTimeRecording>().ToList();
Assert.Single(savedEntities);
Assert.Equal(recording1, savedEntities.First());
}
public ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<HelpdeskTimeRecordingMaps>())
.Mappings(m => m.FluentMappings.Add<StopwatchNotificationMaps>())
.Mappings(m => m.FluentMappings.Add<AppGroupRightAssignmentMaps>());
ISessionFactory sessionFactory = fluentConfiguration.BuildSessionFactory();
ISession session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
return session;
}
}
@@ -0,0 +1,184 @@
using System.Text;
using Centron.BusinessLogic.Administration.Settings;
using Centron.Common.TextCoding;
using Centron.Data.Entities.Accounts;
using Centron.Data.Entities.Administration;
using Centron.Data.Entities.Administration.Logins;
using Centron.Data.Entities.Administration.Settings;
using Centron.Data.Entities.CustomerArea;
using Centron.Data.Entities.EmployeeArea;
using Centron.DAO.TemporaryEntities;
using Centron.Interfaces.Administration.Settings;
using NHibernate;
namespace Centron.Tests.BL.SetupUtils;
public static class DbSetupUtils
{
public static AppUser SaveAppUser(
ISession session,
string? name,
string? email,
int id,
string? subjectId = null,
bool isAccountDisabled = false,
DateTime? commencementDate = null,
DateTime? leavingDate = null,
int state = 1,
string? plainTextPassword = null,
DateTime? disabledFromDate = null,
DateTime? disabledToDate = null,
bool isEmployeeActive = true)
{
var employee = new EmployeeCompact
{
I3D = id,
Email = email,
CommencementDate = commencementDate,
LeavingDate = leavingDate,
State = state,
IsActive = isEmployeeActive
};
string? encodedPassword = null;
if(plainTextPassword is not null)
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
encodedPassword = SHA1Decoder.GetDecodedSHA1String(plainTextPassword);
}
var appUser = new AppUser
{
Name = name,
I3D = id,
Employee = employee,
OpenIdConnectSubjectIdentifier = subjectId,
IsAccountDisabled = isAccountDisabled,
Password = encodedPassword,
AccountDisabledFromDate = disabledFromDate,
AccountDisabledToDate = disabledToDate
};
return SaveAppUser(session, appUser, employee, id);
}
public static AppUser SaveAppUser(ISession session, AppUser appUser, EmployeeCompact employee, int id)
{
session.Save(employee, id);
session.Save(appUser, id);
session.Flush();
return session.Get<AppUser>(id);
}
public static int SetupResponsibleEmployeeForWebaccounts(ISession session, EmployeeCompact responsibleEmployee)
{
// Get the AppUser associated with this employee
var appUser = session.Query<AppUser>()
.FirstOrDefault(u => u.Employee.I3D == responsibleEmployee.I3D);
if (appUser == null)
throw new InvalidOperationException($"No AppUser found for Employee I3D {responsibleEmployee.I3D}");
var id = (int)ApplicationSettingID.CentronSystemUser;
var setting = new ApplicationSetting
{
I3D = id,
ValueInt = appUser.I3D,
Description = "Centron System User for Webaccounts"
};
session.Save(setting);
session.Flush();
return appUser.I3D;
}
public static WebAccount SaveWebAccount(
ISession session,
string? name,
string? email,
int id,
string? plainTextPassword,
int status = 1)
{
var customer = new Customer
{
I3D = id,
};
// var address = new Address
// {
// I3D = id,
// CustomerI3D = customer.I3D,
// State = 1,
// };
// var contactPerson = new ContactPerson // Personen
// {
// I3D = id,
// Address = address,
// State = 1
// };
var account = new Account
{
I3D = id,
IsActive = true,
IsLocked = false
};
var accountAddress = new AccountAddress
{
I3D = id,
IsActive = true,
AccountI3D = account.I3D
};
var addressContact = new AccountAddressContact
{
I3D = id,
AccountAddressI3D = accountAddress.I3D,
IsActive = true
};
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var encodedPassword = SHA1Decoder.GetDecodedSHA1String(plainTextPassword);
var webAccount = new WebAccount
{
I3D = id,
Username = name,
Status = status,
CustomerI3D = customer.I3D,
// AddressContactI3D = contactPerson.I3D,
AccountAddressContactI3D = addressContact.I3D,
Password = encodedPassword,
};
session.Save(customer, id);
// session.Save(address, id);
// session.Save(contactPerson, id);
session.Save(account, id);
session.Save(accountAddress, id);
session.Save(addressContact, id);
session.Save(webAccount, id);
session.Flush();
return session.Get<WebAccount>(id);
}
public static ApplicationSetting SaveApplicationSetting(
ISession session,
ApplicationSettingID id,
int? valueInt = null,
string? valueText = null,
decimal? valueDecimal = null,
string? description = null)
{
var setting = session.Get<ApplicationSetting>((int)id);
if (setting == null)
{
setting = new ApplicationSetting
{
I3D = (int)id
};
}
setting.ValueInt = valueInt;
setting.ValueText = valueText;
setting.ValueDecimal = valueDecimal;
setting.Description = description ?? "";
session.SaveOrUpdate(setting);
session.Flush();
return setting;
}
}
@@ -0,0 +1,61 @@
using System.Reflection;
using Centron.BusinessLogic.Administration.Licensing;
using Centron.Data.Entities.Administration.Logins;
using Centron.Interfaces.Administration.Logins;
using Centron.Interfaces.BL;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Abstractions.Serialization;
using NSubstitute;
namespace Centron.Tests.BL.SetupUtils;
public static class MockSetupUtils
{
public static ILicenseManager PrepareLicenseManagerMock(params Result<Guid>[] licenseGuids)
{
var mockedLicenseServer = Substitute.For<ILicenseManager>();
foreach (var license in licenseGuids)
{
mockedLicenseServer.CheckLicense(
Arg.Any<ApplicationKind>(),
Arg.Any<string>(),
Arg.Any<LoggedInUser>()).Returns(license);
mockedLicenseServer.HasLicense(
Arg.Is(license.Data)).Returns(license is { Status: ResultStatus.Success });
}
return mockedLicenseServer;
}
public static GraphServiceClient PrepareGraphMock(List<User> users)
{
return PrepareGraphMock<UserCollectionResponse>(users);
}
public static GraphServiceClient PrepareGraphMock<T>(object value) where T : IParsable, new()
{
var requestAdapter = Substitute.For<IRequestAdapter>();
var mock = new T();
var valueProperty = typeof(T).GetProperty("Value");
if (valueProperty == null)
throw new Exception("Value property not found");
if (!valueProperty.PropertyType.IsInstanceOfType(value))
throw new InvalidOperationException($"Value property type mismatch: expected {valueProperty.PropertyType}, got {value.GetType()}");
valueProperty.SetValue(mock, value);
requestAdapter.SendAsync(
Arg.Any<RequestInformation>(),
Arg.Any<ParsableFactory<T>>(),
Arg.Any<Dictionary<string, ParsableFactory<IParsable>>>(),
Arg.Any<CancellationToken>())
.ReturnsForAnyArgs(mock);
return new GraphServiceClient(requestAdapter);
}
}
@@ -0,0 +1,195 @@
using Centron.BusinessLogic.Tapi;
using Centron.Interfaces;
using Microsoft.Graph.Models.CallRecords;
namespace Centron.Tests.BL.Tapi;
public class PhoneCallBLTest
{
private const int CentronSystemEmployeeI3D = 999;
[Fact]
public void CreateSyncedPhoneCallsKeepsOneRowPerParticipant()
{
var organizer = CreateEmployeeCaller(1, "Organizer");
var employeeParticipant = CreateEmployeeCaller(2, "Participant");
var externalParticipant1 = CreateExternalCaller("external1@example.com");
var externalParticipant2 = CreateExternalCaller("external2@example.com");
var result = PhoneCallBL.CreateSyncedPhoneCalls(
CreateCall(CallType.GroupCall),
organizer,
[organizer, employeeParticipant, externalParticipant1, externalParticipant2, externalParticipant2],
[1, 2],
CentronSystemEmployeeI3D);
Assert.Equal(3, result.Count);
Assert.All(result, phoneCall =>
{
Assert.Equal(organizer.ObjektI3D, phoneCall.CreatedBy);
Assert.Equal(organizer.Name, phoneCall.CreatorName);
Assert.True(phoneCall.IsGroupCall);
});
Assert.Equal(
[employeeParticipant.Number, externalParticipant1.Number, externalParticipant2.Number],
result.Select(x => x.CallerNumber).OrderBy(x => x));
}
[Fact]
public void CreateSyncedPhoneCallsKeepsPeerToPeerCallAsSingleRow()
{
var organizer = CreateEmployeeCaller(1, "Organizer");
var participant = CreateEmployeeCaller(2, "Participant");
var phoneCall = Assert.Single(PhoneCallBL.CreateSyncedPhoneCalls(
CreateCall(CallType.PeerToPeer),
organizer,
[organizer, participant],
[1, 2],
CentronSystemEmployeeI3D));
Assert.Equal(1, phoneCall.CreatedBy);
Assert.Equal(2, phoneCall.OwnerI3D);
Assert.Equal(participant.Number, phoneCall.CallerNumber);
Assert.True(phoneCall.WasOutgoing);
Assert.True(phoneCall.WasInternalCall);
Assert.False(phoneCall.IsGroupCall);
}
[Fact]
public void CreateSyncedPhoneCallsKeepsRowWhenOnlyParticipantIsSynchronized()
{
var organizer = CreateEmployeeCaller(1, "Organizer");
var participant = CreateEmployeeCaller(2, "Participant");
var phoneCall = Assert.Single(PhoneCallBL.CreateSyncedPhoneCalls(
CreateCall(CallType.PeerToPeer),
organizer,
[participant],
[2],
CentronSystemEmployeeI3D));
Assert.Equal(1, phoneCall.CreatedBy);
Assert.Equal(2, phoneCall.OwnerI3D);
}
[Fact]
public void CreateSyncedPhoneCallsIgnoresCallsWithoutSynchronizedEmployee()
{
var organizer = CreateEmployeeCaller(1, "Organizer");
var participant = CreateEmployeeCaller(2, "Participant");
var result = PhoneCallBL.CreateSyncedPhoneCalls(
CreateCall(CallType.PeerToPeer),
organizer,
[participant],
[3],
CentronSystemEmployeeI3D);
Assert.Empty(result);
}
[Fact]
public void CreateSyncedPhoneCallsKeepsExternalParticipantsOfRelevantGroupCall()
{
var organizer = CreateExternalCaller("external-organizer@example.com");
var participant1 = CreateEmployeeCaller(1, "Participant 1");
var participant2 = CreateEmployeeCaller(2, "Participant 2");
var externalParticipant = CreateExternalCaller("external-participant@example.com");
var result = PhoneCallBL.CreateSyncedPhoneCalls(
CreateCall(CallType.GroupCall),
organizer,
[participant1, participant2, externalParticipant],
[1, 2],
CentronSystemEmployeeI3D);
Assert.Equal(3, result.Count);
Assert.Equal([1, 2, CentronSystemEmployeeI3D], result.Select(x => x.CreatedBy).OrderBy(x => x));
Assert.All(result, phoneCall => Assert.Equal(organizer.Number, phoneCall.CallerNumber));
}
[Fact]
public void PairComparisonAllowsParticipantAddedAfterInitialImport()
{
var organizer = CreateEmployeeCaller(1, "Organizer");
var initialParticipant = CreateEmployeeCaller(2, "Initial Participant");
var laterParticipant = CreateEmployeeCaller(3, "Later Participant");
var call = CreateCall(CallType.GroupCall);
var existingPhoneCall = Assert.Single(PhoneCallBL.CreateSyncedPhoneCalls(
call,
organizer,
[initialParticipant],
[1, 2, 3],
CentronSystemEmployeeI3D));
var refreshedPhoneCalls = PhoneCallBL.CreateSyncedPhoneCalls(
call,
organizer,
[initialParticipant, laterParticipant],
[1, 2, 3],
CentronSystemEmployeeI3D);
Assert.True(PhoneCallBL.IsSameSyncedPhoneCall(existingPhoneCall, refreshedPhoneCalls.Single(x => x.OwnerI3D == 2)));
Assert.False(PhoneCallBL.IsSameSyncedPhoneCall(existingPhoneCall, refreshedPhoneCalls.Single(x => x.OwnerI3D == 3)));
}
[Fact]
public void UpdateSyncedPhoneCallRefreshesCallAfterParticipantWasAdded()
{
var organizer = CreateEmployeeCaller(1, "Organizer");
var initialParticipant = CreateEmployeeCaller(2, "Initial Participant");
var laterParticipant = CreateEmployeeCaller(3, "Later Participant");
var initialCall = CreateCall(CallType.PeerToPeer);
initialCall.EndDateTime = initialCall.StartDateTime.GetValueOrDefault().AddMinutes(4);
var existingPhoneCall = Assert.Single(PhoneCallBL.CreateSyncedPhoneCalls(
initialCall,
organizer,
[initialParticipant],
[1, 2, 3],
CentronSystemEmployeeI3D));
var refreshedCall = CreateCall(CallType.GroupCall);
refreshedCall.Id = initialCall.Id;
refreshedCall.EndDateTime = refreshedCall.StartDateTime.GetValueOrDefault().AddMinutes(15);
var refreshedPhoneCall = PhoneCallBL.CreateSyncedPhoneCalls(
refreshedCall,
organizer,
[initialParticipant, laterParticipant],
[1, 2, 3],
CentronSystemEmployeeI3D)
.Single(x => x.OwnerI3D == initialParticipant.ObjektI3D);
var wasUpdated = PhoneCallBL.UpdateSyncedPhoneCall(existingPhoneCall, refreshedPhoneCall);
Assert.True(wasUpdated);
Assert.Equal(refreshedCall.EndDateTime.Value.LocalDateTime, existingPhoneCall.EndTime);
Assert.Equal(15 * 60, existingPhoneCall.DurationInSeconds);
Assert.True(existingPhoneCall.IsGroupCall);
Assert.False(PhoneCallBL.UpdateSyncedPhoneCall(existingPhoneCall, refreshedPhoneCall));
}
private static CallRecord CreateCall(CallType callType)
=> new()
{
Id = Guid.NewGuid().ToString(),
Type = callType,
StartDateTime = new DateTimeOffset(2026, 7, 13, 10, 0, 0, TimeSpan.Zero),
EndDateTime = new DateTimeOffset(2026, 7, 13, 11, 0, 0, TimeSpan.Zero)
};
private static Caller CreateEmployeeCaller(int employeeI3D, string name)
=> new()
{
ObjektI3D = employeeI3D,
ObjektArt = CentronObjectKindNumeric.EmployeeClass,
Name = name,
Number = $"employee{employeeI3D}@example.com"
};
private static Caller CreateExternalCaller(string number)
=> new()
{
ObjektArt = CentronObjectKindNumeric.Unknown,
Name = number,
Number = number
};
}
@@ -0,0 +1,389 @@
using Centron.BusinessLogic.Telemetry;
using Centron.Data.Entities.Telemetry;
using Centron.Host.AspNetCore.Telemetry;
using Centron.Interfaces.BL;
using JetBrains.Annotations;
using NSubstitute;
namespace Centron.Tests.BL.Telemetry;
[TestSubject(typeof(TelemetryAggregator))]
public class TelemetryAggregatorTest
{
private static readonly DateTime _bucket = new(2026, 5, 2, 12, 15, 0, DateTimeKind.Utc);
private static (TelemetryAggregator Aggregator, TelemetryBL BlMock) CreateAggregator()
{
var bl = Substitute.For<TelemetryBL>((Centron.DAO.DAOSession?)null);
bl.UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>())
.Returns(Result.AsSuccess());
bl.UpsertApiCallBatch(Arg.Any<IReadOnlyCollection<ApiCallBucketIncrement>>())
.Returns(Result.AsSuccess());
// Lookup-resolution always returns one I3D per distinct name (auto-numbered, deterministic).
var toolNameCounter = 0;
bl.ResolveMcpToolNameI3Ds(Arg.Any<IReadOnlyCollection<string>>())
.Returns(call =>
{
var names = call.Arg<IReadOnlyCollection<string>>();
var dict = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var n in names.Distinct(StringComparer.Ordinal))
dict[n] = ++toolNameCounter;
return Result<Dictionary<string, int>>.AsSuccess(dict);
});
var methodNameCounter = 0;
bl.ResolveApiMethodNameI3Ds(Arg.Any<IReadOnlyCollection<string>>())
.Returns(call =>
{
var names = call.Arg<IReadOnlyCollection<string>>();
var dict = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var n in names.Distinct(StringComparer.Ordinal))
dict[n] = ++methodNameCounter;
return Result<Dictionary<string, int>>.AsSuccess(dict);
});
var hardwareIdCounter = 0;
bl.ResolveHardwareIDI3Ds(Arg.Any<IReadOnlyCollection<string>>())
.Returns(call =>
{
var names = call.Arg<IReadOnlyCollection<string>>();
var dict = new Dictionary<string, int>(StringComparer.Ordinal);
foreach (var n in names.Distinct(StringComparer.Ordinal))
dict[n] = ++hardwareIdCounter;
return Result<Dictionary<string, int>>.AsSuccess(dict);
});
bl.LoadAllHardwareIDs()
.Returns(Result<List<HardwareIDLookup>>.AsSuccess(new List<HardwareIDLookup>()));
var aggregator = new TelemetryAggregator(action => action(bl));
return (aggregator, bl);
}
#region SnapToBucket
[Theory]
[InlineData(0, 0)]
[InlineData(7, 0)]
[InlineData(14, 0)]
[InlineData(15, 15)]
[InlineData(29, 15)]
[InlineData(30, 30)]
[InlineData(44, 30)]
[InlineData(45, 45)]
[InlineData(59, 45)]
public void SnapToBucket_RoundsDownTo15MinuteBoundary(int inputMinute, int expectedMinute)
{
var input = new DateTime(2026, 5, 2, 12, inputMinute, 0, DateTimeKind.Utc);
var snapped = TelemetryAggregator.SnapToBucket(input);
Assert.Equal(expectedMinute, snapped.Minute);
Assert.Equal(12, snapped.Hour);
}
[Fact]
public void SnapToBucket_PreservesUtcKind()
{
var input = new DateTime(2026, 5, 2, 12, 17, 0, DateTimeKind.Utc);
var snapped = TelemetryAggregator.SnapToBucket(input);
Assert.Equal(DateTimeKind.Utc, snapped.Kind);
}
[Fact]
public void SnapToBucket_StripsSecondsAndMilliseconds()
{
var input = new DateTime(2026, 5, 2, 12, 14, 59, 999, DateTimeKind.Utc);
var snapped = TelemetryAggregator.SnapToBucket(input);
Assert.Equal(0, snapped.Second);
Assert.Equal(0, snapped.Millisecond);
Assert.Equal(0, snapped.Minute);
}
#endregion
#region IncrementToolUsage
[Fact]
public async Task IncrementToolUsage_MergesIdenticalKeysIntoSingleBucket()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<McpToolUsageBucketIncrement>? captured = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
for (int i = 0; i < 5; i++)
aggregator.IncrementToolUsage(42, "create_ticket", McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
var item = Assert.Single(captured);
Assert.Equal(42, item.UserID);
Assert.True(item.ToolNameI3D > 0);
Assert.Equal(McpToolMode.User, item.ToolMode);
Assert.Equal(_bucket, item.BucketStartUtc);
Assert.Equal(5, item.Increment);
}
[Fact]
public async Task IncrementToolUsage_DistinctKeys_CreateSeparateBuckets()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<McpToolUsageBucketIncrement>? captured = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
aggregator.IncrementToolUsage(1, "tool", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(2, "tool", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(3, "tool", McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
Assert.Equal(3, captured.Count);
Assert.All(captured, c => Assert.Equal(1, c.Increment));
Assert.Equal(new[] { 1, 2, 3 }, captured.Select(c => c.UserID).OrderBy(x => x));
}
[Fact]
public async Task IncrementToolUsage_DifferentBucketStarts_CreateSeparateBuckets()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<McpToolUsageBucketIncrement>? captured = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
var laterBucket = _bucket.AddMinutes(15);
aggregator.IncrementToolUsage(7, "tool", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(7, "tool", McpToolMode.User, laterBucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
Assert.Equal(2, captured.Count);
Assert.All(captured, c => Assert.Equal(1, c.Increment));
}
[Theory]
[InlineData(0, "tool")]
[InlineData(-1, "tool")]
[InlineData(1, "")]
public async Task IncrementToolUsage_IgnoresInvalidInput(int userId, string toolName)
{
var (aggregator, bl) = CreateAggregator();
aggregator.IncrementToolUsage(userId, toolName, McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
bl.DidNotReceive().UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>());
}
[Fact]
public async Task IncrementToolUsage_IgnoresNullToolName()
{
var (aggregator, bl) = CreateAggregator();
aggregator.IncrementToolUsage(1, null!, McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
bl.DidNotReceive().UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>());
}
[Fact]
public async Task IncrementToolUsage_IsThreadSafe_UnderConcurrentLoad()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<McpToolUsageBucketIncrement>? captured = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
Parallel.For(0, 10_000, _ =>
aggregator.IncrementToolUsage(99, "tool", McpToolMode.User, _bucket));
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
var item = Assert.Single(captured);
Assert.Equal(10_000, item.Increment);
}
#endregion
#region IncrementApiCall
[Theory]
[InlineData(0, "method")]
[InlineData(-1, "method")]
[InlineData(1, "")]
public async Task IncrementApiCall_IgnoresInvalidInput(int userId, string methodName)
{
var (aggregator, bl) = CreateAggregator();
aggregator.IncrementApiCall(userId, TelemetryUserKind.User, TelemetryLicenseKind.Centron, methodName, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
bl.DidNotReceive().UpsertApiCallBatch(Arg.Any<IReadOnlyCollection<ApiCallBucketIncrement>>());
}
[Fact]
public async Task IncrementApiCall_IgnoresNullMethodName()
{
var (aggregator, bl) = CreateAggregator();
aggregator.IncrementApiCall(1, TelemetryUserKind.User, TelemetryLicenseKind.Centron, null!, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
bl.DidNotReceive().UpsertApiCallBatch(Arg.Any<IReadOnlyCollection<ApiCallBucketIncrement>>());
}
[Fact]
public async Task IncrementApiCall_NullLicenseKind_AggregatedSeparatelyFromKnownLicense()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<ApiCallBucketIncrement>? captured = null;
bl.UpsertApiCallBatch(Arg.Do<IReadOnlyCollection<ApiCallBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
// Both calls use the SAME UserKind so the test isolates the LicenseKind splitter
// (would still pass even if UserKind were the discriminator if we varied it).
aggregator.IncrementApiCall(1, TelemetryUserKind.User, null, "method", _bucket);
aggregator.IncrementApiCall(1, TelemetryUserKind.User, TelemetryLicenseKind.Centron, "method", _bucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
Assert.Equal(2, captured.Count);
Assert.Contains(captured, c => c.LicenseKind == null);
Assert.Contains(captured, c => c.LicenseKind == TelemetryLicenseKind.Centron);
}
#endregion
#region FlushAsync
[Fact]
public async Task FlushAsync_OnEmptyBuckets_DoesNotCallBl()
{
var (aggregator, bl) = CreateAggregator();
await aggregator.FlushAsync(CancellationToken.None);
bl.DidNotReceive().UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>());
bl.DidNotReceive().UpsertApiCallBatch(Arg.Any<IReadOnlyCollection<ApiCallBucketIncrement>>());
}
[Fact]
public async Task FlushAsync_PassesAggregatedIncrementsToBl()
{
var (aggregator, bl) = CreateAggregator();
IReadOnlyCollection<McpToolUsageBucketIncrement>? tools = null;
IReadOnlyCollection<ApiCallBucketIncrement>? apis = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => tools = b))
.Returns(Result.AsSuccess());
bl.UpsertApiCallBatch(Arg.Do<IReadOnlyCollection<ApiCallBucketIncrement>>(b => apis = b))
.Returns(Result.AsSuccess());
aggregator.IncrementToolUsage(1, "a", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(2, "b", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(3, "c", McpToolMode.User, _bucket);
aggregator.IncrementApiCall(1, TelemetryUserKind.User, TelemetryLicenseKind.Centron, "m1", _bucket);
aggregator.IncrementApiCall(2, TelemetryUserKind.WebAccount, TelemetryLicenseKind.Centron, "m2", _bucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(tools);
Assert.NotNull(apis);
Assert.Equal(3, tools.Count);
Assert.Equal(2, apis.Count);
Assert.Contains(tools, t => t.UserID == 2);
Assert.Contains(apis, a => a.UserKind == TelemetryUserKind.WebAccount);
}
[Fact]
public async Task FlushAsync_BatchesAt200ItemsPerCall()
{
var (aggregator, bl) = CreateAggregator();
var batchSizes = new List<int>();
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => batchSizes.Add(b.Count)))
.Returns(Result.AsSuccess());
for (int i = 0; i < 450; i++)
aggregator.IncrementToolUsage(i + 1, "tool", McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
Assert.Equal(new[] { 200, 200, 50 }, batchSizes);
}
[Fact]
public async Task FlushAsync_OnBlError_RestoresSnapshotIntoBuckets()
{
var (aggregator, bl) = CreateAggregator();
var responses = new Queue<Result>();
responses.Enqueue(Result.AsError("boom"));
responses.Enqueue(Result.AsSuccess());
IReadOnlyCollection<McpToolUsageBucketIncrement>? secondCallCaptured = null;
bl.UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>())
.Returns(call =>
{
var arg = call.Arg<IReadOnlyCollection<McpToolUsageBucketIncrement>>();
var result = responses.Dequeue();
if (result.Status == ResultStatus.Success)
secondCallCaptured = arg;
return result;
});
aggregator.IncrementToolUsage(1, "tool", McpToolMode.User, _bucket);
aggregator.IncrementToolUsage(1, "tool", McpToolMode.User, _bucket);
await aggregator.FlushAsync(CancellationToken.None);
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(secondCallCaptured);
var item = Assert.Single(secondCallCaptured);
Assert.Equal(2, item.Increment);
}
[Fact]
public async Task FlushAsync_OnBlError_RestoreCombinesWithIncrementsArrivedDuringFlush()
{
var (aggregator, bl) = CreateAggregator();
var blocker = new ManualResetEventSlim(false);
var midFlight = new ManualResetEventSlim(false);
var firstCall = true;
bl.UpsertMcpToolUsageBatch(Arg.Any<IReadOnlyCollection<McpToolUsageBucketIncrement>>())
.Returns(_ =>
{
if (firstCall)
{
firstCall = false;
midFlight.Set();
blocker.Wait();
return Result.AsError("boom");
}
return Result.AsSuccess();
});
aggregator.IncrementToolUsage(1, "tool", McpToolMode.User, _bucket);
var flushTask = Task.Run(() => aggregator.FlushAsync(CancellationToken.None));
midFlight.Wait();
aggregator.IncrementToolUsage(1, "tool", McpToolMode.User, _bucket);
blocker.Set();
await flushTask;
IReadOnlyCollection<McpToolUsageBucketIncrement>? captured = null;
bl.UpsertMcpToolUsageBatch(Arg.Do<IReadOnlyCollection<McpToolUsageBucketIncrement>>(b => captured = b))
.Returns(Result.AsSuccess());
await aggregator.FlushAsync(CancellationToken.None);
Assert.NotNull(captured);
var item = Assert.Single(captured);
Assert.Equal(2, item.Increment);
}
#endregion
}
@@ -0,0 +1,286 @@
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<McpToolUsageBucketIncrement>());
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<ApiCallBucketIncrement>());
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<ArtificialIntelligenceToolUsageBucketIncrement>());
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<long>(), 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<long>(), 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<long>(), 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<string>(), "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<string>());
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<string>());
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<McpToolUsageTelemetryMaps>();
m.FluentMappings.Add<ArtificialIntelligenceToolUsageTelemetryMaps>();
m.FluentMappings.Add<ApiCallTelemetryMaps>();
m.FluentMappings.Add<McpToolNameLookupMaps>();
m.FluentMappings.Add<ApiMethodNameLookupMaps>();
m.FluentMappings.Add<HardwareIDLookupMaps>();
});
var sessionFactory = fluentConfig.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfig.BuildConfiguration())
.Execute(false, true, false, session.Connection, null);
return session;
}
}
@@ -0,0 +1,73 @@
using Centron.Data.Entities.Telemetry;
using Centron.Interfaces.Administration.Logins;
using JetBrains.Annotations;
namespace Centron.Tests.BL.Telemetry;
[TestSubject(typeof(TelemetryLicenseKindMapping))]
public class TelemetryLicenseKindMappingTest
{
[Fact]
public void FromGuid_KnownLicense_ReturnsKind()
{
var kind = TelemetryLicenseKindMapping.FromGuid(LicenseGuids.Centron);
Assert.Equal(TelemetryLicenseKind.Centron, kind);
}
[Fact]
public void FromGuidString_KnownLicense_ReturnsKind()
{
var kind = TelemetryLicenseKindMapping.FromGuidString(LicenseGuids.ServiceBoard.ToString());
Assert.Equal(TelemetryLicenseKind.ServiceBoard, kind);
}
[Fact]
public void FromGuid_UnknownGuid_ReturnsNull()
{
var kind = TelemetryLicenseKindMapping.FromGuid(Guid.NewGuid());
Assert.Null(kind);
}
[Fact]
public void FromGuidString_NullOrEmpty_ReturnsNull()
{
Assert.Null(TelemetryLicenseKindMapping.FromGuidString(null));
Assert.Null(TelemetryLicenseKindMapping.FromGuidString(""));
Assert.Null(TelemetryLicenseKindMapping.FromGuidString(" "));
}
[Fact]
public void FromGuidString_Garbage_ReturnsNull()
{
Assert.Null(TelemetryLicenseKindMapping.FromGuidString("not-a-guid"));
}
[Fact]
public void ToGuid_KnownKind_ReturnsGuid()
{
var guid = TelemetryLicenseKindMapping.ToGuid(TelemetryLicenseKind.Centron);
Assert.Equal(LicenseGuids.Centron, guid);
}
[Fact]
public void ToGuid_Unknown_ReturnsNull()
{
var guid = TelemetryLicenseKindMapping.ToGuid(TelemetryLicenseKind.Unknown);
Assert.Null(guid);
}
[Fact]
public void RoundTrip_AllKnownLicenses_AreStable()
{
// Every enum value (except Unknown) must map to a real GUID and back to itself.
foreach (TelemetryLicenseKind kind in Enum.GetValues<TelemetryLicenseKind>())
{
if (kind == TelemetryLicenseKind.Unknown) continue;
var guid = TelemetryLicenseKindMapping.ToGuid(kind);
Assert.True(guid.HasValue, $"Enum value {kind} has no matching LicenseGuids field.");
var roundTrip = TelemetryLicenseKindMapping.FromGuid(guid.Value);
Assert.Equal(kind, roundTrip);
}
}
}
@@ -0,0 +1,173 @@
namespace Centron.Tests.BL.Warehousing.StockManagement;
using System.Linq;
using Centron.BusinessLogic.Warehousing.StockManagement;
using Centron.DAO;
using Centron.DAO.Mappings.Storage;
using Centron.DAO.Mappings.Warehousing.StockManagement;
using Centron.Data.Entities.Storage;
using Centron.Data.Entities.Warehousing.StockManagement;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
using Xunit;
/// <summary>
/// Integration tests for the StoragePlaceBL class.
/// These tests verify the behavior of storage place operations, particularly the cascading delete functionality.
/// </summary>
[TestSubject(typeof(StoragePlaceBL))]
public class StoragePlaceBLTest
{
[Fact]
public void SaveOrUpdateStoragePlace_CascadesDeleteToStorageAreas_WhenStateIsZero()
{
// Arrange
using var session = CreateSession();
var daoSession = new DAOSession(session);
// Create a storage place
var storagePlace = new StoragePlace
{
Name = "TestPlace",
Description = "Test Storage Place",
State = 1 // Active
};
// Save the storage place to get an I3D
session.Save(storagePlace);
session.Flush();
// Create associated storage areas
var storageArea1 = new StorageArea
{
ShortDescription = "Area1",
Description = "Test Storage Area 1",
StoragePlaceI3D = storagePlace.I3D,
State = 1 // Active
};
var storageArea2 = new StorageArea
{
ShortDescription = "Area2",
Description = "Test Storage Area 2",
StoragePlaceI3D = storagePlace.I3D,
State = 1 // Active
};
// Save the storage areas
session.Save(storageArea1);
session.Save(storageArea2);
session.Flush();
// Create the system under test (SUT)
var storagePlaceBL = new StoragePlaceBL(daoSession);
// Mark the storage place as deleted
storagePlace.State = 0;
// Act
storagePlaceBL.SaveOrUpdateStoragePlace(storagePlace);
// Refresh session to ensure we're getting fresh data
session.Clear();
// Assert
// Verify the storage place is marked as deleted
var updatedStoragePlace = session.Get<StoragePlace>(storagePlace.I3D);
Assert.Equal(0, updatedStoragePlace.State);
// Verify all associated storage areas are marked as deleted
var updatedStorageArea1 = session.Get<StorageArea>(storageArea1.I3D);
var updatedStorageArea2 = session.Get<StorageArea>(storageArea2.I3D);
Assert.Equal(0, updatedStorageArea1.State);
Assert.Equal(0, updatedStorageArea2.State);
}
[Fact]
public void SaveOrUpdateStoragePlace_DoesNotCascadeDelete_WhenStateIsNotZero()
{
// Arrange
using var session = CreateSession();
var daoSession = new DAOSession(session);
// Create a storage place
var storagePlace = new StoragePlace
{
Name = "TestPlace",
Description = "Test Storage Place",
State = 1 // Initially active
};
// Save the storage place to get an I3D
session.Save(storagePlace);
session.Flush();
// Create associated storage areas
var storageArea1 = new StorageArea
{
ShortDescription = "Area1",
Description = "Test Storage Area 1",
StoragePlaceI3D = storagePlace.I3D,
State = 1 // Initially active
};
var storageArea2 = new StorageArea
{
ShortDescription = "Area2",
Description = "Test Storage Area 2",
StoragePlaceI3D = storagePlace.I3D,
State = 1 // Initially active
};
// Save the storage areas
session.Save(storageArea1);
session.Save(storageArea2);
session.Flush();
// Create the system under test (SUT)
var storagePlaceBL = new StoragePlaceBL(daoSession);
// Update the storage place to a different non-deleted state (e.g., 2 for a different active state)
storagePlace.State = 2;
// Act
storagePlaceBL.SaveOrUpdateStoragePlace(storagePlace);
// Refresh session to ensure we're getting fresh data
session.Clear();
// Assert
// Verify the storage place has the new state
var updatedStoragePlace = session.Get<StoragePlace>(storagePlace.I3D);
Assert.Equal(2, updatedStoragePlace.State);
// Verify storage areas remain active (not changed)
var updatedStorageArea1 = session.Get<StorageArea>(storageArea1.I3D);
var updatedStorageArea2 = session.Get<StorageArea>(storageArea2.I3D);
Assert.Equal(1, updatedStorageArea1.State);
Assert.Equal(1, updatedStorageArea2.State);
}
private static ISession CreateSession()
{
var fluentConfig = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m =>
{
m.FluentMappings.Add<StoragePlaceMaps>();
m.FluentMappings.Add<StorageAreaMaps>();
});
var sessionFactory = fluentConfig.BuildSessionFactory();
var session = sessionFactory.OpenSession();
new SchemaExport(fluentConfig.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
return session;
}
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="JetBrains.Annotations" Version="2025.2.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="System.Data.SQLite" Version="1.0.119" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\src\backend\Centron.BL\Centron.BL.csproj" />
<ProjectReference Include="..\..\..\src\backend\Centron.DAO\Centron.DAO.csproj" />
<ProjectReference Include="..\..\..\src\backend\Centron.Entities\Centron.Entities.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,145 @@
using Centron.DAO.Mappings.Accounts;
using Centron.DAO.Mappings.CustomerArea;
using Centron.DAO.UserTypes;
using Centron.Data.Entities.Accounts;
using Centron.Data.Entities.CustomerArea;
using Centron.Data.Entities.ObjectTypes;
using Centron.Data.Entities.Sales.Receipts;
using Centron.Interfaces.Accounts;
using Centron.Interfaces.CustomerArea;
using Centron.Interfaces.Sales.CustomerAssets;
using FluentNHibernate.Mapping;
namespace Centron.Tests.BL.DatabaseMappings
{
/// <summary>
/// <b>IMPORTANT</b>: This is a replacement class for the real <see cref="AccountCustomerMaps"/> class for running in-memory unit tests.
/// Make sure to keep this class in sync with the real class.
/// </summary>
public class AccountCustomerMapsForTest : ClassMap<AccountCustomer>
{
public AccountCustomerMapsForTest()
{
Table("AccountCustomers");
Id(m => m.I3D).Column("I3D");
Map(m => m.BookKeepingExportDate).Column("BookKeepingExportDate").Nullable();
Map(m => m.Number).Column("Number");
Map(m => m.ReceiptConditionOfferI3D).Column("ReceiptConditionOfferI3D").Nullable();
Map(m => m.ReceiptConditionOrderI3D).Column("ReceiptConditionOrderI3D").Nullable();
Map(m => m.ReceiptConditionDeliveryListI3D).Column("ReceiptConditionDeliveryListI3D").Nullable();
Map(m => m.ReceiptConditionPickupListI3D).Column("ReceiptConditionPickupListI3D").Nullable();
Map(m => m.InvoiceDeliveryKind).Column("InvoiceDeliveryKind").Nullable();
Map(m => m.AlternativeInvoiceMailRecipientI3D).Column("AlternativeInvoiceMailRecipientI3D").Nullable();
Map(m => m.SalesControllingI3D).Column("SalesControllingI3D").Nullable();
Map(m => m.RootDirI3D).Column("RootDirI3D").Nullable();
Map(m => m.AlternativeDeliveryAddressContactI3D).Column("AlternativeDeliveryAddressContactI3D").Nullable();
Map(m => m.ClassificationI3D).Column("ClassificationI3D").Nullable();
Map(m => m.BookKeepingExportEmployeeI3D).Column("BookKeepingExportEmployeeI3D").Nullable();
Map(m => m.SpecialAgreementI3D).Column("SpecialAgreementI3D").Nullable();
Map(m => m.WarehouseI3D).Column("WarehouseI3D").Nullable();
Map(m => m.PriceListFromDistributorI3D).Column("PriceListFromDistributorI3D").Nullable();
Map(m => m.LockOrderAfterDunningLevel).Column("LockOrderAfterDunningLevel").Nullable();
Map(m => m.AlternativeInvoiceAccountI3D).Column("AlternativeInvoiceAccountI3D").Nullable();
Map(m => m.AlternativeInvoiceAddressI3D).Column("AlternativeInvoiceAddressI3D").Nullable();
Map(m => m.AlternativeInvoiceAddressContactI3D).Column("AlternativeInvoiceAddressContactI3D").Nullable();
Map(m => m.AlternativeDeliveryAccountI3D).Column("AlternativeDeliveryAccountI3D").Nullable();
Map(m => m.AlternativeDeliveryAddressI3D).Column("AlternativeDeliveryAddressI3D").Nullable();
Map(m => m.PriceList).Column("PriceList").Nullable();
Map(m => m.DunningLetterRecipientPersonI3D).Column("DunningLetterRecipientPersonI3D").Nullable();
Map(m => m.DunningLetterUseAlternativeInvoiceAddress).Column("DunningLetterUseAlternativeInvoiceAddress").Not.Nullable();
Map(m => m.DunningLetterAfterDays1).Column("DunningLetterAfterDays1").Nullable();
Map(m => m.DunningLetterAfterDays2).Column("DunningLetterAfterDays2").Nullable();
Map(m => m.DunningLetterAfterDays3).Column("DunningLetterAfterDays3").Nullable();
Map(m => m.ReceiptConditionInvoiceI3D).Column("ReceiptConditionInvoiceI3D").Nullable();
Map(m => m.ReceiptConditionCreditVoucherI3D).Column("ReceiptConditionCreditVoucherI3D").Nullable();
Map(m => m.ReceiptConditionDeliveryI3D).Column("ReceiptConditionDeliveryI3D").Nullable();
Map(m => m.ReceiptConditionDeliveryOfferI3D).Column("ReceiptConditionDeliveryOfferI3D").Nullable();
Map(m => m.ReceiptConditionDeliveryOrderI3D).Column("ReceiptConditionDeliveryOrderI3D").Nullable();
Map(m => m.LimitCalculationKind).Column("LimitCalculationKind").Nullable();
//Map(m => m.IsAccountKind1).Column("IsAccountKind1");
//Map(m => m.IsAccountKind2).Column("IsAccountKind2");
//Map(m => m.IsAccountKind3).Column("IsAccountKind3");
//Map(m => m.IsAccountKind4).Column("IsAccountKind4");
//Map(m => m.IsAccountKind5).Column("IsAccountKind5");
Map(m => m.IsPurchaseOrderNumberRequired).Column("IsPurchaseOrderNumberRequired");
Map(m => m.PrintProductionConfiguration).Column("PrintProductionConfiguration");
Map(m => m.ShippingCostsDeaktivated).Column("ShippingCostsDeaktivated");
Map(m => m.HasTicketApproval).Column("HasTicketApproval").Nullable();
Map(m => m.IsExclusiveOfVAT).Column("IsExclusiveOfVAT");
Map(m => m.IsBookKeepingExportDone).Column("IsBookKeepingExportDone");
Map(m => m.IsBookKeepingExportDeaktivated).Column("IsBookKeepingExportDeaktivated");
Map(m => m.IsProjectNumberRequired).Column("IsProjectNumberRequired");
Map(m => m.IsDiscountTextVisibilityDeaktivated).Column("IsDiscountTextVisibilityDeaktivated");
Map(m => m.IsProductionConfigurationMandatory).Column("IsProductionConfigurationMandatory");
Map(m => m.Discount).Column("Discount");
// IMPORTANT: This is the problematic column that causes a syntax error in the SQLite in-memory database.
// The original name of the column is 'Limit' which is a reserved keyword in SQLite.
// This is changed here for 'CreditLimit' to avoid the syntax error.
Map(m => m.Limit).Column("CreditLimit");
Map(m => m.ExtraChargeRetailPrice).Column("ExtraChargeRetailPrice");
Map(m => m.BookKeepingNumber).Column("BookKeepingNumber").Length(64);
Map(m => m.DeliveryText).Column("DeliveryText").Length(500).Nullable();
Map(m => m.DunningLetterKind).Column("DunningLetterKind").Length(50).Nullable();
Map(m => m.AlternativeInvoiceReceiver).Column("AlternativeInvoiceReceiver").Length(500).Nullable();
Map(m => m.AlternativeDeliveryReceiver).Column("AlternativeDeliveryReceiver").Length(500).Nullable();
Map(m => m.CommentOffer).Column("CommentOffer").Length(int.MaxValue).Nullable();
Map(m => m.BookKeepingCollectionAccount).Column("BookKeepingCollectionAccount").Length(24).Nullable();
Map(m => m.ProductRecipientNumber).Column("ProductRecipientNumber").Length(50).Nullable();
Map(m => m.MailNotificationAtHelpdeskCC).Column("MailNotificationAtHelpdeskCC").Length(255).Nullable();
Map(m => m.MailNotificationAtHelpdeskBCC).Column("MailNotificationAtHelpdeskBCC").Length(255).Nullable();
Map(m => m.CommentOrder).Column("CommentOrder").Length(int.MaxValue).Nullable();
Map(m => m.CommentDeliveryList).Column("CommentDeliveryList").Length(int.MaxValue).Nullable();
Map(m => m.CommentPickupList).Column("CommentPickupList").Length(int.MaxValue).Nullable();
Map(m => m.CommentInvoice).Column("CommentInvoice").Length(int.MaxValue).Nullable();
Map(m => m.CommentCreditVoucher).Column("CommentCreditVoucher").Length(int.MaxValue).Nullable();
Map(m => m.CommentHelpdesk).Column("CommentHelpdesk").Length(int.MaxValue).Nullable();
Map(m => m.DeliveryOption).Column("DeliveryOption").CustomType<DeliveryOption>().Nullable();
Map(m => m.CountServer).Column("CountServer").Nullable();
Map(m => m.CountPc).Column("CountPc").Nullable();
Map(m => m.ServerManufacturor).Column("ServerManufacturor").Length(80).Nullable();
Map(m => m.PcManufacturor).Column("PcManufacturor").Length(80).Nullable();
Map(m => m.PrinterManufacturor).Column("PrinterManufacturor").Length(80).Nullable();
Map(m => m.AcquisionComplete).Column("AcquisionComplete").Nullable();
Map(m => m.AcquisionImportant).Column("AcquisionImportant").Nullable();
Map(m => m.Vip).Column("Vip").Nullable();
Map(m => m.AdditionalInformation).Column("AdditionalInformation").Length(int.MaxValue).Nullable();
Map(m => m.BoughtByThusFar).Column("BoughtByThusFar").Length(80).Nullable();
Map(m => m.CrmText1).Column("CrmText1").Length(100).Nullable();
Map(m => m.CrmText2).Column("CrmText2").Length(100).Nullable();
Map(m => m.CrmText3).Column("CrmText3").Length(100).Nullable();
Map(m => m.CrmText4).Column("CrmText4").Length(100).Nullable();
Map(m => m.CrmText5).Column("CrmText5").Length(100).Nullable();
Map(m => m.CrmText6).Column("CrmText6").Length(100).Nullable();
Map(m => m.AcquisionFreeDate1).Column("AcquisionFreeDate1").Nullable();
Map(m => m.AcquisionFreeDate2).Column("AcquisionFreeDate2").Nullable();
Map(m => m.CoreDataComplete).Column("CoreDataComplete").Nullable();
Map(m => m.RiverbirdMsp).Column("RiverbirdMsp").Length(32).Nullable();
Map(m => m.HelpdeskClosingDontNotifyCustomer).Column("HelpdeskClosingDontNotifyCustomer").Nullable();
Map(m => m.ExportZUGFeRDDocument).Column("ExportZUGFeRDDocument").Nullable();
Map(m => m.LeitwegID).Column("LeitwegID").Length(50).Nullable();
Map(m => m.OwnSupplierNumber).Column("OwnSupplierNumber").Length(20).Nullable();
Map(m => m.ProvisionSchemaI3D).Column("ProvisionSchemaI3D").Nullable();
Map(m => m.CanSeeTicketsSBO).Column("CanSeeTicketsSBO").Not.Nullable();
Map(m => m.TicketsVisibleFromDateSBO).Column("TicketsVisibleFromDateSBO").Nullable();
Map(m => m.CustomerApprovalEnabledSBO).Column("CustomerApprovalEnabledSBO").Not.Nullable();
Map(m => m.DunningStop).Column("DunningStop").Nullable();
Map(m => m.DunningStopBegin).Column("DunningStopBegin").Nullable();
Map(m => m.DunningStopEnd).Column("DunningStopEnd").Nullable();
Map(m => m.DunningInfo).Column("DunningInfo").Length(int.MaxValue).Nullable();
Map(m => m.RiverbirdCustomerReference).Column("RiverbirdCustomerReference").Nullable();
Map(m => m.TelekomDiveCustomerNote).Column("TelekomDiveCustomerNote").Length(300).Nullable();
Map(m => m.ShowTelekomDiveCustomerNote).Column("ShowTelekomDiveCustomerNote").Nullable();
Map(m => m.MandatorBank).Column("MandatorBank").Nullable();
References<ReceiptReceiver>(f => f.ReceiptReceiverDelivery).Column("ReceiptReceiverDeliveryI3D").Nullable().Fetch.Join().Cascade.All();
References<ReceiptReceiver>(f => f.ReceiptReceiverInvoice).Column("ReceiptReceiverInvoiceI3D").Nullable().Fetch.Join().Cascade.All();
}
}
}
@@ -0,0 +1,116 @@
using Centron.BusinessLogic.WebServices;
using Centron.DAO.Mappings.Accounts;
using Centron.DAO.Mappings.Sales.Receipts;
using Centron.Data.Entities.Accounts;
using Centron.Data.Entities.Sales.Receipts;
using Centron.Tests.BL.DatabaseMappings;
using CentronSoftware.Centron.WebServices.Entities.Accounts;
using CentronSoftware.Centron.WebServices.Entities.Sales.Receipts.ReceiptReceiver;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using JetBrains.Annotations;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.DAO.Mappings.Accounts;
[TestSubject(typeof(AccountCustomerMaps))]
public class AccountCustomerMapsTest
{
[Fact]
public void ReceiptReceiverCanBeMapped()
{
var session = CreateSession();
var receipt = new AccountCustomer
{
I3D = 1,
Number = 2,
ReceiptReceiverDelivery = new ReceiptReceiver()
{
CompanyName = "Company",
Department = "Department",
ContactName = "Contact",
ContactDepartment = "C Department",
Street = "Street",
HouseNumber = "1",
Zip = "12345",
City = "City",
Country = "Country",
CountryI3D = 4,
PostOfficeBox = "",
HasPostOfficeBox = false,
AdditionalAddressSupplement = ""
},
ReceiptReceiverInvoice = new ReceiptReceiver()
{
CompanyName = "Company Invoice",
Department = "Department",
ContactName = "Contact",
ContactDepartment = "C Department",
Street = "Street Invoice",
HouseNumber = "1",
Zip = "12345",
City = "City",
Country = "Country",
CountryI3D = 4,
PostOfficeBox = "",
HasPostOfficeBox = false,
AdditionalAddressSupplement = ""
}
,
};
session.Save(receipt);
session.Flush();
var savedEntity = session.Get<AccountCustomer>(1);
Assert.NotNull(savedEntity);
var dto = ObjectMapper.Map<AccountCustomer, AccountCustomerDTO>(savedEntity);
Assert.NotNull(dto);
Assert.Equal("Company Invoice", dto.ReceiptReceiverInvoice.CompanyName);
var savedReceiptReceivers = session.Query<ReceiptReceiver>().ToList();
Assert.Equal(2, savedReceiptReceivers.Count);
dto.ReceiptReceiverDelivery = new ReceiptReceiverDTO()
{
CompanyName = "New Company Delivery",
Department = "Department",
ContactName = "Contact",
ContactDepartment = "C Department",
Street = "New Street Delivery",
HouseNumber = "1",
Zip = "12345",
City = "City",
Country = "Country",
CountryI3D = 4,
PostOfficeBox = "",
HasPostOfficeBox = false,
AdditionalAddressSupplement = ""
};
savedEntity.ReceiptReceiverDelivery = null;
savedEntity = ObjectMapper.Map(dto, savedEntity);
session.Save(savedEntity);
session.Flush();
var newSavedEntity = session.Get<AccountCustomer>(1);
Assert.NotNull(newSavedEntity);
dto = ObjectMapper.Map<AccountCustomer, AccountCustomerDTO>(newSavedEntity);
Assert.NotNull(dto);
Assert.Equal("New Company Delivery", dto.ReceiptReceiverDelivery.CompanyName);
savedReceiptReceivers = session.Query<ReceiptReceiver>().ToList();
Assert.Equal(3, savedReceiptReceivers.Count);
}
public ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<AccountCustomerMapsForTest>())
.Mappings(m => m.FluentMappings.Add<ReceiptReceiverMaps>())
;
ISessionFactory sessionFactory = fluentConfiguration.BuildSessionFactory();
ISession session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
return session;
}
}
@@ -0,0 +1,177 @@
using Centron.BusinessLogic.WebServices;
using Centron.DAO.Mappings.Sales.Receipts;
using Centron.DAO.Mappings.Sales.Receipts.Invoices;
using Centron.Data.Entities.Sales.Receipts;
using Centron.Data.Entities.Sales.Receipts.Invoices;
using Centron.Data.WebServices.Sales.Receipts.Invoices;
using Centron.Interfaces.Administration.Environment;
using Centron.Interfaces.Sales.Receipts;
using Centron.Interfaces.Sales.Receipts.Invoices;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Tool.hbm2ddl;
namespace Centron.Tests.DAO.Mappings.Sales.Receipts;
public class ReceiptBaseMapsTest
{
[Fact]
public void ReceiptReceiverCanBeMapped()
{
var session = CreateSession();
var receipt = new ReceiptInvoice()
{
I3D = 1,
CustomerI3D = 10,
Information = "Information",
ShowInformation = false,
IsCashAsset = false,
PurchaseOrderNumber = "12345",
AdditionalText = "Additional text",
ProjectNumber = "123456",
DeliveryConditionText = "",
PaymentConditionText = "",
DeliveryAddress = "Delivery address",
DeliveryAddressInformation = "",
InvoiceAddress = "Invoice address",
InvoiceAddressInformation = "",
LicenseeAddress = "Licensee address",
LicenseeAddressInformation = "",
IsPartialDeliveryPossible = false,
EsrAmount = "1",
EsrCodelineAmount = "1",
EsrReferenceNumber = "123",
IsFixed = false,
UsedAlternativeDeliveryAddress = false,
UsedAlternativeInvoiceAddress = false,
Provision = new List<ReceiptProvision>(),
PaidFC = 2,
ExternalInvoiceNumber = "345",
CurrencyFactorIsFixed = true,
CollectiveAccount = "Collective account",
TrackingNumber = "567",
TrackingNumberURL = "TrackURL",
Number = 2,
Date = DateTime.Now,
Version = 1,
State = ReceiptState.Active,
BranchOrigin = BranchOrigin.Creator,
CurrencyFactor = 1,
CurrencyString = "€",
ExclusiveOfVAT = false,
Receiver = "Receiver",
Phone = "12345",
Fax = "12345",
Email = "mail@a.com",
Street = "Street",
HasPostOfficeBox = false,
PostOfficeBox = "",
Zip = "12345",
City = "City",
ContactName = "Contact name",
CreatedThroughApplicationVersion = "2.0.2504.5",
ChangedThroughApplicationVersion = "",
ChangedThroughApplication = CreatedThroughApplication.WebServices,
ConcurrencyControlGuid = Guid.NewGuid(),
ReceiptReceiver = new ReceiptReceiver()
{
CompanyName = "Company",
Department = "Department",
ContactName = "Contact",
ContactDepartment = "C Department",
Street = "Street",
HouseNumber = "1",
Zip = "12345",
City = "City",
Country = "Country",
CountryI3D = 4,
PostOfficeBox = "",
HasPostOfficeBox = false,
AdditionalAddressSupplement = ""
},
ReceiptReceiverInvoice = new ReceiptReceiver()
{
CompanyName = "Company",
Department = "Department",
ContactName = "Contact",
ContactDepartment = "C Department",
Street = "Street Invoice",
HouseNumber = "1",
Zip = "12345",
City = "City",
Country = "Country",
CountryI3D = 4,
PostOfficeBox = "",
HasPostOfficeBox = false,
AdditionalAddressSupplement = ""
}
,
ReceiptReceiverLicense = new ReceiptReceiver()
{
CompanyName = "Company License",
Department = "Department",
ContactName = "Contact",
ContactDepartment = "C Department",
Street = "Street",
HouseNumber = "1",
Zip = "12345",
City = "City",
Country = "Country",
CountryI3D = 4,
PostOfficeBox = "",
HasPostOfficeBox = false,
AdditionalAddressSupplement = ""
}
};
session.Save(receipt);
session.Flush();
var savedEntity = session.Get<ReceiptInvoice>(1);
Assert.NotNull(savedEntity);
var dto = ObjectMapper.Map<IReceiptInvoice, ReceiptInvoiceDTO>(savedEntity);
Assert.NotNull(dto);
Assert.Equal("Company License", dto.ReceiptReceiverLicense.CompanyName);
var savedReceiptReceivers = session.Query<ReceiptReceiver>().ToList();
Assert.Equal(3, savedReceiptReceivers.Count);
}
public ISession CreateSession()
{
var fluentConfiguration = Fluently.Configure()
.Database(SQLiteConfiguration.Standard.InMemory)
.Mappings(m => m.FluentMappings.Add<ReceiptInvoiceMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptReceiverMaps>())
.Mappings(m => m.FluentMappings.Add<ReceiptInvoiceItemMaps>())
;
ISessionFactory sessionFactory = fluentConfiguration.BuildSessionFactory();
ISession session = sessionFactory.OpenSession();
new SchemaExport(fluentConfiguration.BuildConfiguration()).Execute(true, true, false, session.Connection, Console.Out);
return session;
}
}