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