Build ErsatzTV Image / Functional E2E (curl contracts) (push) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (push) Has been skipped
Build ErsatzTV Image / CI image pin matches docker/ci (push) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build CI Toolchain Image / Build & push CI image (push) Failing after 10m41s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 18m20s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 18m31s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m58s
Co-authored-by: Timothy <timothy.look@gmail.com> Co-committed-by: Timothy <timothy.look@gmail.com>
241 lines
8.4 KiB
C#
241 lines
8.4 KiB
C#
using ErsatzTV.Application.MediaSources;
|
|
using ErsatzTV.Core.Api.MediaSources;
|
|
using ErsatzTV.Core.Domain;
|
|
using ErsatzTV.Infrastructure.Data;
|
|
using ErsatzTV.Tests.Support;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NUnit.Framework;
|
|
using Shouldly;
|
|
|
|
namespace ErsatzTV.Tests.Application.MediaSources;
|
|
|
|
[TestFixture]
|
|
public class GetAllMediaSourcesForApiHandlerTests
|
|
{
|
|
private InMemoryTvContext _db = null!;
|
|
|
|
[SetUp]
|
|
public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync();
|
|
|
|
[TearDown]
|
|
public async Task TearDown() => await _db.DisposeAsync();
|
|
|
|
[Test]
|
|
public async Task Handle_Should_Group_Configured_Libraries_By_Source_With_Item_Counts()
|
|
{
|
|
await SeedMediaSources();
|
|
var handler = new GetAllMediaSourcesForApiHandler(_db.Factory);
|
|
|
|
var result = await handler.Handle(new GetAllMediaSourcesForApi(), CancellationToken.None);
|
|
|
|
result.Count.ShouldBe(4);
|
|
|
|
result[0].Kind.ShouldBe("Local");
|
|
result[0].Name.ShouldBe("Local");
|
|
result[0].ConnectionAddress.ShouldBeNull();
|
|
result[0].Libraries.Single().Name.ShouldBe("Local Movies");
|
|
result[0].Libraries.Single().LastScan.ShouldBe(new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc));
|
|
result[0].Libraries.Single().ItemCount.ShouldBe(2);
|
|
|
|
result[1].Kind.ShouldBe("Emby");
|
|
result[1].Name.ShouldBe("Emby Server");
|
|
result[1].ConnectionAddress.ShouldBe("http://emby.local");
|
|
result[1].Libraries.Single().Name.ShouldBe("Emby Shows");
|
|
result[1].Libraries.Single().ItemCount.ShouldBe(1);
|
|
|
|
result[2].Kind.ShouldBe("Jellyfin");
|
|
result[2].Name.ShouldBe("Jellyfin Server");
|
|
result[2].ConnectionAddress.ShouldBe("http://jellyfin.local");
|
|
result[2].Libraries.ShouldBeEmpty();
|
|
|
|
result[3].Kind.ShouldBe("Plex");
|
|
result[3].Name.ShouldBe("Plex Server");
|
|
result[3].ConnectionAddress.ShouldBe("http://plex.local");
|
|
result[3].Libraries.Single().Name.ShouldBe("Plex Movies");
|
|
result[3].Libraries.Single().ItemCount.ShouldBe(0);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Handle_Should_Succeed_On_Empty_Database()
|
|
{
|
|
// regression: the Dapper item-count query 500'd on a fresh database because
|
|
// SQLite reports COUNT(*) as BLOB when there are no rows to infer from
|
|
var handler = new GetAllMediaSourcesForApiHandler(_db.Factory);
|
|
|
|
var result = await handler.Handle(new GetAllMediaSourcesForApi(), CancellationToken.None);
|
|
|
|
result.ShouldBeEmpty();
|
|
}
|
|
|
|
[Test]
|
|
public async Task Handle_Should_Report_Sentinel_And_Null_LastScan_As_Null()
|
|
{
|
|
await using (TvContext context = _db.CreateContext())
|
|
{
|
|
var source = new LocalMediaSource
|
|
{
|
|
Libraries =
|
|
[
|
|
new LocalLibrary
|
|
{
|
|
Name = "Never Scanned Sentinel",
|
|
MediaKind = LibraryMediaKind.Movies,
|
|
LastScan = new DateTime(1, 1, 1),
|
|
Paths = [MakePath("/media/sentinel", 0)]
|
|
},
|
|
new LocalLibrary
|
|
{
|
|
Name = "Never Scanned Null",
|
|
MediaKind = LibraryMediaKind.Movies,
|
|
LastScan = null,
|
|
Paths = [MakePath("/media/nullscan", 0)]
|
|
},
|
|
new LocalLibrary
|
|
{
|
|
Name = "Really Scanned",
|
|
MediaKind = LibraryMediaKind.Movies,
|
|
LastScan = new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc),
|
|
Paths = [MakePath("/media/scanned", 0)]
|
|
}
|
|
]
|
|
};
|
|
|
|
context.MediaSources.Add(source);
|
|
await context.SaveChangesAsync();
|
|
}
|
|
|
|
var handler = new GetAllMediaSourcesForApiHandler(_db.Factory);
|
|
var result = await handler.Handle(new GetAllMediaSourcesForApi(), CancellationToken.None);
|
|
|
|
MediaSourceLibraryResponseModel[] libraries = result.Single().Libraries.ToArray();
|
|
|
|
libraries.Single(l => l.Name == "Never Scanned Sentinel").LastScan.ShouldBeNull();
|
|
libraries.Single(l => l.Name == "Never Scanned Null").LastScan.ShouldBeNull();
|
|
libraries.Single(l => l.Name == "Really Scanned").LastScan
|
|
.ShouldBe(new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc));
|
|
}
|
|
|
|
[Test]
|
|
public async Task Handle_Should_Exclude_Unconfigured_And_Not_Synced_Libraries()
|
|
{
|
|
await SeedMediaSources();
|
|
var handler = new GetAllMediaSourcesForApiHandler(_db.Factory);
|
|
|
|
var result = await handler.Handle(new GetAllMediaSourcesForApi(), CancellationToken.None);
|
|
|
|
result.SelectMany(s => s.Libraries).Select(l => l.Name)
|
|
.ShouldNotContain("Empty Local");
|
|
result.SelectMany(s => s.Libraries).Select(l => l.Name)
|
|
.ShouldNotContain("Disabled Jellyfin");
|
|
}
|
|
|
|
private async Task SeedMediaSources()
|
|
{
|
|
await using TvContext context = _db.CreateContext();
|
|
|
|
var localSource = new LocalMediaSource
|
|
{
|
|
Libraries =
|
|
[
|
|
new LocalLibrary
|
|
{
|
|
Name = "Local Movies",
|
|
MediaKind = LibraryMediaKind.Movies,
|
|
LastScan = new DateTime(2026, 7, 1, 0, 0, 0, DateTimeKind.Utc),
|
|
Paths = [MakePath("/media/movies", 2)]
|
|
},
|
|
new LocalLibrary
|
|
{
|
|
Name = "Empty Local",
|
|
MediaKind = LibraryMediaKind.Shows,
|
|
Paths = []
|
|
}
|
|
]
|
|
};
|
|
|
|
var plexSource = new PlexMediaSource
|
|
{
|
|
ServerName = "Plex Server",
|
|
ProductVersion = "1",
|
|
Platform = "Linux",
|
|
PlatformVersion = "1",
|
|
ClientIdentifier = "plex",
|
|
Connections = [new PlexConnection { IsActive = true, Uri = "http://plex.local" }],
|
|
PathReplacements = [],
|
|
Libraries =
|
|
[
|
|
new PlexLibrary
|
|
{
|
|
Name = "Plex Movies",
|
|
MediaKind = LibraryMediaKind.Movies,
|
|
Key = "1",
|
|
ShouldSyncItems = true,
|
|
Paths = []
|
|
}
|
|
]
|
|
};
|
|
|
|
var embySource = new EmbyMediaSource
|
|
{
|
|
ServerName = "Emby Server",
|
|
OperatingSystem = "Linux",
|
|
Connections = [new EmbyConnection { Address = "http://emby.local" }],
|
|
PathReplacements = [],
|
|
Libraries =
|
|
[
|
|
new EmbyLibrary
|
|
{
|
|
Name = "Emby Shows",
|
|
MediaKind = LibraryMediaKind.Shows,
|
|
ItemId = "emby-shows",
|
|
ShouldSyncItems = true,
|
|
PathInfos = [],
|
|
Paths = [MakePath("/emby/shows", 1)]
|
|
}
|
|
]
|
|
};
|
|
|
|
var jellyfinSource = new JellyfinMediaSource
|
|
{
|
|
ServerName = "Jellyfin Server",
|
|
OperatingSystem = "Linux",
|
|
Connections = [new JellyfinConnection { Address = "http://jellyfin.local" }],
|
|
PathReplacements = [],
|
|
Libraries =
|
|
[
|
|
new JellyfinLibrary
|
|
{
|
|
Name = "Disabled Jellyfin",
|
|
MediaKind = LibraryMediaKind.Movies,
|
|
ItemId = "jellyfin-movies",
|
|
ShouldSyncItems = false,
|
|
PathInfos = [],
|
|
Paths = [MakePath("/jellyfin/movies", 1)]
|
|
}
|
|
]
|
|
};
|
|
|
|
context.MediaSources.AddRange(localSource, plexSource, embySource, jellyfinSource);
|
|
await context.SaveChangesAsync();
|
|
context.ChangeTracker.Clear();
|
|
}
|
|
|
|
private static LibraryPath MakePath(string path, int mediaItemCount) =>
|
|
new()
|
|
{
|
|
Path = path,
|
|
LibraryFolders = [],
|
|
MediaItems = Enumerable.Range(0, mediaItemCount)
|
|
.Select(_ => new Movie
|
|
{
|
|
MovieMetadata = [],
|
|
MediaVersions = [],
|
|
Collections = [],
|
|
CollectionItems = [],
|
|
TraktListItems = []
|
|
})
|
|
.Cast<MediaItem>()
|
|
.ToList()
|
|
};
|
|
}
|