Skip to content

Commit 82808c2

Browse files
authored
#4036 feat(e2e-csharp): add C# end-to-end tests via Npgsql and Testcontainers (#4038)
1 parent 5668d2b commit 82808c2

8 files changed

Lines changed: 378 additions & 10 deletions

File tree

.github/workflows/mvn-test.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,35 @@ jobs:
409409
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
410410
ARCADEDB_DOCKER_IMAGE: ${{ needs.build-and-package.outputs.image-tag }}
411411

412+
csharp-e2e-tests:
413+
runs-on: ubuntu-latest
414+
needs: build-and-package
415+
steps:
416+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
417+
418+
- name: Set up .NET
419+
uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1
420+
with:
421+
dotnet-version: "10.0"
422+
cache: true
423+
cache-dependency-path: "e2e-csharp/ArcadeDB.E2ETests/ArcadeDB.E2ETests.csproj"
424+
425+
- name: Restore Docker image
426+
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
427+
with:
428+
path: /tmp/arcadedb-image.tar
429+
key: docker-image-${{ github.run_id }}-${{ github.run_attempt }}
430+
431+
- name: Load Docker image
432+
run: docker load < /tmp/arcadedb-image.tar
433+
434+
- name: E2E C# Tests
435+
working-directory: e2e-csharp/ArcadeDB.E2ETests
436+
run: dotnet test --logger "trx;LogFileName=test-results.trx"
437+
env:
438+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
439+
ARCADEDB_DOCKER_IMAGE: ${{ needs.build-and-package.outputs.image-tag }}
440+
412441
coverage-report:
413442
runs-on: ubuntu-latest
414443
needs: [ unit-tests, integration-tests, slow-unit-tests ]

e2e-csharp/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
bin/
2+
obj/
3+
*.user
4+
*.suo
5+
.vs/
6+
TestResults/
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!--
2+
Copyright © 2021-present Arcade Data Ltd ([email protected])
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
-->
16+
<Project Sdk="Microsoft.NET.Sdk">
17+
<PropertyGroup>
18+
<TargetFramework>net10.0</TargetFramework>
19+
<Nullable>enable</Nullable>
20+
<ImplicitUsings>enable</ImplicitUsings>
21+
<IsPackable>false</IsPackable>
22+
</PropertyGroup>
23+
24+
<ItemGroup>
25+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.5.1" />
26+
<PackageReference Include="xunit" Version="2.9.3" />
27+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
28+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
29+
<PrivateAssets>all</PrivateAssets>
30+
</PackageReference>
31+
<PackageReference Include="Testcontainers" Version="4.11.0" />
32+
<PackageReference Include="Npgsql" Version="10.0.2" />
33+
</ItemGroup>
34+
</Project>
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.Net;
18+
using System.Net.Http.Headers;
19+
using System.Text;
20+
using DotNet.Testcontainers.Builders;
21+
using DotNet.Testcontainers.Containers;
22+
using Npgsql;
23+
using Xunit;
24+
25+
namespace ArcadeDB.E2ETests;
26+
27+
[CollectionDefinition("ArcadeDB")]
28+
public class ArcadeDbCollection : ICollectionFixture<ArcadeDbFixture> { }
29+
30+
public class ArcadeDbFixture : IAsyncLifetime
31+
{
32+
private const string RootUser = "root";
33+
private const string RootPassword = "playwithdata";
34+
35+
private IContainer _container = null!;
36+
public NpgsqlDataSource DataSource { get; private set; } = null!;
37+
38+
public async Task InitializeAsync()
39+
{
40+
var imageEnv = Environment.GetEnvironmentVariable("ARCADEDB_DOCKER_IMAGE");
41+
var image = string.IsNullOrWhiteSpace(imageEnv) ? "arcadedata/arcadedb:latest" : imageEnv;
42+
43+
_container = new ContainerBuilder(image)
44+
.WithPortBinding(2480, true)
45+
.WithPortBinding(5432, true)
46+
.WithEnvironment("JAVA_OPTS",
47+
$"-Darcadedb.server.rootPassword={RootPassword} " +
48+
"-Darcadedb.server.plugins=PostgresProtocolPlugin")
49+
.WithWaitStrategy(Wait.ForUnixContainer()
50+
.UntilHttpRequestIsSucceeded(r => r
51+
.ForPort(2480)
52+
.ForPath("/api/v1/ready")
53+
.ForStatusCode(HttpStatusCode.NoContent)))
54+
.Build();
55+
56+
await _container.StartAsync();
57+
58+
var httpPort = _container.GetMappedPublicPort(2480);
59+
var authHeader = new AuthenticationHeaderValue(
60+
"Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{RootUser}:{RootPassword}")));
61+
62+
using var http = new HttpClient();
63+
http.DefaultRequestHeaders.Authorization = authHeader;
64+
using var response = await http.PostAsync(
65+
$"http://{_container.Hostname}:{httpPort}/api/v1/server",
66+
new StringContent(
67+
"{\"command\":\"create database NpgsqlE2ETest\"}",
68+
Encoding.UTF8,
69+
"application/json"));
70+
response.EnsureSuccessStatusCode();
71+
72+
using var createTypeResponse = await http.PostAsync(
73+
$"http://{_container.Hostname}:{httpPort}/api/v1/command/NpgsqlE2ETest",
74+
new StringContent(
75+
"{\"language\":\"sql\",\"command\":\"CREATE DOCUMENT TYPE NpgsqlTest\"}",
76+
Encoding.UTF8,
77+
"application/json"));
78+
createTypeResponse.EnsureSuccessStatusCode();
79+
80+
var pgPort = _container.GetMappedPublicPort(5432);
81+
DataSource = NpgsqlDataSource.Create(
82+
$"Host={_container.Hostname};Port={pgPort};Database=NpgsqlE2ETest;" +
83+
$"Username={RootUser};Password={RootPassword};SSL Mode=Disable;" +
84+
"Server Compatibility Mode=NoTypeLoading;No Reset On Close=true");
85+
}
86+
87+
public async Task DisposeAsync()
88+
{
89+
if (DataSource is not null)
90+
await DataSource.DisposeAsync();
91+
if (_container is not null)
92+
await _container.DisposeAsync();
93+
}
94+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright © 2021-present Arcade Data Ltd ([email protected])
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using Npgsql;
18+
using NpgsqlTypes;
19+
using Xunit;
20+
21+
namespace ArcadeDB.E2ETests;
22+
23+
[Collection("ArcadeDB")]
24+
public class PostgresE2ETests
25+
{
26+
private readonly ArcadeDbFixture _fixture;
27+
28+
public PostgresE2ETests(ArcadeDbFixture fixture)
29+
{
30+
_fixture = fixture;
31+
}
32+
33+
[Fact]
34+
public async Task BasicConnection()
35+
{
36+
await using var conn = await _fixture.DataSource.OpenConnectionAsync();
37+
Assert.Equal(System.Data.ConnectionState.Open, conn.State);
38+
}
39+
40+
[Fact]
41+
public async Task SimpleQuery()
42+
{
43+
await using var conn = await _fixture.DataSource.OpenConnectionAsync();
44+
await using var cmd = conn.CreateCommand();
45+
cmd.CommandText = "SELECT FROM schema:types";
46+
await using var reader = await cmd.ExecuteReaderAsync();
47+
Assert.True(reader.HasRows);
48+
}
49+
50+
[Fact]
51+
public async Task CreateTypeAndInsert()
52+
{
53+
await using var conn = await _fixture.DataSource.OpenConnectionAsync();
54+
await using var cmd = conn.CreateCommand();
55+
56+
cmd.CommandText = "INSERT INTO NpgsqlTest SET id = 'ci1', name = 'Alice', value = '100'";
57+
await cmd.ExecuteNonQueryAsync();
58+
cmd.CommandText = "INSERT INTO NpgsqlTest SET id = 'ci2', name = 'Bob', value = '200'";
59+
await cmd.ExecuteNonQueryAsync();
60+
61+
cmd.CommandText = "SELECT FROM NpgsqlTest WHERE id IN ['ci1', 'ci2']";
62+
await using var reader = await cmd.ExecuteReaderAsync();
63+
var count = 0;
64+
while (await reader.ReadAsync()) count++;
65+
Assert.True(count >= 2);
66+
}
67+
68+
[Fact]
69+
public async Task ParameterizedSelect()
70+
{
71+
await using var conn = await _fixture.DataSource.OpenConnectionAsync();
72+
await using var insert = conn.CreateCommand();
73+
insert.CommandText = "INSERT INTO NpgsqlTest SET id = 'ps1', name = 'Alice', value = '100'";
74+
await insert.ExecuteNonQueryAsync();
75+
76+
await using var select = conn.CreateCommand();
77+
select.CommandText = "SELECT FROM NpgsqlTest WHERE id = $1";
78+
select.Parameters.AddWithValue("ps1");
79+
await using var reader = await select.ExecuteReaderAsync();
80+
Assert.True(await reader.ReadAsync());
81+
Assert.Equal("Alice", reader.GetString(reader.GetOrdinal("name")));
82+
}
83+
84+
[Fact]
85+
public async Task MultipleParameters()
86+
{
87+
await using var conn = await _fixture.DataSource.OpenConnectionAsync();
88+
await using var insert = conn.CreateCommand();
89+
insert.CommandText = "INSERT INTO NpgsqlTest SET id = 'mp1', name = 'Alice', value = '100'";
90+
await insert.ExecuteNonQueryAsync();
91+
92+
await using var select = conn.CreateCommand();
93+
select.CommandText = "SELECT FROM NpgsqlTest WHERE name = $1 AND value = $2";
94+
select.Parameters.AddWithValue("Alice");
95+
select.Parameters.AddWithValue("100");
96+
await using var reader = await select.ExecuteReaderAsync();
97+
Assert.True(await reader.ReadAsync());
98+
}
99+
100+
[Fact]
101+
public async Task ParameterizedInsert()
102+
{
103+
await using var conn = await _fixture.DataSource.OpenConnectionAsync();
104+
await using var insert = conn.CreateCommand();
105+
insert.CommandText = "INSERT INTO NpgsqlTest SET id = $1, name = $2, value = $3";
106+
insert.Parameters.AddWithValue("pi1");
107+
insert.Parameters.AddWithValue("Charlie");
108+
insert.Parameters.AddWithValue("300");
109+
110+
try
111+
{
112+
await insert.ExecuteNonQueryAsync();
113+
}
114+
catch (NpgsqlException ex) when (string.IsNullOrEmpty(ex.SqlState))
115+
{
116+
// ArcadeDB sends a RowDescription after INSERT which Npgsql rejects (protocol deviation); SqlState is null for protocol errors so SQL errors propagate normally.
117+
}
118+
119+
await using var select = conn.CreateCommand();
120+
select.CommandText = "SELECT FROM NpgsqlTest WHERE id = $1";
121+
select.Parameters.AddWithValue("pi1");
122+
await using var reader = await select.ExecuteReaderAsync();
123+
Assert.True(await reader.ReadAsync());
124+
Assert.Equal("Charlie", reader.GetString(reader.GetOrdinal("name")));
125+
}
126+
127+
[Fact]
128+
public async Task TextOidParameterBinding()
129+
{
130+
await using var conn = await _fixture.DataSource.OpenConnectionAsync();
131+
await using var insert = conn.CreateCommand();
132+
insert.CommandText = "INSERT INTO NpgsqlTest SET id = 'tob1', name = 'OidTextUser', value = '77'";
133+
await insert.ExecuteNonQueryAsync();
134+
135+
await using var select = conn.CreateCommand();
136+
select.CommandText = "SELECT FROM NpgsqlTest WHERE name = $1";
137+
// NpgsqlDbType.Text forces OID 25 in the bind message; PG JDBC sends varchar (OID 1043) instead
138+
select.Parameters.Add(new NpgsqlParameter { Value = "OidTextUser", NpgsqlDbType = NpgsqlDbType.Text });
139+
await using var reader = await select.ExecuteReaderAsync();
140+
Assert.True(await reader.ReadAsync());
141+
Assert.Equal("OidTextUser", reader.GetString(reader.GetOrdinal("name")));
142+
}
143+
144+
[Fact]
145+
public async Task Transaction()
146+
{
147+
await using var conn = await _fixture.DataSource.OpenConnectionAsync();
148+
149+
// ArcadeDB only accepts bare BEGIN; Npgsql's BeginTransactionAsync sends
150+
// BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED which ArcadeDB rejects.
151+
await using var begin = conn.CreateCommand();
152+
begin.CommandText = "BEGIN";
153+
await begin.ExecuteNonQueryAsync();
154+
155+
await using var insert = conn.CreateCommand();
156+
insert.CommandText = "INSERT INTO NpgsqlTest SET id = 'tx1', name = 'TxTest', value = '999'";
157+
await insert.ExecuteNonQueryAsync();
158+
159+
await using var commit = conn.CreateCommand();
160+
commit.CommandText = "COMMIT";
161+
await commit.ExecuteNonQueryAsync();
162+
163+
await using var select = conn.CreateCommand();
164+
select.CommandText = "SELECT FROM NpgsqlTest WHERE id = $1";
165+
select.Parameters.AddWithValue("tx1");
166+
await using var reader = await select.ExecuteReaderAsync();
167+
Assert.True(await reader.ReadAsync());
168+
Assert.Equal("TxTest", reader.GetString(reader.GetOrdinal("name")));
169+
}
170+
}

e2e-csharp/run-tests.sh

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
cd "$(dirname "$0")/ArcadeDB.E2ETests"
5+
6+
if ! command -v dotnet >/dev/null 2>&1; then
7+
echo "error: dotnet not found. Install .NET 10 SDK from https://dot.net" >&2
8+
exit 1
9+
fi
10+
11+
if ! command -v docker >/dev/null 2>&1; then
12+
echo "error: docker not found. Install Docker Desktop or Docker Engine." >&2
13+
exit 1
14+
fi
15+
16+
exec dotnet test --logger "console;verbosity=normal" "$@"

0 commit comments

Comments
 (0)