diff --git a/Directory.Packages.props b/Directory.Packages.props index 2eb42a0..f44376c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,8 +4,13 @@ - - + + + + @@ -53,18 +58,20 @@ - - + + - + - + + diff --git a/src/Presentation/Agents/Ard/Krautwatch.Agents.Ard.csproj b/src/Presentation/Agents/Ard/Krautwatch.Agents.Ard.csproj index a9f263a..6bc44b4 100644 --- a/src/Presentation/Agents/Ard/Krautwatch.Agents.Ard.csproj +++ b/src/Presentation/Agents/Ard/Krautwatch.Agents.Ard.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Presentation/Agents/Ard/Program.cs b/src/Presentation/Agents/Ard/Program.cs index 1124ffe..82c7653 100644 --- a/src/Presentation/Agents/Ard/Program.cs +++ b/src/Presentation/Agents/Ard/Program.cs @@ -1,6 +1,7 @@ using Krautwatch.Application; using Krautwatch.Application.Crawling; using Krautwatch.Infrastructure; +using JasperFx.CodeGeneration.Model; using Wolverine; using Wolverine.Postgresql; @@ -42,6 +43,12 @@ { opts.PersistMessagesWithPostgresql(connectionString); opts.Policies.UseDurableLocalQueues(); + // Wolverine 6 changed the default ServiceLocationPolicy to NotAllowed (5.x was AllowedButWarn), + // which refuses to generate a handler needing container resolution. CrawlShowHandler needs it: + // IEnumerable is an opaque lambda registration, and IEpisodeRepository's + // graph reaches EF's own DbContextOptions factory — not something we control. Restore the 5.x + // behaviour: allowed, but keep Wolverine's warning so the nudge to inline stays visible. + opts.ServiceLocationPolicy = ServiceLocationPolicy.AllowedButWarn; // Discover the Crawling Action (CrawlShowHandler) in the Application assembly. opts.Discovery.IncludeAssembly(typeof(CrawlShowCommand).Assembly); }); diff --git a/src/Presentation/Agents/Zdf/Krautwatch.Agents.Zdf.csproj b/src/Presentation/Agents/Zdf/Krautwatch.Agents.Zdf.csproj index a9f263a..6bc44b4 100644 --- a/src/Presentation/Agents/Zdf/Krautwatch.Agents.Zdf.csproj +++ b/src/Presentation/Agents/Zdf/Krautwatch.Agents.Zdf.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Presentation/Agents/Zdf/Program.cs b/src/Presentation/Agents/Zdf/Program.cs index bda7683..d932c99 100644 --- a/src/Presentation/Agents/Zdf/Program.cs +++ b/src/Presentation/Agents/Zdf/Program.cs @@ -1,6 +1,7 @@ using Krautwatch.Application; using Krautwatch.Application.Crawling; using Krautwatch.Infrastructure; +using JasperFx.CodeGeneration.Model; using Wolverine; using Wolverine.Postgresql; @@ -38,6 +39,12 @@ { opts.PersistMessagesWithPostgresql(connectionString); opts.Policies.UseDurableLocalQueues(); + // Wolverine 6 changed the default ServiceLocationPolicy to NotAllowed (5.x was AllowedButWarn), + // which refuses to generate a handler needing container resolution. CrawlShowHandler needs it: + // IEnumerable is an opaque lambda registration, and IEpisodeRepository's + // graph reaches EF's own DbContextOptions factory — not something we control. Restore the 5.x + // behaviour: allowed, but keep Wolverine's warning so the nudge to inline stays visible. + opts.ServiceLocationPolicy = ServiceLocationPolicy.AllowedButWarn; // Discover the Crawling Action (CrawlShowHandler) in the Application assembly. opts.Discovery.IncludeAssembly(typeof(CrawlShowCommand).Assembly); }); diff --git a/tests/Application.Tests/CrawlShowHandlerTests.cs b/tests/Application.Tests/CrawlShowHandlerTests.cs index 003789d..010d9be 100644 --- a/tests/Application.Tests/CrawlShowHandlerTests.cs +++ b/tests/Application.Tests/CrawlShowHandlerTests.cs @@ -41,7 +41,7 @@ public async Task Handle_selects_crawler_by_provider_and_upserts_the_crawled_epi var repo = Substitute.For(); var handler = new CrawlShowHandler([otherProvider, zdf], repo, NullLogger.Instance); - await handler.HandleAsync(new CrawlShowCommand("zdf", "heute-show")); + await handler.HandleAsync(new CrawlShowCommand("zdf", "heute-show"), TestContext.Current.CancellationToken); zdf.LastQuery.ShouldBe("heute-show"); otherProvider.LastQuery.ShouldBeNull(); // the ARD crawler must not be invoked @@ -56,7 +56,7 @@ public async Task Handle_is_case_insensitive_on_provider_key() var repo = Substitute.For(); var handler = new CrawlShowHandler([zdf], repo, NullLogger.Instance); - await handler.HandleAsync(new CrawlShowCommand("ZDF", "heute-show")); + await handler.HandleAsync(new CrawlShowCommand("ZDF", "heute-show"), TestContext.Current.CancellationToken); zdf.LastQuery.ShouldBe("heute-show"); await repo.Received(1).UpsertManyAsync(Arg.Any>(), Arg.Any()); @@ -68,7 +68,7 @@ public async Task Handle_unknown_provider_is_a_no_op() var repo = Substitute.For(); var handler = new CrawlShowHandler([], repo, NullLogger.Instance); - await handler.HandleAsync(new CrawlShowCommand("kika", "Biene Maja")); + await handler.HandleAsync(new CrawlShowCommand("kika", "Biene Maja"), TestContext.Current.CancellationToken); await repo.DidNotReceive().UpsertManyAsync(Arg.Any>(), Arg.Any()); } @@ -80,7 +80,7 @@ public async Task Handle_empty_crawl_result_does_not_upsert() var repo = Substitute.For(); var handler = new CrawlShowHandler([ard], repo, NullLogger.Instance); - await handler.HandleAsync(new CrawlShowCommand("ard", "Nonexistent Show")); + await handler.HandleAsync(new CrawlShowCommand("ard", "Nonexistent Show"), TestContext.Current.CancellationToken); await repo.DidNotReceive().UpsertManyAsync(Arg.Any>(), Arg.Any()); } diff --git a/tests/Application.Tests/DownloadHandlerTests.cs b/tests/Application.Tests/DownloadHandlerTests.cs index dabedaa..5090cd5 100644 --- a/tests/Application.Tests/DownloadHandlerTests.cs +++ b/tests/Application.Tests/DownloadHandlerTests.cs @@ -67,43 +67,42 @@ public class StartDownloadHandlerTests public async Task ValidRequest_CreatesJobAndEnqueues() { var episode = Fixtures.MakeEpisode(); - _episodes.GetByIdAsync("ep-1", default).Returns(episode); + _episodes.GetByIdAsync("ep-1", Arg.Any()).Returns(episode); - var result = await Handler().HandleAsync(new StartDownloadRequest("ep-1", "stream-1")); + var result = await Handler().HandleAsync(new StartDownloadRequest("ep-1", "stream-1"), TestContext.Current.CancellationToken); result.ShouldNotBeNull(); result!.EpisodeId.ShouldBe("ep-1"); result.Status.ShouldBe(nameof(DownloadStatus.Queued)); - await _jobs.Received(1).AddAsync(Arg.Any(), default); + await _jobs.Received(1).AddAsync(Arg.Any(), TestContext.Current.CancellationToken); await _queue.Received(1).EnqueueAsync( Arg.Any(), - "https://example.com/ep.mp4", - default); + "https://example.com/ep.mp4", TestContext.Current.CancellationToken); } [Fact] public async Task EpisodeNotFound_ReturnsNull_NoJobCreated() { - _episodes.GetByIdAsync("bad-id", default).Returns((Episode?)null); + _episodes.GetByIdAsync("bad-id", Arg.Any()).Returns((Episode?)null); - var result = await Handler().HandleAsync(new StartDownloadRequest("bad-id", "stream-1")); + var result = await Handler().HandleAsync(new StartDownloadRequest("bad-id", "stream-1"), TestContext.Current.CancellationToken); result.ShouldBeNull(); - await _jobs.DidNotReceive().AddAsync(Arg.Any(), default); - await _queue.DidNotReceive().EnqueueAsync(Arg.Any(), Arg.Any(), default); + await _jobs.DidNotReceive().AddAsync(Arg.Any(), TestContext.Current.CancellationToken); + await _queue.DidNotReceive().EnqueueAsync(Arg.Any(), Arg.Any(), TestContext.Current.CancellationToken); } [Fact] public async Task StreamNotFound_ReturnsNull_NoJobCreated() { var episode = Fixtures.MakeEpisode("real-stream"); - _episodes.GetByIdAsync("ep-1", default).Returns(episode); + _episodes.GetByIdAsync("ep-1", Arg.Any()).Returns(episode); - var result = await Handler().HandleAsync(new StartDownloadRequest("ep-1", "wrong-stream")); + var result = await Handler().HandleAsync(new StartDownloadRequest("ep-1", "wrong-stream"), TestContext.Current.CancellationToken); result.ShouldBeNull(); - await _jobs.DidNotReceive().AddAsync(Arg.Any(), default); + await _jobs.DidNotReceive().AddAsync(Arg.Any(), TestContext.Current.CancellationToken); } } @@ -125,13 +124,13 @@ public async Task ActiveStatus_CancelsAndReturnsTrue(DownloadStatus status) { var job = Fixtures.MakeJob(); ApplyStatus(job, status); - _jobs.GetByIdAsync(job.Id, default).Returns(job); + _jobs.GetByIdAsync(job.Id, Arg.Any()).Returns(job); - var result = await Handler().HandleAsync(job.Id); + var result = await Handler().HandleAsync(job.Id, TestContext.Current.CancellationToken); result.ShouldBeTrue(); job.Status.ShouldBe(DownloadStatus.Cancelled); - await _jobs.Received(1).UpdateAsync(job, default); + await _jobs.Received(1).UpdateAsync(job, TestContext.Current.CancellationToken); } [Theory] @@ -144,20 +143,20 @@ public async Task TerminalStatus_ReturnsFalse_NoUpdate(DownloadStatus status) { var job = Fixtures.MakeJob(); ApplyStatus(job, status); - _jobs.GetByIdAsync(job.Id, default).Returns(job); + _jobs.GetByIdAsync(job.Id, Arg.Any()).Returns(job); - var result = await Handler().HandleAsync(job.Id); + var result = await Handler().HandleAsync(job.Id, TestContext.Current.CancellationToken); result.ShouldBeFalse(); - await _jobs.DidNotReceive().UpdateAsync(Arg.Any(), default); + await _jobs.DidNotReceive().UpdateAsync(Arg.Any(), TestContext.Current.CancellationToken); } [Fact] public async Task JobNotFound_ReturnsFalse() { - _jobs.GetByIdAsync(Arg.Any(), default).Returns((DownloadJob?)null); + _jobs.GetByIdAsync(Arg.Any(), Arg.Any()).Returns((DownloadJob?)null); - var result = await Handler().HandleAsync(Guid.NewGuid()); + var result = await Handler().HandleAsync(Guid.NewGuid(), TestContext.Current.CancellationToken); result.ShouldBeFalse(); } @@ -197,17 +196,17 @@ public async Task FailedOrCancelledJob_CreatesNewJobAndEnqueues(DownloadStatus s { var original = Fixtures.MakeJob(); ApplyTerminal(original, status); - _jobs.GetByIdAsync(original.Id, default).Returns(original); + _jobs.GetByIdAsync(original.Id, Arg.Any()).Returns(original); - var result = await Handler().HandleAsync(original.Id); + var result = await Handler().HandleAsync(original.Id, TestContext.Current.CancellationToken); result.ShouldNotBeNull(); result!.Status.ShouldBe(nameof(DownloadStatus.Queued)); // New job created — not the original id result.JobId.ShouldNotBe(original.Id); - await _jobs.Received(1).AddAsync(Arg.Any(), default); - await _queue.Received(1).RequeueAsync(Arg.Any(), original.StreamUrl, default); + await _jobs.Received(1).AddAsync(Arg.Any(), TestContext.Current.CancellationToken); + await _queue.Received(1).RequeueAsync(Arg.Any(), original.StreamUrl, TestContext.Current.CancellationToken); } [Fact] @@ -215,32 +214,32 @@ public async Task CompletedJob_ReturnsNull_NothingEnqueued() { var job = Fixtures.MakeJob(); job.MarkCompleted("/out/file.mp4", 1024); - _jobs.GetByIdAsync(job.Id, default).Returns(job); + _jobs.GetByIdAsync(job.Id, Arg.Any()).Returns(job); - var result = await Handler().HandleAsync(job.Id); + var result = await Handler().HandleAsync(job.Id, TestContext.Current.CancellationToken); result.ShouldBeNull(); - await _queue.DidNotReceive().RequeueAsync(Arg.Any(), Arg.Any(), default); + await _queue.DidNotReceive().RequeueAsync(Arg.Any(), Arg.Any(), TestContext.Current.CancellationToken); } [Fact] public async Task ActiveJob_ReturnsNull_NothingEnqueued() { var job = Fixtures.MakeJob(); // Queued — IsTerminal = false - _jobs.GetByIdAsync(job.Id, default).Returns(job); + _jobs.GetByIdAsync(job.Id, Arg.Any()).Returns(job); - var result = await Handler().HandleAsync(job.Id); + var result = await Handler().HandleAsync(job.Id, TestContext.Current.CancellationToken); result.ShouldBeNull(); - await _queue.DidNotReceive().RequeueAsync(Arg.Any(), Arg.Any(), default); + await _queue.DidNotReceive().RequeueAsync(Arg.Any(), Arg.Any(), TestContext.Current.CancellationToken); } [Fact] public async Task JobNotFound_ReturnsNull() { - _jobs.GetByIdAsync(Arg.Any(), default).Returns((DownloadJob?)null); + _jobs.GetByIdAsync(Arg.Any(), Arg.Any()).Returns((DownloadJob?)null); - var result = await Handler().HandleAsync(Guid.NewGuid()); + var result = await Handler().HandleAsync(Guid.NewGuid(), TestContext.Current.CancellationToken); result.ShouldBeNull(); } diff --git a/tests/Application.Tests/IndexingTests.cs b/tests/Application.Tests/IndexingTests.cs index 6bd7edb..ff64b92 100644 --- a/tests/Application.Tests/IndexingTests.cs +++ b/tests/Application.Tests/IndexingTests.cs @@ -60,7 +60,7 @@ public async Task Query_maps_episodes_to_releases_with_stable_guid_and_token() repo.SearchAsync("heute-show", Arg.Any()) .Returns(new[] { Ep("zdf:doc-1", SeriesType.Daily, null, null) }); - var releases = await new SearchReleasesHandler(repo).HandleAsync(new SearchReleasesQuery("heute-show")); + var releases = await new SearchReleasesHandler(repo).HandleAsync(new SearchReleasesQuery("heute-show"), TestContext.Current.CancellationToken); var release = releases.ShouldHaveSingleItem(); release.Guid.ShouldBe("zdf:doc-1"); @@ -76,7 +76,7 @@ public async Task Empty_query_reads_the_recent_feed() repo.GetRecentAsync(Arg.Any(), Arg.Any()) .Returns(new[] { Ep("zdf:doc-9", SeriesType.Daily, null, null) }); - var releases = await new SearchReleasesHandler(repo).HandleAsync(new SearchReleasesQuery(Q: null)); + var releases = await new SearchReleasesHandler(repo).HandleAsync(new SearchReleasesQuery(Q: null), TestContext.Current.CancellationToken); releases.ShouldHaveSingleItem().Guid.ShouldBe("zdf:doc-9"); await repo.DidNotReceive().SearchAsync(Arg.Any(), Arg.Any()); @@ -93,7 +93,7 @@ public async Task Season_and_episode_filter_a_standard_series() }); var releases = await new SearchReleasesHandler(repo) - .HandleAsync(new SearchReleasesQuery("Die Biene Maja", Season: 2, Episode: 52)); + .HandleAsync(new SearchReleasesQuery("Die Biene Maja", Season: 2, Episode: 52), TestContext.Current.CancellationToken); var release = releases.ShouldHaveSingleItem(); release.Guid.ShouldBe("kika:2"); diff --git a/tests/Application.Tests/Krautwatch.Application.Tests.csproj b/tests/Application.Tests/Krautwatch.Application.Tests.csproj index 4e33c92..615c20e 100644 --- a/tests/Application.Tests/Krautwatch.Application.Tests.csproj +++ b/tests/Application.Tests/Krautwatch.Application.Tests.csproj @@ -1,5 +1,10 @@ + + + Exe + + @@ -7,7 +12,7 @@ - + diff --git a/tests/Application.Tests/RefreshProxyListHandlerTests.cs b/tests/Application.Tests/RefreshProxyListHandlerTests.cs index 5c2c3d7..e578ca7 100644 --- a/tests/Application.Tests/RefreshProxyListHandlerTests.cs +++ b/tests/Application.Tests/RefreshProxyListHandlerTests.cs @@ -22,7 +22,7 @@ public async Task Upserts_the_fetched_candidates() source.FetchAsync(Arg.Any()).Returns([P("1.1.1.1"), P("2.2.2.2")]); var repo = Substitute.For(); - await new RefreshProxyListHandler(source, repo, NullLogger.Instance).HandleAsync(); + await new RefreshProxyListHandler(source, repo, NullLogger.Instance).HandleAsync(TestContext.Current.CancellationToken); await repo.Received(1).UpsertBatchAsync( Arg.Is>(ps => ps != null && ps.Count() == 2), Arg.Any()); @@ -35,7 +35,7 @@ public async Task An_empty_fetch_keeps_the_cached_rows_untouched() source.FetchAsync(Arg.Any()).Returns([]); var repo = Substitute.For(); - await new RefreshProxyListHandler(source, repo, NullLogger.Instance).HandleAsync(); + await new RefreshProxyListHandler(source, repo, NullLogger.Instance).HandleAsync(TestContext.Current.CancellationToken); await repo.DidNotReceive().UpsertBatchAsync(Arg.Any>(), Arg.Any()); } diff --git a/tests/Application.Tests/RunDownloadHandlerTests.cs b/tests/Application.Tests/RunDownloadHandlerTests.cs index 4c37394..3343129 100644 --- a/tests/Application.Tests/RunDownloadHandlerTests.cs +++ b/tests/Application.Tests/RunDownloadHandlerTests.cs @@ -52,7 +52,7 @@ public async Task Downloads_and_marks_the_job_completed() provider.DownloadAsync(job, "/downloads", Arg.Any>(), Arg.Any()) .Returns(new DownloadResult("/downloads/ZDF/heute-show/x.mp4", 123_456)); - await Sut(jobs, provider, settings).HandleAsync(job); + await Sut(jobs, provider, settings).HandleAsync(job, TestContext.Current.CancellationToken); job.Status.ShouldBe(DownloadStatus.Completed); job.OutputPath.ShouldBe("/downloads/ZDF/heute-show/x.mp4"); @@ -68,7 +68,7 @@ public async Task Provider_failure_marks_the_job_download_failed() provider.DownloadAsync(Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()) .ThrowsAsync(new HttpRequestException("stream 403")); - await Sut(jobs, provider, settings).HandleAsync(job); + await Sut(jobs, provider, settings).HandleAsync(job, TestContext.Current.CancellationToken); job.Status.ShouldBe(DownloadStatus.DownloadFailed); job.ErrorMessage.ShouldBe("stream 403"); @@ -80,7 +80,7 @@ public async Task A_job_missing_episode_metadata_fails_without_downloading() var job = new DownloadJob { Id = Guid.NewGuid(), EpisodeId = "x", StreamUrl = "https://cdn/x.mp4", Quality = VideoQuality.High }; var (jobs, provider, settings) = Deps(); - await Sut(jobs, provider, settings).HandleAsync(job); + await Sut(jobs, provider, settings).HandleAsync(job, TestContext.Current.CancellationToken); job.Status.ShouldBe(DownloadStatus.DownloadFailed); await provider.DidNotReceive().DownloadAsync(Arg.Any(), Arg.Any(), Arg.Any>(), Arg.Any()); diff --git a/tests/Application.Tests/SabnzbdDownloadTests.cs b/tests/Application.Tests/SabnzbdDownloadTests.cs index a33611e..e97a42b 100644 --- a/tests/Application.Tests/SabnzbdDownloadTests.cs +++ b/tests/Application.Tests/SabnzbdDownloadTests.cs @@ -64,7 +64,7 @@ public async Task Creates_and_enqueues_a_job_for_a_known_token() var jobs = Substitute.For(); var queue = Substitute.For(); - var jobId = await new AddDownloadByTokenHandler(episodes, jobs, queue).HandleAsync("zdf:1"); + var jobId = await new AddDownloadByTokenHandler(episodes, jobs, queue).HandleAsync("zdf:1", TestContext.Current.CancellationToken); jobId.ShouldNotBeNull(); await jobs.Received(1).AddAsync(Arg.Is(j => j != null && j.EpisodeId == "zdf:1" && j.StreamUrl == "https://cdn/x.mp4"), Arg.Any()); @@ -80,7 +80,7 @@ public async Task Snapshots_the_episodes_geo_restriction_onto_the_job() var jobs = Substitute.For(); var queue = Substitute.For(); - await new AddDownloadByTokenHandler(episodes, jobs, queue).HandleAsync("kika:1"); + await new AddDownloadByTokenHandler(episodes, jobs, queue).HandleAsync("kika:1", TestContext.Current.CancellationToken); await jobs.Received(1).AddAsync(Arg.Is(j => j != null && j.GeoRestricted), Arg.Any()); } @@ -93,7 +93,7 @@ public async Task Unknown_token_returns_null_and_enqueues_nothing() var jobs = Substitute.For(); var queue = Substitute.For(); - var jobId = await new AddDownloadByTokenHandler(episodes, jobs, queue).HandleAsync("nope"); + var jobId = await new AddDownloadByTokenHandler(episodes, jobs, queue).HandleAsync("nope", TestContext.Current.CancellationToken); jobId.ShouldBeNull(); await queue.DidNotReceive().EnqueueAsync(Arg.Any(), Arg.Any(), Arg.Any()); @@ -110,7 +110,7 @@ public async Task Episode_without_a_stream_returns_null() var jobs = Substitute.For(); var queue = Substitute.For(); - var jobId = await new AddDownloadByTokenHandler(episodes, jobs, queue).HandleAsync("zdf:2"); + var jobId = await new AddDownloadByTokenHandler(episodes, jobs, queue).HandleAsync("zdf:2", TestContext.Current.CancellationToken); jobId.ShouldBeNull(); await jobs.DidNotReceive().AddAsync(Arg.Any(), Arg.Any()); diff --git a/tests/Application.Tests/SearchCatalogQueryHandlerTests.cs b/tests/Application.Tests/SearchCatalogQueryHandlerTests.cs index a1e95f4..ef1c714 100644 --- a/tests/Application.Tests/SearchCatalogQueryHandlerTests.cs +++ b/tests/Application.Tests/SearchCatalogQueryHandlerTests.cs @@ -56,7 +56,7 @@ public async Task HandleAsync_WithResults_ReturnsMappedResponses() .Returns(episodes); // Act - var results = await _sut.HandleAsync(new SearchCatalogQuery("tagesschau")); + var results = await _sut.HandleAsync(new SearchCatalogQuery("tagesschau"), TestContext.Current.CancellationToken); // Assert results.Count.ShouldBe(1); @@ -80,7 +80,7 @@ public async Task HandleAsync_NoResults_ReturnsEmptyList() .SearchAsync(Arg.Any(), Arg.Any()) .Returns(new List()); - var results = await _sut.HandleAsync(new SearchCatalogQuery("noresults")); + var results = await _sut.HandleAsync(new SearchCatalogQuery("noresults"), TestContext.Current.CancellationToken); results.ShouldBeEmpty(); } @@ -92,7 +92,7 @@ public async Task HandleAsync_CallsRepositoryWithCorrectQuery() .SearchAsync(Arg.Any(), Arg.Any()) .Returns(new List()); - await _sut.HandleAsync(new SearchCatalogQuery("tagesthemen")); + await _sut.HandleAsync(new SearchCatalogQuery("tagesthemen"), TestContext.Current.CancellationToken); await _repository .Received(1) diff --git a/tests/Application.Tests/SearchCatalogValidatorTests.cs b/tests/Application.Tests/SearchCatalogValidatorTests.cs index 6681905..2215eae 100644 --- a/tests/Application.Tests/SearchCatalogValidatorTests.cs +++ b/tests/Application.Tests/SearchCatalogValidatorTests.cs @@ -11,7 +11,7 @@ public class SearchCatalogQueryValidatorTests [Fact] public async Task Validate_ValidQuery_PassesValidation() { - var result = await _sut.ValidateAsync(new SearchCatalogQuery("tagesschau")); + var result = await _sut.ValidateAsync(new SearchCatalogQuery("tagesschau"), TestContext.Current.CancellationToken); result.IsValid.ShouldBeTrue(); } @@ -20,7 +20,7 @@ public async Task Validate_ValidQuery_PassesValidation() [InlineData(" ")] public async Task Validate_EmptyQuery_FailsValidation(string query) { - var result = await _sut.ValidateAsync(new SearchCatalogQuery(query)); + var result = await _sut.ValidateAsync(new SearchCatalogQuery(query), TestContext.Current.CancellationToken); result.IsValid.ShouldBeFalse(); result.Errors.ShouldContain(e => e.PropertyName == "Query"); } @@ -28,7 +28,7 @@ public async Task Validate_EmptyQuery_FailsValidation(string query) [Fact] public async Task Validate_SingleCharQuery_FailsValidation() { - var result = await _sut.ValidateAsync(new SearchCatalogQuery("a")); + var result = await _sut.ValidateAsync(new SearchCatalogQuery("a"), TestContext.Current.CancellationToken); result.IsValid.ShouldBeFalse(); result.Errors.ShouldContain(e => e.ErrorMessage.Contains("2 characters")); } @@ -37,7 +37,7 @@ public async Task Validate_SingleCharQuery_FailsValidation() public async Task Validate_QueryExceeding200Chars_FailsValidation() { var longQuery = new string('a', 201); - var result = await _sut.ValidateAsync(new SearchCatalogQuery(longQuery)); + var result = await _sut.ValidateAsync(new SearchCatalogQuery(longQuery), TestContext.Current.CancellationToken); result.IsValid.ShouldBeFalse(); result.Errors.ShouldContain(e => e.ErrorMessage.Contains("200 characters")); } diff --git a/tests/Architecture.Tests/ApplicationSliceSpecs.cs b/tests/Architecture.Tests/ApplicationSliceSpecs.cs index b11a5a2..4b85f2b 100644 --- a/tests/Architecture.Tests/ApplicationSliceSpecs.cs +++ b/tests/Architecture.Tests/ApplicationSliceSpecs.cs @@ -1,5 +1,5 @@ using ArchUnitNET.Loader; -using ArchUnitNET.xUnit; +using ArchUnitNET.Fluent; using Xunit; using static ArchUnitNET.Fluent.ArchRuleDefinition; diff --git a/tests/Architecture.Tests/ArchRuleAssert.cs b/tests/Architecture.Tests/ArchRuleAssert.cs new file mode 100644 index 0000000..cb5e0e5 --- /dev/null +++ b/tests/Architecture.Tests/ArchRuleAssert.cs @@ -0,0 +1,27 @@ +using ArchUnitNET.Domain; +using ArchUnitNET.Fluent; +using Xunit; + +namespace Krautwatch.Architecture.Tests; + +/// +/// Replaces TngTech.ArchUnitNET.xUnit's Check() extension. That package still depends on +/// xunit.assert 2.x, which cannot coexist with xunit v3, so we evaluate the rule against the +/// core ArchUnitNET API and fail through xunit ourselves. +/// +internal static class ArchRuleAssert +{ + /// Evaluates the rule and fails the test with every violation listed if it does not hold. + public static void Check(this IArchRule rule, ArchUnitNET.Domain.Architecture architecture) + { + if (rule.HasNoViolations(architecture)) return; + + var violations = rule.Evaluate(architecture) + .Where(result => !result.Passed) + .Select(result => $" - {result.Description}") + .ToList(); + + Assert.Fail($"Architecture rule violated: {rule.Description}{Environment.NewLine}" + + string.Join(Environment.NewLine, violations)); + } +} diff --git a/tests/Architecture.Tests/HexagonalArchitectureSpecs.cs b/tests/Architecture.Tests/HexagonalArchitectureSpecs.cs index e765647..bd2ccfb 100644 --- a/tests/Architecture.Tests/HexagonalArchitectureSpecs.cs +++ b/tests/Architecture.Tests/HexagonalArchitectureSpecs.cs @@ -1,6 +1,6 @@ using ArchUnitNET.Domain; using ArchUnitNET.Loader; -using ArchUnitNET.xUnit; +using ArchUnitNET.Fluent; using Xunit; using static ArchUnitNET.Fluent.ArchRuleDefinition; diff --git a/tests/Architecture.Tests/Krautwatch.Architecture.Tests.csproj b/tests/Architecture.Tests/Krautwatch.Architecture.Tests.csproj index 8eef37d..11e0fa8 100644 --- a/tests/Architecture.Tests/Krautwatch.Architecture.Tests.csproj +++ b/tests/Architecture.Tests/Krautwatch.Architecture.Tests.csproj @@ -1,5 +1,10 @@ + + + Exe + + @@ -8,9 +13,9 @@ - + - + diff --git a/tests/Domain.Tests/Krautwatch.Domain.Tests.csproj b/tests/Domain.Tests/Krautwatch.Domain.Tests.csproj index 8598ffd..c3eaf43 100644 --- a/tests/Domain.Tests/Krautwatch.Domain.Tests.csproj +++ b/tests/Domain.Tests/Krautwatch.Domain.Tests.csproj @@ -1,12 +1,17 @@ + + + Exe + + - + diff --git a/tests/Infrastructure.Tests/DownloadJobRepositoryTests.cs b/tests/Infrastructure.Tests/DownloadJobRepositoryTests.cs index e0d67ec..b7cfa63 100644 --- a/tests/Infrastructure.Tests/DownloadJobRepositoryTests.cs +++ b/tests/Infrastructure.Tests/DownloadJobRepositoryTests.cs @@ -15,7 +15,7 @@ public class DownloadJobRepositoryTests(PostgresFixture postgres) : IAsyncLifeti // ExecuteUpdate + a re-read on the same context returns a stale tracked entity. private DbContextOptions _options = null!; - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { _options = await postgres.CreateDatabaseAsync(); @@ -30,7 +30,7 @@ public async Task InitializeAsync() await db.SaveChangesAsync(); } - public Task DisposeAsync() => Task.CompletedTask; + public ValueTask DisposeAsync() => ValueTask.CompletedTask; private DownloadJobRepository Repo() => new(new AppDbContext(_options)); @@ -56,7 +56,7 @@ public async Task TryClaimNext_claims_the_oldest_queued_job_with_its_episode() var older = await AddQueuedAsync(DateTimeOffset.UtcNow.AddMinutes(-10)); await AddQueuedAsync(DateTimeOffset.UtcNow.AddMinutes(-1)); - var claimed = await Repo().TryClaimNextAsync("worker-1"); + var claimed = await Repo().TryClaimNextAsync("worker-1", TestContext.Current.CancellationToken); claimed.ShouldNotBeNull(); claimed!.Id.ShouldBe(older); @@ -68,7 +68,7 @@ public async Task TryClaimNext_claims_the_oldest_queued_job_with_its_episode() [Fact] public async Task TryClaimNext_returns_null_when_nothing_is_queued() { - (await Repo().TryClaimNextAsync("worker-1")).ShouldBeNull(); + (await Repo().TryClaimNextAsync("worker-1", TestContext.Current.CancellationToken)).ShouldBeNull(); } [Fact] @@ -76,20 +76,20 @@ public async Task TryClaimNext_will_not_hand_the_same_job_to_two_workers() { await AddQueuedAsync(DateTimeOffset.UtcNow.AddMinutes(-5)); - (await Repo().TryClaimNextAsync("worker-1")).ShouldNotBeNull(); - (await Repo().TryClaimNextAsync("worker-2")).ShouldBeNull(); // the only job is already claimed + (await Repo().TryClaimNextAsync("worker-1", TestContext.Current.CancellationToken)).ShouldNotBeNull(); + (await Repo().TryClaimNextAsync("worker-2", TestContext.Current.CancellationToken)).ShouldBeNull(); // the only job is already claimed } [Fact] public async Task ReclaimStale_requeues_this_workers_downloading_jobs() { var id = await AddQueuedAsync(DateTimeOffset.UtcNow.AddMinutes(-5)); - await Repo().TryClaimNextAsync("worker-1"); // → Downloading, worker-1 + await Repo().TryClaimNextAsync("worker-1", TestContext.Current.CancellationToken); // → Downloading, worker-1 - var reclaimed = await Repo().ReclaimStaleAsync("worker-1"); + var reclaimed = await Repo().ReclaimStaleAsync("worker-1", TestContext.Current.CancellationToken); reclaimed.ShouldBe(1); - var job = await Repo().GetByIdAsync(id); + var job = await Repo().GetByIdAsync(id, TestContext.Current.CancellationToken); job!.Status.ShouldBe(DownloadStatus.Queued); job.WorkerId.ShouldBeNull(); } @@ -98,9 +98,9 @@ public async Task ReclaimStale_requeues_this_workers_downloading_jobs() public async Task ReclaimStale_leaves_other_workers_jobs_alone() { await AddQueuedAsync(DateTimeOffset.UtcNow.AddMinutes(-5)); - await Repo().TryClaimNextAsync("worker-1"); + await Repo().TryClaimNextAsync("worker-1", TestContext.Current.CancellationToken); - (await Repo().ReclaimStaleAsync("worker-2")).ShouldBe(0); + (await Repo().ReclaimStaleAsync("worker-2", TestContext.Current.CancellationToken)).ShouldBe(0); } [Fact] @@ -108,9 +108,9 @@ public async Task Delete_removes_the_job() { var id = await AddQueuedAsync(DateTimeOffset.UtcNow); - await Repo().DeleteAsync(id); + await Repo().DeleteAsync(id, TestContext.Current.CancellationToken); - (await Repo().GetByIdAsync(id)).ShouldBeNull(); + (await Repo().GetByIdAsync(id, TestContext.Current.CancellationToken)).ShouldBeNull(); } } diff --git a/tests/Infrastructure.Tests/EgressProxyTests.cs b/tests/Infrastructure.Tests/EgressProxyTests.cs index 9e10d44..427400a 100644 --- a/tests/Infrastructure.Tests/EgressProxyTests.cs +++ b/tests/Infrastructure.Tests/EgressProxyTests.cs @@ -20,9 +20,9 @@ public class ProxyRepositoryTests(PostgresFixture postgres) : IAsyncLifetime { private DbContextOptions _options = null!; - public async Task InitializeAsync() => _options = await postgres.CreateDatabaseAsync(); + public async ValueTask InitializeAsync() => _options = await postgres.CreateDatabaseAsync(); - public Task DisposeAsync() => Task.CompletedTask; + public ValueTask DisposeAsync() => ValueTask.CompletedTask; private ProxyRepository Repo() => new(new AppDbContext(_options)); @@ -35,13 +35,13 @@ public class ProxyRepositoryTests(PostgresFixture postgres) : IAsyncLifetime [Fact] public async Task Upsert_adds_new_rows_then_refreshes_metrics_but_keeps_our_feedback() { - await Repo().UpsertBatchAsync([P("1.1.1.1", 3128, upTime: 40)]); - await Repo().RecordProbeResultAsync("http://1.1.1.1:3128", ok: true); + await Repo().UpsertBatchAsync([P("1.1.1.1", 3128, upTime: 40)], TestContext.Current.CancellationToken); + await Repo().RecordProbeResultAsync("http://1.1.1.1:3128", ok: true, TestContext.Current.CancellationToken); // A refresh brings new source metrics for the same host:port. - await Repo().UpsertBatchAsync([P("1.1.1.1", 3128, upTime: 90)]); + await Repo().UpsertBatchAsync([P("1.1.1.1", 3128, upTime: 90)], TestContext.Current.CancellationToken); - var row = (await Repo().GetRankedAsync("DE", 10)).ShouldHaveSingleItem(); + var row = (await Repo().GetRankedAsync("DE", 10, TestContext.Current.CancellationToken)).ShouldHaveSingleItem(); row.UpTime.ShouldBe(90); // source metric refreshed row.LastProbeOk.ShouldBe(true); // our feedback preserved across the refresh } @@ -54,9 +54,9 @@ await Repo().UpsertBatchAsync( P("bad", 1, upTime: 99, probeOk: false), // known-bad sinks despite high uptime P("unknown", 2, upTime: 60), // untested P("good", 3, upTime: 10, probeOk: true), // known-good floats up despite low uptime - ]); + ], TestContext.Current.CancellationToken); - var ranked = await Repo().GetRankedAsync("DE", 10); + var ranked = await Repo().GetRankedAsync("DE", 10, TestContext.Current.CancellationToken); ranked.Select(p => p.Host).ShouldBe(["good", "unknown", "bad"]); } @@ -66,9 +66,9 @@ public async Task GetRanked_only_returns_the_requested_country() { var de = P("de", 1); var other = new Proxy { Id = "ch:2", Host = "ch", Port = 2, Protocol = "http", Source = "geonode", Country = "CH" }; - await Repo().UpsertBatchAsync([de, other]); + await Repo().UpsertBatchAsync([de, other], TestContext.Current.CancellationToken); - (await Repo().GetRankedAsync("DE", 10)).ShouldHaveSingleItem().Host.ShouldBe("de"); + (await Repo().GetRankedAsync("DE", 10, TestContext.Current.CancellationToken)).ShouldHaveSingleItem().Host.ShouldBe("de"); } } @@ -96,7 +96,7 @@ public async Task Parses_the_geonode_shape_and_skips_malformed_rows() var http = new HttpClient(new StubHandler(Json)); var source = new GeoNodeProxyListSource(http, new ProxyListOptions(), NullLogger.Instance); - var proxies = await source.FetchAsync(); + var proxies = await source.FetchAsync(TestContext.Current.CancellationToken); var p = proxies.ShouldHaveSingleItem(); // the port-less row is dropped p.Id.ShouldBe("1.2.3.4:3128"); @@ -116,7 +116,7 @@ public async Task Byo_proxy_is_offered_when_configured() var opts = new EgressProxyOptions { ProxyUrl = "http://10.0.0.9:3128" }; var sut = new EgressProxyProvider(opts, Substitute.For()); - (await sut.GetCandidatesAsync()).ShouldBe(["http://10.0.0.9:3128"]); + (await sut.GetCandidatesAsync(TestContext.Current.CancellationToken)).ShouldBe(["http://10.0.0.9:3128"]); } [Fact] @@ -124,7 +124,7 @@ public async Task No_candidates_when_nothing_is_configured() { var sut = new EgressProxyProvider(new EgressProxyOptions(), Substitute.For()); - (await sut.GetCandidatesAsync()).ShouldBeEmpty(); + (await sut.GetCandidatesAsync(TestContext.Current.CancellationToken)).ShouldBeEmpty(); } } diff --git a/tests/Infrastructure.Tests/EpisodeRepositoryTests.cs b/tests/Infrastructure.Tests/EpisodeRepositoryTests.cs index b335319..832f620 100644 --- a/tests/Infrastructure.Tests/EpisodeRepositoryTests.cs +++ b/tests/Infrastructure.Tests/EpisodeRepositoryTests.cs @@ -15,7 +15,7 @@ public class EpisodeRepositoryTests(PostgresFixture postgres) : IAsyncLifetime private AppDbContext _db = null!; private EpisodeRepository _sut = null!; - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { _db = new AppDbContext(await postgres.CreateDatabaseAsync()); _sut = new EpisodeRepository(_db); @@ -23,10 +23,10 @@ public async Task InitializeAsync() SeedTestData(); } - public Task DisposeAsync() + public ValueTask DisposeAsync() { _db.Dispose(); - return Task.CompletedTask; + return ValueTask.CompletedTask; } private void SeedTestData() @@ -77,7 +77,7 @@ private void SeedTestData() [Fact] public async Task GetByIdAsync_ExistingEpisode_ReturnsEpisodeWithStreams() { - var result = await _sut.GetByIdAsync("ep-1"); + var result = await _sut.GetByIdAsync("ep-1", TestContext.Current.CancellationToken); result.ShouldNotBeNull(); result.Title.ShouldBe("Tagesschau 20 Uhr"); @@ -88,14 +88,14 @@ public async Task GetByIdAsync_ExistingEpisode_ReturnsEpisodeWithStreams() [Fact] public async Task GetByIdAsync_NonExistentId_ReturnsNull() { - var result = await _sut.GetByIdAsync("does-not-exist"); + var result = await _sut.GetByIdAsync("does-not-exist", TestContext.Current.CancellationToken); result.ShouldBeNull(); } [Fact] public async Task SearchAsync_MatchingTitle_ReturnsResults() { - var results = await _sut.SearchAsync("tagesschau"); + var results = await _sut.SearchAsync("tagesschau", TestContext.Current.CancellationToken); results.ShouldNotBeEmpty(); results.ShouldContain(e => e.Title == "Tagesschau 20 Uhr"); @@ -104,7 +104,7 @@ public async Task SearchAsync_MatchingTitle_ReturnsResults() [Fact] public async Task SearchAsync_MatchingDescription_ReturnsResults() { - var results = await _sut.SearchAsync("nachrichten"); + var results = await _sut.SearchAsync("nachrichten", TestContext.Current.CancellationToken); results.ShouldNotBeEmpty(); results.ShouldContain(e => e.Id == "ep-1"); @@ -113,14 +113,14 @@ public async Task SearchAsync_MatchingDescription_ReturnsResults() [Fact] public async Task SearchAsync_NoMatch_ReturnsEmpty() { - var results = await _sut.SearchAsync("zdfmediathek-xyz-nomatch"); + var results = await _sut.SearchAsync("zdfmediathek-xyz-nomatch", TestContext.Current.CancellationToken); results.ShouldBeEmpty(); } [Fact] public async Task GetByChannelAsync_KnownChannel_ReturnsEpisodes() { - var results = await _sut.GetByChannelAsync("ard"); + var results = await _sut.GetByChannelAsync("ard", ct: TestContext.Current.CancellationToken); results.Count.ShouldBe(2); results.ShouldAllBe(e => e.Show.Channel.Id == "ard"); @@ -129,8 +129,8 @@ public async Task GetByChannelAsync_KnownChannel_ReturnsEpisodes() [Fact] public async Task UpsertManyAsync_NewEpisodes_AreInserted() { - var channel = await _db.Channels.FindAsync("ard"); - var show = await _db.Shows.FindAsync("show-1"); + var channel = await _db.Channels.FindAsync(new object[] { "ard" }, TestContext.Current.CancellationToken); + var show = await _db.Shows.FindAsync(new object[] { "show-1" }, TestContext.Current.CancellationToken); var newEpisodes = new[] { @@ -146,9 +146,9 @@ public async Task UpsertManyAsync_NewEpisodes_AreInserted() } }; - await _sut.UpsertManyAsync(newEpisodes); + await _sut.UpsertManyAsync(newEpisodes, TestContext.Current.CancellationToken); - var result = await _sut.GetByIdAsync("ep-new-1"); + var result = await _sut.GetByIdAsync("ep-new-1", TestContext.Current.CancellationToken); result.ShouldNotBeNull(); result.Title.ShouldBe("New Episode"); } @@ -182,12 +182,12 @@ public async Task UpsertManyAsync_FreshCrawlGraph_InsertsChannelShowEpisodeAndSt ] }).ToList(); - await _sut.UpsertManyAsync(episodes); + await _sut.UpsertManyAsync(episodes, TestContext.Current.CancellationToken); - (await _db.Channels.FindAsync("zdf")).ShouldNotBeNull(); - (await _db.Shows.FindAsync("zdf:heute-show")).ShouldNotBeNull(); + (await _db.Channels.FindAsync(new object[] { "zdf" }, TestContext.Current.CancellationToken)).ShouldNotBeNull(); + (await _db.Shows.FindAsync(new object[] { "zdf:heute-show" }, TestContext.Current.CancellationToken)).ShouldNotBeNull(); - var persisted = await _sut.GetByIdAsync("zdf:doc-1"); + var persisted = await _sut.GetByIdAsync("zdf:doc-1", TestContext.Current.CancellationToken); persisted.ShouldNotBeNull(); persisted.Show.Channel.Name.ShouldBe("ZDF"); persisted.Streams.ShouldHaveSingleItem().Url.ShouldBe("https://cdn.zdf.de/doc-1.mp4"); @@ -212,10 +212,10 @@ public async Task UpsertManyAsync_RecrawlSameEpisode_UpdatesInPlaceWithoutDuplic ] }; - await _sut.UpsertManyAsync([Build("first title")]); - await _sut.UpsertManyAsync([Build("updated title")]); + await _sut.UpsertManyAsync([Build("first title")], TestContext.Current.CancellationToken); + await _sut.UpsertManyAsync([Build("updated title")], TestContext.Current.CancellationToken); - var all = await _sut.GetByShowAsync("ardx:extra-3"); + var all = await _sut.GetByShowAsync("ardx:extra-3", TestContext.Current.CancellationToken); all.ShouldHaveSingleItem().Title.ShouldBe("updated title"); } diff --git a/tests/Infrastructure.Tests/Krautwatch.Infrastructure.Tests.csproj b/tests/Infrastructure.Tests/Krautwatch.Infrastructure.Tests.csproj index d58b725..0ea2a9a 100644 --- a/tests/Infrastructure.Tests/Krautwatch.Infrastructure.Tests.csproj +++ b/tests/Infrastructure.Tests/Krautwatch.Infrastructure.Tests.csproj @@ -1,5 +1,10 @@ + + + Exe + + @@ -7,7 +12,7 @@ - + diff --git a/tests/Infrastructure.Tests/PostgresFixture.cs b/tests/Infrastructure.Tests/PostgresFixture.cs index 5916bff..4186fdc 100644 --- a/tests/Infrastructure.Tests/PostgresFixture.cs +++ b/tests/Infrastructure.Tests/PostgresFixture.cs @@ -20,9 +20,9 @@ public sealed class PostgresFixture : IAsyncLifetime private readonly PostgreSqlContainer _container = new PostgreSqlBuilder("postgres:17-alpine").Build(); - public Task InitializeAsync() => _container.StartAsync(); + public async ValueTask InitializeAsync() => await _container.StartAsync(); - public Task DisposeAsync() => _container.DisposeAsync().AsTask(); + public ValueTask DisposeAsync() => _container.DisposeAsync(); /// /// Creates an isolated database with the schema applied, and returns options bound to it. diff --git a/tests/Live.Tests/ArdLiveTests.cs b/tests/Live.Tests/ArdLiveTests.cs index a24e31a..f515a7c 100644 --- a/tests/Live.Tests/ArdLiveTests.cs +++ b/tests/Live.Tests/ArdLiveTests.cs @@ -25,7 +25,7 @@ private static HttpClient CreateHttp() [Fact] public async Task Search_finds_Extra3_on_ARD() { - var show = await Client.FindShowAsync("Extra 3"); + var show = await Client.FindShowAsync("Extra 3", ct: TestContext.Current.CancellationToken); show.ShouldNotBeNull(); show!.Title.ShouldContain("extra 3", Case.Insensitive); @@ -35,10 +35,10 @@ public async Task Search_finds_Extra3_on_ARD() [Fact] public async Task Fetches_Extra3_full_episodes_with_sane_metadata() { - var show = await Client.FindShowAsync("Extra 3"); + var show = await Client.FindShowAsync("Extra 3", ct: TestContext.Current.CancellationToken); show.ShouldNotBeNull(); - var episodes = await Client.GetFullEpisodesAsync(show!); + var episodes = await Client.GetFullEpisodesAsync(show!, TestContext.Current.CancellationToken); episodes.ShouldNotBeEmpty(); // Full "extra 3" episodes follow the "extra 3 vom " pattern and run ~30-50 min. @@ -52,11 +52,11 @@ public async Task Fetches_Extra3_full_episodes_with_sane_metadata() [Fact] public async Task Downloads_a_full_Extra3_episode() { - var show = await Client.FindShowAsync("Extra 3"); + var show = await Client.FindShowAsync("Extra 3", ct: TestContext.Current.CancellationToken); show.ShouldNotBeNull(); - var episodes = await Client.GetFullEpisodesAsync(show!); + var episodes = await Client.GetFullEpisodesAsync(show!, TestContext.Current.CancellationToken); var full = episodes.First(e => e.Title.Contains("extra 3 vom", StringComparison.OrdinalIgnoreCase)); - var detail = await Client.FetchEpisodeDetailAsync(full); + var detail = await Client.FetchEpisodeDetailAsync(full, TestContext.Current.CancellationToken); detail.ShouldNotBeNull(); detail!.StreamUrl.ShouldNotBeNull(); @@ -77,7 +77,7 @@ static ArdKikaLiveTests() => [Fact] public async Task Search_finds_Biene_Maja_on_KiKA() { - var show = await Client.FindShowAsync("Biene Maja", client: "kika"); + var show = await Client.FindShowAsync("Biene Maja", client: "kika", TestContext.Current.CancellationToken); show.ShouldNotBeNull(); show!.Title.ShouldContain("Biene Maja", Case.Insensitive); @@ -88,9 +88,9 @@ public async Task Search_finds_Biene_Maja_on_KiKA() [Fact] public async Task Fetches_Biene_Maja_episodes() { - var show = await Client.FindShowAsync("Biene Maja", client: "kika"); + var show = await Client.FindShowAsync("Biene Maja", client: "kika", TestContext.Current.CancellationToken); show.ShouldNotBeNull(); - var episodes = await Client.GetFullEpisodesAsync(show!); + var episodes = await Client.GetFullEpisodesAsync(show!, TestContext.Current.CancellationToken); episodes.ShouldNotBeEmpty(); } } diff --git a/tests/Live.Tests/BroadcasterCrawlerLiveTests.cs b/tests/Live.Tests/BroadcasterCrawlerLiveTests.cs index f540c2d..29e931d 100644 --- a/tests/Live.Tests/BroadcasterCrawlerLiveTests.cs +++ b/tests/Live.Tests/BroadcasterCrawlerLiveTests.cs @@ -29,7 +29,7 @@ public async Task Ard_crawler_maps_Extra3_to_domain_episodes_with_streams() { var crawler = new ArdBroadcasterCrawler(new ArdCatalogClient(Http), "ard", "ard", "ARD"); - var episodes = await crawler.CrawlShowAsync("Extra 3"); + var episodes = await crawler.CrawlShowAsync("Extra 3", TestContext.Current.CancellationToken); episodes.ShouldNotBeEmpty(); var episode = episodes[0]; @@ -48,7 +48,7 @@ public async Task Zdf_crawler_maps_HeuteShow_to_domain_episodes_with_streams() { var crawler = new ZdfBroadcasterCrawler(new ZdfCatalogClient(Http)); - var episodes = await crawler.CrawlShowAsync("heute-show"); + var episodes = await crawler.CrawlShowAsync("heute-show", TestContext.Current.CancellationToken); episodes.ShouldNotBeEmpty(); episodes.ShouldAllBe(e => e.Id.StartsWith("zdf:")); diff --git a/tests/Live.Tests/FullDownloadTests.cs b/tests/Live.Tests/FullDownloadTests.cs index f9eeb52..affb2e5 100644 --- a/tests/Live.Tests/FullDownloadTests.cs +++ b/tests/Live.Tests/FullDownloadTests.cs @@ -33,10 +33,10 @@ static FullDownloadTests() => public async Task Downloads_a_full_Extra3_episode_from_ARD() { var ard = new ArdCatalogClient(Http); - var show = await ard.FindShowAsync("Extra 3"); - var full = (await ard.GetFullEpisodesAsync(show!)) + var show = await ard.FindShowAsync("Extra 3", ct: TestContext.Current.CancellationToken); + var full = (await ard.GetFullEpisodesAsync(show!, TestContext.Current.CancellationToken)) .First(e => e.Title.Contains("extra 3 vom", StringComparison.OrdinalIgnoreCase)); - await DownloadAndVerifyAsync(await ard.FetchEpisodeDetailAsync(full)); + await DownloadAndVerifyAsync(await ard.FetchEpisodeDetailAsync(full, TestContext.Current.CancellationToken)); } // A DE egress proxy for local runs (#45). Set it to do the REAL geo-restricted download; leave it @@ -48,11 +48,11 @@ public async Task Downloads_a_full_Extra3_episode_from_ARD() public async Task Downloads_a_full_BieneMaja_episode_from_KiKA() { var ard = new ArdCatalogClient(Http); - var show = await ard.FindShowAsync("Biene Maja", client: "kika"); - var full = (await ard.GetFullEpisodesAsync(show!)).First(); + var show = await ard.FindShowAsync("Biene Maja", client: "kika", TestContext.Current.CancellationToken); + var full = (await ard.GetFullEpisodesAsync(show!, TestContext.Current.CancellationToken)).First(); // Biene Maja is DACH geo-fenced. With a DE egress (KRAUTWATCH_TEST_PROXY) it downloads for real; // without one, the provider fails fast (geo-restricted + no egress) — tolerated here. - await DownloadAndVerifyAsync(await ard.FetchEpisodeDetailAsync(full), + await DownloadAndVerifyAsync(await ard.FetchEpisodeDetailAsync(full, TestContext.Current.CancellationToken), tolerateGeoBlock: string.IsNullOrWhiteSpace(TestProxy)); } @@ -60,9 +60,9 @@ await DownloadAndVerifyAsync(await ard.FetchEpisodeDetailAsync(full), public async Task Downloads_a_full_HeuteShow_episode_from_ZDF() { var zdf = new ZdfCatalogClient(Http); - var full = (await zdf.SearchEpisodesAsync("Heute Show")) + var full = (await zdf.SearchEpisodesAsync("Heute Show", TestContext.Current.CancellationToken)) .First(e => e.Title.Contains("heute-show vom", StringComparison.OrdinalIgnoreCase)); - await DownloadAndVerifyAsync(await zdf.FetchEpisodeDetailAsync(full)); + await DownloadAndVerifyAsync(await zdf.FetchEpisodeDetailAsync(full, TestContext.Current.CancellationToken)); } // ── shared: run the real provider, assert a genuine MP4 landed, then clean up ── diff --git a/tests/Live.Tests/FullEpisodeFetchTests.cs b/tests/Live.Tests/FullEpisodeFetchTests.cs index 6ce73e5..bce3adb 100644 --- a/tests/Live.Tests/FullEpisodeFetchTests.cs +++ b/tests/Live.Tests/FullEpisodeFetchTests.cs @@ -21,12 +21,12 @@ static FullEpisodeFetchTests() => public async Task Fetches_a_full_Extra3_episode_from_ARD() { var ard = new ArdCatalogClient(Http); - var show = await ard.FindShowAsync("Extra 3"); + var show = await ard.FindShowAsync("Extra 3", ct: TestContext.Current.CancellationToken); show.ShouldNotBeNull(); - var episodes = await ard.GetFullEpisodesAsync(show!); + var episodes = await ard.GetFullEpisodesAsync(show!, TestContext.Current.CancellationToken); var full = episodes.First(e => e.Title.Contains("extra 3 vom", StringComparison.OrdinalIgnoreCase)); - var detail = await ard.FetchEpisodeDetailAsync(full); + var detail = await ard.FetchEpisodeDetailAsync(full, TestContext.Current.CancellationToken); detail.ShouldNotBeNull(); detail!.Title.ShouldContain("extra 3 vom", Case.Insensitive); @@ -45,12 +45,12 @@ public async Task Fetches_a_full_Extra3_episode_from_ARD() public async Task Fetches_a_full_BieneMaja_episode_from_KiKA() { var ard = new ArdCatalogClient(Http); - var show = await ard.FindShowAsync("Biene Maja", client: "kika"); + var show = await ard.FindShowAsync("Biene Maja", client: "kika", TestContext.Current.CancellationToken); show.ShouldNotBeNull(); - var episodes = await ard.GetFullEpisodesAsync(show!); + var episodes = await ard.GetFullEpisodesAsync(show!, TestContext.Current.CancellationToken); var full = episodes.First(); - var detail = await ard.FetchEpisodeDetailAsync(full); + var detail = await ard.FetchEpisodeDetailAsync(full, TestContext.Current.CancellationToken); detail.ShouldNotBeNull(); detail!.Title.ShouldNotBeNullOrWhiteSpace(); @@ -66,10 +66,10 @@ public async Task Fetches_a_full_BieneMaja_episode_from_KiKA() public async Task Fetches_a_full_HeuteShow_episode_from_ZDF() { var zdf = new ZdfCatalogClient(Http); - var episodes = await zdf.SearchEpisodesAsync("Heute Show"); + var episodes = await zdf.SearchEpisodesAsync("Heute Show", TestContext.Current.CancellationToken); var full = episodes.First(e => e.Title.Contains("heute-show vom", StringComparison.OrdinalIgnoreCase)); - var detail = await zdf.FetchEpisodeDetailAsync(full); + var detail = await zdf.FetchEpisodeDetailAsync(full, TestContext.Current.CancellationToken); detail.ShouldNotBeNull(); detail!.Title.ShouldContain("heute-show vom", Case.Insensitive); diff --git a/tests/Live.Tests/Krautwatch.Live.Tests.csproj b/tests/Live.Tests/Krautwatch.Live.Tests.csproj index 191fa07..b6a0498 100644 --- a/tests/Live.Tests/Krautwatch.Live.Tests.csproj +++ b/tests/Live.Tests/Krautwatch.Live.Tests.csproj @@ -1,5 +1,10 @@ + + + Exe + + @@ -7,7 +12,7 @@ - + diff --git a/tests/Live.Tests/ZdfLiveTests.cs b/tests/Live.Tests/ZdfLiveTests.cs index c37b224..462d485 100644 --- a/tests/Live.Tests/ZdfLiveTests.cs +++ b/tests/Live.Tests/ZdfLiveTests.cs @@ -21,7 +21,7 @@ static ZdfLiveTests() => [Fact] public async Task Search_finds_HeuteShow_episodes() { - var episodes = await Client.SearchEpisodesAsync("Heute Show"); + var episodes = await Client.SearchEpisodesAsync("Heute Show", TestContext.Current.CancellationToken); episodes.ShouldNotBeEmpty(); episodes.ShouldContain(e => e.Title.Contains("heute-show vom", StringComparison.OrdinalIgnoreCase)); @@ -31,10 +31,10 @@ public async Task Search_finds_HeuteShow_episodes() [Fact] public async Task Resolves_a_HeuteShow_progressive_MP4() { - var episodes = await Client.SearchEpisodesAsync("Heute Show"); + var episodes = await Client.SearchEpisodesAsync("Heute Show", TestContext.Current.CancellationToken); var episode = episodes.First(e => e.Title.Contains("heute-show vom", StringComparison.OrdinalIgnoreCase)); - var stream = await Client.ResolveBestMp4Async(episode.Canonical); + var stream = await Client.ResolveBestMp4Async(episode.Canonical, TestContext.Current.CancellationToken); stream.ShouldNotBeNull(); stream!.MimeType.ShouldContain("mp4"); @@ -47,26 +47,26 @@ public async Task Downloads_a_HeuteShow_episode() { // Real download of a real ZDF stream, bounded to ~5 MB so it's fast + small. // The production Downloader agent streams the whole file; this proves the pipeline. - var episodes = await Client.SearchEpisodesAsync("Heute Show"); + var episodes = await Client.SearchEpisodesAsync("Heute Show", TestContext.Current.CancellationToken); var episode = episodes.First(e => e.Title.Contains("heute-show vom", StringComparison.OrdinalIgnoreCase)); - var stream = await Client.ResolveBestMp4Async(episode.Canonical); + var stream = await Client.ResolveBestMp4Async(episode.Canonical, TestContext.Current.CancellationToken); stream.ShouldNotBeNull(); var path = Path.Combine(Path.GetTempPath(), $"krautwatch-heuteshow-{Guid.NewGuid():N}.mp4"); try { const int cap = 5 * 1024 * 1024; - using var resp = await Http.GetAsync(stream!.Url, HttpCompletionOption.ResponseHeadersRead); + using var resp = await Http.GetAsync(stream!.Url, HttpCompletionOption.ResponseHeadersRead, TestContext.Current.CancellationToken); resp.EnsureSuccessStatusCode(); - await using (var source = await resp.Content.ReadAsStreamAsync()) + await using (var source = await resp.Content.ReadAsStreamAsync(TestContext.Current.CancellationToken)) await using (var file = File.Create(path)) { var buffer = new byte[81920]; long total = 0; int read; - while (total < cap && (read = await source.ReadAsync(buffer)) > 0) + while (total < cap && (read = await source.ReadAsync(buffer, TestContext.Current.CancellationToken)) > 0) { - await file.WriteAsync(buffer.AsMemory(0, read)); + await file.WriteAsync(buffer.AsMemory(0, read), TestContext.Current.CancellationToken); total += read; } }