using System.Net; using System.Text.Json; using GmRelay.Web; using GmRelay.Web.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; namespace GmRelay.Bot.Tests.Web; public class DiscordAuthServiceTests { [Fact] public void BuildAuthorizeUrl_GeneratesCorrectUrl() { var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Discord:ClientId"] = "12345", ["Discord:ClientSecret"] = "secret", ["Discord:RedirectUri"] = "https://example.com/callback" }) .Build(); var service = new DiscordAuthService(new TestHttpClientFactory(), config, NullLogger.Instance); var url = service.BuildAuthorizeUrl("state123"); Assert.Contains("client_id=12345", url); Assert.Contains("response_type=code", url); Assert.Contains("state=state123", url); Assert.Contains("https%3A%2F%2Fexample.com%2Fcallback", url); } [Fact] public void BuildAuthorizeUrl_WithMissingConfig_ThrowsInvalidOperationException() { var config = new ConfigurationBuilder().Build(); var service = new DiscordAuthService(new TestHttpClientFactory(), config, NullLogger.Instance); Assert.Throws(() => service.BuildAuthorizeUrl("state")); } [Fact] public async Task ExchangeCodeAsync_WithValidCode_ReturnsUser() { var handler = new TestHttpMessageHandler((request) => { if (request.RequestUri?.AbsolutePath == "/api/oauth2/token") { var response = new DiscordTokenResponse("access_123", "Bearer", 604800, "identify"); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(response)) }; } if (request.RequestUri?.AbsolutePath == "/api/users/@me") { var user = new DiscordUser("98765", "TestUser", "0", "avatar123", null); return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(JsonSerializer.Serialize(user)) }; } return new HttpResponseMessage(HttpStatusCode.NotFound); }); var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Discord:ClientId"] = "client", ["Discord:ClientSecret"] = "secret", ["Discord:RedirectUri"] = "https://example.com/callback" }) .Build(); var factory = new TestHttpClientFactory(handler); var service = new DiscordAuthService(factory, config, NullLogger.Instance); var result = await service.ExchangeCodeAsync("valid_code"); Assert.NotNull(result); Assert.Equal("98765", result.Id); Assert.Equal("TestUser", result.Username); Assert.Equal("https://cdn.discordapp.com/avatars/98765/avatar123.png", result.AvatarUrl); } [Fact] public async Task ExchangeCodeAsync_WithInvalidCode_ReturnsNull() { var handler = new TestHttpMessageHandler((request) => { return new HttpResponseMessage(HttpStatusCode.BadRequest); }); var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Discord:ClientId"] = "client", ["Discord:ClientSecret"] = "secret", ["Discord:RedirectUri"] = "https://example.com/callback" }) .Build(); var factory = new TestHttpClientFactory(handler); var service = new DiscordAuthService(factory, config, NullLogger.Instance); var result = await service.ExchangeCodeAsync("invalid_code"); Assert.Null(result); } private class TestHttpClientFactory : IHttpClientFactory { private readonly HttpMessageHandler? _handler; public TestHttpClientFactory(HttpMessageHandler? handler = null) { _handler = handler; } public HttpClient CreateClient(string name) => _handler is not null ? new HttpClient(_handler) : new HttpClient(); } private class TestHttpMessageHandler : HttpMessageHandler { private readonly Func _handler; public TestHttpMessageHandler(Func handler) { _handler = handler; } protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { return Task.FromResult(_handler(request)); } } }