96 integration tests through PrivaPubHost, the real pipeline end to end:
- OAuth: the token's subject is the persona, and neither the token response,
verify_credentials nor the stored token entries name the root. A wrong password shows
an error and sets no login cookie; a login without the antiforgery token is a 400; the
return address never leaves the site; deny answers access_denied with no code; another
root's persona re-renders the choice with no code; a banned root is sent back to the
login and a code issued before the ban buys no token; force_login asks again; a code
works once and its reuse revokes the token it bought; password and refresh_token
grants are refused; a client_credentials token gets 401 on user routes; a read-only
token gets 403 with a Mastodon error on POST /api/v1/statuses; follow covers
read:follows; revoke works; the login and authorize pages send their CSP and no-store;
the 11th /oauth/login from one address in a minute is a 429.
- Accounts: sign-up, duplicates in any case, invalid models answer 400 with a message,
login and logout, recovery email, settings, password change, invitation sign-up and
login (refusing a persona named after the login), recovery without an email, through
an unreachable mail server, with a wrong and with a valid code, token refresh, the 11th
sign-up from one address, expired, garbage and foreign-key JWTs.
- Personas: a rootId in the body is ignored, the username regex and reserved names hold,
personas and groups share ReservedName, an update delivers Update{Person} to followers,
PublishedOn and the id's day fall within two weeks before creation, the list holds only
one's own personas, another root's persona cannot be updated.
- Groups: communities and circles are created, joining takes the code and the password,
members leave and owners cannot, a remote follow request becomes a member only on
approval, a circle never shows in lookup, account by id, v2 search or /@name, and its
/flock and /wardens answer 404 unsigned and to non-members, 200 to a member's signed GET.
- Moderation (Exclusive, it suspends localhost): ban, unban and remove; non-admins get
403; reports are listed without the reporter and resolved; domain blocks are inserted,
listed and deleted, bad domains refused, and a suspended server's delivery is answered
202 and kept nowhere; the data endpoints.
- AdminCommands: exit codes 0, 1 and 2 and the resulting policies.
Fixed:
- A banned or removed root kept using /clientapi with its JWT until it expired: only /api
re-checked the root. JwtEvents.TokenValidated now loads the root and fails the request
when it is banned or deleted, and takes the policy claims from the database, so a
demoted admin loses admin at once (and a promoted one gains it).
- The 401 and 403 bodies JwtEvents writes were PascalCase while every other /clientapi
answer is camelCase; they now use the web defaults.
- /clientapi/user/sniff/again (token refresh) answered an empty 200; it now answers a
fresh JwtUser, like login.
- Password recovery answered SMTP reply codes as HTTP statuses (421, 454, 554, and 550 for
an invalid address); a mail server failure is now 503 and an invalid address 400.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
446 lines
22 KiB
C#
446 lines
22 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.IdentityModel.Tokens;
|
|
|
|
using MongoDB.Entities;
|
|
|
|
using PrivaPub.ClientModels;
|
|
using PrivaPub.Models.Group;
|
|
using PrivaPub.Models.User;
|
|
using PrivaPub.Tests.Support;
|
|
using PrivaPub.Tests.Support.Host;
|
|
|
|
using System.IdentityModel.Tokens.Jwt;
|
|
using System.Net;
|
|
using System.Security.Claims;
|
|
using System.Text;
|
|
using System.Text.Json.Nodes;
|
|
|
|
using GroupEntity = PrivaPub.Models.Group.Group;
|
|
|
|
namespace PrivaPub.Tests.Http
|
|
{
|
|
[Trait("Category", "Integration")]
|
|
public sealed class ClientApiAccountsTests : IAsyncLifetime
|
|
{
|
|
const string NewPassword = "Other-Pass-2!";
|
|
|
|
PrivaPubHost _host;
|
|
|
|
public async ValueTask InitializeAsync()
|
|
{
|
|
Assert.SkipUnless(MongoFixture.Enabled, MongoFixture.Skip);
|
|
_host = await PrivaPubHost.Shared();
|
|
}
|
|
|
|
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
|
|
|
static string Name(string prefix) => $"{prefix}{Guid.NewGuid():N}"[..20];
|
|
|
|
async Task<HttpResponseMessage> LogIn(string userName, string password)
|
|
{
|
|
using var client = _host.Client();
|
|
return await client.PostJson("/clientapi/user/login", new { userName, password });
|
|
}
|
|
|
|
async Task<HttpStatusCode> Settings(string jwt)
|
|
{
|
|
using var client = _host.As(jwt);
|
|
return (await client.GetAsync("/clientapi/user/settings", TestContext.Current.CancellationToken)).StatusCode;
|
|
}
|
|
|
|
static async Task<string> Message(HttpResponseMessage response)
|
|
{
|
|
var body = await response.JsonBody();
|
|
return body["errorMessage"]?.GetValue<string>() ?? body["title"]?.GetValue<string>();
|
|
}
|
|
|
|
string Jwt(string rootId, string userName, DateTime expires, string key = default)
|
|
{
|
|
var configuration = _host.Get<IConfiguration>();
|
|
var signing = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key ?? configuration["AppConfiguration:Jwt:Key"]!));
|
|
var token = new JwtSecurityToken(configuration["AppConfiguration:Jwt:Issuer"], configuration["AppConfiguration:Jwt:Audience"],
|
|
new[] { new Claim(ClaimTypes.UserData, rootId), new Claim(ClaimTypes.Name, userName), new Claim(Policies.IsUser, "true") },
|
|
notBefore: expires.AddHours(-1), expires: expires, signingCredentials: new SigningCredentials(signing, SecurityAlgorithms.HmacSha512));
|
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Signing_up_answers_a_token_for_the_new_root()
|
|
{
|
|
var userName = Name("Signup");
|
|
using var client = _host.Client();
|
|
|
|
var response = await client.PostJson("/clientapi/user/signup", new { userName, password = Accounts.Password });
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var jwt = await response.JsonBody();
|
|
Assert.Equal(userName.ToLowerInvariant(), jwt["username"]!.GetValue<string>());
|
|
Assert.Equal(new[] { Policies.IsUser }, jwt["policies"]!.AsArray().Select(p => p!.GetValue<string>()));
|
|
Assert.Equal(HttpStatusCode.OK, await Settings(jwt["token"]!.GetValue<string>()));
|
|
var stored = await DB.Default.Find<RootUser>().MatchID(jwt["userId"]!.GetValue<string>()).ExecuteFirstAsync(TestContext.Current.CancellationToken);
|
|
Assert.NotEqual(Accounts.Password, stored.HashedPassword);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_taken_username_is_refused_whatever_its_case()
|
|
{
|
|
var root = await _host.SignUp("taken");
|
|
using var client = _host.Client();
|
|
|
|
var again = await client.PostJson("/clientapi/user/signup", new { userName = root.UserName, password = Accounts.Password });
|
|
var shouted = await client.PostJson("/clientapi/user/signup", new { userName = root.UserName.ToUpperInvariant(), password = Accounts.Password });
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, again.StatusCode);
|
|
Assert.Contains("already taken", await Message(again));
|
|
Assert.Equal(HttpStatusCode.BadRequest, shouted.StatusCode);
|
|
Assert.Equal(1, await DB.Default.CountAsync<RootUser>(u => u.UserName == root.UserName, TestContext.Current.CancellationToken));
|
|
}
|
|
|
|
public static TheoryData<string> InvalidSignUps() => new()
|
|
{
|
|
"{}",
|
|
"{\"userName\":\"ab\",\"password\":\"Test-Pass-1!\"}",
|
|
"{\"userName\":\"has space\",\"password\":\"Test-Pass-1!\"}",
|
|
"{\"userName\":\"validname\",\"password\":\"short\"}",
|
|
"{\"userName\":\"validname\",\"password\":\"nouppercase1\"}",
|
|
"{\"userName\":\"validname\",\"password\":\"Has Space1\"}",
|
|
"{\"userName\":\"validname\",\"password\":\"Test-Pass-1!\",\"lightThemeIndexColour\":400}",
|
|
"{\"userName\":\"" + new string('a', 40) + "\",\"password\":\"Test-Pass-1!\"}",
|
|
"not json"
|
|
};
|
|
|
|
[Theory]
|
|
[MemberData(nameof(InvalidSignUps))]
|
|
public async Task An_invalid_sign_up_answers_400_with_a_message(string body)
|
|
{
|
|
using var client = _host.Client();
|
|
|
|
var response = await client.PostAsync("/clientapi/user/signup", new StringContent(body, Encoding.UTF8, "application/json"), TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
|
Assert.False(string.IsNullOrEmpty(await Message(response)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Logging_in_and_out()
|
|
{
|
|
var root = await _host.SignUp("login");
|
|
|
|
var ok = await LogIn(root.UserName, root.Password);
|
|
var wrong = await LogIn(root.UserName, "Wrong-Pass-1!");
|
|
var unknown = await LogIn(Name("nobody"), root.Password);
|
|
using var signedIn = _host.As((await ok.JsonBody())["token"]!.GetValue<string>());
|
|
var logout = await signedIn.GetAsync("/clientapi/user/logout", TestContext.Current.CancellationToken);
|
|
using var anonymous = _host.Client();
|
|
var anonymousLogout = await anonymous.GetAsync("/clientapi/user/logout", TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, ok.StatusCode);
|
|
Assert.Equal(root.Id, (await ok.JsonBody())["userId"]!.GetValue<string>());
|
|
Assert.Equal(HttpStatusCode.BadRequest, wrong.StatusCode);
|
|
Assert.Equal("Wrong password.", await Message(wrong));
|
|
Assert.Equal(HttpStatusCode.BadRequest, unknown.StatusCode);
|
|
Assert.Equal(HttpStatusCode.OK, logout.StatusCode);
|
|
Assert.Equal(HttpStatusCode.Unauthorized, anonymousLogout.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Update_sets_the_recovery_email_and_refuses_one_in_use()
|
|
{
|
|
var root = await _host.SignUp("email");
|
|
var other = await _host.SignUp("email");
|
|
var email = $"{Guid.NewGuid():N}@example.test";
|
|
using var client = _host.As(root.Jwt);
|
|
using var otherClient = _host.As(other.Jwt);
|
|
|
|
var set = await client.PostJson("/clientapi/user/update", new { email });
|
|
var taken = await otherClient.PostJson("/clientapi/user/update", new { email });
|
|
var invalid = await otherClient.PostJson("/clientapi/user/update", new { email = "not an email" });
|
|
|
|
Assert.Equal(HttpStatusCode.OK, set.StatusCode);
|
|
Assert.Equal(email, (await DB.Default.Find<RootUser>().MatchID(root.Id).ExecuteFirstAsync(TestContext.Current.CancellationToken)).Email);
|
|
Assert.Equal(HttpStatusCode.BadRequest, taken.StatusCode);
|
|
Assert.Contains("already taken", await Message(taken));
|
|
Assert.Equal(HttpStatusCode.BadRequest, invalid.StatusCode);
|
|
Assert.Null((await DB.Default.Find<RootUser>().MatchID(other.Id).ExecuteFirstAsync(TestContext.Current.CancellationToken)).Email);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Settings_round_trip()
|
|
{
|
|
var root = await _host.SignUp("settings");
|
|
using var client = _host.As(root.Jwt);
|
|
|
|
var updated = await client.PostJson("/clientapi/user/update/settings", new
|
|
{
|
|
languageCode = "it",
|
|
lightThemeIndexColour = 100,
|
|
darkThemeIndexColour = 200,
|
|
iconsThemeIndexColour = 50,
|
|
themeIsDarkMode = true,
|
|
themeIsDarkGray = true
|
|
});
|
|
var settings = await (await client.GetAsync("/clientapi/user/settings", TestContext.Current.CancellationToken)).JsonBody();
|
|
var unsupported = await client.PostJson("/clientapi/user/update/settings", new { languageCode = "xx" });
|
|
var outOfRange = await client.PostJson("/clientapi/user/update/settings", new { languageCode = "en", lightThemeIndexColour = 360 });
|
|
|
|
Assert.Equal(HttpStatusCode.OK, updated.StatusCode);
|
|
Assert.Equal("it", settings["languageCode"]!.GetValue<string>());
|
|
Assert.Equal(100, settings["lightThemeIndexColour"]!.GetValue<int>());
|
|
Assert.Equal(200, settings["darkThemeIndexColour"]!.GetValue<int>());
|
|
Assert.Equal(50, settings["iconsThemeIndexColour"]!.GetValue<int>());
|
|
Assert.True(settings["themeIsDarkMode"]!.GetValue<bool>());
|
|
Assert.True(settings["themeIsDarkGray"]!.GetValue<bool>());
|
|
Assert.Equal(HttpStatusCode.BadRequest, unsupported.StatusCode);
|
|
Assert.Contains("unsupported", await Message(unsupported));
|
|
Assert.Equal(HttpStatusCode.BadRequest, outOfRange.StatusCode);
|
|
Assert.Equal("it", (await DB.Default.Find<RootUser>().MatchID(root.Id).ExecuteFirstAsync(TestContext.Current.CancellationToken)).Settings.LanguageCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Changing_the_password_needs_the_old_one()
|
|
{
|
|
var root = await _host.SignUp("password");
|
|
using var client = _host.As(root.Jwt);
|
|
|
|
var wrongOld = await client.PostJson("/clientapi/user/update/password", new { oldPassword = "Wrong-Pass-1!", newPassword = NewPassword, repeatedPassword = NewPassword });
|
|
var mismatch = await client.PostJson("/clientapi/user/update/password", new { oldPassword = root.Password, newPassword = NewPassword, repeatedPassword = "Something-3!" });
|
|
var weak = await client.PostJson("/clientapi/user/update/password", new { oldPassword = root.Password, newPassword = "weak", repeatedPassword = "weak" });
|
|
var changed = await client.PostJson("/clientapi/user/update/password", new { oldPassword = root.Password, newPassword = NewPassword, repeatedPassword = NewPassword });
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, wrongOld.StatusCode);
|
|
Assert.Equal("Wrong password.", await Message(wrongOld));
|
|
Assert.Equal(HttpStatusCode.BadRequest, mismatch.StatusCode);
|
|
Assert.Equal(HttpStatusCode.BadRequest, weak.StatusCode);
|
|
Assert.Equal(HttpStatusCode.OK, changed.StatusCode);
|
|
Assert.Equal(HttpStatusCode.BadRequest, (await LogIn(root.UserName, root.Password)).StatusCode);
|
|
Assert.Equal(HttpStatusCode.OK, (await LogIn(root.UserName, NewPassword)).StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_invitation_signs_up_a_root_with_a_persona_in_the_group()
|
|
{
|
|
var owner = await _host.Persona(await _host.SignUp("owner"), "owner");
|
|
var group = await _host.Group(owner, community: false, password: "door-Pass-1");
|
|
var invitationCode = group["invitationCode"]!.GetValue<string>();
|
|
var userName = Name("invited");
|
|
var avatarUserName = Name("guest");
|
|
using var client = _host.Client();
|
|
|
|
var wrongPassword = await client.PostJson("/clientapi/user/invitation/signup", new { userName, password = Accounts.Password, invitationCode, invitationPassword = "nope", avatarUserName });
|
|
var wrongCode = await client.PostJson("/clientapi/user/invitation/signup", new { userName, password = Accounts.Password, invitationCode = $"{Guid.NewGuid():N}{Guid.NewGuid():N}", invitationPassword = "door-Pass-1", avatarUserName });
|
|
var response = await client.PostJson("/clientapi/user/invitation/signup", new { userName, password = Accounts.Password, invitationCode, invitationPassword = "door-Pass-1", avatarUserName, avatarName = "Guest" });
|
|
|
|
Assert.Equal(HttpStatusCode.NotAcceptable, wrongPassword.StatusCode);
|
|
Assert.Equal(HttpStatusCode.NotFound, wrongCode.StatusCode);
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var jwt = await response.JsonBody();
|
|
using var invited = _host.As(jwt["token"]!.GetValue<string>());
|
|
var avatars = await (await invited.GetAsync("/clientapi/avatar/private/list", TestContext.Current.CancellationToken)).JsonItems();
|
|
var avatar = Assert.Single(avatars)!;
|
|
Assert.Equal(avatarUserName, avatar["userName"]!.GetValue<string>());
|
|
Assert.Equal("Guest", avatar["name"]!.GetValue<string>());
|
|
var stored = await DB.Default.Find<GroupEntity>().MatchID(group["id"]!.GetValue<string>()).ExecuteFirstAsync(TestContext.Current.CancellationToken);
|
|
Assert.Contains(stored.Members, m => !m.IsForeign && m.AvatarId == avatar["id"]!.GetValue<string>() && m.Role == GroupRole.Member);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_invitation_never_names_the_persona_after_the_login()
|
|
{
|
|
var owner = await _host.Persona(await _host.SignUp("owner"), "owner");
|
|
var invitationCode = (await _host.Group(owner, community: true))["invitationCode"]!.GetValue<string>();
|
|
var userName = Name("same");
|
|
using var client = _host.Client();
|
|
|
|
var same = await client.PostJson("/clientapi/user/invitation/signup", new { userName, password = Accounts.Password, invitationCode, avatarUserName = userName });
|
|
var shouted = await client.PostJson("/clientapi/user/invitation/signup", new { userName = userName.ToUpperInvariant(), password = Accounts.Password, invitationCode, avatarUserName = userName });
|
|
var missing = await client.PostJson("/clientapi/user/invitation/signup", new { userName, password = Accounts.Password, invitationCode });
|
|
|
|
Assert.Equal(HttpStatusCode.BadRequest, same.StatusCode);
|
|
Assert.Equal("Your persona's username must differ from your login.", await Message(same));
|
|
Assert.Equal(HttpStatusCode.BadRequest, shouted.StatusCode);
|
|
Assert.Equal("Your persona's username must differ from your login.", await Message(shouted));
|
|
Assert.Equal(HttpStatusCode.BadRequest, missing.StatusCode);
|
|
Assert.False(await DB.Default.Find<RootUser>().Match(u => u.UserName == userName).ExecuteAnyAsync(TestContext.Current.CancellationToken));
|
|
Assert.False(await DB.Default.Find<Avatar>().Match(a => a.UserName == userName).ExecuteAnyAsync(TestContext.Current.CancellationToken));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task An_existing_root_joins_through_an_invitation_with_a_new_persona()
|
|
{
|
|
var owner = await _host.Persona(await _host.SignUp("owner"), "owner");
|
|
var group = await _host.Group(owner, community: true);
|
|
var root = await _host.SignUp("joiner");
|
|
var avatarUserName = Name("joined");
|
|
using var client = _host.Client();
|
|
|
|
var response = await client.PostJson("/clientapi/user/invitation/login", new
|
|
{
|
|
userName = root.UserName,
|
|
password = root.Password,
|
|
invitationCode = group["invitationCode"]!.GetValue<string>(),
|
|
avatarUserName
|
|
});
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
using var joined = _host.As(root.Jwt);
|
|
var avatar = Assert.Single(await (await joined.GetAsync("/clientapi/avatar/private/list", TestContext.Current.CancellationToken)).JsonItems())!;
|
|
Assert.Equal(avatarUserName, avatar["userName"]!.GetValue<string>());
|
|
var groups = await (await joined.GetAsync("/clientapi/group/list?avatarId=" + avatar["id"]!.GetValue<string>(), TestContext.Current.CancellationToken)).JsonItems();
|
|
Assert.Equal(group["id"]!.GetValue<string>(), Assert.Single(groups)!["id"]!.GetValue<string>());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Recovery_without_an_email_is_refused()
|
|
{
|
|
var root = await _host.SignUp("noemail");
|
|
using var client = _host.Client();
|
|
|
|
var response = await client.PostJson("/clientapi/user/recover/password", new { userName = root.UserName });
|
|
var unknown = await client.PostJson("/clientapi/user/recover/password", new { userName = Name("nobody") });
|
|
var neither = await client.PostJson("/clientapi/user/recover/password", new { });
|
|
|
|
Assert.Equal(HttpStatusCode.Locked, response.StatusCode);
|
|
Assert.Contains("doesn't have an email", await Message(response));
|
|
Assert.False(await DB.Default.Find<EmailRecovery>().Match(r => r.RootUserId == root.Id).ExecuteAnyAsync(TestContext.Current.CancellationToken));
|
|
Assert.Equal(HttpStatusCode.NotFound, unknown.StatusCode);
|
|
Assert.Equal(HttpStatusCode.BadRequest, neither.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Recovery_through_an_unreachable_mail_server_says_so_and_nothing_more()
|
|
{
|
|
var root = await _host.SignUp("smtp");
|
|
var email = $"{Guid.NewGuid():N}@example.test";
|
|
using (var signedIn = _host.As(root.Jwt))
|
|
Assert.Equal(HttpStatusCode.OK, (await signedIn.PostJson("/clientapi/user/update", new { email })).StatusCode);
|
|
using var client = _host.Client();
|
|
|
|
var byName = await client.PostJson("/clientapi/user/recover/password", new { userName = root.UserName });
|
|
var byEmail = await client.PostJson("/clientapi/user/recover/password", new { email });
|
|
|
|
foreach (var response in new[] { byName, byEmail })
|
|
{
|
|
var body = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
|
|
Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode);
|
|
Assert.Equal("Failed to send email.", JsonNode.Parse(body)!["errorMessage"]!.GetValue<string>());
|
|
foreach (var leak in new[] { "xception", "efused", "ocket", "127.0.0.1", "smtp", "SMTP", " at " })
|
|
Assert.DoesNotContain(leak, body);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_wrong_recovery_code_changes_nothing()
|
|
{
|
|
using var client = _host.Client();
|
|
var code = Guid.NewGuid().ToString("N");
|
|
|
|
var valid = await client.PostAsync("/clientapi/user/recover/valid", new StringContent($"\"{code}\"", Encoding.UTF8, "application/json"), TestContext.Current.CancellationToken);
|
|
var change = await client.PostJson("/clientapi/user/recover/update/password", new { newPassword = NewPassword, repeatedPassword = NewPassword, recoveryCode = code });
|
|
|
|
Assert.Equal(HttpStatusCode.OK, valid.StatusCode);
|
|
Assert.Equal("false", await valid.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
|
|
Assert.Equal(HttpStatusCode.NotFound, change.StatusCode);
|
|
Assert.Equal("Invalid recovery code.", await Message(change));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_valid_recovery_code_resets_the_password_once()
|
|
{
|
|
var root = await _host.SignUp("recover");
|
|
var code = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N");
|
|
await DB.Default.SaveAsync(new EmailRecovery { RootUserId = root.Id, RecoveryCode = code }, TestContext.Current.CancellationToken);
|
|
using var client = _host.Client();
|
|
|
|
var valid = await client.PostAsync("/clientapi/user/recover/valid", new StringContent($"\"{code}\"", Encoding.UTF8, "application/json"), TestContext.Current.CancellationToken);
|
|
var change = await client.PostJson("/clientapi/user/recover/update/password", new { newPassword = NewPassword, repeatedPassword = NewPassword, recoveryCode = code });
|
|
var again = await client.PostJson("/clientapi/user/recover/update/password", new { newPassword = "Third-Pass-3!", repeatedPassword = "Third-Pass-3!", recoveryCode = code });
|
|
|
|
Assert.Equal("true", await valid.Content.ReadAsStringAsync(TestContext.Current.CancellationToken));
|
|
Assert.True(change.IsSuccessStatusCode);
|
|
Assert.Equal(HttpStatusCode.NotFound, again.StatusCode);
|
|
Assert.Equal(HttpStatusCode.BadRequest, (await LogIn(root.UserName, root.Password)).StatusCode);
|
|
Assert.Equal(HttpStatusCode.OK, (await LogIn(root.UserName, NewPassword)).StatusCode);
|
|
Assert.False(await DB.Default.Find<EmailRecovery>().Match(r => r.RootUserId == root.Id).ExecuteAnyAsync(TestContext.Current.CancellationToken));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Sniffing_again_answers_a_fresh_token()
|
|
{
|
|
var root = await _host.SignUp("sniff");
|
|
using var client = _host.As(root.Jwt);
|
|
|
|
var response = await client.GetAsync("/clientapi/user/sniff/again", TestContext.Current.CancellationToken);
|
|
using var anonymous = _host.Client();
|
|
var refused = await anonymous.GetAsync("/clientapi/user/sniff/again", TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
var jwt = await response.JsonBody();
|
|
Assert.Equal(root.Id, jwt["userId"]!.GetValue<string>());
|
|
Assert.Equal(root.UserName, jwt["username"]!.GetValue<string>());
|
|
Assert.True(jwt["expiration"]!.GetValue<long>() > DateTime.UtcNow.Ticks);
|
|
Assert.Equal(HttpStatusCode.OK, await Settings(jwt["token"]!.GetValue<string>()));
|
|
Assert.Equal(HttpStatusCode.Unauthorized, refused.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task The_eleventh_sign_up_from_one_address_in_a_minute_is_refused()
|
|
{
|
|
using var client = _host.ClientAt("198.51.100.21");
|
|
for (var attempt = 1; attempt <= 10; attempt++)
|
|
Assert.Equal(HttpStatusCode.BadRequest, (await client.PostJson("/clientapi/user/signup", new { })).StatusCode);
|
|
|
|
var eleventh = await client.PostJson("/clientapi/user/signup", new { userName = Name("limited"), password = Accounts.Password });
|
|
|
|
Assert.Equal(HttpStatusCode.TooManyRequests, eleventh.StatusCode);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_bad_token_answers_401_with_a_json_body()
|
|
{
|
|
var root = await _host.SignUp("badjwt");
|
|
var tokens = new[]
|
|
{
|
|
"not-a-jwt",
|
|
Jwt(root.Id, root.UserName, DateTime.UtcNow.AddHours(-1)),
|
|
Jwt(root.Id, root.UserName, DateTime.UtcNow.AddHours(1), key: "another-key-entirely-0123456789abcdef0123456789abcdef0123456789abcdef")
|
|
};
|
|
|
|
foreach (var token in tokens)
|
|
{
|
|
using var client = _host.As(token);
|
|
var response = await client.GetAsync("/clientapi/user/settings", TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
Assert.Equal("application/json", response.Content.Headers.ContentType!.MediaType);
|
|
var body = await response.JsonBody();
|
|
Assert.Equal(401, body["statusCode"]!.GetValue<int>());
|
|
Assert.False(body["isValid"]!.GetValue<bool>());
|
|
Assert.Contains("invalid_token", response.Headers.WwwAuthenticate.ToString());
|
|
}
|
|
Assert.Equal(HttpStatusCode.OK, await Settings(Jwt(root.Id, root.UserName, DateTime.UtcNow.AddHours(1))));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task A_banned_or_removed_root_loses_the_client_api_at_once()
|
|
{
|
|
var banned = await _host.SignUp("banned");
|
|
var removed = await _host.SignUp("removed");
|
|
Assert.Equal(HttpStatusCode.OK, await Settings(banned.Jwt));
|
|
Assert.Equal(HttpStatusCode.OK, await Settings(removed.Jwt));
|
|
|
|
await ClientApi.Ban(banned.Id);
|
|
await DB.Default.Update<RootUser>().MatchID(removed.Id).Modify(u => u.DeletedAt, DateTime.UtcNow).ExecuteAsync(TestContext.Current.CancellationToken);
|
|
|
|
Assert.Equal(HttpStatusCode.Unauthorized, await Settings(banned.Jwt));
|
|
Assert.Equal(HttpStatusCode.Unauthorized, await Settings(removed.Jwt));
|
|
using (var client = _host.As(banned.Jwt))
|
|
{
|
|
Assert.Equal(HttpStatusCode.Unauthorized, (await client.GetAsync("/clientapi/user/sniff/again", TestContext.Current.CancellationToken)).StatusCode);
|
|
Assert.Equal(HttpStatusCode.Unauthorized, (await client.PostJson("/clientapi/avatar/private/insert", new { userName = Name("late"), name = "late", biography = "testing" })).StatusCode);
|
|
}
|
|
await ClientApi.Ban(banned.Id, banned: false);
|
|
Assert.Equal(HttpStatusCode.OK, await Settings(banned.Jwt));
|
|
}
|
|
}
|
|
}
|