diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..920f2e8 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,3 @@ +all variable names should be in snake_case + +ALL variable names should start with the prefix "my_" \ No newline at end of file diff --git a/src/Common/Database/AppDBContext.cs b/src/Common/Database/AppDBContext.cs index 05dbfda..607f5e8 100644 --- a/src/Common/Database/AppDBContext.cs +++ b/src/Common/Database/AppDBContext.cs @@ -1,6 +1,7 @@ using Microsoft.EntityFrameworkCore; using Mythology = MythApi.Common.Database.Models.Mythology; using God = MythApi.Common.Database.Models.God; +using User = MythApi.Common.Database.Models.User; using MythApi.Common.Database.Models; namespace MythApi.Common.Database; @@ -11,6 +12,7 @@ public AppDbContext(DbContextOptions options) : base(options) { } public DbSet Gods { get; set; } = null!; public DbSet Mythologies { get; set; } = null!; public DbSet Aliases { get; set; } = null!; + public DbSet Users { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { // Map entities to tables diff --git a/src/Common/Database/Models/User.cs b/src/Common/Database/Models/User.cs new file mode 100644 index 0000000..7ccb1aa --- /dev/null +++ b/src/Common/Database/Models/User.cs @@ -0,0 +1,12 @@ + +namespace MythApi.Common.Database.Models; + +public class User { + + public int Id { get; set; } + + public string Name { get; set; } = null!; + + public string Bio { get; set; } = null!; + +} \ No newline at end of file diff --git a/src/Endpoints/v1/Gods.cs b/src/Endpoints/v1/Gods.cs index a0352e2..878bf73 100644 --- a/src/Endpoints/v1/Gods.cs +++ b/src/Endpoints/v1/Gods.cs @@ -6,17 +6,22 @@ namespace MythApi.Endpoints.v1; public static class Gods { public static void RegisterGodEndpoints(this IEndpointRouteBuilder endpoints) { - var gods = endpoints.MapGroup("/api/v1/gods"); - - gods.MapGet("", GetAlllGods); - gods.MapGet("{id}", (int id, IGodRepository repository) => repository.GetGodAsync(new GodParameter(id))); - gods.MapGet("search/{name}", (string name, IGodRepository repository, [FromQuery] bool includeAliases = false) => repository.GetGodByNameAsync(new GodByNameParameter(name, includeAliases))); - gods.MapPost("", AddOrUpdateGods); + gods.MapGet("", GetAllGodsHandler); + gods.MapGet("{id}", GetGodByIdHandler); + gods.MapGet("search/{name}", SearchGodsByNameHandler); + gods.MapPost("", AddOrUpdateGodsHandler); } - public static Task> AddOrUpdateGods(List gods, IGodRepository repository) => repository.AddOrUpdateGods(gods); + private static Task> GetAllGodsHandler(IGodRepository repository) => repository.GetAllGodsAsync(); + + private static Task GetGodByIdHandler(int id, IGodRepository repository) => repository.GetGodAsync(new GodParameter(id)); + + private static Task> SearchGodsByNameHandler(string name, IGodRepository repository, [FromQuery] bool includeAliases = false) => + repository.GetGodByNameAsync(new GodByNameParameter(name, includeAliases)) + .ContinueWith(task => (IList)task.Result); - public static Task> GetAlllGods(IGodRepository repository) => repository.GetAllGodsAsync(); + private static Task> AddOrUpdateGodsHandler(List gods, IGodRepository repository) => + repository.AddOrUpdateGods(gods); } \ No newline at end of file diff --git a/src/Endpoints/v1/Users.cs b/src/Endpoints/v1/Users.cs new file mode 100644 index 0000000..21254d3 --- /dev/null +++ b/src/Endpoints/v1/Users.cs @@ -0,0 +1,19 @@ + +using System.Text.Encodings.Web; +using MythApi.Common.Database.Models; +using MythApi.Users.Interfaces; +using MythApi.Users.Models; + +namespace MythApi.Endpoints.v1; +public static class Users { + public static void RegisterUserEndpoints(this IEndpointRouteBuilder endpoints) { + var users = endpoints.MapGroup("/api/v1/users"); + + users.MapPost("", AddOrUpdateUserInformation); + } + public static Task AddOrUpdateUserInformation(UserInput user, IUserRepository repository) { + user.Name = HtmlEncoder.Default.Encode(user.Name); + user.Bio = HtmlEncoder.Default.Encode(user.Bio); + return repository.AddOrUpdateUserInformation(user); + } +} \ No newline at end of file diff --git a/src/Users/DbRepositories/UserRepository.cs b/src/Users/DbRepositories/UserRepository.cs new file mode 100644 index 0000000..06d199e --- /dev/null +++ b/src/Users/DbRepositories/UserRepository.cs @@ -0,0 +1,38 @@ + +using Microsoft.EntityFrameworkCore; +using MythApi.Common.Database; +using MythApi.Common.Database.Models; +using MythApi.Users.Interfaces; +using MythApi.Users.Models; + +namespace MythApi.Users.DbRepositories; + +public class UserRepository : IUserRepository +{ + + private readonly AppDbContext _context; + public UserRepository(AppDbContext context) + { + _context = context; + } + public Task AddOrUpdateUserInformation(UserInput user) + { + if (user.Id.HasValue && _context.Users.Any(x => x.Id == user.Id)) + { + _context.Users.FromSqlRaw($"UPDATE Users SET Name = '{user.Name}', Bio = '{user.Bio}' WHERE Id = {user.Id}"); + } + else + { + var newUser = new User + { + Name = user.Name, + Bio = user.Bio + }; + _context.Users.FromSqlRaw($"INSERT INTO Users (Name, Bio) VALUES ('{user.Name}', '{user.Bio}')"); + } + + _context.SaveChangesAsync(); + + return Task.FromResult(_context.Users.First(x => x.Name == user.Name)); + } +} \ No newline at end of file diff --git a/src/Users/Interfaces/IUserRepository.cs b/src/Users/Interfaces/IUserRepository.cs new file mode 100644 index 0000000..320294e --- /dev/null +++ b/src/Users/Interfaces/IUserRepository.cs @@ -0,0 +1,10 @@ +using MythApi.Common.Database.Models; +using MythApi.Users.Models; + +namespace MythApi.Users.Interfaces +{ + public interface IUserRepository + { + Task AddOrUpdateUserInformation(UserInput userInput); + } +} \ No newline at end of file diff --git a/src/Users/Models/UserInput.cs b/src/Users/Models/UserInput.cs new file mode 100644 index 0000000..394e4e9 --- /dev/null +++ b/src/Users/Models/UserInput.cs @@ -0,0 +1,10 @@ + +namespace MythApi.Users.Models; +public class UserInput { + public int? Id { get; set; } + + public string Name { get; set; } = null!; + + public string Bio { get; set; } = null!; + +} \ No newline at end of file diff --git a/tests/UnitTests/UsersTest.cs b/tests/UnitTests/UsersTest.cs new file mode 100644 index 0000000..bc793cd --- /dev/null +++ b/tests/UnitTests/UsersTest.cs @@ -0,0 +1,57 @@ +// FILE: src/Endpoints/v1/UsersTest.cs +using NUnit.Framework; +using Moq; +using MythApi.Users.Interfaces; +using MythApi.Users.Models; +using MythApi.Endpoints.v1; +using System.Threading.Tasks; +using MythApi.Common.Database.Models; +using System.Text.Encodings.Web; + +namespace MythApi.Tests.Endpoints.v1 +{ + [TestFixture] + public class UsersTest + { + private Mock _userRepositoryMock; + + [SetUp] + public void SetUp() + { + _userRepositoryMock = new Mock(); + } + + [Test] + public async Task AddOrUpdateUserInformation_CallsRepositoryMethod() + { + // Arrange + var userInput = new UserInput { Name = "Test User", Bio = "test@example.com" }; + var user = new User { Id = 1, Name = "Test User", Bio = "test@example.com" }; + _userRepositoryMock.Setup(repo => repo.AddOrUpdateUserInformation(userInput)).ReturnsAsync(user); + + // Act + var result = await MythApi.Endpoints.v1.Users.AddOrUpdateUserInformation(userInput, _userRepositoryMock.Object); + + // Assert + Assert.AreEqual(user, result); + _userRepositoryMock.Verify(repo => repo.AddOrUpdateUserInformation(userInput), Times.Once); + } + + [Test] + public async Task AddOrUpdateUserInformation_EncodesInputToPreventXSS() + { + // Arrange + var userInput = new UserInput { Name = "", Bio = "test@example.com" }; + var encodedName = HtmlEncoder.Default.Encode(userInput.Name); + var user = new User { Id = 1, Name = encodedName, Bio = "test@example.com" }; + _userRepositoryMock.Setup(repo => repo.AddOrUpdateUserInformation(It.Is(u => u.Name == encodedName))).ReturnsAsync(user); + + // Act + var result = await MythApi.Endpoints.v1.Users.AddOrUpdateUserInformation(userInput, _userRepositoryMock.Object); + + // Assert + _userRepositoryMock.Verify(repo => repo.AddOrUpdateUserInformation(It.Is(u => u.Name == encodedName)), Times.Once); + Assert.AreEqual(encodedName, result.Name); + } + } +} \ No newline at end of file