diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..c307f2f --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,49 @@ +name: Deploy + +on: + push: + branches: [main] + workflow_dispatch: + +env: + IMAGE_NAME: ${{ secrets.DOCKERHUB_USERNAME }}/socialmedia-webapi + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Extract metadata (tags & labels) + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix=sha-,format=short + + - name: Build and push + uses: docker/build-push-action@v7 + with: + context: . + file: Dockerfile.prod + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index fe28b2f..52af5fe 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -8,42 +8,44 @@ on: workflow_dispatch: jobs: -unit-tests: - name: Unit Tests - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Build test image - uses: docker/build-push-action@v7 - with: - context: . - file: Docker/Dockerfile.test - push: false - load: true - tags: socialmedia-test:${{ github.sha }} - cache-from: type=gha,scope=test-image - cache-to: type=gha,mode=max,scope=test-image - - name: Run unit tests - run: | - mkdir -p ${{ github.workspace }}/test-results/unit - docker run --rm \ - -v ${{ github.workspace }}/test-results/unit:/app/test-results \ - socialmedia-test:${{ github.sha }} \ - --no-build \ - --filter "Category=Unit" \ - --logger "trx;LogFileName=unit-results.trx" \ - --results-directory /app/test-results - - - name: Publish unit test results - uses: dorny/test-reporter@v3 - if: always() - with: - name: Unit Test Results - path: test-results/unit/*.trx - reporter: dotnet-trx + unit-tests: + name: Unit Tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + + - name: Cache NuGet packages + uses: actions/cache@v5 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + - name: Run unit tests + run: | + dotnet test --no-build --configuration Release \ + --filter "Category=Unit" \ + --logger "trx;LogFileName=test-results.trx" \ + --results-directory test-results/unit + + - name: Publish unit test results + uses: dorny/test-reporter@v3 + if: always() + with: + name: Unit Test Results + path: test-results/unit/*.trx + reporter: dotnet-trx diff --git a/.github/workflows/main-ci.yml b/.github/workflows/main-ci.yml index 77ed615..7bb3b2c 100644 --- a/.github/workflows/main-ci.yml +++ b/.github/workflows/main-ci.yml @@ -20,29 +20,31 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x - - name: Build test image - uses: docker/build-push-action@v7 + - name: Cache NuGet packages + uses: actions/cache@v5 with: - context: . - file: Docker/Dockerfile.test - push: false - load: true - tags: socialmedia-test:${{ github.sha }} - cache-from: type=gha,scope=test-image - cache-to: type=gha,mode=max,scope=test-image + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + - name: Run unit tests run: | - mkdir -p ${{ github.workspace }}/test-results/unit - docker run --rm \ - -v ${{ github.workspace }}/test-results/unit:/app/test-results \ - socialmedia-test:${{ github.sha }} \ - --no-build \ + dotnet test --no-build --configuration Release \ --filter "Category=Unit" \ - --logger "trx;LogFileName=unit-results.trx" \ - --results-directory /app/test-results + --logger "trx;LogFileName=test-results.trx" \ + --results-directory test-results/unit - name: Publish unit test results uses: dorny/test-reporter@v3 @@ -72,34 +74,10 @@ jobs: push: false load: true tags: socialmedia-test:${{ github.sha }} - cache-from: type=gha,scope=test-image - cache-to: type=gha,mode=max,scope=test-image - - - name: Cache Testcontainers Docker images - id: cache-docker - uses: actions/cache@v5 - with: - path: /tmp/docker-images - key: docker-images-${{ runner.os }}-mssql-2022-ryuk-0.14.0 - - - name: Load cached images - if: steps.cache-docker.outputs.cache-hit == 'true' - run: | - docker load < /tmp/docker-images/mssql.tar - docker load < /tmp/docker-images/ryuk.tar - - - name: Pull and save images - if: steps.cache-docker.outputs.cache-hit != 'true' - run: | - docker pull mcr.microsoft.com/mssql/server:2022-latest - docker pull testcontainersofficial/ryuk:0.14.0 - mkdir -p /tmp/docker-images - docker save mcr.microsoft.com/mssql/server:2022-latest > /tmp/docker-images/mssql.tar - docker save testcontainersofficial/ryuk:0.14.0 > /tmp/docker-images/ryuk.tar + cache-from: type=gha + cache-to: type=gha,mode=max - name: Run integration tests - env: - TESTCONTAINERS_RYUK_DISABLED: "false" run: | mkdir -p ${{ github.workspace }}/test-results/integration docker run --rm \ diff --git a/Docker/Dockerfile.prod b/Docker/Dockerfile.prod index ed30be8..c6b765f 100644 --- a/Docker/Dockerfile.prod +++ b/Docker/Dockerfile.prod @@ -25,7 +25,6 @@ COPY . . RUN --mount=type=cache,id=nuget-http,target=/root/.local/share/NuGet/http-cache \ --mount=type=cache,id=nuget-global,target=$NUGET_PACKAGES \ dotnet build ./SocialMedia.slnx \ - --no-restore \ -c Release diff --git a/Docker/Dockerfile.test b/Docker/Dockerfile.test index 6fef0a1..fa933e9 100644 --- a/Docker/Dockerfile.test +++ b/Docker/Dockerfile.test @@ -1,7 +1,9 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build + WORKDIR /app ENV NUGET_PACKAGES=/root/.nuget/packages +ENV ASPNETCORE_ENVIRONMENT=Testing COPY Directory.Packages.props . COPY Directory.Build.props . @@ -13,16 +15,13 @@ COPY ./tests/SocialMedia.Application.UnitTests/*.csproj ./tests/SocialMedia.Ap COPY ./tests/SocialMedia.IntegrationTests/*.csproj ./tests/SocialMedia.IntegrationTests/ COPY ./SocialMedia.slnx . -RUN --mount=type=cache,id=nuget-http,target=/root/.local/share/NuGet/http-cache \ - --mount=type=cache,id=nuget-global,target=$NUGET_PACKAGES \ +RUN --mount=type=cache,target=$NUGET_PACKAGES \ dotnet restore ./SocialMedia.slnx COPY . . -RUN --mount=type=cache,id=nuget-http,target=/root/.local/share/NuGet/http-cache \ - --mount=type=cache,id=nuget-global,target=$NUGET_PACKAGES \ +RUN --mount=type=cache,target=$NUGET_PACKAGES \ dotnet build ./SocialMedia.slnx \ - --no-restore \ -c Debug ENTRYPOINT ["dotnet", "test"] diff --git a/src/SocialMedia.WebApi/SocialMedia.WebApi.csproj b/src/SocialMedia.WebApi/SocialMedia.WebApi.csproj index 9307c6f..ed77b38 100644 --- a/src/SocialMedia.WebApi/SocialMedia.WebApi.csproj +++ b/src/SocialMedia.WebApi/SocialMedia.WebApi.csproj @@ -28,8 +28,10 @@ - - + + + + diff --git a/src/SocialMedia.WebApi/appsettings.Testing.json b/src/SocialMedia.WebApi/appsettings.Testing.json new file mode 100644 index 0000000..a4c2594 --- /dev/null +++ b/src/SocialMedia.WebApi/appsettings.Testing.json @@ -0,0 +1,59 @@ +{ + "Serilog": { + "Using": ["Serilog.Sinks.Console", "Serilog.Sinks.Seq"], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "System": "Warning" + } + }, + "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"], + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext} - {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "Seq", + "Args": { + "serverUrl": "http://social-seq:5341" + } + } + ] + }, + "ConnectionStrings": { + "sqlServerConnectionString": "Data Source=;Initial Catalog=SocialMedia;User ID=sa;Password=;Connect Timeout=30;Trust Server Certificate=True;Authentication=SqlPassword;" + }, + "jwt": { + "Issuer": "SocialMedia.WebApi", + "Audience": "SocialMedia.Frontend", + "Lifetime": 60, + "RefreshTokenLifetime": 120, + "SigningKey": "your-secure-signing-key-for-jwt-tokensaklhg" + }, + "email": { + "Smtp": "smtp.gmail.com", + "Port": 587, + "Username": "your-email@gmail.com", + "Password": "your-app-password", + "From": "your-email@gmail.com", + "BaseUrl": "https://localhost:5000", + "EmailVerificationPath": "/api/auth/verify-email?token={token}", + "PasswordResetPath": "/api/auth/reset-password?token={token}" + }, + "supabase": { + "Url": "https://test-supabase-url.supabase.co", + "Key": "test-supabase-key" + }, + "durations": { + "EmailVerificationTokenExpiryMinutes": 30, + "PasswordResetTokenExpiryMinutes": 30 + }, + "FileUpload": { + "Provider": "Server" + } +} diff --git a/tests/SocialMedia.IntegrationTests/Infrastructure/IntegrationTestWebAppFactory.cs b/tests/SocialMedia.IntegrationTests/Infrastructure/IntegrationTestWebAppFactory.cs index 7659e6b..3eb3e96 100644 --- a/tests/SocialMedia.IntegrationTests/Infrastructure/IntegrationTestWebAppFactory.cs +++ b/tests/SocialMedia.IntegrationTests/Infrastructure/IntegrationTestWebAppFactory.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; @@ -29,31 +30,35 @@ public class IntegrationTestWebAppFactory : WebApplicationFactory, IAsy .WithCleanUp(true) .Build(); public HttpClient HttpClient { get; private set; } = null!; - private SqlConnection _dbConnection; + private SqlConnection _dbConnection = null!; private Respawner _respawner = null!; protected override void ConfigureWebHost(IWebHostBuilder builder) { base.ConfigureWebHost(builder); + var testConnectionString = _msSqlContainer.GetConnectionString(); + builder.ConfigureTestServices(services => { services.RemoveAll(typeof(DbContextOptions)); services.AddDbContext(options => { - options.UseSqlServer(_msSqlContainer.GetConnectionString()); + options.UseSqlServer(testConnectionString); }); services.RemoveAll(typeof(IHostedService)); services.RemoveAll(typeof(ISqlConnectionFactory)); - services.AddSingleton(new SqlConnectionFactory(_msSqlContainer.GetConnectionString())); + services.AddSingleton(new SqlConnectionFactory(testConnectionString)); }); } public async ValueTask InitializeAsync() { await _msSqlContainer.StartAsync(); + var testConnectionString = _msSqlContainer.GetConnectionString(); + HttpClient = CreateClient(); - _dbConnection = new SqlConnection(_msSqlContainer.GetConnectionString()); + _dbConnection = new SqlConnection(testConnectionString); await _dbConnection.OpenAsync(); _respawner = await Respawner.CreateAsync(_dbConnection, new RespawnerOptions {