T2: CI runs every test against a throwaway mongod
tools/ci/with-test-mongod.sh starts mongod (or podman's mongo:8) on a random localhost port with a temporary data directory, runs the command, and removes both. build.yml and deploy.yml test through it, so the ~85 integration tests stop being skipped in CI. MongoFixture refuses production's port and data directory, and in CI anything but the wrapper's mongod; with PRIVAPUB_TEST_REQUIRE_MONGOD=1 a missing mongod fails instead of skipping. The deploy now passes the PRIVAPUB_SMOKE_TOKEN secret to the Mastodon smoke check, so its signed-in half runs once the owner creates the persona and the secret. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
This commit is contained in:
1 parent
00b2685cf4
commit
85fdff606d
5 files changed
+110
-9
No files matched your search
@@ -15,5 +15,5 @@ jobs:
|
||||
- name: Build
|
||||
run: dotnet build PrivaPub.sln -c Release
|
||||
|
||||
- name: Test
|
||||
run: dotnet test PrivaPub.sln -c Release --no-build
|
||||
- name: Test (unit and integration, on a throwaway mongod)
|
||||
run: tools/ci/with-test-mongod.sh dotnet test PrivaPub.sln -c Release --no-build
|
||||
@@ -28,8 +28,8 @@ jobs:
|
||||
echo "BUILD_REF=${GITHUB_REF_NAME:-master}" >> "$GITHUB_ENV"
|
||||
echo "BUILD_TIME=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Test
|
||||
run: dotnet test PrivaPub.sln -c Release
|
||||
- name: Test (unit and integration, on a throwaway mongod)
|
||||
run: tools/ci/with-test-mongod.sh dotnet test PrivaPub.sln -c Release
|
||||
|
||||
- name: Publish
|
||||
run: |
|
||||
@@ -90,6 +90,8 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Verify what is being served
|
||||
env:
|
||||
SMOKE_TOKEN: ${{ secrets.PRIVAPUB_SMOKE_TOKEN }}
|
||||
run: |
|
||||
served=$(curl -fsS "$PUBLIC_URL/build.json" | python3 -c "import json,sys; print(json.load(sys.stdin).get('commit',''))")
|
||||
[ "$served" = "$BUILD_COMMIT" ] || { echo "::error::served build is '$served', expected '$BUILD_COMMIT'"; exit 1; }
|
||||
@@ -104,5 +106,6 @@ jobs:
|
||||
code=$(curl -s -o /dev/null -w '%{http_code}' -X POST -H 'Content-Type: application/activity+json' \
|
||||
--data '{"type":"Follow","actor":"https://example.org/users/x","object":"'"$PUBLIC_URL"'/peasants/privapub"}' "$PUBLIC_URL/human-centipede")
|
||||
[ "$code" = "401" ] || { echo "::error::an unsigned inbox POST answered $code"; exit 1; }
|
||||
tools/smoke/mastodon-api.sh "$PUBLIC_URL"
|
||||
tools/smoke/mastodon-api.sh "$PUBLIC_URL" "$SMOKE_TOKEN"
|
||||
[ -n "$SMOKE_TOKEN" ] || echo "::warning::PRIVAPUB_SMOKE_TOKEN is not set, so the signed-in API was not checked"
|
||||
echo "::notice::serving $served"
|
||||
@@ -124,6 +124,7 @@ dotnet build PrivaPub.sln -c Release
|
||||
dotnet test PrivaPub.sln # unit tests; integration tests skip
|
||||
PRIVAPUB_TEST_MONGOD=1 dotnet test PrivaPub.sln # all of them, against mongod on 127.0.0.1:27017
|
||||
# (PRIVAPUB_TEST_MONGO overrides; a fresh database per run, dropped after)
|
||||
tools/ci/with-test-mongod.sh dotnet test PrivaPub.sln # all of them, on a throwaway mongod, as CI runs them
|
||||
cd PrivaPub && ASPNETCORE_ENVIRONMENT=Development \
|
||||
Kestrel__Endpoints__Http__Url=http://127.0.0.1:6970 Kestrel__Endpoints__Http__Protocols=Http1AndHttp2 \
|
||||
AppConfiguration__BackendBaseAddress=http://127.0.0.1:6970 MongoSettings__Database=PrivaPubTest \
|
||||
@@ -323,7 +324,11 @@ cd /var/www/privapub.thepra.dev && sudo -u www-data ASPNETCORE_ENVIRONMENT=Produ
|
||||
## Testing
|
||||
|
||||
`PrivaPub.Tests` (xUnit v3). Unit tests need nothing; tests marked `Category=Integration` need a mongod and skip
|
||||
without `PRIVAPUB_TEST_MONGOD=1`. CI runs the unit tests only (the box's mongods are production).
|
||||
without `PRIVAPUB_TEST_MONGOD=1`. CI (`build.yml` and `deploy.yml`) runs all of them through
|
||||
`tools/ci/with-test-mongod.sh`, which starts a throwaway mongod on a random localhost port and deletes it afterwards.
|
||||
The box's own mongods are production, so `MongoFixture` refuses port 27022, a data directory under `/var/lib/privapub`,
|
||||
and in CI anything but the wrapper's mongod. With `PRIVAPUB_TEST_REQUIRE_MONGOD=1`, which the wrapper sets, a missing
|
||||
mongod fails the run instead of silently skipping half the tests.
|
||||
|
||||
- `Support/Peer` is an in-process HTTP server answering on two origins (`127.0.0.1` and `localhost`), so origin rules
|
||||
can be tested; `Support/RemoteActor` signs real deliveries with its own key.
|
||||
|
||||
@@ -13,15 +13,24 @@ namespace PrivaPub.Tests.Support
|
||||
public sealed class MongoFixture : IAsyncLifetime
|
||||
{
|
||||
public const string Skip = "set PRIVAPUB_TEST_MONGOD=1 (and optionally PRIVAPUB_TEST_MONGO) to run the tests that need a mongod";
|
||||
const int ProductionPort = 27022;
|
||||
const int DefaultPort = 27017;
|
||||
|
||||
public static bool Enabled => Environment.GetEnvironmentVariable("PRIVAPUB_TEST_MONGOD") == "1";
|
||||
static bool Required => Environment.GetEnvironmentVariable("PRIVAPUB_TEST_REQUIRE_MONGOD") == "1";
|
||||
static bool InCi => !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) || !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITEA_ACTIONS"));
|
||||
|
||||
public string Database { get; } = $"PrivaPubTests_{Guid.NewGuid():N}";
|
||||
public static string Connection { get; } = Environment.GetEnvironmentVariable("PRIVAPUB_TEST_MONGO") ?? $"mongodb://127.0.0.1:{DefaultPort}";
|
||||
public static string Database { get; } = $"PrivaPubTests_{Guid.NewGuid():N}";
|
||||
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
if (Required && !Enabled)
|
||||
throw new InvalidOperationException("PRIVAPUB_TEST_REQUIRE_MONGOD=1 but PRIVAPUB_TEST_MONGOD is not 1: the integration tests would be skipped");
|
||||
if (!Enabled)
|
||||
return;
|
||||
var settings = MongoClientSettings.FromConnectionString(Connection);
|
||||
Refuse(settings);
|
||||
try
|
||||
{
|
||||
BsonSerializer.RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));
|
||||
@@ -29,8 +38,8 @@ namespace PrivaPub.Tests.Support
|
||||
catch (BsonSerializationException)
|
||||
{
|
||||
}
|
||||
var connection = Environment.GetEnvironmentVariable("PRIVAPUB_TEST_MONGO") ?? "mongodb://127.0.0.1:27017";
|
||||
await DB.InitAsync(Database, MongoClientSettings.FromConnectionString(connection));
|
||||
await DB.InitAsync(Database, settings);
|
||||
await RefuseProductionData();
|
||||
EntityMaps.Warm();
|
||||
await Indexes.Create();
|
||||
}
|
||||
@@ -40,5 +49,32 @@ namespace PrivaPub.Tests.Support
|
||||
if (Enabled)
|
||||
await DB.Default.Database().Client.DropDatabaseAsync(Database);
|
||||
}
|
||||
|
||||
static void Refuse(MongoClientSettings settings)
|
||||
{
|
||||
var ports = settings.Servers.Select(s => s.Port).ToList();
|
||||
if (ports.Contains(ProductionPort))
|
||||
throw new InvalidOperationException($"the tests never run against port {ProductionPort}: that is production's mongod");
|
||||
if (InCi && (Environment.GetEnvironmentVariable("PRIVAPUB_TEST_MONGO") == default || ports.Contains(DefaultPort)))
|
||||
throw new InvalidOperationException("in CI the tests run only against a throwaway mongod: use tools/ci/with-test-mongod.sh");
|
||||
}
|
||||
|
||||
static async Task RefuseProductionData()
|
||||
{
|
||||
BsonDocument options;
|
||||
try
|
||||
{
|
||||
options = await DB.Default.Database().Client.GetDatabase("admin").RunCommandAsync<BsonDocument>(new BsonDocument("getCmdLineOpts", 1));
|
||||
}
|
||||
catch (MongoCommandException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var dbPath = options.GetValue("parsed", new BsonDocument()).AsBsonDocument
|
||||
.GetValue("storage", new BsonDocument()).AsBsonDocument
|
||||
.GetValue("dbPath", BsonNull.Value);
|
||||
if (dbPath.IsString && dbPath.AsString.StartsWith("/var/lib/privapub", StringComparison.Ordinal))
|
||||
throw new InvalidOperationException($"the mongod at {Connection} keeps its data in {dbPath.AsString}: that is production's");
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs a command (normally `dotnet test`) with a throwaway mongod: a fresh data directory on a random localhost port,
|
||||
# stopped and deleted when the command ends. The box's own mongods (27017 shared, 27022 PrivaPub's) are never touched.
|
||||
# usage: tools/ci/with-test-mongod.sh dotnet test PrivaPub.sln -c Release
|
||||
set -euo pipefail
|
||||
|
||||
root="${RUNNER_TEMP:-${TMPDIR:-/tmp}}/privapub-test-mongod"
|
||||
mkdir -p "$root"
|
||||
|
||||
# Leftovers of a cancelled run: anything here older than two hours.
|
||||
find "$root" -mindepth 1 -maxdepth 1 -type d -mmin +120 -print0 2>/dev/null | while IFS= read -r -d '' old; do
|
||||
[ -f "$old/pid" ] && kill "$(cat "$old/pid")" 2>/dev/null || true
|
||||
[ -f "$old/container" ] && podman rm -f "$(cat "$old/container")" >/dev/null 2>&1 || true
|
||||
rm -rf "$old"
|
||||
done
|
||||
|
||||
listening() { (exec 3<>"/dev/tcp/127.0.0.1/$1") 2>/dev/null; }
|
||||
|
||||
port=""
|
||||
for _ in $(seq 1 20); do
|
||||
candidate=$(python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')
|
||||
case "$candidate" in 27017|27022) continue ;; esac
|
||||
listening "$candidate" && continue
|
||||
port=$candidate; break
|
||||
done
|
||||
[ -n "$port" ] || { echo "::error::no free port for the test mongod"; exit 1; }
|
||||
|
||||
dir=$(mktemp -d "$root/run-XXXXXX")
|
||||
cleanup() {
|
||||
status=$?
|
||||
[ -f "$dir/pid" ] && kill "$(cat "$dir/pid")" 2>/dev/null || true
|
||||
[ -f "$dir/container" ] && podman rm -f "$(cat "$dir/container")" >/dev/null 2>&1 || true
|
||||
for _ in $(seq 1 20); do listening "$port" || break; sleep 0.5; done
|
||||
rm -rf "$dir"
|
||||
exit $status
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
if command -v mongod >/dev/null 2>&1; then
|
||||
mkdir -p "$dir/db"
|
||||
timeout 7200 mongod --dbpath "$dir/db" --port "$port" --bind_ip 127.0.0.1 --noauth \
|
||||
--wiredTigerCacheSizeGB 0.25 --quiet --logpath "$dir/mongod.log" &
|
||||
echo $! > "$dir/pid"
|
||||
how="mongod $(mongod --version | head -1)"
|
||||
elif command -v podman >/dev/null 2>&1; then
|
||||
podman run -d --rm -p "127.0.0.1:$port:27017" docker.io/library/mongo:8 --quiet > "$dir/container"
|
||||
how="podman mongo:8"
|
||||
else
|
||||
echo "::error::neither mongod nor podman is available for the test mongod"; exit 1
|
||||
fi
|
||||
|
||||
for _ in $(seq 1 60); do listening "$port" && break; sleep 0.5; done
|
||||
listening "$port" || { echo "::error::the test mongod did not start"; cat "$dir/mongod.log" 2>/dev/null | tail -20; exit 1; }
|
||||
echo "test mongod: $how on 127.0.0.1:$port"
|
||||
|
||||
export PRIVAPUB_TEST_MONGOD=1 PRIVAPUB_TEST_REQUIRE_MONGOD=1 PRIVAPUB_TEST_MONGO="mongodb://127.0.0.1:$port"
|
||||
"$@"
|
||||
Reference in new issue
Block a user