diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7088a97dc9..3a28a6e403 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -440,6 +440,39 @@ jobs: - name: MySQL Tests run: dotnet test ./tests/Paramore.Brighter.MySQL.Tests/Paramore.Brighter.MySQL.Tests.csproj --filter "Fragile!=CI" --configuration Release --logger "console;verbosity=normal" --logger GitHubActions --blame -v n + oracle-ci: + runs-on: ubuntu-latest + timeout-minutes: 12 + needs: [build] + + services: + oracle: + image: gvenzl/oracle-free:latest + ports: + - 1521:1521 + env: + ORACLE_PASSWORD: oracle + APP_USER: brighter + APP_USER_PASSWORD: BrighterTests1! + options: >- + --health-cmd healthcheck.sh + --health-interval 10s + --health-timeout 5s + --health-retries 30 + + steps: + - uses: actions/checkout@v7 + - name: Setup dotnet + uses: actions/setup-dotnet@v5 + with: + dotnet-version: | + 9.0.x + 10.0.x + - name: Grant DBMS_LOCK to brighter user + run: docker exec -i "${{ job.services.oracle.id }}" sqlplus -s "/ as sysdba" @/dev/stdin < docker/oracle-init/01-grants.sql + - name: Oracle Tests + run: dotnet test ./tests/Paramore.Brighter.Oracle.Tests/Paramore.Brighter.Oracle.Tests.csproj --filter "Fragile!=CI" --configuration Release --logger "console;verbosity=normal" --logger GitHubActions --blame -v n + dynamo-ci: runs-on: ubuntu-latest timeout-minutes: 8 diff --git a/Brighter.slnx b/Brighter.slnx index 794ccea98c..b36ace71ba 100644 --- a/Brighter.slnx +++ b/Brighter.slnx @@ -223,6 +223,7 @@ + @@ -250,6 +251,7 @@ + @@ -266,6 +268,7 @@ + @@ -276,6 +279,7 @@ + @@ -303,6 +307,7 @@ + @@ -310,6 +315,7 @@ + diff --git a/Directory.Packages.props b/Directory.Packages.props index 34b9e51b87..516a1b8044 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -127,6 +127,8 @@ + + diff --git a/docker-compose-oracle.yaml b/docker-compose-oracle.yaml new file mode 100644 index 0000000000..4d4a3f3439 --- /dev/null +++ b/docker-compose-oracle.yaml @@ -0,0 +1,17 @@ +# Oracle Database Free (gvenzl/oracle-free) +# +# First startup may take a few minutes while the database is initialized. +# Connection string (ODP.NET): +# Data Source=//localhost:1521/FREEPDB1;User Id=system;Password=BrighterTests1!; + +services: + oracle: + image: gvenzl/oracle-free:slim + ports: + - "1521:1521" + volumes: + - ./docker/oracle-init:/container-entrypoint-initdb.d + environment: + ORACLE_RANDOM_PASSWORD: true + APP_USER: brighter + APP_USER_PASSWORD: BrighterTests1! diff --git a/docker/oracle-init/01-grants.sql b/docker/oracle-init/01-grants.sql new file mode 100644 index 0000000000..6d0891c709 --- /dev/null +++ b/docker/oracle-init/01-grants.sql @@ -0,0 +1,2 @@ +ALTER SESSION SET CONTAINER = FREEPDB1; +GRANT EXECUTE ON DBMS_LOCK TO brighter; diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/IOracleAdvisoryLock.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/IOracleAdvisoryLock.cs new file mode 100644 index 0000000000..279158b4d9 --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/IOracleAdvisoryLock.cs @@ -0,0 +1,75 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Encapsulates the Oracle session-level advisory-lock primitive used to serialise +/// box-table migrations. Backed by DBMS_LOCK.REQUEST / DBMS_LOCK.RELEASE. +/// Requires the executing user to hold EXECUTE ON DBMS_LOCK (or +/// EXECUTE ANY PROCEDURE). +/// +/// +/// The default implementation is . +/// accepts an injected instance through its constructor for testability and for advanced +/// integrators. Lock-key derivation is performed by the runner via +/// ; the abstraction owns only the +/// DBMS_LOCK.REQUEST / DBMS_LOCK.RELEASE PL/SQL calls. +/// +public interface IOracleAdvisoryLock +{ + /// + /// Acquires a session-level exclusive advisory lock via DBMS_LOCK.REQUEST. + /// + /// The open on which to run the PL/SQL block. + /// The lock name; must be within the 128-character DBMS_LOCK.ALLOCATE_UNIQUE limit. + /// Callers should derive via . + /// Maximum wait. Values are truncated to whole seconds; zero means + /// try-once (NOWAIT). + /// Cancellation token observed by the underlying command. + /// Thrown when DBMS_LOCK.REQUEST returns 1 + /// (could not acquire within ). + /// Thrown when DBMS_LOCK.REQUEST returns + /// 2 (deadlock), 3 (parameter error), or 5 (illegal handle). + Task AcquireAsync( + OracleConnection connection, string lockKey, + TimeSpan timeout, CancellationToken cancellationToken); + + /// + /// Releases the advisory lock via DBMS_LOCK.RELEASE. + /// + /// The same on which the lock was acquired. + /// The lock name used at acquisition. + /// Cancellation token. + /// + /// true — released successfully (code 0). + /// false — not the owner or parameter error (codes 3–5). + /// null — lock was never acquired (no handle allocated for this session). + /// + Task ReleaseAsync( + OracleConnection connection, string lockKey, + CancellationToken cancellationToken); +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLock.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLock.cs new file mode 100644 index 0000000000..510aab29fb --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLock.cs @@ -0,0 +1,194 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Default backed by Oracle DBMS_LOCK.ALLOCATE_UNIQUE, +/// DBMS_LOCK.REQUEST, and DBMS_LOCK.RELEASE. +/// +/// +/// +/// The connecting user must hold EXECUTE ON DBMS_LOCK (or +/// EXECUTE ANY PROCEDURE). Both ALLOCATE_UNIQUE and REQUEST are called +/// in the same session so the allocated handle is stable for the lock's lifetime. +/// +/// +/// Because ALLOCATE_UNIQUE is idempotent for the same lock name within the same +/// database, ReleaseAsync re-allocates the handle for the given +/// before releasing — this keeps the interface stateless and avoids storing per-session state +/// on the instance. +/// +/// +/// DBMS_LOCK.REQUEST return codes: +/// 0 = success, 1 = timeout, 2 = deadlock, 3 = parameter error, 4 = already held (same +/// session), 5 = illegal lock handle. +/// +/// +public class OracleAdvisoryLock : IOracleAdvisoryLock +{ + /// + public async Task AcquireAsync( + OracleConnection connection, string lockKey, + TimeSpan timeout, CancellationToken cancellationToken) + { + if (timeout < TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(timeout), timeout, + "Migration lock timeout must be non-negative."); + } + + var timeoutSeconds = (int)Math.Ceiling(timeout.TotalSeconds); + var lockHandle = await AllocateLockHandleAsync(connection, lockKey, cancellationToken); + if (string.IsNullOrEmpty(lockHandle)) + { + throw new OracleAdvisoryLockException($"DBMS_LOCK.ALLOCATE_UNIQUE returned an empty handle for '{lockKey}'."); + } + + var result = await RequestLockAsync(connection, lockHandle!, timeoutSeconds, cancellationToken); + + switch (result) + { + case 0: + case 4: + return; + case 1: throw new TimeoutException( + $"Could not acquire migration lock '{lockKey}' within {timeoutSeconds}s."); + case 2: throw new OracleAdvisoryLockException( + $"DBMS_LOCK.REQUEST on '{lockKey}' detected a deadlock (return code 2)."); + default: throw new OracleAdvisoryLockException( + $"DBMS_LOCK.REQUEST on '{lockKey}' failed with return code {result}."); + } + } + + /// + public async Task ReleaseAsync( + OracleConnection connection, string lockKey, + CancellationToken cancellationToken) + { + try + { + var lockHandle = await AllocateLockHandleAsync(connection, lockKey, cancellationToken); + if (string.IsNullOrEmpty(lockHandle)) + { + return null; + } + + var result = await ReleaseLockByHandleAsync(connection, lockHandle!, cancellationToken); + return result == 0; + } + catch + { + return null; + } + } + + private static async Task AllocateLockHandleAsync( + OracleConnection connection, string lockKey, + CancellationToken cancellationToken) + { + const string plsql = """ + BEGIN + DBMS_LOCK.ALLOCATE_UNIQUE(:name, :handle); + END; + """; + +#if NETFRAMEWORK + using var cmd = new OracleCommand(plsql, connection); +#else + await using var cmd = new OracleCommand(plsql, connection); +#endif + + var handleParam = new OracleParameter("handle", OracleDbType.Varchar2, 128) + { + Direction = ParameterDirection.Output + }; + + cmd.Parameters.Add( + new OracleParameter("name", OracleDbType.Varchar2) + { + Value = lockKey, + Direction = ParameterDirection.Input + }); + cmd.Parameters.Add(handleParam); + + await cmd.ExecuteNonQueryAsync(cancellationToken); + return handleParam.Value?.ToString(); + } + + private static async Task RequestLockAsync( + OracleConnection connection, + string lockHandle, + int timeoutSeconds, + CancellationToken cancellationToken) + { + const string plsql = """ + BEGIN + :result := DBMS_LOCK.REQUEST(:lockhandle, 6, :timeout, FALSE); + END; + """; + +#if NETFRAMEWORK + using var cmd = new OracleCommand(plsql, connection); +#else + await using var cmd = new OracleCommand(plsql, connection); +#endif + + var resultParam = new OracleParameter("result", OracleDbType.Int32, ParameterDirection.Output); + cmd.Parameters.Add(resultParam); + cmd.Parameters.Add("lockhandle", OracleDbType.Varchar2, lockHandle, ParameterDirection.Input); + cmd.Parameters.Add("timeout", OracleDbType.Int32, timeoutSeconds, ParameterDirection.Input); + + await cmd.ExecuteNonQueryAsync(cancellationToken); + return Convert.ToInt32(resultParam.Value?.ToString()); + } + + private static async Task ReleaseLockByHandleAsync( + OracleConnection connection, + string lockHandle, + CancellationToken cancellationToken) + { + const string plsql = """ + BEGIN + :result := DBMS_LOCK.RELEASE(:lockhandle); + END; + """; + +#if NETFRAMEWORK + using var cmd = new OracleCommand(plsql, connection); +#else + await using var cmd = new OracleCommand(plsql, connection); +#endif + + var resultParam = new OracleParameter("result", OracleDbType.Int32, ParameterDirection.Output); + cmd.Parameters.Add(resultParam); + cmd.Parameters.Add("lockhandle", OracleDbType.Varchar2, lockHandle, ParameterDirection.Input); + + await cmd.ExecuteNonQueryAsync(cancellationToken); + return Convert.ToInt32(resultParam.Value?.ToString()); + } +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLockException.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLockException.cs new file mode 100644 index 0000000000..e1fcccc44d --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleAdvisoryLockException.cs @@ -0,0 +1,43 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Thrown when DBMS_LOCK.REQUEST returns an error code that is not a timeout. +/// Distinct from so operators can tell a contended lock +/// (timeout, code 1) apart from a server-side error (deadlock code 2, parameter error +/// code 3, illegal handle code 5). +/// +public class OracleAdvisoryLockException : Exception +{ + /// + /// Initializes a new instance of . + /// + public OracleAdvisoryLockException(string message) : base(message) { } + + /// + /// Initializes a new instance of with an inner exception. + /// + public OracleAdvisoryLockException(string message, Exception inner) : base(message, inner) { } +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxDetectionHelper.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxDetectionHelper.cs new file mode 100644 index 0000000000..058bc81b5c --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxDetectionHelper.cs @@ -0,0 +1,235 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Oracle detection queries used by the box provisioners (pre-lock) and the migration +/// runner (under DBMS_LOCK). Oracle identifiers are stored in uppercase by the data +/// dictionary; comparisons against ALL_TABLES and ALL_TAB_COLUMNS are performed +/// with UPPER() so callers may pass mixed-case names. The column set comparer is +/// to match PascalCase LogicalColumns +/// stored on each migration against Oracle's uppercase column name values. +/// +/// +/// Oracle DDL has implicit per-statement commit, so the OracleTransaction parameter +/// is accepted but not consumed. The runner relies on DBMS_LOCK for mutual exclusion +/// across instances. +/// +/// When is null, queries resolve the owner via +/// SYS_CONTEXT('USERENV','CURRENT_SCHEMA'). Stateless service; safe to register as +/// a DI singleton. +/// +/// +public class OracleBoxDetectionHelper : + IAmAVersionDetectingMigrationHelper +{ + /// + /// Returns true if a table with the given name exists in the given schema. + /// + /// Optional. Null is resolved to SYS_CONTEXT('USERENV','CURRENT_SCHEMA'). + /// Accepted and ignored — Oracle DDL auto-commits. + public async Task DoesTableExistAsync( + OracleConnection connection, string tableName, string? schemaName, + CancellationToken cancellationToken = default, + OracleTransaction? transaction = null) + { + var resolvedSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken); + + using var command = (OracleCommand)connection.CreateCommand(); + command.BindByName = true; + command.CommandText = @" +SELECT COUNT(1) FROM ALL_TABLES +WHERE OWNER = UPPER(:SchemaName) AND TABLE_NAME = UPPER(:TableName)"; + command.Parameters.Add(new OracleParameter("SchemaName", resolvedSchema)); + command.Parameters.Add(new OracleParameter("TableName", tableName)); + + var result = await command.ExecuteScalarAsync(cancellationToken); + return Convert.ToInt32(result) > 0; + } + + /// + /// Returns true if the migration history table exists and has at least one row for the given + /// box table. + /// + /// Optional. Null is resolved via SYS_CONTEXT. + /// Accepted and ignored — Oracle keeps history in the + /// connected user's schema; is a no-op. + /// Accepted and ignored — Oracle DDL auto-commits. + public async Task DoesHistoryExistAsync( + OracleConnection connection, string tableName, string? schemaName, string? historySchema, + CancellationToken cancellationToken = default, + OracleTransaction? transaction = null) + { + _ = historySchema; + var historyTableExists = await DoesTableExistAsync( + connection, "__BRIGHTERMIGRATIONHISTORY", schemaName, cancellationToken); + if (!historyTableExists) + return false; + + var resolvedSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken); + + using var command = (OracleCommand)connection.CreateCommand(); + command.BindByName = true; + command.CommandText = @" +SELECT COUNT(1) FROM __BRIGHTERMIGRATIONHISTORY +WHERE BoxTableName = :BoxTableName AND SchemaName = :SchemaName"; + command.Parameters.Add(new OracleParameter("BoxTableName", tableName)); + command.Parameters.Add(new OracleParameter("SchemaName", resolvedSchema)); + + var raw = await command.ExecuteScalarAsync(cancellationToken); + return Convert.ToInt32(raw) > 0; + } + + /// + /// Returns the highest migration version recorded in history for the given box table, or 0 + /// if no rows exist. + /// + /// Optional. Null is resolved via SYS_CONTEXT. + /// Accepted and ignored — Oracle keeps history in the connected user's schema. + /// Accepted and ignored — Oracle DDL auto-commits. + public async Task GetMaxVersionAsync( + OracleConnection connection, string tableName, string? schemaName, string? historySchema, + CancellationToken cancellationToken = default, + OracleTransaction? transaction = null) + { + _ = historySchema; + var resolvedSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken); + + using var command = (OracleCommand)connection.CreateCommand(); + command.BindByName = true; + command.CommandText = @" +SELECT COALESCE(MAX(MigrationVersion), 0) FROM __BRIGHTERMIGRATIONHISTORY +WHERE BoxTableName = :BoxTableName AND SchemaName = :SchemaName"; + command.Parameters.Add(new OracleParameter("BoxTableName", tableName)); + command.Parameters.Add(new OracleParameter("SchemaName", resolvedSchema)); + + return Convert.ToInt32(await command.ExecuteScalarAsync(cancellationToken)); + } + + /// + /// Reads the column name set for the given table from ALL_TAB_COLUMNS. + /// Oracle stores column names in uppercase; the returned set uses + /// for PascalCase comparisons. + /// + /// Optional. Null is resolved via SYS_CONTEXT. + /// Accepted and ignored — Oracle DDL auto-commits. + public async Task> GetTableColumnsAsync( + OracleConnection connection, string tableName, string? schemaName, + CancellationToken cancellationToken = default, + OracleTransaction? transaction = null) + { + return await GetTableColumnsAsHashSetAsync(connection, tableName, schemaName, cancellationToken); + } + + /// + /// Detects the current logical schema version of a box table by inspecting its column set. + /// Returns one of three values per ADR 0057 §3: + /// + /// -1 when the discriminator column is absent (the table is not a Brighter box). + /// 0 when the discriminator is present but the V1 column set is incomplete (unknown schema). + /// V >= 1 for the highest version whose cumulative LogicalColumns is a subset of the actual columns. + /// + /// + /// Selects the discriminator: HeaderBag for outbox, CommandBody for inbox. + /// Optional. Null is resolved via SYS_CONTEXT. + /// Accepted and ignored — Oracle DDL auto-commits. + public async Task DetectCurrentVersionAsync( + OracleConnection connection, string tableName, string? schemaName, + BoxType boxType, IReadOnlyList migrations, + CancellationToken cancellationToken = default, + OracleTransaction? transaction = null) + { + var actualColumns = await GetTableColumnsAsHashSetAsync( + connection, tableName, schemaName, cancellationToken); + + var discriminator = DiscriminatorFor(boxType); + if (!actualColumns.Contains(discriminator)) + return -1; + + if (migrations.Count == 0 || !actualColumns.IsSupersetOf(migrations[0].LogicalColumns)) + return 0; + + var matched = migrations[0].Version; + for (var i = 1; i < migrations.Count; i++) + { + if (!actualColumns.IsSupersetOf(migrations[i].LogicalColumns)) + break; + matched = migrations[i].Version; + } + return matched; + } + + /// + /// The discriminator column that distinguishes a Brighter outbox/inbox table from any other + /// table that happens to share its name. + /// + public string DiscriminatorFor(BoxType boxType) => boxType switch + { + BoxType.Outbox => "HeaderBag", + BoxType.Inbox => "CommandBody", + _ => throw new ArgumentOutOfRangeException(nameof(boxType), boxType, "Unknown BoxType") + }; + + internal async Task> GetTableColumnsAsHashSetAsync( + OracleConnection connection, string tableName, string? schemaName, + CancellationToken cancellationToken) + { + var resolvedSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken); + + using var command = (OracleCommand)connection.CreateCommand(); + command.BindByName = true; + command.CommandText = @" +SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS +WHERE OWNER = UPPER(:SchemaName) AND TABLE_NAME = UPPER(:TableName)"; + command.Parameters.Add(new OracleParameter("SchemaName", resolvedSchema)); + command.Parameters.Add(new OracleParameter("TableName", tableName)); + + var columns = new HashSet(StringComparer.OrdinalIgnoreCase); + using var reader = await command.ExecuteReaderAsync(cancellationToken); + while (await reader.ReadAsync(cancellationToken)) + { + columns.Add(reader.GetString(0)); + } + return columns; + } + + private static async Task ResolveSchemaAsync( + OracleConnection connection, string? schemaName, + CancellationToken cancellationToken) + { + if (schemaName is not null) + return schemaName; + + using var command = (OracleCommand)connection.CreateCommand(); + command.CommandText = "SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL"; + var result = await command.ExecuteScalarAsync(cancellationToken); + return result?.ToString() + ?? throw new InvalidOperationException( + "Could not resolve the current Oracle schema via SYS_CONTEXT('USERENV','CURRENT_SCHEMA')."); + } +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxMigrationRunner.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxMigrationRunner.cs new file mode 100644 index 0000000000..9ff5b3f930 --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxMigrationRunner.cs @@ -0,0 +1,270 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Oracle.ManagedDataAccess.Client; +using Paramore.Brighter.Logging; +using Paramore.Brighter.Observability; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Runs box migrations against Oracle. Derives from +/// for orchestration and +/// supplies Oracle-specific hooks for connection, advisory lock UoW, history table, and +/// migration-application paths. +/// +/// +/// Oracle DDL auto-commits per statement. The runner therefore coordinates concurrency with +/// and does not rely on a transaction rollback model for DDL. +/// +public class OracleBoxMigrationRunner : SqlBoxMigrationRunner +{ + private const string MIGRATION_HISTORY_TABLE = "BRIGHTER_MIGRATION_HISTORY"; + private readonly IOracleAdvisoryLock _advisoryLock; + + /// + /// Initialises the runner with an explicit detection helper and optional dependencies. + /// + public OracleBoxMigrationRunner( + OracleBoxDetectionHelper detectionHelper, + IAmABoxMigrationCatalog catalog, + IAmARelationalDatabaseConfiguration configuration, + IOracleAdvisoryLock? advisoryLock = null, + ILogger? logger = null, + TimeSpan? lockTimeout = null, + IAmABrighterTracer? tracer = null, + MigrationHistoryScope scope = MigrationHistoryScope.Global) + : base(detectionHelper, catalog, configuration, lockTimeout ?? TimeSpan.FromSeconds(30), + logger ?? ApplicationLogging.CreateLogger(), + tracer, scope) + { + _advisoryLock = advisoryLock ?? new OracleAdvisoryLock(); + } + + /// + /// Convenience ctor used by registration extensions. + /// + public OracleBoxMigrationRunner( + IAmABoxMigrationCatalog catalog, + IAmARelationalDatabaseConfiguration configuration, + TimeSpan lockTimeout, + IOracleAdvisoryLock? advisoryLock = null, + ILogger? logger = null, + IAmABrighterTracer? tracer = null, + MigrationHistoryScope scope = MigrationHistoryScope.Global) + : this(new OracleBoxDetectionHelper(), catalog, configuration, advisoryLock, logger, lockTimeout, tracer, scope) + { + } + + /// + protected override DbSystem DbSystem => DbSystem.Oracle; + + /// + /// + /// Oracle history is stored in the connected user's schema; there is no fixed default + /// schema constant for this backend. + /// + protected override string? DefaultHistorySchema => null; + + protected override async Task OpenConnectionAsync(CancellationToken cancellationToken) + { + var connection = new OracleConnection(Configuration.ConnectionString); + await connection.OpenAsync(cancellationToken); + return connection; + } + + protected override Task> CreateUnitOfWorkAsync( + OracleConnection connection, string? schemaName, string tableName, CancellationToken cancellationToken) + { + _ = schemaName; + _ = cancellationToken; + return Task.FromResult>( + new OracleProvisioningUnitOfWork(connection, _advisoryLock, Logger, tableName)); + } + + protected override string LockResourceFor(string? schemaName, string tableName) + => OracleMigrationLockName.For(schemaName, tableName); + + protected override async Task EnsureHistoryTableAsync( + OracleConnection connection, OracleTransaction? transaction, string? schemaName, string tableName, + CancellationToken cancellationToken) + { + _ = transaction; + _ = schemaName; + _ = tableName; + + const string ddl = """ + CREATE TABLE BRIGHTER_MIGRATION_HISTORY ( + MigrationVersion NUMBER(10) NOT NULL, + SchemaName VARCHAR2(256) NOT NULL, + BoxTableName VARCHAR2(256) NOT NULL, + Description NVARCHAR2(512) NOT NULL, + AppliedAt TIMESTAMP WITH TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL, + CONSTRAINT PK_BrighterMigrationHistory PRIMARY KEY (SchemaName, BoxTableName, MigrationVersion) + ) + """; + + var escapedDdl = ddl.Replace("'", "''"); + + using var command = (OracleCommand)connection.CreateCommand(); + command.CommandText = $""" + BEGIN + EXECUTE IMMEDIATE '{escapedDdl}'; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -955 THEN + RAISE; + END IF; + END; + """; + + await command.ExecuteNonQueryAsync(cancellationToken); + } + + protected override async Task RunFreshPathAsync( + OracleConnection connection, OracleTransaction? transaction, string? schemaName, string tableName, + string freshInstallDdl, int latestVersion, CancellationToken cancellationToken) + { + _ = transaction; + var effectiveSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken); + + await ExecuteDdlAsync(connection, freshInstallDdl, cancellationToken); + + await InsertHistoryRowAsync( + connection, effectiveSchema, tableName, + latestVersion, $"fresh install at V{latestVersion}", cancellationToken); + } + + protected override async Task RunBootstrapPathAsync( + OracleConnection connection, OracleTransaction? transaction, string? schemaName, string tableName, + BoxType boxType, IReadOnlyList migrations, CancellationToken cancellationToken) + { + var effectiveSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken); + + var detected = await DetectionHelper.DetectCurrentVersionAsync( + connection, tableName, effectiveSchema, boxType, migrations, cancellationToken, transaction); + + if (detected == -1) + { + var discriminator = DetectionHelper.DiscriminatorFor(boxType); + throw new ConfigurationException( + $"Table '{effectiveSchema}.{tableName}' is not a Brighter {boxType.ToString().ToLowerInvariant()}: " + + $"missing discriminator column '{discriminator}'."); + } + + if (detected == 0) + { + throw new ConfigurationException( + $"Table '{effectiveSchema}.{tableName}' does not match any known schema version. " + + $"Cannot bootstrap a Brighter {boxType.ToString().ToLowerInvariant()} from an unrecognised column set."); + } + + await InsertHistoryRowAsync( + connection, effectiveSchema, tableName, + detected, $"bootstrap: detected at V{detected}", cancellationToken); + + for (var i = 0; i < migrations.Count; i++) + { + var migration = migrations[i]; + if (migration.Version <= detected) continue; + + await ExecuteUpScriptAsync(connection, migration, cancellationToken); + await InsertHistoryRowAsync( + connection, effectiveSchema, tableName, + migration.Version, migration.Description.Value, cancellationToken); + } + } + + protected override async Task RunNormalPathAsync( + OracleConnection connection, OracleTransaction? transaction, string? schemaName, string tableName, + IReadOnlyList migrations, CancellationToken cancellationToken) + { + var effectiveSchema = await ResolveSchemaAsync(connection, schemaName, cancellationToken); + + var maxVersion = await DetectionHelper.GetMaxVersionAsync( + connection, tableName, effectiveSchema, ResolveHistorySchema(), cancellationToken, transaction); + + foreach (var migration in migrations) + { + if (migration.Version <= maxVersion) continue; + + await ExecuteUpScriptAsync(connection, migration, cancellationToken); + await InsertHistoryRowAsync( + connection, effectiveSchema, tableName, + migration.Version, migration.Description.Value, cancellationToken); + } + } + + private static Task ExecuteUpScriptAsync( + OracleConnection connection, IAmABoxMigration migration, + CancellationToken cancellationToken) + => ExecuteDdlAsync(connection, migration.UpScript.Value, cancellationToken); + + private static async Task ExecuteDdlAsync( + OracleConnection connection, string ddl, + CancellationToken cancellationToken) + { + using var command = (OracleCommand)connection.CreateCommand(); + command.BindByName = true; + command.CommandText = ddl; + await command.ExecuteNonQueryAsync(cancellationToken); + } + + private static async Task InsertHistoryRowAsync( + OracleConnection connection, string schemaName, string tableName, + int version, string description, CancellationToken cancellationToken) + { + using var command = (OracleCommand)connection.CreateCommand(); + command.BindByName = true; + command.CommandText = """ + INSERT INTO BRIGHTER_MIGRATION_HISTORY (MigrationVersion, SchemaName, BoxTableName, Description) + VALUES (:Version, :SchemaName, :BoxTableName, :Description) + """; + command.Parameters.Add(new OracleParameter("Version", version)); + command.Parameters.Add(new OracleParameter("SchemaName", schemaName)); + command.Parameters.Add(new OracleParameter("BoxTableName", tableName)); + command.Parameters.Add(new OracleParameter("Description", description)); + + await command.ExecuteNonQueryAsync(cancellationToken); + } + + private static async Task ResolveSchemaAsync( + OracleConnection connection, string? schemaName, + CancellationToken cancellationToken) + { + if (schemaName is not null) + { + return schemaName; + } + + using var command = (OracleCommand)connection.CreateCommand(); + command.CommandText = "SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL"; + var result = await command.ExecuteScalarAsync(cancellationToken); + return result?.ToString() + ?? throw new InvalidOperationException( + "Could not resolve the current Oracle schema via SYS_CONTEXT('USERENV','CURRENT_SCHEMA')."); + } +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxProvisioningExtensions.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxProvisioningExtensions.cs new file mode 100644 index 0000000000..bdca9e9fd4 --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleBoxProvisioningExtensions.cs @@ -0,0 +1,172 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Paramore.Brighter.Observability; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Extension methods for registering Oracle box provisioners with . +/// +public static class OracleBoxProvisioningExtensions +{ + /// + /// Register Oracle outbox provisioning with an explicit configuration. + /// + public static BoxProvisioningOptions AddOracleOutbox( + this BoxProvisioningOptions options, + IAmARelationalDatabaseConfiguration configuration) + { + options.Add(services => + { + RegisterSharedRoleImpls(services); + services.TryAddSingleton(); + services.AddSingleton(sp => + { + var catalog = sp.GetRequiredService(); + var runner = new OracleBoxMigrationRunner(catalog, configuration, options.MigrationLockTimeout, + tracer: sp.GetService(), scope: options.MigrationHistoryScope); + return new OracleOutboxProvisioner( + sp.GetRequiredService(), + catalog, + sp.GetRequiredService(), + configuration, + runner); + }); + }); + return options; + } + + /// + /// Register Oracle outbox provisioning with a connection name resolved from IConfiguration at runtime. + /// + public static BoxProvisioningOptions AddOracleOutbox( + this BoxProvisioningOptions options, + string connectionName, + string? outboxTableName = null, + string? schemaName = null, + bool binaryMessagePayload = false) + { + options.Add(services => + { + RegisterSharedRoleImpls(services); + services.TryAddSingleton(); + services.AddSingleton(sp => + { + var config = sp.GetRequiredService(); + var connectionString = config.GetConnectionString(connectionName) + ?? throw new InvalidOperationException( + $"Connection string '{connectionName}' not found in configuration."); + var dbConfig = new RelationalDatabaseConfiguration( + connectionString, + outBoxTableName: outboxTableName ?? "Outbox", + schemaName: schemaName, + binaryMessagePayload: binaryMessagePayload); + var catalog = sp.GetRequiredService(); + var runner = new OracleBoxMigrationRunner(catalog, dbConfig, options.MigrationLockTimeout, + tracer: sp.GetService(), scope: options.MigrationHistoryScope); + return new OracleOutboxProvisioner( + sp.GetRequiredService(), + catalog, + sp.GetRequiredService(), + dbConfig, + runner); + }); + }); + return options; + } + + /// + /// Register Oracle inbox provisioning with an explicit configuration. + /// + public static BoxProvisioningOptions AddOracleInbox( + this BoxProvisioningOptions options, + IAmARelationalDatabaseConfiguration configuration) + { + options.Add(services => + { + RegisterSharedRoleImpls(services); + services.TryAddSingleton(); + services.AddSingleton(sp => + { + var catalog = sp.GetRequiredService(); + var runner = new OracleBoxMigrationRunner(catalog, configuration, options.MigrationLockTimeout, + tracer: sp.GetService(), scope: options.MigrationHistoryScope); + return new OracleInboxProvisioner( + sp.GetRequiredService(), + catalog, + sp.GetRequiredService(), + configuration, + runner); + }); + }); + return options; + } + + /// + /// Register Oracle inbox provisioning with a connection name resolved from IConfiguration at runtime. + /// + public static BoxProvisioningOptions AddOracleInbox( + this BoxProvisioningOptions options, + string connectionName, + string? inboxTableName = null, + string? schemaName = null, + bool binaryMessagePayload = false) + { + options.Add(services => + { + RegisterSharedRoleImpls(services); + services.TryAddSingleton(); + services.AddSingleton(sp => + { + var config = sp.GetRequiredService(); + var connectionString = config.GetConnectionString(connectionName) + ?? throw new InvalidOperationException( + $"Connection string '{connectionName}' not found in configuration."); + var dbConfig = new RelationalDatabaseConfiguration( + connectionString, + inboxTableName: inboxTableName ?? "Inbox", + schemaName: schemaName, + binaryMessagePayload: binaryMessagePayload); + var catalog = sp.GetRequiredService(); + var runner = new OracleBoxMigrationRunner(catalog, dbConfig, options.MigrationLockTimeout, + tracer: sp.GetService(), scope: options.MigrationHistoryScope); + return new OracleInboxProvisioner( + sp.GetRequiredService(), + catalog, + sp.GetRequiredService(), + dbConfig, + runner); + }); + }); + return options; + } + + private static void RegisterSharedRoleImpls(IServiceCollection services) + { + services.TryAddSingleton(); + services.TryAddSingleton(); + } +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxMigrationCatalog.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxMigrationCatalog.cs new file mode 100644 index 0000000000..e4794496a3 --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxMigrationCatalog.cs @@ -0,0 +1,79 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System.Collections.Generic; +using Paramore.Brighter.Inbox.Oracle; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Defines the migration history for Oracle inbox tables. +/// +/// +/// The Oracle inbox was introduced with the full current column set from the start (V1 = +/// V_latest). V1's UpScript is the full current DDL. The bootstrap detection branch +/// identifies pre-BoxProvisioning Oracle inbox tables (created directly via +/// ) as V1 because the V1 LogicalColumns set +/// equals the columns the builder always produced. +/// +/// LogicalColumns use — Oracle stores +/// column names as uppercase in ALL_TAB_COLUMNS but the comparison must handle +/// PascalCase names from the migration definitions. +/// +/// +public class OracleInboxMigrationCatalog : IAmABoxMigrationCatalog +{ + // Oracle inbox was born with all columns present; V1 LogicalColumns = full set. + private static readonly string[] s_v1Columns = + ["CommandId", "CommandType", "CommandBody", "Timestamp", "ContextKey"]; + + /// + public string FreshInstallDdl(IAmARelationalDatabaseConfiguration configuration) + { + Identifiers.AssertSafe( + configuration.InBoxTableName, + nameof(IAmARelationalDatabaseConfiguration.InBoxTableName)); + return OracleInboxBuilder.GetDDL( + configuration.InBoxTableName, + configuration.BinaryMessagePayload); + } + + /// + /// Returns all migrations for the Oracle inbox, ordered by version. + /// + /// The relational database configuration. + /// An ordered list containing V1 (the full column set — born at V_latest). + public IReadOnlyList All(IAmARelationalDatabaseConfiguration configuration) + { + var table = configuration.InBoxTableName; + Identifiers.AssertSafe(table, nameof(IAmARelationalDatabaseConfiguration.InBoxTableName)); + + return + [ + new BoxMigration( + Version: 1, + Description: "Create inbox table (Oracle born at full column set)", + UpScript: OracleInboxBuilder.GetDDL(table, configuration.BinaryMessagePayload), + LogicalColumns: new System.Collections.Generic.HashSet( + s_v1Columns, System.StringComparer.OrdinalIgnoreCase)) + ]; + } +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxProvisioner.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxProvisioner.cs new file mode 100644 index 0000000000..b76c3715ce --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleInboxProvisioner.cs @@ -0,0 +1,52 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Provisions an Oracle inbox table. Pre-lock detection and payload-mode validation are owned +/// by the base; this class supplies +/// only the abstract hooks for the Oracle connection factory and the inbox payload column name. +/// +public class OracleInboxProvisioner : SqlBoxProvisioner +{ + /// + /// Initialises the provisioner with all required dependencies. + /// + public OracleInboxProvisioner( + IAmAVersionDetectingMigrationHelper detectionHelper, + IAmABoxMigrationCatalog catalog, + IAmABoxPayloadModeValidator payloadValidator, + IAmARelationalDatabaseConfiguration configuration, + IAmABoxMigrationRunner migrationRunner) + : base(detectionHelper, catalog, payloadValidator, configuration, migrationRunner, BoxType.Inbox) + { + } + + /// + protected override OracleConnection CreateConnection(string connectionString) + => new OracleConnection(connectionString); + + /// + protected override string PayloadColumnName => "CommandBody"; +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleMigrationLockName.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleMigrationLockName.cs new file mode 100644 index 0000000000..cf6005b31f --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleMigrationLockName.cs @@ -0,0 +1,92 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System.Security.Cryptography; +using System.Text; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Builds the advisory-lock identifier passed to DBMS_LOCK.ALLOCATE_UNIQUE for a +/// given (schema, table) pair. +/// +/// +/// DBMS_LOCK.ALLOCATE_UNIQUE supports lock names up to 128 characters. +/// The schema is folded into the lock name so two same-named tables in different schemas +/// acquire distinct advisory locks. +/// +/// For composites that fit the simple form +/// (BrighterMigration_<schema>.<table> ≤ 128 chars, or +/// BrighterMigration_<table> when schema is null/empty) the form is preserved +/// for diagnostic readability. For longer composites a SHA-256 suffix is folded in to stay +/// within the limit while remaining collision-resistant. +/// +/// +/// Hash truncation: the suffix is the first 8 bytes (16 hex characters) of the SHA-256 +/// digest. Birthday-bound collision probability is ~1 in 2^32 over distinct (schema, table) +/// pairs — accepted as negligible for typical per-deployment box-table counts. +/// +/// +public static class OracleMigrationLockName +{ + private const string Prefix = "BrighterMigration_"; + private const int DbmsLockNameLimit = 128; + private const int HashHexChars = 16; + // Long-form layout: Prefix(18) + truncatedPrefix(N) + '_'(1) + hashHex(16) ≤ 128 → N ≤ 93. + private const int LongFormPrefixBudget = 93; + + /// + /// Builds the lock name for the given and , + /// guaranteed not to exceed the 128-character DBMS_LOCK.ALLOCATE_UNIQUE limit. + /// + /// The Oracle schema the table lives in. When null or empty the schema + /// is omitted; the runner always passes the resolved current schema. + /// The box table name. + /// A lock identifier suitable for DBMS_LOCK.ALLOCATE_UNIQUE. + public static string For(string? schema, string tableName) + { + var composite = string.IsNullOrEmpty(schema) + ? tableName + : $"{schema}.{tableName}"; + + var simpleForm = Prefix + composite; + if (simpleForm.Length <= DbmsLockNameLimit) + return simpleForm; + + var hashSuffix = ShortHashOf(composite); + var truncatedPrefix = composite.Substring(0, LongFormPrefixBudget); + return $"{Prefix}{truncatedPrefix}_{hashSuffix}"; + } + + private static string ShortHashOf(string composite) + { + using var sha256 = SHA256.Create(); + var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(composite)); + + var builder = new StringBuilder(HashHexChars); + for (var i = 0; i < HashHexChars / 2; i++) + { + builder.Append(hashBytes[i].ToString("x2")); + } + + return builder.ToString(); + } +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxMigrationCatalog.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxMigrationCatalog.cs new file mode 100644 index 0000000000..84825bac41 --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxMigrationCatalog.cs @@ -0,0 +1,92 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System.Collections.Generic; +using Paramore.Brighter.Outbox.Oracle; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Defines the migration history for Oracle outbox tables. +/// +/// +/// The Oracle outbox was introduced with the full current column set from the start (V1 = +/// V_latest). Unlike MySQL whose V1 carried only the March-2019 minimal shape and accumulated +/// columns through V7, the Oracle provider was never shipped with a subset of these columns. +/// +/// V1's UpScript is therefore the full current DDL — the same as . +/// The fresh-install fast path (ADR 0057 §3) still sources its DDL from +/// so the bootstrap and fresh-install paths are consistent. The detection bootstrap branch +/// identifies pre-BoxProvisioning Oracle tables (created directly via +/// ) as V1 because the V1 LogicalColumns set +/// is a subset of (or equal to) every column the builder ever produced. +/// +/// +/// LogicalColumns use — Oracle stores +/// column names as uppercase in ALL_TAB_COLUMNS but the comparison must handle +/// PascalCase names from the migration definitions. +/// +/// +public class OracleOutboxMigrationCatalog : IAmABoxMigrationCatalog +{ + // Oracle outbox was born with all columns present; V1 LogicalColumns = full set. + private static readonly string[] s_v1Columns = + [ + "MessageId", "Topic", "MessageType", "Timestamp", + "CorrelationId", "ReplyTo", "ContentType", "PartitionKey", + "WorkflowId", "JobId", "Dispatched", "HeaderBag", "Body", + "Source", "Type", "DataSchema", "Subject", + "TraceParent", "TraceState", "Baggage", + "DataRef", "SpecVersion" + ]; + + /// + public string FreshInstallDdl(IAmARelationalDatabaseConfiguration configuration) + { + Identifiers.AssertSafe( + configuration.OutBoxTableName, + nameof(IAmARelationalDatabaseConfiguration.OutBoxTableName)); + return OracleOutboxBuilder.GetDDL( + configuration.OutBoxTableName, + configuration.BinaryMessagePayload); + } + + /// + /// Returns all migrations for the Oracle outbox, ordered by version. + /// + /// The relational database configuration. + /// An ordered list containing V1 (the full column set — born at V_latest). + public IReadOnlyList All(IAmARelationalDatabaseConfiguration configuration) + { + var table = configuration.OutBoxTableName; + Identifiers.AssertSafe(table, nameof(IAmARelationalDatabaseConfiguration.OutBoxTableName)); + + return + [ + new BoxMigration( + Version: 1, + Description: "Create outbox table (Oracle born at full column set)", + UpScript: OracleOutboxBuilder.GetDDL(table, configuration.BinaryMessagePayload), + LogicalColumns: new System.Collections.Generic.HashSet( + s_v1Columns, System.StringComparer.OrdinalIgnoreCase)) + ]; + } +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxProvisioner.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxProvisioner.cs new file mode 100644 index 0000000000..cb01b2c281 --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleOutboxProvisioner.cs @@ -0,0 +1,52 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Provisions an Oracle outbox table. Pre-lock detection and payload-mode validation are owned +/// by the base; this class supplies +/// only the abstract hooks for the Oracle connection factory and the outbox payload column name. +/// +public class OracleOutboxProvisioner : SqlBoxProvisioner +{ + /// + /// Initialises the provisioner with all required dependencies. + /// + public OracleOutboxProvisioner( + IAmAVersionDetectingMigrationHelper detectionHelper, + IAmABoxMigrationCatalog catalog, + IAmABoxPayloadModeValidator payloadValidator, + IAmARelationalDatabaseConfiguration configuration, + IAmABoxMigrationRunner migrationRunner) + : base(detectionHelper, catalog, payloadValidator, configuration, migrationRunner, BoxType.Outbox) + { + } + + /// + protected override OracleConnection CreateConnection(string connectionString) + => new OracleConnection(connectionString); + + /// + protected override string PayloadColumnName => "Body"; +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OraclePayloadModeValidator.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OraclePayloadModeValidator.cs new file mode 100644 index 0000000000..bdb6c66c17 --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OraclePayloadModeValidator.cs @@ -0,0 +1,107 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Validates that the payload column type in an existing Oracle box table matches the +/// configured binary/text mode. Throws on mismatch +/// and returns quietly on match. +/// +/// +/// Queries ALL_TAB_COLUMNS for the column's DATA_TYPE. Oracle stores column +/// names and owner names in uppercase by default; identifiers are uppercased before the +/// lookup so callers may pass mixed-case names without issue. +/// When is null the current schema is resolved via +/// SYS_CONTEXT('USERENV','CURRENT_SCHEMA'). +/// Stateless service; safe to register as a DI singleton. +/// +public class OraclePayloadModeValidator : IAmABoxPayloadModeValidator +{ + /// + /// Validates the payload mode of an existing table column against ALL_TAB_COLUMNS. + /// + /// An open Oracle connection. + /// The unqualified box table name. + /// + /// Optional. Null is resolved to SYS_CONTEXT('USERENV','CURRENT_SCHEMA'). + /// + /// The payload column name (e.g. "Body" or "CommandBody"). + /// Whether binary payload mode is configured. + /// Cancellation token. + public async Task ValidateAsync( + OracleConnection connection, string tableName, string? schemaName, + string columnName, bool binaryMessagePayload, + CancellationToken cancellationToken = default) + { + var resolvedSchema = schemaName is not null + ? schemaName + : await ResolveCurrentSchemaAsync(connection, cancellationToken); + + using var command = (OracleCommand)connection.CreateCommand(); + command.BindByName = true; + command.CommandText = @" +SELECT DATA_TYPE FROM ALL_TAB_COLUMNS +WHERE OWNER = UPPER(:SchemaName) AND TABLE_NAME = UPPER(:TableName) AND COLUMN_NAME = UPPER(:ColumnName)"; + command.Parameters.Add(new OracleParameter("SchemaName", resolvedSchema)); + command.Parameters.Add(new OracleParameter("TableName", tableName)); + command.Parameters.Add(new OracleParameter("ColumnName", columnName)); + + var dataType = (string?)await command.ExecuteScalarAsync(cancellationToken); + if (dataType == null) return; + + var isBinary = dataType.Equals("BLOB", StringComparison.OrdinalIgnoreCase); + var isText = dataType.Equals("CLOB", StringComparison.OrdinalIgnoreCase) + || dataType.Equals("NCLOB", StringComparison.OrdinalIgnoreCase); + + if (binaryMessagePayload && isText) + { + throw new ConfigurationException( + $"Payload mode mismatch for table \"{resolvedSchema}\".\"{tableName}\". " + + $"Column \"{columnName}\" is {dataType} (text) but binary mode was configured. " + + "Either change the configuration or alter the column type."); + } + + if (!binaryMessagePayload && isBinary) + { + throw new ConfigurationException( + $"Payload mode mismatch for table \"{resolvedSchema}\".\"{tableName}\". " + + $"Column \"{columnName}\" is {dataType} (binary) but text mode was configured. " + + "Either change the configuration or alter the column type."); + } + } + + private static async Task ResolveCurrentSchemaAsync( + OracleConnection connection, CancellationToken cancellationToken) + { + using var command = (OracleCommand)connection.CreateCommand(); + command.CommandText = "SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') FROM DUAL"; + var result = await command.ExecuteScalarAsync(cancellationToken); + return result?.ToString() + ?? throw new InvalidOperationException( + "Could not resolve the current Oracle schema via SYS_CONTEXT('USERENV','CURRENT_SCHEMA')."); + } +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleProvisioningUnitOfWork.cs b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleProvisioningUnitOfWork.cs new file mode 100644 index 0000000000..e83928b3ae --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/OracleProvisioningUnitOfWork.cs @@ -0,0 +1,122 @@ +// The MIT License (MIT) +// Copyright © 2026 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.BoxProvisioning.Oracle; + +/// +/// Oracle unit-of-work for box-table migrations. Oracle is the transactionless member of the +/// relational family for migration purposes — DDL implicitly commits the surrounding transaction, +/// so the UoW only acquires the session-level DBMS_LOCK that serialises concurrent +/// runners and never opens a ; the +/// property is always null. +/// +/// +/// The class implements with +/// TTransaction = OracleTransaction for symmetry with the other relational backends — +/// see the XML-doc which +/// documents the null-Transaction contract for transactionless backends. +/// +public class OracleProvisioningUnitOfWork( + OracleConnection connection, + IOracleAdvisoryLock advisoryLock, + ILogger logger, + string? tableName = null) : IAmAProvisioningUnitOfWork +{ + private string? _lockResource; + + /// + /// + /// Always returns null — Oracle DDL auto-commits per statement. + /// + public OracleTransaction? Transaction => null; + + /// + public async Task BeginAsync(string lockResource, TimeSpan lockTimeout, CancellationToken cancellationToken) + { + // Oracle DDL implicitly commits, so wrapping the migration run in a BEGIN/COMMIT yields + // nothing useful. The UoW therefore only acquires the DBMS_LOCK advisory lock and stores + // the lock resource for explicit RELEASE on Commit/Rollback. No transaction is opened. + // + // _lockResource is assigned ONLY AFTER AcquireAsync returns successfully. If Acquire + // throws, _lockResource remains null so a defensive Rollback is a clean no-op via the + // `if (_lockResource is null) return;` short-circuit in ReleaseLockAsync. + logger.LogTrace("Beginning Oracle provisioning UoW for resource {LockResource}", lockResource); + await advisoryLock.AcquireAsync(connection, lockResource, lockTimeout, cancellationToken); + _lockResource = lockResource; + } + + /// + public Task CommitAsync(CancellationToken cancellationToken) => + ReleaseLockAsync(cancellationToken); + + /// + public Task RollbackAsync(CancellationToken cancellationToken) => + ReleaseLockAsync(cancellationToken); + + private async Task ReleaseLockAsync(CancellationToken cancellationToken) + { + if (_lockResource is null) return; + + bool? releaseResult; + try + { + releaseResult = await advisoryLock.ReleaseAsync(connection, _lockResource, cancellationToken); + } + catch (Exception ex) + { + // DBMS_LOCK.RELEASE executes SQL on the same connection. If the connection is dead, + // ReleaseAsync throws. RollbackAsync MUST NOT throw — otherwise the runner's catch + // path replaces the original migration exception with this cleanup-side failure. + // The session-scoped DBMS_LOCK is released by the server when the connection closes. + logger.LogWarning( + ex, + "Oracle provisioning UoW: DBMS_LOCK.RELEASE threw for lock resource '{LockResource}' — release skipped, the session will release the lock when the connection closes.", + _lockResource); + return; + } + + if (releaseResult is true) return; + + var marker = releaseResult is null ? "null" : "false"; + var meaning = releaseResult is null ? "lock was never acquired" : "not the lock owner or parameter error"; + if (tableName is not null) + { + logger.LogWarning( + "Oracle advisory lock for migration of '{TableName}' (key '{LockKey}') was not released by this session: DBMS_LOCK.RELEASE returned {Result} ({Marker} = {Meaning}). This is likely a Brighter defect — please report it.", + tableName, _lockResource, releaseResult, marker, meaning); + } + else + { + logger.LogWarning( + "Oracle advisory lock '{LockResource}' was not released by this session: DBMS_LOCK.RELEASE returned {Result} ({Marker} = {Meaning}). This is likely a Brighter defect — please report it.", + _lockResource, releaseResult, marker, meaning); + } + } + + /// + public ValueTask DisposeAsync() => default; +} diff --git a/src/Paramore.Brighter.BoxProvisioning.Oracle/Paramore.Brighter.BoxProvisioning.Oracle.csproj b/src/Paramore.Brighter.BoxProvisioning.Oracle/Paramore.Brighter.BoxProvisioning.Oracle.csproj new file mode 100644 index 0000000000..4ba1542b0f --- /dev/null +++ b/src/Paramore.Brighter.BoxProvisioning.Oracle/Paramore.Brighter.BoxProvisioning.Oracle.csproj @@ -0,0 +1,27 @@ + + + + Oracle Database box provisioning for Paramore.Brighter. Provides automatic creation and migration of inbox and outbox tables in Oracle at application startup. + Rafael Andrade + $(BrighterMongoTargetFrameworks) + Oracle;Oracle Database;Database;Outbox;Inbox;Provisioning;Migration;Command;Event;Command Processor;Messaging;Brighter + enable + + + + + + + + + + + + + + + + + + + diff --git a/src/Paramore.Brighter.Inbox.Oracle/OracleInbox.cs b/src/Paramore.Brighter.Inbox.Oracle/OracleInbox.cs new file mode 100644 index 0000000000..3e91e29a8b --- /dev/null +++ b/src/Paramore.Brighter.Inbox.Oracle/OracleInbox.cs @@ -0,0 +1,104 @@ +// The MIT License (MIT) +// Copyright © 2014 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Data; +using System.Data.Common; +using Oracle.ManagedDataAccess.Client; +using Paramore.Brighter.Logging; +using Paramore.Brighter.Observability; +using Paramore.Brighter.Oracle; + +namespace Paramore.Brighter.Inbox.Oracle; + +/// +/// Implements an Inbox using Oracle as a backing store. +/// Requires Oracle 12c or later. +/// +public class OracleInbox : RelationalDatabaseInbox +{ + // ORA-00001: unique constraint violated + private const int DuplicateKeyError = 1; + + /// + /// Initializes a new instance of the class. + /// + /// The relational database configuration. + /// The connection provider. + public OracleInbox(IAmARelationalDatabaseConfiguration configuration, + IAmARelationalDbConnectionProvider connectionProvider) + : base(DbSystem.Oracle, configuration, connectionProvider, new OracleQueries(), + ApplicationLogging.CreateLogger()) + { + } + + /// + /// Initializes a new instance of the class using a default connection provider. + /// + /// The relational database configuration. + public OracleInbox(IAmARelationalDatabaseConfiguration configuration) + : this(configuration, new OracleConnectionProvider(configuration)) + { + } + + /// + protected override bool IsExceptionUniqueOrDuplicateIssue(Exception ex) + { + return ex is OracleException { Number: DuplicateKeyError }; + } + + /// + protected override IDbDataParameter CreateSqlParameter(string parameterName, object? value) + { + parameterName = parameterName.Replace("@", ":"); + return new OracleParameter { ParameterName = parameterName, Value = value ?? DBNull.Value }; + } + + /// + /// + /// Uses rather than OracleDbType.Json because + /// the native Oracle JSON type is only available from Oracle Database 21c onwards. + /// Oracle 19c stores JSON as a CLOB column validated by an IS JSON check constraint. + /// + protected override IDbDataParameter CreateJsonSqlParameter(string parameterName, object? value) + { + parameterName = parameterName.Replace("@", ":"); + return new OracleParameter + { + ParameterName = parameterName, Value = value ?? DBNull.Value, OracleDbType = OracleDbType.Clob + }; + } + + /// + /// Sets to so that + /// named :param placeholders in the SQL are matched to parameters by name. + protected override DbCommand CreateCommand(DbConnection connection, string sqlText, int outBoxTimeout, + params IDbDataParameter[] parameters) + { + var command = (OracleCommand)connection.CreateCommand(); + command.BindByName = true; + command.CommandTimeout = outBoxTimeout < 0 ? 0 : outBoxTimeout; + command.CommandText = sqlText; + command.Parameters.AddRange(parameters); + return command; + } + +} diff --git a/src/Paramore.Brighter.Inbox.Oracle/OracleInboxBuilder.cs b/src/Paramore.Brighter.Inbox.Oracle/OracleInboxBuilder.cs new file mode 100644 index 0000000000..30d1fe86cf --- /dev/null +++ b/src/Paramore.Brighter.Inbox.Oracle/OracleInboxBuilder.cs @@ -0,0 +1,126 @@ +// The MIT License (MIT) +// Copyright © 2014 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System.ComponentModel; + +namespace Paramore.Brighter.Inbox.Oracle; + +/// +/// Provides DDL statements for creating and checking the existence of an Inbox table in Oracle. +/// Requires Oracle 12c or later (uses identity columns and TIMESTAMP WITH TIME ZONE). +/// +public class OracleInboxBuilder +{ + private const string TextInboxDDL = + """ + CREATE TABLE {0} + ( + Id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + CommandId VARCHAR2(256 CHAR) NOT NULL, + CommandType VARCHAR2(256 CHAR), + CommandBody CLOB, + Timestamp TIMESTAMP WITH TIME ZONE, + ContextKey VARCHAR2(256 CHAR), + CONSTRAINT UQ_{0}_Command_Context UNIQUE (CommandId, ContextKey) -- Optional: keeps the unique combo constraint if needed + ); + """; + + private const string BinaryInboxDDL = + """ + CREATE TABLE {0} + ( + Id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + CommandId VARCHAR2(256 CHAR) NOT NULL, + CommandType VARCHAR2(256 CHAR), + CommandBody BLOB, + Timestamp TIMESTAMP WITH TIME ZONE, + ContextKey VARCHAR2(256 CHAR), + CONSTRAINT UQ_{0}_Command_Context UNIQUE (CommandId, ContextKey) -- Optional: keeps the unique combo constraint if needed + ); + """; + + private const string JsonInboxDDL = + """ + CREATE TABLE {0} + ( + Id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + CommandId VARCHAR2(256 CHAR) NOT NULL, + CommandType VARCHAR2(256 CHAR), + CommandBody BLOB, + Timestamp TIMESTAMP WITH TIME ZONE, + ContextKey VARCHAR2(256 CHAR), + CONSTRAINT UQ_{0}_Command_Context UNIQUE (CommandId, ContextKey), + CONSTRAINT ENSURE_{0}_JSON CHECK (CommandBody IS JSON) + ); + """; + + private const string InboxExistsQuery = + """ + SELECT COUNT(1) + FROM ALL_TABLES + WHERE OWNER = '{0}' + AND TABLE_NAME = '{1}'; + """; + + + /// + /// Gets the DDL statement required to create the Inbox table in Oracle. + /// + /// The name to give the Inbox table. + /// + /// When , the CommandBody column is created as BLOB. + /// Ignored when is . + /// + /// + /// When , the CommandBody column is created as BLOB + /// with an IS JSON check constraint. Requires Oracle 12c or later. + /// Takes precedence over . + /// + /// A CREATE TABLE statement formatted with the given table name. + public static string GetDDL(string inboxTableName, bool binaryMessage = false, bool jsonMessage = false) + { + if (jsonMessage) + { + return string.Format(JsonInboxDDL, inboxTableName); + } + + if (binaryMessage) + { + return string.Format(BinaryInboxDDL, inboxTableName); + } + + return string.Format(TextInboxDDL, inboxTableName); + } + + /// + /// Gets the SQL statement that checks whether the Inbox table already exists in the given schema. + /// + /// The Oracle schema (owner) to search within. + /// The table name to check. + /// A SQL query that returns 1 if the table exists, 0 otherwise. + /// Thrown when or is null or empty. + public static string GetExistsQuery(string ownerName, string inboxTableName) + { + if (string.IsNullOrEmpty(ownerName) || string.IsNullOrEmpty(inboxTableName)) + throw new InvalidEnumArgumentException("You must provide a table schema and tablename for the inbox table"); + return string.Format(InboxExistsQuery, ownerName, inboxTableName); + } +} diff --git a/src/Paramore.Brighter.Inbox.Oracle/OracleQueries.cs b/src/Paramore.Brighter.Inbox.Oracle/OracleQueries.cs new file mode 100644 index 0000000000..1258efeffd --- /dev/null +++ b/src/Paramore.Brighter.Inbox.Oracle/OracleQueries.cs @@ -0,0 +1,44 @@ +// The MIT License (MIT) +// Copyright © 2014 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +namespace Paramore.Brighter.Inbox.Oracle; + +/// +/// Oracle-specific SQL queries for the Brighter Inbox. +/// Uses named bind variables prefixed with :; parameter names are matched by name +/// via . +/// +public class OracleQueries : IRelationalDatabaseInboxQueries +{ + /// + public string AddCommand { get; } = + "INSERT INTO {0} (CommandId,CommandType,CommandBody,Timestamp,ContextKey) " + + "VALUES (:CommandID,:CommandType,:CommandBody,:Timestamp,:ContextKey)"; + + /// + public string ExistsCommand { get; } = + "SELECT CommandId FROM {0} WHERE CommandId = :CommandID AND ContextKey = :ContextKey " + + "FETCH FIRST 1 ROW ONLY"; + + /// + public string GetCommand { get; } = + "SELECT * FROM {0} WHERE CommandId = :CommandID AND ContextKey = :ContextKey"; +} diff --git a/src/Paramore.Brighter.Inbox.Oracle/Paramore.Brighter.Inbox.Oracle.csproj b/src/Paramore.Brighter.Inbox.Oracle/Paramore.Brighter.Inbox.Oracle.csproj new file mode 100644 index 0000000000..456ba6d279 --- /dev/null +++ b/src/Paramore.Brighter.Inbox.Oracle/Paramore.Brighter.Inbox.Oracle.csproj @@ -0,0 +1,16 @@ + + + + Oracle Database inbox provider for Paramore.Brighter Command Processor. Enables inbox pattern implementation for idempotent command and event handling using Oracle as the persistence layer. + Rafael Andrade + $(BrighterMongoTargetFrameworks) + Oracle;Oracle Database;Database;Inbox;Idempotent;Command;Event;Command Processor;Relational Database;Brighter + enable + + + + + + + + diff --git a/src/Paramore.Brighter.Locking.Oracle/OracleLockingProvider.cs b/src/Paramore.Brighter.Locking.Oracle/OracleLockingProvider.cs new file mode 100644 index 0000000000..f8af289fd3 --- /dev/null +++ b/src/Paramore.Brighter.Locking.Oracle/OracleLockingProvider.cs @@ -0,0 +1,235 @@ +using System.Collections.Concurrent; +using System.Data; +using Microsoft.Extensions.Logging; +using Oracle.ManagedDataAccess.Client; +using Paramore.Brighter.Logging; +using Paramore.Brighter.Oracle; + +namespace Paramore.Brighter.Locking.Oracle; + +/// +/// Provides Oracle-backed distributed locking using DBMS_LOCK APIs. +/// +/// Provides Oracle connections used to allocate, request, and release locks. +public partial class OracleLockingProvider(OracleConnectionProvider connectionProvider) : IDistributedLock, IAsyncDisposable +{ + private readonly ILogger _logger = ApplicationLogging.CreateLogger(); + private readonly ConcurrentDictionary _connections = new(); + + +#if NETFRAMEWORK + /// + /// Releases all held Oracle connections tracked by this lock provider. + /// + /// A completed once all connections are released. + public ValueTask DisposeAsync() + { + foreach (var connection in _connections.Values) + { + connection.Close(); + connection.Dispose(); + } + + _connections.Clear(); + return new ValueTask(); + } +#else + /// + /// Releases all held Oracle connections tracked by this lock provider. + /// + /// A that completes when all connections are asynchronously released. + public async ValueTask DisposeAsync() + { + foreach (var connection in _connections.Values) + { + await connection.CloseAsync(); + await connection.DisposeAsync(); + } + + _connections.Clear(); + } +#endif + + /// + /// Attempts to obtain an exclusive lock for the specified resource. + /// + /// The resource identifier to lock. + /// A used to cancel the asynchronous operation. + /// + /// A containing the Oracle lock handle when the lock is acquired; otherwise . + /// + public async Task ObtainLockAsync(string resource, CancellationToken cancellationToken) + { + if (_connections.ContainsKey(resource)) + { + Log.LockAlreadyHeld(_logger, resource); + return null; + } + + string? lockHandler = null; + + var connection = (OracleConnection)await connectionProvider.GetConnectionAsync(cancellationToken); + try + { + lockHandler = await AllocateLockHandleAsync(connection, resource); + if (string.IsNullOrEmpty(lockHandler)) + { + Log.LockHandleAllocationFailed(_logger, resource); + return null; + } + + var requestLock = await RequestLock(connection, lockHandler, 1); + if (requestLock > 0) + { + Log.LockNotGranted(_logger, resource, requestLock); + return null; + } + + _connections.TryAdd(resource, connection); + Log.LockObtained(_logger, resource, lockHandler!); + return lockHandler; + } + catch (Exception e) + { + Log.LockAcquisitionFailed(_logger, e, resource); + if (!string.IsNullOrEmpty(lockHandler)) + { + await ReleaseLockHandlerAsync(connection, lockHandler!); + } +#if NETFRAMEWORK + connection.Close(); + connection.Dispose(); +#else + await connection.CloseAsync(); + await connection.DisposeAsync(); +#endif + return null; + } + } + + /// + /// Releases a previously obtained lock for the specified resource. + /// + /// The resource identifier that is currently locked. + /// The lock identifier returned by . + /// A used to cancel the asynchronous operation. + /// A that completes when the lock release operation has finished. + public async Task ReleaseLockAsync(string resource, string lockId, CancellationToken cancellationToken) + { + if (!_connections.TryRemove(resource, out var connection)) + { + Log.LockReleaseSkipped(_logger, resource, lockId); + return; + } + + await ReleaseLockHandlerAsync(connection, lockId); + Log.LockReleased(_logger, resource, lockId); + +#if NETFRAMEWORK + connection.Close(); + connection.Dispose(); +#else + await connection.CloseAsync(); + await connection.DisposeAsync(); +#endif + } + + private static partial class Log + { + [LoggerMessage(LogLevel.Information, "Unable to obtain lock for resource {Resource}; a connection is already tracking this lock")] + public static partial void LockAlreadyHeld(ILogger logger, string resource); + + [LoggerMessage(LogLevel.Information, "Unable to obtain lock for resource {Resource}; Oracle DBMS_LOCK returned status {ResultCode}")] + public static partial void LockNotGranted(ILogger logger, string resource, int resultCode); + + [LoggerMessage(LogLevel.Warning, "Unable to obtain lock for resource {Resource}; Oracle DBMS_LOCK did not return a lock handle")] + public static partial void LockHandleAllocationFailed(ILogger logger, string resource); + + [LoggerMessage(LogLevel.Information, "Obtained lock for resource {Resource} with handle {LockHandle}")] + public static partial void LockObtained(ILogger logger, string resource, string lockHandle); + + [LoggerMessage(LogLevel.Error, "Failed to obtain lock for resource {Resource}")] + public static partial void LockAcquisitionFailed(ILogger logger, Exception exception, string resource); + + [LoggerMessage(LogLevel.Debug, "Skipping release for resource {Resource}; no tracked connection for lock {LockId}")] + public static partial void LockReleaseSkipped(ILogger logger, string resource, string lockId); + + [LoggerMessage(LogLevel.Information, "Released lock {LockId} for resource {Resource}")] + public static partial void LockReleased(ILogger logger, string resource, string lockId); + } + + private static async Task AllocateLockHandleAsync(OracleConnection conn, string lockName) + { + const string plsql = """ + BEGIN + DBMS_LOCK.ALLOCATE_UNIQUE(:name, :handle); + END; + """; + +#if NETFRAMEWORK + using var cmd = new OracleCommand(plsql, conn); +#else + await using var cmd = new OracleCommand(plsql, conn); +#endif + + var handle = new OracleParameter("handle", OracleDbType.Varchar2, 128) + { + Direction = ParameterDirection.Output + }; + + cmd.Parameters.Add(new OracleParameter("name", OracleDbType.Varchar2) + { + Value = lockName, + Direction = ParameterDirection.Input + }); + cmd.Parameters.Add(handle); + await cmd.ExecuteNonQueryAsync(); + + return handle.Value?.ToString(); + } + + + private static async Task RequestLock(OracleConnection conn, string? lockHandle, int timeoutSeconds) + { + // 6 = X_MODE (Exclusive Mode). release_on_commit = FALSE keeps lock alive across app updates. + const string plsql = """ + BEGIN + :result := DBMS_LOCK.REQUEST(:lockhandle, 6, :timeout, FALSE); + END; + """; + +#if NETFRAMEWORK + using var cmd = new OracleCommand(plsql, conn); +#else + await using var cmd = new OracleCommand(plsql, conn); +#endif + var resultParam = new OracleParameter("result", OracleDbType.Int32, ParameterDirection.Output); + + cmd.Parameters.Add(resultParam); + cmd.Parameters.Add("lockhandle", OracleDbType.Varchar2, lockHandle, ParameterDirection.Input); + cmd.Parameters.Add("timeout", OracleDbType.Int32, timeoutSeconds, ParameterDirection.Input); + + await cmd.ExecuteNonQueryAsync(); + return Convert.ToInt32(resultParam.Value?.ToString()); + } + + private static async Task ReleaseLockHandlerAsync(OracleConnection conn, string lockHandle) + { + const string plsql = """ + BEGIN + :result := DBMS_LOCK.RELEASE(:lockhandle); + END; + """; + +#if NETFRAMEWORK + using var cmd = new OracleCommand(plsql, conn); +#else + await using var cmd = new OracleCommand(plsql, conn); +#endif + var resultParam = new OracleParameter("result", OracleDbType.Int32, ParameterDirection.Output); + cmd.Parameters.Add(resultParam); + cmd.Parameters.Add("lockhandle", OracleDbType.Varchar2, lockHandle, ParameterDirection.Input); + + await cmd.ExecuteNonQueryAsync(); + } +} diff --git a/src/Paramore.Brighter.Locking.Oracle/Paramore.Brighter.Locking.Oracle.csproj b/src/Paramore.Brighter.Locking.Oracle/Paramore.Brighter.Locking.Oracle.csproj new file mode 100644 index 0000000000..029f33cb21 --- /dev/null +++ b/src/Paramore.Brighter.Locking.Oracle/Paramore.Brighter.Locking.Oracle.csproj @@ -0,0 +1,16 @@ + + + + Oracle Database distributed locking provider for Paramore.Brighter Command Processor. Provides lock coordination across multiple instances using Oracle DBMS_LOCK. + Rafael Andrade + $(BrighterMongoTargetFrameworks) + Oracle;Oracle Database;Database;Distributed Locking;Concurrency;Lock;Command Processor;Relational Database;Brighter + enable + enable + + + + + + + diff --git a/src/Paramore.Brighter.Oracle/OracleConnectionProvider.cs b/src/Paramore.Brighter.Oracle/OracleConnectionProvider.cs new file mode 100644 index 0000000000..d5d6b72d20 --- /dev/null +++ b/src/Paramore.Brighter.Oracle/OracleConnectionProvider.cs @@ -0,0 +1,63 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.Oracle; + +/// +/// A connection provider for Oracle using a connection string for database access. +/// +public class OracleConnectionProvider : RelationalDbConnectionProvider +{ + private readonly string _connectionString; + + /// + /// Initializes a new instance of the class. + /// + /// The relational database configuration containing the connection string. + /// Thrown when the connection string is null or empty. + public OracleConnectionProvider(IAmARelationalDatabaseConfiguration configuration) + { + if (string.IsNullOrEmpty(configuration.ConnectionString)) + { + throw new ArgumentNullException(nameof(configuration.ConnectionString)); + } + _connectionString = configuration.ConnectionString; + } + + /// + /// Creates and opens a new Oracle connection. + /// This is not a shared connection; you should manage its lifetime. + /// + /// An open . + public override DbConnection GetConnection() + { + var connection = new OracleConnection(_connectionString); + if (connection.State != ConnectionState.Open) + { + connection.Open(); + } + + return connection; + } + + /// + /// Creates and opens a new Oracle connection asynchronously. + /// This is not a shared connection; you should manage its lifetime. + /// + /// A cancellation token to cancel the operation. + /// An open . + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + var connection = new OracleConnection(_connectionString); + if (connection.State != ConnectionState.Open) + { + await connection.OpenAsync(cancellationToken); + } + + return connection; + } +} diff --git a/src/Paramore.Brighter.Oracle/OracleTransactionProvider.cs b/src/Paramore.Brighter.Oracle/OracleTransactionProvider.cs new file mode 100644 index 0000000000..301d925ac2 --- /dev/null +++ b/src/Paramore.Brighter.Oracle/OracleTransactionProvider.cs @@ -0,0 +1,142 @@ +// The MIT License (MIT) +// Copyright © 2014 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.Oracle; + +/// +/// A connection and transaction provider for Oracle, intended for use with a Unit of Work. +/// The connection and transaction are shared across operations within the same unit of work. +/// +public class OracleTransactionProvider : RelationalDbTransactionProvider +{ + private readonly string _connectionString; + + /// + /// Initializes a new instance of the class. + /// + /// The relational database configuration containing the connection string. + /// Thrown when the connection string is null or empty. + public OracleTransactionProvider(IAmARelationalDatabaseConfiguration configuration) + { + if (string.IsNullOrEmpty(configuration.ConnectionString)) + { + throw new ArgumentNullException(nameof(configuration.ConnectionString)); + } + + _connectionString = configuration.ConnectionString; + } + +#if !NETFRAMEWORK + /// + /// Commits the current transaction asynchronously and releases it. + /// + /// A cancellation token to cancel the operation. + public override async Task CommitAsync(CancellationToken cancellationToken) + { + if (HasOpenTransaction) + { + var transaction = (OracleTransaction)Transaction!; + await transaction.CommitAsync(cancellationToken); + + Transaction = null; + } + } +#endif + + /// + /// Creates and opens a new Oracle connection. + /// This is not a shared connection; you should manage its lifetime. + /// + /// An open . + public override DbConnection GetConnection() + { + Connection ??= new OracleConnection(_connectionString); + if (Connection.State != ConnectionState.Open) + { + Connection.Open(); + } + + return Connection; + } + + /// + /// Creates and opens a new Oracle connection asynchronously. + /// This is not a shared connection; you should manage its lifetime. + /// + /// A cancellation token to cancel the operation. + /// An open . + public override async Task GetConnectionAsync(CancellationToken cancellationToken = default) + { + Connection ??= new OracleConnection(_connectionString); + if (Connection.State != ConnectionState.Open) + { + await Connection.OpenAsync(cancellationToken); + } + + return Connection; + } + + /// + /// Creates and opens a shared Oracle transaction. + /// Reuses the existing transaction if one is already open. + /// + /// The shared . + public override DbTransaction GetTransaction() + { + Connection ??= GetConnection(); + + if (!HasOpenTransaction) + { + Transaction = ((OracleConnection)Connection).BeginTransaction(); + } + + return Transaction!; + } + + /// + /// Creates and opens a shared Oracle transaction asynchronously. + /// Reuses the existing transaction if one is already open. + /// + /// A cancellation token to cancel the operation. + /// The shared . + public override async Task GetTransactionAsync(CancellationToken cancellationToken = default) + { + Connection ??= await GetConnectionAsync(cancellationToken); + + if (!HasOpenTransaction) + { +#if NETFRAMEWORK + Transaction = ((OracleConnection)Connection).BeginTransaction(); +#else + Transaction = await ((OracleConnection)Connection).BeginTransactionAsync(cancellationToken); +#endif + } + + return Transaction!; + } +} diff --git a/src/Paramore.Brighter.Oracle/Paramore.Brighter.Oracle.csproj b/src/Paramore.Brighter.Oracle/Paramore.Brighter.Oracle.csproj new file mode 100644 index 0000000000..39c1dad730 --- /dev/null +++ b/src/Paramore.Brighter.Oracle/Paramore.Brighter.Oracle.csproj @@ -0,0 +1,22 @@ + + + + Oracle Database provider for Paramore.Brighter Command Processor. Provides common components for connecting to Oracle, including connection and transaction management for inbox, outbox, and messaging gateway implementations. + Rafael Andrade + $(BrighterMongoTargetFrameworks) + Oracle;Oracle Database;Database;Connection Provider;Transaction;Command;Event;Command Processor;Brighter + enable + + + + + + + + + + + + + + diff --git a/src/Paramore.Brighter.Outbox.Oracle/OracleOutbox.cs b/src/Paramore.Brighter.Outbox.Oracle/OracleOutbox.cs new file mode 100644 index 0000000000..929fb10d7c --- /dev/null +++ b/src/Paramore.Brighter.Outbox.Oracle/OracleOutbox.cs @@ -0,0 +1,147 @@ +// The MIT License (MIT) +// Copyright © 2014 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using Oracle.ManagedDataAccess.Client; +using Paramore.Brighter.Logging; +using Paramore.Brighter.Observability; +using Paramore.Brighter.Oracle; + +namespace Paramore.Brighter.Outbox.Oracle; + +/// +/// Implements an Outbox using Oracle as a backing store. +/// Requires Oracle 12c or later. +/// +public class OracleOutbox : RelationDatabaseOutbox +{ + /// + /// Initializes a new instance of the class. + /// + /// The relational database configuration. + /// The connection provider. + public OracleOutbox(IAmARelationalDatabaseConfiguration configuration, + IAmARelationalDbConnectionProvider connectionProvider) + : base(DbSystem.Oracle, configuration, connectionProvider, new OracleQueries(), + ApplicationLogging.CreateLogger()) + { + } + + /// + /// Initializes a new instance of the class using a default connection provider. + /// + /// The relational database configuration. + public OracleOutbox(IAmARelationalDatabaseConfiguration configuration) + : this(configuration, new OracleConnectionProvider(configuration)) + { + } + + // ORA-00001: unique constraint violated + private const int DuplicateValue = 1; + + /// + protected override bool IsExceptionUniqueOrDuplicateIssue(Exception ex) + { + return ex is OracleException { Number: DuplicateValue }; + } + + /// + protected override IDbDataParameter CreateSqlParameter(string parameterName, object? value) + { + parameterName = parameterName.Replace("@", ":"); + return new OracleParameter { ParameterName = parameterName, Value = value ?? DBNull.Value }; + } + + /// + protected override IDbDataParameter CreateSqlParameter(string parameterName, DbType dbType, object? value) + { + parameterName = parameterName.Replace("@", ":"); + return new OracleParameter { ParameterName = parameterName, DbType = dbType, Value = value ?? DBNull.Value }; + } + + /// + /// Sets to so that + /// named :param placeholders in the SQL are matched to parameters by name. + protected override DbCommand CreateCommand(DbConnection connection, string sqlText, int outBoxTimeout, + params IDbDataParameter[] parameters) + { + var oracleConnection = (OracleConnection)connection; + var command = oracleConnection.CreateCommand(); + command.BindByName = true; + command.CommandTimeout = outBoxTimeout < 0 ? 0 : outBoxTimeout; + command.CommandText = sqlText; + command.Parameters.AddRange(parameters); + return command; + } + + /// + /// + /// Generates SELECT … FROM DUAL UNION ALL SELECT … FROM DUAL rather than + /// multi-row VALUES tuples, which Oracle does not support before version 23c. + /// + protected override (string insertClause, IDbDataParameter[] parameters) GenerateBulkInsert(List messages) + { + var selects = new List(); + var parameters = new List(); + + for (var i = 0; i < messages.Count; i++) + { + selects.Add( + $"SELECT :p{i}_MessageId,:p{i}_MessageType,:p{i}_Topic,:p{i}_Timestamp,:p{i}_CorrelationId," + + $":p{i}_ReplyTo,:p{i}_ContentType,:p{i}_PartitionKey,:p{i}_HeaderBag,:p{i}_Body," + + $":p{i}_Source,:p{i}_Type,:p{i}_DataSchema,:p{i}_Subject,:p{i}_TraceParent,:p{i}_TraceState," + + $":p{i}_Baggage,:p{i}_WorkflowId,:p{i}_JobId FROM DUAL"); + parameters.AddRange(InitAddDbParameters(messages[i], i)); + } + + return (string.Join(" UNION ALL ", selects), parameters.ToArray()); + } + + protected override (string inClause, IDbDataParameter[] parameters) GenerateInClauseAndAddParameters( + List messageIds) + { + var paramNames = messageIds.Select((_, i) => ":p" + i).ToArray(); + + var parameters = new IDbDataParameter[messageIds.Count]; + for (int i = 0; i < paramNames.Length; i++) + { + parameters[i] = CreateSqlParameter(paramNames[i], DbType.String, messageIds[i]); + } + + return (string.Join(",", paramNames), parameters); + } + + /// + protected override DateTimeOffset GetTimeStamp(DbDataReader dr) + { + if (!TryGetOrdinal(dr, TimestampColumnName, out var ordinal) || dr.IsDBNull(ordinal)) + { + return DateTimeOffset.MinValue; + } + + var reader = (OracleDataReader)dr; + return reader.GetDateTimeOffset(ordinal); + } +} diff --git a/src/Paramore.Brighter.Outbox.Oracle/OracleOutboxBuilder.cs b/src/Paramore.Brighter.Outbox.Oracle/OracleOutboxBuilder.cs new file mode 100644 index 0000000000..0c4e7de36c --- /dev/null +++ b/src/Paramore.Brighter.Outbox.Oracle/OracleOutboxBuilder.cs @@ -0,0 +1,130 @@ +// The MIT License (MIT) +// Copyright © 2014 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; + +namespace Paramore.Brighter.Outbox.Oracle; + +/// +/// Provides DDL statements for creating and checking the existence of an Outbox table in Oracle. +/// Requires Oracle 12c or later (uses identity columns and TIMESTAMP WITH TIME ZONE). +/// +public class OracleOutboxBuilder +{ + private const string TextOutboxDdl = + """ + CREATE TABLE {0} + ( + Id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + MessageId VARCHAR2(255 CHAR) UNIQUE NOT NULL, + Topic VARCHAR2(255 CHAR), + MessageType VARCHAR2(32 CHAR), + Timestamp TIMESTAMP WITH TIME ZONE, + CorrelationId VARCHAR2(255 CHAR), + ReplyTo VARCHAR2(255 CHAR), + ContentType VARCHAR2(128 CHAR), + PartitionKey VARCHAR2(128 CHAR), + WorkflowId VARCHAR2(255 CHAR), + JobId VARCHAR2(255 CHAR), + Dispatched TIMESTAMP WITH TIME ZONE, + HeaderBag CLOB, + Body CLOB, + Source VARCHAR2(255 CHAR), + Type VARCHAR2(255 CHAR), + DataSchema VARCHAR2(255 CHAR), + Subject VARCHAR2(255 CHAR), + TraceParent VARCHAR2(255 CHAR), + TraceState VARCHAR2(255 CHAR), + Baggage CLOB, + DataRef VARCHAR2(255 CHAR), + SpecVersion VARCHAR2(255 CHAR) + ); + """; + + private const string BinaryOutboxDdl = + """ + CREATE TABLE {0} + ( + Id NUMBER(19) GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + MessageId VARCHAR2(255 CHAR) UNIQUE NOT NULL, + Topic VARCHAR2(255 CHAR), + MessageType VARCHAR2(32 CHAR), + Timestamp TIMESTAMP WITH TIME ZONE, + CorrelationId VARCHAR2(255 CHAR), + ReplyTo VARCHAR2(255 CHAR), + ContentType VARCHAR2(128 CHAR), + PartitionKey VARCHAR2(128 CHAR), + WorkflowId VARCHAR2(255 CHAR), + JobId VARCHAR2(255 CHAR), + Dispatched TIMESTAMP WITH TIME ZONE, + HeaderBag CLOB, + Body BLOB, + Source VARCHAR2(255 CHAR), + Type VARCHAR2(255 CHAR), + DataSchema VARCHAR2(255 CHAR), + Subject VARCHAR2(255 CHAR), + TraceParent VARCHAR2(255 CHAR), + TraceState VARCHAR2(255 CHAR), + Baggage CLOB, + DataRef VARCHAR2(255 CHAR), + SpecVersion VARCHAR2(255 CHAR) + ); + """; + + private const string OutboxExistsSql = + """ + SELECT COUNT(1) + FROM ALL_TABLES + WHERE OWNER = '{0}' + AND TABLE_NAME = '{1}'; + """; + + /// + /// Gets the SQL statement that checks whether the Outbox table already exists in the given schema. + /// + /// The table name to check. + /// The Oracle schema (owner) to search within. + /// A SQL query that returns 1 if the table exists, 0 otherwise. + public static string GetExistsQuery(string outboxTable, string ownerName) + { + return string.Format(OutboxExistsSql, outboxTable, ownerName); + } + + /// + /// Gets the DDL statement required to create the Outbox table in Oracle. + /// + /// The name to give the Outbox table. + /// + /// When , the Body column is created as BLOB; + /// otherwise it is created as CLOB. + /// + /// A CREATE TABLE statement formatted with the given table name. + /// Thrown when is null or empty. + public static string GetDDL(string outboxTableName, bool hasBinaryMessagePayload = false) + { + if (string.IsNullOrEmpty(outboxTableName)) + { + throw new ArgumentNullException(outboxTableName, $"You must provide a tablename for the OutBox table"); + } + + return string.Format(hasBinaryMessagePayload ? BinaryOutboxDdl : TextOutboxDdl, outboxTableName); + } +} diff --git a/src/Paramore.Brighter.Outbox.Oracle/OracleQueries.cs b/src/Paramore.Brighter.Outbox.Oracle/OracleQueries.cs new file mode 100644 index 0000000000..0fc2541e6a --- /dev/null +++ b/src/Paramore.Brighter.Outbox.Oracle/OracleQueries.cs @@ -0,0 +1,87 @@ +// The MIT License (MIT) +// Copyright © 2014 Ian Cooper +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +namespace Paramore.Brighter.Outbox.Oracle; + +/// +/// Oracle-specific SQL queries for the Brighter Outbox. +/// Uses OFFSET … ROWS FETCH NEXT … ROWS ONLY for pagination (Oracle 12c+) +/// and named bind variables prefixed with :. +/// Parameter names are matched by name via . +/// +public class OracleQueries : IRelationDatabaseOutboxQueries +{ + /// + public string PagedDispatchedCommand { get; } = + "SELECT * FROM {0} WHERE Dispatched IS NOT NULL AND Dispatched < :DispatchedSince " + + "ORDER BY Timestamp DESC OFFSET :Skip ROWS FETCH NEXT :Take ROWS ONLY"; + + /// + public string PagedReadCommand { get; } = + "SELECT * FROM {0} ORDER BY Timestamp ASC OFFSET :Skip ROWS FETCH NEXT :Take ROWS ONLY"; + + /// + public string PagedOutstandingCommand { get; } = + "SELECT * FROM {0} WHERE Dispatched IS NULL AND Timestamp < :TimestampSince {1} " + + "ORDER BY Timestamp DESC OFFSET :Skip ROWS FETCH NEXT :Take ROWS ONLY"; + + /// + public string PagedOutstandingCommandInStatement { get; } = "AND Topic NOT IN ( {0} )"; + + /// + public string AddCommand { get; } = + "INSERT INTO {0} (MessageId,MessageType,Topic,Timestamp,CorrelationId,ReplyTo,ContentType,PartitionKey," + + "HeaderBag,Body,Source,Type,DataSchema,Subject,TraceParent,TraceState,Baggage,WorkflowId,JobId) " + + "VALUES (:MessageId,:MessageType,:Topic,:Timestamp,:CorrelationId,:ReplyTo,:ContentType,:PartitionKey," + + ":HeaderBag,:Body,:Source,:Type,:DataSchema,:Subject,:TraceParent,:TraceState,:Baggage,:WorkflowId,:JobId)"; + + /// + /// + /// {1} is replaced with SELECT :p0_… FROM DUAL UNION ALL SELECT :p1_… FROM DUAL, + /// generated by . Oracle does not support + /// multi-row VALUES clauses before version 23c. + /// + public string BulkAddCommand { get; } = + "INSERT INTO {0} (MessageId,MessageType,Topic,Timestamp,CorrelationId,ReplyTo,ContentType,PartitionKey," + + "HeaderBag,Body,Source,Type,DataSchema,Subject,TraceParent,TraceState,Baggage,WorkflowId,JobId) {1}"; + + /// + public string MarkDispatchedCommand { get; } = + "UPDATE {0} SET Dispatched = :DispatchedAt WHERE MessageId = :MessageId"; + + /// + public string MarkMultipleDispatchedCommand { get; } = + "UPDATE {0} SET Dispatched = :DispatchedAt WHERE MessageId IN ( {1} )"; + + /// + public string GetMessageCommand { get; } = "SELECT * FROM {0} WHERE MessageId = :MessageId"; + + /// + public string GetMessagesCommand { get; } = + "SELECT * FROM {0} WHERE MessageId IN ( {1} ) ORDER BY Timestamp ASC"; + + /// + public string DeleteMessagesCommand { get; } = "DELETE FROM {0} WHERE MessageId IN ( {1} )"; + + /// + public string GetNumberOfOutstandingMessagesCommand { get; } = + "SELECT COUNT(1) FROM {0} WHERE Dispatched IS NULL"; +} diff --git a/src/Paramore.Brighter.Outbox.Oracle/Paramore.Brighter.Outbox.Oracle.csproj b/src/Paramore.Brighter.Outbox.Oracle/Paramore.Brighter.Outbox.Oracle.csproj new file mode 100644 index 0000000000..42564c3ebd --- /dev/null +++ b/src/Paramore.Brighter.Outbox.Oracle/Paramore.Brighter.Outbox.Oracle.csproj @@ -0,0 +1,15 @@ + + + + Oracle Database outbox implementation for Paramore.Brighter Command Processor. Provides reliable message publishing with outbox pattern using Oracle for transactional consistency and guaranteed delivery. + Rafael Andrade + $(BrighterMongoTargetFrameworks) + Oracle;Oracle Database;Database;Outbox;Transactional;Command;Event;Service Activator;Decoupled;Invocation;Messaging;Remote;Command Dispatcher;Command Processor;Request;Service;Task Queue;Work Queue;Retry;Circuit Breaker;Availability;Brighter + enable + + + + + + + diff --git a/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/ExpectedMigrationVersions.cs b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/ExpectedMigrationVersions.cs new file mode 100644 index 0000000000..73a880c004 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/ExpectedMigrationVersions.cs @@ -0,0 +1,7 @@ +namespace Paramore.Brighter.Oracle.Tests.BoxProvisioning; + +internal static class ExpectedMigrationVersions +{ + public const int OutboxLatest = 1; + public const int InboxLatest = 1; +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/OracleBoxProvisioningTestHelper.cs b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/OracleBoxProvisioningTestHelper.cs new file mode 100644 index 0000000000..5f3f758ae6 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/OracleBoxProvisioningTestHelper.cs @@ -0,0 +1,67 @@ +using System; +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.Oracle.Tests.BoxProvisioning; + +internal static class OracleBoxProvisioningTestHelper +{ + public static async Task TableExistsAsync(string connectionString, string tableName) + { + await using var connection = new OracleConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.BindByName = true; + command.CommandText = """ + SELECT COUNT(1) FROM ALL_TABLES + WHERE OWNER = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') + AND TABLE_NAME = UPPER(:TableName) + """; + command.Parameters.Add(new OracleParameter("TableName", tableName)); + + var count = Convert.ToInt32(await command.ExecuteScalarAsync()); + return count > 0; + } + + public static async Task HistoryCountForVersionAsync(string connectionString, string tableName, int version) + { + await using var connection = new OracleConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.BindByName = true; + command.CommandText = """ + SELECT COUNT(1) FROM BRIGHTER_MIGRATION_HISTORY + WHERE BoxTableName = :BoxTableName + AND SchemaName = SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') + AND MigrationVersion = :MigrationVersion + """; + command.Parameters.Add(new OracleParameter("BoxTableName", tableName)); + command.Parameters.Add(new OracleParameter("MigrationVersion", version)); + + return Convert.ToInt32(await command.ExecuteScalarAsync()); + } + + public static async Task DropTableIfExistsAsync(string connectionString, string tableName) + { + await using var connection = new OracleConnection(connectionString); + await connection.OpenAsync(); + + await using var command = connection.CreateCommand(); + command.BindByName = true; + command.CommandText = """ + BEGIN + EXECUTE IMMEDIATE 'DROP TABLE ' || :TableName; + EXCEPTION + WHEN OTHERS THEN + IF SQLCODE != -942 THEN + RAISE; + END IF; + END; + """; + command.Parameters.Add(new OracleParameter("TableName", tableName)); + + await command.ExecuteNonQueryAsync(); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_inbox_provisioner_runs_on_fresh_database_it_should_create_inbox_table.cs b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_inbox_provisioner_runs_on_fresh_database_it_should_create_inbox_table.cs new file mode 100644 index 0000000000..c1f2e1fcbc --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_inbox_provisioner_runs_on_fresh_database_it_should_create_inbox_table.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using Paramore.Brighter.BoxProvisioning.Oracle; +using Xunit; + +namespace Paramore.Brighter.Oracle.Tests.BoxProvisioning; + +public class OracleInboxProvisionerFreshDatabaseTests : IAsyncLifetime +{ + private readonly string _connectionString = Const.DefaultConnectingString; + private readonly string _tableName; + private readonly OracleInboxProvisioner _provisioner; + + public OracleInboxProvisionerFreshDatabaseTests() + { + _tableName = $"TEST_INBOX_{Guid.NewGuid():N}"; + + var configuration = new RelationalDatabaseConfiguration( + _connectionString, + inboxTableName: _tableName); + + var runner = new OracleBoxMigrationRunner( + new OracleInboxMigrationCatalog(), + configuration, + TimeSpan.FromSeconds(30)); + + _provisioner = new OracleInboxProvisioner( + new OracleBoxDetectionHelper(), + new OracleInboxMigrationCatalog(), + new OraclePayloadModeValidator(), + configuration, + runner); + } + + [Fact] + public async Task When_inbox_provisioner_runs_on_fresh_database_it_should_create_inbox_table() + { + await _provisioner.ProvisionAsync(); + + var tableExists = await OracleBoxProvisioningTestHelper.TableExistsAsync(_connectionString, _tableName); + Assert.True(tableExists); + + var historyCount = await OracleBoxProvisioningTestHelper.HistoryCountForVersionAsync( + _connectionString, + _tableName, + ExpectedMigrationVersions.InboxLatest); + + Assert.Equal(1, historyCount); + } + + public Task InitializeAsync() => Task.CompletedTask; + + public async Task DisposeAsync() + { + await OracleBoxProvisioningTestHelper.DropTableIfExistsAsync(_connectionString, _tableName); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_outbox_provisioner_runs_on_fresh_database_it_should_create_outbox_table.cs b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_outbox_provisioner_runs_on_fresh_database_it_should_create_outbox_table.cs new file mode 100644 index 0000000000..e70cf83cca --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/BoxProvisioning/When_oracle_outbox_provisioner_runs_on_fresh_database_it_should_create_outbox_table.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading.Tasks; +using Paramore.Brighter.BoxProvisioning.Oracle; +using Xunit; + +namespace Paramore.Brighter.Oracle.Tests.BoxProvisioning; + +public class OracleOutboxProvisionerFreshDatabaseTests : IAsyncLifetime +{ + private readonly string _connectionString = Const.DefaultConnectingString; + private readonly string _tableName; + private readonly OracleOutboxProvisioner _provisioner; + + public OracleOutboxProvisionerFreshDatabaseTests() + { + _tableName = $"TEST_OUTBOX_{Guid.NewGuid():N}"; + + var configuration = new RelationalDatabaseConfiguration( + _connectionString, + outBoxTableName: _tableName); + + var runner = new OracleBoxMigrationRunner( + new OracleOutboxMigrationCatalog(), + configuration, + TimeSpan.FromSeconds(30)); + + _provisioner = new OracleOutboxProvisioner( + new OracleBoxDetectionHelper(), + new OracleOutboxMigrationCatalog(), + new OraclePayloadModeValidator(), + configuration, + runner); + } + + [Fact] + public async Task When_outbox_provisioner_runs_on_fresh_database_it_should_create_outbox_table() + { + await _provisioner.ProvisionAsync(); + + var tableExists = await OracleBoxProvisioningTestHelper.TableExistsAsync(_connectionString, _tableName); + Assert.True(tableExists); + + var historyCount = await OracleBoxProvisioningTestHelper.HistoryCountForVersionAsync( + _connectionString, + _tableName, + ExpectedMigrationVersions.OutboxLatest); + + Assert.Equal(1, historyCount); + } + + public Task InitializeAsync() => Task.CompletedTask; + + public async Task DisposeAsync() + { + await OracleBoxProvisioningTestHelper.DropTableIfExistsAsync(_connectionString, _tableName); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Const.cs b/tests/Paramore.Brighter.Oracle.Tests/Const.cs new file mode 100644 index 0000000000..a6794fd4c8 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Const.cs @@ -0,0 +1,9 @@ +namespace Paramore.Brighter.Oracle.Tests; + +public static class Const +{ + public const string DefaultConnectingString = + "User Id=brighter;Password=BrighterTests1!;Data Source=//localhost:1521/FREEPDB1"; + + public const string TablePrefix = "Table"; +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageAssertion.cs b/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageAssertion.cs new file mode 100644 index 0000000000..80fb3e469d --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageAssertion.cs @@ -0,0 +1,64 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +namespace Paramore.Brighter.Oracle.Tests; + +/// +/// Default implementation of that asserts equality of message headers and body. +/// +public class DefaultMessageAssertion : IAmAMessageAssertion +{ + /// + /// Asserts that the actual message matches the expected message by comparing all header properties and body value. + /// + /// The expected message. + /// The actual message to compare. + public void Assert(Message expected, Message actual) + { + Xunit.Assert.Equal(expected.Header.MessageType, actual.Header.MessageType); + Xunit.Assert.Equal(expected.Header.ContentType, actual.Header.ContentType); + Xunit.Assert.Equal(expected.Header.CorrelationId, actual.Header.CorrelationId); + Xunit.Assert.Equal(expected.Header.DataSchema, actual.Header.DataSchema); + Xunit.Assert.Equal(expected.Header.MessageId, actual.Header.MessageId); + Xunit.Assert.Equal(expected.Header.PartitionKey, actual.Header.PartitionKey); + Xunit.Assert.Equal(expected.Header.ReplyTo, actual.Header.ReplyTo); + Xunit.Assert.Equal(expected.Header.Subject, actual.Header.Subject); + Xunit.Assert.Equal(expected.Header.SpecVersion, actual.Header.SpecVersion); + Xunit.Assert.Equal(expected.Header.Source, actual.Header.Source); + Xunit.Assert.Equal(expected.Header.Topic, actual.Header.Topic); + Xunit.Assert.Equal(expected.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss"), actual.Header.TimeStamp.ToString("yyyy-MM-ddTHH:mm:ss")); + Xunit.Assert.Equal(expected.Header.Type, actual.Header.Type); + Xunit.Assert.Equal(expected.Header.HandledCount, actual.Header.HandledCount); + Xunit.Assert.Equal(System.TimeSpan.Zero, actual.Header.Delayed); + Xunit.Assert.Equal(expected.Body.Value, actual.Body.Value); + Xunit.Assert.Equal(expected.Header.TraceParent, actual.Header.TraceParent); + Xunit.Assert.Equal(expected.Header.TraceState, actual.Header.TraceState); + Xunit.Assert.Equal(expected.Header.Baggage, actual.Header.Baggage); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageBuilder.cs b/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageBuilder.cs new file mode 100644 index 0000000000..c8da9c2196 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/DefaultMessageBuilder.cs @@ -0,0 +1,256 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// +using System; +using System.Collections.Generic; +using System.Net.Mime; +using System.Text; + +using Paramore.Brighter.Observability; + +namespace Paramore.Brighter.Oracle.Tests; + +/// +/// Default implementation of that creates messages with randomly generated test data. +/// +public class DefaultMessageBuilder : IAmAMessageBuilder +{ + private Dictionary _bag = new() + { + ["header1"] = Uuid.NewAsString(), + ["header2"] = Uuid.NewAsString(), + ["header3"] = Uuid.NewAsString(), + ["header4"] = Uuid.NewAsString(), + ["header5"] = Uuid.NewAsString() + }; + private Baggage _baggage = new(); + private ContentType _contentType = new ContentType(MediaTypeNames.Text.Plain); + private Id _correlationId = Id.Random(); + private Uri? _dataSchema = new Uri($"https://{Uuid.New():N}.test"); + private string? _dataRef = Uuid.NewAsString(); + private TimeSpan? _delayed; + private Id? _jobId = Id.Random(); + private Id? _messageId = Id.Random(); + private MessageType _messageType = MessageType.MT_EVENT; + private PartitionKey _partitionKey = PartitionKey.Empty; + private RoutingKey? _replyTo = new RoutingKey(Uuid.NewAsString()); + private string? _subject = Uuid.NewAsString(); + private string? _specVersion = "1.0"; + private Uri? _source = new Uri(Uuid.NewAsString(), UriKind.Relative); + private RoutingKey _topic = new RoutingKey(Uuid.NewAsString()); + private DateTimeOffset _timeStamp = DateTimeOffset.UtcNow; + private TraceParent? _traceParent = new TraceParent(Uuid.NewAsString()); + private TraceState? _traceState = new TraceState(Uuid.NewAsString()); + private CloudEventsType _type = new CloudEventsType(Uuid.NewAsString()); + private Id? _workflowId = Id.Random(); + private byte[] _body = Encoding.UTF8.GetBytes(Uuid.NewAsString()); + + /// + public IAmAMessageBuilder SetBag(Dictionary bag) + { + _bag = bag; + return this; + } + + /// + public IAmAMessageBuilder SetBaggage(Baggage baggage) + { + _baggage = baggage; + return this; + } + + /// + public IAmAMessageBuilder SetContentType(ContentType contentType) + { + _contentType = contentType; + return this; + } + + /// + public IAmAMessageBuilder SetCorrelationId(Id correlationId) + { + _correlationId = correlationId; + return this; + } + + /// + public IAmAMessageBuilder SetDataSchema(Uri? dataSchema) + { + _dataSchema = dataSchema; + return this; + } + + /// + public IAmAMessageBuilder SetDataRef(string? dataRef) + { + _dataRef = dataRef; + return this; + } + + /// + public IAmAMessageBuilder SetDelayed(TimeSpan? delayed) + { + _delayed = delayed; + return this; + } + + /// + public IAmAMessageBuilder SetJobId(Id? jobId) + { + _jobId = jobId; + return this; + } + + /// + public IAmAMessageBuilder SetMessageId(Id? messageId) + { + _messageId = messageId; + return this; + } + + /// + public IAmAMessageBuilder SetMessageType(MessageType messageType) + { + _messageType = messageType; + return this; + } + + /// + public IAmAMessageBuilder SetPartitionKey(PartitionKey partitionKey) + { + _partitionKey = partitionKey; + return this; + } + + /// + public IAmAMessageBuilder SetReplyTo(RoutingKey? replyTo) + { + _replyTo = replyTo; + return this; + } + + /// + public IAmAMessageBuilder SetSubject(string? subject) + { + _subject = subject; + return this; + } + + /// + public IAmAMessageBuilder SetSpecVersion(string? specVersion) + { + _specVersion = specVersion; + return this; + } + + /// + public IAmAMessageBuilder SetSource(Uri? source) + { + _source = source; + return this; + } + + /// + public IAmAMessageBuilder SetTopic(RoutingKey topic) + { + _topic = topic; + return this; + } + + /// + public IAmAMessageBuilder SetTimeStamp(DateTimeOffset timeStamp) + { + _timeStamp = timeStamp; + return this; + } + + /// + public IAmAMessageBuilder SetTraceParent(TraceParent? traceParent) + { + _traceParent = traceParent; + return this; + } + + /// + public IAmAMessageBuilder SetTraceState(TraceState? traceState) + { + _traceState = traceState; + return this; + } + + /// + public IAmAMessageBuilder SetType(CloudEventsType type) + { + _type = type; + return this; + } + + /// + public IAmAMessageBuilder SetWorkflowId(Id? workflowId) + { + _workflowId = workflowId; + return this; + } + + /// + public IAmAMessageBuilder SetBody(byte[] body) + { + _body = body; + return this; + } + + /// + public Message Build() + { + var messageHeader = new MessageHeader( + messageId: _messageId, + topic: _topic, + messageType: _messageType, + source: _source, + type: _type, + timeStamp: _timeStamp, + correlationId: _correlationId, + replyTo: _replyTo, + contentType: _contentType, + partitionKey: _partitionKey, + subject: _subject, + dataSchema: _dataSchema, + delayed: _delayed, + traceParent: _traceParent, + traceState: _traceState, + baggage: _baggage, + workflowId: _workflowId, + jobId: _jobId) + { + SpecVersion = _specVersion, + Bag = _bag + }; + + return new Message(messageHeader, new MessageBody(_body)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageAssertion.cs b/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageAssertion.cs new file mode 100644 index 0000000000..76a45c64dc --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageAssertion.cs @@ -0,0 +1,43 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +namespace Paramore.Brighter.Oracle.Tests; + +/// +/// Defines a contract for asserting equality between two Message instances. +/// +public interface IAmAMessageAssertion +{ + /// + /// Asserts that the actual message matches the expected message. + /// + /// The expected message. + /// The actual message to compare. + void Assert(Message expected, Message actual); +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageBuilder.cs b/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageBuilder.cs new file mode 100644 index 0000000000..30d7e07941 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/IAmAMessageBuilder.cs @@ -0,0 +1,201 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// +using System; +using System.Collections.Generic; +using System.Net.Mime; +using Paramore.Brighter.Observability; + +namespace Paramore.Brighter.Oracle.Tests; + +/// +/// Builder interface for creating test messages with customizable header values and body content. +/// +public interface IAmAMessageBuilder +{ + /// + /// Sets the bag of additional header values. + /// + /// The dictionary of additional header values. + /// The builder instance for method chaining. + IAmAMessageBuilder SetBag(Dictionary bag); + + /// + /// Sets the baggage for distributed tracing context propagation. + /// + /// The baggage for distributed tracing. + /// The builder instance for method chaining. + IAmAMessageBuilder SetBaggage(Baggage baggage); + + /// + /// Sets the content type of the message body. + /// + /// The content type of the message. + /// The builder instance for method chaining. + IAmAMessageBuilder SetContentType(ContentType contentType); + + /// + /// Sets the correlation identifier for tracking related messages. + /// + /// The correlation identifier. + /// The builder instance for method chaining. + IAmAMessageBuilder SetCorrelationId(Id correlationId); + + /// + /// Sets the URI identifying the schema of the data in the message body. + /// + /// The data schema URI. + /// The builder instance for method chaining. + IAmAMessageBuilder SetDataSchema(Uri? dataSchema); + + /// + /// Sets a reference to external data. + /// + /// The external data reference. + /// The builder instance for method chaining. + IAmAMessageBuilder SetDataRef(string? dataRef); + + /// + /// Sets the delay before the message should be processed. + /// + /// The delay timespan. + /// The builder instance for method chaining. + IAmAMessageBuilder SetDelayed(TimeSpan? delayed); + + /// + /// Sets the job identifier for workflow tracking. + /// + /// The job identifier. + /// The builder instance for method chaining. + IAmAMessageBuilder SetJobId(Id? jobId); + + /// + /// Sets the unique message identifier. + /// + /// The message identifier. + /// The builder instance for method chaining. + IAmAMessageBuilder SetMessageId(Id? messageId); + + /// + /// Sets the message type (Event, Command, etc.). + /// + /// The message type. + /// The builder instance for method chaining. + IAmAMessageBuilder SetMessageType(MessageType messageType); + + /// + /// Sets the partition key for message routing. + /// + /// The partition key. + /// The builder instance for method chaining. + IAmAMessageBuilder SetPartitionKey(PartitionKey partitionKey); + + /// + /// Sets the reply-to routing key for response messages. + /// + /// The reply-to routing key. + /// The builder instance for method chaining. + IAmAMessageBuilder SetReplyTo(RoutingKey? replyTo); + + /// + /// Sets the subject or title of the message. + /// + /// The message subject. + /// The builder instance for method chaining. + IAmAMessageBuilder SetSubject(string? subject); + + /// + /// Sets the CloudEvents specification version. + /// + /// The CloudEvents spec version. + /// The builder instance for method chaining. + IAmAMessageBuilder SetSpecVersion(string? specVersion); + + /// + /// Sets the URI identifying the source of the message. + /// + /// The message source URI. + /// The builder instance for method chaining. + IAmAMessageBuilder SetSource(Uri? source); + + /// + /// Sets the topic routing key for message publishing. + /// + /// The topic routing key. + /// The builder instance for method chaining. + IAmAMessageBuilder SetTopic(RoutingKey topic); + + /// + /// Sets the timestamp when the message was created. + /// + /// The message timestamp. + /// The builder instance for method chaining. + IAmAMessageBuilder SetTimeStamp(DateTimeOffset timeStamp); + + /// + /// Sets the W3C trace parent for distributed tracing. + /// + /// The W3C trace parent. + /// The builder instance for method chaining. + IAmAMessageBuilder SetTraceParent(TraceParent? traceParent); + + /// + /// Sets the W3C trace state for vendor-specific tracing information. + /// + /// The W3C trace state. + /// The builder instance for method chaining. + IAmAMessageBuilder SetTraceState(TraceState? traceState); + + /// + /// Sets the CloudEvents type describing the message content. + /// + /// The CloudEvents type. + /// The builder instance for method chaining. + IAmAMessageBuilder SetType(CloudEventsType type); + + /// + /// Sets the workflow identifier for process tracking. + /// + /// The workflow identifier. + /// The builder instance for method chaining. + IAmAMessageBuilder SetWorkflowId(Id? workflowId); + + /// + /// Sets the message body content as a byte array. + /// + /// The message body content. + /// The builder instance for method chaining. + IAmAMessageBuilder SetBody(byte[] body); + + /// + /// Builds and returns the configured message. + /// + /// The constructed message with all configured properties. + Message Build(); +} + diff --git a/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxAsyncTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxAsyncTest.cs new file mode 100644 index 0000000000..e78cc1853e --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxAsyncTest.cs @@ -0,0 +1,6 @@ +namespace Paramore.Brighter.Oracle.Tests.Inbox; + +public class OracleBinaryInboxAsyncTest : OracleTextInboxAsyncTest +{ + protected override bool BinaryMessagePayload => true; +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxTest.cs new file mode 100644 index 0000000000..af74edc95d --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleBinaryInboxTest.cs @@ -0,0 +1,6 @@ +namespace Paramore.Brighter.Oracle.Tests.Inbox; + +public class OracleBinaryInboxTest : OracleTextInboxTest +{ + protected override bool BinaryMessagePayload => true; +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleJsonInboxTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleJsonInboxTest.cs new file mode 100644 index 0000000000..75abc7490c --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleJsonInboxTest.cs @@ -0,0 +1,7 @@ +namespace Paramore.Brighter.Oracle.Tests.Inbox; + +public class OracleJsonInboxTest : OracleTextInboxTest +{ + protected override bool JsonMessagePayload => true; + protected override bool BinaryMessagePayload => true; +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxAsyncTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxAsyncTest.cs new file mode 100644 index 0000000000..66bedffa91 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxAsyncTest.cs @@ -0,0 +1,40 @@ +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; +using Paramore.Brighter.Base.Test.Inbox; +using Paramore.Brighter.Inbox.Oracle; + +namespace Paramore.Brighter.Oracle.Tests.Inbox; + +public class OracleTextInboxAsyncTest : RelationalDatabaseInboxAsyncTests +{ + protected override string DefaultConnectingString => Const.DefaultConnectingString; + protected override string TableNamePrefix => Const.TablePrefix; + protected override bool BinaryMessagePayload => false; + protected override bool JsonMessagePayload => false; + + protected override RelationalDatabaseInbox CreateInbox(RelationalDatabaseConfiguration configuration) + { + return new OracleInbox(configuration); + } + + protected override async Task CreateInboxTableAsync(RelationalDatabaseConfiguration configuration) + { + await using var connection = new OracleConnection(configuration.ConnectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = OracleInboxBuilder.GetDDL( + configuration.InBoxTableName, + BinaryMessagePayload, + JsonMessagePayload); + await command.ExecuteNonQueryAsync(); + } + + protected override async Task DeleteInboxTableAsync(RelationalDatabaseConfiguration configuration) + { + await using var connection = new OracleConnection(configuration.ConnectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = $"DROP TABLE {configuration.InBoxTableName}"; + await command.ExecuteNonQueryAsync(); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxTest.cs new file mode 100644 index 0000000000..8faef78ab4 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Inbox/OracleTextInboxTest.cs @@ -0,0 +1,39 @@ +using Oracle.ManagedDataAccess.Client; +using Paramore.Brighter.Base.Test.Inbox; +using Paramore.Brighter.Inbox.Oracle; + +namespace Paramore.Brighter.Oracle.Tests.Inbox; + +public class OracleTextInboxTest : RelationalDatabaseInboxTests +{ + protected override string DefaultConnectingString => Const.DefaultConnectingString; + protected override string TableNamePrefix => Const.TablePrefix; + protected override bool BinaryMessagePayload => false; + protected override bool JsonMessagePayload => false; + + protected override RelationalDatabaseInbox CreateInbox(RelationalDatabaseConfiguration configuration) + { + return new OracleInbox(configuration); + } + + protected override void CreateInboxTable(RelationalDatabaseConfiguration configuration) + { + using var connection = new OracleConnection(configuration.ConnectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = OracleInboxBuilder.GetDDL( + configuration.InBoxTableName, + BinaryMessagePayload, + JsonMessagePayload); + command.ExecuteNonQuery(); + } + + protected override void DeleteInboxTable(RelationalDatabaseConfiguration configuration) + { + using var connection = new OracleConnection(configuration.ConnectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = $"DROP TABLE {configuration.InBoxTableName}"; + command.ExecuteNonQuery(); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Locking/OracleDistributedLockingTest.cs b/tests/Paramore.Brighter.Oracle.Tests/Locking/OracleDistributedLockingTest.cs new file mode 100644 index 0000000000..cc361a7ad6 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Locking/OracleDistributedLockingTest.cs @@ -0,0 +1,14 @@ +using Paramore.Brighter.Base.Test.Locking; +using Paramore.Brighter.Locking.Oracle; + +namespace Paramore.Brighter.Oracle.Tests.Locking; + +public class OracleDistributedLockingTest : RelationalDatabaseDistributedLockingAsyncTest +{ + protected override string DefaultConnectingString => Const.DefaultConnectingString; + + protected override IDistributedLock CreateDistributedLock() + { + return new OracleLockingProvider(new OracleConnectionProvider(Configuration)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/IAmAnOutboxProviderAsync.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/IAmAnOutboxProviderAsync.cs new file mode 100644 index 0000000000..1c9e892028 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/IAmAnOutboxProviderAsync.cs @@ -0,0 +1,68 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +/// +/// Provider for managing outbox storage and creating outbox instances. +/// +public interface IAmAnOutboxProviderAsync +{ + /// + /// Creates the outbox data store. + /// + Task CreateStoreAsync(); + + /// + /// Deletes the outbox data store and removes the specified messages. + /// + /// The messages to remove from the store. + Task DeleteStoreAsync(IEnumerable messages); + + /// + /// Creates a new outbox instance for storing and retrieving messages. + /// + /// A new instance. + IAmAnOutboxAsync CreateOutboxAsync(); + + /// + /// Gets all the messages in the outbox. + /// + /// A list of all messages in the outbox. + Task> GetAllMessagesAsync(); + + /// + /// Creates a new transaction provider. + /// + /// A new instance. + IAmABoxTransactionProvider CreateTransactionProvider(); +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs new file mode 100644 index 0000000000..910231ad22 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs @@ -0,0 +1,54 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenAddingADuplicateMessageItShouldNotThrowAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingADuplicateMessageItShouldNotThrowAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _messageBuilder = new DefaultMessageBuilder(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async() + { + // Arrange + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync(message, context); + + // Assert + // Just adding a simple assertion to remove any warning + Assert.True(true); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs new file mode 100644 index 0000000000..b40250bfdd --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs @@ -0,0 +1,87 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenAddingAMessageItShouldBeStoredWithAllPropertiesAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageItShouldBeStoredWithAllPropertiesAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async() + { + // Arrange + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync(message, context); + + var storedMessage = await outbox.GetAsync(message.Id, context); + + // Assert + Assert.Equal(message.Body.Value, storedMessage.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, storedMessage.Header.Topic); + Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox + Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, storedMessage.Header.Bag); + Assert.Equal(val, storedMessage.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId); + Assert.Equal(message.Header.JobId, storedMessage.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, storedMessage.Header.Source); + Assert.Equal(message.Header.Type, storedMessage.Header.Type); + Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema); + Assert.Equal(message.Header.Subject, storedMessage.Header.Subject); + Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent); + Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs new file mode 100644 index 0000000000..0f408e255c --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs @@ -0,0 +1,62 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStoredAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStoredAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async() + { + // Arrange + var outbox = _outboxProvider.CreateOutboxAsync(); + + var transaction = _outboxProvider.CreateTransactionProvider(); + _ = await transaction.GetTransactionAsync(); + + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + await outbox.AddAsync(message, context, transactionProvider: transaction); + await transaction.RollbackAsync(); + + var storedMessage = await outbox.GetAsync(message.Id, context); + + // Assert + Assert.Equal(MessageType.MT_NONE, storedMessage.Header.MessageType); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs new file mode 100644 index 0000000000..23db262140 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs @@ -0,0 +1,92 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommitAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommitAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async() + { + // Arrange + var outbox = _outboxProvider.CreateOutboxAsync(); + + var transaction = _outboxProvider.CreateTransactionProvider(); + _ = await transaction.GetTransactionAsync(); + + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message ); + + // Act + await outbox.AddAsync(message, context, transactionProvider: transaction); + await transaction.CommitAsync(); + + var storedMessage = await outbox.GetAsync(message.Id, context); + + // Assert + Assert.Equal(message.Body.Value, storedMessage.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, storedMessage.Header.Topic); + Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox + Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, storedMessage.Header.Bag); + Assert.Equal(val, storedMessage.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId); + Assert.Equal(message.Header.JobId, storedMessage.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, storedMessage.Header.Source); + Assert.Equal(message.Header.Type, storedMessage.Header.Type); + Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema); + Assert.Equal(message.Header.Subject, storedMessage.Header.Subject); + Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent); + Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs new file mode 100644 index 0000000000..46a718245a --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs @@ -0,0 +1,65 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutboxAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutboxAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async() + { + // Arrange + var context = new RequestContext(); + var firstMessage = new DefaultMessageBuilder().Build(); + var secondMessage = new DefaultMessageBuilder().Build(); + var thirdMessage = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(firstMessage); + _createdMessages.Add(secondMessage); + _createdMessages.Add(thirdMessage); + + // Act + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync(firstMessage, context); + await outbox.AddAsync(secondMessage, context); + await outbox.AddAsync(thirdMessage, context); + + await outbox.DeleteAsync([firstMessage.Id, secondMessage.Id, thirdMessage.Id], context); + + // Assert + var messages = (await outbox + .OutstandingMessagesAsync(TimeSpan.Zero, context)) + .ToArray(); + + Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(secondMessage.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(thirdMessage.Id, messages.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs new file mode 100644 index 0000000000..62bb258f5a --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs @@ -0,0 +1,91 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenDeletingOneMessageItShouldBeRemovedFromOutboxAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenDeletingOneMessageItShouldBeRemovedFromOutboxAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async() + { + // Arrange + var context = new RequestContext(); + + var firstMessage = new DefaultMessageBuilder().Build(); + var secondMessage = new DefaultMessageBuilder().Build(); + var thirdMessage = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(firstMessage); + _createdMessages.Add(secondMessage); + _createdMessages.Add(thirdMessage); + + // Act + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync(firstMessage, context); + await outbox.AddAsync(secondMessage, context); + await outbox.AddAsync(thirdMessage, context); + + await outbox.DeleteAsync([firstMessage.Id], context); + + // Assert + var messages = (await outbox + .OutstandingMessagesAsync(TimeSpan.Zero, context)) + .ToArray(); + + Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id)); + Assert.Contains(secondMessage.Id, messages.Select(x => x.Id)); + Assert.Contains(thirdMessage.Id, messages.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs new file mode 100644 index 0000000000..07bf9e25b6 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs @@ -0,0 +1,115 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessageAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessageAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync([earliest, dispatched, undispatched], context); + await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var message = await outbox.GetAsync(dispatched.Id, context); + + // Assert + Assert.NotNull(message); + Assert.Equal(message.Body.Value, dispatched.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, dispatched.Header.Topic); + Assert.Equal(message.Header.MessageType, dispatched.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, dispatched.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, dispatched.Header.HandledCount); // -- should be zero when read from outbox + // Assert.Equal(TimeSpan.Zero, dispatched.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, dispatched.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, dispatched.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.MediaType, dispatched.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, dispatched.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, dispatched.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, dispatched.Header.Bag); + Assert.Equal(val.ToString(), dispatched.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, dispatched.Header.WorkflowId); + Assert.Equal(message.Header.JobId, dispatched.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, dispatched.Header.Source); + Assert.Equal(message.Header.Type, dispatched.Header.Type); + Assert.Equal(message.Header.DataSchema, dispatched.Header.DataSchema); + Assert.Equal(message.Header.Subject, dispatched.Header.Subject); + Assert.Equal(message.Header.TraceParent, dispatched.Header.TraceParent); + Assert.Equal(message.Header.TraceState, dispatched.Header.TraceState); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs new file mode 100644 index 0000000000..3a5aecb18e --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs @@ -0,0 +1,52 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenRetrievingANonExistentMessageItShouldReturnEmptyMessageAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenRetrievingANonExistentMessageItShouldReturnEmptyMessageAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async() + { + // Arrange + var context = new RequestContext(); + + var outbox = _outboxProvider.CreateOutboxAsync(); + + // Act + var message = await outbox.GetAsync(Id.Random(), context); + + // Assert + Assert.Equal(MessageType.MT_NONE, message.Header.MessageType); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs new file mode 100644 index 0000000000..3c23e7e3bd --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs @@ -0,0 +1,87 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatchedAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatchedAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync([earliest, dispatched, undispatched], context); + await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var messages = (await _outboxProvider.GetAllMessagesAsync()).ToArray(); + + // Assert + Assert.True(messages.Length >= 3, "Expecting at least 3 messages"); + Assert.Contains(earliest.Id, messages.Select(x => x.Id)); + Assert.Contains(dispatched.Id, messages.Select(x => x.Id)); + Assert.Contains(undispatched.Id, messages.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs new file mode 100644 index 0000000000..14e5cc7c61 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs @@ -0,0 +1,73 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenRetrievingDispatchedMessagesItShouldFilterByAgeAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingDispatchedMessagesItShouldFilterByAgeAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync([earliest, dispatched, undispatched], context); + await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var allDispatched = (await outbox.DispatchedMessagesAsync(TimeSpan.Zero, context)).ToArray(); + var messagesOverAnHour = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(1), context)).ToArray(); + var messagesOver4Hours = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context)).ToArray(); + + // Assert + Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages"); + Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id)); + Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id)); + + Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message"); + Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id)); + + Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs new file mode 100644 index 0000000000..92a9e8b50c --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs @@ -0,0 +1,89 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessagesAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessagesAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync([earliest, dispatched, undispatched], context); + await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var messages = (await outbox + .GetAsync([earliest.Id, undispatched.Id], context)) + .ToArray(); + + // Assert + Assert.Equal(2, messages.Length); + Assert.Contains(earliest.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messages.Select(x => x.Id)); + Assert.Contains(undispatched.Id, messages.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs new file mode 100644 index 0000000000..f1bc0e5149 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs @@ -0,0 +1,73 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleBinaryOutbox")] +public class WhenRetrievingOutstandingMessagesItShouldFilterByAgeAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingOutstandingMessagesItShouldFilterByAgeAsync() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync([earliest, dispatched, undispatched], context); + await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var allDispatched = (await outbox.DispatchedMessagesAsync(TimeSpan.Zero, context)).ToArray(); + var messagesOverAnHour = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(1), context)).ToArray(); + var messagesOver4Hours = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context)).ToArray(); + + // Assert + Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages"); + Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id)); + Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id)); + + Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message"); + Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id)); + + Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/IAmAnOutboxProviderSync.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/IAmAnOutboxProviderSync.cs new file mode 100644 index 0000000000..f13cff19c4 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/IAmAnOutboxProviderSync.cs @@ -0,0 +1,67 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +/// +/// Provider for managing outbox storage and creating outbox instances. +/// +public interface IAmAnOutboxProviderSync +{ + /// + /// Creates the outbox data store. + /// + void CreateStore(); + + /// + /// Deletes the outbox data store and removes the specified messages. + /// + /// The messages to remove from the store. + void DeleteStore(IEnumerable messages); + + /// + /// Creates a new outbox instance for storing and retrieving messages. + /// + /// A new instance. + IAmAnOutboxSync CreateOutbox(); + + /// + /// Gets all the messages in the outbox. + /// + /// A list of all messages in the outbox. + IEnumerable GetAllMessages(); + + /// + /// Creates a new transaction provider. + /// + /// A new instance. + IAmABoxTransactionProvider CreateTransactionProvider(); +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs new file mode 100644 index 0000000000..925d4c84b0 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs @@ -0,0 +1,73 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenAddingADuplicateMessageItShouldNotThrow : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingADuplicateMessageItShouldNotThrow() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + [Fact] + public void When_Adding_A_Duplicate_Message_It_Should_Not_Throw() + { + // Arrange + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add(message, context); + + // Assert + // Just adding a simple assertion to remove any warning + Assert.True(true); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs new file mode 100644 index 0000000000..52fd7a5a7c --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs @@ -0,0 +1,105 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenAddingAMessageItShouldBeStoredWithAllProperties : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageItShouldBeStoredWithAllProperties() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + [Fact] + public void When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties() + { + // Arrange + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add(message, context); + + var storedMessage = outbox.Get(message.Id, context); + + // Assert + Assert.Equal(message.Body.Value, storedMessage.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, storedMessage.Header.Topic); + Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox + Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, storedMessage.Header.Bag); + Assert.Equal(val, storedMessage.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId); + Assert.Equal(message.Header.JobId, storedMessage.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, storedMessage.Header.Source); + Assert.Equal(message.Header.Type, storedMessage.Header.Type); + Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema); + Assert.Equal(message.Header.Subject, storedMessage.Header.Subject); + Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent); + Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs new file mode 100644 index 0000000000..b34bf19e08 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs @@ -0,0 +1,79 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStored : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStored() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + [Fact] + public void When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored() + { + // Arrange + var outbox = _outboxProvider.CreateOutbox(); + + var transaction = _outboxProvider.CreateTransactionProvider(); + _ = transaction.GetTransaction(); + + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + outbox.Add(message, context, transactionProvider: transaction); + transaction.Rollback(); + + var storedMessage = outbox.Get(message.Id, context); + + // Assert + Assert.Equal(MessageType.MT_NONE, storedMessage.Header.MessageType); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs new file mode 100644 index 0000000000..037a50065e --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs @@ -0,0 +1,110 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommit : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommit() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + [Fact] + public void When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit() + { + // Arrange + var outbox = _outboxProvider.CreateOutbox(); + + var transaction = _outboxProvider.CreateTransactionProvider(); + _ = transaction.GetTransaction(); + + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message ); + + // Act + outbox.Add(message, context, transactionProvider: transaction); + transaction.Commit(); + + var storedMessage = outbox.Get(message.Id, context); + + // Assert + Assert.Equal(message.Body.Value, storedMessage.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, storedMessage.Header.Topic); + Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox + Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, storedMessage.Header.Bag); + Assert.Equal(val, storedMessage.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId); + Assert.Equal(message.Header.JobId, storedMessage.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, storedMessage.Header.Source); + Assert.Equal(message.Header.Type, storedMessage.Header.Type); + Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema); + Assert.Equal(message.Header.Subject, storedMessage.Header.Subject); + Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent); + Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs new file mode 100644 index 0000000000..b7e5d929d3 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs @@ -0,0 +1,84 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutbox : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutbox() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox() + { + // Arrange + var context = new RequestContext(); + var firstMessage = new DefaultMessageBuilder().Build(); + var secondMessage = new DefaultMessageBuilder().Build(); + var thirdMessage = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(firstMessage); + _createdMessages.Add(secondMessage); + _createdMessages.Add(thirdMessage); + + // Act + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add(firstMessage, context); + outbox.Add(secondMessage, context); + outbox.Add(thirdMessage, context); + + outbox.Delete([firstMessage.Id, secondMessage.Id, thirdMessage.Id], context); + + // Assert + var messages = outbox + .OutstandingMessages(TimeSpan.Zero, context) + .ToArray(); + + Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(secondMessage.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(thirdMessage.Id, messages.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs new file mode 100644 index 0000000000..6022a2ca70 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs @@ -0,0 +1,84 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenDeletingOneMessageItShouldBeRemovedFromOutbox : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenDeletingOneMessageItShouldBeRemovedFromOutbox() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox() + { + // Arrange + var context = new RequestContext(); + var firstMessage = new DefaultMessageBuilder().Build(); + var secondMessage = new DefaultMessageBuilder().Build(); + var thirdMessage = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(firstMessage); + _createdMessages.Add(secondMessage); + _createdMessages.Add(thirdMessage); + + // Act + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add(firstMessage, context); + outbox.Add(secondMessage, context); + outbox.Add(thirdMessage, context); + + outbox.Delete([firstMessage.Id], context); + + // Assert + var messages = outbox + .OutstandingMessages(TimeSpan.Zero, context) + .ToArray(); + + Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id)); + Assert.Contains(secondMessage.Id, messages.Select(x => x.Id)); + Assert.Contains(thirdMessage.Id, messages.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs new file mode 100644 index 0000000000..ef0db87a87 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs @@ -0,0 +1,109 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessage : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessage() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add([earliest, dispatched, undispatched], context); + outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var message = outbox.Get(dispatched.Id, context); + + // Assert + Assert.NotNull(message); + Assert.Equal(message.Body.Value, dispatched.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, dispatched.Header.Topic); + Assert.Equal(message.Header.MessageType, dispatched.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, dispatched.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, dispatched.Header.HandledCount); // -- should be zero when read from outbox + // Assert.Equal(TimeSpan.Zero, dispatched.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, dispatched.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, dispatched.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.MediaType, dispatched.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, dispatched.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, dispatched.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, dispatched.Header.Bag); + Assert.Equal(val.ToString(), dispatched.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, dispatched.Header.WorkflowId); + Assert.Equal(message.Header.JobId, dispatched.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, dispatched.Header.Source); + Assert.Equal(message.Header.Type, dispatched.Header.Type); + Assert.Equal(message.Header.DataSchema, dispatched.Header.DataSchema); + Assert.Equal(message.Header.Subject, dispatched.Header.Subject); + Assert.Equal(message.Header.TraceParent, dispatched.Header.TraceParent); + Assert.Equal(message.Header.TraceState, dispatched.Header.TraceState); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs new file mode 100644 index 0000000000..8c9f3a1177 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs @@ -0,0 +1,70 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingANonExistentMessageItShouldReturnEmptyMessage : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenRetrievingANonExistentMessageItShouldReturnEmptyMessage() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + [Fact] + public void When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message() + { + // Arrange + var context = new RequestContext(); + + var outbox = _outboxProvider.CreateOutbox(); + + // Act + var message = outbox.Get(Id.Random(), context); + + // Assert + Assert.Equal(MessageType.MT_NONE, message.Header.MessageType); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs new file mode 100644 index 0000000000..f9c1a2d22e --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs @@ -0,0 +1,81 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatched : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatched() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add([earliest, dispatched, undispatched], context); + outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var messages = _outboxProvider.GetAllMessages().ToArray(); + + // Assert + Assert.True(messages.Length >= 3, "Expecting at least 3 messages"); + Assert.Contains(earliest.Id, messages.Select(x => x.Id)); + Assert.Contains(dispatched.Id, messages.Select(x => x.Id)); + Assert.Contains(undispatched.Id, messages.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs new file mode 100644 index 0000000000..da785f0881 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs @@ -0,0 +1,92 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingDispatchedMessagesItShouldFilterByAge : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingDispatchedMessagesItShouldFilterByAge() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add([earliest, dispatched, undispatched], context); + outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var allDispatched = outbox.DispatchedMessages(TimeSpan.Zero, context).ToArray(); + var messagesOverAnHour = outbox.DispatchedMessages(TimeSpan.FromHours(1), context).ToArray(); + var messagesOver4Hours = outbox.DispatchedMessages(TimeSpan.FromHours(4), context).ToArray(); + + // Assert + Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages"); + Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id)); + Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id)); + + Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message"); + Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id)); + + Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs new file mode 100644 index 0000000000..0856e67a5f --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs @@ -0,0 +1,83 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessages : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessages() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add([earliest, dispatched, undispatched], context); + outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var messages = outbox + .Get([earliest.Id, undispatched.Id], context) + .ToArray(); + + // Assert + Assert.Equal(2, messages.Length); + Assert.Contains(earliest.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messages.Select(x => x.Id)); + Assert.Contains(undispatched.Id, messages.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs new file mode 100644 index 0000000000..8646a44770 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs @@ -0,0 +1,92 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingOutstandingMessagesItShouldFilterByAge : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingOutstandingMessagesItShouldFilterByAge() + { + _outboxProvider = new OracleBinaryOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add([earliest, dispatched, undispatched], context); + outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var allDispatched = outbox.DispatchedMessages(TimeSpan.Zero, context).ToArray(); + var messagesOverAnHour = outbox.DispatchedMessages(TimeSpan.FromHours(1), context).ToArray(); + var messagesOver4Hours = outbox.DispatchedMessages(TimeSpan.FromHours(4), context).ToArray(); + + // Assert + Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages"); + Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id)); + Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id)); + + Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message"); + Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id)); + + Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/OracleBinaryOutboxProvider.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/OracleBinaryOutboxProvider.cs new file mode 100644 index 0000000000..0ba52d5849 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Binary/OracleBinaryOutboxProvider.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; +using Paramore.Brighter.Oracle.Tests.Outbox.Binary.Async; +using Paramore.Brighter.Oracle.Tests.Outbox.Binary.Sync; +using Paramore.Brighter.Outbox.Oracle; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Binary; + +public class OracleBinaryOutboxProvider : IAmAnOutboxProviderSync, IAmAnOutboxProviderAsync +{ + private readonly RelationalDatabaseConfiguration _configuration = new( + Const.DefaultConnectingString, + outBoxTableName: $"{Const.TablePrefix}{Uuid.New():N}", + binaryMessagePayload: true); + + private readonly OracleTransactionProvider _transactionProvider; + private readonly OracleOutbox _outbox; + + public OracleBinaryOutboxProvider() + { + _transactionProvider = new OracleTransactionProvider(_configuration); + _outbox = new OracleOutbox(_configuration, _transactionProvider); + } + + public IAmAnOutboxSync CreateOutbox() + { + return _outbox; + } + + public IAmAnOutboxAsync CreateOutboxAsync() + { + return _outbox; + } + + public void CreateStore() + { + using var connection = new OracleConnection(_configuration.ConnectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = OracleOutboxBuilder.GetDDL(_configuration.OutBoxTableName, true); + command.ExecuteNonQuery(); + } + + public async Task CreateStoreAsync() + { + await using var connection = new OracleConnection(_configuration.ConnectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = OracleOutboxBuilder.GetDDL(_configuration.OutBoxTableName, true); + await command.ExecuteNonQueryAsync(); + } + + public IAmABoxTransactionProvider CreateTransactionProvider() + { + return _transactionProvider; + } + + public void DeleteStore(IEnumerable messages) + { + using var connection = new OracleConnection(_configuration.ConnectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = $"DROP TABLE {_configuration.OutBoxTableName}"; + command.ExecuteNonQuery(); + } + + public async Task DeleteStoreAsync(IEnumerable messages) + { + await using var connection = new OracleConnection(_configuration.ConnectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = $"DROP TABLE {_configuration.OutBoxTableName}"; + await command.ExecuteNonQueryAsync(); + } + + public IEnumerable GetAllMessages() + { + var outbox = new OracleOutbox(_configuration); + return outbox.Get(new RequestContext()); + } + + public async Task> GetAllMessagesAsync() + { + var outbox = new OracleOutbox(_configuration); + return await outbox.GetAsync(new RequestContext()); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/IAmAnOutboxProviderAsync.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/IAmAnOutboxProviderAsync.cs new file mode 100644 index 0000000000..3a99c6cde3 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/IAmAnOutboxProviderAsync.cs @@ -0,0 +1,68 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +/// +/// Provider for managing outbox storage and creating outbox instances. +/// +public interface IAmAnOutboxProviderAsync +{ + /// + /// Creates the outbox data store. + /// + Task CreateStoreAsync(); + + /// + /// Deletes the outbox data store and removes the specified messages. + /// + /// The messages to remove from the store. + Task DeleteStoreAsync(IEnumerable messages); + + /// + /// Creates a new outbox instance for storing and retrieving messages. + /// + /// A new instance. + IAmAnOutboxAsync CreateOutboxAsync(); + + /// + /// Gets all the messages in the outbox. + /// + /// A list of all messages in the outbox. + Task> GetAllMessagesAsync(); + + /// + /// Creates a new transaction provider. + /// + /// A new instance. + IAmABoxTransactionProvider CreateTransactionProvider(); +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs new file mode 100644 index 0000000000..db0d2f1dc4 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async.cs @@ -0,0 +1,54 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenAddingADuplicateMessageItShouldNotThrowAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingADuplicateMessageItShouldNotThrowAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + _messageBuilder = new DefaultMessageBuilder(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Adding_A_Duplicate_Message_It_Should_Not_Throw_Async() + { + // Arrange + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync(message, context); + + // Assert + // Just adding a simple assertion to remove any warning + Assert.True(true); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs new file mode 100644 index 0000000000..cead1f2337 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async.cs @@ -0,0 +1,87 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenAddingAMessageItShouldBeStoredWithAllPropertiesAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageItShouldBeStoredWithAllPropertiesAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties_Async() + { + // Arrange + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync(message, context); + + var storedMessage = await outbox.GetAsync(message.Id, context); + + // Assert + Assert.Equal(message.Body.Value, storedMessage.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, storedMessage.Header.Topic); + Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox + Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, storedMessage.Header.Bag); + Assert.Equal(val, storedMessage.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId); + Assert.Equal(message.Header.JobId, storedMessage.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, storedMessage.Header.Source); + Assert.Equal(message.Header.Type, storedMessage.Header.Type); + Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema); + Assert.Equal(message.Header.Subject, storedMessage.Header.Subject); + Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent); + Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs new file mode 100644 index 0000000000..283bb3faa4 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async.cs @@ -0,0 +1,61 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStoredAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStoredAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored_Async() + { + // Arrange + var outbox = _outboxProvider.CreateOutboxAsync(); + + var transaction = _outboxProvider.CreateTransactionProvider(); + _ = await transaction.GetTransactionAsync(); + + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + await outbox.AddAsync(message, context, transactionProvider: transaction); + await transaction.RollbackAsync(); + + var storedMessage = await outbox.GetAsync(message.Id, context); + + // Assert + Assert.Equal(MessageType.MT_NONE, storedMessage.Header.MessageType); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs new file mode 100644 index 0000000000..19e11b198f --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async.cs @@ -0,0 +1,92 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommitAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommitAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit_Async() + { + // Arrange + var outbox = _outboxProvider.CreateOutboxAsync(); + + var transaction = _outboxProvider.CreateTransactionProvider(); + _ = await transaction.GetTransactionAsync(); + + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message ); + + // Act + await outbox.AddAsync(message, context, transactionProvider: transaction); + await transaction.CommitAsync(); + + var storedMessage = await outbox.GetAsync(message.Id, context); + + // Assert + Assert.Equal(message.Body.Value, storedMessage.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, storedMessage.Header.Topic); + Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox + Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, storedMessage.Header.Bag); + Assert.Equal(val, storedMessage.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId); + Assert.Equal(message.Header.JobId, storedMessage.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, storedMessage.Header.Source); + Assert.Equal(message.Header.Type, storedMessage.Header.Type); + Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema); + Assert.Equal(message.Header.Subject, storedMessage.Header.Subject); + Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent); + Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs new file mode 100644 index 0000000000..1e8b7413eb --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async.cs @@ -0,0 +1,65 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutboxAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutboxAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox_Async() + { + // Arrange + var context = new RequestContext(); + var firstMessage = new DefaultMessageBuilder().Build(); + var secondMessage = new DefaultMessageBuilder().Build(); + var thirdMessage = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(firstMessage); + _createdMessages.Add(secondMessage); + _createdMessages.Add(thirdMessage); + + // Act + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync(firstMessage, context); + await outbox.AddAsync(secondMessage, context); + await outbox.AddAsync(thirdMessage, context); + + await outbox.DeleteAsync([firstMessage.Id, secondMessage.Id, thirdMessage.Id], context); + + // Assert + var messages = (await outbox + .OutstandingMessagesAsync(TimeSpan.Zero, context)) + .ToArray(); + + Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(secondMessage.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(thirdMessage.Id, messages.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs new file mode 100644 index 0000000000..480fa6d7bc --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async.cs @@ -0,0 +1,91 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenDeletingOneMessageItShouldBeRemovedFromOutboxAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenDeletingOneMessageItShouldBeRemovedFromOutboxAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox_Async() + { + // Arrange + var context = new RequestContext(); + + var firstMessage = new DefaultMessageBuilder().Build(); + var secondMessage = new DefaultMessageBuilder().Build(); + var thirdMessage = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(firstMessage); + _createdMessages.Add(secondMessage); + _createdMessages.Add(thirdMessage); + + // Act + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync(firstMessage, context); + await outbox.AddAsync(secondMessage, context); + await outbox.AddAsync(thirdMessage, context); + + await outbox.DeleteAsync([firstMessage.Id], context); + + // Assert + var messages = (await outbox + .OutstandingMessagesAsync(TimeSpan.Zero, context)) + .ToArray(); + + Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id)); + Assert.Contains(secondMessage.Id, messages.Select(x => x.Id)); + Assert.Contains(thirdMessage.Id, messages.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs new file mode 100644 index 0000000000..563aaaccdc --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async.cs @@ -0,0 +1,115 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessageAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessageAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message_Async() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync([earliest, dispatched, undispatched], context); + await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var message = await outbox.GetAsync(dispatched.Id, context); + + // Assert + Assert.NotNull(message); + Assert.Equal(message.Body.Value, dispatched.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, dispatched.Header.Topic); + Assert.Equal(message.Header.MessageType, dispatched.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, dispatched.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, dispatched.Header.HandledCount); // -- should be zero when read from outbox + // Assert.Equal(TimeSpan.Zero, dispatched.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, dispatched.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, dispatched.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.MediaType, dispatched.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, dispatched.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, dispatched.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, dispatched.Header.Bag); + Assert.Equal(val.ToString(), dispatched.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, dispatched.Header.WorkflowId); + Assert.Equal(message.Header.JobId, dispatched.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, dispatched.Header.Source); + Assert.Equal(message.Header.Type, dispatched.Header.Type); + Assert.Equal(message.Header.DataSchema, dispatched.Header.DataSchema); + Assert.Equal(message.Header.Subject, dispatched.Header.Subject); + Assert.Equal(message.Header.TraceParent, dispatched.Header.TraceParent); + Assert.Equal(message.Header.TraceState, dispatched.Header.TraceState); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs new file mode 100644 index 0000000000..b70032192e --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async.cs @@ -0,0 +1,52 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenRetrievingANonExistentMessageItShouldReturnEmptyMessageAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenRetrievingANonExistentMessageItShouldReturnEmptyMessageAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message_Async() + { + // Arrange + var context = new RequestContext(); + + var outbox = _outboxProvider.CreateOutboxAsync(); + + // Act + var message = await outbox.GetAsync(Id.Random(), context); + + // Assert + Assert.Equal(MessageType.MT_NONE, message.Header.MessageType); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs new file mode 100644 index 0000000000..b893c8a4de --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async.cs @@ -0,0 +1,87 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatchedAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatchedAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched_Async() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync([earliest, dispatched, undispatched], context); + await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var messages = (await _outboxProvider.GetAllMessagesAsync()).ToArray(); + + // Assert + Assert.True(messages.Length >= 3, "Expecting at least 3 messages"); + Assert.Contains(earliest.Id, messages.Select(x => x.Id)); + Assert.Contains(dispatched.Id, messages.Select(x => x.Id)); + Assert.Contains(undispatched.Id, messages.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs new file mode 100644 index 0000000000..1217d34784 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async.cs @@ -0,0 +1,73 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenRetrievingDispatchedMessagesItShouldFilterByAgeAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingDispatchedMessagesItShouldFilterByAgeAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age_Async() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync([earliest, dispatched, undispatched], context); + await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var allDispatched = (await outbox.DispatchedMessagesAsync(TimeSpan.Zero, context)).ToArray(); + var messagesOverAnHour = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(1), context)).ToArray(); + var messagesOver4Hours = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context)).ToArray(); + + // Assert + Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages"); + Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id)); + Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id)); + + Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message"); + Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id)); + + Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs new file mode 100644 index 0000000000..d9ea8f4bda --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async.cs @@ -0,0 +1,89 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessagesAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessagesAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages_Async() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync([earliest, dispatched, undispatched], context); + await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var messages = (await outbox + .GetAsync([earliest.Id, undispatched.Id], context)) + .ToArray(); + + // Assert + Assert.Equal(2, messages.Length); + Assert.Contains(earliest.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messages.Select(x => x.Id)); + Assert.Contains(undispatched.Id, messages.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs new file mode 100644 index 0000000000..4f946a2d7f --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Async/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async.cs @@ -0,0 +1,73 @@ +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; + +[Trait("Category", "Oracle")] +[Collection("OracleTextOutbox")] +public class WhenRetrievingOutstandingMessagesItShouldFilterByAgeAsync : IAsyncLifetime +{ + private readonly IAmAnOutboxProviderAsync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingOutstandingMessagesItShouldFilterByAgeAsync() + { + _outboxProvider = new OracleTextOutboxProvider(); + } + + public async Task InitializeAsync() + { + await _outboxProvider.CreateStoreAsync(); + } + + public async Task DisposeAsync() + { + await _outboxProvider.DeleteStoreAsync(_createdMessages); + } + + [Fact] + public async Task When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age_Async() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutboxAsync(); + await outbox.AddAsync([earliest, dispatched, undispatched], context); + await outbox.MarkDispatchedAsync(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + await outbox.MarkDispatchedAsync(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var allDispatched = (await outbox.DispatchedMessagesAsync(TimeSpan.Zero, context)).ToArray(); + var messagesOverAnHour = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(1), context)).ToArray(); + var messagesOver4Hours = (await outbox.DispatchedMessagesAsync(TimeSpan.FromHours(4), context)).ToArray(); + + // Assert + Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages"); + Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id)); + Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id)); + + Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message"); + Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id)); + + Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id)); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/IAmAnOutboxProviderSync.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/IAmAnOutboxProviderSync.cs new file mode 100644 index 0000000000..6e6596ffc9 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/IAmAnOutboxProviderSync.cs @@ -0,0 +1,67 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +/// +/// Provider for managing outbox storage and creating outbox instances. +/// +public interface IAmAnOutboxProviderSync +{ + /// + /// Creates the outbox data store. + /// + void CreateStore(); + + /// + /// Deletes the outbox data store and removes the specified messages. + /// + /// The messages to remove from the store. + void DeleteStore(IEnumerable messages); + + /// + /// Creates a new outbox instance for storing and retrieving messages. + /// + /// A new instance. + IAmAnOutboxSync CreateOutbox(); + + /// + /// Gets all the messages in the outbox. + /// + /// A list of all messages in the outbox. + IEnumerable GetAllMessages(); + + /// + /// Creates a new transaction provider. + /// + /// A new instance. + IAmABoxTransactionProvider CreateTransactionProvider(); +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs new file mode 100644 index 0000000000..f8f02654f8 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Duplicate_Message_It_Should_Not_Throw.cs @@ -0,0 +1,73 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenAddingADuplicateMessageItShouldNotThrow : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingADuplicateMessageItShouldNotThrow() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + [Fact] + public void When_Adding_A_Duplicate_Message_It_Should_Not_Throw() + { + // Arrange + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add(message, context); + + // Assert + // Just adding a simple assertion to remove any warning + Assert.True(true); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs new file mode 100644 index 0000000000..7ddae5c64c --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties.cs @@ -0,0 +1,105 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenAddingAMessageItShouldBeStoredWithAllProperties : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageItShouldBeStoredWithAllProperties() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + [Fact] + public void When_Adding_A_Message_It_Should_Be_Stored_With_All_Properties() + { + // Arrange + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add(message, context); + + var storedMessage = outbox.Get(message.Id, context); + + // Assert + Assert.Equal(message.Body.Value, storedMessage.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, storedMessage.Header.Topic); + Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox + Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, storedMessage.Header.Bag); + Assert.Equal(val, storedMessage.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId); + Assert.Equal(message.Header.JobId, storedMessage.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, storedMessage.Header.Source); + Assert.Equal(message.Header.Type, storedMessage.Header.Type); + Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema); + Assert.Equal(message.Header.Subject, storedMessage.Header.Subject); + Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent); + Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs new file mode 100644 index 0000000000..bd5a3016bd --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored.cs @@ -0,0 +1,79 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStored : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageWithinTransactionAndRollbackItShouldNotBeStored() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + [Fact] + public void When_Adding_A_Message_Within_Transaction_And_Rollback_It_Should_Not_Be_Stored() + { + // Arrange + var outbox = _outboxProvider.CreateOutbox(); + + var transaction = _outboxProvider.CreateTransactionProvider(); + _ = transaction.GetTransaction(); + + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message); + + // Act + outbox.Add(message, context, transactionProvider: transaction); + transaction.Rollback(); + + var storedMessage = outbox.Get(message.Id, context); + + // Assert + Assert.Equal(MessageType.MT_NONE, storedMessage.Header.MessageType); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs new file mode 100644 index 0000000000..103e71d288 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit.cs @@ -0,0 +1,110 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommit : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenAddingAMessageWithinTransactionItShouldBeStoredAfterCommit() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + [Fact] + public void When_Adding_A_Message_Within_Transaction_It_Should_Be_Stored_After_Commit() + { + // Arrange + var outbox = _outboxProvider.CreateOutbox(); + + var transaction = _outboxProvider.CreateTransactionProvider(); + _ = transaction.GetTransaction(); + + var context = new RequestContext(); + var message = _messageBuilder.Build(); + + _createdMessages.Add(message ); + + // Act + outbox.Add(message, context, transactionProvider: transaction); + transaction.Commit(); + + var storedMessage = outbox.Get(message.Id, context); + + // Assert + Assert.Equal(message.Body.Value, storedMessage.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, storedMessage.Header.Topic); + Assert.Equal(message.Header.MessageType, storedMessage.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, storedMessage.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, storedMessage.Header.HandledCount); // -- should be zero when read from outbox + Assert.Equal(TimeSpan.Zero, storedMessage.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, storedMessage.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, storedMessage.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.ToString(), storedMessage.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, storedMessage.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, storedMessage.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, storedMessage.Header.Bag); + Assert.Equal(val, storedMessage.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, storedMessage.Header.WorkflowId); + Assert.Equal(message.Header.JobId, storedMessage.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, storedMessage.Header.Source); + Assert.Equal(message.Header.Type, storedMessage.Header.Type); + Assert.Equal(message.Header.DataSchema, storedMessage.Header.DataSchema); + Assert.Equal(message.Header.Subject, storedMessage.Header.Subject); + Assert.Equal(message.Header.TraceParent, storedMessage.Header.TraceParent); + Assert.Equal(message.Header.TraceState, storedMessage.Header.TraceState); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs new file mode 100644 index 0000000000..b0414a2eff --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox.cs @@ -0,0 +1,84 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutbox : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenDeletingMultipleMessagesTheyShouldBeRemovedFromOutbox() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Deleting_Multiple_Messages_They_Should_Be_Removed_From_Outbox() + { + // Arrange + var context = new RequestContext(); + var firstMessage = new DefaultMessageBuilder().Build(); + var secondMessage = new DefaultMessageBuilder().Build(); + var thirdMessage = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(firstMessage); + _createdMessages.Add(secondMessage); + _createdMessages.Add(thirdMessage); + + // Act + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add(firstMessage, context); + outbox.Add(secondMessage, context); + outbox.Add(thirdMessage, context); + + outbox.Delete([firstMessage.Id, secondMessage.Id, thirdMessage.Id], context); + + // Assert + var messages = outbox + .OutstandingMessages(TimeSpan.Zero, context) + .ToArray(); + + Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(secondMessage.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(thirdMessage.Id, messages.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs new file mode 100644 index 0000000000..07758291b4 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox.cs @@ -0,0 +1,84 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenDeletingOneMessageItShouldBeRemovedFromOutbox : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenDeletingOneMessageItShouldBeRemovedFromOutbox() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Deleting_One_Message_It_Should_Be_Removed_From_Outbox() + { + // Arrange + var context = new RequestContext(); + var firstMessage = new DefaultMessageBuilder().Build(); + var secondMessage = new DefaultMessageBuilder().Build(); + var thirdMessage = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(firstMessage); + _createdMessages.Add(secondMessage); + _createdMessages.Add(thirdMessage); + + // Act + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add(firstMessage, context); + outbox.Add(secondMessage, context); + outbox.Add(thirdMessage, context); + + outbox.Delete([firstMessage.Id], context); + + // Assert + var messages = outbox + .OutstandingMessages(TimeSpan.Zero, context) + .ToArray(); + + Assert.DoesNotContain(firstMessage.Id, messages.Select(x => x.Id)); + Assert.Contains(secondMessage.Id, messages.Select(x => x.Id)); + Assert.Contains(thirdMessage.Id, messages.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs new file mode 100644 index 0000000000..baec27e975 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message.cs @@ -0,0 +1,109 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessage : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingAMessageByIdItShouldReturnTheCorrectMessage() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Retrieving_A_Message_By_Id_It_Should_Return_The_Correct_Message() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add([earliest, dispatched, undispatched], context); + outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var message = outbox.Get(dispatched.Id, context); + + // Assert + Assert.NotNull(message); + Assert.Equal(message.Body.Value, dispatched.Body.Value); + + //should read the header from the sql outbox + Assert.Equal(message.Header.Topic, dispatched.Header.Topic); + Assert.Equal(message.Header.MessageType, dispatched.Header.MessageType); + Assert.Equal(message.Header.TimeStamp, dispatched.Header.TimeStamp, TimeSpan.FromSeconds(1)); + Assert.Equal(0, dispatched.Header.HandledCount); // -- should be zero when read from outbox + // Assert.Equal(TimeSpan.Zero, dispatched.Header.Delayed); // -- should be zero when read from outbox + Assert.Equal(message.Header.CorrelationId, dispatched.Header.CorrelationId); + Assert.Equal(message.Header.ReplyTo, dispatched.Header.ReplyTo); + Assert.StartsWith(message.Header.ContentType.MediaType, dispatched.Header.ContentType.ToString()); + Assert.Equal(message.Header.PartitionKey, dispatched.Header.PartitionKey); + + //Bag serialization + Assert.Equal(message.Header.Bag.Count, dispatched.Header.Bag.Count); + foreach (var (key, val) in message.Header.Bag) + { + Assert.Contains(key, dispatched.Header.Bag); + Assert.Equal(val.ToString(), dispatched.Header.Bag[key].ToString()); + } + + //Asserts for workflow properties + Assert.Equal(message.Header.WorkflowId, dispatched.Header.WorkflowId); + Assert.Equal(message.Header.JobId, dispatched.Header.JobId); + + // new fields assertions + Assert.Equal(message.Header.Source, dispatched.Header.Source); + Assert.Equal(message.Header.Type, dispatched.Header.Type); + Assert.Equal(message.Header.DataSchema, dispatched.Header.DataSchema); + Assert.Equal(message.Header.Subject, dispatched.Header.Subject); + Assert.Equal(message.Header.TraceParent, dispatched.Header.TraceParent); + Assert.Equal(message.Header.TraceState, dispatched.Header.TraceState); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs new file mode 100644 index 0000000000..6e8a4c3d57 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message.cs @@ -0,0 +1,70 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingANonExistentMessageItShouldReturnEmptyMessage : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private readonly IAmAMessageBuilder _messageBuilder; + private List _createdMessages = []; + + public WhenRetrievingANonExistentMessageItShouldReturnEmptyMessage() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + + _messageBuilder = new DefaultMessageBuilder(); + } + + [Fact] + public void When_Retrieving_A_Non_Existent_Message_It_Should_Return_Empty_Message() + { + // Arrange + var context = new RequestContext(); + + var outbox = _outboxProvider.CreateOutbox(); + + // Act + var message = outbox.Get(Id.Random(), context); + + // Assert + Assert.Equal(MessageType.MT_NONE, message.Header.MessageType); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs new file mode 100644 index 0000000000..ea824132c6 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched.cs @@ -0,0 +1,81 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatched : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingAllMessagesTheyShouldIncludeDispatchedAndUndispatched() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Retrieving_All_Messages_They_Should_Include_Dispatched_And_Undispatched() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add([earliest, dispatched, undispatched], context); + outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var messages = _outboxProvider.GetAllMessages().ToArray(); + + // Assert + Assert.True(messages.Length >= 3, "Expecting at least 3 messages"); + Assert.Contains(earliest.Id, messages.Select(x => x.Id)); + Assert.Contains(dispatched.Id, messages.Select(x => x.Id)); + Assert.Contains(undispatched.Id, messages.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs new file mode 100644 index 0000000000..e22c9e7873 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age.cs @@ -0,0 +1,92 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingDispatchedMessagesItShouldFilterByAge : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingDispatchedMessagesItShouldFilterByAge() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Retrieving_Dispatched_Messages_It_Should_Filter_By_Age() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add([earliest, dispatched, undispatched], context); + outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var allDispatched = outbox.DispatchedMessages(TimeSpan.Zero, context).ToArray(); + var messagesOverAnHour = outbox.DispatchedMessages(TimeSpan.FromHours(1), context).ToArray(); + var messagesOver4Hours = outbox.DispatchedMessages(TimeSpan.FromHours(4), context).ToArray(); + + // Assert + Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages"); + Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id)); + Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id)); + + Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message"); + Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id)); + + Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs new file mode 100644 index 0000000000..124ccacd14 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages.cs @@ -0,0 +1,83 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessages : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingMessagesByIdsItShouldReturnOnlyRequestedMessages() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Retrieving_Messages_By_Ids_It_Should_Return_Only_Requested_Messages() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add([earliest, dispatched, undispatched], context); + outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var messages = outbox + .Get([earliest.Id, undispatched.Id], context) + .ToArray(); + + // Assert + Assert.Equal(2, messages.Length); + Assert.Contains(earliest.Id, messages.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messages.Select(x => x.Id)); + Assert.Contains(undispatched.Id, messages.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs new file mode 100644 index 0000000000..6784528dc2 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/Generated/Sync/When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age.cs @@ -0,0 +1,92 @@ +#region Licence + +/* The MIT License (MIT) +Copyright © 2014 Ian Cooper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion + +// +// This file is auto-generated by Paramore.Brighter.Test.Generator +// + +using Xunit; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; + +[Trait("Category", "Oracle")] +public class WhenRetrievingOutstandingMessagesItShouldFilterByAge : IDisposable +{ + private readonly IAmAnOutboxProviderSync _outboxProvider; + private List _createdMessages = []; + + public WhenRetrievingOutstandingMessagesItShouldFilterByAge() + { + _outboxProvider = new OracleTextOutboxProvider(); + _outboxProvider.CreateStore(); + } + + [Fact] + public void When_Retrieving_Outstanding_Messages_It_Should_Filter_By_Age() + { + // Arrange + var context = new RequestContext(); + var earliest = new DefaultMessageBuilder().Build(); + var dispatched = new DefaultMessageBuilder().Build(); + var undispatched = new DefaultMessageBuilder().Build(); + + _createdMessages.Add(earliest); + _createdMessages.Add(dispatched); + _createdMessages.Add(undispatched); + + var outbox = _outboxProvider.CreateOutbox(); + outbox.Add([earliest, dispatched, undispatched], context); + outbox.MarkDispatched(earliest.Id, context, DateTime.UtcNow.AddHours(-3)); + outbox.MarkDispatched(dispatched.Id, context, DateTime.UtcNow.AddSeconds(-30)); + + // Act + var allDispatched = outbox.DispatchedMessages(TimeSpan.Zero, context).ToArray(); + var messagesOverAnHour = outbox.DispatchedMessages(TimeSpan.FromHours(1), context).ToArray(); + var messagesOver4Hours = outbox.DispatchedMessages(TimeSpan.FromHours(4), context).ToArray(); + + // Assert + Assert.True(allDispatched.Length >= 2, "Expecting at least 2 messages"); + Assert.Contains(earliest.Id, allDispatched.Select(x => x.Id)); + Assert.Contains(dispatched.Id, allDispatched.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, allDispatched.Select(x => x.Id)); + + Assert.True(messagesOverAnHour.Length >= 1, "Expecting at least 1 message"); + Assert.Contains(earliest.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOverAnHour.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOverAnHour.Select(x => x.Id)); + + Assert.DoesNotContain(earliest.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(dispatched.Id, messagesOver4Hours.Select(x => x.Id)); + Assert.DoesNotContain(undispatched.Id, messagesOver4Hours.Select(x => x.Id)); + } + + public void Dispose() + { + _outboxProvider.DeleteStore(_createdMessages); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/OracleTextOutboxProvider.cs b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/OracleTextOutboxProvider.cs new file mode 100644 index 0000000000..0dab362b00 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Outbox/Text/OracleTextOutboxProvider.cs @@ -0,0 +1,80 @@ +using System.Collections.Generic; +using System.Data.Common; +using System.Threading.Tasks; +using Oracle.ManagedDataAccess.Client; +using Paramore.Brighter.Oracle.Tests.Outbox.Text.Async; +using Paramore.Brighter.Oracle.Tests.Outbox.Text.Sync; +using Paramore.Brighter.Outbox.Oracle; + +namespace Paramore.Brighter.Oracle.Tests.Outbox.Text; + +public class OracleTextOutboxProvider : IAmAnOutboxProviderSync, IAmAnOutboxProviderAsync +{ + private readonly RelationalDatabaseConfiguration _configuration = new( + Const.DefaultConnectingString, + outBoxTableName: $"{Const.TablePrefix}{Uuid.New():N}", + binaryMessagePayload: false); + + public IAmAnOutboxSync CreateOutbox() + { + return new OracleOutbox(_configuration); + } + + public IAmAnOutboxAsync CreateOutboxAsync() + { + return new OracleOutbox(_configuration); + } + + public void CreateStore() + { + using var connection = new OracleConnection(_configuration.ConnectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = OracleOutboxBuilder.GetDDL(_configuration.OutBoxTableName); + command.ExecuteNonQuery(); + } + + public async Task CreateStoreAsync() + { + await using var connection = new OracleConnection(_configuration.ConnectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = OracleOutboxBuilder.GetDDL(_configuration.OutBoxTableName); + await command.ExecuteNonQueryAsync(); + } + + public IAmABoxTransactionProvider CreateTransactionProvider() + { + return new OracleTransactionProvider(_configuration); + } + + public void DeleteStore(IEnumerable messages) + { + using var connection = new OracleConnection(_configuration.ConnectionString); + connection.Open(); + using var command = connection.CreateCommand(); + command.CommandText = $"DROP TABLE {_configuration.OutBoxTableName}"; + command.ExecuteNonQuery(); + } + + public async Task DeleteStoreAsync(IEnumerable messages) + { + await using var connection = new OracleConnection(_configuration.ConnectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = $"DROP TABLE {_configuration.OutBoxTableName}"; + await command.ExecuteNonQueryAsync(); + } + + public IEnumerable GetAllMessages() + { + var outbox = new OracleOutbox(_configuration); + return outbox.Get(new RequestContext()); + } + + public async Task> GetAllMessagesAsync() + { + var outbox = new OracleOutbox(_configuration); + return await outbox.GetAsync(new RequestContext()); + } +} diff --git a/tests/Paramore.Brighter.Oracle.Tests/Paramore.Brighter.Oracle.Tests.csproj b/tests/Paramore.Brighter.Oracle.Tests/Paramore.Brighter.Oracle.Tests.csproj new file mode 100644 index 0000000000..faa756bc67 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/Paramore.Brighter.Oracle.Tests.csproj @@ -0,0 +1,37 @@ + + + + $(BrighterTestTargetFrameworks) + false + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + diff --git a/tests/Paramore.Brighter.Oracle.Tests/test-configuration.json b/tests/Paramore.Brighter.Oracle.Tests/test-configuration.json new file mode 100644 index 0000000000..5964a998c6 --- /dev/null +++ b/tests/Paramore.Brighter.Oracle.Tests/test-configuration.json @@ -0,0 +1,17 @@ +{ + "Namespace": "Paramore.Brighter.Oracle.Tests", + "Outboxes": { + "Text": { + "Transaction": "System.Data.Common.DbTransaction", + "OutboxProvider": "OracleTextOutboxProvider", + "Category": "Oracle", + "CollectionName": "OracleTextOutbox" + }, + "Binary": { + "Transaction": "System.Data.Common.DbTransaction", + "OutboxProvider": "OracleBinaryOutboxProvider", + "Category": "Oracle", + "CollectionName": "OracleBinaryOutbox" + } + } +}