using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using Microsoft.AspNetCore.Mvc.Testing;
using Wiki.Host.Auth;
namespace Wiki.Host.Tests;
/// A Secret-like directory: one user per hash format Traefik accepts, split over two keys, plus a
/// hidden entry that must be skipped. Every password is the user name.
public static class BasicUsers
{
// From the Traefik BasicAuth docs (htpasswd -m, password "test"); openssl passwd -apr1 agrees.
public const string Apr1Line = "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/";
public static readonly string Directory = Write();
private static string Write()
{
var dir = Path.Combine(Path.GetTempPath(), "wiki-host-tests", $"htpasswd-{Guid.NewGuid():N}");
System.IO.Directory.CreateDirectory(dir);
var bcrypt = BCrypt.Net.BCrypt.HashPassword("agent", 4).Replace("$2a$", "$2y$");
var sha = "{SHA}" + Convert.ToBase64String(System.Security.Cryptography.SHA1.HashData(Encoding.UTF8.GetBytes("legacy")));
File.WriteAllLines(Path.Combine(dir, "users"), [Apr1Line, $"agent:{bcrypt}"]);
File.WriteAllLines(Path.Combine(dir, "more"), [$"legacy:{sha}"]);
File.WriteAllLines(Path.Combine(dir, "..hidden"), ["ghost:{SHA}" + Convert.ToBase64String(System.Security.Cryptography.SHA1.HashData(Encoding.UTF8.GetBytes("ghost")))]);
return dir;
}
}
public class BasicAuthTests(WikiHostFactory factory) : IClassFixture
{
private HttpClient As(string user, string password)
{
var client = factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic",
Convert.ToBase64String(Encoding.UTF8.GetBytes($"{user}:{password}")));
return client;
}
[Fact]
public void Apr1MatchesApacheHtpasswd() =>
Assert.Equal("$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", Htpasswd.Apr1("test", "H6uskkkW"));
[Theory]
[InlineData("test", "test")] // apr1
[InlineData("agent", "agent")] // bcrypt $2y$
[InlineData("legacy", "legacy")] // {SHA}
public async Task TheMachineDoorCredentialsReadTheApi(string user, string password)
{
var client = As(user, password);
Assert.Equal(HttpStatusCode.OK, (await client.GetAsync("/api/v1/tags")).StatusCode);
Assert.Equal(HttpStatusCode.BadRequest, (await client.PostAsJsonAsync("/rank", new { query = " " })).StatusCode);
}
[Theory]
[InlineData("test", "wrong")]
[InlineData("nobody", "test")]
[InlineData("agent", "")]
[InlineData("ghost", "ghost")] // only in a hidden entry
public async Task WrongCredentialsAre401(string user, string password) =>
Assert.Equal(HttpStatusCode.Unauthorized, (await As(user, password).GetAsync("/api/v1/tags")).StatusCode);
[Fact]
public async Task BasicCredentialsCannotWrite()
{
var r = await As("test", "test").PostAsJsonAsync("/api/v1/facts", new { title = "t", source = "s", kind = "fact", content = new string('x', 50) });
Assert.Equal(HttpStatusCode.Unauthorized, r.StatusCode);
}
[Fact]
public async Task BasicCredentialsDoNotOpenTheUi() =>
Assert.Equal(HttpStatusCode.Redirect, (await As("test", "test").GetAsync("/facts")).StatusCode);
}