diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 6fff877..254ea7a 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -35,15 +35,36 @@ services: DOTNET_RUNNING_IN_CONTAINER: "true" DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" MASTER_DB_CONNECTION_STRING: "${MASTER_DB_CONNECTION_STRING}" + CLEAR_DATABASE: "true" restart: "no" networks: - devnet + + database.seed: + image: database.seed + container_name: dev-env-database-seed + depends_on: + database.migrations: + condition: service_completed_successfully + build: + context: ./src/Core + dockerfile: Database/Database.Seed/Dockerfile + args: + BUILD_CONFIGURATION: Release + APP_UID: 1000 + environment: + DOTNET_RUNNING_IN_CONTAINER: "true" + DB_CONNECTION_STRING: "${DB_CONNECTION_STRING}" + restart: "no" + networks: + - devnet + api.core: image: api.core container_name: dev-env-api-core depends_on: - sqlserver: - condition: service_healthy + database.seed: + condition: service_completed_successfully build: context: ./src/Core dockerfile: API/API.Core/Dockerfile diff --git a/docker-compose.test.yaml b/docker-compose.test.yaml new file mode 100644 index 0000000..bedcc5f --- /dev/null +++ b/docker-compose.test.yaml @@ -0,0 +1,94 @@ +services: + sqlserver: + image: mcr.microsoft.com/mssql/server:2022-latest + platform: linux/amd64 + container_name: test-env-sqlserver + environment: + ACCEPT_EULA: "Y" + SA_PASSWORD: "${SA_PASSWORD}" + MSSQL_PID: "Express" + DOTNET_RUNNING_IN_CONTAINER: "true" + volumes: + - sqlserverdata-test:/var/opt/mssql + healthcheck: + test: ["CMD-SHELL", "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '${SA_PASSWORD}' -C -Q 'SELECT 1' || exit 1"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 30s + networks: + - testnet + + database.migrations: + image: database.migrations + container_name: test-env-database-migrations + depends_on: + sqlserver: + condition: service_healthy + build: + context: ./src/Core/Database + dockerfile: Database.Migrations/Dockerfile + args: + BUILD_CONFIGURATION: Release + APP_UID: 1000 + environment: + DOTNET_RUNNING_IN_CONTAINER: "true" + DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" + MASTER_DB_CONNECTION_STRING: "${TEST_MASTER_DB_CONNECTION_STRING}" + CLEAR_DATABASE: "true" + restart: "no" + networks: + - testnet + + database.seed: + image: database.seed + container_name: test-env-database-seed + depends_on: + database.migrations: + condition: service_completed_successfully + build: + context: ./src/Core + dockerfile: Database/Database.Seed/Dockerfile + args: + BUILD_CONFIGURATION: Release + APP_UID: 1000 + environment: + DOTNET_RUNNING_IN_CONTAINER: "true" + DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" + restart: "no" + networks: + - testnet + + api.specs: + image: api.specs + container_name: test-env-api-specs + depends_on: + sqlserver: + condition: service_healthy + database.migrations: + condition: service_completed_successfully + database.seed: + condition: service_completed_successfully + build: + context: ./src/Core + dockerfile: API/API.Specs/Dockerfile + args: + BUILD_CONFIGURATION: Release + environment: + ASPNETCORE_ENVIRONMENT: "Test" + DOTNET_RUNNING_IN_CONTAINER: "true" + API_BASE_URL: "http://api.core:8080" + DB_CONNECTION_STRING: "${TEST_DB_CONNECTION_STRING}" + volumes: + - ./test-results:/app/test-results + restart: "no" + networks: + - testnet + +volumes: + sqlserverdata-test: + driver: local + +networks: + testnet: + driver: bridge diff --git a/src/Core/API/API.Specs/Dockerfile b/src/Core/API/API.Specs/Dockerfile new file mode 100644 index 0000000..d253af6 --- /dev/null +++ b/src/Core/API/API.Specs/Dockerfile @@ -0,0 +1,24 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["API/API.Core/API.Core.csproj", "API/API.Core/"] +COPY ["API/API.Specs/API.Specs.csproj", "API/API.Specs/"] +COPY ["Repository/Repository.Core/Repository.Core.csproj", "Repository/Repository.Core/"] +COPY ["Service/Service.Core/Service.Core.csproj", "Service/Service.Core/"] +RUN dotnet restore "API/API.Specs/API.Specs.csproj" +COPY . . +WORKDIR "/src/API/API.Specs" +RUN dotnet build "./API.Specs.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./API.Specs.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS final +WORKDIR /src +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +# Copy the entire source tree for testing +COPY . . +RUN mkdir -p /app/test-results +WORKDIR /src/API/API.Specs +ENTRYPOINT ["dotnet", "test", "API.Specs.csproj", "-c", "Release", "--logger", "trx;LogFileName=/app/test-results/test-results.trx"] diff --git a/src/Core/Database/Database.Migrations/Database.Migrations.csproj b/src/Core/Database/Database.Migrations/Database.Migrations.csproj index 3488889..ab0b5d7 100644 --- a/src/Core/Database/Database.Migrations/Database.Migrations.csproj +++ b/src/Core/Database/Database.Migrations/Database.Migrations.csproj @@ -4,7 +4,7 @@ net10.0 enable enable - DataLayer + Database.Migrations Linux diff --git a/src/Core/Database/Database.Migrations/Program.cs b/src/Core/Database/Database.Migrations/Program.cs index 1fa4b3a..86281cb 100644 --- a/src/Core/Database/Database.Migrations/Program.cs +++ b/src/Core/Database/Database.Migrations/Program.cs @@ -3,7 +3,7 @@ using System.Reflection; using DbUp; using Microsoft.Data.SqlClient; -namespace DataLayer; +namespace Database.Migrations; public static class Program { @@ -22,6 +22,57 @@ public static class Program return result.Successful; } + private static bool ClearDatabase() + { + var myConn = new SqlConnection(masterConnectionString); + + try + { + myConn.Open(); + + // First, set the database to single user mode to close all connections + var setModeCommand = new SqlCommand( + "IF EXISTS (SELECT 1 FROM sys.databases WHERE name = 'Biergarten') " + + "ALTER DATABASE [Biergarten] SET SINGLE_USER WITH ROLLBACK IMMEDIATE;", + myConn); + try + { + setModeCommand.ExecuteNonQuery(); + Console.WriteLine("Database set to single user mode."); + } + catch (System.Exception ex) + { + Console.WriteLine($"Warning: Could not set single user mode: {ex.Message}"); + } + + // Then drop the database + var dropCommand = new SqlCommand("DROP DATABASE IF EXISTS [Biergarten];", myConn); + try + { + dropCommand.ExecuteNonQuery(); + Console.WriteLine("Database cleared successfully."); + } + catch (System.Exception ex) + { + Console.WriteLine($"Error dropping database: {ex}"); + return false; + } + } + catch (System.Exception ex) + { + Console.WriteLine($"Error clearing database: {ex}"); + return false; + } + finally + { + if (myConn.State == ConnectionState.Open) + { + myConn.Close(); + } + } + return true; + } + private static bool CreateDatabaseIfNotExists() { var myConn = new SqlConnection(masterConnectionString); @@ -58,6 +109,13 @@ public static class Program try { + var clearDatabase = Environment.GetEnvironmentVariable("CLEAR_DATABASE"); + if (clearDatabase == "true") + { + Console.WriteLine("CLEAR_DATABASE is enabled. Clearing existing database..."); + ClearDatabase(); + } + CreateDatabaseIfNotExists(); var success = DeployMigrations(); diff --git a/src/Core/Database/Database.Seed/Database.Seed.csproj b/src/Core/Database/Database.Seed/Database.Seed.csproj index 656d3be..4c68909 100644 --- a/src/Core/Database/Database.Seed/Database.Seed.csproj +++ b/src/Core/Database/Database.Seed/Database.Seed.csproj @@ -4,7 +4,7 @@ net10.0 enable enable - DBSeed + Database.Seed @@ -18,7 +18,6 @@ - diff --git a/src/Core/Database/Database.Seed/Dockerfile b/src/Core/Database/Database.Seed/Dockerfile new file mode 100644 index 0000000..1bfaf89 --- /dev/null +++ b/src/Core/Database/Database.Seed/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +# Copy everything from the context (src/Core) +COPY . . +RUN dotnet restore "./Database/Database.Seed/Database.Seed.csproj" +RUN dotnet build "./Database/Database.Seed/Database.Seed.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./Database/Database.Seed/Database.Seed.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/runtime:10.0 AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Database.Seed.dll"] diff --git a/src/Core/Database/Database.Seed/Program.cs b/src/Core/Database/Database.Seed/Program.cs index 720005b..84b60e9 100644 --- a/src/Core/Database/Database.Seed/Program.cs +++ b/src/Core/Database/Database.Seed/Program.cs @@ -5,73 +5,31 @@ using System.Reflection; try { - var connectionString = Environment.GetEnvironmentVariable( - "DB_CONNECTION_STRING" - ); - if (string.IsNullOrWhiteSpace(connectionString)) - throw new InvalidOperationException( - "Environment variable DB_CONNECTION_STRING is not set or is empty." - ); - - await using var connection = new SqlConnection(connectionString); - await connection.OpenAsync(); - - // drop and recreate the database - var useMaster = connection.CreateCommand(); - useMaster.CommandText = "USE master;"; - await useMaster.ExecuteNonQueryAsync(); - - var dbName = "Biergarten"; - var dropDb = connection.CreateCommand(); - dropDb.CommandText = $@" - IF DB_ID(N'{dbName}') IS NOT NULL - BEGIN - ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; - DROP DATABASE [{dbName}]; - END"; - await dropDb.ExecuteNonQueryAsync(); - - var createDb = connection.CreateCommand(); - createDb.CommandText = $@"CREATE DATABASE [{dbName}];"; - await createDb.ExecuteNonQueryAsync(); - await connection.CloseAsync(); - await connection.OpenAsync(); - + var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION_STRING"); Console.WriteLine("Connected to database."); + Console.WriteLine("Starting seeding..."); - Console.WriteLine("Starting migrations..."); - - // Run Database.Core migrations (embedded resources) via DbUp - var migrationAssembly = Assembly.Load("Database.Core"); - var upgrader = DeployChanges - .To.SqlDatabase(connectionString) - .WithScriptsEmbeddedInAssembly(migrationAssembly) - .LogToConsole() - .Build(); - - var upgradeResult = upgrader.PerformUpgrade(); - if (!upgradeResult.Successful) - throw upgradeResult.Error; - - Console.WriteLine("Migrations completed."); - - - ISeeder[] seeders = - [ - new LocationSeeder(), - new UserSeeder(), - ]; - - foreach (var seeder in seeders) + using (var connection = new SqlConnection(connectionString)) { - Console.WriteLine($"Seeding {seeder.GetType().Name}..."); - await seeder.SeedAsync(connection); - Console.WriteLine($"{seeder.GetType().Name} seeded."); + await connection.OpenAsync(); + + ISeeder[] seeders = + [ + new LocationSeeder(), + new UserSeeder(), + ]; + + foreach (var seeder in seeders) + { + Console.WriteLine($"Seeding {seeder.GetType().Name}..."); + await seeder.SeedAsync(connection); + Console.WriteLine($"{seeder.GetType().Name} seeded."); + } + + Console.WriteLine("Seed completed successfully."); } - Console.WriteLine("Seed completed successfully."); - await connection.CloseAsync(); return 0; } catch (Exception ex)