mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-06-01 10:04:00 +00:00
Service refactor (#153)
* remove email out of register service * Update auth service, move JWT handling out of controller * add docker config for service auth test * Update mock email system * Format: ./src/Core/Service * Refactor authentication payloads and services for registration and login processes * Format: src/Core/API, src/Core/Service
This commit is contained in:
@@ -17,11 +17,17 @@
|
||||
<!-- Reqnroll core, xUnit adapter and code-behind generator -->
|
||||
<PackageReference Include="Reqnroll" Version="3.3.3" />
|
||||
<PackageReference Include="Reqnroll.xUnit" Version="3.3.3" />
|
||||
<PackageReference Include="Reqnroll.Tools.MsBuild.Generation" Version="3.3.3"
|
||||
PrivateAssets="all" />
|
||||
<PackageReference
|
||||
Include="Reqnroll.Tools.MsBuild.Generation"
|
||||
Version="3.3.3"
|
||||
PrivateAssets="all"
|
||||
/>
|
||||
|
||||
<!-- ASP.NET Core integration testing -->
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.1" />
|
||||
<PackageReference
|
||||
Include="Microsoft.AspNetCore.Mvc.Testing"
|
||||
Version="9.0.1"
|
||||
/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -35,7 +41,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\API.Core\API.Core.csproj" />
|
||||
<ProjectReference
|
||||
Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.Email\Infrastructure.Email.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
68
src/Core/API/API.Specs/Mocks/MockEmailProvider.cs
Normal file
68
src/Core/API/API.Specs/Mocks/MockEmailProvider.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using Infrastructure.Email;
|
||||
|
||||
namespace API.Specs.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// Mock email provider for testing that doesn't actually send emails.
|
||||
/// Tracks sent emails for verification in tests if needed.
|
||||
/// </summary>
|
||||
public class MockEmailProvider : IEmailProvider
|
||||
{
|
||||
public List<SentEmail> SentEmails { get; } = new();
|
||||
|
||||
public Task SendAsync(
|
||||
string to,
|
||||
string subject,
|
||||
string body,
|
||||
bool isHtml = true
|
||||
)
|
||||
{
|
||||
SentEmails.Add(
|
||||
new SentEmail
|
||||
{
|
||||
To = [to],
|
||||
Subject = subject,
|
||||
Body = body,
|
||||
IsHtml = isHtml,
|
||||
SentAt = DateTime.UtcNow,
|
||||
}
|
||||
);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendAsync(
|
||||
IEnumerable<string> to,
|
||||
string subject,
|
||||
string body,
|
||||
bool isHtml = true
|
||||
)
|
||||
{
|
||||
SentEmails.Add(
|
||||
new SentEmail
|
||||
{
|
||||
To = to.ToList(),
|
||||
Subject = subject,
|
||||
Body = body,
|
||||
IsHtml = isHtml,
|
||||
SentAt = DateTime.UtcNow,
|
||||
}
|
||||
);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
SentEmails.Clear();
|
||||
}
|
||||
|
||||
public class SentEmail
|
||||
{
|
||||
public List<string> To { get; init; } = new();
|
||||
public string Subject { get; init; } = string.Empty;
|
||||
public string Body { get; init; } = string.Empty;
|
||||
public bool IsHtml { get; init; }
|
||||
public DateTime SentAt { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -1,54 +1,38 @@
|
||||
using Infrastructure.Email;
|
||||
using Domain.Entities;
|
||||
using Service.Emails;
|
||||
|
||||
namespace API.Specs.Mocks;
|
||||
|
||||
/// <summary>
|
||||
/// Mock email service for testing that doesn't actually send emails.
|
||||
/// Tracks sent emails for verification in tests if needed.
|
||||
/// </summary>
|
||||
public class MockEmailProvider : IEmailProvider
|
||||
public class MockEmailService : IEmailService
|
||||
{
|
||||
public List<SentEmail> SentEmails { get; } = new();
|
||||
public List<RegistrationEmail> SentRegistrationEmails { get; } = new();
|
||||
|
||||
public Task SendAsync(string to, string subject, string body, bool isHtml = true)
|
||||
{
|
||||
SentEmails.Add(new SentEmail
|
||||
{
|
||||
To = [to],
|
||||
Subject = subject,
|
||||
Body = body,
|
||||
IsHtml = isHtml,
|
||||
SentAt = DateTime.UtcNow
|
||||
});
|
||||
public Task SendRegistrationEmailAsync(
|
||||
UserAccount createdUser,
|
||||
string confirmationToken
|
||||
)
|
||||
{
|
||||
SentRegistrationEmails.Add(
|
||||
new RegistrationEmail
|
||||
{
|
||||
UserAccount = createdUser,
|
||||
ConfirmationToken = confirmationToken,
|
||||
SentAt = DateTime.UtcNow,
|
||||
}
|
||||
);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task SendAsync(IEnumerable<string> to, string subject, string body, bool isHtml = true)
|
||||
{
|
||||
SentEmails.Add(new SentEmail
|
||||
{
|
||||
To = to.ToList(),
|
||||
Subject = subject,
|
||||
Body = body,
|
||||
IsHtml = isHtml,
|
||||
SentAt = DateTime.UtcNow
|
||||
});
|
||||
public void Clear()
|
||||
{
|
||||
SentRegistrationEmails.Clear();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
SentEmails.Clear();
|
||||
}
|
||||
|
||||
public class SentEmail
|
||||
{
|
||||
public List<string> To { get; init; } = new();
|
||||
public string Subject { get; init; } = string.Empty;
|
||||
public string Body { get; init; } = string.Empty;
|
||||
public bool IsHtml { get; init; }
|
||||
public DateTime SentAt { get; init; }
|
||||
}
|
||||
public class RegistrationEmail
|
||||
{
|
||||
public UserAccount UserAccount { get; init; } = null!;
|
||||
public string ConfirmationToken { get; init; } = string.Empty;
|
||||
public DateTime SentAt { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Reqnroll;
|
||||
using FluentAssertions;
|
||||
using API.Specs;
|
||||
using FluentAssertions;
|
||||
using Reqnroll;
|
||||
|
||||
namespace API.Specs.Steps;
|
||||
|
||||
@@ -20,7 +20,12 @@ public class ApiGeneralSteps(ScenarioContext scenario)
|
||||
return client;
|
||||
}
|
||||
|
||||
var factory = scenario.TryGetValue<TestApiFactory>(FactoryKey, out var f) ? f : new TestApiFactory();
|
||||
var factory = scenario.TryGetValue<TestApiFactory>(
|
||||
FactoryKey,
|
||||
out var f
|
||||
)
|
||||
? f
|
||||
: new TestApiFactory();
|
||||
scenario[FactoryKey] = factory;
|
||||
|
||||
client = factory.CreateClient();
|
||||
@@ -35,13 +40,21 @@ public class ApiGeneralSteps(ScenarioContext scenario)
|
||||
}
|
||||
|
||||
[When("I send an HTTP request {string} to {string} with body:")]
|
||||
public async Task WhenISendAnHttpRequestStringToStringWithBody(string method, string url, string jsonBody)
|
||||
public async Task WhenISendAnHttpRequestStringToStringWithBody(
|
||||
string method,
|
||||
string url,
|
||||
string jsonBody
|
||||
)
|
||||
{
|
||||
var client = GetClient();
|
||||
|
||||
var requestMessage = new HttpRequestMessage(new HttpMethod(method), url)
|
||||
{
|
||||
Content = new StringContent(jsonBody, System.Text.Encoding.UTF8, "application/json")
|
||||
Content = new StringContent(
|
||||
jsonBody,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json"
|
||||
),
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
@@ -52,10 +65,16 @@ public class ApiGeneralSteps(ScenarioContext scenario)
|
||||
}
|
||||
|
||||
[When("I send an HTTP request {string} to {string}")]
|
||||
public async Task WhenISendAnHttpRequestStringToString(string method, string url)
|
||||
public async Task WhenISendAnHttpRequestStringToString(
|
||||
string method,
|
||||
string url
|
||||
)
|
||||
{
|
||||
var client = GetClient();
|
||||
var requestMessage = new HttpRequestMessage(new HttpMethod(method), url);
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
new HttpMethod(method),
|
||||
url
|
||||
);
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
var responseBody = await response.Content.ReadAsStringAsync();
|
||||
|
||||
@@ -66,34 +85,68 @@ public class ApiGeneralSteps(ScenarioContext scenario)
|
||||
[Then("the response status code should be {int}")]
|
||||
public void ThenTheResponseStatusCodeShouldBeInt(int expected)
|
||||
{
|
||||
scenario.TryGetValue<HttpResponseMessage>(ResponseKey, out var response).Should().BeTrue();
|
||||
scenario
|
||||
.TryGetValue<HttpResponseMessage>(ResponseKey, out var response)
|
||||
.Should()
|
||||
.BeTrue();
|
||||
((int)response!.StatusCode).Should().Be(expected);
|
||||
}
|
||||
|
||||
[Then("the response has HTTP status {int}")]
|
||||
public void ThenTheResponseHasHttpStatusInt(int expectedCode)
|
||||
{
|
||||
scenario.TryGetValue<HttpResponseMessage>(ResponseKey, out var response).Should().BeTrue("No response was received from the API");
|
||||
scenario
|
||||
.TryGetValue<HttpResponseMessage>(ResponseKey, out var response)
|
||||
.Should()
|
||||
.BeTrue("No response was received from the API");
|
||||
((int)response!.StatusCode).Should().Be(expectedCode);
|
||||
}
|
||||
|
||||
[Then("the response JSON should have {string} equal {string}")]
|
||||
public void ThenTheResponseJsonShouldHaveStringEqualString(string field, string expected)
|
||||
public void ThenTheResponseJsonShouldHaveStringEqualString(
|
||||
string field,
|
||||
string expected
|
||||
)
|
||||
{
|
||||
scenario.TryGetValue<HttpResponseMessage>(ResponseKey, out var response).Should().BeTrue();
|
||||
scenario.TryGetValue<string>(ResponseBodyKey, out var responseBody).Should().BeTrue();
|
||||
scenario
|
||||
.TryGetValue<HttpResponseMessage>(ResponseKey, out var response)
|
||||
.Should()
|
||||
.BeTrue();
|
||||
scenario
|
||||
.TryGetValue<string>(ResponseBodyKey, out var responseBody)
|
||||
.Should()
|
||||
.BeTrue();
|
||||
|
||||
using var doc = JsonDocument.Parse(responseBody!);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty(field, out var value))
|
||||
{
|
||||
root.TryGetProperty("payload", out var payloadElem).Should().BeTrue("Expected field '{0}' to be present either at the root or inside 'payload'", field);
|
||||
payloadElem.ValueKind.Should().Be(JsonValueKind.Object, "payload must be an object");
|
||||
payloadElem.TryGetProperty(field, out value).Should().BeTrue("Expected field '{0}' to be present inside 'payload'", field);
|
||||
root.TryGetProperty("payload", out var payloadElem)
|
||||
.Should()
|
||||
.BeTrue(
|
||||
"Expected field '{0}' to be present either at the root or inside 'payload'",
|
||||
field
|
||||
);
|
||||
payloadElem
|
||||
.ValueKind.Should()
|
||||
.Be(JsonValueKind.Object, "payload must be an object");
|
||||
payloadElem
|
||||
.TryGetProperty(field, out value)
|
||||
.Should()
|
||||
.BeTrue(
|
||||
"Expected field '{0}' to be present inside 'payload'",
|
||||
field
|
||||
);
|
||||
}
|
||||
|
||||
value.ValueKind.Should().Be(JsonValueKind.String, "Expected field '{0}' to be a string", field);
|
||||
value
|
||||
.ValueKind.Should()
|
||||
.Be(
|
||||
JsonValueKind.String,
|
||||
"Expected field '{0}' to be a string",
|
||||
field
|
||||
);
|
||||
value.GetString().Should().Be(expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Reqnroll;
|
||||
using FluentAssertions;
|
||||
using API.Specs;
|
||||
using FluentAssertions;
|
||||
using Reqnroll;
|
||||
|
||||
namespace API.Specs.Steps;
|
||||
|
||||
@@ -21,7 +21,12 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
return client;
|
||||
}
|
||||
|
||||
var factory = scenario.TryGetValue<TestApiFactory>(FactoryKey, out var f) ? f : new TestApiFactory();
|
||||
var factory = scenario.TryGetValue<TestApiFactory>(
|
||||
FactoryKey,
|
||||
out var f
|
||||
)
|
||||
? f
|
||||
: new TestApiFactory();
|
||||
scenario[FactoryKey] = factory;
|
||||
|
||||
client = factory.CreateClient();
|
||||
@@ -45,15 +50,25 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
public async Task WhenISubmitALoginRequestWithAUsernameAndPassword()
|
||||
{
|
||||
var client = GetClient();
|
||||
var (username, password) = scenario.TryGetValue<(string username, string password)>(TestUserKey, out var user)
|
||||
var (username, password) = scenario.TryGetValue<(
|
||||
string username,
|
||||
string password
|
||||
)>(TestUserKey, out var user)
|
||||
? user
|
||||
: ("test.user", "password");
|
||||
|
||||
var body = JsonSerializer.Serialize(new { username, password });
|
||||
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/login")
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
"/api/auth/login"
|
||||
)
|
||||
{
|
||||
Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json")
|
||||
Content = new StringContent(
|
||||
body,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json"
|
||||
),
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
@@ -69,9 +84,16 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
var client = GetClient();
|
||||
var body = JsonSerializer.Serialize(new { password = "test" });
|
||||
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/login")
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
"/api/auth/login"
|
||||
)
|
||||
{
|
||||
Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json")
|
||||
Content = new StringContent(
|
||||
body,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json"
|
||||
),
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
@@ -87,9 +109,16 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
var client = GetClient();
|
||||
var body = JsonSerializer.Serialize(new { username = "test" });
|
||||
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/login")
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
"/api/auth/login"
|
||||
)
|
||||
{
|
||||
Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json")
|
||||
Content = new StringContent(
|
||||
body,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json"
|
||||
),
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
@@ -103,9 +132,16 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
public async Task WhenISubmitALoginRequestWithBothUsernameAndPasswordMissing()
|
||||
{
|
||||
var client = GetClient();
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/login")
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
"/api/auth/login"
|
||||
)
|
||||
{
|
||||
Content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json")
|
||||
Content = new StringContent(
|
||||
"{}",
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json"
|
||||
),
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
@@ -118,37 +154,55 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
[Then("the response JSON should have an access token")]
|
||||
public void ThenTheResponseJsonShouldHaveAnAccessToken()
|
||||
{
|
||||
scenario.TryGetValue<HttpResponseMessage>(ResponseKey, out var response).Should().BeTrue();
|
||||
scenario.TryGetValue<string>(ResponseBodyKey, out var responseBody).Should().BeTrue();
|
||||
scenario
|
||||
.TryGetValue<HttpResponseMessage>(ResponseKey, out var response)
|
||||
.Should()
|
||||
.BeTrue();
|
||||
scenario
|
||||
.TryGetValue<string>(ResponseBodyKey, out var responseBody)
|
||||
.Should()
|
||||
.BeTrue();
|
||||
|
||||
var doc = JsonDocument.Parse(responseBody!);
|
||||
var root = doc.RootElement;
|
||||
JsonElement tokenElem = default;
|
||||
var hasToken = false;
|
||||
|
||||
|
||||
if (root.TryGetProperty("payload", out var payloadElem) && payloadElem.ValueKind == JsonValueKind.Object)
|
||||
if (
|
||||
root.TryGetProperty("payload", out var payloadElem)
|
||||
&& payloadElem.ValueKind == JsonValueKind.Object
|
||||
)
|
||||
{
|
||||
hasToken = payloadElem.TryGetProperty("accessToken", out tokenElem)
|
||||
|| payloadElem.TryGetProperty("AccessToken", out tokenElem);
|
||||
hasToken =
|
||||
payloadElem.TryGetProperty("accessToken", out tokenElem)
|
||||
|| payloadElem.TryGetProperty("AccessToken", out tokenElem);
|
||||
}
|
||||
|
||||
|
||||
hasToken.Should().BeTrue("Expected an access token either at the root or inside 'payload'");
|
||||
hasToken
|
||||
.Should()
|
||||
.BeTrue(
|
||||
"Expected an access token either at the root or inside 'payload'"
|
||||
);
|
||||
|
||||
var token = tokenElem.GetString();
|
||||
token.Should().NotBeNullOrEmpty();
|
||||
}
|
||||
|
||||
|
||||
[When("I submit a login request using a GET request")]
|
||||
public async Task WhenISubmitALoginRequestUsingAgetRequest()
|
||||
{
|
||||
var client = GetClient();
|
||||
// testing GET
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/api/auth/login")
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
"/api/auth/login"
|
||||
)
|
||||
{
|
||||
Content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json")
|
||||
Content = new StringContent(
|
||||
"{}",
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json"
|
||||
),
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
@@ -184,14 +238,21 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
lastName,
|
||||
email,
|
||||
dateOfBirth,
|
||||
password
|
||||
password,
|
||||
};
|
||||
|
||||
var body = JsonSerializer.Serialize(registrationData);
|
||||
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/auth/register")
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Post,
|
||||
"/api/auth/register"
|
||||
)
|
||||
{
|
||||
Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json")
|
||||
Content = new StringContent(
|
||||
body,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json"
|
||||
),
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
@@ -205,9 +266,16 @@ public class AuthSteps(ScenarioContext scenario)
|
||||
public async Task WhenISubmitARegistrationRequestUsingAGetRequest()
|
||||
{
|
||||
var client = GetClient();
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/api/auth/register")
|
||||
var requestMessage = new HttpRequestMessage(
|
||||
HttpMethod.Get,
|
||||
"/api/auth/register"
|
||||
)
|
||||
{
|
||||
Content = new StringContent("{}", System.Text.Encoding.UTF8, "application/json")
|
||||
Content = new StringContent(
|
||||
"{}",
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json"
|
||||
),
|
||||
};
|
||||
|
||||
var response = await client.SendAsync(requestMessage);
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Service.Emails;
|
||||
|
||||
namespace API.Specs
|
||||
{
|
||||
@@ -16,16 +17,29 @@ namespace API.Specs
|
||||
|
||||
builder.ConfigureServices(services =>
|
||||
{
|
||||
// Replace the real email service with mock for testing
|
||||
var descriptor = services.SingleOrDefault(
|
||||
d => d.ServiceType == typeof(IEmailProvider));
|
||||
// Replace the real email provider with mock for testing
|
||||
var emailProviderDescriptor = services.SingleOrDefault(d =>
|
||||
d.ServiceType == typeof(IEmailProvider)
|
||||
);
|
||||
|
||||
if (descriptor != null)
|
||||
if (emailProviderDescriptor != null)
|
||||
{
|
||||
services.Remove(descriptor);
|
||||
services.Remove(emailProviderDescriptor);
|
||||
}
|
||||
|
||||
services.AddScoped<IEmailProvider, MockEmailProvider>();
|
||||
|
||||
// Replace the real email service with mock for testing
|
||||
var emailServiceDescriptor = services.SingleOrDefault(d =>
|
||||
d.ServiceType == typeof(IEmailService)
|
||||
);
|
||||
|
||||
if (emailServiceDescriptor != null)
|
||||
{
|
||||
services.Remove(emailServiceDescriptor);
|
||||
}
|
||||
|
||||
services.AddScoped<IEmailService, MockEmailService>();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user