117 lines
5.4 KiB
C#
117 lines
5.4 KiB
C#
using PrivaPub.Infrastructure.Cli;
|
|||
|
|
|
||
|
|
using System.Net;
|
||
|
|
using System.Net.Http.Headers;
|
||
|
|
using System.Net.Http.Json;
|
||
|
|
using System.Text.Json.Nodes;
|
||
|
|
using System.Text.RegularExpressions;
|
||
|
|
|
||
|
|
namespace PrivaPub.Tests.Support.Host
|
||
|
|
{
|
||
|
|
public sealed record Root(string Id, string UserName, string Password, string Jwt);
|
||
|
|
|
||
|
|
public sealed record Persona(string Id, string UserName, Root Root);
|
||
|
|
|
||
|
|
public static partial class Accounts
|
||
|
|
{
|
||
|
|
public const string Password = "Test-Pass-1!";
|
||
|
|
const string OutOfBand = "urn:ietf:wg:oauth:2.0:oob";
|
||
|
|
|
||
|
|
public static async Task<Root> SignUp(this PrivaPubHost host, string name = "root")
|
||
|
|
{
|
||
|
|
var userName = $"{name}{Guid.NewGuid():N}"[..24];
|
||
|
|
using var client = host.Client();
|
||
|
|
var response = await client.PostAsJsonAsync("/clientapi/user/signup", new { userName, password = Password });
|
||
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||
|
|
var jwt = (await response.Content.ReadFromJsonAsync<JsonObject>())!;
|
||
|
|
return new Root(jwt["userId"]!.GetValue<string>(), userName, Password, jwt["token"]!.GetValue<string>());
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async Task<Root> LogIn(this PrivaPubHost host, Root root)
|
||
|
|
{
|
||
|
|
using var client = host.Client();
|
||
|
|
var response = await client.PostAsJsonAsync("/clientapi/user/login", new { userName = root.UserName, password = root.Password });
|
||
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||
|
|
var jwt = (await response.Content.ReadFromJsonAsync<JsonObject>())!;
|
||
|
|
return root with { Jwt = jwt["token"]!.GetValue<string>() };
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async Task<Root> Admin(this PrivaPubHost host)
|
||
|
|
{
|
||
|
|
var root = await host.SignUp("admin");
|
||
|
|
Assert.Equal(0, await AdminCommands.Run(new[] { "promote", root.UserName }));
|
||
|
|
return await host.LogIn(root);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async Task<Persona> Persona(this PrivaPubHost host, Root root, string name = "persona")
|
||
|
|
{
|
||
|
|
var userName = $"{name}{Guid.NewGuid():N}"[..20];
|
||
|
|
using var client = host.As(root.Jwt);
|
||
|
|
var response = await client.PostAsJsonAsync("/clientapi/avatar/private/insert", new { userName, name, biography = "testing" });
|
||
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||
|
|
var avatar = (await response.Content.ReadFromJsonAsync<JsonObject>())!;
|
||
|
|
return new Persona(avatar["id"]!.GetValue<string>(), userName, root);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async Task<string> MastodonToken(this PrivaPubHost host, Persona persona, string scopes = "read write follow")
|
||
|
|
{
|
||
|
|
using var client = host.Client(cookies: true);
|
||
|
|
var app = await Form(client, "/api/v1/apps", ("client_name", "privapub-tests"), ("redirect_uris", OutOfBand), ("scopes", scopes));
|
||
|
|
var clientId = app["client_id"]!.GetValue<string>();
|
||
|
|
var clientSecret = app["client_secret"]!.GetValue<string>();
|
||
|
|
var query = $"client_id={Uri.EscapeDataString(clientId)}&redirect_uri={Uri.EscapeDataString(OutOfBand)}&response_type=code&scope={Uri.EscapeDataString(scopes)}";
|
||
|
|
|
||
|
|
var code = await Authorize(client, persona, query);
|
||
|
|
var token = await Form(client, "/oauth/token", ("grant_type", "authorization_code"), ("code", code), ("client_id", clientId),
|
||
|
|
("client_secret", clientSecret), ("redirect_uri", OutOfBand));
|
||
|
|
return token["access_token"]!.GetValue<string>();
|
||
|
|
}
|
||
|
|
|
||
|
|
public static async Task<string> Authorize(HttpClient client, Persona persona, string query, string decision = "allow")
|
||
|
|
{
|
||
|
|
var returnUrl = "/oauth/authorize?" + query;
|
||
|
|
var login = await client.GetStringAsync("/oauth/login?returnUrl=" + Uri.EscapeDataString(returnUrl));
|
||
|
|
var antiforgery = AntiforgeryToken().Match(login).Groups[1].Value;
|
||
|
|
var signedIn = await client.PostAsync("/oauth/login", new FormUrlEncodedContent(new Dictionary<string, string>
|
||
|
|
{
|
||
|
|
["returnUrl"] = returnUrl,
|
||
|
|
["__RequestVerificationToken"] = antiforgery,
|
||
|
|
["userName"] = persona.Root.UserName,
|
||
|
|
["password"] = persona.Root.Password
|
||
|
|
}));
|
||
|
|
Assert.Equal(HttpStatusCode.Redirect, signedIn.StatusCode);
|
||
|
|
|
||
|
|
var choose = await client.GetStringAsync(returnUrl + "&signed_in=1");
|
||
|
|
var fields = HiddenInput().Matches(choose).Select(m => new KeyValuePair<string, string>(m.Groups[1].Value, WebUtility.HtmlDecode(m.Groups[2].Value))).ToList();
|
||
|
|
fields.Add(new("avatarId", persona.Id));
|
||
|
|
fields.Add(new("decision", decision));
|
||
|
|
var answer = await client.PostAsync("/oauth/authorize", new FormUrlEncodedContent(fields));
|
||
|
|
var page = await answer.Content.ReadAsStringAsync();
|
||
|
|
return Code().Match(page) is { Success: true } match ? match.Groups[1].Value : default;
|
||
|
|
}
|
||
|
|
|
||
|
|
public static HttpClient As(this PrivaPubHost host, string bearer)
|
||
|
|
{
|
||
|
|
var client = host.Client();
|
||
|
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearer);
|
||
|
|
return client;
|
||
|
|
}
|
||
|
|
|
||
|
|
static async Task<JsonObject> Form(HttpClient client, string path, params (string Key, string Value)[] fields)
|
||
|
|
{
|
||
|
|
var response = await client.PostAsync(path, new FormUrlEncodedContent(fields.Select(f => new KeyValuePair<string, string>(f.Key, f.Value))));
|
||
|
|
var body = await response.Content.ReadAsStringAsync();
|
||
|
|
Assert.True(response.IsSuccessStatusCode, $"{path} answered {(int)response.StatusCode}: {body}");
|
||
|
|
return JsonNode.Parse(body)!.AsObject();
|
||
|
|
}
|
||
|
|
|
||
|
|
[GeneratedRegex("name=\"__RequestVerificationToken\" type=\"hidden\" value=\"([^\"]*)\"")]
|
||
|
|
private static partial Regex AntiforgeryToken();
|
||
|
|
|
||
|
|
[GeneratedRegex("<input type=\"hidden\" name=\"([^\"]*)\" value=\"([^\"]*)\"")]
|
||
|
|
private static partial Regex HiddenInput();
|
||
|
|
|
||
|
|
[GeneratedRegex("<code>([^<]*)</code>")]
|
||
|
|
private static partial Regex Code();
|
||
|
|
}
|
||
|
|
}
|