using System.Net.Http.Headers;
using System.Text.Json;
using DorkNet.Server.Services;
namespace DorkNet.Server.Tests;
///
/// A Quest client patched without its own Meta app has no platform session,
/// so every headset reports the same placeholder platform user id ("1").
/// Accounts must stay keyed by device id: two headsets get two accounts, a
/// headset gets its own account back on the next boot, and the placeholder is
/// never stored or matched by the remembered-logins lookup — otherwise every
/// Quest player would be offered every other Quest player's account.
///
public sealed class QuestPlaceholderPlatformIdTests : IClassFixture
{
// RecNet platform value the 2023 Quest client sends on /connect/token.
// The fix does not depend on it; any platform with a placeholder id is
// handled the same way.
private const string QuestPlatform = "1";
private const string PlaceholderPlatformId = "1";
private readonly DorkNetServerFactory _factory;
public QuestPlaceholderPlatformIdTests(DorkNetServerFactory factory) => _factory = factory;
[Theory]
[InlineData(null, true)]
[InlineData("", true)]
[InlineData(" ", true)]
[InlineData("0", true)]
[InlineData("1", true)]
[InlineData(" 1 ", true)]
[InlineData("10", false)]
[InlineData("2173678582678296", false)]
public void Placeholder_platform_ids_are_recognised(string? platformId, bool expected)
=> Assert.Equal(expected, PlayerService.IsPlaceholderPlatformId(platformId));
[Fact]
public async Task Two_headsets_with_the_placeholder_platform_id_get_separate_accounts()
{
using var client = Client("auth");
var headsetA = $"quest-headset-a-{Guid.NewGuid():N}";
var headsetB = $"quest-headset-b-{Guid.NewGuid():N}";
var a = await CreateAccountAsync(client, headsetA);
var b = await CreateAccountAsync(client, headsetB);
Assert.NotEqual(a, b);
// Same headset, next boot: its own account comes back.
var aAgain = await CreateAccountAsync(client, headsetA);
Assert.Equal(a, aAgain);
}
[Fact]
public async Task Placeholder_platform_id_never_lists_remembered_logins()
{
using var client = Client("auth");
await CreateAccountAsync(client, $"quest-headset-{Guid.NewGuid():N}");
await CreateAccountAsync(client, $"quest-headset-{Guid.NewGuid():N}");
using var response = await client.GetAsync(
$"/cachedlogin/forplatformid/{QuestPlatform}/{PlaceholderPlatformId}");
var body = await response.Content.ReadAsStringAsync();
Assert.True(response.IsSuccessStatusCode,
$"GET cachedlogin/forplatformid -> {(int)response.StatusCode}: {body}");
using var json = JsonDocument.Parse(body);
Assert.Equal(JsonValueKind.Array, json.RootElement.ValueKind);
Assert.Equal(0, json.RootElement.GetArrayLength());
}
[Fact]
public async Task A_real_platform_id_still_lists_its_remembered_login()
{
using var client = Client("auth");
var realPlatformId = $"{Random.Shared.NextInt64(10_000_000_000, 99_999_999_999)}";
var id = await CreateAccountAsync(client, $"quest-meta-{Guid.NewGuid():N}", realPlatformId);
using var response = await client.GetAsync(
$"/cachedlogin/forplatformid/{QuestPlatform}/{realPlatformId}");
var body = await response.Content.ReadAsStringAsync();
Assert.True(response.IsSuccessStatusCode,
$"GET cachedlogin/forplatformid -> {(int)response.StatusCode}: {body}");
Assert.Contains(id.ToString(), body);
}
// The 2023 client's account-creation request (LBNJFPOLCDL: grant_type,
// client_id, platform, platform_id, device_id, ...). No username/password,
// so the server takes the device-id path.
private async Task CreateAccountAsync(
HttpClient client, string deviceId, string platformId = PlaceholderPlatformId)
{
var form = new Dictionary
{
["grant_type"] = "create_account",
["client_id"] = "recroom",
["platform"] = QuestPlatform,
["platform_id"] = platformId,
["device_id"] = deviceId,
};
using var request = new HttpRequestMessage(HttpMethod.Post, "/connect/token")
{
Content = new FormUrlEncodedContent(form),
};
request.Headers.Accept.ParseAdd("application/json");
request.Headers.UserAgent.ParseAdd("RecRoom/2023.03.17");
request.Headers.TryAddWithoutValidation("X-DorkNet-Version", "march_2023_03_21");
using var response = await client.SendAsync(request);
var body = await response.Content.ReadAsStringAsync();
Assert.True(response.IsSuccessStatusCode,
$"POST /connect/token -> {(int)response.StatusCode}: {body}");
using var json = JsonDocument.Parse(body);
var accessToken = json.RootElement.GetProperty("access_token").GetString()!;
using var userInfo = new HttpRequestMessage(HttpMethod.Get, "/connect/userinfo");
userInfo.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
using var userInfoResponse = await client.SendAsync(userInfo);
userInfoResponse.EnsureSuccessStatusCode();
using var userJson = JsonDocument.Parse(await userInfoResponse.Content.ReadAsStringAsync());
return long.Parse(userJson.RootElement.GetProperty("sub").GetString()!);
}
private HttpClient Client(string host)
{
var client = _factory.CreateClient(new() { AllowAutoRedirect = false });
client.BaseAddress = new Uri($"http://{host}.{_factory.ApexDomain}");
return client;
}
}