UserEventFirer, web hook tests

This commit is contained in:
andy 2021-12-20 23:04:53 +00:00
parent 8174f9f6f6
commit 7a56f7f586
14 changed files with 454 additions and 161 deletions

View File

@ -30,6 +30,8 @@ namespace Selector.CLI
private readonly IAudioFeatureInjectorFactory AudioFeatureInjectorFactory; private readonly IAudioFeatureInjectorFactory AudioFeatureInjectorFactory;
private readonly IPlayCounterFactory PlayCounterFactory; private readonly IPlayCounterFactory PlayCounterFactory;
private readonly IUserEventFirerFactory UserEventFirerFactory;
private readonly IPublisherFactory PublisherFactory; private readonly IPublisherFactory PublisherFactory;
private readonly ICacheWriterFactory CacheWriterFactory; private readonly ICacheWriterFactory CacheWriterFactory;
private ConcurrentDictionary<string, IWatcherCollection> Watchers { get; set; } = new(); private ConcurrentDictionary<string, IWatcherCollection> Watchers { get; set; } = new();
@ -48,7 +50,9 @@ namespace Selector.CLI
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IPublisherFactory publisherFactory = null, IPublisherFactory publisherFactory = null,
ICacheWriterFactory cacheWriterFactory = null ICacheWriterFactory cacheWriterFactory = null,
IUserEventFirerFactory userEventFirerFactory = null
) )
{ {
Logger = logger; Logger = logger;
@ -62,6 +66,8 @@ namespace Selector.CLI
AudioFeatureInjectorFactory = audioFeatureInjectorFactory; AudioFeatureInjectorFactory = audioFeatureInjectorFactory;
PlayCounterFactory = playCounterFactory; PlayCounterFactory = playCounterFactory;
UserEventFirerFactory = userEventFirerFactory;
PublisherFactory = publisherFactory; PublisherFactory = publisherFactory;
CacheWriterFactory = cacheWriterFactory; CacheWriterFactory = cacheWriterFactory;
} }
@ -123,6 +129,8 @@ namespace Selector.CLI
if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.Get()); if (CacheWriterFactory is not null) consumers.Add(await CacheWriterFactory.Get());
if (PublisherFactory is not null) consumers.Add(await PublisherFactory.Get()); if (PublisherFactory is not null) consumers.Add(await PublisherFactory.Get());
if (UserEventFirerFactory is not null) consumers.Add(await UserEventFirerFactory.Get());
if (!string.IsNullOrWhiteSpace(dbWatcher.User.LastFmUsername)) if (!string.IsNullOrWhiteSpace(dbWatcher.User.LastFmUsername))
{ {
consumers.Add(await PlayCounterFactory.Get(creds: new() { Username = dbWatcher.User.LastFmUsername })); consumers.Add(await PlayCounterFactory.Get(creds: new() { Username = dbWatcher.User.LastFmUsername }));

View File

@ -125,8 +125,8 @@ namespace Selector.CLI
services.AddRedisServices(config.RedisOptions.ConnectionString); services.AddRedisServices(config.RedisOptions.ConnectionString);
Console.WriteLine("> Adding cache event maps..."); Console.WriteLine("> Adding cache event maps...");
services.AddTransient<IEventMapping, SpotifyLinkFromCacheMapping>(); services.AddTransient<IEventMapping, FromPubSub.SpotifyLink>();
services.AddTransient<IEventMapping, LastfmFromCacheMapping>(); services.AddTransient<IEventMapping, FromPubSub.Lastfm>();
Console.WriteLine("> Adding caching Spotify consumers..."); Console.WriteLine("> Adding caching Spotify consumers...");
services.AddCachingSpotify(); services.AddCachingSpotify();

View File

@ -14,13 +14,15 @@ namespace Selector.Events
public string NewUsername { get; set; } public string NewUsername { get; set; }
} }
public class LastfmFromCacheMapping : IEventMapping public partial class FromPubSub
{ {
private readonly ILogger<LastfmFromCacheMapping> Logger; public class Lastfm : IEventMapping
{
private readonly ILogger<Lastfm> Logger;
private readonly ISubscriber Subscriber; private readonly ISubscriber Subscriber;
private readonly UserEventBus UserEvent; private readonly UserEventBus UserEvent;
public LastfmFromCacheMapping(ILogger<LastfmFromCacheMapping> logger, public Lastfm(ILogger<Lastfm> logger,
ISubscriber subscriber, ISubscriber subscriber,
UserEventBus userEvent) UserEventBus userEvent)
{ {
@ -35,7 +37,8 @@ namespace Selector.Events
(await Subscriber.SubscribeAsync(Key.AllUserLastfm)).OnMessage(message => { (await Subscriber.SubscribeAsync(Key.AllUserLastfm)).OnMessage(message => {
try{ try
{
var userId = Key.Param(message.Channel); var userId = Key.Param(message.Channel);
var deserialised = JsonSerializer.Deserialize<LastfmChange>(message.Message); var deserialised = JsonSerializer.Deserialize<LastfmChange>(message.Message);
@ -48,21 +51,24 @@ namespace Selector.Events
UserEvent.OnLastfmCredChange(this, deserialised); UserEvent.OnLastfmCredChange(this, deserialised);
} }
catch(Exception e) catch (Exception e)
{ {
Logger.LogError(e, "Error parsing Last.fm username event"); Logger.LogError(e, "Error parsing Last.fm username event");
} }
}); });
} }
} }
}
public class LastfmToCacheMapping : IEventMapping public partial class ToPubSub
{ {
private readonly ILogger<LastfmToCacheMapping> Logger; public class Lastfm : IEventMapping
{
private readonly ILogger<Lastfm> Logger;
private readonly ISubscriber Subscriber; private readonly ISubscriber Subscriber;
private readonly UserEventBus UserEvent; private readonly UserEventBus UserEvent;
public LastfmToCacheMapping(ILogger<LastfmToCacheMapping> logger, public Lastfm(ILogger<Lastfm> logger,
ISubscriber subscriber, ISubscriber subscriber,
UserEventBus userEvent) UserEventBus userEvent)
{ {
@ -84,4 +90,5 @@ namespace Selector.Events
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
}
} }

View File

@ -7,13 +7,15 @@ using Selector.Cache;
namespace Selector.Events namespace Selector.Events
{ {
public class NowPlayingFromCacheMapping : IEventMapping public partial class FromPubSub
{ {
private readonly ILogger<NowPlayingFromCacheMapping> Logger; public class NowPlaying : IEventMapping
{
private readonly ILogger<NowPlaying> Logger;
private readonly ISubscriber Subscriber; private readonly ISubscriber Subscriber;
private readonly UserEventBus UserEvent; private readonly UserEventBus UserEvent;
public NowPlayingFromCacheMapping(ILogger<NowPlayingFromCacheMapping> logger, public NowPlaying(ILogger<NowPlaying> logger,
ISubscriber subscriber, ISubscriber subscriber,
UserEventBus userEvent) UserEventBus userEvent)
{ {
@ -28,7 +30,8 @@ namespace Selector.Events
(await Subscriber.SubscribeAsync(Key.AllCurrentlyPlaying)).OnMessage(message => { (await Subscriber.SubscribeAsync(Key.AllCurrentlyPlaying)).OnMessage(message => {
try{ try
{
var userId = Key.Param(message.Channel); var userId = Key.Param(message.Channel);
var deserialised = JsonSerializer.Deserialize<CurrentlyPlayingDTO>(message.Message); var deserialised = JsonSerializer.Deserialize<CurrentlyPlayingDTO>(message.Message);
@ -36,11 +39,46 @@ namespace Selector.Events
UserEvent.OnCurrentlyPlayingChange(this, userId, deserialised); UserEvent.OnCurrentlyPlayingChange(this, userId, deserialised);
} }
catch(Exception e) catch (Exception e)
{ {
Logger.LogError(e, $"Error parsing new currently playing [{message}]"); Logger.LogError(e, $"Error parsing new currently playing [{message}]");
} }
}); });
} }
} }
}
public partial class ToPubSub
{
public class NowPlaying : IEventMapping
{
private readonly ILogger<NowPlaying> Logger;
private readonly ISubscriber Subscriber;
private readonly UserEventBus UserEvent;
public NowPlaying(ILogger<NowPlaying> logger,
ISubscriber subscriber,
UserEventBus userEvent)
{
Logger = logger;
Subscriber = subscriber;
UserEvent = userEvent;
}
public Task ConstructMapping()
{
Logger.LogDebug("Forming now playing event mapping TO cache FROM event bus");
UserEvent.CurrentlyPlaying += async (o, e) =>
{
(string id, CurrentlyPlayingDTO args) = e;
var payload = JsonSerializer.Serialize(e);
await Subscriber.PublishAsync(Key.CurrentlyPlaying(id), payload);
};
return Task.CompletedTask;
}
}
}
} }

View File

@ -14,13 +14,15 @@ namespace Selector.Events
public bool NewLinkState { get; set; } public bool NewLinkState { get; set; }
} }
public class SpotifyLinkFromCacheMapping : IEventMapping public partial class FromPubSub
{ {
private readonly ILogger<SpotifyLinkFromCacheMapping> Logger; public class SpotifyLink : IEventMapping
{
private readonly ILogger<SpotifyLink> Logger;
private readonly ISubscriber Subscriber; private readonly ISubscriber Subscriber;
private readonly UserEventBus UserEvent; private readonly UserEventBus UserEvent;
public SpotifyLinkFromCacheMapping(ILogger<SpotifyLinkFromCacheMapping> logger, public SpotifyLink(ILogger<SpotifyLink> logger,
ISubscriber subscriber, ISubscriber subscriber,
UserEventBus userEvent) UserEventBus userEvent)
{ {
@ -35,7 +37,8 @@ namespace Selector.Events
(await Subscriber.SubscribeAsync(Key.AllUserSpotify)).OnMessage(message => { (await Subscriber.SubscribeAsync(Key.AllUserSpotify)).OnMessage(message => {
try{ try
{
var userId = Key.Param(message.Channel); var userId = Key.Param(message.Channel);
var deserialised = JsonSerializer.Deserialize<SpotifyLinkChange>(message.Message); var deserialised = JsonSerializer.Deserialize<SpotifyLinkChange>(message.Message);
@ -48,25 +51,28 @@ namespace Selector.Events
UserEvent.OnSpotifyLinkChange(this, deserialised); UserEvent.OnSpotifyLinkChange(this, deserialised);
} }
catch(TaskCanceledException) catch (TaskCanceledException)
{ {
Logger.LogDebug("Task Cancelled"); Logger.LogDebug("Task Cancelled");
} }
catch(Exception e) catch (Exception e)
{ {
Logger.LogError(e, "Error parsing new Spotify link event"); Logger.LogError(e, "Error parsing new Spotify link event");
} }
}); });
} }
} }
}
public class SpotifyLinkToCacheMapping : IEventMapping public partial class ToPubSub
{ {
private readonly ILogger<SpotifyLinkToCacheMapping> Logger; public class SpotifyLink : IEventMapping
{
private readonly ILogger<SpotifyLink> Logger;
private readonly ISubscriber Subscriber; private readonly ISubscriber Subscriber;
private readonly UserEventBus UserEvent; private readonly UserEventBus UserEvent;
public SpotifyLinkToCacheMapping(ILogger<SpotifyLinkToCacheMapping> logger, public SpotifyLink(ILogger<SpotifyLink> logger,
ISubscriber subscriber, ISubscriber subscriber,
UserEventBus userEvent) UserEventBus userEvent)
{ {
@ -88,4 +94,5 @@ namespace Selector.Events
return Task.CompletedTask; return Task.CompletedTask;
} }
} }
}
} }

View File

@ -0,0 +1,83 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Selector.Events;
namespace Selector
{
public class UserEventFirer : IConsumer
{
protected readonly IPlayerWatcher Watcher;
protected readonly ILogger<UserEventFirer> Logger;
protected readonly UserEventBus UserEvent;
public CancellationToken CancelToken { get; set; }
public UserEventFirer(
IPlayerWatcher watcher,
UserEventBus userEvent,
ILogger<UserEventFirer> logger = null,
CancellationToken token = default
)
{
Watcher = watcher;
UserEvent = userEvent;
Logger = logger ?? NullLogger<UserEventFirer>.Instance;
CancelToken = token;
}
public void Callback(object sender, ListeningChangeEventArgs e)
{
if (e.Current is null) return;
Task.Run(async () => {
try
{
await AsyncCallback(e);
}
catch (Exception e)
{
Logger.LogError(e, "Error occured during callback");
}
}, CancelToken);
}
public Task AsyncCallback(ListeningChangeEventArgs e)
{
Logger.LogDebug("Firing now playing event on user bus [{username}/{userId}]", e.SpotifyUsername, e.Id);
UserEvent.OnCurrentlyPlayingChange(this, e.Id, (CurrentlyPlayingDTO) e);
return Task.CompletedTask;
}
public void Subscribe(IWatcher watch = null)
{
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast)
{
watcherCast.ItemChange += Callback;
}
else
{
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
}
}
public void Unsubscribe(IWatcher watch = null)
{
var watcher = watch ?? Watcher ?? throw new ArgumentNullException("No watcher provided");
if (watcher is IPlayerWatcher watcherCast)
{
watcherCast.ItemChange -= Callback;
}
else
{
throw new ArgumentException("Provided watcher is not a PlayerWatcher");
}
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Selector.Events;
namespace Selector
{
public interface IUserEventFirerFactory
{
public Task<UserEventFirer> Get(IPlayerWatcher watcher = null);
}
public class UserEventFirerFactory: IUserEventFirerFactory
{
private readonly ILoggerFactory LoggerFactory;
private readonly UserEventBus UserEvent;
public UserEventFirerFactory(ILoggerFactory loggerFactory, UserEventBus userEvent)
{
LoggerFactory = loggerFactory;
UserEvent = userEvent;
}
public Task<UserEventFirer> Get(IPlayerWatcher watcher = null)
{
return Task.FromResult(new UserEventFirer(
watcher,
UserEvent,
LoggerFactory.CreateLogger<UserEventFirer>()
));
}
}
}

View File

@ -8,6 +8,9 @@ namespace Selector.Events
{ {
services.AddEventBus(); services.AddEventBus();
services.AddEventMappingAgent(); services.AddEventMappingAgent();
services.AddTransient<IUserEventFirerFactory, UserEventFirerFactory>();
services.AddTransient<UserEventFirerFactory>();
} }
public static void AddEventBus(this IServiceCollection services) public static void AddEventBus(this IServiceCollection services)

View File

@ -1,9 +1,4 @@
using System; using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Selector.Model; using Selector.Model;

View File

@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.Http;
using Xunit;
using Moq;
using Moq.Protected;
using FluentAssertions;
using System.Net;
using SpotifyAPI.Web;
namespace Selector.Tests
{
public class WebHookTest
{
[Fact(Skip = "Not working atm")]
public async Task TestHttpClientUsed()
{
var msg = new HttpResponseMessage(HttpStatusCode.OK);
var httpHandlerMock = new Mock<HttpMessageHandler>();
httpHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(msg);
var watcherMock = new Mock<IPlayerWatcher>();
watcherMock.SetupAdd(w => w.ItemChange += It.IsAny<EventHandler<ListeningChangeEventArgs>>());
watcherMock.SetupRemove(w => w.ItemChange -= It.IsAny<EventHandler<ListeningChangeEventArgs>>());
var link = "https://link";
var content = new StringContent("");
var config = new WebHookConfig()
{
Url = link,
Content = content,
};
var http = new HttpClient(httpHandlerMock.Object);
var webHook = new WebHook(watcherMock.Object, http, config);
webHook.Subscribe();
watcherMock.Raise(w => w.ItemChange += null, this, new ListeningChangeEventArgs());
await Task.Delay(100);
httpHandlerMock.Protected().Verify<Task<HttpResponseMessage>>("SendAsync", Times.Once(), ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>());
}
[Theory]
[InlineData(200, true, true)]
[InlineData(404, true, false)]
[InlineData(500, true, false)]
public async Task TestEventFiring(int code, bool predicate, bool successful)
{
var msg = new HttpResponseMessage(Enum.Parse<HttpStatusCode>(code.ToString()));
var httpHandlerMock = new Mock<HttpMessageHandler>();
httpHandlerMock.Protected()
.Setup<Task<HttpResponseMessage>>("SendAsync", ItExpr.IsAny<HttpRequestMessage>(), ItExpr.IsAny<CancellationToken>())
.ReturnsAsync(msg);
var watcherMock = new Mock<IPlayerWatcher>();
var link = "https://link";
var content = new StringContent("");
var config = new WebHookConfig()
{
Url = link,
Content = content,
};
var http = new HttpClient(httpHandlerMock.Object);
bool predicateEvent = false, successfulEvent = false, failedEvent = false;
var webHook = new WebHook(watcherMock.Object, http, config);
webHook.PredicatePass += (o, e) =>
{
predicateEvent = predicate;
};
webHook.SuccessfulRequest += (o, e) =>
{
successfulEvent = successful;
};
webHook.FailedRequest += (o, e) =>
{
failedEvent = !successful;
};
await webHook.AsyncCallback(ListeningChangeEventArgs.From(new (), new (), new()));
predicateEvent.Should().Be(predicate);
successfulEvent.Should().Be(successful);
failedEvent.Should().Be(!successful);
}
}
}

View File

@ -5,7 +5,7 @@
ViewData["ActivePage"] = ManageNavPages.LastFm; ViewData["ActivePage"] = ManageNavPages.LastFm;
} }
<h4>@ViewData["Title"] <a href="https://last.fm"><img src="/last-fm.png" class="lastfm-logo central" /></a></h4> <h4>@ViewData["Title"] <a href="https://last.fm" target="_blank"><img src="/last-fm.png" class="lastfm-logo central" /></a></h4>
<partial name="_StatusMessage" model="Model.StatusMessage" /> <partial name="_StatusMessage" model="Model.StatusMessage" />
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">

View File

@ -5,7 +5,7 @@
ViewData["ActivePage"] = ManageNavPages.Spotify; ViewData["ActivePage"] = ManageNavPages.Spotify;
} }
<h4>@ViewData["Title"] <a href="https://spotify.com"><img src="/Spotify_Icon_RGB_White.png" class="spotify-logo central" /></a></h4> <h4>@ViewData["Title"] <a href="https://spotify.com" target="_blank"><img src="/Spotify_Icon_RGB_White.png" class="spotify-logo central" /></a></h4>
<partial name="_StatusMessage" model="Model.StatusMessage" /> <partial name="_StatusMessage" model="Model.StatusMessage" />
<div class="row"> <div class="row">

View File

@ -107,9 +107,9 @@ namespace Selector.Web
Console.WriteLine("> Adding cache event maps..."); Console.WriteLine("> Adding cache event maps...");
services.AddTransient<IEventMapping, SpotifyLinkToCacheMapping>(); services.AddTransient<IEventMapping, ToPubSub.SpotifyLink>();
services.AddTransient<IEventMapping, LastfmToCacheMapping>(); services.AddTransient<IEventMapping, ToPubSub.Lastfm>();
services.AddTransient<IEventMapping, NowPlayingFromCacheMapping>(); services.AddTransient<IEventMapping, FromPubSub.NowPlaying>();
services.AddCacheHubProxy(); services.AddCacheHubProxy();

View File

@ -38,9 +38,9 @@ namespace Selector
protected readonly WebHookConfig Config; protected readonly WebHookConfig Config;
protected event EventHandler PredicatePass; public event EventHandler PredicatePass;
protected event EventHandler SuccessfulRequest; public event EventHandler SuccessfulRequest;
protected event EventHandler FailedRequest; public event EventHandler FailedRequest;
public CancellationToken CancelToken { get; set; } public CancellationToken CancelToken { get; set; }
@ -80,12 +80,14 @@ namespace Selector
public async Task AsyncCallback(ListeningChangeEventArgs e) public async Task AsyncCallback(ListeningChangeEventArgs e)
{ {
if(Config.ShouldRequest(e)) if(Config.ShouldRequest(e))
{
try
{ {
Logger.LogDebug("[{name}] predicate passed, making request to [{url}]", Config.Name, Config.Url); Logger.LogDebug("[{name}] predicate passed, making request to [{url}]", Config.Name, Config.Url);
var response = await HttpClient.PostAsync(Config.Url, Config.Content, CancelToken);
OnPredicatePass(new EventArgs()); OnPredicatePass(new EventArgs());
var response = await HttpClient.PostAsync(Config.Url, Config.Content, CancelToken);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
Logger.LogDebug("[{name}] request success", Config.Name); Logger.LogDebug("[{name}] request success", Config.Name);
@ -97,6 +99,15 @@ namespace Selector
OnFailedRequest(new EventArgs()); OnFailedRequest(new EventArgs());
} }
} }
catch(HttpRequestException ex)
{
Logger.LogError(ex, "Exception occured during request");
}
catch (TaskCanceledException)
{
Logger.LogDebug("Task cancelled");
}
}
else else
{ {
Logger.LogTrace("[{name}] predicate failed, skipping", Config.Name); Logger.LogTrace("[{name}] predicate failed, skipping", Config.Name);