Add more unit tests (#757)

* Reorganize tests into unit/integration pattern

* Make DbSets virtual so they can be overridden by tests

* Add MessageControllerTests

* Implement DigestMiddlewareTests

* Refactor SMTPHelper to follow DI pattern which allows for mocking in unit tests.

* Fix MailQueueService service registration and shutdown

* Implement tests for Status and StatisticsController and reorganize tests

* Start working on UserControllerTests

* Start refactoring tests to use In-Memory EF provider

* Refactor integration tests to reset the database every time
Change default unit testing database credentials

* Update credentials to use default root with different passwords

* Throw exception when integration db is not available instead of falling back to in-memory

* Evaluate DbConnected every time

* Remove default DbContext constructor

* Setup DbContexts with options builder

* Convert remaining Moq DbContexts to InMemory ones

* Add more tests and use Assert.IsType for testing status code

* Add collection attribute to LighthouseServerTest

* Remove unused directives and calculate digest in tests

* Fix digest calculation in tests

* Add test database call

* Clear rooms after each test

* Fix CommentControllerTests.cs

* Disable test parallelization for gameserver tests

* Fix failing tests

Fix SlotTests

Make CreateUser actually add user to database

Fix dbConnected Lazy and change expected status codes

Properly Remove fragment from url for digest calculation

Fix digest calculation for regular requests

[skip ci] Remove unused directive

Don't use Database CreateUser function

Get rid of userId argument for generating random user

Rewrite logic for generating random users

Fix integration tests

* Implement changes from self-code review

* Fix registration tests

* Replace MailQueueService usages with IMailService
This commit is contained in:
Josh 2023-05-15 15:00:33 -05:00 committed by GitHub
parent 02f520c717
commit 1bf4ed6218
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 2419 additions and 378 deletions

View file

@ -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

View file

@ -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<DatabaseContext>();
services.AddDbContext<DatabaseContext>(builder =>
{
builder.UseMySql(ServerConfiguration.Instance.DbConnectionString,
MySqlServerVersion.LatestSupportedServerVersion);
});
services.AddSwaggerGen
(

View file

@ -96,7 +96,7 @@ public class CommentController : ControllerBase
GameTokenEntity token = this.GetToken();
GameComment? comment = await this.DeserializeBody<GameComment>();
if (comment == null) return this.BadRequest();
if (comment?.Message == null) return this.BadRequest();
if ((slotId == 0 || SlotHelper.IsTypeInvalid(slotType)) == (username == null)) return this.BadRequest();

View file

@ -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 <https://www.gnu.org/licenses/>.";
/// The response sent is the text that will appear in-game.
/// </summary>
[HttpPost("filter")]
public async Task<IActionResult> Filter()
public async Task<IActionResult> 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 <https://www.gnu.org/licenses/>.";
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 <https://www.gnu.org/licenses/>.";
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);
}

View file

@ -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;

View file

@ -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<DatabaseContext>();
services.AddDbContext<DatabaseContext>(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<ForwardedHeadersOptions>
(

View file

@ -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));
}
}

View file

@ -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();
}

View file

@ -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<IActionResult> 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";

View file

@ -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,

View file

@ -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);

View file

@ -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) : "";
}

View file

@ -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;

View file

@ -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);

View file

@ -42,7 +42,7 @@
</h1>
}
@{
await using DatabaseContext context = new();
await using DatabaseContext context = DatabaseContext.CreateNewInstance();
int hearts = Model.GetHeartCount(context);
int comments = Model.GetCommentCount(context);

View file

@ -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
{

View file

@ -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<DatabaseContext>();
services.AddDbContext<DatabaseContext>(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<ICaptchaService, CaptchaService>("CaptchaAPI",
client =>

View file

@ -1,3 +1,3 @@
using Xunit;
using Xunit;
[assembly: CollectionBehavior(DisableTestParallelization = true)]

View file

@ -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<GameServerTestStartup>
{
[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<GameServerTestStartup>
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);
}
}

View file

@ -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<GameServerTestStartup>
{
[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<GameServerTestStartup>
Assert.NotNull(userA);
Assert.NotNull(userB);
await database.RemoveUser(userA); // Only remove userA since userA and userB are the same user
}
}

View file

@ -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<GameServerTestStartup>
{
[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);
}
}
}

View file

@ -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<GameServerTestStartup>
{
[Fact]
public async Task Match_ShouldRejectEmptyData()
{
await IntegrationHelper.GetIntegrationDatabase();
LoginResult loginResult = await this.Authenticate();
HttpResponseMessage result = await this.AuthenticatedUploadDataRequest("/LITTLEBIGPLANETPS3_XML/match", Array.Empty<byte>(), 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);
}
}

View file

@ -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<GameServerTestStartup>
{
[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<GameServerTestStartup>
SlotEntity slotB = new()
{
Creator = userB,
CreatorId = userB.UserId,
Name = "slotB",
ResourceCollection = "",
@ -48,31 +45,23 @@ public class SlotTests : LighthouseServerTest<GameServerTestStartup>
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();
}
}

View file

@ -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<GameServerTestStartup>
{
public UploadTests()
@ -18,53 +20,73 @@ public class UploadTests : LighthouseServerTest<GameServerTestStartup>
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);
}
}

View file

@ -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<GameServerTestStartup>
{
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<byte>(), 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);
}
}

View file

@ -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("<comment><message>test</message></comment>");
const string expectedCommentMessage = "test";
IActionResult result = await commentController.PostComment("unittest", null, 0);
Assert.IsType<OkResult>(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("<comment><message>zamn</message></comment>");
CensorConfiguration.Instance.FilteredWordList = new List<string>
{
"zamn",
};
CensorConfiguration.Instance.UserInputFilterMode = FilterMode.Asterisks;
const string expectedCommentMessage = "****";
IActionResult result = await commentController.PostComment("unittest", null, 0);
Assert.IsType<OkResult>(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("<comment><message>zamn</message></comment>");
CensorConfiguration.Instance.FilteredWordList = new List<string>
{
"zamn",
};
CensorConfiguration.Instance.UserInputFilterMode = FilterMode.None;
IActionResult result = await commentController.PostComment("unittest", null, 0);
const string expectedCommentMessage = "zamn";
Assert.IsType<OkResult>(result);
CommentEntity? comment = dbMock.Comments.FirstOrDefault();
Assert.NotNull(comment);
Assert.Equal(expectedCommentMessage, comment.Message);
}
[Fact]
public async Task PostComment_ShouldPostUserLevelComment_WhenBodyIsValid()
{
List<SlotEntity> 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("<comment><message>test</message></comment>");
const string expectedCommentMessage = "test";
IActionResult result = await commentController.PostComment(null, "user", 1);
Assert.IsType<OkResult>(result);
CommentEntity? comment = dbMock.Comments.FirstOrDefault();
Assert.NotNull(comment);
Assert.Equal(expectedCommentMessage, comment.Message);
}
[Fact]
public async Task PostComment_ShouldPostDeveloperLevelComment_WhenBodyIsValid()
{
List<SlotEntity> 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("<comment><message>test</message></comment>");
const string expectedCommentMessage = "test";
IActionResult result = await commentController.PostComment(null, "developer", 12345);
Assert.IsType<OkResult>(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("<comment><message>test</message></comment>");
IActionResult result = await commentController.PostComment("unittest2", null, 0);
Assert.IsType<BadRequestResult>(result);
}
[Fact]
public async Task PostComment_ShouldNotPostUserLevelComment_WhenLevelInvalid()
{
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase();
CommentController commentController = new(dbMock);
commentController.SetupTestController("<comment><message>test</message></comment>");
IActionResult result = await commentController.PostComment(null, "user", 1);
Assert.IsType<BadRequestResult>(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<BadRequestResult>(result);
}
[Fact]
public async Task PostComment_ShouldNotPostComment_WhenBodyIsInvalid()
{
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase();
CommentController commentController = new(dbMock);
commentController.SetupTestController("<comment></comment>");
IActionResult result = await commentController.PostComment("unittest", null, 0);
Assert.IsType<BadRequestResult>(result);
}
[Fact]
public async Task PostComment_ShouldFail_WhenSlotTypeIsInvalid()
{
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase();
CommentController commentController = new(dbMock);
commentController.SetupTestController("<comment><message>test</message></comment>");
IActionResult result = await commentController.PostComment(null, "banana", 0);
Assert.IsType<BadRequestResult>(result);
}
[Fact]
public async Task PostComment_ShouldFail_WhenAllArgumentsAreEmpty()
{
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase();
CommentController commentController = new(dbMock);
commentController.SetupTestController("<comment><message>test</message></comment>");
IActionResult result = await commentController.PostComment(null, null, 0);
Assert.IsType<BadRequestResult>(result);
}
[Fact]
public async Task PostComment_ShouldFail_WhenSlotTypeAndUsernameAreProvided()
{
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase();
CommentController commentController = new(dbMock);
commentController.SetupTestController("<comment><message>test</message></comment>");
IActionResult result = await commentController.PostComment("unittest", "user", 10);
Assert.IsType<BadRequestResult>(result);
}
}

View file

@ -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 <https://www.gnu.org/licenses/>." + "\n";
IActionResult result = messageController.Eula();
Assert.IsType<OkObjectResult>(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 <https://www.gnu.org/licenses/>." + "\nunit test eula text";
IActionResult result = messageController.Eula();
Assert.IsType<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkResult>(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<OkObjectResult>(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<string>
{
"bruh",
};
const string expectedBody = "unit test message ****";
IActionResult result = await messageController.Filter(new NullMailService());
Assert.IsType<OkObjectResult>(result);
OkObjectResult? okObjectResult = result as OkObjectResult;
Assert.NotNull(okObjectResult);
Assert.NotNull(okObjectResult.Value);
Assert.Equal(expectedBody, (string)okObjectResult.Value);
}
private static Mock<IMailService> getMailServiceMock()
{
Mock<IMailService> mailMock = new();
mailMock.Setup(x => x.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
.Returns(Task.FromResult(true));
return mailMock;
}
[Fact]
public async void Filter_ShouldNotSendEmail_WhenMailDisabled()
{
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase();
Mock<IMailService> 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<string>();
const int expectedStatus = 200;
const string expected = "/setemail unittest@unittest.com";
IActionResult result = await messageController.Filter(mailMock.Object);
Assert.IsType<OkObjectResult>(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<IMailService> 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<OkResult>(result);
Assert.Equal(expectedEmail, dbMock.Users.First().EmailAddress);
mailMock.Verify(x => x.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
[Fact]
public async void Filter_ShouldNotSendEmail_WhenMailEnabled_AndEmailTaken()
{
List<UserEntity> users = new()
{
MockHelper.GetUnitTestUser(),
new UserEntity
{
UserId = 2,
EmailAddress = "unittest@unittest.com",
EmailAddressVerified = false,
},
};
await using DatabaseContext dbMock = await MockHelper.GetTestDatabase(users);
Mock<IMailService> 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<OkResult>(result);
mailMock.Verify(x => x.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), 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<UserEntity>
{
unitTestUser,
});
Mock<IMailService> 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<OkResult>(result);
mailMock.Verify(x => x.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), 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<UserEntity>
{
unitTestUser,
});
Mock<IMailService> 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<OkResult>(result);
mailMock.Verify(x => x.SendEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Never);
}
}

View file

@ -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<OkObjectResult>(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<SlotEntity> 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<OkObjectResult>(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<SlotEntity> 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<OkObjectResult>(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<SlotEntity> 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<OkObjectResult>(result);
OkObjectResult? objectResult = result as OkObjectResult;
Assert.NotNull(objectResult);
Assert.Equal(expectedTotal, objectResult.Value);
}
[Fact]
public async void TotalLevelCount_ShouldReturnCorrectCount_WhenSlotsAreNotCompatible()
{
List<SlotEntity> 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<OkObjectResult>(result);
OkObjectResult? objectResult = result as OkObjectResult;
Assert.NotNull(objectResult);
Assert.Equal(expectedStatusCode, objectResult.StatusCode);
Assert.Equal(expectedTotal, objectResult.Value);
}
}

View file

@ -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<OkResult>(result);
}
}

View file

@ -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<OkObjectResult>(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<NotFoundResult>(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<OkObjectResult>(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<OkObjectResult>(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<OkObjectResult>(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<UserEntity> 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<OkObjectResult>(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<BadRequestResult>(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<OkObjectResult>(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<UserEntity> 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<OkObjectResult>(result);
OkObjectResult? okObject = result as OkObjectResult;
Assert.NotNull(okObject);
Assert.Equal(expectedPins, dbMock.Users.First().Pins);
Assert.Equal(expectedResponse, okObject.Value);
}
}

View file

@ -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<string, StringValues>("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<string, StringValues>("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<string, StringValues>("Cookie", "MM_AUTH=unittest"),
KeyValuePair.Create<string, StringValues>("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<string, StringValues>("Cookie", "MM_AUTH=unittest"),
KeyValuePair.Create<string, StringValues>("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<string, StringValues>("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<string, StringValues>("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<string, StringValues>("Cookie", "MM_AUTH=unittest"),
KeyValuePair.Create<string, StringValues>("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<string, StringValues>("Cookie", "MM_AUTH=unittest"),
KeyValuePair.Create<string, StringValues>("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<string, StringValues>("Cookie", "MM_AUTH=unittest"),
KeyValuePair.Create<string, StringValues>("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<string, StringValues>("Cookie", "MM_AUTH=unittest"),
KeyValuePair.Create<string, StringValues>("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<string, StringValues>("Cookie", "MM_AUTH=unittest"),
KeyValuePair.Create<string, StringValues>("X-Digest-A", "80714c0936408855d86d47a650320f91895812d0"),
KeyValuePair.Create<string, StringValues>("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);
}
}

View file

@ -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<string, StringValues>("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<string, StringValues>("Cookie", "MM_AUTH=unittest"),
},
},
};
SetLastContactMiddleware middleware = new(httpContext =>
{
httpContext.Response.StatusCode = 200;
httpContext.Response.WriteAsync("");
return Task.CompletedTask;
});
List<LastContactEntity> 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<string, StringValues>("Cookie", "MM_AUTH=unittest"),
},
},
};
SetLastContactMiddleware middleware = new(httpContext =>
{
httpContext.Response.StatusCode = 200;
httpContext.Response.WriteAsync("");
return Task.CompletedTask;
});
List<GameTokenEntity> 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);
}
}

View file

@ -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);
}
}

View file

@ -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"));

View file

@ -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<WebsiteTestStartup>().UseWebRoot("StaticFiles").Build();
protected readonly IWebDriver Driver;
private readonly IWebHost webHost = new WebHostBuilder().UseKestrel().UseStartup<WebsiteTestStartup>().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>();
IServerAddressesFeature? serverAddressesFeature = this.webHost.ServerFeatures.Get<IServerAddressesFeature>();
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);
}

View file

@ -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;
}
}

View file

@ -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<bool> dbConnected = new(IsDbConnected);
private static bool IsDbConnected() => ServerStatics.DbConnected;
/// <summary>
/// Resets the database to a clean state and returns a new DatabaseContext.
/// </summary>
/// <returns>A new fresh instance of DatabaseContext</returns>
public static async Task<DatabaseContext> 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();
}
}

View file

@ -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<DatabaseContext> GetTestDatabase(IEnumerable<IList> sets, [CallerMemberName] string caller = "", [CallerLineNumber] int lineNum = 0)
{
Dictionary<Type, IList> 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<GameTokenEntity>
{
GetUnitTestToken(),
};
}
if (!setDict.TryGetValue(typeof(UserEntity), out _))
{
setDict[typeof(UserEntity)] = new List<UserEntity>
{
GetUnitTestUser(),
};
}
DbContextOptions<DatabaseContext> options = new DbContextOptionsBuilder<DatabaseContext>()
.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<DatabaseContext> GetTestDatabase(List<UserEntity>? users = null, List<GameTokenEntity>? tokens = null,
[CallerMemberName] string caller = "", [CallerLineNumber] int lineNum = 0
)
{
users ??= new List<UserEntity>
{
GetUnitTestUser(),
};
tokens ??= new List<GameTokenEntity>
{
GetUnitTestToken(),
};
DbContextOptions<DatabaseContext> options = new DbContextOptionsBuilder<DatabaseContext>()
.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;
}
}

View file

@ -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();
}
}

View file

@ -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<TStartup>))]
public class LighthouseServerTest<TStartup> 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<TStartup>());
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<TStartup>());
this.Client = this.server.CreateClient();
}
public async Task<string> 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<UserEntity> 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<HttpResponseMessage> AuthenticateResponse(int number = -1, bool createUser = true)
protected async Task<HttpResponseMessage> 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<TStartup> where TStartup : class
return response;
}
public async Task<LoginResult> Authenticate(int number = -1)
protected async Task<LoginResult> 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<TStartup> where TStartup : class
return (LoginResult)serializer.Deserialize(new StringReader(responseContent))!;
}
public Task<HttpResponseMessage> AuthenticatedRequest(string endpoint, string mmAuth) => this.AuthenticatedRequest(endpoint, mmAuth, HttpMethod.Get);
protected Task<HttpResponseMessage> AuthenticatedRequest(string endpoint, string mmAuth) => this.AuthenticatedRequest(endpoint, mmAuth, HttpMethod.Get);
public Task<HttpResponseMessage> AuthenticatedRequest(string endpoint, string mmAuth, HttpMethod method)
private static string GetDigestCookie(string mmAuth) => mmAuth["MM_AUTH=".Length..];
private Task<HttpResponseMessage> 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<byte>(), "lighthouse");
requestMessage.Headers.Add("X-Digest-A", digest);
return this.Client.SendAsync(requestMessage);
}
@ -89,13 +106,15 @@ public class LighthouseServerTest<TStartup> where TStartup : class
return await this.Client.PostAsync($"/LITTLEBIGPLANETPS3_XML/upload/{hash}", new ByteArrayContent(bytes));
}
public async Task<HttpResponseMessage> AuthenticatedUploadFileEndpointRequest(string filePath, string mmAuth)
protected async Task<HttpResponseMessage> 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<TStartup> where TStartup : class
return await this.Client.SendAsync(requestMessage);
}
public async Task<HttpResponseMessage> AuthenticatedUploadDataRequest(string endpoint, byte[] data, string mmAuth)
protected async Task<HttpResponseMessage> 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);
}
}

View file

@ -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)

View file

@ -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());

View file

@ -29,6 +29,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.4" />
</ItemGroup>
<ItemGroup>

View file

@ -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<int, long>? GetInternalDict =>
typeof(SMTPHelper).GetField("recentlySentMail", BindingFlags.NonPublic | BindingFlags.Static)?.GetValue(null) as ConcurrentDictionary<int, long>;
/*
* 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 _));
}
}

View file

@ -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]

View file

@ -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]

View file

@ -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]

View file

@ -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);

View file

@ -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)
{

View file

@ -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[]

View file

@ -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]);

View file

@ -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[]

View file

@ -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()

View file

@ -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));

View file

@ -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.";

View file

@ -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.";

View file

@ -31,7 +31,7 @@ public static class RepeatingTaskHandler
Queue<IRepeatingTask> taskQueue = new();
foreach (IRepeatingTask task in MaintenanceHelper.RepeatingTasks) taskQueue.Enqueue(task);
DatabaseContext database = new();
DatabaseContext database = DatabaseContext.CreateNewInstance();
while (true)
{

View file

@ -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)

View file

@ -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;
}

View file

@ -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<DatabaseContext> options) : base(options)
{ }
public static DatabaseContext CreateNewInstance()
{
DbContextOptionsBuilder<DatabaseContext> builder = new();
builder.UseMySql(ServerConfiguration.Instance.DbConnectionString,
MySqlServerVersion.LatestSupportedServerVersion);
return new DatabaseContext(builder.Options);
}
}

View file

@ -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);
}

View file

@ -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<int, long> recentlySentEmail = new();
private static readonly ConcurrentDictionary<int, long> recentlySentMail = new();
public static async Task<bool> 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<int, long> entry = recentlySentEmail.ElementAt(i);
bool valueExists = recentlySentEmail.TryGetValue(entry.Key, out long timestamp);
if (!valueExists)
KeyValuePair<int, long> 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<bool> 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;
}
}

View file

@ -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<Room> Rooms => RoomStore.GetRooms();
@ -87,7 +87,7 @@ public class RoomHelper
Locations = new List<string>(),
};
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)

View file

@ -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<EmailEntry> 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<bool> resultTask = new();
Instance.SendEmail(recipientAddress, subject, body, resultTask);
this.SendEmail(recipientAddress, subject, body, resultTask);
}
public static Task<bool> SendEmailAsync(string recipientAddress, string subject, string body)
public Task<bool> SendEmailAsync(string recipientAddress, string subject, string body)
{
TaskCompletionSource<bool> 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<bool> resultTask)
private void SendEmail(string recipientAddress, string subject, string body, TaskCompletionSource<bool> 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<bool> Result { get; set; }
public MailMessage Message { get; }
public TaskCompletionSource<bool> Result { get; }
public EmailEntry(MailMessage message, TaskCompletionSource<bool> result)
{

View file

@ -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<bool> SendEmailAsync(string recipientAddress, string subject, string body) => Task.FromResult(true);
public void Dispose() => GC.SuppressFinalize(this);
}

View file

@ -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);
}
}

View file

@ -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();

View file

@ -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);
}

View file

@ -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)

View file

@ -0,0 +1,8 @@
using System.Net.Mail;
namespace LBPUnion.ProjectLighthouse.Types.Mail;
public interface IMailSender
{
public void SendEmail(MailMessage message);
}

View file

@ -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<bool> SendEmailAsync(string recipientAddress, string subject, string body);
}