From 1bf4ed621898c998510eb9d9b1d471489f761dc4 Mon Sep 17 00:00:00 2001 From: Josh Date: Mon, 15 May 2023 15:00:33 -0500 Subject: [PATCH] Add more unit tests (#757) * Reorganize tests into unit/integration pattern * Make DbSets virtual so they can be overridden by tests * Add MessageControllerTests * Implement DigestMiddlewareTests * Refactor SMTPHelper to follow DI pattern which allows for mocking in unit tests. * Fix MailQueueService service registration and shutdown * Implement tests for Status and StatisticsController and reorganize tests * Start working on UserControllerTests * Start refactoring tests to use In-Memory EF provider * Refactor integration tests to reset the database every time Change default unit testing database credentials * Update credentials to use default root with different passwords * Throw exception when integration db is not available instead of falling back to in-memory * Evaluate DbConnected every time * Remove default DbContext constructor * Setup DbContexts with options builder * Convert remaining Moq DbContexts to InMemory ones * Add more tests and use Assert.IsType for testing status code * Add collection attribute to LighthouseServerTest * Remove unused directives and calculate digest in tests * Fix digest calculation in tests * Add test database call * Clear rooms after each test * Fix CommentControllerTests.cs * Disable test parallelization for gameserver tests * Fix failing tests Fix SlotTests Make CreateUser actually add user to database Fix dbConnected Lazy and change expected status codes Properly Remove fragment from url for digest calculation Fix digest calculation for regular requests [skip ci] Remove unused directive Don't use Database CreateUser function Get rid of userId argument for generating random user Rewrite logic for generating random users Fix integration tests * Implement changes from self-code review * Fix registration tests * Replace MailQueueService usages with IMailService --- .github/workflows/ci.yml | 4 +- .../Startup/ApiStartup.cs | 8 +- .../Controllers/CommentController.cs | 2 +- .../Controllers/MessageController.cs | 11 +- .../Middlewares/DigestMiddleware.cs | 2 +- .../Startup/GameServerStartup.cs | 16 +- .../Types/Categories/CategoryHelper.cs | 2 +- .../Email/SendVerificationEmailPage.cshtml.cs | 11 +- .../Login/PasswordResetRequestForm.cshtml.cs | 27 +- .../Pages/Login/RegisterForm.cshtml.cs | 7 +- .../Pages/Partials/CommentsPartial.cshtml | 2 +- .../Partials/Links/UserLinkPartial.cshtml | 2 +- .../Partials/ModerationCasePartial.cshtml | 2 +- .../Pages/Partials/SlotCardPartial.cshtml | 2 +- .../Pages/Partials/UserCardPartial.cshtml | 2 +- .../Pages/SlotPage.cshtml | 2 +- .../Startup/WebsiteStartup.cs | 16 +- .../AssemblyInfo.cs | 2 +- .../AuthenticationTests.cs | 53 ++- .../{Tests => Integration}/DatabaseTests.cs | 13 +- .../{Tests => Integration}/LoginTests.cs | 92 ++-- .../Integration/MatchTests.cs | 69 +++ .../{Tests => Integration}/SlotTests.cs | 45 +- .../{Tests => Integration}/UploadTests.cs | 56 ++- .../Tests/MatchTests.cs | 64 --- .../Controllers/CommentControllerTests.cs | 231 ++++++++++ .../Controllers/MessageControllerTests.cs | 316 +++++++++++++ .../Controllers/StatisticsControllerTests.cs | 192 ++++++++ .../Unit/Controllers/StatusControllerTests.cs | 23 + .../Unit/Controllers/UserControllerTests.cs | 202 ++++++++ .../Unit/Middlewares/DigestMiddlewareTests.cs | 435 ++++++++++++++++++ .../SetLastContactMiddlewareTests.cs | 150 ++++++ .../{Tests => Integration}/AdminTests.cs | 19 +- .../AuthenticationTests.cs | 21 +- .../LighthouseWebTest.cs | 20 +- .../{Tests => Integration}/RegisterTests.cs | 37 +- .../Helpers/IntegrationHelper.cs | 41 ++ ProjectLighthouse.Tests/Helpers/MockHelper.cs | 144 ++++++ .../DatabaseFactAttribute.cs | 4 +- .../{ => Integration}/LighthouseServerTest.cs | 85 ++-- .../SerializationDependencyTests.cs | 3 +- .../Serialization/SerializationTests.cs | 3 +- .../ProjectLighthouse.Tests.csproj | 2 + .../Unit/EmailCooldownTests.cs | 97 ++++ .../{Tests => Unit}/FileTypeTests.cs | 3 +- .../{Tests => Unit}/LocationTests.cs | 3 +- .../{Tests => Unit}/ResourceTests.cs | 3 +- .../Commands/CreateAPIKeyCommand.cs | 2 +- .../Maintenance/Commands/CreateUserCommand.cs | 2 +- .../Maintenance/Commands/DeleteUserCommand.cs | 2 +- .../Maintenance/Commands/RenameUserCommand.cs | 2 +- .../Commands/ResetPasswordCommand.cs | 2 +- .../Commands/WipeTokensForUserCommand.cs | 2 +- .../Maintenance/MaintenanceHelper.cs | 2 +- .../CleanupBrokenPhotosMaintenanceJob.cs | 2 +- .../DeleteAllTokensMaintenanceJob.cs | 2 +- .../Administration/RepeatingTaskHandler.cs | 2 +- .../Configuration/ServerStatics.cs | 2 +- .../Database/DatabaseContext.Utils.cs | 7 - ProjectLighthouse/Database/DatabaseContext.cs | 12 +- .../Extensions/RoomExtensions.cs | 2 +- ProjectLighthouse/Helpers/EmailHelper.cs | 80 +++- ProjectLighthouse/Helpers/RoomHelper.cs | 6 +- .../MailQueueService.cs} | 53 ++- ProjectLighthouse/Mail/NullMailService.cs | 12 + ProjectLighthouse/Mail/SmtpMailSender.cs | 20 + ProjectLighthouse/StartupTasks.cs | 2 +- .../Types/Entities/Level/SlotEntity.cs | 14 +- .../Types/Entities/Profile/CommentEntity.cs | 4 +- ProjectLighthouse/Types/Mail/IMailSender.cs | 8 + ProjectLighthouse/Types/Mail/IMailService.cs | 9 + 71 files changed, 2419 insertions(+), 378 deletions(-) rename ProjectLighthouse.Tests.GameApiTests/{Tests => Integration}/AuthenticationTests.cs (53%) rename ProjectLighthouse.Tests.GameApiTests/{Tests => Integration}/DatabaseTests.cs (70%) rename ProjectLighthouse.Tests.GameApiTests/{Tests => Integration}/LoginTests.cs (53%) create mode 100644 ProjectLighthouse.Tests.GameApiTests/Integration/MatchTests.cs rename ProjectLighthouse.Tests.GameApiTests/{Tests => Integration}/SlotTests.cs (53%) rename ProjectLighthouse.Tests.GameApiTests/{Tests => Integration}/UploadTests.cs (59%) delete mode 100644 ProjectLighthouse.Tests.GameApiTests/Tests/MatchTests.cs create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/CommentControllerTests.cs create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatusControllerTests.cs create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/DigestMiddlewareTests.cs create mode 100644 ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/SetLastContactMiddlewareTests.cs rename ProjectLighthouse.Tests.WebsiteTests/{Tests => Integration}/AdminTests.cs (80%) rename ProjectLighthouse.Tests.WebsiteTests/{Tests => Integration}/AuthenticationTests.cs (86%) rename ProjectLighthouse.Tests.WebsiteTests/{Tests => Integration}/LighthouseWebTest.cs (59%) rename ProjectLighthouse.Tests.WebsiteTests/{Tests => Integration}/RegisterTests.cs (67%) create mode 100644 ProjectLighthouse.Tests/Helpers/IntegrationHelper.cs create mode 100644 ProjectLighthouse.Tests/Helpers/MockHelper.cs rename ProjectLighthouse.Tests/{ => Integration}/DatabaseFactAttribute.cs (81%) rename ProjectLighthouse.Tests/{ => Integration}/LighthouseServerTest.cs (52%) rename ProjectLighthouse.Tests/{Tests => Integration}/Serialization/SerializationDependencyTests.cs (97%) rename ProjectLighthouse.Tests/{Tests => Integration}/Serialization/SerializationTests.cs (98%) create mode 100644 ProjectLighthouse.Tests/Unit/EmailCooldownTests.cs rename ProjectLighthouse.Tests/{Tests => Unit}/FileTypeTests.cs (95%) rename ProjectLighthouse.Tests/{Tests => Unit}/LocationTests.cs (97%) rename ProjectLighthouse.Tests/{Tests => Unit}/ResourceTests.cs (97%) rename ProjectLighthouse/{Helpers/SMTPHelper.cs => Mail/MailQueueService.cs} (59%) create mode 100644 ProjectLighthouse/Mail/NullMailService.cs create mode 100644 ProjectLighthouse/Mail/SmtpMailSender.cs create mode 100644 ProjectLighthouse/Types/Mail/IMailSender.cs create mode 100644 ProjectLighthouse/Types/Mail/IMailService.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a1b3db9..0c962d6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,9 @@ jobs: - { prettyName: Linux, fullName: ubuntu-latest, database: true, webTest: true } timeout-minutes: 5 env: - DB_DATABASE: lighthouse + DB_DATABASE: lighthouse_tests DB_USER: root - DB_PASSWORD: lighthouse + DB_PASSWORD: lighthouse_tests steps: - name: Checkout uses: actions/checkout@v3 diff --git a/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs b/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs index fd42fdc1..a070862c 100644 --- a/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs +++ b/ProjectLighthouse.Servers.API/Startup/ApiStartup.cs @@ -1,6 +1,8 @@ +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Middlewares; using LBPUnion.ProjectLighthouse.Serialization; +using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; namespace LBPUnion.ProjectLighthouse.Servers.API.Startup; @@ -26,7 +28,11 @@ public class ApiStartup } ); - services.AddDbContext(); + services.AddDbContext(builder => + { + builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, + MySqlServerVersion.LatestSupportedServerVersion); + }); services.AddSwaggerGen ( diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs index 1a5b98ce..be54b098 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/CommentController.cs @@ -96,7 +96,7 @@ public class CommentController : ControllerBase GameTokenEntity token = this.GetToken(); GameComment? comment = await this.DeserializeBody(); - if (comment == null) return this.BadRequest(); + if (comment?.Message == null) return this.BadRequest(); if ((slotId == 0 || SlotHelper.IsTypeInvalid(slotType)) == (username == null)) return this.BadRequest(); diff --git a/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs b/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs index 9043c5a7..03c9877d 100644 --- a/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs +++ b/ProjectLighthouse.Servers.GameServer/Controllers/MessageController.cs @@ -7,6 +7,7 @@ using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Logging; +using LBPUnion.ProjectLighthouse.Types.Mail; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -79,13 +80,13 @@ along with this program. If not, see ."; /// The response sent is the text that will appear in-game. /// [HttpPost("filter")] - public async Task Filter() + public async Task Filter(IMailService mailService) { GameTokenEntity token = this.GetToken(); string message = await this.ReadBodyAsync(); - if (message.StartsWith("/setemail ")) + if (message.StartsWith("/setemail ") && ServerConfiguration.Instance.Mail.MailEnabled) { string email = message[(message.IndexOf(" ", StringComparison.Ordinal)+1)..]; if (!SanitizationHelper.IsValidEmail(email)) return this.Ok(); @@ -96,7 +97,7 @@ along with this program. If not, see ."; if (user == null || user.EmailAddressVerified) return this.Ok(); user.EmailAddress = email; - await SMTPHelper.SendVerificationEmail(this.database, user); + await SMTPHelper.SendVerificationEmail(this.database, mailService, user); return this.Ok(); } @@ -105,8 +106,8 @@ along with this program. If not, see ."; string username = await this.database.UsernameFromGameToken(token); - if (ServerConfiguration.Instance.LogChatFiltering) - Logger.Info($"{username}: {message} / {filteredText}", LogArea.Filter); + if (ServerConfiguration.Instance.LogChatFiltering) + Logger.Info($"{username}: {message} / {filteredText}", LogArea.Filter); return this.Ok(filteredText); } diff --git a/ProjectLighthouse.Servers.GameServer/Middlewares/DigestMiddleware.cs b/ProjectLighthouse.Servers.GameServer/Middlewares/DigestMiddleware.cs index 1b59d801..5bb9e194 100644 --- a/ProjectLighthouse.Servers.GameServer/Middlewares/DigestMiddleware.cs +++ b/ProjectLighthouse.Servers.GameServer/Middlewares/DigestMiddleware.cs @@ -85,7 +85,7 @@ public class DigestMiddleware : Middleware #if !DEBUG // The game doesn't start sending digests until after the announcement so if it's not one of those requests // and it doesn't include a digest we need to reject the request - else if (!ServerStatics.IsUnitTesting && !exemptPathList.Contains(strippedPath)) + else if (!exemptPathList.Contains(strippedPath)) { context.Response.StatusCode = 403; return; diff --git a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs index cd2c9cb0..2b73bdfc 100644 --- a/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs +++ b/ProjectLighthouse.Servers.GameServer/Startup/GameServerStartup.cs @@ -2,13 +2,16 @@ using System.Net; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Logging; +using LBPUnion.ProjectLighthouse.Mail; using LBPUnion.ProjectLighthouse.Middlewares; using LBPUnion.ProjectLighthouse.Serialization; using LBPUnion.ProjectLighthouse.Servers.GameServer.Middlewares; using LBPUnion.ProjectLighthouse.Types.Logging; +using LBPUnion.ProjectLighthouse.Types.Mail; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; @@ -19,7 +22,7 @@ public class GameServerStartup this.Configuration = configuration; } - public IConfiguration Configuration { get; } + private IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) @@ -49,7 +52,16 @@ public class GameServerStartup } ); - services.AddDbContext(); + services.AddDbContext(builder => + { + builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, + MySqlServerVersion.LatestSupportedServerVersion); + }); + + IMailService mailService = ServerConfiguration.Instance.Mail.MailEnabled + ? new MailQueueService(new SmtpMailSender()) + : new NullMailService(); + services.AddSingleton(mailService); services.Configure ( diff --git a/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryHelper.cs b/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryHelper.cs index d67b196e..74c40005 100644 --- a/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryHelper.cs +++ b/ProjectLighthouse.Servers.GameServer/Types/Categories/CategoryHelper.cs @@ -19,7 +19,7 @@ public static class CategoryHelper Categories.Add(new HeartedCategory()); Categories.Add(new LuckyDipCategory()); - using DatabaseContext database = new(); + using DatabaseContext database = DatabaseContext.CreateNewInstance(); foreach (DatabaseCategoryEntity category in database.CustomCategories) Categories.Add(new CustomCategory(category)); } } \ No newline at end of file diff --git a/ProjectLighthouse.Servers.Website/Pages/Email/SendVerificationEmailPage.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Email/SendVerificationEmailPage.cshtml.cs index 96066cb2..e95e1abd 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Email/SendVerificationEmailPage.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Email/SendVerificationEmailPage.cshtml.cs @@ -4,14 +4,19 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Mail; using Microsoft.AspNetCore.Mvc; namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Email; public class SendVerificationEmailPage : BaseLayout { - public SendVerificationEmailPage(DatabaseContext database) : base(database) - {} + public readonly IMailService Mail; + + public SendVerificationEmailPage(DatabaseContext database, IMailService mail) : base(database) + { + this.Mail = mail; + } public bool Success { get; set; } @@ -24,7 +29,7 @@ public class SendVerificationEmailPage : BaseLayout if (user.EmailAddressVerified) return this.Redirect("/"); - this.Success = await SMTPHelper.SendVerificationEmail(this.Database, user); + this.Success = await SMTPHelper.SendVerificationEmail(this.Database, this.Mail, user); return this.Page(); } diff --git a/ProjectLighthouse.Servers.Website/Pages/Login/PasswordResetRequestForm.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Login/PasswordResetRequestForm.cshtml.cs index bb7cf9ea..9d9aeff7 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Login/PasswordResetRequestForm.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Login/PasswordResetRequestForm.cshtml.cs @@ -4,7 +4,7 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; -using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Mail; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -12,18 +12,20 @@ namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login; public class PasswordResetRequestForm : BaseLayout { + public IMailService Mail; public string? Error { get; private set; } public string? Status { get; private set; } - public PasswordResetRequestForm(DatabaseContext database) : base(database) - { } + public PasswordResetRequestForm(DatabaseContext database, IMailService mail) : base(database) + { + this.Mail = mail; + } [UsedImplicitly] public async Task OnPost(string email) { - if (!ServerConfiguration.Instance.Mail.MailEnabled) { this.Error = "Email is not configured on this server, so password resets cannot be issued. Please contact your instance administrator for more details."; @@ -51,22 +53,7 @@ public class PasswordResetRequestForm : BaseLayout return this.Page(); } - PasswordResetTokenEntity token = new() - { - Created = DateTime.Now, - UserId = user.UserId, - ResetToken = CryptoHelper.GenerateAuthToken(), - }; - - string messageBody = $"Hello, {user.Username}.\n\n" + - "A request to reset your account's password was issued. If this wasn't you, this can probably be ignored.\n\n" + - $"If this was you, your {ServerConfiguration.Instance.Customization.ServerName} password can be reset at the following link:\n" + - $"{ServerConfiguration.Instance.ExternalUrl}/passwordReset?token={token.ResetToken}"; - - SMTPHelper.SendEmail(user.EmailAddress, $"Project Lighthouse Password Reset Request for {user.Username}", messageBody); - - this.Database.PasswordResetTokens.Add(token); - await this.Database.SaveChangesAsync(); + await SMTPHelper.SendPasswordResetEmail(this.Database, this.Mail, user); this.Status = $"A password reset request has been sent to the email {email}. " + "If you do not receive an email verify that you have entered the correct email address"; diff --git a/ProjectLighthouse.Servers.Website/Pages/Login/RegisterForm.cshtml.cs b/ProjectLighthouse.Servers.Website/Pages/Login/RegisterForm.cshtml.cs index 00627e05..b15b42a3 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Login/RegisterForm.cshtml.cs +++ b/ProjectLighthouse.Servers.Website/Pages/Login/RegisterForm.cshtml.cs @@ -8,6 +8,7 @@ using LBPUnion.ProjectLighthouse.Servers.Website.Captcha; using LBPUnion.ProjectLighthouse.Servers.Website.Pages.Layouts; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Mail; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -15,10 +16,12 @@ namespace LBPUnion.ProjectLighthouse.Servers.Website.Pages.Login; public class RegisterForm : BaseLayout { + public readonly IMailService Mail; private readonly ICaptchaService captchaService; - public RegisterForm(DatabaseContext database, ICaptchaService captchaService) : base(database) + public RegisterForm(DatabaseContext database, IMailService mail, ICaptchaService captchaService) : base(database) { + this.Mail = mail; this.captchaService = captchaService; } @@ -80,6 +83,8 @@ public class RegisterForm : BaseLayout UserEntity user = await this.Database.CreateUser(username, CryptoHelper.BCryptHash(password), emailAddress); + if(ServerConfiguration.Instance.Mail.MailEnabled) SMTPHelper.SendRegistrationEmail(this.Mail, user); + WebTokenEntity webToken = new() { UserId = user.UserId, diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml index 122df763..3f20a152 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/CommentsPartial.cshtml @@ -50,7 +50,7 @@ int yourThumb = commentAndReaction.Value?.Rating ?? 0; DateTimeOffset timestamp = DateTimeOffset.FromUnixTimeSeconds(comment.Timestamp / 1000).ToLocalTime(); StringWriter messageWriter = new(); - HttpUtility.HtmlDecode(comment.getComment(), messageWriter); + HttpUtility.HtmlDecode(comment.GetCommentMessage(), messageWriter); string decodedMessage = messageWriter.ToString(); string? url = Url.RouteUrl(ViewContext.RouteData.Values); diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml index 49903bea..2dedb34f 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/Links/UserLinkPartial.cshtml @@ -7,7 +7,7 @@ string language = (string?)ViewData["Language"] ?? LocalizationManager.DefaultLang; string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; bool includeStatus = (bool?)ViewData["IncludeStatus"] ?? false; - await using DatabaseContext database = new(); + await using DatabaseContext database = DatabaseContext.CreateNewInstance(); string userStatus = includeStatus ? Model.GetStatus(database).ToTranslatedString(language, timeZone) : ""; } diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/ModerationCasePartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/ModerationCasePartial.cshtml index 28b1defc..e047b4cb 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/ModerationCasePartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/ModerationCasePartial.cshtml @@ -6,7 +6,7 @@ @model LBPUnion.ProjectLighthouse.Types.Entities.Moderation.ModerationCaseEntity @{ - DatabaseContext database = new(); + DatabaseContext database = DatabaseContext.CreateNewInstance(); string color = "blue"; string timeZone = (string?)ViewData["TimeZone"] ?? TimeZoneInfo.Local.Id; diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml index a5c0bfa8..23f0c039 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/SlotCardPartial.cshtml @@ -11,7 +11,7 @@ @{ UserEntity? user = (UserEntity?)ViewData["User"]; - await using DatabaseContext database = new(); + await using DatabaseContext database = DatabaseContext.CreateNewInstance(); string slotName = HttpUtility.HtmlDecode(string.IsNullOrEmpty(Model!.Name) ? "Unnamed Level" : Model.Name); diff --git a/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml b/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml index 8591d3c2..b06a3359 100644 --- a/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/Partials/UserCardPartial.cshtml @@ -42,7 +42,7 @@ } @{ - await using DatabaseContext context = new(); + await using DatabaseContext context = DatabaseContext.CreateNewInstance(); int hearts = Model.GetHeartCount(context); int comments = Model.GetCommentCount(context); diff --git a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml index f4907a29..8d947cb9 100644 --- a/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml +++ b/ProjectLighthouse.Servers.Website/Pages/SlotPage.cshtml @@ -55,7 +55,7 @@ string[] authorLabels; if (Model.Slot?.GameVersion == GameVersion.LittleBigPlanet1) { - authorLabels = Model.Slot.LevelTags(new DatabaseContext()); + authorLabels = Model.Slot.LevelTags(DatabaseContext.CreateNewInstance()); } else { diff --git a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs index 88789749..69daff13 100644 --- a/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs +++ b/ProjectLighthouse.Servers.Website/Startup/WebsiteStartup.cs @@ -4,11 +4,14 @@ using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Configuration.ConfigurationCategories; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Localization; +using LBPUnion.ProjectLighthouse.Mail; using LBPUnion.ProjectLighthouse.Middlewares; using LBPUnion.ProjectLighthouse.Servers.Website.Captcha; using LBPUnion.ProjectLighthouse.Servers.Website.Middlewares; +using LBPUnion.ProjectLighthouse.Types.Mail; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Localization; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.FileProviders; #if !DEBUG @@ -26,7 +29,7 @@ public class WebsiteStartup this.Configuration = configuration; } - public IConfiguration Configuration { get; } + private IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) @@ -45,7 +48,16 @@ public class WebsiteStartup services.AddRazorPages().WithRazorPagesAtContentRoot(); #endif - services.AddDbContext(); + services.AddDbContext(builder => + { + builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, + MySqlServerVersion.LatestSupportedServerVersion); + }); + + IMailService mailService = ServerConfiguration.Instance.Mail.MailEnabled + ? new MailQueueService(new SmtpMailSender()) + : new NullMailService(); + services.AddSingleton(mailService); services.AddHttpClient("CaptchaAPI", client => diff --git a/ProjectLighthouse.Tests.GameApiTests/AssemblyInfo.cs b/ProjectLighthouse.Tests.GameApiTests/AssemblyInfo.cs index 41a898e3..7db84979 100644 --- a/ProjectLighthouse.Tests.GameApiTests/AssemblyInfo.cs +++ b/ProjectLighthouse.Tests.GameApiTests/AssemblyInfo.cs @@ -1,3 +1,3 @@ -using Xunit; +using Xunit; [assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs b/ProjectLighthouse.Tests.GameApiTests/Integration/AuthenticationTests.cs similarity index 53% rename from ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs rename to ProjectLighthouse.Tests.GameApiTests/Integration/AuthenticationTests.cs index 991d4dc3..bc705cf9 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Tests/AuthenticationTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Integration/AuthenticationTests.cs @@ -3,35 +3,48 @@ using System.Net.Http; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; -using LBPUnion.ProjectLighthouse.Tests; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Tests.Integration; using LBPUnion.ProjectLighthouse.Types.Users; using Xunit; -namespace ProjectLighthouse.Tests.GameApiTests.Tests; +namespace ProjectLighthouse.Tests.GameApiTests.Integration; +[Trait("Category", "Integration")] public class AuthenticationTests : LighthouseServerTest { [Fact] public async Task ShouldReturnErrorOnNoPostData() { + await IntegrationHelper.GetIntegrationDatabase(); + HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", null!); - Assert.False(response.IsSuccessStatusCode); - Assert.True(response.StatusCode == HttpStatusCode.BadRequest); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.BadRequest; + + Assert.Equal(expectedStatusCode, response.StatusCode); } - [DatabaseFact] - public async Task ShouldReturnWithValidData() + [Fact] + public async Task Login_ShouldReturnWithValidData() { + await IntegrationHelper.GetIntegrationDatabase(); + HttpResponseMessage response = await this.AuthenticateResponse(); - Assert.True(response.IsSuccessStatusCode); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, response.StatusCode); string responseContent = await response.Content.ReadAsStringAsync(); Assert.Contains("MM_AUTH=", responseContent); Assert.Contains(VersionHelper.EnvVer, responseContent); } - [DatabaseFact] - public async Task CanSerializeBack() + [Fact] + public async Task Login_CanSerializeBack() { + await IntegrationHelper.GetIntegrationDatabase(); + LoginResult loginResult = await this.Authenticate(); Assert.NotNull(loginResult); @@ -42,22 +55,30 @@ public class AuthenticationTests : LighthouseServerTest Assert.Equal(VersionHelper.EnvVer, loginResult.ServerBrand); } - [DatabaseFact] - public async Task CanUseToken() + [Fact] + public async Task Login_CanUseToken() { + await IntegrationHelper.GetIntegrationDatabase(); + LoginResult loginResult = await this.Authenticate(); HttpResponseMessage response = await this.AuthenticatedRequest("/LITTLEBIGPLANETPS3_XML/enterLevel/420", loginResult.AuthTicket); await response.Content.ReadAsStringAsync(); - Assert.False(response.StatusCode == HttpStatusCode.Forbidden); + const HttpStatusCode expectedStatusCode = HttpStatusCode.NotFound; + + Assert.Equal(expectedStatusCode, response.StatusCode); } - [DatabaseFact] - public async Task ShouldReturnForbiddenWhenNotAuthenticated() + [Fact] + public async Task Login_ShouldReturnForbiddenWhenNotAuthenticated() { + await IntegrationHelper.GetIntegrationDatabase(); + HttpResponseMessage response = await this.Client.GetAsync("/LITTLEBIGPLANETPS3_XML/announce"); - Assert.False(response.IsSuccessStatusCode); - Assert.True(response.StatusCode == HttpStatusCode.Forbidden); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.Forbidden; + + Assert.Equal(expectedStatusCode, response.StatusCode); } } \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Tests/DatabaseTests.cs b/ProjectLighthouse.Tests.GameApiTests/Integration/DatabaseTests.cs similarity index 70% rename from ProjectLighthouse.Tests.GameApiTests/Tests/DatabaseTests.cs rename to ProjectLighthouse.Tests.GameApiTests/Integration/DatabaseTests.cs index 6292426f..cc8ddc27 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Tests/DatabaseTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Integration/DatabaseTests.cs @@ -3,18 +3,21 @@ using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; -using LBPUnion.ProjectLighthouse.Tests; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Tests.Integration; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using Xunit; -namespace ProjectLighthouse.Tests.GameApiTests.Tests; +namespace ProjectLighthouse.Tests.GameApiTests.Integration; +[Trait("Category", "Integration")] public class DatabaseTests : LighthouseServerTest { - [DatabaseFact] + [Fact] public async Task CanCreateUserTwice() { - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); + int rand = new Random().Next(); UserEntity userA = await database.CreateUser("unitTestUser" + rand, CryptoHelper.GenerateAuthToken()); @@ -22,7 +25,5 @@ public class DatabaseTests : LighthouseServerTest Assert.NotNull(userA); Assert.NotNull(userB); - - await database.RemoveUser(userA); // Only remove userA since userA and userB are the same user } } \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Tests/LoginTests.cs b/ProjectLighthouse.Tests.GameApiTests/Integration/LoginTests.cs similarity index 53% rename from ProjectLighthouse.Tests.GameApiTests/Tests/LoginTests.cs rename to ProjectLighthouse.Tests.GameApiTests/Integration/LoginTests.cs index ba659fff..5dcc2db7 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Tests/LoginTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Integration/LoginTests.cs @@ -5,100 +5,118 @@ using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; -using LBPUnion.ProjectLighthouse.Tests; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Tests.Integration; using LBPUnion.ProjectLighthouse.Tickets; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Users; -using Microsoft.EntityFrameworkCore; using Xunit; -namespace ProjectLighthouse.Tests.GameApiTests.Tests; +namespace ProjectLighthouse.Tests.GameApiTests.Integration; +[Trait("Category", "Integration")] public class LoginTests : LighthouseServerTest { [Fact] public async Task ShouldLoginWithGoodTicket() { - string username = await this.CreateRandomUser(); - ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]); + await IntegrationHelper.GetIntegrationDatabase(); + + UserEntity user = await this.CreateRandomUser(); byte[] ticketData = new TicketBuilder() - .SetUsername(username) - .SetUserId(userId) + .SetUsername(user.Username) + .SetUserId((ulong)user.UserId) .Build(); HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData)); - Assert.True(response.IsSuccessStatusCode); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, response.StatusCode); } [Fact] public async Task ShouldNotLoginWithExpiredTicket() { - string username = await this.CreateRandomUser(); - ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]); + await IntegrationHelper.GetIntegrationDatabase(); + + UserEntity user = await this.CreateRandomUser(); byte[] ticketData = new TicketBuilder() - .SetUsername(username) - .SetUserId(userId) + .SetUsername(user.Username) + .SetUserId((ulong)user.UserId) .setExpirationTime((ulong)TimeHelper.TimestampMillis - 1000 * 60) .Build(); HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData)); - Assert.False(response.IsSuccessStatusCode); - Assert.True(response.StatusCode == HttpStatusCode.BadRequest); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.BadRequest; + + Assert.Equal(expectedStatusCode, response.StatusCode); } [Fact] public async Task ShouldNotLoginWithBadTitleId() { - string username = await this.CreateRandomUser(); - ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]); + await IntegrationHelper.GetIntegrationDatabase(); + + UserEntity user = await this.CreateRandomUser(); byte[] ticketData = new TicketBuilder() - .SetUsername(username) - .SetUserId(userId) + .SetUsername(user.Username) + .SetUserId((ulong)user.UserId) .SetTitleId("UP9000-BLUS30079_00") .Build(); HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData)); - Assert.False(response.IsSuccessStatusCode); - Assert.True(response.StatusCode == HttpStatusCode.BadRequest); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.BadRequest; + + Assert.Equal(expectedStatusCode, response.StatusCode); } [Fact] public async Task ShouldNotLoginWithBadSignature() { - string username = await this.CreateRandomUser(); - ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]); + await IntegrationHelper.GetIntegrationDatabase(); + + UserEntity user = await this.CreateRandomUser(); byte[] ticketData = new TicketBuilder() - .SetUsername(username) - .SetUserId(userId) + .SetUsername(user.Username) + .SetUserId((ulong)user.UserId) .Build(); // Create second ticket and replace the first tickets signature with the first. byte[] ticketData2 = new TicketBuilder() - .SetUsername(username) - .SetUserId(userId) + .SetUsername(user.Username) + .SetUserId((ulong)user.UserId) .Build(); Array.Copy(ticketData2, ticketData2.Length - 0x38, ticketData, ticketData.Length - 0x38, 0x38); HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData)); - Assert.False(response.IsSuccessStatusCode); - Assert.True(response.StatusCode == HttpStatusCode.BadRequest); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.BadRequest; + + Assert.Equal(expectedStatusCode, response.StatusCode); } [Fact] public async Task ShouldNotLoginIfBanned() { - string username = await this.CreateRandomUser(); - ulong userId = (ulong)Convert.ToInt32(username["unitTestUser".Length..]); - await using DatabaseContext database = new(); - UserEntity user = await database.Users.FirstAsync(u => u.Username == username); + DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); + + UserEntity user = await this.CreateRandomUser(); + user.PermissionLevel = PermissionLevel.Banned; + + database.Users.Update(user); await database.SaveChangesAsync(); byte[] ticketData = new TicketBuilder() - .SetUsername(username) - .SetUserId(userId) + .SetUsername(user.Username) + .SetUserId((ulong)user.UserId) .Build(); HttpResponseMessage response = await this.Client.PostAsync("/LITTLEBIGPLANETPS3_XML/login", new ByteArrayContent(ticketData)); - Assert.False(response.IsSuccessStatusCode); - Assert.True(response.StatusCode == HttpStatusCode.Forbidden); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.Forbidden; + + Assert.Equal(expectedStatusCode, response.StatusCode); } -} \ No newline at end of file +} diff --git a/ProjectLighthouse.Tests.GameApiTests/Integration/MatchTests.cs b/ProjectLighthouse.Tests.GameApiTests/Integration/MatchTests.cs new file mode 100644 index 00000000..2044d41b --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Integration/MatchTests.cs @@ -0,0 +1,69 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Tests.Integration; +using LBPUnion.ProjectLighthouse.Types.Users; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Integration; + +[Trait("Category", "Integration")] +public class MatchTests : LighthouseServerTest +{ + [Fact] + public async Task Match_ShouldRejectEmptyData() + { + await IntegrationHelper.GetIntegrationDatabase(); + + LoginResult loginResult = await this.Authenticate(); + + HttpResponseMessage result = await this.AuthenticatedUploadDataRequest("/LITTLEBIGPLANETPS3_XML/match", Array.Empty(), loginResult.AuthTicket); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.BadRequest; + + Assert.Equal(expectedStatusCode, result.StatusCode); + } + + [Fact] + public async Task Match_ShouldReturnOk_WithGoodRequest() + { + await IntegrationHelper.GetIntegrationDatabase(); + + LoginResult loginResult = await this.Authenticate(); + + HttpResponseMessage result = await this.AuthenticatedUploadDataRequest + ("/LITTLEBIGPLANETPS3_XML/match", "[UpdateMyPlayerData,[\"Player\":\"1984\"]]"u8.ToArray(), loginResult.AuthTicket); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, result.StatusCode); + } + + [Fact] + public async Task Match_ShouldIncrementPlayerCount() + { + await IntegrationHelper.GetIntegrationDatabase(); + + LoginResult loginResult = await this.Authenticate(); + + await using DatabaseContext database = DatabaseContext.CreateNewInstance(); + + int oldPlayerCount = await StatisticsHelper.RecentMatches(database); + + HttpResponseMessage result = await this.AuthenticatedUploadDataRequest + ("/LITTLEBIGPLANETPS3_XML/match", "[UpdateMyPlayerData,[\"Player\":\"1984\"]]"u8.ToArray(), loginResult.AuthTicket); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, result.StatusCode); + + int playerCount = await StatisticsHelper.RecentMatches(database); + + Assert.Equal(oldPlayerCount + 1, playerCount); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Tests/SlotTests.cs b/ProjectLighthouse.Tests.GameApiTests/Integration/SlotTests.cs similarity index 53% rename from ProjectLighthouse.Tests.GameApiTests/Tests/SlotTests.cs rename to ProjectLighthouse.Tests.GameApiTests/Integration/SlotTests.cs index 2ec62c9b..56e506bc 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Tests/SlotTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Integration/SlotTests.cs @@ -1,32 +1,30 @@ -using System; +using System.Net; using System.Net.Http; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; -using LBPUnion.ProjectLighthouse.Tests; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Tests.Integration; using LBPUnion.ProjectLighthouse.Types.Entities.Level; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Users; using Xunit; -namespace ProjectLighthouse.Tests.GameApiTests.Tests; +namespace ProjectLighthouse.Tests.GameApiTests.Integration; +[Trait("Category", "Integration")] public class SlotTests : LighthouseServerTest { - [DatabaseFact] + [Fact] public async Task ShouldOnlyShowUsersLevels() { - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); - Random r = new(); - - UserEntity userA = await database.CreateUser($"unitTestUser{r.Next()}", CryptoHelper.GenerateAuthToken()); - UserEntity userB = await database.CreateUser($"unitTestUser{r.Next()}", CryptoHelper.GenerateAuthToken()); + UserEntity userA = await this.CreateRandomUser(); + UserEntity userB = await this.CreateRandomUser(); SlotEntity slotA = new() { - Creator = userA, CreatorId = userA.UserId, Name = "slotA", ResourceCollection = "", @@ -34,7 +32,6 @@ public class SlotTests : LighthouseServerTest SlotEntity slotB = new() { - Creator = userB, CreatorId = userB.UserId, Name = "slotB", ResourceCollection = "", @@ -48,31 +45,23 @@ public class SlotTests : LighthouseServerTest LoginResult loginResult = await this.Authenticate(); HttpResponseMessage respMessageA = await this.AuthenticatedRequest - ($"LITTLEBIGPLANETPS3_XML/slots/by?u={userA.Username}&pageStart=1&pageSize=1", loginResult.AuthTicket); + ($"/LITTLEBIGPLANETPS3_XML/slots/by?u={userA.Username}&pageStart=1&pageSize=1", loginResult.AuthTicket); HttpResponseMessage respMessageB = await this.AuthenticatedRequest - ($"LITTLEBIGPLANETPS3_XML/slots/by?u={userB.Username}&pageStart=1&pageSize=1", loginResult.AuthTicket); + ($"/LITTLEBIGPLANETPS3_XML/slots/by?u={userB.Username}&pageStart=1&pageSize=1", loginResult.AuthTicket); - Assert.True(respMessageA.IsSuccessStatusCode); - Assert.True(respMessageB.IsSuccessStatusCode); + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, respMessageA.StatusCode); + Assert.Equal(expectedStatusCode, respMessageB.StatusCode); string respA = await respMessageA.Content.ReadAsStringAsync(); string respB = await respMessageB.Content.ReadAsStringAsync(); - Assert.False(string.IsNullOrEmpty(respA)); - Assert.False(string.IsNullOrEmpty(respB)); + Assert.NotNull(respA); + Assert.NotNull(respB); Assert.NotEqual(respA, respB); Assert.DoesNotContain(respA, "slotB"); Assert.DoesNotContain(respB, "slotA"); - - // Cleanup - - database.Slots.Remove(slotA); - database.Slots.Remove(slotB); - - await database.RemoveUser(userA); - await database.RemoveUser(userB); - - await database.SaveChangesAsync(); } } \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Tests/UploadTests.cs b/ProjectLighthouse.Tests.GameApiTests/Integration/UploadTests.cs similarity index 59% rename from ProjectLighthouse.Tests.GameApiTests/Tests/UploadTests.cs rename to ProjectLighthouse.Tests.GameApiTests/Integration/UploadTests.cs index f1e006c4..9e3c90c1 100644 --- a/ProjectLighthouse.Tests.GameApiTests/Tests/UploadTests.cs +++ b/ProjectLighthouse.Tests.GameApiTests/Integration/UploadTests.cs @@ -4,12 +4,14 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; -using LBPUnion.ProjectLighthouse.Tests; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Tests.Integration; using LBPUnion.ProjectLighthouse.Types.Users; using Xunit; -namespace ProjectLighthouse.Tests.GameApiTests.Tests; +namespace ProjectLighthouse.Tests.GameApiTests.Integration; +[Trait("Category", "Integration")] public class UploadTests : LighthouseServerTest { public UploadTests() @@ -18,53 +20,73 @@ public class UploadTests : LighthouseServerTest if (Directory.Exists(assetsDirectory)) Directory.Delete(assetsDirectory, true); } - [DatabaseFact] + [Fact] public async Task ShouldNotAcceptScript() { + await IntegrationHelper.GetIntegrationDatabase(); + LoginResult loginResult = await this.Authenticate(); HttpResponseMessage response = await this.AuthenticatedUploadFileEndpointRequest("ExampleFiles/TestScript.ff", loginResult.AuthTicket); - Assert.False(response.StatusCode == HttpStatusCode.Forbidden); - Assert.False(response.IsSuccessStatusCode); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.Conflict; + + Assert.Equal(expectedStatusCode, response.StatusCode); } - [DatabaseFact] + [Fact] public async Task ShouldNotAcceptFarc() { + await IntegrationHelper.GetIntegrationDatabase(); + LoginResult loginResult = await this.Authenticate(); HttpResponseMessage response = await this.AuthenticatedUploadFileEndpointRequest("ExampleFiles/TestFarc.farc", loginResult.AuthTicket); - Assert.False(response.StatusCode == HttpStatusCode.Forbidden); - Assert.False(response.IsSuccessStatusCode); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.Conflict; + + Assert.Equal(expectedStatusCode, response.StatusCode); } - [DatabaseFact] + [Fact] public async Task ShouldNotAcceptGarbage() { + await IntegrationHelper.GetIntegrationDatabase(); + LoginResult loginResult = await this.Authenticate(); HttpResponseMessage response = await this.AuthenticatedUploadFileEndpointRequest("ExampleFiles/TestGarbage.bin", loginResult.AuthTicket); - Assert.False(response.StatusCode == HttpStatusCode.Forbidden); - Assert.False(response.IsSuccessStatusCode); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.Conflict; + + Assert.Equal(expectedStatusCode, response.StatusCode); } - [DatabaseFact] + [Fact] public async Task ShouldAcceptTexture() { + await IntegrationHelper.GetIntegrationDatabase(); + LoginResult loginResult = await this.Authenticate(); HttpResponseMessage response = await this.AuthenticatedUploadFileEndpointRequest("ExampleFiles/TestTexture.tex", loginResult.AuthTicket); - Assert.False(response.StatusCode == HttpStatusCode.Forbidden); - Assert.True(response.IsSuccessStatusCode); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, response.StatusCode); } - [DatabaseFact] + [Fact] public async Task ShouldAcceptLevel() { + await IntegrationHelper.GetIntegrationDatabase(); + LoginResult loginResult = await this.Authenticate(); HttpResponseMessage response = await this.AuthenticatedUploadFileEndpointRequest("ExampleFiles/TestLevel.lvl", loginResult.AuthTicket); - Assert.False(response.StatusCode == HttpStatusCode.Forbidden); - Assert.True(response.IsSuccessStatusCode); + + const HttpStatusCode expectedStatusCode = HttpStatusCode.OK; + + Assert.Equal(expectedStatusCode, response.StatusCode); } } \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Tests/MatchTests.cs b/ProjectLighthouse.Tests.GameApiTests/Tests/MatchTests.cs deleted file mode 100644 index 7312a05e..00000000 --- a/ProjectLighthouse.Tests.GameApiTests/Tests/MatchTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using LBPUnion.ProjectLighthouse.Database; -using LBPUnion.ProjectLighthouse.Helpers; -using LBPUnion.ProjectLighthouse.Servers.GameServer.Startup; -using LBPUnion.ProjectLighthouse.Tests; -using LBPUnion.ProjectLighthouse.Types.Users; -using Xunit; - -namespace ProjectLighthouse.Tests.GameApiTests.Tests; - -public class MatchTests : LighthouseServerTest -{ - private static readonly SemaphoreSlim semaphore = new(1, 1); - - [DatabaseFact] - public async Task ShouldRejectEmptyData() - { - LoginResult loginResult = await this.Authenticate(); - await semaphore.WaitAsync(); - - HttpResponseMessage result = await this.AuthenticatedUploadDataRequest("LITTLEBIGPLANETPS3_XML/match", Array.Empty(), loginResult.AuthTicket); - - semaphore.Release(); - Assert.False(result.IsSuccessStatusCode); - } - - [DatabaseFact] - public async Task ShouldReturnOk() - { - LoginResult loginResult = await this.Authenticate(); - await semaphore.WaitAsync(); - - HttpResponseMessage result = await this.AuthenticatedUploadDataRequest - ("LITTLEBIGPLANETPS3_XML/match", "[UpdateMyPlayerData,[\"Player\":\"1984\"]]"u8.ToArray(), loginResult.AuthTicket); - - semaphore.Release(); - Assert.True(result.IsSuccessStatusCode); - } - - [DatabaseFact] - public async Task ShouldIncrementPlayerCount() - { - LoginResult loginResult = await this.Authenticate(new Random().Next()); - - await semaphore.WaitAsync(); - - await using DatabaseContext database = new(); - - int oldPlayerCount = await StatisticsHelper.RecentMatches(database); - - HttpResponseMessage result = await this.AuthenticatedUploadDataRequest - ("LITTLEBIGPLANETPS3_XML/match", "[UpdateMyPlayerData,[\"Player\":\"1984\"]]"u8.ToArray(), loginResult.AuthTicket); - - Assert.True(result.IsSuccessStatusCode); - - int playerCount = await StatisticsHelper.RecentMatches(database); - - semaphore.Release(); - Assert.Equal(oldPlayerCount + 1, playerCount); - } -} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/CommentControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/CommentControllerTests.cs new file mode 100644 index 00000000..f8a95ce0 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/CommentControllerTests.cs @@ -0,0 +1,231 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Levels; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +[Trait("Category", "Unit")] +public class CommentControllerTests +{ + [Fact] + public async Task PostComment_ShouldPostProfileComment_WhenBodyIsValid() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + CommentController commentController = new(dbMock); + commentController.SetupTestController("test"); + + const string expectedCommentMessage = "test"; + + IActionResult result = await commentController.PostComment("unittest", null, 0); + + Assert.IsType(result); + CommentEntity? comment = dbMock.Comments.FirstOrDefault(); + Assert.NotNull(comment); + Assert.Equal(expectedCommentMessage, comment.Message); + } + + [Fact] + public async Task PostComment_ShouldCensorComment_WhenFilterEnabled() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + CommentController commentController = new(dbMock); + commentController.SetupTestController("zamn"); + + CensorConfiguration.Instance.FilteredWordList = new List + { + "zamn", + }; + CensorConfiguration.Instance.UserInputFilterMode = FilterMode.Asterisks; + const string expectedCommentMessage = "****"; + + IActionResult result = await commentController.PostComment("unittest", null, 0); + + + Assert.IsType(result); + CommentEntity? comment = dbMock.Comments.FirstOrDefault(); + Assert.NotNull(comment); + Assert.Equal(expectedCommentMessage, comment.Message); + } + + [Fact] + public async Task PostComment_ShouldCensorComment_WhenFilterDisabled() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + CommentController commentController = new(dbMock); + commentController.SetupTestController("zamn"); + + CensorConfiguration.Instance.FilteredWordList = new List + { + "zamn", + }; + CensorConfiguration.Instance.UserInputFilterMode = FilterMode.None; + + IActionResult result = await commentController.PostComment("unittest", null, 0); + + const string expectedCommentMessage = "zamn"; + + Assert.IsType(result); + CommentEntity? comment = dbMock.Comments.FirstOrDefault(); + Assert.NotNull(comment); + Assert.Equal(expectedCommentMessage, comment.Message); + } + + [Fact] + public async Task PostComment_ShouldPostUserLevelComment_WhenBodyIsValid() + { + List slots = new() + { + new SlotEntity + { + SlotId = 1, + CreatorId = 1, + Type = SlotType.User, + }, + }; + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(new[]{slots,}); + + CommentController commentController = new(dbMock); + commentController.SetupTestController("test"); + + const string expectedCommentMessage = "test"; + + IActionResult result = await commentController.PostComment(null, "user", 1); + + Assert.IsType(result); + CommentEntity? comment = dbMock.Comments.FirstOrDefault(); + Assert.NotNull(comment); + Assert.Equal(expectedCommentMessage, comment.Message); + } + + [Fact] + public async Task PostComment_ShouldPostDeveloperLevelComment_WhenBodyIsValid() + { + List slots = new() + { + new SlotEntity + { + SlotId = 1, + InternalSlotId = 12345, + CreatorId = 1, + Type = SlotType.Developer, + }, + }; + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(new[] { slots, }); + + CommentController commentController = new(dbMock); + commentController.SetupTestController("test"); + + const string expectedCommentMessage = "test"; + + IActionResult result = await commentController.PostComment(null, "developer", 12345); + + Assert.IsType(result); + CommentEntity? comment = dbMock.Comments.FirstOrDefault(); + Assert.NotNull(comment); + Assert.Equal(expectedCommentMessage, comment.Message); + } + + [Fact] + public async Task PostComment_ShouldNotPostProfileComment_WhenTargetProfileInvalid() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + CommentController commentController = new(dbMock); + commentController.SetupTestController("test"); + + IActionResult result = await commentController.PostComment("unittest2", null, 0); + + Assert.IsType(result); + } + + [Fact] + public async Task PostComment_ShouldNotPostUserLevelComment_WhenLevelInvalid() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + CommentController commentController = new(dbMock); + commentController.SetupTestController("test"); + + IActionResult result = await commentController.PostComment(null, "user", 1); + + Assert.IsType(result); + } + + [Fact] + public async Task PostComment_ShouldNotPostComment_WhenBodyIsEmpty() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + CommentController commentController = new(dbMock); + commentController.SetupTestController(""); + + IActionResult result = await commentController.PostComment("unittest", null, 0); + + Assert.IsType(result); + } + + [Fact] + public async Task PostComment_ShouldNotPostComment_WhenBodyIsInvalid() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + CommentController commentController = new(dbMock); + commentController.SetupTestController(""); + + IActionResult result = await commentController.PostComment("unittest", null, 0); + + Assert.IsType(result); + } + + [Fact] + public async Task PostComment_ShouldFail_WhenSlotTypeIsInvalid() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + CommentController commentController = new(dbMock); + commentController.SetupTestController("test"); + + IActionResult result = await commentController.PostComment(null, "banana", 0); + + Assert.IsType(result); + } + + [Fact] + public async Task PostComment_ShouldFail_WhenAllArgumentsAreEmpty() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + CommentController commentController = new(dbMock); + commentController.SetupTestController("test"); + + IActionResult result = await commentController.PostComment(null, null, 0); + + Assert.IsType(result); + } + + [Fact] + public async Task PostComment_ShouldFail_WhenSlotTypeAndUsernameAreProvided() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + CommentController commentController = new(dbMock); + commentController.SetupTestController("test"); + + IActionResult result = await commentController.PostComment("unittest", "user", 10); + + Assert.IsType(result); + } + +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs new file mode 100644 index 00000000..a6ad5cf5 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/MessageControllerTests.cs @@ -0,0 +1,316 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Mail; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Mail; +using Microsoft.AspNetCore.Mvc; +using Moq; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +[Trait("Category", "Unit")] +public class MessageControllerTests +{ + [Fact] + public void Eula_ShouldReturnLicense_WhenConfigEmpty() + { + MessageController messageController = new(null!); + messageController.SetupTestController(); + + ServerConfiguration.Instance.EulaText = ""; + + const string expected = @" +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see ." + "\n"; + + IActionResult result = messageController.Eula(); + + Assert.IsType(result); + OkObjectResult? okObjectResult = result as OkObjectResult; + Assert.NotNull(okObjectResult); + Assert.NotNull(okObjectResult.Value); + Assert.Equal(expected, (string)okObjectResult.Value); + } + + [Fact] + public void Eula_ShouldReturnLicenseAndConfigString_WhenConfigNotEmpty() + { + MessageController messageController = new(null!); + messageController.SetupTestController(); + + ServerConfiguration.Instance.EulaText = "unit test eula text"; + + const string expected = @" +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see ." + "\nunit test eula text"; + + IActionResult result = messageController.Eula(); + + Assert.IsType(result); + OkObjectResult? okObjectResult = result as OkObjectResult; + Assert.NotNull(okObjectResult); + Assert.NotNull(okObjectResult.Value); + Assert.Equal(expected, (string)okObjectResult.Value); + } + + [Fact] + public async Task Announcement_WithVariables_ShouldBeResolved() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + MessageController messageController = new(dbMock); + messageController.SetupTestController(); + + ServerConfiguration.Instance.AnnounceText = "you are now logged in as %user (id: %id)"; + + const string expected = "you are now logged in as unittest (id: 1)\n"; + + IActionResult result = await messageController.Announce(); + + Assert.IsType(result); + OkObjectResult? okObjectResult = result as OkObjectResult; + Assert.NotNull(okObjectResult); + Assert.NotNull(okObjectResult.Value); + Assert.Equal(expected, (string)okObjectResult.Value); + } + + [Fact] + public async Task Announcement_WithEmptyString_ShouldBeEmpty() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + MessageController messageController = new(dbMock); + messageController.SetupTestController(); + + ServerConfiguration.Instance.AnnounceText = ""; + + const string expected = ""; + + IActionResult result = await messageController.Announce(); + + Assert.IsType(result); + OkObjectResult? okObjectResult = result as OkObjectResult; + Assert.NotNull(okObjectResult); + Assert.NotNull(okObjectResult.Value); + Assert.Equal(expected, (string)okObjectResult.Value); + } + + [Fact] + public async Task Notification_ShouldReturn_Empty() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + MessageController messageController = new(dbMock); + messageController.SetupTestController(); + + IActionResult result = messageController.Notification(); + + Assert.IsType(result); + } + + [Fact] + public async Task Filter_ShouldNotCensor_WhenCensorDisabled() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + const string request = "unit test message"; + MessageController messageController = new(dbMock); + messageController.SetupTestController(request); + + CensorConfiguration.Instance.UserInputFilterMode = FilterMode.None; + + const string expectedBody = "unit test message"; + + IActionResult result = await messageController.Filter(new NullMailService()); + + Assert.IsType(result); + OkObjectResult? okObjectResult = result as OkObjectResult; + Assert.NotNull(okObjectResult); + Assert.NotNull(okObjectResult.Value); + Assert.Equal(expectedBody, (string)okObjectResult.Value); + } + + [Fact] + public async Task Filter_ShouldCensor_WhenCensorEnabled() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + const string request = "unit test message bruh"; + MessageController messageController = new(dbMock); + messageController.SetupTestController(request); + + CensorConfiguration.Instance.UserInputFilterMode = FilterMode.Asterisks; + CensorConfiguration.Instance.FilteredWordList = new List + { + "bruh", + }; + + const string expectedBody = "unit test message ****"; + + IActionResult result = await messageController.Filter(new NullMailService()); + + Assert.IsType(result); + OkObjectResult? okObjectResult = result as OkObjectResult; + Assert.NotNull(okObjectResult); + Assert.NotNull(okObjectResult.Value); + Assert.Equal(expectedBody, (string)okObjectResult.Value); + } + + private static Mock getMailServiceMock() + { + Mock mailMock = new(); + mailMock.Setup(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.FromResult(true)); + return mailMock; + } + + [Fact] + public async void Filter_ShouldNotSendEmail_WhenMailDisabled() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + Mock mailMock = getMailServiceMock(); + const string request = "/setemail unittest@unittest.com"; + MessageController messageController = new(dbMock); + messageController.SetupTestController(request); + + ServerConfiguration.Instance.Mail.MailEnabled = false; + CensorConfiguration.Instance.FilteredWordList = new List(); + + const int expectedStatus = 200; + const string expected = "/setemail unittest@unittest.com"; + + IActionResult result = await messageController.Filter(mailMock.Object); + + Assert.IsType(result); + OkObjectResult? okObjectResult = result as OkObjectResult; + Assert.NotNull(okObjectResult); + Assert.Equal(expectedStatus, okObjectResult.StatusCode); + Assert.Equal(expected, okObjectResult.Value); + } + + [Fact] + public async void Filter_ShouldSendEmail_WhenMailEnabled_AndEmailNotTaken() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + Mock mailMock = getMailServiceMock(); + + const string request = "/setemail unittest@unittest.com"; + + MessageController messageController = new(dbMock); + messageController.SetupTestController(request); + + ServerConfiguration.Instance.Mail.MailEnabled = true; + + const string expectedEmail = "unittest@unittest.com"; + + IActionResult result = await messageController.Filter(mailMock.Object); + + Assert.IsType(result); + Assert.Equal(expectedEmail, dbMock.Users.First().EmailAddress); + mailMock.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async void Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailTaken() + { + List users = new() + { + MockHelper.GetUnitTestUser(), + new UserEntity + { + UserId = 2, + EmailAddress = "unittest@unittest.com", + EmailAddressVerified = false, + }, + }; + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(users); + Mock mailMock = getMailServiceMock(); + + const string request = "/setemail unittest@unittest.com"; + + MessageController messageController = new(dbMock); + messageController.SetupTestController(request); + + ServerConfiguration.Instance.Mail.MailEnabled = true; + + IActionResult result = await messageController.Filter(mailMock.Object); + + Assert.IsType(result); + mailMock.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async void Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailAlreadyVerified() + { + UserEntity unitTestUser = MockHelper.GetUnitTestUser(); + unitTestUser.EmailAddressVerified = true; + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(new List + { + unitTestUser, + }); + + Mock mailMock = getMailServiceMock(); + + const string request = "/setemail unittest@unittest.com"; + + MessageController messageController = new(dbMock); + messageController.SetupTestController(request); + + ServerConfiguration.Instance.Mail.MailEnabled = true; + + IActionResult result = await messageController.Filter(mailMock.Object); + + Assert.IsType(result); + mailMock.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async void Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailFormatInvalid() + { + UserEntity unitTestUser = MockHelper.GetUnitTestUser(); + unitTestUser.EmailAddressVerified = true; + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(new List + { + unitTestUser, + }); + + Mock mailMock = getMailServiceMock(); + + const string request = "/setemail unittestinvalidemail@@@"; + + MessageController messageController = new(dbMock); + messageController.SetupTestController(request); + + ServerConfiguration.Instance.Mail.MailEnabled = true; + + IActionResult result = await messageController.Filter(mailMock.Object); + + Assert.IsType(result); + mailMock.Verify(x => x.SendEmailAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs new file mode 100644 index 00000000..825643f1 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatisticsControllerTests.cs @@ -0,0 +1,192 @@ +using System.Collections.Generic; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Level; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +[Trait("Category", "Unit")] +public class StatisticsControllerTests +{ + [Fact] + public async void PlanetStats_ShouldReturnCorrectCounts_WhenEmpty() + { + await using DatabaseContext db = await MockHelper.GetTestDatabase(); + + StatisticsController statsController = new(db); + statsController.SetupTestController(); + + const int expectedSlots = 0; + const int expectedTeamPicks = 0; + + IActionResult result = await statsController.PlanetStats(); + + Assert.IsType(result); + OkObjectResult? objectResult = result as OkObjectResult; + Assert.NotNull(objectResult); + PlanetStatsResponse? response = objectResult.Value as PlanetStatsResponse; + Assert.NotNull(response); + Assert.Equal(expectedSlots, response.TotalSlotCount); + Assert.Equal(expectedTeamPicks, response.TeamPickCount); + } + + [Fact] + public async void PlanetStats_ShouldReturnCorrectCounts_WhenNotEmpty() + { + List slots = new() + { + new SlotEntity + { + SlotId = 1, + }, + new SlotEntity + { + SlotId = 2, + }, + new SlotEntity + { + SlotId = 3, + TeamPick = true, + }, + }; + await using DatabaseContext db = await MockHelper.GetTestDatabase(new []{slots,}); + + StatisticsController statsController = new(db); + statsController.SetupTestController(); + + const int expectedSlots = 3; + const int expectedTeamPicks = 1; + + IActionResult result = await statsController.PlanetStats(); + + Assert.IsType(result); + OkObjectResult? objectResult = result as OkObjectResult; + Assert.NotNull(objectResult); + PlanetStatsResponse? response = objectResult.Value as PlanetStatsResponse; + Assert.NotNull(response); + Assert.Equal(expectedSlots, response.TotalSlotCount); + Assert.Equal(expectedTeamPicks, response.TeamPickCount); + } + + [Fact] + public async void PlanetStats_ShouldReturnCorrectCounts_WhenSlotsAreIncompatibleGameVersion() + { + List slots = new() + { + new SlotEntity + { + SlotId = 1, + GameVersion = GameVersion.LittleBigPlanet2, + }, + new SlotEntity + { + SlotId = 2, + GameVersion = GameVersion.LittleBigPlanet2, + }, + new SlotEntity + { + SlotId = 3, + TeamPick = true, + GameVersion = GameVersion.LittleBigPlanet2, + }, + }; + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(new[]{slots,}); + + StatisticsController statsController = new(dbMock); + statsController.SetupTestController(); + + const int expectedSlots = 0; + const int expectedTeamPicks = 0; + + IActionResult result = await statsController.PlanetStats(); + + Assert.IsType(result); + OkObjectResult? objectResult = result as OkObjectResult; + Assert.NotNull(objectResult); + PlanetStatsResponse? response = objectResult.Value as PlanetStatsResponse; + Assert.NotNull(response); + Assert.Equal(expectedSlots, response.TotalSlotCount); + Assert.Equal(expectedTeamPicks, response.TeamPickCount); + } + + [Fact] + public async void TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreCompatible() + { + List slots = new() + { + new SlotEntity + { + SlotId = 1, + GameVersion = GameVersion.LittleBigPlanet1, + }, + new SlotEntity + { + SlotId = 2, + GameVersion = GameVersion.LittleBigPlanet1, + }, + new SlotEntity + { + SlotId = 3, + TeamPick = true, + GameVersion = GameVersion.LittleBigPlanet1, + }, + }; + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(new[] {slots,}); + + StatisticsController statsController = new(dbMock); + statsController.SetupTestController(); + + const string expectedTotal = "3"; + + IActionResult result = await statsController.TotalLevelCount(); + + Assert.IsType(result); + OkObjectResult? objectResult = result as OkObjectResult; + Assert.NotNull(objectResult); + Assert.Equal(expectedTotal, objectResult.Value); + } + + [Fact] + public async void TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreNotCompatible() + { + List slots = new() + { + new SlotEntity + { + SlotId = 1, + GameVersion = GameVersion.LittleBigPlanet2, + }, + new SlotEntity + { + SlotId = 2, + GameVersion = GameVersion.LittleBigPlanet2, + }, + new SlotEntity + { + SlotId = 3, + TeamPick = true, + GameVersion = GameVersion.LittleBigPlanet2, + }, + }; + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(new[] {slots,}); + + StatisticsController statsController = new(dbMock); + statsController.SetupTestController(); + + const int expectedStatusCode = 200; + const string expectedTotal = "0"; + + IActionResult result = await statsController.TotalLevelCount(); + + Assert.IsType(result); + OkObjectResult? objectResult = result as OkObjectResult; + Assert.NotNull(objectResult); + Assert.Equal(expectedStatusCode, objectResult.StatusCode); + Assert.Equal(expectedTotal, objectResult.Value); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatusControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatusControllerTests.cs new file mode 100644 index 00000000..c2d5c944 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/StatusControllerTests.cs @@ -0,0 +1,23 @@ +using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +[Trait("Category", "Unit")] +public class StatusControllerTests +{ + [Fact] + public void Status_ShouldReturnOk() + { + StatusController statusController = new() + { + ControllerContext = MockHelper.GetMockControllerContext(), + }; + + IActionResult result = statusController.GetStatus(); + + Assert.IsType(result); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs new file mode 100644 index 00000000..d46fecd5 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Controllers/UserControllerTests.cs @@ -0,0 +1,202 @@ +using System.Collections.Generic; +using System.Linq; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Controllers; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Serialization; +using Microsoft.AspNetCore.Mvc; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Controllers; + +[Trait("Category", "Unit")] +public class UserControllerTests +{ + [Fact] + public async void GetUser_WithValidUser_ShouldReturnUser() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController(); + + const int expectedId = 1; + + IActionResult result = await userController.GetUser("unittest"); + + Assert.IsType(result); + OkObjectResult? okObject = result as OkObjectResult; + Assert.NotNull(okObject); + GameUser? gameUser = okObject.Value as GameUser; + Assert.NotNull(gameUser); + Assert.Equal(expectedId, gameUser.UserId); + } + + [Fact] + public async void GetUser_WithInvalidUser_ShouldReturnNotFound() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController(); + + IActionResult result = await userController.GetUser("notfound"); + + Assert.IsType(result); + } + + [Fact] + public async void GetUserAlt_WithInvalidUser_ShouldReturnEmptyList() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController(); + + IActionResult result = await userController.GetUserAlt(new[]{"notfound",}); + + Assert.IsType(result); + OkObjectResult? okObject = result as OkObjectResult; + Assert.NotNull(okObject); + MinimalUserListResponse? userList = okObject.Value as MinimalUserListResponse? ?? default; + Assert.NotNull(userList); + Assert.Empty(userList.Value.Users); + } + + [Fact] + public async void GetUserAlt_WithOnlyInvalidUsers_ShouldReturnEmptyList() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController(); + + IActionResult result = await userController.GetUserAlt(new[] + { + "notfound", "notfound2", "notfound3", + }); + + Assert.IsType(result); + OkObjectResult? okObject = result as OkObjectResult; + Assert.NotNull(okObject); + MinimalUserListResponse? userList = okObject.Value as MinimalUserListResponse? ?? default; + Assert.NotNull(userList); + Assert.Empty(userList.Value.Users); + } + + [Fact] + public async void GetUserAlt_WithTwoInvalidUsers_AndOneValidUser_ShouldReturnOne() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController(); + + + IActionResult result = await userController.GetUserAlt(new[] + { + "notfound", "unittest", "notfound3", + }); + + Assert.IsType(result); + OkObjectResult? okObject = result as OkObjectResult; + Assert.NotNull(okObject); + MinimalUserListResponse? userList = okObject.Value as MinimalUserListResponse? ?? default; + Assert.NotNull(userList); + Assert.Single(userList.Value.Users); + } + + [Fact] + public async void GetUserAlt_WithTwoValidUsers_ShouldReturnTwo() + { + List users = new() + { + MockHelper.GetUnitTestUser(), + new UserEntity + { + UserId = 2, + Username = "unittest2", + }, + }; + + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(users); + + UserController userController = new(dbMock); + userController.SetupTestController(); + + const int expectedLength = 2; + + IActionResult result = await userController.GetUserAlt(new[] + { + "unittest2", "unittest", + }); + + Assert.IsType(result); + OkObjectResult? okObject = result as OkObjectResult; + Assert.NotNull(okObject); + MinimalUserListResponse? userList = okObject.Value as MinimalUserListResponse? ?? default; + Assert.NotNull(userList); + Assert.Equal(expectedLength, userList.Value.Users.Count); + } + + [Fact] + public async void UpdateMyPins_ShouldReturnBadRequest_WhenBodyIsInvalid() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController("{}"); + + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + } + + [Fact] + public async void UpdateMyPins_ShouldUpdatePins() + { + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + UserController userController = new(dbMock); + userController.SetupTestController("{\"profile_pins\": [1234]}"); + + const string expectedPins = "1234"; + const string expectedResponse = "[{\"StatusCode\":200}]"; + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + OkObjectResult? okObject = result as OkObjectResult; + Assert.NotNull(okObject); + Assert.Equal(expectedPins, dbMock.Users.First().Pins); + Assert.Equal(expectedResponse, okObject.Value); + } + + [Fact] + public async void UpdateMyPins_ShouldNotSave_WhenPinsAreEqual() + { + UserEntity entity = MockHelper.GetUnitTestUser(); + entity.Pins = "1234"; + List users = new() + { + entity, + }; + await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(users); + + UserController userController = new(dbMock); + userController.SetupTestController("{\"profile_pins\": [1234]}"); + + const string expectedPins = "1234"; + const string expectedResponse = "[{\"StatusCode\":200}]"; + + IActionResult result = await userController.UpdateMyPins(); + + Assert.IsType(result); + OkObjectResult? okObject = result as OkObjectResult; + Assert.NotNull(okObject); + Assert.Equal(expectedPins, dbMock.Users.First().Pins); + Assert.Equal(expectedResponse, okObject.Value); + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/DigestMiddlewareTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/DigestMiddlewareTests.cs new file mode 100644 index 00000000..f5e705b5 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/DigestMiddlewareTests.cs @@ -0,0 +1,435 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Middlewares; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Middlewares; + +[Trait("Category", "Unit")] +public class DigestMiddlewareTests +{ + + [Fact] + public async void DigestMiddleware_ShouldNotComputeDigests_WhenDigestsDisabled() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/notification", + Headers = { KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), }, + }, + }; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }, false); + + await middleware.InvokeAsync(context); + + const int expectedCode = 200; + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.Empty(context.Response.Headers["X-Digest-A"]); + Assert.Empty(context.Response.Headers["X-Digest-B"]); + } + + [Fact] + public async void DigestMiddleware_ShouldReject_WhenDigestHeaderIsMissing() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/notification", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + }, + }, + }; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "bruh"; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }, + true); + + await middleware.InvokeAsync(context); + + const int expectedCode = 403; + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.Empty(context.Response.Headers["X-Digest-A"]); + Assert.Empty(context.Response.Headers["X-Digest-B"]); + } + + [Fact] + public async void DigestMiddleware_ShouldReject_WhenRequestDigestInvalid() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/notification", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + KeyValuePair.Create("X-Digest-A", "invalid_digest"), + }, + }, + }; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "bruh"; + ServerConfiguration.Instance.DigestKey.AlternateDigestKey = "test"; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }, + true); + + await middleware.InvokeAsync(context); + + const int expectedCode = 403; + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.Empty(context.Response.Headers["X-Digest-A"]); + Assert.Empty(context.Response.Headers["X-Digest-B"]); + } + + [Fact] + public async void DigestMiddleware_ShouldUseAlternateDigest_WhenPrimaryDigestInvalid() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/notification", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + KeyValuePair.Create("X-Digest-A", "df619790a2579a077eae4a6b6864966ff4768723"), + }, + }, + }; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "test"; + ServerConfiguration.Instance.DigestKey.AlternateDigestKey = "bruh"; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }, + true); + + await middleware.InvokeAsync(context); + + const int expectedCode = 200; + const string expectedServerDigest = "df619790a2579a077eae4a6b6864966ff4768723"; + const string expectedClientDigest = "df619790a2579a077eae4a6b6864966ff4768723"; + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.NotEmpty(context.Response.Headers["X-Digest-A"]); + Assert.NotEmpty(context.Response.Headers["X-Digest-B"]); + Assert.Equal(expectedServerDigest, context.Response.Headers["X-Digest-A"][0]); + Assert.Equal(expectedClientDigest, context.Response.Headers["X-Digest-B"][0]); + } + +[Fact] + public async void DigestMiddleware_ShouldNotReject_WhenRequestingAnnounce() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/announce", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + }, + }, + }; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "bruh"; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }, + true); + + await middleware.InvokeAsync(context); + + const int expectedCode = 200; + const string expectedServerDigest = "9243acecfa83ac25bdfefe97f5681b439c003f1e"; + const string expectedClientDigest = "9243acecfa83ac25bdfefe97f5681b439c003f1e"; + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.NotEmpty(context.Response.Headers["X-Digest-A"]); + Assert.NotEmpty(context.Response.Headers["X-Digest-B"]); + Assert.Equal(expectedServerDigest, context.Response.Headers["X-Digest-A"]); + Assert.Equal(expectedClientDigest, context.Response.Headers["X-Digest-B"]); + } + + [Fact] + public async void DigestMiddleware_ShouldCalculate_WhenAuthCookieEmpty() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/notification", + Headers = + { + KeyValuePair.Create("X-Digest-A", "0a06d25662c2d3bab2a767c0c504898df2385e62"), + }, + }, + }; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "bruh"; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }, + true); + + await middleware.InvokeAsync(context); + + const int expectedCode = 200; + const string expectedServerDigest = "0a06d25662c2d3bab2a767c0c504898df2385e62"; + const string expectedClientDigest = "0a06d25662c2d3bab2a767c0c504898df2385e62"; + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.NotEmpty(context.Response.Headers["X-Digest-A"]); + Assert.NotEmpty(context.Response.Headers["X-Digest-B"]); + Assert.Equal(expectedServerDigest, context.Response.Headers["X-Digest-A"][0]); + Assert.Equal(expectedClientDigest, context.Response.Headers["X-Digest-B"][0]); + } + + [Fact] + public async void DigestMiddleware_ShouldComputeDigestsWithNoBody_WhenDigestsEnabled() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/notification", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + KeyValuePair.Create("X-Digest-A", "df619790a2579a077eae4a6b6864966ff4768723"), + }, + }, + }; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "bruh"; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }, + true); + + await middleware.InvokeAsync(context); + + const int expectedCode = 200; + const string expectedServerDigest = "df619790a2579a077eae4a6b6864966ff4768723"; + const string expectedClientDigest = "df619790a2579a077eae4a6b6864966ff4768723"; + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.NotEmpty(context.Response.Headers["X-Digest-A"]); + Assert.NotEmpty(context.Response.Headers["X-Digest-B"]); + Assert.Equal(expectedServerDigest, context.Response.Headers["X-Digest-A"][0]); + Assert.Equal(expectedClientDigest, context.Response.Headers["X-Digest-B"][0]); + } + + [Fact] + public async void DigestMiddleware_ShouldComputeDigestsWithBody_WhenDigestsEnabled_AndNoResponseBody() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream("digest test"u8.ToArray()), + Path = "/LITTLEBIGPLANETPS3_XML/filter", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + KeyValuePair.Create("X-Digest-A", "3105059f9283773f7982a4d79455bcc97c330f10"), + }, + }, + }; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "bruh"; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }, + true); + + await middleware.InvokeAsync(context); + + const int expectedCode = 200; + const string expectedServerDigest = "c87ef375f095d36369bb6d9689220fd0ce0e0d4b"; + const string expectedClientDigest = "3105059f9283773f7982a4d79455bcc97c330f10"; + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.NotEmpty(context.Response.Headers["X-Digest-A"]); + Assert.NotEmpty(context.Response.Headers["X-Digest-B"]); + Assert.Equal(expectedServerDigest, context.Response.Headers["X-Digest-A"][0]); + Assert.Equal(expectedClientDigest, context.Response.Headers["X-Digest-B"][0]); + } + + [Fact] + public async void DigestMiddleware_ShouldComputeDigestsWithBody_WhenDigestsEnabled_AndResponseBody() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream("digest test"u8.ToArray()), + Path = "/LITTLEBIGPLANETPS3_XML/filter", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + KeyValuePair.Create("X-Digest-A", "3105059f9283773f7982a4d79455bcc97c330f10"), + }, + }, + }; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "bruh"; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync("digest test"); + return Task.CompletedTask; + }, + true); + + await middleware.InvokeAsync(context); + + const int expectedCode = 200; + const string expectedServerDigest = "3105059f9283773f7982a4d79455bcc97c330f10"; + const string expectedClientDigest = "3105059f9283773f7982a4d79455bcc97c330f10"; + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.NotEmpty(context.Response.Headers["X-Digest-A"]); + Assert.NotEmpty(context.Response.Headers["X-Digest-B"]); + Assert.Equal(expectedServerDigest, context.Response.Headers["X-Digest-A"][0]); + Assert.Equal(expectedClientDigest, context.Response.Headers["X-Digest-B"][0]); + } + + [Fact] + public async void DigestMiddleware_ShouldComputeDigestsWithBody_WhenUploading() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream("digest test"u8.ToArray()), + Path = "/LITTLEBIGPLANETPS3_XML/upload/unittesthash", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + KeyValuePair.Create("X-Digest-B", "2e54cd2bc69ff8c1ff85dd3b4f62e0a0e27d9e23"), + }, + }, + }; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "bruh"; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }, + true); + + await middleware.InvokeAsync(context); + + const int expectedCode = 200; + const string expectedServerDigest = "2e54cd2bc69ff8c1ff85dd3b4f62e0a0e27d9e23"; + const string expectedClientDigest = "2e54cd2bc69ff8c1ff85dd3b4f62e0a0e27d9e23"; + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.NotEmpty(context.Response.Headers["X-Digest-A"]); + Assert.NotEmpty(context.Response.Headers["X-Digest-B"]); + Assert.Equal(expectedServerDigest, context.Response.Headers["X-Digest-A"][0]); + Assert.Equal(expectedClientDigest, context.Response.Headers["X-Digest-B"][0]); + } + + [Fact] + public async void DigestMiddleware_ShouldCompressResponse_WhenAcceptEncodingHeaderIsPresent() + { + DefaultHttpContext context = new() + { + Response = + { + Body = new MemoryStream(), + }, + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/r/testing", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + KeyValuePair.Create("X-Digest-A", "80714c0936408855d86d47a650320f91895812d0"), + KeyValuePair.Create("Accept-Encoding", "deflate"), + }, + }, + }; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "bruh"; + DigestMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(new string('a', 1000 * 2)); + httpContext.Response.Headers.ContentType = "text/xml"; + return Task.CompletedTask; + }, + true); + + await middleware.InvokeAsync(context); + + const int expectedCode = 200; + const string expectedServerDigest = "404e589cafbff7886fe9fc5ee8a5454b57d9cb50"; + const string expectedClientDigest = "80714c0936408855d86d47a650320f91895812d0"; + const string expectedContentLen = "2000"; + const string expectedCompressedContentLen = "23"; + const string expectedData = "783F4B4C1C053F60143F3F51300A463F500700643F3F"; + + context.Response.Body.Position = 0; + string output = await new StreamReader(context.Response.Body).ReadToEndAsync(); + string outputBytes = Convert.ToHexString(Encoding.ASCII.GetBytes(output)); + + Assert.Equal(expectedCode, context.Response.StatusCode); + Assert.NotEmpty(context.Response.Headers["X-Digest-A"]); + Assert.NotEmpty(context.Response.Headers["X-Digest-B"]); + Assert.NotEmpty(context.Response.Headers["Content-Encoding"]); + Assert.NotEmpty(context.Response.Headers["X-Original-Content-Length"]); + Assert.Equal(expectedServerDigest, context.Response.Headers["X-Digest-A"][0]); + Assert.Equal(expectedClientDigest, context.Response.Headers["X-Digest-B"][0]); + Assert.Equal(expectedContentLen, context.Response.Headers["X-Original-Content-Length"][0]); + Assert.Equal(expectedCompressedContentLen, context.Response.Headers["Content-Length"][0]); + Assert.Equal(expectedData, outputBytes); + } + +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/SetLastContactMiddlewareTests.cs b/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/SetLastContactMiddlewareTests.cs new file mode 100644 index 00000000..fa57bc47 --- /dev/null +++ b/ProjectLighthouse.Tests.GameApiTests/Unit/Middlewares/SetLastContactMiddlewareTests.cs @@ -0,0 +1,150 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Servers.GameServer.Middlewares; +using LBPUnion.ProjectLighthouse.Tests.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace ProjectLighthouse.Tests.GameApiTests.Unit.Middlewares; + +[Trait("Category", "Unit")] +public class SetLastContactMiddlewareTests +{ + + [Fact] + public async void SetLastContact_ShouldAddLastContact_WhenTokenIsLBP1() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/notification", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + }, + }, + }; + SetLastContactMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }); + + DatabaseContext dbMock = await MockHelper.GetTestDatabase(); + + await middleware.InvokeAsync(context, dbMock); + + const int expectedCode = 200; + const int expectedUserId = 1; + const GameVersion expectedGameVersion = GameVersion.LittleBigPlanet1; + + Assert.Equal(expectedCode, context.Response.StatusCode); + LastContactEntity? lastContactEntity = dbMock.LastContacts.FirstOrDefault(); + Assert.NotNull(lastContactEntity); + Assert.Equal(expectedUserId, lastContactEntity.UserId); + Assert.Equal(expectedGameVersion, lastContactEntity.GameVersion); + } + + [Fact] + public async void SetLastContact_ShouldUpdateLastContact_WhenTokenIsLBP1() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/notification", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + }, + }, + }; + SetLastContactMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }); + + List lastContacts = new() + { + new LastContactEntity + { + UserId = 1, + GameVersion = GameVersion.LittleBigPlanet1, + Timestamp = 0, + Platform = Platform.UnitTest, + }, + }; + + DatabaseContext dbMock = await MockHelper.GetTestDatabase(new[]{lastContacts,}); + + await middleware.InvokeAsync(context, dbMock); + + const int expectedCode = 200; + const int expectedUserId = 1; + const int oldTimestamp = 0; + const GameVersion expectedGameVersion = GameVersion.LittleBigPlanet1; + + Assert.Equal(expectedCode, context.Response.StatusCode); + LastContactEntity? lastContactEntity = dbMock.LastContacts.FirstOrDefault(); + Assert.NotNull(lastContactEntity); + Assert.Equal(expectedUserId, lastContactEntity.UserId); + Assert.Equal(expectedGameVersion, lastContactEntity.GameVersion); + Assert.NotEqual(oldTimestamp, lastContactEntity.Timestamp); + } + + [Fact] + public async void SetLastContact_ShouldNotAddLastContact_WhenTokenIsNotLBP1() + { + DefaultHttpContext context = new() + { + Request = + { + Body = new MemoryStream(), + Path = "/LITTLEBIGPLANETPS3_XML/notification", + Headers = + { + KeyValuePair.Create("Cookie", "MM_AUTH=unittest"), + }, + }, + }; + SetLastContactMiddleware middleware = new(httpContext => + { + httpContext.Response.StatusCode = 200; + httpContext.Response.WriteAsync(""); + return Task.CompletedTask; + }); + + List tokens = new() + { + MockHelper.GetUnitTestToken(), + }; + tokens[0].GameVersion = GameVersion.LittleBigPlanet2; + + DatabaseContext dbMock = await MockHelper.GetTestDatabase(new[] + { + tokens, + }); + + await middleware.InvokeAsync(context, dbMock); + + const int expectedCode = 200; + + Assert.Equal(expectedCode, context.Response.StatusCode); + LastContactEntity? lastContactEntity = dbMock.LastContacts.FirstOrDefault(); + Assert.Null(lastContactEntity); + } + +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests.WebsiteTests/Tests/AdminTests.cs b/ProjectLighthouse.Tests.WebsiteTests/Integration/AdminTests.cs similarity index 80% rename from ProjectLighthouse.Tests.WebsiteTests/Tests/AdminTests.cs rename to ProjectLighthouse.Tests.WebsiteTests/Integration/AdminTests.cs index a6af8487..ec8d9782 100644 --- a/ProjectLighthouse.Tests.WebsiteTests/Tests/AdminTests.cs +++ b/ProjectLighthouse.Tests.WebsiteTests/Integration/AdminTests.cs @@ -2,23 +2,24 @@ using System; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Helpers; -using LBPUnion.ProjectLighthouse.Tests; +using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using LBPUnion.ProjectLighthouse.Types.Users; using OpenQA.Selenium; using Xunit; -namespace ProjectLighthouse.Tests.WebsiteTests.Tests; +namespace ProjectLighthouse.Tests.WebsiteTests.Integration; +[Trait("Category", "Integration")] public class AdminTests : LighthouseWebTest { - public const string AdminPanelButtonXPath = "/html/body/div/header/div/div/div/a[1]"; + private const string adminPanelButtonXPath = "/html/body/div/header/div/div/div/a[1]"; - [DatabaseFact] + [Fact] public async Task ShouldShowAdminPanelButtonWhenAdmin() { - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); Random random = new(); UserEntity user = await database.CreateUser($"unitTestUser{random.Next()}", CryptoHelper.BCryptHash("i'm an engineering failure")); @@ -38,13 +39,13 @@ public class AdminTests : LighthouseWebTest this.Driver.Manage().Cookies.AddCookie(new Cookie("LighthouseToken", webToken.UserToken)); this.Driver.Navigate().Refresh(); - Assert.Contains("Admin", this.Driver.FindElement(By.XPath(AdminPanelButtonXPath)).Text); + Assert.Contains("Admin", this.Driver.FindElement(By.XPath(adminPanelButtonXPath)).Text); } - [DatabaseFact] + [Fact] public async Task ShouldNotShowAdminPanelButtonWhenNotAdmin() { - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); Random random = new(); UserEntity user = await database.CreateUser($"unitTestUser{random.Next()}", CryptoHelper.BCryptHash("i'm an engineering failure")); @@ -64,6 +65,6 @@ public class AdminTests : LighthouseWebTest this.Driver.Manage().Cookies.AddCookie(new Cookie("LighthouseToken", webToken.UserToken)); this.Driver.Navigate().Refresh(); - Assert.DoesNotContain("Admin", this.Driver.FindElement(By.XPath(AdminPanelButtonXPath)).Text); + Assert.DoesNotContain("Admin", this.Driver.FindElement(By.XPath(adminPanelButtonXPath)).Text); } } \ No newline at end of file diff --git a/ProjectLighthouse.Tests.WebsiteTests/Tests/AuthenticationTests.cs b/ProjectLighthouse.Tests.WebsiteTests/Integration/AuthenticationTests.cs similarity index 86% rename from ProjectLighthouse.Tests.WebsiteTests/Tests/AuthenticationTests.cs rename to ProjectLighthouse.Tests.WebsiteTests/Integration/AuthenticationTests.cs index 00870cde..f1d1f107 100644 --- a/ProjectLighthouse.Tests.WebsiteTests/Tests/AuthenticationTests.cs +++ b/ProjectLighthouse.Tests.WebsiteTests/Integration/AuthenticationTests.cs @@ -3,21 +3,22 @@ using System.Linq; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Helpers; -using LBPUnion.ProjectLighthouse.Tests; +using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; using Microsoft.EntityFrameworkCore; using OpenQA.Selenium; using Xunit; -namespace ProjectLighthouse.Tests.WebsiteTests.Tests; +namespace ProjectLighthouse.Tests.WebsiteTests.Integration; +[Trait("Category", "Integration")] public class AuthenticationTests : LighthouseWebTest { - [DatabaseFact] + [Fact] public async Task ShouldLoginWithPassword() { - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); Random random = new(); string password = CryptoHelper.Sha256Hash(CryptoHelper.GenerateRandomBytes(64).ToArray()); @@ -36,10 +37,10 @@ public class AuthenticationTests : LighthouseWebTest await database.RemoveUser(user); } - [DatabaseFact] + [Fact] public async Task ShouldNotLoginWithNoPassword() { - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); Random random = new(); UserEntity user = await database.CreateUser($"unitTestUser{random.Next()}", CryptoHelper.BCryptHash("just like the hindenberg,")); @@ -55,10 +56,10 @@ public class AuthenticationTests : LighthouseWebTest await database.RemoveUser(user); } - [DatabaseFact] + [Fact] public async Task ShouldNotLoginWithWrongPassword() { - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); Random random = new(); UserEntity user = await database.CreateUser($"unitTestUser{random.Next()}", CryptoHelper.BCryptHash("i'm an engineering failure")); @@ -75,12 +76,12 @@ public class AuthenticationTests : LighthouseWebTest await database.RemoveUser(user); } - [DatabaseFact] + [Fact] public async Task ShouldLoginWithInjectedCookie() { const string loggedInAsUsernameTextXPath = "/html/body/div/div/div/div/p[1]"; - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); Random random = new(); UserEntity user = await database.CreateUser($"unitTestUser{random.Next()}", CryptoHelper.BCryptHash("i'm an engineering failure")); diff --git a/ProjectLighthouse.Tests.WebsiteTests/Tests/LighthouseWebTest.cs b/ProjectLighthouse.Tests.WebsiteTests/Integration/LighthouseWebTest.cs similarity index 59% rename from ProjectLighthouse.Tests.WebsiteTests/Tests/LighthouseWebTest.cs rename to ProjectLighthouse.Tests.WebsiteTests/Integration/LighthouseWebTest.cs index 59a464fc..81698cf4 100644 --- a/ProjectLighthouse.Tests.WebsiteTests/Tests/LighthouseWebTest.cs +++ b/ProjectLighthouse.Tests.WebsiteTests/Integration/LighthouseWebTest.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Servers.Website.Startup; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -7,21 +8,22 @@ using OpenQA.Selenium; using OpenQA.Selenium.Chrome; using Xunit; -namespace ProjectLighthouse.Tests.WebsiteTests.Tests; +namespace ProjectLighthouse.Tests.WebsiteTests.Integration; [Collection(nameof(LighthouseWebTest))] public class LighthouseWebTest : IDisposable { - public readonly string BaseAddress; + protected readonly string BaseAddress; - public readonly IWebDriver Driver; - public readonly IWebHost WebHost = new WebHostBuilder().UseKestrel().UseStartup().UseWebRoot("StaticFiles").Build(); + protected readonly IWebDriver Driver; + private readonly IWebHost webHost = new WebHostBuilder().UseKestrel().UseStartup().UseWebRoot("StaticFiles").Build(); - public LighthouseWebTest() + protected LighthouseWebTest() { - this.WebHost.Start(); + ServerConfiguration.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse_tests;database=lighthouse_tests"; + this.webHost.Start(); - IServerAddressesFeature? serverAddressesFeature = this.WebHost.ServerFeatures.Get(); + IServerAddressesFeature? serverAddressesFeature = this.webHost.ServerFeatures.Get(); if (serverAddressesFeature == null) throw new ArgumentNullException(); this.BaseAddress = serverAddressesFeature.Addresses.First(); @@ -32,7 +34,7 @@ public class LighthouseWebTest : IDisposable chromeOptions.AddArgument("headless"); chromeOptions.AddArgument("no-sandbox"); chromeOptions.AddArgument("disable-dev-shm-usage"); - Console.WriteLine("We are in a CI environment, so chrome headless mode has been enabled."); + Console.WriteLine(@"We are in a CI environment, so chrome headless mode has been enabled."); } this.Driver = new ChromeDriver(chromeOptions); @@ -42,7 +44,7 @@ public class LighthouseWebTest : IDisposable { this.Driver.Close(); this.Driver.Dispose(); - this.WebHost.Dispose(); + this.webHost.Dispose(); GC.SuppressFinalize(this); } diff --git a/ProjectLighthouse.Tests.WebsiteTests/Tests/RegisterTests.cs b/ProjectLighthouse.Tests.WebsiteTests/Integration/RegisterTests.cs similarity index 67% rename from ProjectLighthouse.Tests.WebsiteTests/Tests/RegisterTests.cs rename to ProjectLighthouse.Tests.WebsiteTests/Integration/RegisterTests.cs index 90d09b32..5edc8e56 100644 --- a/ProjectLighthouse.Tests.WebsiteTests/Tests/RegisterTests.cs +++ b/ProjectLighthouse.Tests.WebsiteTests/Integration/RegisterTests.cs @@ -1,24 +1,27 @@ -using System; using System.Linq; using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Helpers; -using LBPUnion.ProjectLighthouse.Tests; +using LBPUnion.ProjectLighthouse.Tests.Helpers; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using Microsoft.EntityFrameworkCore; using OpenQA.Selenium; using Xunit; -namespace ProjectLighthouse.Tests.WebsiteTests.Tests; +namespace ProjectLighthouse.Tests.WebsiteTests.Integration; +[Trait("Category", "Integration")] public class RegisterTests : LighthouseWebTest { - [DatabaseFact] + [Fact] public async Task ShouldRegister() { - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); - string username = ("unitTestUser" + new Random().Next()).Substring(0, 16); + ServerConfiguration.Instance.Authentication.RegistrationEnabled = true; + + string username = ("unitTestUser" + CryptoHelper.GenerateRandomInt32(0, int.MaxValue))[..16]; string password = CryptoHelper.Sha256Hash(CryptoHelper.GenerateRandomBytes(64).ToArray()); this.Driver.Navigate().GoToUrl(this.BaseAddress + "/register"); @@ -36,14 +39,18 @@ public class RegisterTests : LighthouseWebTest Assert.NotNull(user); await database.RemoveUser(user); + + ServerConfiguration.Instance.Authentication.RegistrationEnabled = false; } - [DatabaseFact] + [Fact] public async Task ShouldNotRegisterWithMismatchingPasswords() { - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); - string username = ("unitTestUser" + new Random().Next()).Substring(0, 16); + ServerConfiguration.Instance.Authentication.RegistrationEnabled = true; + + string username = ("unitTestUser" + CryptoHelper.GenerateRandomInt32(0, int.MaxValue))[..16]; string password = CryptoHelper.Sha256Hash(CryptoHelper.GenerateRandomBytes(64).ToArray()); this.Driver.Navigate().GoToUrl(this.BaseAddress + "/register"); @@ -59,14 +66,18 @@ public class RegisterTests : LighthouseWebTest UserEntity? user = await database.Users.FirstOrDefaultAsync(u => u.Username == username); Assert.Null(user); + + ServerConfiguration.Instance.Authentication.RegistrationEnabled = false; } - [DatabaseFact] + [Fact] public async Task ShouldNotRegisterWithTakenUsername() { - await using DatabaseContext database = new(); + await using DatabaseContext database = await IntegrationHelper.GetIntegrationDatabase(); - string username = ("unitTestUser" + new Random().Next())[..16]; + ServerConfiguration.Instance.Authentication.RegistrationEnabled = true; + + string username = ("unitTestUser" + CryptoHelper.GenerateRandomInt32(0, int.MaxValue))[..16]; string password = CryptoHelper.Sha256Hash(CryptoHelper.GenerateRandomBytes(64).ToArray()); await database.CreateUser(username, CryptoHelper.BCryptHash(password)); @@ -85,5 +96,7 @@ public class RegisterTests : LighthouseWebTest this.Driver.FindElement(By.Id("submit")).Click(); Assert.Contains("The username you've chosen is already taken.", this.Driver.PageSource); + + ServerConfiguration.Instance.Authentication.RegistrationEnabled = false; } } \ No newline at end of file diff --git a/ProjectLighthouse.Tests/Helpers/IntegrationHelper.cs b/ProjectLighthouse.Tests/Helpers/IntegrationHelper.cs new file mode 100644 index 00000000..99fb6683 --- /dev/null +++ b/ProjectLighthouse.Tests/Helpers/IntegrationHelper.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Helpers; + +namespace LBPUnion.ProjectLighthouse.Tests.Helpers; + +public static class IntegrationHelper +{ + + private static readonly Lazy dbConnected = new(IsDbConnected); + + private static bool IsDbConnected() => ServerStatics.DbConnected; + + /// + /// Resets the database to a clean state and returns a new DatabaseContext. + /// + /// A new fresh instance of DatabaseContext + public static async Task GetIntegrationDatabase() + { + if (!dbConnected.Value) + { + throw new Exception("Database is not connected.\n" + + "Please ensure that the database is running and that the connection string is correct.\n" + + $"Connection string: {ServerConfiguration.Instance.DbConnectionString}"); + } + await ClearRooms(); + await using DatabaseContext database = DatabaseContext.CreateNewInstance(); + await database.Database.EnsureDeletedAsync(); + await database.Database.EnsureCreatedAsync(); + + return DatabaseContext.CreateNewInstance(); + } + + private static async Task ClearRooms() + { + await RoomHelper.Rooms.RemoveAllAsync(); + } + +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests/Helpers/MockHelper.cs b/ProjectLighthouse.Tests/Helpers/MockHelper.cs new file mode 100644 index 00000000..5ea02505 --- /dev/null +++ b/ProjectLighthouse.Tests/Helpers/MockHelper.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Database; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Users; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.EntityFrameworkCore; + +namespace LBPUnion.ProjectLighthouse.Tests.Helpers; + +public static class MockHelper +{ + + public static UserEntity GetUnitTestUser() => + new() + { + Username = "unittest", + UserId = 1, + }; + + public static GameTokenEntity GetUnitTestToken() => + new() + { + Platform = Platform.UnitTest, + UserId = 1, + ExpiresAt = DateTime.MaxValue, + TokenId = 1, + UserLocation = "127.0.0.1", + UserToken = "unittest", + }; + + public static async Task GetTestDatabase(IEnumerable sets, [CallerMemberName] string caller = "", [CallerLineNumber] int lineNum = 0) + { + Dictionary setDict = new(); + foreach (IList list in sets) + { + Type? type = list.GetType().GetGenericArguments().ElementAtOrDefault(0); + if (type == null) continue; + setDict[type] = list; + } + + if (!setDict.TryGetValue(typeof(GameTokenEntity), out _)) + { + setDict[typeof(GameTokenEntity)] = new List + { + GetUnitTestToken(), + }; + } + + if (!setDict.TryGetValue(typeof(UserEntity), out _)) + { + setDict[typeof(UserEntity)] = new List + { + GetUnitTestUser(), + }; + } + + + DbContextOptions options = new DbContextOptionsBuilder() + .UseInMemoryDatabase($"{caller}_{lineNum}") + .Options; + + await using DatabaseContext context = new(options); + foreach (IList list in setDict.Select(p => p.Value)) + { + foreach (object item in list) + { + context.Add(item); + } + } + + + await context.SaveChangesAsync(); + await context.DisposeAsync(); + return new DatabaseContext(options); + } + + public static async Task GetTestDatabase(List? users = null, List? tokens = null, + [CallerMemberName] string caller = "", [CallerLineNumber] int lineNum = 0 + ) + { + users ??= new List + { + GetUnitTestUser(), + }; + + tokens ??= new List + { + GetUnitTestToken(), + }; + DbContextOptions options = new DbContextOptionsBuilder() + .UseInMemoryDatabase($"{caller}_{lineNum}") + .Options; + await using DatabaseContext context = new(options); + context.Users.AddRange(users); + context.GameTokens.AddRange(tokens); + await context.SaveChangesAsync(); + await context.DisposeAsync(); + return new DatabaseContext(options); + } + + public static void SetupTestController(this ControllerBase controllerBase, string? body = null) + { + controllerBase.ControllerContext = GetMockControllerContext(body); + SetupTestGameToken(controllerBase, GetUnitTestToken()); + } + + public static ControllerContext GetMockControllerContext() => + new() + { + HttpContext = new DefaultHttpContext(), + }; + + private static ControllerContext GetMockControllerContext(string? body) => + new() + { + HttpContext = new DefaultHttpContext + { + Request = + { + ContentLength = body?.Length ?? 0, + Body = new MemoryStream(Encoding.ASCII.GetBytes(body ?? string.Empty)), + }, + }, + ActionDescriptor = new ControllerActionDescriptor + { + ActionName = "", + }, + }; + + private static void SetupTestGameToken(ControllerBase controller, GameTokenEntity token) + { + controller.HttpContext.Items["Token"] = token; + } +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests/DatabaseFactAttribute.cs b/ProjectLighthouse.Tests/Integration/DatabaseFactAttribute.cs similarity index 81% rename from ProjectLighthouse.Tests/DatabaseFactAttribute.cs rename to ProjectLighthouse.Tests/Integration/DatabaseFactAttribute.cs index 8c54b1bb..51cdc8a7 100644 --- a/ProjectLighthouse.Tests/DatabaseFactAttribute.cs +++ b/ProjectLighthouse.Tests/Integration/DatabaseFactAttribute.cs @@ -3,7 +3,7 @@ using LBPUnion.ProjectLighthouse.Database; using Microsoft.EntityFrameworkCore; using Xunit; -namespace LBPUnion.ProjectLighthouse.Tests; +namespace LBPUnion.ProjectLighthouse.Tests.Integration; public sealed class DatabaseFactAttribute : FactAttribute { @@ -16,7 +16,7 @@ public sealed class DatabaseFactAttribute : FactAttribute else lock (migrateLock) { - using DatabaseContext database = new(); + using DatabaseContext database = DatabaseContext.CreateNewInstance(); database.Database.Migrate(); } } diff --git a/ProjectLighthouse.Tests/LighthouseServerTest.cs b/ProjectLighthouse.Tests/Integration/LighthouseServerTest.cs similarity index 52% rename from ProjectLighthouse.Tests/LighthouseServerTest.cs rename to ProjectLighthouse.Tests/Integration/LighthouseServerTest.cs index 0f3fc290..ed691388 100644 --- a/ProjectLighthouse.Tests/LighthouseServerTest.cs +++ b/ProjectLighthouse.Tests/Integration/LighthouseServerTest.cs @@ -1,9 +1,10 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.IO; using System.Net.Http; +using System.Security.Cryptography; using System.Threading.Tasks; using System.Xml.Serialization; +using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Helpers; using LBPUnion.ProjectLighthouse.Tickets; @@ -12,48 +13,58 @@ using LBPUnion.ProjectLighthouse.Types.Users; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; +using Xunit; -namespace LBPUnion.ProjectLighthouse.Tests; +namespace LBPUnion.ProjectLighthouse.Tests.Integration; -[SuppressMessage("ReSharper", "UnusedMember.Global")] +[Collection(nameof(LighthouseServerTest))] public class LighthouseServerTest where TStartup : class { - public readonly HttpClient Client; - public readonly TestServer Server; + protected readonly HttpClient Client; + private readonly TestServer server; - public LighthouseServerTest() + protected LighthouseServerTest() { - this.Server = new TestServer(new WebHostBuilder().UseStartup()); - this.Client = this.Server.CreateClient(); + ServerConfiguration.Instance.DbConnectionString = "server=127.0.0.1;uid=root;pwd=lighthouse_tests;database=lighthouse_tests"; + ServerConfiguration.Instance.DigestKey.PrimaryDigestKey = "lighthouse"; + this.server = new TestServer(new WebHostBuilder().UseStartup()); + this.Client = this.server.CreateClient(); } - public async Task CreateRandomUser(int number = -1, bool createUser = true) - { - if (number == -1) number = new Random().Next(); - const string username = "unitTestUser"; + public TestServer GetTestServer() => this.server; - if (createUser) + protected async Task CreateRandomUser() + { + await using DatabaseContext database = DatabaseContext.CreateNewInstance(); + + int userId = RandomNumberGenerator.GetInt32(int.MaxValue); + const string username = "unitTestUser"; + // if user already exists, find another random number + while (await database.Users.AnyAsync(u => u.Username == $"{username}{userId}")) { - await using DatabaseContext database = new(); - if (await database.Users.FirstOrDefaultAsync(u => u.Username == $"{username}{number}") == null) - { - UserEntity user = await database.CreateUser($"{username}{number}", - CryptoHelper.BCryptHash($"unitTestPassword{number}")); - user.LinkedPsnId = (ulong)number; - await database.SaveChangesAsync(); - } + userId = RandomNumberGenerator.GetInt32(int.MaxValue); } - return $"{username}{number}"; + UserEntity user = new() + { + UserId = userId, + Username = $"{username}{userId}", + Password = CryptoHelper.BCryptHash($"unitTestPassword{userId}"), + LinkedPsnId = (ulong)userId, + }; + + database.Add(user); + await database.SaveChangesAsync(); + return user; } - public async Task AuthenticateResponse(int number = -1, bool createUser = true) + protected async Task AuthenticateResponse() { - string username = await this.CreateRandomUser(number, createUser); + UserEntity user = await this.CreateRandomUser(); byte[] ticketData = new TicketBuilder() - .SetUsername($"{username}{number}") - .SetUserId((ulong)number) + .SetUsername($"{user.Username}{user.UserId}") + .SetUserId((ulong)user.UserId) .Build(); HttpResponseMessage response = await this.Client.PostAsync @@ -61,9 +72,9 @@ public class LighthouseServerTest where TStartup : class return response; } - public async Task Authenticate(int number = -1) + protected async Task Authenticate() { - HttpResponseMessage response = await this.AuthenticateResponse(number); + HttpResponseMessage response = await this.AuthenticateResponse(); string responseContent = await response.Content.ReadAsStringAsync(); @@ -71,12 +82,18 @@ public class LighthouseServerTest where TStartup : class return (LoginResult)serializer.Deserialize(new StringReader(responseContent))!; } - public Task AuthenticatedRequest(string endpoint, string mmAuth) => this.AuthenticatedRequest(endpoint, mmAuth, HttpMethod.Get); + protected Task AuthenticatedRequest(string endpoint, string mmAuth) => this.AuthenticatedRequest(endpoint, mmAuth, HttpMethod.Get); - public Task AuthenticatedRequest(string endpoint, string mmAuth, HttpMethod method) + private static string GetDigestCookie(string mmAuth) => mmAuth["MM_AUTH=".Length..]; + + private Task AuthenticatedRequest(string endpoint, string mmAuth, HttpMethod method) { using HttpRequestMessage requestMessage = new(method, endpoint); requestMessage.Headers.Add("Cookie", mmAuth); + string path = endpoint.Split("?", StringSplitOptions.RemoveEmptyEntries)[0]; + + string digest = CryptoHelper.ComputeDigest(path, GetDigestCookie(mmAuth), Array.Empty(), "lighthouse"); + requestMessage.Headers.Add("X-Digest-A", digest); return this.Client.SendAsync(requestMessage); } @@ -89,13 +106,15 @@ public class LighthouseServerTest where TStartup : class return await this.Client.PostAsync($"/LITTLEBIGPLANETPS3_XML/upload/{hash}", new ByteArrayContent(bytes)); } - public async Task AuthenticatedUploadFileEndpointRequest(string filePath, string mmAuth) + protected async Task AuthenticatedUploadFileEndpointRequest(string filePath, string mmAuth) { byte[] bytes = await File.ReadAllBytesAsync(filePath); string hash = CryptoHelper.Sha1Hash(bytes).ToLower(); using HttpRequestMessage requestMessage = new(HttpMethod.Post, $"/LITTLEBIGPLANETPS3_XML/upload/{hash}"); requestMessage.Headers.Add("Cookie", mmAuth); requestMessage.Content = new ByteArrayContent(bytes); + string digest = CryptoHelper.ComputeDigest($"/LITTLEBIGPLANETPS3_XML/upload/{hash}", GetDigestCookie(mmAuth), bytes, "lighthouse", true); + requestMessage.Headers.Add("X-Digest-B", digest); return await this.Client.SendAsync(requestMessage); } @@ -112,11 +131,13 @@ public class LighthouseServerTest where TStartup : class return await this.Client.SendAsync(requestMessage); } - public async Task AuthenticatedUploadDataRequest(string endpoint, byte[] data, string mmAuth) + protected async Task AuthenticatedUploadDataRequest(string endpoint, byte[] data, string mmAuth) { using HttpRequestMessage requestMessage = new(HttpMethod.Post, endpoint); requestMessage.Headers.Add("Cookie", mmAuth); requestMessage.Content = new ByteArrayContent(data); + string digest = CryptoHelper.ComputeDigest(endpoint, GetDigestCookie(mmAuth), data, "lighthouse"); + requestMessage.Headers.Add("X-Digest-A", digest); return await this.Client.SendAsync(requestMessage); } } \ No newline at end of file diff --git a/ProjectLighthouse.Tests/Tests/Serialization/SerializationDependencyTests.cs b/ProjectLighthouse.Tests/Integration/Serialization/SerializationDependencyTests.cs similarity index 97% rename from ProjectLighthouse.Tests/Tests/Serialization/SerializationDependencyTests.cs rename to ProjectLighthouse.Tests/Integration/Serialization/SerializationDependencyTests.cs index d2dbeb64..96f7f800 100644 --- a/ProjectLighthouse.Tests/Tests/Serialization/SerializationDependencyTests.cs +++ b/ProjectLighthouse.Tests/Integration/Serialization/SerializationDependencyTests.cs @@ -8,8 +8,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Xunit; -namespace LBPUnion.ProjectLighthouse.Tests.Serialization; +namespace LBPUnion.ProjectLighthouse.Tests.Integration.Serialization; +[Trait("Category", "Integration")] public class SerializationDependencyTests { private static IServiceProvider GetTestServiceProvider(params object[] dependencies) diff --git a/ProjectLighthouse.Tests/Tests/Serialization/SerializationTests.cs b/ProjectLighthouse.Tests/Integration/Serialization/SerializationTests.cs similarity index 98% rename from ProjectLighthouse.Tests/Tests/Serialization/SerializationTests.cs rename to ProjectLighthouse.Tests/Integration/Serialization/SerializationTests.cs index 5cf96a78..8d30fbd0 100644 --- a/ProjectLighthouse.Tests/Tests/Serialization/SerializationTests.cs +++ b/ProjectLighthouse.Tests/Integration/Serialization/SerializationTests.cs @@ -6,13 +6,14 @@ using LBPUnion.ProjectLighthouse.Types.Serialization; using Microsoft.Extensions.DependencyInjection; using Xunit; -namespace LBPUnion.ProjectLighthouse.Tests.Serialization; +namespace LBPUnion.ProjectLighthouse.Tests.Integration.Serialization; public class TestSerializable : ILbpSerializable, IHasCustomRoot { public virtual string GetRoot() => "xmlRoot"; } +[Trait("Category", "Integration")] public class SerializationTests { private static IServiceProvider GetEmptyServiceProvider() => new DefaultServiceProviderFactory().CreateServiceProvider(new ServiceCollection()); diff --git a/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj b/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj index 3f99c3c0..4122c496 100644 --- a/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj +++ b/ProjectLighthouse.Tests/ProjectLighthouse.Tests.csproj @@ -29,6 +29,8 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + diff --git a/ProjectLighthouse.Tests/Unit/EmailCooldownTests.cs b/ProjectLighthouse.Tests/Unit/EmailCooldownTests.cs new file mode 100644 index 00000000..682fe576 --- /dev/null +++ b/ProjectLighthouse.Tests/Unit/EmailCooldownTests.cs @@ -0,0 +1,97 @@ +using System.Collections.Concurrent; +using System.Reflection; +using LBPUnion.ProjectLighthouse.Helpers; +using LBPUnion.ProjectLighthouse.Types.Entities.Profile; +using Xunit; + +namespace LBPUnion.ProjectLighthouse.Tests.Unit; + +[Trait("Category", "Unit")] +public class EmailCooldownTests +{ + private static ConcurrentDictionary? GetInternalDict => + typeof(SMTPHelper).GetField("recentlySentMail", BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null) as ConcurrentDictionary; + + /* + * TODO This way of testing sucks because it relies on internal implementation, + * but half of this codebase is static singletons so my hand has kinda been forced + */ + + [Fact] + public void CanSendMail_WhenExpirationReached() + { + MethodInfo? canSendMethod = typeof(SMTPHelper).GetMethod("CanSendMail", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(canSendMethod); + + UserEntity userEntity = new() + { + UserId = 1, + }; + + Assert.NotNull(GetInternalDict); + + GetInternalDict.Clear(); + + GetInternalDict.TryAdd(1, 0); + + bool? canSend = (bool?)canSendMethod.Invoke(null, new object?[] { userEntity, }); + + const bool expectedValue = true; + + Assert.NotNull(canSend); + Assert.Equal(expectedValue, canSend); + } + + [Fact] + public void CanSendMail_WhenExpirationNotReached() + { + MethodInfo? canSendMethod = + typeof(SMTPHelper).GetMethod("CanSendMail", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(canSendMethod); + + UserEntity userEntity = new() + { + UserId = 1, + }; + + Assert.NotNull(GetInternalDict); + + GetInternalDict.Clear(); + + GetInternalDict.TryAdd(1, long.MaxValue); + + bool? canSend = (bool?)canSendMethod.Invoke(null, new object?[] { userEntity, }); + + const bool expectedValue = false; + + Assert.NotNull(canSend); + Assert.Equal(expectedValue, canSend); + } + + [Fact] + public void CanSendMail_ExpiredEntriesAreRemoved() + { + MethodInfo? canSendMethod = typeof(SMTPHelper).GetMethod("CanSendMail", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(canSendMethod); + + UserEntity userEntity = new() + { + UserId = 1, + }; + + Assert.NotNull(GetInternalDict); + + GetInternalDict.Clear(); + + GetInternalDict.TryAdd(2, 0); + GetInternalDict.TryAdd(3, TimeHelper.TimestampMillis - 100); + GetInternalDict.TryAdd(4, long.MaxValue); + + canSendMethod.Invoke(null, new object?[] { userEntity, }); + + Assert.False(GetInternalDict.TryGetValue(2, out _)); + Assert.False(GetInternalDict.TryGetValue(3, out _)); + Assert.True(GetInternalDict.TryGetValue(4, out _)); + } + +} \ No newline at end of file diff --git a/ProjectLighthouse.Tests/Tests/FileTypeTests.cs b/ProjectLighthouse.Tests/Unit/FileTypeTests.cs similarity index 95% rename from ProjectLighthouse.Tests/Tests/FileTypeTests.cs rename to ProjectLighthouse.Tests/Unit/FileTypeTests.cs index a5b1f742..719b0d84 100644 --- a/ProjectLighthouse.Tests/Tests/FileTypeTests.cs +++ b/ProjectLighthouse.Tests/Unit/FileTypeTests.cs @@ -3,8 +3,9 @@ using System.IO; using LBPUnion.ProjectLighthouse.Types.Resources; using Xunit; -namespace LBPUnion.ProjectLighthouse.Tests; +namespace LBPUnion.ProjectLighthouse.Tests.Unit; +[Trait("Category", "Unit")] public class FileTypeTests { [Fact] diff --git a/ProjectLighthouse.Tests/Tests/LocationTests.cs b/ProjectLighthouse.Tests/Unit/LocationTests.cs similarity index 97% rename from ProjectLighthouse.Tests/Tests/LocationTests.cs rename to ProjectLighthouse.Tests/Unit/LocationTests.cs index 081fabb2..a97b80af 100644 --- a/ProjectLighthouse.Tests/Tests/LocationTests.cs +++ b/ProjectLighthouse.Tests/Unit/LocationTests.cs @@ -6,8 +6,9 @@ using LBPUnion.ProjectLighthouse.Types.Misc; using LBPUnion.ProjectLighthouse.Types.Serialization; using Xunit; -namespace LBPUnion.ProjectLighthouse.Tests; +namespace LBPUnion.ProjectLighthouse.Tests.Unit; +[Trait("Category", "Unit")] public class LocationTests { [Fact] diff --git a/ProjectLighthouse.Tests/Tests/ResourceTests.cs b/ProjectLighthouse.Tests/Unit/ResourceTests.cs similarity index 97% rename from ProjectLighthouse.Tests/Tests/ResourceTests.cs rename to ProjectLighthouse.Tests/Unit/ResourceTests.cs index 7b931068..84d04918 100644 --- a/ProjectLighthouse.Tests/Tests/ResourceTests.cs +++ b/ProjectLighthouse.Tests/Unit/ResourceTests.cs @@ -4,8 +4,9 @@ using LBPUnion.ProjectLighthouse.Files; using LBPUnion.ProjectLighthouse.Types.Resources; using Xunit; -namespace LBPUnion.ProjectLighthouse.Tests; +namespace LBPUnion.ProjectLighthouse.Tests.Unit; +[Trait("Category", "Unit")] public class ResourceTests { [Fact] diff --git a/ProjectLighthouse/Administration/Maintenance/Commands/CreateAPIKeyCommand.cs b/ProjectLighthouse/Administration/Maintenance/Commands/CreateAPIKeyCommand.cs index 957d142b..de6f8a68 100644 --- a/ProjectLighthouse/Administration/Maintenance/Commands/CreateAPIKeyCommand.cs +++ b/ProjectLighthouse/Administration/Maintenance/Commands/CreateAPIKeyCommand.cs @@ -25,7 +25,7 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.Commands } key.Key = CryptoHelper.GenerateAuthToken(); key.Created = DateTime.Now; - DatabaseContext database = new(); + DatabaseContext database = DatabaseContext.CreateNewInstance(); await database.APIKeys.AddAsync(key); await database.SaveChangesAsync(); logger.LogSuccess($"The API key has been created (id: {key.Id}), however for security the token will only be shown once.", LogArea.Command); diff --git a/ProjectLighthouse/Administration/Maintenance/Commands/CreateUserCommand.cs b/ProjectLighthouse/Administration/Maintenance/Commands/CreateUserCommand.cs index b135ca34..e5c59dcd 100644 --- a/ProjectLighthouse/Administration/Maintenance/Commands/CreateUserCommand.cs +++ b/ProjectLighthouse/Administration/Maintenance/Commands/CreateUserCommand.cs @@ -12,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.Commands; public class CreateUserCommand : ICommand { - private readonly DatabaseContext _database = new(); + private readonly DatabaseContext _database = DatabaseContext.CreateNewInstance(); public async Task Run(string[] args, Logger logger) { diff --git a/ProjectLighthouse/Administration/Maintenance/Commands/DeleteUserCommand.cs b/ProjectLighthouse/Administration/Maintenance/Commands/DeleteUserCommand.cs index 812faf00..96d6ed1d 100644 --- a/ProjectLighthouse/Administration/Maintenance/Commands/DeleteUserCommand.cs +++ b/ProjectLighthouse/Administration/Maintenance/Commands/DeleteUserCommand.cs @@ -12,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.Commands; public class DeleteUserCommand : ICommand { - private readonly DatabaseContext database = new(); + private readonly DatabaseContext database = DatabaseContext.CreateNewInstance(); public string Name() => "Delete User"; public string[] Aliases() => new[] diff --git a/ProjectLighthouse/Administration/Maintenance/Commands/RenameUserCommand.cs b/ProjectLighthouse/Administration/Maintenance/Commands/RenameUserCommand.cs index ff32bb58..02a37ec9 100644 --- a/ProjectLighthouse/Administration/Maintenance/Commands/RenameUserCommand.cs +++ b/ProjectLighthouse/Administration/Maintenance/Commands/RenameUserCommand.cs @@ -12,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.Commands; public class RenameUserCommand : ICommand { - private readonly DatabaseContext database = new(); + private readonly DatabaseContext database = DatabaseContext.CreateNewInstance(); public async Task Run(string[] args, Logger logger) { UserEntity? user = await this.database.Users.FirstOrDefaultAsync(u => u.Username == args[0]); diff --git a/ProjectLighthouse/Administration/Maintenance/Commands/ResetPasswordCommand.cs b/ProjectLighthouse/Administration/Maintenance/Commands/ResetPasswordCommand.cs index c12b01c8..11c5dd82 100644 --- a/ProjectLighthouse/Administration/Maintenance/Commands/ResetPasswordCommand.cs +++ b/ProjectLighthouse/Administration/Maintenance/Commands/ResetPasswordCommand.cs @@ -13,7 +13,7 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.Commands; public class ResetPasswordCommand : ICommand { - private readonly DatabaseContext database = new(); + private readonly DatabaseContext database = DatabaseContext.CreateNewInstance(); public string Name() => "Reset Password"; public string[] Aliases() => new[] diff --git a/ProjectLighthouse/Administration/Maintenance/Commands/WipeTokensForUserCommand.cs b/ProjectLighthouse/Administration/Maintenance/Commands/WipeTokensForUserCommand.cs index 2b80ea71..59ec8501 100644 --- a/ProjectLighthouse/Administration/Maintenance/Commands/WipeTokensForUserCommand.cs +++ b/ProjectLighthouse/Administration/Maintenance/Commands/WipeTokensForUserCommand.cs @@ -12,7 +12,7 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.Commands; public class WipeTokensForUserCommand : ICommand { - private readonly DatabaseContext database = new(); + private readonly DatabaseContext database = DatabaseContext.CreateNewInstance(); public string Name() => "Wipe tokens for user"; public string[] Aliases() diff --git a/ProjectLighthouse/Administration/Maintenance/MaintenanceHelper.cs b/ProjectLighthouse/Administration/Maintenance/MaintenanceHelper.cs index 9300f34d..1c4af73d 100644 --- a/ProjectLighthouse/Administration/Maintenance/MaintenanceHelper.cs +++ b/ProjectLighthouse/Administration/Maintenance/MaintenanceHelper.cs @@ -71,7 +71,7 @@ public static class MaintenanceHelper public static async Task RunMigration(IMigrationTask migrationTask, DatabaseContext? database = null) { - database ??= new DatabaseContext(); + database ??= DatabaseContext.CreateNewInstance(); // Migrations should never be run twice. Debug.Assert(!await database.CompletedMigrations.Has(m => m.MigrationName == migrationTask.GetType().Name)); diff --git a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs b/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs index 5ec6d32f..2e9e02b3 100644 --- a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs +++ b/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/CleanupBrokenPhotosMaintenanceJob.cs @@ -14,7 +14,7 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.MaintenanceJobs; public class CleanupBrokenPhotosMaintenanceJob : IMaintenanceJob { - private readonly DatabaseContext database = new(); + private readonly DatabaseContext database = DatabaseContext.CreateNewInstance(); public string Name() => "Cleanup Broken Photos"; public string Description() => "Deletes all photos that have missing assets or invalid photo subjects."; diff --git a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/DeleteAllTokensMaintenanceJob.cs b/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/DeleteAllTokensMaintenanceJob.cs index 647f3f87..36335b87 100644 --- a/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/DeleteAllTokensMaintenanceJob.cs +++ b/ProjectLighthouse/Administration/Maintenance/MaintenanceJobs/DeleteAllTokensMaintenanceJob.cs @@ -7,7 +7,7 @@ namespace LBPUnion.ProjectLighthouse.Administration.Maintenance.MaintenanceJobs; public class DeleteAllTokensMaintenanceJob : IMaintenanceJob { - private readonly DatabaseContext database = new(); + private readonly DatabaseContext database = DatabaseContext.CreateNewInstance(); public string Name() => "Delete ALL Tokens"; public string Description() => "Deletes ALL game tokens and web tokens."; diff --git a/ProjectLighthouse/Administration/RepeatingTaskHandler.cs b/ProjectLighthouse/Administration/RepeatingTaskHandler.cs index 35ba1b36..531cc206 100644 --- a/ProjectLighthouse/Administration/RepeatingTaskHandler.cs +++ b/ProjectLighthouse/Administration/RepeatingTaskHandler.cs @@ -31,7 +31,7 @@ public static class RepeatingTaskHandler Queue taskQueue = new(); foreach (IRepeatingTask task in MaintenanceHelper.RepeatingTasks) taskQueue.Enqueue(task); - DatabaseContext database = new(); + DatabaseContext database = DatabaseContext.CreateNewInstance(); while (true) { diff --git a/ProjectLighthouse/Configuration/ServerStatics.cs b/ProjectLighthouse/Configuration/ServerStatics.cs index fde1bdae..23bb4c16 100644 --- a/ProjectLighthouse/Configuration/ServerStatics.cs +++ b/ProjectLighthouse/Configuration/ServerStatics.cs @@ -16,7 +16,7 @@ public static class ServerStatics get { try { - using DatabaseContext db = new(); + using DatabaseContext db = DatabaseContext.CreateNewInstance(); return db.Database.CanConnect(); } catch(Exception e) diff --git a/ProjectLighthouse/Database/DatabaseContext.Utils.cs b/ProjectLighthouse/Database/DatabaseContext.Utils.cs index fd5317b3..f9e2e046 100644 --- a/ProjectLighthouse/Database/DatabaseContext.Utils.cs +++ b/ProjectLighthouse/Database/DatabaseContext.Utils.cs @@ -49,13 +49,6 @@ public partial class DatabaseContext await this.SaveChangesAsync(); - if (!ServerConfiguration.Instance.Mail.MailEnabled || emailAddress == null) return user; - - string body = "An account for Project Lighthouse has been registered with this email address.\n\n" + - $"You can login at {ServerConfiguration.Instance.ExternalUrl}."; - - SMTPHelper.SendEmail(emailAddress, "Project Lighthouse Account Created: " + username, body); - return user; } diff --git a/ProjectLighthouse/Database/DatabaseContext.cs b/ProjectLighthouse/Database/DatabaseContext.cs index 96cb7d44..30f33066 100644 --- a/ProjectLighthouse/Database/DatabaseContext.cs +++ b/ProjectLighthouse/Database/DatabaseContext.cs @@ -64,6 +64,14 @@ public partial class DatabaseContext : DbContext #endregion - protected override void OnConfiguring(DbContextOptionsBuilder options) - => options.UseMySql(ServerConfiguration.Instance.DbConnectionString, MySqlServerVersion.LatestSupportedServerVersion); + public DatabaseContext(DbContextOptions options) : base(options) + { } + + public static DatabaseContext CreateNewInstance() + { + DbContextOptionsBuilder builder = new(); + builder.UseMySql(ServerConfiguration.Instance.DbConnectionString, + MySqlServerVersion.LatestSupportedServerVersion); + return new DatabaseContext(builder.Options); + } } \ No newline at end of file diff --git a/ProjectLighthouse/Extensions/RoomExtensions.cs b/ProjectLighthouse/Extensions/RoomExtensions.cs index 95e4154c..33f4a41a 100644 --- a/ProjectLighthouse/Extensions/RoomExtensions.cs +++ b/ProjectLighthouse/Extensions/RoomExtensions.cs @@ -16,7 +16,7 @@ public static class RoomExtensions foreach (int playerId in room.PlayerIds) { UserEntity? player = database.Users.FirstOrDefault(p => p.UserId == playerId); - Debug.Assert(player != null); + Debug.Assert(player != null, "RoomExtensions: player == null"); players.Add(player); } diff --git a/ProjectLighthouse/Helpers/EmailHelper.cs b/ProjectLighthouse/Helpers/EmailHelper.cs index 094eb086..9fd192d6 100644 --- a/ProjectLighthouse/Helpers/EmailHelper.cs +++ b/ProjectLighthouse/Helpers/EmailHelper.cs @@ -9,44 +9,76 @@ using LBPUnion.ProjectLighthouse.Database; using LBPUnion.ProjectLighthouse.Extensions; using LBPUnion.ProjectLighthouse.Types.Entities.Profile; using LBPUnion.ProjectLighthouse.Types.Entities.Token; +using LBPUnion.ProjectLighthouse.Types.Mail; using Microsoft.EntityFrameworkCore; namespace LBPUnion.ProjectLighthouse.Helpers; -public partial class SMTPHelper +public static class SMTPHelper { // (User id, timestamp of last request + 30 seconds) - private static readonly ConcurrentDictionary recentlySentEmail = new(); + private static readonly ConcurrentDictionary recentlySentMail = new(); - public static async Task SendVerificationEmail(DatabaseContext database, UserEntity user) + private const long emailCooldown = 1000 * 30; + + private static bool CanSendMail(UserEntity user) { // Remove expired entries - for (int i = recentlySentEmail.Count - 1; i >= 0; i--) + for (int i = recentlySentMail.Count - 1; i >= 0; i--) { - KeyValuePair entry = recentlySentEmail.ElementAt(i); - bool valueExists = recentlySentEmail.TryGetValue(entry.Key, out long timestamp); - if (!valueExists) + KeyValuePair entry = recentlySentMail.ElementAt(i); + if (recentlySentMail.TryGetValue(entry.Key, out long expiration) && + TimeHelper.TimestampMillis > expiration) { - recentlySentEmail.TryRemove(entry.Key, out _); - continue; + recentlySentMail.TryRemove(entry.Key, out _); } - - if (TimeHelper.TimestampMillis > timestamp) recentlySentEmail.TryRemove(entry.Key, out _); } - - if (recentlySentEmail.ContainsKey(user.UserId)) + if (recentlySentMail.TryGetValue(user.UserId, out long userExpiration)) { - bool valueExists = recentlySentEmail.TryGetValue(user.UserId, out long timestamp); - if (!valueExists) - { - recentlySentEmail.TryRemove(user.UserId, out _); - } - else if (timestamp > TimeHelper.TimestampMillis) - { - return true; - } + return TimeHelper.TimestampMillis > userExpiration; } + // If they don't have an entry in the dictionary then they can't be on cooldown + return true; + } + + public static async Task SendPasswordResetEmail(DatabaseContext database, IMailService mail, UserEntity user) + { + if (!CanSendMail(user)) return; + + PasswordResetTokenEntity token = new() + { + Created = DateTime.Now, + UserId = user.UserId, + ResetToken = CryptoHelper.GenerateAuthToken(), + }; + + database.PasswordResetTokens.Add(token); + await database.SaveChangesAsync(); + + string messageBody = $"Hello, {user.Username}.\n\n" + + "A request to reset your account's password was issued. If this wasn't you, this can probably be ignored.\n\n" + + $"If this was you, your {ServerConfiguration.Instance.Customization.ServerName} password can be reset at the following link:\n" + + $"{ServerConfiguration.Instance.ExternalUrl}/passwordReset?token={token.ResetToken}"; + + await mail.SendEmailAsync(user.EmailAddress, $"Project Lighthouse Password Reset Request for {user.Username}", messageBody); + + recentlySentMail.TryAdd(user.UserId, TimeHelper.TimestampMillis + emailCooldown); + } + + public static void SendRegistrationEmail(IMailService mail, UserEntity user) + { + // There is intentionally no cooldown here because this is only used for registration + // and a user can only be registered once, i.e. this should only be called once per user + string body = "An account for Project Lighthouse has been registered with this email address.\n\n" + + $"You can login at {ServerConfiguration.Instance.ExternalUrl}."; + + mail.SendEmail(user.EmailAddress, "Project Lighthouse Account Created: " + user.Username, body); + } + + public static async Task SendVerificationEmail(DatabaseContext database, IMailService mail, UserEntity user) + { + if (!CanSendMail(user)) return false; string? existingToken = await database.EmailVerificationTokens.Where(v => v.UserId == user.UserId) .Select(v => v.EmailToken) @@ -69,10 +101,10 @@ public partial class SMTPHelper $"To verify your account, click the following link: {ServerConfiguration.Instance.ExternalUrl}/verifyEmail?token={verifyToken.EmailToken}\n\n\n" + "If this wasn't you, feel free to ignore this email."; - bool success = await SendEmailAsync(user.EmailAddress, "Project Lighthouse Email Verification", body); + bool success = await mail.SendEmailAsync(user.EmailAddress, "Project Lighthouse Email Verification", body); // Don't send another email for 30 seconds - recentlySentEmail.TryAdd(user.UserId, TimeHelper.TimestampMillis + 30 * 1000); + recentlySentMail.TryAdd(user.UserId, TimeHelper.TimestampMillis + emailCooldown); return success; } } \ No newline at end of file diff --git a/ProjectLighthouse/Helpers/RoomHelper.cs b/ProjectLighthouse/Helpers/RoomHelper.cs index 0fbe2acc..5d98f624 100644 --- a/ProjectLighthouse/Helpers/RoomHelper.cs +++ b/ProjectLighthouse/Helpers/RoomHelper.cs @@ -19,7 +19,7 @@ using LBPUnion.ProjectLighthouse.Types.Users; namespace LBPUnion.ProjectLighthouse.Helpers; -public class RoomHelper +public static class RoomHelper { public static readonly object RoomLock = new(); public static StorableList Rooms => RoomStore.GetRooms(); @@ -87,7 +87,7 @@ public class RoomHelper Locations = new List(), }; - foreach (UserEntity player in room.GetPlayers(new DatabaseContext())) + foreach (UserEntity player in room.GetPlayers(DatabaseContext.CreateNewInstance())) { response.Players.Add ( @@ -204,7 +204,7 @@ public class RoomHelper #endif int roomCountBeforeCleanup = rooms.Count(); - database ??= new DatabaseContext(); + database ??= DatabaseContext.CreateNewInstance(); // Remove offline players from rooms foreach (Room room in rooms) diff --git a/ProjectLighthouse/Helpers/SMTPHelper.cs b/ProjectLighthouse/Mail/MailQueueService.cs similarity index 59% rename from ProjectLighthouse/Helpers/SMTPHelper.cs rename to ProjectLighthouse/Mail/MailQueueService.cs index 311b583e..ea5f63e8 100644 --- a/ProjectLighthouse/Helpers/SMTPHelper.cs +++ b/ProjectLighthouse/Mail/MailQueueService.cs @@ -1,19 +1,17 @@ using System; using System.Collections.Concurrent; -using System.Net; using System.Net.Mail; using System.Threading; using System.Threading.Tasks; using LBPUnion.ProjectLighthouse.Configuration; using LBPUnion.ProjectLighthouse.Logging; using LBPUnion.ProjectLighthouse.Types.Logging; +using LBPUnion.ProjectLighthouse.Types.Mail; -namespace LBPUnion.ProjectLighthouse.Helpers; +namespace LBPUnion.ProjectLighthouse.Mail; -public partial class SMTPHelper +public class MailQueueService : IMailService, IDisposable { - internal static readonly SMTPHelper Instance = new(); - private readonly MailAddress fromAddress; private readonly ConcurrentQueue emailQueue = new(); @@ -24,13 +22,15 @@ public partial class SMTPHelper private readonly Task emailThread; - private SMTPHelper() + private readonly IMailSender mailSender; + + public MailQueueService(IMailSender mailSender) { if (!ServerConfiguration.Instance.Mail.MailEnabled) return; - this.fromAddress = new MailAddress(ServerConfiguration.Instance.Mail.FromAddress, ServerConfiguration.Instance.Mail.FromName); + this.mailSender = mailSender; - this.stopSignal = false; + this.fromAddress = new MailAddress(ServerConfiguration.Instance.Mail.FromAddress, ServerConfiguration.Instance.Mail.FromName); this.emailThread = Task.Factory.StartNew(this.EmailQueue); } @@ -43,12 +43,7 @@ public partial class SMTPHelper try { - using SmtpClient client = new(ServerConfiguration.Instance.Mail.Host, ServerConfiguration.Instance.Mail.Port) - { - EnableSsl = ServerConfiguration.Instance.Mail.UseSSL, - Credentials = new NetworkCredential(ServerConfiguration.Instance.Mail.Username, ServerConfiguration.Instance.Mail.Password), - }; - await client.SendMailAsync(entry.Message); + this.mailSender.SendEmail(entry.Message); entry.Result.SetResult(true); } catch (Exception e) @@ -59,27 +54,31 @@ public partial class SMTPHelper } } - public static void Dispose() + public void Dispose() { - Instance.stopSignal = true; - Instance.emailThread.Wait(); - Instance.emailThread.Dispose(); + this.stopSignal = true; + if (this.emailThread != null) + { + this.emailThread.Wait(); + this.emailThread.Dispose(); + } + GC.SuppressFinalize(this); } - public static void SendEmail(string recipientAddress, string subject, string body) + public void SendEmail(string recipientAddress, string subject, string body) { TaskCompletionSource resultTask = new(); - Instance.SendEmail(recipientAddress, subject, body, resultTask); + this.SendEmail(recipientAddress, subject, body, resultTask); } - public static Task SendEmailAsync(string recipientAddress, string subject, string body) + public Task SendEmailAsync(string recipientAddress, string subject, string body) { TaskCompletionSource resultTask = new(); - Instance.SendEmail(recipientAddress, subject, body, resultTask); + this.SendEmail(recipientAddress, subject, body, resultTask); return resultTask.Task; } - public void SendEmail(string recipientAddress, string subject, string body, TaskCompletionSource resultTask) + private void SendEmail(string recipientAddress, string subject, string body, TaskCompletionSource resultTask) { if (!ServerConfiguration.Instance.Mail.MailEnabled) { @@ -87,7 +86,7 @@ public partial class SMTPHelper return; } - MailMessage message = new(Instance.fromAddress, new MailAddress(recipientAddress)) + MailMessage message = new(this.fromAddress, new MailAddress(recipientAddress)) { Subject = subject, Body = body, @@ -97,10 +96,10 @@ public partial class SMTPHelper this.emailSemaphore.Release(); } - internal class EmailEntry + private class EmailEntry { - public MailMessage Message { get; set; } - public TaskCompletionSource Result { get; set; } + public MailMessage Message { get; } + public TaskCompletionSource Result { get; } public EmailEntry(MailMessage message, TaskCompletionSource result) { diff --git a/ProjectLighthouse/Mail/NullMailService.cs b/ProjectLighthouse/Mail/NullMailService.cs new file mode 100644 index 00000000..1f73e54f --- /dev/null +++ b/ProjectLighthouse/Mail/NullMailService.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; +using LBPUnion.ProjectLighthouse.Types.Mail; + +namespace LBPUnion.ProjectLighthouse.Mail; + +public class NullMailService : IMailService, IDisposable +{ + public void SendEmail(string recipientAddress, string subject, string body) { } + public Task SendEmailAsync(string recipientAddress, string subject, string body) => Task.FromResult(true); + public void Dispose() => GC.SuppressFinalize(this); +} \ No newline at end of file diff --git a/ProjectLighthouse/Mail/SmtpMailSender.cs b/ProjectLighthouse/Mail/SmtpMailSender.cs new file mode 100644 index 00000000..806d5fe7 --- /dev/null +++ b/ProjectLighthouse/Mail/SmtpMailSender.cs @@ -0,0 +1,20 @@ +using System.Net; +using System.Net.Mail; +using LBPUnion.ProjectLighthouse.Configuration; +using LBPUnion.ProjectLighthouse.Types.Mail; + +namespace LBPUnion.ProjectLighthouse.Mail; + +public class SmtpMailSender : IMailSender +{ + public async void SendEmail(MailMessage message) + { + using SmtpClient client = new(ServerConfiguration.Instance.Mail.Host, ServerConfiguration.Instance.Mail.Port) + { + EnableSsl = ServerConfiguration.Instance.Mail.UseSSL, + Credentials = new NetworkCredential(ServerConfiguration.Instance.Mail.Username, + ServerConfiguration.Instance.Mail.Password), + }; + await client.SendMailAsync(message); + } +} \ No newline at end of file diff --git a/ProjectLighthouse/StartupTasks.cs b/ProjectLighthouse/StartupTasks.cs index 5cde9d13..85cdcd7d 100644 --- a/ProjectLighthouse/StartupTasks.cs +++ b/ProjectLighthouse/StartupTasks.cs @@ -64,7 +64,7 @@ public static class StartupTasks } if (!dbConnected) Environment.Exit(1); - using DatabaseContext database = new(); + using DatabaseContext database = DatabaseContext.CreateNewInstance(); migrateDatabase(database).Wait(); diff --git a/ProjectLighthouse/Types/Entities/Level/SlotEntity.cs b/ProjectLighthouse/Types/Entities/Level/SlotEntity.cs index 556e250a..1ab0ec3e 100644 --- a/ProjectLighthouse/Types/Entities/Level/SlotEntity.cs +++ b/ProjectLighthouse/Types/Entities/Level/SlotEntity.cs @@ -141,16 +141,16 @@ public class SlotEntity public bool CommentsEnabled { get; set; } = true; [NotMapped] - public int Hearts => new DatabaseContext().HeartedLevels.Count(s => s.SlotId == this.SlotId); + public int Hearts => DatabaseContext.CreateNewInstance().HeartedLevels.Count(s => s.SlotId == this.SlotId); [NotMapped] - public int Comments => new DatabaseContext().Comments.Count(c => c.Type == CommentType.Level && c.TargetId == this.SlotId); + public int Comments => DatabaseContext.CreateNewInstance().Comments.Count(c => c.Type == CommentType.Level && c.TargetId == this.SlotId); [NotMapped] - public int Photos => new DatabaseContext().Photos.Count(p => p.SlotId == this.SlotId); + public int Photos => DatabaseContext.CreateNewInstance().Photos.Count(p => p.SlotId == this.SlotId); [NotMapped] - public int PhotosWithAuthor => new DatabaseContext().Photos.Count(p => p.SlotId == this.SlotId && p.CreatorId == this.CreatorId); + public int PhotosWithAuthor => DatabaseContext.CreateNewInstance().Photos.Count(p => p.SlotId == this.SlotId && p.CreatorId == this.CreatorId); [NotMapped] public int Plays => this.PlaysLBP1 + this.PlaysLBP2 + this.PlaysLBP3; @@ -161,11 +161,11 @@ public class SlotEntity [NotMapped] public int PlaysComplete => this.PlaysLBP1Complete + this.PlaysLBP2Complete + this.PlaysLBP3Complete; - public double RatingLBP1 => new DatabaseContext().RatedLevels.Where(r => r.SlotId == this.SlotId).Average(r => (double?)r.RatingLBP1) ?? 3.0; + public double RatingLBP1 => DatabaseContext.CreateNewInstance().RatedLevels.Where(r => r.SlotId == this.SlotId).Average(r => (double?)r.RatingLBP1) ?? 3.0; [NotMapped] - public int Thumbsup => new DatabaseContext().RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == 1); + public int Thumbsup => DatabaseContext.CreateNewInstance().RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == 1); [NotMapped] - public int Thumbsdown => new DatabaseContext().RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == -1); + public int Thumbsdown => DatabaseContext.CreateNewInstance().RatedLevels.Count(r => r.SlotId == this.SlotId && r.Rating == -1); } \ No newline at end of file diff --git a/ProjectLighthouse/Types/Entities/Profile/CommentEntity.cs b/ProjectLighthouse/Types/Entities/Profile/CommentEntity.cs index b5e42a96..5fa73204 100644 --- a/ProjectLighthouse/Types/Entities/Profile/CommentEntity.cs +++ b/ProjectLighthouse/Types/Entities/Profile/CommentEntity.cs @@ -38,7 +38,7 @@ public class CommentEntity public int ThumbsUp { get; set; } public int ThumbsDown { get; set; } - public string getComment() + public string GetCommentMessage() { if (!this.Deleted) { @@ -50,7 +50,7 @@ public class CommentEntity return "This comment has been deleted by the author."; } - using DatabaseContext database = new(); + using DatabaseContext database = DatabaseContext.CreateNewInstance(); UserEntity deletedBy = database.Users.FirstOrDefault(u => u.Username == this.DeletedBy); if (deletedBy != null && deletedBy.UserId == this.TargetId) diff --git a/ProjectLighthouse/Types/Mail/IMailSender.cs b/ProjectLighthouse/Types/Mail/IMailSender.cs new file mode 100644 index 00000000..d027df91 --- /dev/null +++ b/ProjectLighthouse/Types/Mail/IMailSender.cs @@ -0,0 +1,8 @@ +using System.Net.Mail; + +namespace LBPUnion.ProjectLighthouse.Types.Mail; + +public interface IMailSender +{ + public void SendEmail(MailMessage message); +} \ No newline at end of file diff --git a/ProjectLighthouse/Types/Mail/IMailService.cs b/ProjectLighthouse/Types/Mail/IMailService.cs new file mode 100644 index 00000000..f517fa6e --- /dev/null +++ b/ProjectLighthouse/Types/Mail/IMailService.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace LBPUnion.ProjectLighthouse.Types.Mail; + +public interface IMailService +{ + public void SendEmail(string recipientAddress, string subject, string body); + public Task SendEmailAsync(string recipientAddress, string subject, string body); +} \ No newline at end of file