diff --git a/.dockerignore b/.dockerignore
index cd967fc..d67c213 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -22,4 +22,5 @@
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
-README.md
\ No newline at end of file
+README.md
+sparkly-server.test
\ No newline at end of file
diff --git a/Migrations/20251113212512_Init.Designer.cs b/Migrations/20251113212512_Init.Designer.cs
index dbf0243..5f98075 100644
--- a/Migrations/20251113212512_Init.Designer.cs
+++ b/Migrations/20251113212512_Init.Designer.cs
@@ -9,7 +9,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20251113212512_Init")]
diff --git a/Migrations/20251113212512_Init.cs b/Migrations/20251113212512_Init.cs
index 4084354..a2a5a5c 100644
--- a/Migrations/20251113212512_Init.cs
+++ b/Migrations/20251113212512_Init.cs
@@ -3,7 +3,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
///
public partial class Init : Migration
diff --git a/Migrations/20251114220854_Username.Designer.cs b/Migrations/20251114220854_Username.Designer.cs
index eb9afa7..0c72365 100644
--- a/Migrations/20251114220854_Username.Designer.cs
+++ b/Migrations/20251114220854_Username.Designer.cs
@@ -9,7 +9,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20251114220854_Username")]
diff --git a/Migrations/20251114220854_Username.cs b/Migrations/20251114220854_Username.cs
index 254ca45..32a5fce 100644
--- a/Migrations/20251114220854_Username.cs
+++ b/Migrations/20251114220854_Username.cs
@@ -2,7 +2,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
///
public partial class Username : Migration
diff --git a/Migrations/20251114234534_RefreshToken.Designer.cs b/Migrations/20251114234534_RefreshToken.Designer.cs
index 7bc067b..79f2d12 100644
--- a/Migrations/20251114234534_RefreshToken.Designer.cs
+++ b/Migrations/20251114234534_RefreshToken.Designer.cs
@@ -9,7 +9,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20251114234534_RefreshToken")]
diff --git a/Migrations/20251114234534_RefreshToken.cs b/Migrations/20251114234534_RefreshToken.cs
index 090c366..e5915eb 100644
--- a/Migrations/20251114234534_RefreshToken.cs
+++ b/Migrations/20251114234534_RefreshToken.cs
@@ -3,7 +3,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
///
public partial class RefreshToken : Migration
diff --git a/Migrations/20251119102854_Default_value_for_visibility.Designer.cs b/Migrations/20251119102854_Default_value_for_visibility.Designer.cs
index e704806..70c9d44 100644
--- a/Migrations/20251119102854_Default_value_for_visibility.Designer.cs
+++ b/Migrations/20251119102854_Default_value_for_visibility.Designer.cs
@@ -9,7 +9,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20251119102854_Default_value_for_visibility")]
diff --git a/Migrations/20251119102854_Default_value_for_visibility.cs b/Migrations/20251119102854_Default_value_for_visibility.cs
index 79b4bbc..d7c5d0c 100644
--- a/Migrations/20251119102854_Default_value_for_visibility.cs
+++ b/Migrations/20251119102854_Default_value_for_visibility.cs
@@ -3,7 +3,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
///
public partial class Default_value_for_visibility : Migration
diff --git a/Migrations/20251119191413_Members_project.Designer.cs b/Migrations/20251119191413_Members_project.Designer.cs
index 5045c6a..b53e39a 100644
--- a/Migrations/20251119191413_Members_project.Designer.cs
+++ b/Migrations/20251119191413_Members_project.Designer.cs
@@ -9,7 +9,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20251119191413_Members_project")]
diff --git a/Migrations/20251119191413_Members_project.cs b/Migrations/20251119191413_Members_project.cs
index db83a7b..f1bb499 100644
--- a/Migrations/20251119191413_Members_project.cs
+++ b/Migrations/20251119191413_Members_project.cs
@@ -2,7 +2,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
///
public partial class Members_project : Migration
diff --git a/Migrations/AppDbContextModelSnapshot.cs b/Migrations/AppDbContextModelSnapshot.cs
index 739e3b5..d7db13d 100644
--- a/Migrations/AppDbContextModelSnapshot.cs
+++ b/Migrations/AppDbContextModelSnapshot.cs
@@ -8,7 +8,7 @@
#nullable disable
-namespace sparkly_server.Migrations
+namespace sparkly_server.Services.Users.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
diff --git a/Program.cs b/Program.cs
index aeb5073..9965890 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,13 +1,12 @@
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
+using Scalar.AspNetCore;
using sparkly_server.Enum;
using sparkly_server.Infrastructure;
using sparkly_server.Services.Auth;
-using sparkly_server.Services.Users;
-using sparkly_server.Services.UserServices;
using sparkly_server.Services.Projects;
-using Scalar.AspNetCore;
+using sparkly_server.Services.Users;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
diff --git a/sparkly-server.csproj b/sparkly-server.csproj
index 8b5a614..1e43bac 100644
--- a/sparkly-server.csproj
+++ b/sparkly-server.csproj
@@ -47,4 +47,10 @@
+
+
+
+
+
+
diff --git a/sparkly-server.test/HealthzTest.cs b/sparkly-server.test/HealthzTest.cs
index e58d945..3f45f2e 100644
--- a/sparkly-server.test/HealthzTest.cs
+++ b/sparkly-server.test/HealthzTest.cs
@@ -1,6 +1,7 @@
-using System.Net;
+using Sparkly.Tests.Infrastructure;
+using System.Net;
-namespace sparkly_server.test;
+namespace sparkly_server.Services.Users.test;
public class HealthzTest : IClassFixture
{
diff --git a/sparkly-server.test/TestWebAppliactionFactory.cs b/sparkly-server.test/TestWebAppliactionFactory.cs
index e1bafd4..4d5def0 100644
--- a/sparkly-server.test/TestWebAppliactionFactory.cs
+++ b/sparkly-server.test/TestWebAppliactionFactory.cs
@@ -1,16 +1,28 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using sparkly_server.Infrastructure;
-namespace sparkly_server.test;
+namespace Sparkly.Tests.Infrastructure;
public class TestWebApplicationFactory : WebApplicationFactory
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Testing");
+
+ builder.ConfigureAppConfiguration((config) =>
+ {
+ var settings = new Dictionary
+ {
+ ["SPARKLY_JWT_KEY"] = "this-is-very-long-test-jwt-key-123456",
+ ["SPARKLY_JWT_ISSUER"] = "sparkly-test-issuer"
+ };
+
+ config.AddInMemoryCollection(settings);
+ });
builder.ConfigureServices(services =>
{
@@ -26,6 +38,13 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)
{
options.UseInMemoryDatabase("sparkly-tests");
});
+
+ var sp = services.BuildServiceProvider();
+
+ using var scope = sp.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+ db.Database.EnsureDeleted();
+ db.Database.EnsureCreated();
});
}
}
diff --git a/sparkly-server.test/UserTest.cs b/sparkly-server.test/UserTest.cs
new file mode 100644
index 0000000..e3c2664
--- /dev/null
+++ b/sparkly-server.test/UserTest.cs
@@ -0,0 +1,101 @@
+using Microsoft.Extensions.DependencyInjection;
+using sparkly_server.DTO.Auth;
+using sparkly_server.Infrastructure;
+using Sparkly.Tests.Infrastructure;
+using System.Text;
+using System.Text.Json;
+
+namespace sparkly_server.Services.Users.test
+{
+ public class UserTest : IClassFixture, IAsyncLifetime
+ {
+ private readonly HttpClient _client;
+ private readonly TestWebApplicationFactory _factory;
+
+ public UserTest(TestWebApplicationFactory factory)
+ {
+ _factory = factory;
+ _client = factory.CreateClient();
+ }
+
+ public async Task InitializeAsync()
+ {
+ using var scope = _factory.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+
+ db.Users.RemoveRange(db.Users);
+ db.Projects.RemoveRange(db.Projects);
+ await db.SaveChangesAsync();
+ }
+
+ public Task DisposeAsync() => Task.CompletedTask;
+
+ private async Task RegisterTestUserAsync(string userName, string email, string password)
+ {
+ var payload = new RegisterRequest(Username: userName, Email: email, Password: password);
+
+ var content = new StringContent(
+ JsonSerializer.Serialize(payload),
+ Encoding.UTF8,
+ "application/json"
+ );
+
+ var response = await _client.PostAsync("/api/v1/auth/register", content);
+ response.EnsureSuccessStatusCode();
+ }
+
+ [Fact]
+ public async Task User_CanBeCreated()
+ {
+ var userName = $"user_{Guid.NewGuid():N}";
+ var email = $"{Guid.NewGuid():N}@example.com";
+ var password = "UserPassword";
+
+ await RegisterTestUserAsync(userName, email, password);
+ }
+
+ [Fact]
+ public async Task User_CanLoginByEmail()
+ {
+ var userName = $"user_{Guid.NewGuid():N}";
+ var email = $"{Guid.NewGuid():N}@example.com";
+ var password = "UserPassword";
+
+ await RegisterTestUserAsync(userName, email, password);
+
+ var payload = new LoginRequest(Identifier: email, Password: password);
+
+ var content = new StringContent(
+ JsonSerializer.Serialize(payload),
+ Encoding.UTF8,
+ "application/json"
+ );
+
+ var response = await _client.PostAsync("/api/v1/auth/login", content);
+
+ response.EnsureSuccessStatusCode();
+ }
+
+ [Fact]
+ public async Task User_CanLoginByUsername()
+ {
+ var userName = $"user_{Guid.NewGuid():N}";
+ var email = $"{Guid.NewGuid():N}@example.com";
+ var password = "UserPassword";
+
+ await RegisterTestUserAsync(userName, email, password);
+
+ var payload = new LoginRequest(Identifier: userName, Password: password);
+
+ var content = new StringContent(
+ JsonSerializer.Serialize(payload),
+ Encoding.UTF8,
+ "application/json"
+ );
+
+ var response = await _client.PostAsync("/api/v1/auth/login", content);
+
+ response.EnsureSuccessStatusCode();
+ }
+ }
+}
diff --git a/sparkly-server.test/sparkly-server.test.csproj b/sparkly-server.test/sparkly-server.test.csproj
index b953d0e..ce55d33 100644
--- a/sparkly-server.test/sparkly-server.test.csproj
+++ b/sparkly-server.test/sparkly-server.test.csproj
@@ -6,6 +6,7 @@
enable
enable
false
+ $(NoWarn);MSB3026
diff --git a/src/Controllers/User/AuthController.cs b/src/Controllers/User/AuthController.cs
index 90ac4f6..ee2d606 100644
--- a/src/Controllers/User/AuthController.cs
+++ b/src/Controllers/User/AuthController.cs
@@ -1,10 +1,10 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using sparkly_server.DTO;
+using sparkly_server.DTO.Auth;
using sparkly_server.Services.Auth;
using sparkly_server.Services.Users;
-namespace sparkly_server.Controllers
+namespace sparkly_server.Controllers.User
{
[ApiController]
[Route("api/v1/auth")]
diff --git a/src/Controllers/User/ProfileController.cs b/src/Controllers/User/ProfileController.cs
index e933b70..b97df4e 100644
--- a/src/Controllers/User/ProfileController.cs
+++ b/src/Controllers/User/ProfileController.cs
@@ -1,10 +1,9 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using sparkly_server.Infrastructure;
-using sparkly_server.Services.Auth;
using sparkly_server.Services.Users;
-namespace sparkly_server.Controllers
+namespace sparkly_server.Controllers.User
{
[Authorize]
[ApiController]
diff --git a/src/DTO/Auth/Auth.cs b/src/DTO/Auth/Auth.cs
index 3d93103..8bc7590 100644
--- a/src/DTO/Auth/Auth.cs
+++ b/src/DTO/Auth/Auth.cs
@@ -1,4 +1,4 @@
-namespace sparkly_server.DTO
+namespace sparkly_server.DTO.Auth
{
public record RegisterRequest(string Username, string Email, string Password);
public record LoginRequest(string Identifier, string Password);
diff --git a/src/Domain/Auth/RefreshToken.cs b/src/Domain/Auth/RefreshToken.cs
index aedcbe0..3a818e0 100644
--- a/src/Domain/Auth/RefreshToken.cs
+++ b/src/Domain/Auth/RefreshToken.cs
@@ -1,3 +1,5 @@
+using sparkly_server.Domain.Users;
+
namespace sparkly_server.Domain.Auth
{
public class RefreshToken
diff --git a/src/Domain/Projects/Project.cs b/src/Domain/Projects/Project.cs
index 4169c35..6b696e3 100644
--- a/src/Domain/Projects/Project.cs
+++ b/src/Domain/Projects/Project.cs
@@ -1,13 +1,14 @@
-using sparkly_server.Enum;
+using sparkly_server.Domain.Users;
+using sparkly_server.Enum;
namespace sparkly_server.Domain.Projects
{
public class Project
{
public Guid Id { get; private set; }
- public string ProjectName { get; private set; }
- public string Description { get; private set; }
- public string Slug { get; private set; }
+ public string ProjectName { get; private set; } = string.Empty;
+ public string Description { get; private set; } = string.Empty;
+ public string Slug { get; private set; } = string.Empty;
public DateTime CreatedAt { get; private set; }
public Guid OwnerId { get; private set; }
private readonly List _tags = new();
@@ -152,7 +153,7 @@ public void RemoveMember(User user)
if (user is null)
throw new ArgumentNullException(nameof(user));
- if (_members.Any(m => m.Id == user.Id))
+ if (!_members.Any(m => m.Id == user.Id))
return;
_members.Remove(user);
diff --git a/src/Domain/Users/User.cs b/src/Domain/Users/User.cs
index 8c9ba40..5648acf 100644
--- a/src/Domain/Users/User.cs
+++ b/src/Domain/Users/User.cs
@@ -1,7 +1,7 @@
using sparkly_server.Domain.Auth;
using sparkly_server.Domain.Projects;
-namespace sparkly_server.Domain
+namespace sparkly_server.Domain.Users
{
public class User
{
diff --git a/src/Infrastructure/AppDbContext.cs b/src/Infrastructure/AppDbContext.cs
index fde6e0c..c7ed389 100644
--- a/src/Infrastructure/AppDbContext.cs
+++ b/src/Infrastructure/AppDbContext.cs
@@ -1,7 +1,7 @@
using Microsoft.EntityFrameworkCore;
-using sparkly_server.Domain;
using sparkly_server.Domain.Auth;
using sparkly_server.Domain.Projects;
+using sparkly_server.Domain.Users;
using sparkly_server.Enum;
namespace sparkly_server.Infrastructure
diff --git a/src/Services/Auth/AuthService.cs b/src/Services/Auth/AuthService.cs
index aecb91a..d1a640f 100644
--- a/src/Services/Auth/AuthService.cs
+++ b/src/Services/Auth/AuthService.cs
@@ -55,7 +55,7 @@ public async Task RefreshAsync(string refreshToken, CancellationToke
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
- return null;
+ return null!;
}
var entity = await _db.RefreshTokens
@@ -64,7 +64,7 @@ public async Task RefreshAsync(string refreshToken, CancellationToke
if (entity is null || !entity.IsActive)
{
- return null;
+ return null!;
}
var user = entity.User;
diff --git a/src/Services/Auth/IJwtProvider.cs b/src/Services/Auth/IJwtProvider.cs
index 38194b2..784300c 100644
--- a/src/Services/Auth/IJwtProvider.cs
+++ b/src/Services/Auth/IJwtProvider.cs
@@ -1,4 +1,4 @@
-using sparkly_server.Domain;
+using sparkly_server.Domain.Users;
namespace sparkly_server.Services.Auth
{
diff --git a/src/Services/Auth/JwtProvider.cs b/src/Services/Auth/JwtProvider.cs
index 26f2a36..d5cab24 100644
--- a/src/Services/Auth/JwtProvider.cs
+++ b/src/Services/Auth/JwtProvider.cs
@@ -1,5 +1,5 @@
using Microsoft.IdentityModel.Tokens;
-using sparkly_server.Domain;
+using sparkly_server.Domain.Users;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
diff --git a/src/Services/Projects/ProjectService.cs b/src/Services/Projects/ProjectService.cs
index b4c1b87..5571db0 100644
--- a/src/Services/Projects/ProjectService.cs
+++ b/src/Services/Projects/ProjectService.cs
@@ -41,9 +41,11 @@ public async Task CreateProjectAsync(string name, string description, P
return project;
}
- public Task GetProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default)
+ public async Task GetProjectByIdAsync(Guid projectId, CancellationToken cancellationToken = default)
{
- var project = _projects.GetByIdAsync(projectId, cancellationToken);
+ var project = await _projects.GetByIdAsync(projectId, cancellationToken)
+ ?? throw new InvalidOperationException("Project not found");
+
return project;
}
@@ -214,7 +216,7 @@ public async Task DeleteProjectAsync(Guid projectId, CancellationToken cancellat
if (!isOwner && !isAdmin)
throw new UnauthorizedAccessException("You are not allowed to delete this project.");
- _projects.DeleteAsync(projectId, cancellationToken);
+ await _projects.DeleteAsync(projectId, cancellationToken);
await _projects.SaveChangesAsync(cancellationToken);
}
diff --git a/src/Services/Users/IUserRepository.cs b/src/Services/Users/IUserRepository.cs
index e833189..8d5199e 100644
--- a/src/Services/Users/IUserRepository.cs
+++ b/src/Services/Users/IUserRepository.cs
@@ -1,4 +1,4 @@
-using sparkly_server.Domain;
+using sparkly_server.Domain.Users;
namespace sparkly_server.Services.Users
{
diff --git a/src/Services/Users/IUserService.cs b/src/Services/Users/IUserService.cs
index 31ba33c..ffb8f68 100644
--- a/src/Services/Users/IUserService.cs
+++ b/src/Services/Users/IUserService.cs
@@ -1,4 +1,4 @@
-using sparkly_server.Domain;
+using sparkly_server.Domain.Users;
namespace sparkly_server.Services.Users
{
diff --git a/src/Services/Users/UserRepository.cs b/src/Services/Users/UserRepository.cs
index 17f27b6..34dfe7d 100644
--- a/src/Services/Users/UserRepository.cs
+++ b/src/Services/Users/UserRepository.cs
@@ -1,5 +1,5 @@
using Microsoft.EntityFrameworkCore;
-using sparkly_server.Domain;
+using sparkly_server.Domain.Users;
using sparkly_server.Infrastructure;
namespace sparkly_server.Services.Users
diff --git a/src/Services/Users/UserService.cs b/src/Services/Users/UserService.cs
index e08a726..880f3d2 100644
--- a/src/Services/Users/UserService.cs
+++ b/src/Services/Users/UserService.cs
@@ -1,8 +1,7 @@
using Microsoft.AspNetCore.Identity;
-using sparkly_server.Domain;
-using sparkly_server.Services.Users;
+using sparkly_server.Domain.Users;
-namespace sparkly_server.Services.UserServices
+namespace sparkly_server.Services.Users
{
public class UserService : IUserService
{