Skip to content

Commit 5939ee8

Browse files
authored
Merge pull request #5616 from Particular/rhys-sql-testing
Add full text search to sql container
2 parents edcc40a + 6583cd1 commit 5939ee8

12 files changed

Lines changed: 385 additions & 16 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ Running all tests all the times takes a lot of resources. Tests are filtered bas
5252
- `MSMQ`
5353
- `RabbitMQ`
5454
- `SqlServer`
55+
- `SqlServerPersistence`
56+
- `PostgresSqlPersistence`
5557
- `SQS`
5658

5759
NOTE: If no variable is defined all tests will be executed.
@@ -71,6 +73,7 @@ Local testing guides:
7173
- [Reverse Proxy Testing](docs/reverseproxy-testing.md)
7274
- [Forward Headers Testing](docs/forward-headers-testing.md)
7375
- [Authentication Testing](docs/authentication-testing.md)
76+
- [Persistence Tests](docs/testing-persistence.md)
7477

7578
## How to developer test the PowerShell Module
7679

docs/testing-persistence.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Local Testing of Persistence Providers
2+
3+
ServiceControl supports multiple persistence types
4+
5+
* RavenDB (default)
6+
* Microsoft SQL Server
7+
* PostgreSQL
8+
9+
All persistence test projects can be run with `dotnet test` against the corresponding test project in `src/`.
10+
11+
## RavenDB
12+
13+
RavenDB persistence tests start an embedded RavenDB instance for the duration of the test run.
14+
15+
## SQL Server
16+
17+
SQL Server persistence tests use [Testcontainers](https://testcontainers.com/) and expect the local image
18+
`particular/servicecontrol-testing-sqlserver:latest`.
19+
20+
Build that image locally before running SQL Server persistence tests:
21+
22+
```shell
23+
docker buildx build --platform=linux/amd64 --tag particular/servicecontrol-testing-sqlserver:latest ./src/Scripts/Docker/servicecontrol-testing-sqlserver
24+
```
25+
26+
If you want to use an existing SQL Server instance instead of a test container, set the `ServiceControl_Persistence_SqlServer_ConnectionString` environment variable to a valid SQL Server connection string.
27+
28+
## PostgreSQL
29+
30+
PostgreSQL persistence tests use [Testcontainers](https://testcontainers.com/) and start a `postgres:16-alpine` container automatically.
31+
32+
If you want to use an existing PostgreSQL instance instead of a test container, set:
33+
34+
```shell
35+
ServiceControl_Persistence_PostgreSql_ConnectionString
36+
```
37+
38+
to a valid PostgreSQL connection string.

docs/testing.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Installation engine tests run partial installations and checks:
2323
## Persistence tests
2424

2525
Persistence tests check assumptions at the persistence seam level by exercising each persister.
26+
For local setup details, see [Local Testing of Persistence Providers](testing-persistence.md).
2627

2728
## Transport tests
2829

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
FROM ubuntu:22.04
2+
ENV DEBIAN_FRONTEND=noninteractive
3+
4+
RUN apt-get update && \
5+
apt-get install -y curl gnupg2 apt-transport-https && \
6+
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | \
7+
gpg --dearmor -o /etc/apt/trusted.gpg.d/microsoft.gpg && \
8+
curl -fsSL https://packages.microsoft.com/config/ubuntu/22.04/mssql-server-2025.list | \
9+
tee /etc/apt/sources.list.d/mssql-server-2025.list && \
10+
curl -fsSL https://packages.microsoft.com/config/ubuntu/22.04/prod.list | \
11+
tee /etc/apt/sources.list.d/mssql-release.list && \
12+
apt-get update && \
13+
ACCEPT_EULA=Y apt-get install -y mssql-server mssql-server-fts && \
14+
ACCEPT_EULA=Y apt-get install -y mssql-tools18 unixodbc-dev && \
15+
# Clean up apt cache
16+
apt-get clean && \
17+
rm -rf /var/lib/apt/lists/*
18+
19+
EXPOSE 1433
20+
21+
ENTRYPOINT [ "/opt/mssql/bin/sqlservr" ]

src/ServiceControl.Persistence.EFCore/Implementation/ExternalIntegrationRequestsDataStore.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@ namespace ServiceControl.Persistence.EFCore.Implementation;
55

66
public class ExternalIntegrationRequestsDataStore : IExternalIntegrationRequestsDataStore, IHostedService
77
{
8-
public void Subscribe(Func<object[], Task> callback) =>
9-
throw new NotImplementedException();
8+
public void Subscribe(Func<object[], Task> callback) { }
109

11-
public Task StoreDispatchRequest(IEnumerable<ExternalIntegrationDispatchRequest> dispatchRequests) =>
12-
throw new NotImplementedException();
10+
public Task StoreDispatchRequest(IEnumerable<ExternalIntegrationDispatchRequest> dispatchRequests) => Task.CompletedTask;
1311

14-
public Task StartAsync(CancellationToken cancellationToken) =>
15-
throw new NotImplementedException();
12+
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
1613

17-
public Task StopAsync(CancellationToken cancellationToken) =>
18-
throw new NotImplementedException();
14+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
1915
}

src/ServiceControl.Persistence.EFCore/Implementation/FailedMessageViewIndexNotifications.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ public class FailedMessageViewIndexNotifications : IFailedMessageViewIndexNotifi
77
public IDisposable Subscribe(Func<FailedMessageTotals, Task> callback) =>
88
throw new NotImplementedException();
99

10-
public Task StartAsync(CancellationToken cancellationToken) =>
11-
throw new NotImplementedException();
10+
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
1211

13-
public Task StopAsync(CancellationToken cancellationToken) =>
14-
throw new NotImplementedException();
12+
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
1513
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
namespace ServiceControl.Persistence.Tests;
2+
3+
using System;
4+
using System.Threading.Tasks;
5+
using EFCore.PostgreSql;
6+
using Npgsql;
7+
using NUnit.Framework;
8+
9+
class FullTextSearchTests : PersistenceTestBase
10+
{
11+
[Test]
12+
public async Task Can_create_and_query_full_text_index()
13+
{
14+
var postgreSqlSettings = PersistenceSettings as PostgreSqlPersisterSettings;
15+
Assert.That(postgreSqlSettings, Is.Not.Null);
16+
17+
var tableName = $"fts_{Guid.NewGuid():N}";
18+
var commandTimeoutSeconds = 30;
19+
20+
await using var connection = new NpgsqlConnection(postgreSqlSettings.ConnectionString);
21+
await connection.OpenAsync();
22+
23+
try
24+
{
25+
await ExecuteNonQuery(connection, $"""
26+
CREATE TABLE public."{tableName}" (
27+
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
28+
body TEXT NOT NULL,
29+
search_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', body)) STORED
30+
)
31+
""", commandTimeoutSeconds);
32+
33+
await ExecuteNonQuery(connection, $"""CREATE INDEX "{tableName}_search_vector_idx" ON public."{tableName}" USING GIN (search_vector)""", commandTimeoutSeconds);
34+
35+
await ExecuteNonQuery(connection, $"""
36+
INSERT INTO public."{tableName}"(body) VALUES
37+
('quick brown fox jumps'),
38+
('azure service bus transport')
39+
""", commandTimeoutSeconds);
40+
41+
var matchCount = await ExecuteScalarInt(connection, $"""
42+
SELECT COUNT(1)
43+
FROM public."{tableName}"
44+
WHERE search_vector @@ plainto_tsquery('english', 'quick')
45+
""", commandTimeoutSeconds);
46+
47+
Assert.That(matchCount, Is.EqualTo(1));
48+
}
49+
finally
50+
{
51+
await ExecuteNonQuery(connection, $"""DROP TABLE IF EXISTS public."{tableName}" CASCADE""", commandTimeoutSeconds, ignoreErrors: true);
52+
}
53+
}
54+
55+
static async Task<int> ExecuteScalarInt(NpgsqlConnection connection, string sql, int commandTimeoutSeconds)
56+
{
57+
await using var command = connection.CreateCommand();
58+
command.CommandTimeout = commandTimeoutSeconds;
59+
command.CommandText = sql;
60+
var scalar = await command.ExecuteScalarAsync();
61+
Assert.That(scalar, Is.Not.Null);
62+
return Convert.ToInt32(scalar);
63+
}
64+
65+
static async Task ExecuteNonQuery(NpgsqlConnection connection, string sql, int commandTimeoutSeconds, bool ignoreErrors = false)
66+
{
67+
try
68+
{
69+
await using var command = connection.CreateCommand();
70+
command.CommandTimeout = commandTimeoutSeconds;
71+
command.CommandText = sql;
72+
await command.ExecuteNonQueryAsync();
73+
}
74+
catch when (ignoreErrors)
75+
{
76+
}
77+
}
78+
}

src/ServiceControl.Persistence.Tests.PostgreSql/PostgreSqlSharedContainer.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace ServiceControl.Persistence.Tests;
77

88
static class PostgreSqlSharedContainer
99
{
10+
const string docsPath = "docs/testing-persistence.md#postgresql";
11+
1012
public static async Task<string> GetConnectionStringAsync(CancellationToken ct = default)
1113
{
1214
var envConnStr = Environment.GetEnvironmentVariable("ServiceControl_Persistence_PostgreSql_ConnectionString");
@@ -38,7 +40,17 @@ static async Task<PostgreSqlContainer> StartContainerAsync(CancellationToken ct)
3840
{
3941
var c = new PostgreSqlBuilder("postgres:16-alpine")
4042
.Build();
41-
await c.StartAsync(ct);
43+
try
44+
{
45+
await c.StartAsync(ct);
46+
}
47+
catch (Exception ex)
48+
{
49+
throw new InvalidOperationException(
50+
$"Failed to start PostgreSQL persistence test container. See {docsPath} for setup instructions.",
51+
ex);
52+
}
53+
4254
return c;
4355
}
4456

src/ServiceControl.Persistence.Tests.PostgreSql/ServiceControl.Persistence.Tests.PostgreSql.csproj

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@
2626

2727
<ItemGroup>
2828
<Compile Include="..\ServiceControl.Persistence.Tests\**\*.cs" LinkBase="Shared" />
29+
30+
<!-- as features get implemented remove these exclusions -->
31+
<Compile Remove="..\ServiceControl.Persistence.Tests\BodyStorage\AttachmentsBodyStorageTests.cs" />
32+
<Compile Remove="..\ServiceControl.Persistence.Tests\Recoverability\EditHandlerAuditTests.cs" />
33+
<Compile Remove="..\ServiceControl.Persistence.Tests\Recoverability\EditMessageTests.cs" />
34+
<Compile Remove="..\ServiceControl.Persistence.Tests\Recoverability\RetryConfirmationProcessorTests.cs" />
35+
<Compile Remove="..\ServiceControl.Persistence.Tests\Recoverability\ReturnToSenderDequeuerTests.cs" />
36+
<Compile Remove="..\ServiceControl.Persistence.Tests\Throughput\AuditServiceMetadataTests.cs" />
37+
<Compile Remove="..\ServiceControl.Persistence.Tests\Throughput\BrokerMetadataTests.cs" />
38+
<Compile Remove="..\ServiceControl.Persistence.Tests\Throughput\EndpointsTests.cs" />
39+
<Compile Remove="..\ServiceControl.Persistence.Tests\Throughput\ReportMasksTests.cs" />
40+
<Compile Remove="..\ServiceControl.Persistence.Tests\CustomChecksDataStoreTests.cs" />
41+
<Compile Remove="..\ServiceControl.Persistence.Tests\CustomCheckTests.cs" />
42+
<Compile Remove="..\ServiceControl.Persistence.Tests\EnsureSettingsInContainer.cs" />
43+
<Compile Remove="..\ServiceControl.Persistence.Tests\MonitoringDataStoreTests.cs" />
44+
<Compile Remove="..\ServiceControl.Persistence.Tests\RetryStateTests.cs" />
2945
</ItemGroup>
3046

3147
</Project>

0 commit comments

Comments
 (0)