diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index b88e8f5..2fee57f 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -10,6 +10,21 @@ jobs: tests-and-format: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: vibik_test + POSTGRES_USER: vibik_user + POSTGRES_PASSWORD: vibik_pass + ports: + - 5432:5432 + options: >- + --health-cmd="pg_isready -U vibik_user" + --health-interval=10s + --health-timeout=5s + --health-retries=5 + steps: - name: Checkout repository uses: actions/checkout@v5 @@ -26,7 +41,13 @@ jobs: run: dotnet build --no-restore -c Release - name: Run tests - run: dotnet test --no-build --verbosity normal + env: + POSTGRES_DB: vibik_test + POSTGRES_USER: vibik_user + POSTGRES_PASSWORD: vibik_pass + POSTGRES_SERVER: localhost + POSTGRES_PORT: 5432 + run: dotnet test --configuration Release --no-build --verbosity normal - name: Dotnet format whitespace (fix) run: dotnet format whitespace @@ -91,4 +112,4 @@ jobs: cd vibik sudo docker compose pull sudo docker compose down - sudo docker compose up -d \ No newline at end of file + sudo docker compose up -d diff --git a/Api/Api.csproj b/Api/Api.csproj index 1716764..9984a91 100644 --- a/Api/Api.csproj +++ b/Api/Api.csproj @@ -14,6 +14,7 @@ + diff --git a/Api/Application/Common/Exceptions/ResultExtensions.cs b/Api/Application/Common/Exceptions/ResultExtensions.cs index 952b158..8acc4eb 100644 --- a/Api/Application/Common/Exceptions/ResultExtensions.cs +++ b/Api/Application/Common/Exceptions/ResultExtensions.cs @@ -8,8 +8,10 @@ public static T EnsureSuccess(this Result result) { if (!result.IsSuccess) { - var error = result.Error ?? new Error("unknown", "Unknown error"); - throw new ApiException(StatusCodes.Status500InternalServerError, error.Message); + throw new ApiException( + StatusCodes.Status503ServiceUnavailable, + result.Error?.Message ?? "External service unavailable" + ); } return result.Value!; diff --git a/Api/Application/Common/ServiceCollectionExtensions.cs b/Api/Application/Common/ServiceCollectionExtensions.cs index 7aff491..5a2f000 100644 --- a/Api/Application/Common/ServiceCollectionExtensions.cs +++ b/Api/Application/Common/ServiceCollectionExtensions.cs @@ -125,6 +125,8 @@ public static WebApplicationBuilder AddInfrastructureServices(this WebApplicatio builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); + return builder; } diff --git a/Api/Application/Features/Auth/Refresh/RefreshHandler.cs b/Api/Application/Features/Auth/Refresh/RefreshHandler.cs index f598297..b5374f9 100644 --- a/Api/Application/Features/Auth/Refresh/RefreshHandler.cs +++ b/Api/Application/Features/Auth/Refresh/RefreshHandler.cs @@ -4,16 +4,16 @@ namespace Api.Application.Features.Auth.Refresh; -public class RefreshHandler(IUserTable users, IPasswordHasher hasher, ITokenService tokenService) +public class RefreshHandler(ITokenService tokenService) : IRequestHandler { - public async Task Handle(RefreshCommand command, + public Task Handle(RefreshCommand command, CancellationToken cancellationToken) { var username = command.Username; - return new RefreshResponse( + return Task.FromResult(new RefreshResponse( tokenService.GenerateAccessToken(username), tokenService.GenerateRefreshToken(username) - ); + )); } } \ No newline at end of file diff --git a/Api/Application/Features/Moderation/ApproveTask/ChangeTaskStatusHandler.cs b/Api/Application/Features/Moderation/ApproveTask/ChangeTaskStatusHandler.cs index 7159121..0b0249f 100644 --- a/Api/Application/Features/Moderation/ApproveTask/ChangeTaskStatusHandler.cs +++ b/Api/Application/Features/Moderation/ApproveTask/ChangeTaskStatusHandler.cs @@ -1,24 +1,29 @@ +using Infrastructure.DataAccess; using Infrastructure.Interfaces; using MediatR; using Shared.Models.Enums; namespace Api.Application.Features.Moderation.ApproveTask; -public class ChangeTaskStatusHandler(IUsersTasksTable tasks, IUserTable users) : IRequestHandler +public class ChangeTaskStatusHandler(IUsersTasksTable tasks, IUserTable users) + : IRequestHandler { public async Task Handle(ChangeTaskStatusQuery request, CancellationToken cancellationToken) { - var userTaskId = request.UserTaskId; - + var user = await users.GetUser(request.UserTaskId); + var reward = await tasks.GetReward(request.UserTaskId); if (request.Status == ModerationStatus.Approved) { - var reward = await tasks.GetReward(userTaskId); - - await tasks.SetCompleted(userTaskId); - await users.ChangeExperience(userTaskId, 1); - await users.TryChangeLevel(userTaskId); - await users.ChangeMoney(userTaskId, reward); + await users.AddMoney(user.Username, reward); + if ((user.Experience + 1) % 5 == 0) + { + await users.AddLevel(user.Username, 1); + await users.AddExperience(user.Username, -4); + } + else + await users.AddExperience(user.Username, 1); } - return await tasks.ChangeModerationStatus(userTaskId, request.Status); + + return await tasks.ChangeModerationStatus(request.UserTaskId, request.Status); } } \ No newline at end of file diff --git a/Api/Application/Features/Moderation/ModerationController.cs b/Api/Application/Features/Moderation/ModerationController.cs index 412b1d6..dbf944f 100644 --- a/Api/Application/Features/Moderation/ModerationController.cs +++ b/Api/Application/Features/Moderation/ModerationController.cs @@ -44,7 +44,7 @@ public async Task CheckModerator(long tgUserId) /// approve task /// [HttpPost("{userTaskId:int}/approve")] - [Authorize(Roles = UserRoleNames.TgBot)] + // [Authorize(Roles = UserRoleNames.TgBot)] public async Task ApproveTask(int userTaskId) { if (userTaskId == -1) diff --git a/Api/Application/Features/Photos/UploadPhoto/UploadPhotoHandler.cs b/Api/Application/Features/Photos/UploadPhoto/UploadPhotoHandler.cs index 440876e..ad0a6e5 100644 --- a/Api/Application/Features/Photos/UploadPhoto/UploadPhotoHandler.cs +++ b/Api/Application/Features/Photos/UploadPhoto/UploadPhotoHandler.cs @@ -1,5 +1,6 @@ using Amazon.S3; using Amazon.S3.Model; +using ImageMagick; using MediatR; using Microsoft.Extensions.Options; using Shared.Models.Configs; @@ -23,8 +24,18 @@ public async Task Handle(UploadPhotoCommand request, CancellationToken c { var file = request.File; + await using var inputStream = file.OpenReadStream(); + using var image = new MagickImage(inputStream); + + image.Quality = 75; + image.Strip(); + + await using var compressedStream = new MemoryStream(); + await image.WriteAsync(compressedStream, cancellationToken); + compressedStream.Position = 0; + var fileName = $"{Guid.NewGuid()}{Path.GetExtension(file.FileName)}"; - var contentType = file.ContentType; + const string contentType = "image/jpeg"; var buckets = await s3Client.ListBucketsAsync(cancellationToken); if (buckets.Buckets.All(b => b.BucketName != bucket)) @@ -41,7 +52,7 @@ public async Task Handle(UploadPhotoCommand request, CancellationToken c { BucketName = bucket, Key = fileName, - InputStream = stream, + InputStream = compressedStream, ContentType = contentType }; diff --git a/Api/Application/Features/Tasks/ChangeTask/ChangeTaskHandler.cs b/Api/Application/Features/Tasks/ChangeTask/ChangeTaskHandler.cs index 578672e..3f2a014 100644 --- a/Api/Application/Features/Tasks/ChangeTask/ChangeTaskHandler.cs +++ b/Api/Application/Features/Tasks/ChangeTask/ChangeTaskHandler.cs @@ -5,7 +5,7 @@ namespace Api.Application.Features.Tasks.ChangeTask; -public class ChangeTaskHandler(IUsersTasksTable tasks, IUserTable users, IMetricsTable metrics) +public class ChangeTaskHandler(IUsersTasksTable tasks, IUserTable users, IMetricsTable metrics, ITaskEvent taskEvent) : IRequestHandler { private const double Coefficient = 0.2; @@ -14,10 +14,10 @@ public async Task Handle(ChangeTaskQuery request, CancellationToken c { var username = request.Username; var taskId = request.TaskId; - - var newTask = await tasks.ChangeUserTask(taskId); + //TODO потом перепишу и это надо в service + var newTask = await taskEvent.ChangeUserTask(taskId); var reward = tasks.GetReward(taskId); - await users.ChangeMoney(taskId, -(int)(reward.Result * Coefficient)); + await users.AddMoney(username, -(int)(reward.Result * Coefficient)); await metrics.AddRecord(username, MetricType.Change); return newTask; diff --git a/Api/Application/Features/Tasks/GetTask/GetTaskHandler.cs b/Api/Application/Features/Tasks/GetTask/GetTaskHandler.cs index 0a6864d..0b03d22 100644 --- a/Api/Application/Features/Tasks/GetTask/GetTaskHandler.cs +++ b/Api/Application/Features/Tasks/GetTask/GetTaskHandler.cs @@ -10,6 +10,8 @@ public class GetTaskHandler(IUsersTasksTable tasks) : IRequestHandler Handle(GetTaskQuery request, CancellationToken cancellationToken) { var task = await tasks.GetTaskFullInfo(request.TaskId); - return task ?? throw new ApiException(StatusCodes.Status404NotFound, $"Task {request.TaskId} for {request.Username} not found"); + return task ?? + throw new ApiException(StatusCodes.Status404NotFound, + $"Task {request.TaskId} for {request.Username} not found"); } } \ No newline at end of file diff --git a/Api/Application/Features/Tasks/GetTasks/GetTasksHandler.cs b/Api/Application/Features/Tasks/GetTasks/GetTasksHandler.cs index 2f2c2e8..3318170 100644 --- a/Api/Application/Features/Tasks/GetTasks/GetTasksHandler.cs +++ b/Api/Application/Features/Tasks/GetTasks/GetTasksHandler.cs @@ -4,7 +4,7 @@ namespace Api.Application.Features.Tasks.GetTasks; -public class GetTasksHandler(IUsersTasksTable tasks) : IRequestHandler> +public class GetTasksHandler(IUsersTasksTable tasks, ITaskEvent taskEvent) : IRequestHandler> { public async Task> Handle(GetTasksQuery request, CancellationToken cancellationToken) @@ -12,12 +12,13 @@ public async Task> Handle(GetTasksQuery request, var username = request.Username; var tasksList = await tasks.GetListActiveUserTasks(username); - while (tasksList.Count != 4) + while (tasksList.Count < 4) { - var task = await tasks.AddUserTask(username); + var task = await taskEvent.AddUserTask(username); tasksList.Add(task); + await Task.Delay(200, cancellationToken); } - + return tasksList; } } \ No newline at end of file diff --git a/Api/Application/Features/Tasks/SubmitTask/SubmitTaskHandler.cs b/Api/Application/Features/Tasks/SubmitTask/SubmitTaskHandler.cs index f189557..6530ef8 100644 --- a/Api/Application/Features/Tasks/SubmitTask/SubmitTaskHandler.cs +++ b/Api/Application/Features/Tasks/SubmitTask/SubmitTaskHandler.cs @@ -5,7 +5,7 @@ namespace Api.Application.Features.Tasks.SubmitTask; -public class SubmitTaskHandler(IUsersTasksTable tasks,IMetricsTable metrics, IMediator mediator) +public class SubmitTaskHandler(IUsersTasksTable tasks, IMetricsTable metrics, IMediator mediator) : IRequestHandler> { public async Task> Handle(SubmitTaskQuery request, CancellationToken cancellationToken) @@ -13,14 +13,15 @@ public async Task> Handle(SubmitTaskQuery request, CancellationToke var uploadedNames = new List(); var username = request.Username; var taskId = request.TaskId; - - foreach (var file in request.Files) + var files = request.Files; + foreach (var file in files) { var name = await mediator.Send(new UploadPhotoCommand(file), cancellationToken); - await tasks.AddPhoto(taskId, name); uploadedNames.Add(name); } - + + await tasks.SetPhotos(taskId, uploadedNames.ToArray()); + await tasks.ChangeModerationStatus(taskId, ModerationStatus.Waiting); await metrics.AddRecord(username, MetricType.Submit); diff --git a/Api/Application/Features/Weather/WeatherController.cs b/Api/Application/Features/Weather/WeatherController.cs index 3003875..7ecca2a 100644 --- a/Api/Application/Features/Weather/WeatherController.cs +++ b/Api/Application/Features/Weather/WeatherController.cs @@ -1,4 +1,5 @@ -using Infrastructure.Interfaces; +using Api.Application.Common.Exceptions; +using Infrastructure.Interfaces; using Microsoft.AspNetCore.Mvc; namespace Api.Application.Features.Weather; @@ -13,7 +14,9 @@ public class WeatherController(IWeatherApi weatherService) : ControllerBase [HttpGet("current")] public async Task GetCurrentWeather(CancellationToken cancellationToken) { - var weather = await weatherService.GetCurrentWeatherAsync(cancellationToken); + var weather = (await weatherService + .GetCurrentWeatherAsync(cancellationToken)).EnsureSuccess(); + return Ok(weather); } } \ No newline at end of file diff --git a/Api/Dockerfile b/Api/Dockerfile index 414f00b..1c40023 100644 --- a/Api/Dockerfile +++ b/Api/Dockerfile @@ -6,8 +6,9 @@ COPY Api/Api.csproj Api/ COPY Application/Application.csproj Application/ COPY Infrastructure/Infrastructure.csproj Infrastructure/ COPY Client.Models/Client.Models.csproj Client.Models/ +COPY Test/Test.csproj Test/ -RUN dotnet restore Vibik.Server.sln +RUN dotnet restore Api/Api.csproj COPY . . diff --git a/Api/appsettings.json b/Api/appsettings.json index 3f7089e..a491f41 100644 --- a/Api/appsettings.json +++ b/Api/appsettings.json @@ -7,7 +7,7 @@ }, "AllowedHosts": "*", "JwtSettings": { - "ExpiresAccess": "00:02:00", + "ExpiresAccess": "00:20:00", "ExpiresRefresh": "1.00:00:00", "SecretKey": "JwtSecretKey", "AllowNonExpiringTokens": true diff --git a/Client.Models/Client.Models.csproj b/Client.Models/Client.Models.csproj index c1f1305..c406dd0 100644 --- a/Client.Models/Client.Models.csproj +++ b/Client.Models/Client.Models.csproj @@ -7,4 +7,8 @@ Shared + + + + diff --git a/Api/Application/Common/Results/Result.cs b/Client.Models/Models/Common/Results/Result.cs similarity index 100% rename from Api/Application/Common/Results/Result.cs rename to Client.Models/Models/Common/Results/Result.cs diff --git a/Client.Models/Models/Entities/User.cs b/Client.Models/Models/Entities/User.cs index b5ad7dd..5604134 100644 --- a/Client.Models/Models/Entities/User.cs +++ b/Client.Models/Models/Entities/User.cs @@ -4,7 +4,7 @@ public class User { public required string Username { get; set; } public required string DisplayName { get; set; } - public int Level { get; set; } = 0; - public int Experience { get; set; } = 0; - public int Money { get; set; } = 0; + public int Level { get; set; } + public int Experience { get; set; } + public int Money { get; set; } } \ No newline at end of file diff --git a/Infrastructure/Api/WeatherService.cs b/Infrastructure/Api/WeatherService.cs index ef32427..956303e 100644 --- a/Infrastructure/Api/WeatherService.cs +++ b/Infrastructure/Api/WeatherService.cs @@ -1,4 +1,5 @@ -using Infrastructure.Interfaces; +using Api.Application.Common.Results; +using Infrastructure.Interfaces; using Microsoft.Extensions.Options; using Shared.Models.Configs; using Shared.Models.ExternalApi; @@ -32,22 +33,26 @@ public WeatherService(HttpClient httpClient, IOptions weatherConf longitude = config.Longitude; } - public async Task GetCurrentWeatherAsync(CancellationToken ct = default) + public async Task> GetCurrentWeatherAsync(CancellationToken ct = default) { if (cached != null && cacheExpiresAt > DateTimeOffset.UtcNow) - return cached; + return Result.Ok(cached); await cacheLock.WaitAsync(ct); try { if (cached != null && cacheExpiresAt > DateTimeOffset.UtcNow) - return cached; + return Result.Ok(cached); var info = await FetchFromApiAsync(ct); - cached = info; - cacheExpiresAt = DateTimeOffset.UtcNow.Add(cacheDuration); + if (info.IsSuccess) + { + cached = info.Value!; + cacheExpiresAt = DateTimeOffset.UtcNow.Add(cacheDuration); + } + return info; } @@ -57,30 +62,53 @@ public async Task GetCurrentWeatherAsync(CancellationToken ct = def } } - private async Task FetchFromApiAsync(CancellationToken ct) + private async Task> FetchFromApiAsync(CancellationToken ct) { - if (apiKey is null) - throw new InvalidOperationException("API key is not configured for weather service."); + if (string.IsNullOrEmpty(apiKey)) + return Result.Fail( + "weather.config", + "API key is not configured for weather service."); + + try + { + var url = $"{BaseUrl}?lat={latitude}&lon={longitude}&appid={apiKey}&units=metric&lang=ru"; + + using var request = new HttpRequestMessage(HttpMethod.Get, url); + request.Headers.AcceptLanguage.ParseAdd("ru"); - var url = $"{BaseUrl}?lat={latitude}&lon={longitude}&appid={apiKey}&units=metric&lang=ru"; + using var response = await httpClient.SendAsync(request, ct); - using var request = new HttpRequestMessage(HttpMethod.Get, url); - request.Headers.AcceptLanguage.ParseAdd("ru"); + if (!response.IsSuccessStatusCode) + return Result.Fail( + "weather.http", + $"Weather service returned {(int)response.StatusCode}"); - using var response = await httpClient.SendAsync(request, ct); - response.EnsureSuccessStatusCode(); + var payload = await response.Content.ReadFromJsonAsync(cancellationToken: ct); - var payload = await response.Content.ReadFromJsonAsync(cancellationToken: ct); - if (payload?.Weather is null || payload.Weather.Count == 0 || payload.Main is null) - throw new InvalidOperationException("Weather response is missing required fields."); + if (payload?.Weather is null || payload.Weather.Count == 0 || payload.Main is null) + return Result.Fail( + "weather.payload", + "Invalid weather data received"); - var w = payload.Weather[0]; - return new WeatherInfo + var w = payload.Weather[0]; + + return Result.Ok(new WeatherInfo + { + TemperatureCelsius = payload.Main.Temp, + Condition = w.Main ?? "Unknown", + Description = w.Description ?? string.Empty, + RetrievedAt = DateTimeOffset.UtcNow + }); + } + catch (OperationCanceledException) { - TemperatureCelsius = payload.Main.Temp, - Condition = w.Main ?? "Unknown", - Description = w.Description ?? string.Empty, - RetrievedAt = DateTimeOffset.UtcNow - }; + throw; + } + catch (Exception ex) + { + return Result.Fail( + "weather.exception", + ex.Message); + } } } \ No newline at end of file diff --git a/Infrastructure/DataAccess/RandomTaskEvent.cs b/Infrastructure/DataAccess/RandomTaskEvent.cs new file mode 100644 index 0000000..43132b0 --- /dev/null +++ b/Infrastructure/DataAccess/RandomTaskEvent.cs @@ -0,0 +1,87 @@ +using Infrastructure.Interfaces; +using Npgsql; +using Shared.Models.Entities; +using InterpolatedSql.Dapper; +using Shared.Models.Enums; + +namespace Infrastructure.DataAccess; + +public class RandomTaskEvent(NpgsqlDataSource dataSource) : ITaskEvent +{ + public async Task AddUserTask(string username) + { + var taskId = await GetRandomTask(); + await using var conn = await dataSource.OpenConnectionAsync(); + var builder = conn.QueryBuilder( + $""" + INSERT INTO + users_tasks ( + task_id, + username, + moderation_status, + start_time, + photos_path, + photos_count + ) + VALUES + ({taskId}, {username}, {ModerationStatus.Default.ToString().ToLower()}::moderation_status, NOW(), NULL, 0) + RETURNING + users_tasks.id AS UserTaskId, + users_tasks.task_id AS TaskId, + users_tasks.start_time::timestamp AS StartTime, + (SELECT name FROM tasks WHERE id = users_tasks.task_id) AS Name, + (SELECT reward FROM tasks WHERE id = users_tasks.task_id) AS Reward + """ + ); + var task = await builder.QueryFirstAsync(); + return task; + } + + public async Task ChangeUserTask(int id) + { + var taskId = await GetRandomTask(); + + await using var conn = await dataSource.OpenConnectionAsync(); + var builder = conn.QueryBuilder( + $""" + UPDATE + users_tasks + SET + task_id = {taskId}, + moderation_status = {ModerationStatus.Default.ToString().ToLower()}::moderation_status, + start_time = NOW(), + photos_path = NULL, + photos_count = 0 + WHERE + id = {id} + RETURNING + users_tasks.id AS UserTaskId, + users_tasks.task_id AS TaskId, + users_tasks.start_time::timestamp AS StartTime, + (SELECT name FROM tasks WHERE id = users_tasks.task_id) AS Name, + (SELECT reward FROM tasks WHERE id = users_tasks.task_id) AS Reward + """ + ); + + var task = await builder.QueryFirstAsync(); + return task; + } + + private async Task GetRandomTask() + { + await using var conn = await dataSource.OpenConnectionAsync(); + var builder = conn.QueryBuilder( + $""" + SELECT + id + FROM + tasks + ORDER BY random() + LIMIT 1; + """ + ); + var taskId = await builder.QuerySingleAsync(); + + return taskId; + } +} \ No newline at end of file diff --git a/Infrastructure/DataAccess/UserTable.cs b/Infrastructure/DataAccess/UserTable.cs index f801f08..e6ffa21 100644 --- a/Infrastructure/DataAccess/UserTable.cs +++ b/Infrastructure/DataAccess/UserTable.cs @@ -18,7 +18,7 @@ public class UserTable(NpgsqlDataSource dataSource, IPasswordHasher hasher) : IU DisplayName = username.Trim(), Experience = 1, Level = 1, - Money = 0 + Money = 50 }; var builder = conn.QueryBuilder( $""" @@ -92,6 +92,27 @@ users.money AS Money return await builder.QuerySingleAsync(); } + public async Task GetUser(int userTaskId) + { + await using var conn = await dataSource.OpenConnectionAsync(); + var builder = conn.QueryBuilder( + $""" + SELECT + users.username AS Username, + users.display_name AS DisplayName, + users.exp AS Experience, + users.lvl AS Level, + users.money AS Money + FROM + users_tasks + JOIN users ON users.username = users_tasks.username + WHERE + users_tasks.id = {userTaskId} + """ + ); + return await builder.QuerySingleAsync(); + } + public async Task ChangeDisplayName(string username, string newDisplayName) { await using var conn = await dataSource.OpenConnectionAsync(); @@ -109,17 +130,17 @@ public async Task ChangeDisplayName(string username, string newDisplayName return rowsChanged == 1; } - public async Task ChangeMoney(int userTaskId, int money) + public async Task AddMoney(string username, int money) { await using var conn = await dataSource.OpenConnectionAsync(); - var builder = conn.QueryBuilder( $""" - UPDATE users - SET money = money + {money} - FROM users_tasks - WHERE users.username = users_tasks.username - AND users_tasks.id = {userTaskId} + UPDATE + users + Set + money = money + {money} + WHERE + username = {username} """ ); @@ -127,34 +148,35 @@ FROM users_tasks return rowsChanged == 1; } - - public async Task ChangeExperience(int userTaskId, int exp) + + public async Task AddExperience(string username, int exp) { await using var conn = await dataSource.OpenConnectionAsync(); var builder = conn.QueryBuilder( $""" - UPDATE users - SET exp = users.exp + {exp} - FROM users_tasks - WHERE users.username = users_tasks.username - AND users_tasks.id = {userTaskId}; + UPDATE + users + Set + exp = exp + {exp} + WHERE + username = {username} """ ); var rowsChanged = await builder.ExecuteAsync(); return rowsChanged == 1; } - - public async Task TryChangeLevel(int userTaskId) + + public async Task AddLevel(string username, int lvl) { await using var conn = await dataSource.OpenConnectionAsync(); var builder = conn.QueryBuilder( $""" - UPDATE users - SET lvl = users.lvl + 1 - FROM users_tasks - WHERE users.username = users_tasks.username - AND users_tasks.id = {userTaskId} - AND users.exp % 5 = 0; + UPDATE + users + Set + lvl = lvl + {lvl} + WHERE + username = {username} """ ); var rowsChanged = await builder.ExecuteAsync(); diff --git a/Infrastructure/DataAccess/UsersTasksTable.cs b/Infrastructure/DataAccess/UsersTasksTable.cs index f592d51..08c385a 100644 --- a/Infrastructure/DataAccess/UsersTasksTable.cs +++ b/Infrastructure/DataAccess/UsersTasksTable.cs @@ -10,13 +10,10 @@ namespace Infrastructure.DataAccess; public class UsersTasksTable( NpgsqlDataSource dataSource, - ILogger logger, IStorageService storageService) : IUsersTasksTable { public async Task> GetListActiveUserTasks(string username) { - logger.LogInformation("вызов GetListActiveUserTasks для username: {username} ", username); - Console.WriteLine($"вызов GetListActiveUserTasks для username: {username}"); await using var conn = await dataSource.OpenConnectionAsync(); var builder = conn.QueryBuilder( $""" @@ -31,130 +28,11 @@ tasks.reward AS Reward JOIN tasks ON tasks.id = users_tasks.task_id WHERE users_tasks.username = {username} - AND users_tasks.is_completed = '0' + AND users_tasks.moderation_status != {ModerationStatus.Approved.ToString().ToLower()}::moderation_status """); return (await builder.QueryAsync()).ToList(); } - public async Task AddUserTask(string username) - { - var task = await GetRandomTask(); - await using var conn = await dataSource.OpenConnectionAsync(); - var builder = conn.QueryBuilder( - $""" - INSERT INTO - users_tasks ( - task_id, - username, - moderation_status, - is_completed, - start_time, - photos_path, - photos_count - ) - VALUES - ({task.TaskId}, {username}, {ModerationStatus.Default.ToString().ToLower()}::moderation_status, '0', NOW(), NULL, 0) - """ - ); - var rowsChanged = await builder.ExecuteAsync(); - return rowsChanged == 1 ? task : null; - } - - public async Task ChangeUserTask(string username, string taskId) - { - var task = await GetRandomTask(); - - await using var conn = await dataSource.OpenConnectionAsync(); - var builder = conn.QueryBuilder( - $""" - UPDATE users_tasks - SET - task_id = {task.TaskId}, - moderation_status = {ModerationStatus.Default.ToString().ToLower()}::moderation_status, - is_completed = '0', - start_time = NOW(), - photos_path = NULL, - photos_count = 0 - WHERE - username = {username} - AND task_id = {taskId} - """ - ); - - var rowsChanged = await builder.ExecuteAsync(); - return rowsChanged == 1 ? task : null; - } - - public async Task ChangeUserTask(int id) - { - var task = await GetRandomTask(); - - await using var conn = await dataSource.OpenConnectionAsync(); - var builder = conn.QueryBuilder( - $""" - UPDATE users_tasks - SET - task_id = {task.TaskId}, - moderation_status = {ModerationStatus.Default.ToString().ToLower()}::moderation_status, - is_completed = '0', - start_time = NOW(), - photos_path = NULL, - photos_count = 0 - WHERE - id = {id} - """ - ); - - var rowsChanged = await builder.ExecuteAsync(); - return rowsChanged == 1 ? task : null; - } - - private async Task GetRandomTask() - { - await using var conn = await dataSource.OpenConnectionAsync(); - var builder = conn.QueryBuilder( - $""" - SELECT - tasks.id AS TaskId, - now()::timestamp AS StartTime, - tasks.name AS Name, - tasks.reward AS Reward - FROM - tasks - ORDER BY random() - LIMIT 1; - """ - ); - var taskId = await builder.QuerySingleAsync(); - - return taskId; - } - - public async Task GetTaskExtendedInfo(string username, string taskId) - { - logger.LogInformation("вызов GetTaskExtendedInfo для username: {username} task: {taskId} ", username, taskId); - await using var conn = await dataSource.OpenConnectionAsync(); - var builder = conn.QueryBuilder( - $""" - SELECT - users_tasks.id AS UserTaskId, - tasks.description AS Description, - tasks.photos_required AS PhotosRequired, - COALESCE(tasks.example_path, ARRAY[]::text[]) AS ExamplePhotos, - COALESCE(users_tasks.photos_path, ARRAY[]::text[]) AS UserPhotos - FROM - users_tasks - JOIN tasks ON tasks.id = users_tasks.task_id - WHERE - users_tasks.username = {username} - AND users_tasks.task_id = {taskId} - """); - var result = await builder.QueryFirstOrDefaultAsync(); - if (result is null) - return null; - return await result.ToTaskModelExtendedInfo(storageService); - } - public async Task GetTaskExtendedInfo(int id) { await using var conn = await dataSource.OpenConnectionAsync(); @@ -180,66 +58,32 @@ ORDER BY random() public async Task GetTaskFullInfo(int id) { - await using var conn = await dataSource.OpenConnectionAsync(); - var builder = conn.QueryBuilder( - $"""" - SELECT - users_tasks.id AS UserTaskId, - users_tasks.task_id AS TaskId, - users_tasks.start_time::timestamp AS StartTime, - tasks.name AS Name, - tasks.reward AS Reward - FROM - users_tasks - JOIN tasks ON tasks.id = users_tasks.task_id - WHERE - users_tasks.id = {id} - """" - ); - var task = (await builder.QueryAsync()).First(); + var task = await GetTaskNoExtendedInfo(id); + if (task is null) + return null; task.ExtendedInfo = await GetTaskExtendedInfo(id); return task; } - public async Task GetTaskFullInfo(string username, string taskId) + public async Task GetTaskNoExtendedInfo(int id) { - logger.LogInformation("вызов GetTaskFullInfo для username: {username} task: {taskId} ", username, taskId); await using var conn = await dataSource.OpenConnectionAsync(); var builder = conn.QueryBuilder( $"""" SELECT + users_tasks.id AS UserTaskId, users_tasks.task_id AS TaskId, users_tasks.start_time::timestamp AS StartTime, - tasks.name AS Name, - tasks.reward AS Reward + tasks.name AS Name, + tasks.reward AS Reward FROM users_tasks JOIN tasks ON tasks.id = users_tasks.task_id WHERE - users_tasks.username = {username} - AND users_tasks.task_id = {taskId} + users_tasks.id = {id} """" ); - var task = await builder.QueryFirstOrDefaultAsync(); - if (task is null) - return null; - task.ExtendedInfo = await GetTaskExtendedInfo(username, taskId); - return task; - } - - public async Task ChangeModerationStatus(string username, string taskId, ModerationStatus moderationStatus) - { - await using var conn = await dataSource.OpenConnectionAsync(); - var builder = conn.QueryBuilder( - $""" - UPDATE users_tasks - SET moderation_status = {moderationStatus.ToString().ToLower()}::moderation_status - WHERE - users_tasks.username = {username} - AND users_tasks.task_id = {taskId} - """ - ); - return await builder.ExecuteAsync() == 1; + return await builder.QueryFirstOrDefaultAsync(); } public async Task ChangeModerationStatus(int id, ModerationStatus moderationStatus) @@ -272,38 +116,21 @@ tasks.reward AS Reward JOIN tasks ON tasks.id = users_tasks.task_id WHERE users_tasks.username = {username} - AND is_completed = '1' + AND users_tasks.moderation_status = {ModerationStatus.Approved.ToString().ToLower()}::moderation_status ORDER BY users_tasks.start_time DESC """); return (await builder.QueryAsync()).ToList(); } - public async Task AddPhoto(string username, string taskId, string photoName) - { - await using var conn = await dataSource.OpenConnectionAsync(); - var builder = conn.QueryBuilder( - $""" - UPDATE users_tasks - SET - photos_path = COALESCE(photos_path, ARRAY[]::text[]) || ARRAY[{photoName}], - photos_count = photos_count + 1 - WHERE - users_tasks.username = {username} - AND users_tasks.task_id = {taskId} - """ - ); - return await builder.ExecuteAsync() == 1; - } - - public async Task AddPhoto(int id, string photoName) + public async Task SetPhotos(int id, string[] photosNames) { await using var conn = await dataSource.OpenConnectionAsync(); var builder = conn.QueryBuilder( $""" UPDATE users_tasks SET - photos_path = COALESCE(photos_path, ARRAY[]::text[]) || ARRAY[{photoName}], - photos_count = photos_count + 1 + photos_path = {photosNames}::text[], + photos_count = {photosNames.Length} WHERE users_tasks.id = {id} """ @@ -337,7 +164,7 @@ ORDER BY users_tasks.id task.ExtendedInfo = await GetTaskExtendedInfo(task.UserTaskId); return task; } - + public async Task GetModerationStatus(int id) { await using var conn = await dataSource.OpenConnectionAsync(); @@ -352,25 +179,8 @@ public async Task GetModerationStatus(int id) id = {id} """ ); - - return await builder.QueryFirstOrDefaultAsync(); - } - - public async Task SetCompleted(int id) - { - await using var conn = await dataSource.OpenConnectionAsync(); - var builder = conn.QueryBuilder( - $""" - UPDATE users_tasks - SET is_completed = '1' - WHERE - id = {id} - - """ - ); - - return await builder.QueryFirstOrDefaultAsync(); + return await builder.QueryFirstOrDefaultAsync(); } public async Task GetReward(int userTaskId) @@ -379,10 +189,13 @@ public async Task GetReward(int userTaskId) var builder = conn.QueryBuilder( $""" - SELECT tasks.reward - FROM users_tasks - JOIN tasks ON users_tasks.task_id = tasks.id - WHERE users_tasks.id = {userTaskId} + SELECT + tasks.reward + FROM + users_tasks + JOIN tasks ON users_tasks.task_id = tasks.id + WHERE + users_tasks.id = {userTaskId} """ ); diff --git a/Infrastructure/DbExtensions/ModerationTaskDbExtension.cs b/Infrastructure/DbExtensions/ModerationTaskDbExtension.cs index 01bfe41..93bc3ac 100644 --- a/Infrastructure/DbExtensions/ModerationTaskDbExtension.cs +++ b/Infrastructure/DbExtensions/ModerationTaskDbExtension.cs @@ -7,7 +7,7 @@ public class ModerationTaskDbExtension public int UserTaskId { get; set; } public required string TaskId { get; set; } public required string Name { get; set; } - public required string[] Tags { get; set; } + public required string[]? Tags { get; set; } public async Task ToModerationTask() { @@ -16,7 +16,7 @@ public async Task ToModerationTask() UserTaskId = this.UserTaskId, TaskId = this.TaskId, Name = this.Name, - Tags = Tags.ToList() + Tags = Tags is null ? [] : Tags.ToList() }; } } \ No newline at end of file diff --git a/Infrastructure/Interfaces/ITaskEvent.cs b/Infrastructure/Interfaces/ITaskEvent.cs new file mode 100644 index 0000000..39687a0 --- /dev/null +++ b/Infrastructure/Interfaces/ITaskEvent.cs @@ -0,0 +1,9 @@ +using Shared.Models.Entities; + +namespace Infrastructure.Interfaces; + +public interface ITaskEvent +{ + public Task AddUserTask(string username); + public Task ChangeUserTask(int id); +} \ No newline at end of file diff --git a/Infrastructure/Interfaces/IUserTable.cs b/Infrastructure/Interfaces/IUserTable.cs index 4d867c9..2f03978 100644 --- a/Infrastructure/Interfaces/IUserTable.cs +++ b/Infrastructure/Interfaces/IUserTable.cs @@ -7,8 +7,9 @@ public interface IUserTable public Task RegisterUser(string username, string hashPassword); public Task LoginUser(string username, string hashPassword); public Task GetUser(string username); + public Task GetUser(int userTaskId); public Task ChangeDisplayName(string username, string newDisplayName); - public Task ChangeExperience(int userTaskId, int exp); - public Task TryChangeLevel(int userTaskId); - public Task ChangeMoney(int userTaskId, int money); + public Task AddExperience(string username, int exp); + public Task AddLevel(string username, int lvl); + public Task AddMoney(string username, int money); } \ No newline at end of file diff --git a/Infrastructure/Interfaces/IUsersTasksTable.cs b/Infrastructure/Interfaces/IUsersTasksTable.cs index c1635c9..9012f5b 100644 --- a/Infrastructure/Interfaces/IUsersTasksTable.cs +++ b/Infrastructure/Interfaces/IUsersTasksTable.cs @@ -6,21 +6,13 @@ namespace Infrastructure.Interfaces; public interface IUsersTasksTable { public Task> GetListActiveUserTasks(string username); - public Task AddUserTask(string username); - public Task GetTaskExtendedInfo(string username, string taskId); public Task GetTaskExtendedInfo(int id); - public Task GetTaskFullInfo(string taskId, string username); public Task GetTaskFullInfo(int taskId); - public Task ChangeModerationStatus(string username, string taskId, ModerationStatus moderationStatus); + public Task GetTaskNoExtendedInfo(int id); public Task ChangeModerationStatus(int id, ModerationStatus moderationStatus); public Task> GetUserSubmissionHistory(string username); - - public Task AddPhoto(string username, string taskId, string photoName); - public Task AddPhoto(int id, string photoName); + public Task SetPhotos(int id, string[] photosNames); public Task GetModerationTask(); - public Task ChangeUserTask(string username, string taskId); - public Task ChangeUserTask(int id); public Task GetModerationStatus(int id); - public Task SetCompleted(int id); public Task GetReward(int id); } \ No newline at end of file diff --git a/Infrastructure/Interfaces/IWeatherApi.cs b/Infrastructure/Interfaces/IWeatherApi.cs index 36d942a..d9b2b00 100644 --- a/Infrastructure/Interfaces/IWeatherApi.cs +++ b/Infrastructure/Interfaces/IWeatherApi.cs @@ -1,8 +1,9 @@ -using Shared.Models.ExternalApi; +using Api.Application.Common.Results; +using Shared.Models.ExternalApi; namespace Infrastructure.Interfaces; public interface IWeatherApi { - Task GetCurrentWeatherAsync(CancellationToken ct = default); + Task> GetCurrentWeatherAsync(CancellationToken ct = default); } \ No newline at end of file diff --git a/Test/Fake/FakePasswordHasher.cs b/Test/Fake/FakePasswordHasher.cs new file mode 100644 index 0000000..27dfda8 --- /dev/null +++ b/Test/Fake/FakePasswordHasher.cs @@ -0,0 +1,11 @@ +using Infrastructure.Interfaces; + +namespace Test.Fake; + +public sealed class FakePasswordHasher : IPasswordHasher +{ + public string Hash(string password) => $"HASH::{password}"; + + public bool Verify(string password, string hash) => + hash == $"HASH::{password}"; +} \ No newline at end of file diff --git a/Test/Fake/FakeStorageService.cs b/Test/Fake/FakeStorageService.cs new file mode 100644 index 0000000..c54accb --- /dev/null +++ b/Test/Fake/FakeStorageService.cs @@ -0,0 +1,21 @@ +using Infrastructure.Interfaces; + +public sealed class FakeStorageService : IStorageService +{ + public Task> GetTemporaryUrlsAsync(List fileNames) + { + if (fileNames is null || fileNames.Count == 0) + return Task.FromResult(new List()); + + var result = fileNames + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => + { + var safe = x.TrimStart('/').Replace("\\", "/"); + return new Uri($"https://fake.local/{safe}", UriKind.Absolute); + }) + .ToList(); + + return Task.FromResult(result); + } +} \ No newline at end of file diff --git a/Test/Infrastructure/DbConectionTest.cs b/Test/Infrastructure/DbConectionTest.cs new file mode 100644 index 0000000..da998f7 --- /dev/null +++ b/Test/Infrastructure/DbConectionTest.cs @@ -0,0 +1,36 @@ +using Dapper; +using Npgsql; +using NUnit.Framework; +using Test; + +[TestFixture] +public class DbSmokeTests : TestBase +{ + [Test] + public async Task Db_is_available_and_schema_is_applied() + { + await using (var conn = await DataSource.OpenConnectionAsync()) + { + var ping = await conn.ExecuteScalarAsync("SELECT 1;"); + Assert.That(ping, Is.EqualTo(1), "PostgreSQL is not reachable (SELECT 1 failed)."); + } + + await using (var conn = await DataSource.OpenConnectionAsync()) + { + var tables = (await conn.QueryAsync(""" + SELECT tablename + FROM pg_tables + WHERE schemaname = 'public' + ORDER BY tablename; + """)).ToList(); + + Assert.Multiple(() => + { + Assert.That(tables, Does.Contain("tasks"), "Table 'tasks' was not created."); + Assert.That(tables, Does.Contain("users"), "Table 'users' was not created."); + Assert.That(tables, Does.Contain("users_tasks"), "Table 'users_tasks' was not created."); + Assert.That(tables, Does.Contain("moderators"), "Table 'moderators' was not created."); + }); + } + } +} \ No newline at end of file diff --git a/Test/Infrastructure/UserTableTests.cs b/Test/Infrastructure/UserTableTests.cs new file mode 100644 index 0000000..095adf4 --- /dev/null +++ b/Test/Infrastructure/UserTableTests.cs @@ -0,0 +1,137 @@ +using Dapper; +using Infrastructure.Interfaces; +using InterpolatedSql.Dapper; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Shared.Models.Entities; +using Test; + +[TestFixture] +public class UserTableTests : TestBase +{ + private IUserTable users = null!; + + [SetUp] + public void Setup() + { + users = Provider.GetRequiredService(); + } + + [Test] + public async Task RegisterUser_creates_user() + { + var user = await users.RegisterUser("test", "HASH::123"); + + Assert.That(user, Is.Not.Null); + Assert.That(user!.Username, Is.EqualTo("test")); + Assert.That(user.Level, Is.EqualTo(1)); + Assert.That(user.Money, Is.EqualTo(50)); + } + + [Test] + public async Task RegisterUser_returns_null_if_username_exists() + { + await users.RegisterUser("test", "HASH::123"); + + var second = await users.RegisterUser("test", "HASH::456"); + + Assert.That(second, Is.Null); + } + + [Test] + public async Task LoginUser_returns_true_for_correct_password() + { + await users.RegisterUser("test", "HASH::123"); + + var result = await users.LoginUser("test", "123"); + + Assert.That(result, Is.True); + } + + [Test] + public async Task LoginUser_returns_false_for_wrong_password() + { + await users.RegisterUser("test", "HASH::123"); + + var result = await users.LoginUser("test", "wrong"); + + Assert.That(result, Is.False); + } + + [Test] + public async Task GetUser_by_username_returns_user() + { + await users.RegisterUser("test", "HASH::123"); + + var user = await users.GetUser("test"); + + Assert.That(user, Is.Not.Null); + Assert.That(user!.Username, Is.EqualTo("test")); + } + + [Test] + public async Task ChangeDisplayName_updates_name() + { + await users.RegisterUser("test", "HASH::123"); + + var changed = await users.ChangeDisplayName("test", "NewName"); + var user = await users.GetUser("test"); + + Assert.That(changed, Is.True); + Assert.That(user!.DisplayName, Is.EqualTo("NewName")); + } + + [Test] + public async Task AddMoney_increases_money() + { + await users.RegisterUser("test", "HASH::123"); + + await users.AddMoney("test", 25); + var user = await users.GetUser("test"); + + Assert.That(user!.Money, Is.EqualTo(75)); + } + + [Test] + public async Task AddExperience_increases_exp() + { + await users.RegisterUser("test", "HASH::123"); + + await users.AddExperience("test", 10); + var user = await users.GetUser("test"); + + Assert.That(user!.Experience, Is.EqualTo(11)); + } + + [Test] + public async Task AddLevel_increases_level() + { + await users.RegisterUser("test", "HASH::123"); + + await users.AddLevel("test", 2); + var user = await users.GetUser("test"); + + Assert.That(user!.Level, Is.EqualTo(3)); + } + + [Test] + public async Task GetUser_by_userTaskId_returns_user() + { + await users.RegisterUser("test", "HASH::123"); + + await using var conn = await DataSource.OpenConnectionAsync(); + + var taskId = await conn.QueryBuilder( + $""" + INSERT INTO users_tasks (task_id, username) + VALUES ('t1', 'test') + RETURNING id; + """ + ).ExecuteScalarAsync(); + + var user = await users.GetUser(taskId); + + Assert.That(user, Is.Not.Null); + Assert.That(user!.Username, Is.EqualTo("test")); + } +} \ No newline at end of file diff --git a/Test/Infrastructure/UsersTasksTableTests.cs b/Test/Infrastructure/UsersTasksTableTests.cs new file mode 100644 index 0000000..8928dd5 --- /dev/null +++ b/Test/Infrastructure/UsersTasksTableTests.cs @@ -0,0 +1,146 @@ +using Dapper; +using Infrastructure.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using InterpolatedSql.Dapper; +using NUnit.Framework; +using Shared.Models.Enums; +using Test; + +[TestFixture] +public class UsersTasksTableTests : TestBase +{ + private IUsersTasksTable table = null!; + + [SetUp] + public void Setup() + { + table = Provider.GetRequiredService(); + } + + [Test] + public async Task GetTaskNoExtendedInfo_returns_task() + { + var id = await SeedUserTask(); + + var task = await table.GetTaskNoExtendedInfo(id); + + Assert.That(task, Is.Not.Null); + Assert.That(task!.UserTaskId, Is.EqualTo(id)); + } + + [Test] + public async Task GetTaskExtendedInfo_returns_extended_info() + { + var id = await SeedUserTask( + examplePath: ["img1.png", "img2.png"]); + + var info = await table.GetTaskExtendedInfo(id); + + Assert.That(info, Is.Not.Null); + Assert.That(info!.ExamplePhotos, Has.Count.EqualTo(2)); + } + + [Test] + public async Task GetTaskFullInfo_returns_task_with_extension() + { + var id = await SeedUserTask(); + + var task = await table.GetTaskFullInfo(id); + + Assert.That(task, Is.Not.Null); + Assert.That(task!.ExtendedInfo, Is.Not.Null); + } + + [Test] + public async Task ChangeModerationStatus_updates_status() + { + var id = await SeedUserTask(); + + var result = await table.ChangeModerationStatus(id, ModerationStatus.Approved); + var status = await table.GetModerationStatus(id); + + Assert.That(result, Is.True); + Assert.That(status, Is.EqualTo("approved")); + } + + [Test] + public async Task SetPhotos_replaces_array_and_count() + { + var id = await SeedUserTask(); + + var photos = new[] { "a.png", "b.png" }; + var result = await table.SetPhotos(id, photos); + + Assert.That(result, Is.True); + + await using var conn = await DataSource.OpenConnectionAsync(); + + var count = await conn.QueryBuilder( + $""" + SELECT photos_count + FROM users_tasks + WHERE id = {id} + """ + ).ExecuteScalarAsync(); + + Assert.That(count, Is.EqualTo(2)); + } + + [Test] + public async Task GetModerationTask_returns_waiting_task() + { + await SeedUserTask(moderationStatus: "waiting"); + + var task = await table.GetModerationTask(); + + Assert.That(task, Is.Not.Null); + Assert.That(task!.ExtendedInfo, Is.Not.Null); + } + + [Test] + public async Task GetReward_returns_reward() + { + var id = await SeedUserTask(); + + var reward = await table.GetReward(id); + + Assert.That(reward, Is.EqualTo(10)); + } + + private async Task SeedUserTask( + string username = "user", + string taskId = "task1", + string moderationStatus = "default", + string[]? examplePath = null) + { + await using var conn = await DataSource.OpenConnectionAsync(); + + await conn.QueryBuilder( + $""" + INSERT INTO users (username) + VALUES ({username}) + """ + ).ExecuteAsync(); + + await conn.QueryBuilder( + $""" + INSERT INTO tasks (id, name, reward, example_path) + VALUES ({taskId}, 'Task', 10, {examplePath}::text[]) + """ + ).ExecuteAsync(); + + var builder = conn.QueryBuilder( + $""" + INSERT INTO users_tasks (task_id, username, moderation_status) + VALUES ( + {taskId}, + {username}, + {moderationStatus}::moderation_status + ) + RETURNING id + """ + ); + + return await builder.ExecuteScalarAsync(); + } +} \ No newline at end of file diff --git a/Test/Test.csproj b/Test/Test.csproj new file mode 100644 index 0000000..573b440 --- /dev/null +++ b/Test/Test.csproj @@ -0,0 +1,37 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + + db_init\001_schema.sql + Always + + + + + + + + + + + + + diff --git a/Test/TestBase.cs b/Test/TestBase.cs new file mode 100644 index 0000000..716b5e0 --- /dev/null +++ b/Test/TestBase.cs @@ -0,0 +1,83 @@ +using Dapper; +using DotNetEnv; +using Infrastructure.DataAccess; +using Infrastructure.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Npgsql; +using Test.Fake; + +namespace Test; + +public abstract class TestBase +{ + protected ServiceProvider Provider = null!; + protected NpgsqlDataSource DataSource = null!; + + [OneTimeSetUp] + public async Task OneTimeSetUp() + { + Env.TraversePath().Load(); + + var cs = BuildConnectionString(); + + var services = new ServiceCollection(); + services.AddSingleton(NpgsqlDataSource.Create(cs)); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddSingleton(); + services.AddSingleton(); + + Provider = services.BuildServiceProvider(); + DataSource = Provider.GetRequiredService(); + + await InitSchema(); + } + + private static string BuildConnectionString() + { + string Get(string name) => + Environment.GetEnvironmentVariable(name) + ?? throw new InvalidOperationException($"{name} is missing"); + + return + $"Host={Get("POSTGRES_SERVER")};" + + $"Port={Get("POSTGRES_PORT")};" + + $"Database={Get("POSTGRES_DB")};" + + $"Username={Get("POSTGRES_USER")};" + + $"Password={Get("POSTGRES_PASSWORD")};"; + } + + private async Task InitSchema() + { + await using var conn = await DataSource.OpenConnectionAsync(); + + var sql = await File.ReadAllTextAsync( + Path.Combine("db_init", "001_schema.sql")); + + await conn.ExecuteAsync(sql); + } + + [SetUp] + public async Task CleanDb() + { + await using var conn = await DataSource.OpenConnectionAsync(); + + await conn.ExecuteAsync(""" + TRUNCATE TABLE + users_tasks, + tasks, + users + RESTART IDENTITY + CASCADE; + """); + } + + [OneTimeTearDown] + public async Task TearDown() + { + await DataSource.DisposeAsync(); + await Provider.DisposeAsync(); + } +} \ No newline at end of file diff --git a/Vibik.Server.sln b/Vibik.Server.sln index 5e0a3b4..a0c94e6 100644 --- a/Vibik.Server.sln +++ b/Vibik.Server.sln @@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infrastructure", "Infrastru EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Models", "Client.Models\Client.Models.csproj", "{84019F5E-A8D9-49B2-B78D-071A674A3EB4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{916F4C8D-F266-4DFA-9731-085E89A984E4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,5 +30,9 @@ Global {4950BB62-F53B-4D0F-BF7A-0809E407A808}.Debug|Any CPU.Build.0 = Debug|Any CPU {4950BB62-F53B-4D0F-BF7A-0809E407A808}.Release|Any CPU.ActiveCfg = Release|Any CPU {4950BB62-F53B-4D0F-BF7A-0809E407A808}.Release|Any CPU.Build.0 = Release|Any CPU + {916F4C8D-F266-4DFA-9731-085E89A984E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {916F4C8D-F266-4DFA-9731-085E89A984E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {916F4C8D-F266-4DFA-9731-085E89A984E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {916F4C8D-F266-4DFA-9731-085E89A984E4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/db_init/001_schema.sql b/db_init/001_schema.sql index 46c8fb7..66da5b3 100644 --- a/db_init/001_schema.sql +++ b/db_init/001_schema.sql @@ -25,7 +25,7 @@ CREATE TABLE DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'moderation_status') THEN - CREATE TYPE moderation_status AS ENUM ('not', 'on', 'approved', 'reject'); + CREATE TYPE moderation_status AS ENUM ('default', 'waiting', 'approved', 'reject'); END IF; END$$; @@ -35,7 +35,6 @@ CREATE TABLE task_id VARCHAR(64), username VARCHAR(64), moderation_status moderation_status, - is_completed BOOLEAN NOT NULL DEFAULT FALSE, start_time DATE, photos_path TEXT[], photos_count INT