Add IMessageChannel and Line REST implementation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ROLAC.API.Services.Notifications;
|
||||
using Xunit;
|
||||
|
||||
namespace ROLAC.API.Tests.Services.Notifications;
|
||||
|
||||
public class LineMessageChannelTests
|
||||
{
|
||||
// Captures the outgoing request and returns a canned response.
|
||||
private sealed class CapturingHandler : HttpMessageHandler
|
||||
{
|
||||
public HttpRequestMessage? LastRequest { get; private set; }
|
||||
public string? LastBody { get; private set; }
|
||||
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.OK;
|
||||
public string ResponseBody { get; set; } = "{}";
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(
|
||||
HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
LastRequest = request;
|
||||
LastBody = request.Content is null ? null : await request.Content.ReadAsStringAsync(cancellationToken);
|
||||
return new HttpResponseMessage(StatusCode) { Content = new StringContent(ResponseBody) };
|
||||
}
|
||||
}
|
||||
|
||||
private static LineMessageChannel BuildChannel(CapturingHandler handler)
|
||||
{
|
||||
var http = new HttpClient(handler);
|
||||
var options = Options.Create(new LineOptions { ChannelAccessToken = "tok", ChannelSecret = "sec" });
|
||||
return new LineMessageChannel(http, options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PushToUserAsync_PostsTextMessage_WithBearerToken()
|
||||
{
|
||||
var handler = new CapturingHandler();
|
||||
var channel = BuildChannel(handler);
|
||||
|
||||
var result = await channel.PushToUserAsync("U123", "hello");
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal("https://api.line.me/v2/bot/message/push", handler.LastRequest!.RequestUri!.ToString());
|
||||
Assert.Equal("Bearer", handler.LastRequest.Headers.Authorization!.Scheme);
|
||||
Assert.Equal("tok", handler.LastRequest.Headers.Authorization.Parameter);
|
||||
|
||||
using var doc = JsonDocument.Parse(handler.LastBody!);
|
||||
Assert.Equal("U123", doc.RootElement.GetProperty("to").GetString());
|
||||
Assert.Equal("hello", doc.RootElement.GetProperty("messages")[0].GetProperty("text").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReplyAsync_PostsToReplyEndpoint_WithReplyToken()
|
||||
{
|
||||
var handler = new CapturingHandler();
|
||||
var channel = BuildChannel(handler);
|
||||
|
||||
await channel.ReplyAsync("RTOKEN", "hi back");
|
||||
|
||||
Assert.Equal("https://api.line.me/v2/bot/message/reply", handler.LastRequest!.RequestUri!.ToString());
|
||||
using var doc = JsonDocument.Parse(handler.LastBody!);
|
||||
Assert.Equal("RTOKEN", doc.RootElement.GetProperty("replyToken").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PushToUserAsync_ReturnsFailure_OnNonSuccessStatus()
|
||||
{
|
||||
var handler = new CapturingHandler { StatusCode = HttpStatusCode.TooManyRequests, ResponseBody = "quota" };
|
||||
var channel = BuildChannel(handler);
|
||||
|
||||
var result = await channel.PushToUserAsync("U123", "hello");
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("429", result.Error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ROLAC.API.Services.Notifications;
|
||||
|
||||
/// <summary>Result of one Line REST call.</summary>
|
||||
public sealed record MessageSendResult(bool Success, string? Error);
|
||||
|
||||
/// <summary>Abstraction over a chat channel's send/reply (Line today; future channels later).</summary>
|
||||
public interface IMessageChannel
|
||||
{
|
||||
Task<MessageSendResult> PushToUserAsync(string externalId, string text, CancellationToken ct = default);
|
||||
Task<MessageSendResult> PushToGroupAsync(string externalId, string text, CancellationToken ct = default);
|
||||
Task<MessageSendResult> ReplyAsync(string replyToken, string text, CancellationToken ct = default);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace ROLAC.API.Services.Notifications;
|
||||
|
||||
/// <summary>Sends text messages and replies via the Line Messaging API REST endpoints.</summary>
|
||||
public sealed class LineMessageChannel : IMessageChannel
|
||||
{
|
||||
private const string PushUrl = "https://api.line.me/v2/bot/message/push";
|
||||
private const string ReplyUrl = "https://api.line.me/v2/bot/message/reply";
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly LineOptions _options;
|
||||
|
||||
public LineMessageChannel(HttpClient http, IOptions<LineOptions> options)
|
||||
{
|
||||
_http = http;
|
||||
_options = options.Value;
|
||||
}
|
||||
|
||||
public Task<MessageSendResult> PushToUserAsync(string externalId, string text, CancellationToken ct = default)
|
||||
=> PostAsync(PushUrl, new { to = externalId, messages = new[] { new { type = "text", text } } }, ct);
|
||||
|
||||
public Task<MessageSendResult> PushToGroupAsync(string externalId, string text, CancellationToken ct = default)
|
||||
=> PostAsync(PushUrl, new { to = externalId, messages = new[] { new { type = "text", text } } }, ct);
|
||||
|
||||
public Task<MessageSendResult> ReplyAsync(string replyToken, string text, CancellationToken ct = default)
|
||||
=> PostAsync(ReplyUrl, new { replyToken, messages = new[] { new { type = "text", text } } }, ct);
|
||||
|
||||
private async Task<MessageSendResult> PostAsync(string url, object payload, CancellationToken ct)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Post, url)
|
||||
{
|
||||
Content = JsonContent.Create(payload),
|
||||
};
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.ChannelAccessToken);
|
||||
|
||||
using var response = await _http.SendAsync(request, ct);
|
||||
if (response.IsSuccessStatusCode) return new MessageSendResult(true, null);
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
return new MessageSendResult(false, $"{(int)response.StatusCode}: {body}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new MessageSendResult(false, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user