diff --git a/Jobs/BotAvatarJob.cs b/Jobs/BotAvatarJob.cs index 308b3dc..b63deeb 100644 --- a/Jobs/BotAvatarJob.cs +++ b/Jobs/BotAvatarJob.cs @@ -27,10 +27,11 @@ public BotAvatarJob(DiscordSocketClient client, DB db, LogsService logsService) public async Task Execute(IJobExecutionContext context) { + CancellationToken cancellationToken = context.CancellationToken; try { - var setting = await _db.BotSettings.FirstOrDefaultAsync(s => s.Key == "BotAvatar"); + var setting = await _db.BotSettings.FirstOrDefaultAsync(s => s.Key == "BotAvatar", cancellationToken); string current = setting?.Value ?? "unknown"; bool isDecember = DateTime.UtcNow.Month == 12; @@ -46,7 +47,9 @@ public async Task Execute(IJobExecutionContext context) return; await using var fs = File.OpenRead(filePath); - await _client.CurrentUser.ModifyAsync(x => x.Avatar = new Image(fs)); + await _client.CurrentUser.ModifyAsync( + x => x.Avatar = new Image(fs), + new RequestOptions { CancelToken = cancellationToken }); if (setting == null) { @@ -60,9 +63,13 @@ public async Task Execute(IJobExecutionContext context) _db.BotSettings.Update(setting); } - await _db.SaveChangesAsync(); + await _db.SaveChangesAsync(cancellationToken); _logsService.Log($"Quartz Job - Bot avatar updated to {targetKey}"); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + throw; + } catch (Exception ex) { _logsService.Log($"Quartz Job - Error updating bot avatar: {ex}"); diff --git a/Morpheus.Tests/BotAvatarJobTests.cs b/Morpheus.Tests/BotAvatarJobTests.cs new file mode 100644 index 0000000..1268aa6 --- /dev/null +++ b/Morpheus.Tests/BotAvatarJobTests.cs @@ -0,0 +1,56 @@ +using Discord.WebSocket; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Morpheus.Database; +using Morpheus.Jobs; +using Morpheus.Services; +using Quartz; +using System.Reflection; + +namespace Morpheus.Tests; + +public class BotAvatarJobTests +{ + [Fact] + public async Task Execute_WhenCanceledBeforeLookup_PropagatesCancellation() + { + await using SqliteConnection connection = new("Data Source=:memory:"); + await connection.OpenAsync(); + DbContextOptions options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + await using DB db = new(options); + await db.Database.EnsureCreatedAsync(); + using DiscordSocketClient discordClient = new(); + + BotAvatarJob job = new(discordClient, db, new LogsService(new LogQueue())); + using CancellationTokenSource cancellation = new(); + await cancellation.CancelAsync(); + + IJobExecutionContext context = CreateContext(cancellation.Token); + + await Assert.ThrowsAnyAsync(() => job.Execute(context)); + } + + private static IJobExecutionContext CreateContext(CancellationToken cancellationToken) + { + JobExecutionContextProxy.CurrentCancellationToken = cancellationToken; + return DispatchProxy.Create(); + } + + private class JobExecutionContextProxy : DispatchProxy + { + public static CancellationToken CurrentCancellationToken { get; set; } + + protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) + { + if (targetMethod?.ReturnType == typeof(CancellationToken)) + return CurrentCancellationToken; + + Type returnType = targetMethod?.ReturnType ?? typeof(void); + return returnType == typeof(void) || !returnType.IsValueType + ? null + : Activator.CreateInstance(returnType); + } + } +}