Merge 8abcaee846
into 086fbd49cf
commit
47d0d0961a
@ -0,0 +1,320 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Models.HomeSectionDto;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Jellyfin.Api.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Home Section controller.
|
||||
/// </summary>
|
||||
[Route("Users/{userId}/HomeSections")]
|
||||
[Authorize]
|
||||
public class HomeSectionController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly IHomeSectionManager _homeSectionManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IDtoService _dtoService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HomeSectionController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="homeSectionManager">Instance of the <see cref="IHomeSectionManager"/> interface.</param>
|
||||
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
|
||||
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
|
||||
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
|
||||
public HomeSectionController(
|
||||
IHomeSectionManager homeSectionManager,
|
||||
IUserManager userManager,
|
||||
ILibraryManager libraryManager,
|
||||
IDtoService dtoService)
|
||||
{
|
||||
_homeSectionManager = homeSectionManager;
|
||||
_userManager = userManager;
|
||||
_libraryManager = libraryManager;
|
||||
_dtoService = dtoService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all home sections.
|
||||
/// </summary>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <response code="200">Home sections retrieved.</response>
|
||||
/// <returns>An <see cref="IEnumerable{EnrichedHomeSectionDto}"/> containing the home sections.</returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<List<EnrichedHomeSectionDto>> GetHomeSections([FromRoute, Required] Guid userId)
|
||||
{
|
||||
var sections = _homeSectionManager.GetHomeSections(userId);
|
||||
var result = new List<EnrichedHomeSectionDto>();
|
||||
var user = _userManager.GetUserById(userId);
|
||||
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound("User not found");
|
||||
}
|
||||
|
||||
foreach (var section in sections)
|
||||
{
|
||||
var enrichedSection = new EnrichedHomeSectionDto
|
||||
{
|
||||
Id = null, // We'll need to retrieve the ID from the database
|
||||
SectionOptions = section,
|
||||
Items = GetItemsForSection(userId, section)
|
||||
};
|
||||
|
||||
result.Add(enrichedSection);
|
||||
}
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get home section.
|
||||
/// </summary>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="sectionId">Section id.</param>
|
||||
/// <response code="200">Home section retrieved.</response>
|
||||
/// <response code="404">Home section not found.</response>
|
||||
/// <returns>An <see cref="EnrichedHomeSectionDto"/> containing the home section.</returns>
|
||||
[HttpGet("{sectionId}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult<EnrichedHomeSectionDto> GetHomeSection([FromRoute, Required] Guid userId, [FromRoute, Required] Guid sectionId)
|
||||
{
|
||||
var section = _homeSectionManager.GetHomeSection(userId, sectionId);
|
||||
if (section == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return NotFound("User not found");
|
||||
}
|
||||
|
||||
var result = new EnrichedHomeSectionDto
|
||||
{
|
||||
Id = sectionId,
|
||||
SectionOptions = section,
|
||||
Items = GetItemsForSection(userId, section)
|
||||
};
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new home section.
|
||||
/// </summary>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="dto">The home section dto.</param>
|
||||
/// <response code="201">Home section created.</response>
|
||||
/// <returns>An <see cref="HomeSectionDto"/> containing the new home section.</returns>
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status201Created)]
|
||||
public ActionResult<HomeSectionDto> CreateHomeSection([FromRoute, Required] Guid userId, [FromBody, Required] HomeSectionDto dto)
|
||||
{
|
||||
var sectionId = _homeSectionManager.CreateHomeSection(userId, dto.SectionOptions);
|
||||
_homeSectionManager.SaveChanges();
|
||||
|
||||
dto.Id = sectionId;
|
||||
return CreatedAtAction(nameof(GetHomeSection), new { userId, sectionId }, dto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update a home section.
|
||||
/// </summary>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="sectionId">Section id.</param>
|
||||
/// <param name="dto">The home section dto.</param>
|
||||
/// <response code="204">Home section updated.</response>
|
||||
/// <response code="404">Home section not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpPut("{sectionId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult UpdateHomeSection(
|
||||
[FromRoute, Required] Guid userId,
|
||||
[FromRoute, Required] Guid sectionId,
|
||||
[FromBody, Required] HomeSectionDto dto)
|
||||
{
|
||||
var success = _homeSectionManager.UpdateHomeSection(userId, sectionId, dto.SectionOptions);
|
||||
if (!success)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_homeSectionManager.SaveChanges();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a home section.
|
||||
/// </summary>
|
||||
/// <param name="userId">User id.</param>
|
||||
/// <param name="sectionId">Section id.</param>
|
||||
/// <response code="204">Home section deleted.</response>
|
||||
/// <response code="404">Home section not found.</response>
|
||||
/// <returns>A <see cref="NoContentResult"/>.</returns>
|
||||
[HttpDelete("{sectionId}")]
|
||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public ActionResult DeleteHomeSection([FromRoute, Required] Guid userId, [FromRoute, Required] Guid sectionId)
|
||||
{
|
||||
var success = _homeSectionManager.DeleteHomeSection(userId, sectionId);
|
||||
if (!success)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
_homeSectionManager.SaveChanges();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItemDto> GetItemsForSection(Guid userId, HomeSectionOptions options)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return Array.Empty<BaseItemDto>();
|
||||
}
|
||||
|
||||
switch (options.SectionType)
|
||||
{
|
||||
case HomeSectionType.None:
|
||||
return Array.Empty<BaseItemDto>();
|
||||
case HomeSectionType.SmallLibraryTiles:
|
||||
return GetLibraryTilesHomeSectionItems(userId, true);
|
||||
case HomeSectionType.LibraryButtons:
|
||||
return GetLibraryTilesHomeSectionItems(userId, false);
|
||||
// TODO: Implement GetActiveRecordingsHomeSectionItems
|
||||
case HomeSectionType.ActiveRecordings:
|
||||
return Array.Empty<BaseItemDto>();
|
||||
// TODO: Implement GetResumeItemsHomeSectionItems
|
||||
case HomeSectionType.Resume:
|
||||
return Array.Empty<BaseItemDto>();
|
||||
// TODO: Implement GetResumeAudioHomeSectionItems
|
||||
case HomeSectionType.ResumeAudio:
|
||||
return Array.Empty<BaseItemDto>();
|
||||
case HomeSectionType.LatestMedia:
|
||||
return GetLatestMediaHomeSectionItems(userId, options.MaxItems);
|
||||
// TODO: Implement GetNextUpHomeSectionItems
|
||||
case HomeSectionType.NextUp:
|
||||
return Array.Empty<BaseItemDto>();
|
||||
// TODO: Implement GetLiveTvHomeSectionItems
|
||||
case HomeSectionType.LiveTv:
|
||||
return Array.Empty<BaseItemDto>();
|
||||
// TODO: Implement ResumeBookHomeSectionItems
|
||||
case HomeSectionType.ResumeBook:
|
||||
return Array.Empty<BaseItemDto>();
|
||||
// Major TODO: Implement GetPinnedCollectionHomeSectionItems and add HomeSectionType.PinnedCollection
|
||||
// See example at https://github.com/johnpc/jellyfin-plugin-home-sections/blob/main/Jellyfin.Plugin.HomeSections/Api/HomeSectionsController.cs
|
||||
// Question: what should I do in the case of an unexpected HomeSectionType? Throw an exception?
|
||||
default:
|
||||
return Array.Empty<BaseItemDto>();
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItemDto> GetLatestMediaHomeSectionItems(Guid userId, int maxItems)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return Array.Empty<BaseItemDto>();
|
||||
}
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
Recursive = true,
|
||||
Limit = maxItems,
|
||||
IsVirtualItem = false,
|
||||
OrderBy = new[] { (ItemSortBy.DateCreated, SortOrder.Descending) }
|
||||
};
|
||||
|
||||
var items = _libraryManager.GetItemsResult(query);
|
||||
|
||||
return items.Items
|
||||
.Where(i => i != null && (i is Movie || i is Series || i is Episode))
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _dtoService.GetBaseItemDto(i, new DtoOptions(), user);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the error but don't crash
|
||||
System.Diagnostics.Debug.WriteLine($"Error converting item {i.Id} to DTO: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Where(dto => dto != null)
|
||||
.Cast<BaseItemDto>();
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItemDto> GetLibraryTilesHomeSectionItems(Guid userId, bool smallTiles = false)
|
||||
{
|
||||
var user = _userManager.GetUserById(userId);
|
||||
if (user == null)
|
||||
{
|
||||
return Array.Empty<BaseItemDto>();
|
||||
}
|
||||
|
||||
// Get the user's view items (libraries)
|
||||
var folders = _libraryManager.GetUserRootFolder()
|
||||
.GetChildren(user, true)
|
||||
.Where(i => i.IsFolder && !i.IsHidden)
|
||||
.OrderBy(i => i.SortName)
|
||||
.ToList();
|
||||
|
||||
// Convert to DTOs with appropriate options
|
||||
var options = new DtoOptions
|
||||
{
|
||||
// For small tiles, we might want to limit the fields returned
|
||||
// to make the response smaller
|
||||
Fields = smallTiles
|
||||
? new[] { ItemFields.PrimaryImageAspectRatio, ItemFields.DisplayPreferencesId }
|
||||
: new[]
|
||||
{
|
||||
ItemFields.PrimaryImageAspectRatio,
|
||||
ItemFields.DisplayPreferencesId,
|
||||
ItemFields.Overview,
|
||||
ItemFields.ChildCount
|
||||
}
|
||||
};
|
||||
|
||||
return folders
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _dtoService.GetBaseItemDto(i, options, user);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.Where(dto => dto != null)
|
||||
.Cast<BaseItemDto>();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace Jellyfin.Api.Models.HomeSectionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Home section dto with items.
|
||||
/// </summary>
|
||||
public class EnrichedHomeSectionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the section options.
|
||||
/// </summary>
|
||||
public HomeSectionOptions SectionOptions { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the items.
|
||||
/// </summary>
|
||||
public IEnumerable<BaseItemDto> Items { get; set; } = Array.Empty<BaseItemDto>();
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
||||
namespace Jellyfin.Api.Models.HomeSectionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Home section DTO.
|
||||
/// </summary>
|
||||
public class HomeSectionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the id.
|
||||
/// </summary>
|
||||
public Guid? Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the section options.
|
||||
/// </summary>
|
||||
[Required]
|
||||
public HomeSectionOptions SectionOptions { get; set; } = new HomeSectionOptions();
|
||||
}
|
||||
}
|
@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Users
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages the storage and retrieval of home sections through Entity Framework.
|
||||
/// </summary>
|
||||
public sealed class HomeSectionManager : IHomeSectionManager, IAsyncDisposable
|
||||
{
|
||||
private readonly JellyfinDbContext _dbContext;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HomeSectionManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dbContextFactory">The database context factory.</param>
|
||||
public HomeSectionManager(IDbContextFactory<JellyfinDbContext> dbContextFactory)
|
||||
{
|
||||
_dbContext = dbContextFactory.CreateDbContext();
|
||||
// QUESTION FOR MAINTAINERS: How do I handle the db migration?
|
||||
// I'm sure you don't want the table to be created lazily like this.
|
||||
EnsureTableExists();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the UserHomeSections table exists in the database.
|
||||
/// </summary>
|
||||
private void EnsureTableExists()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Check if table exists by attempting to query it
|
||||
_dbContext.Database.ExecuteSqlRaw("SELECT 1 FROM UserHomeSections LIMIT 1");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Table doesn't exist, create it
|
||||
_dbContext.Database.ExecuteSqlRaw(@"
|
||||
CREATE TABLE IF NOT EXISTS UserHomeSections (
|
||||
Id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
UserId TEXT NOT NULL,
|
||||
SectionId TEXT NOT NULL,
|
||||
Name TEXT NOT NULL,
|
||||
SectionType INTEGER NOT NULL,
|
||||
Priority INTEGER NOT NULL,
|
||||
MaxItems INTEGER NOT NULL,
|
||||
SortOrder INTEGER NOT NULL,
|
||||
SortBy INTEGER NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS IX_UserHomeSections_UserId_SectionId ON UserHomeSections(UserId, SectionId);
|
||||
");
|
||||
|
||||
// Add the migration record to __EFMigrationsHistory
|
||||
_dbContext.Database.ExecuteSqlRaw(@"
|
||||
CREATE TABLE IF NOT EXISTS __EFMigrationsHistory (
|
||||
MigrationId TEXT NOT NULL CONSTRAINT PK___EFMigrationsHistory PRIMARY KEY,
|
||||
ProductVersion TEXT NOT NULL
|
||||
);
|
||||
INSERT OR IGNORE INTO __EFMigrationsHistory (MigrationId, ProductVersion)
|
||||
VALUES ('20250331000000_AddUserHomeSections', '3.1.0');
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IList<HomeSectionOptions> GetHomeSections(Guid userId)
|
||||
{
|
||||
return _dbContext.UserHomeSections
|
||||
.Where(section => section.UserId.Equals(userId))
|
||||
.OrderBy(section => section.Priority)
|
||||
.Select(section => new HomeSectionOptions
|
||||
{
|
||||
Name = section.Name,
|
||||
SectionType = section.SectionType,
|
||||
Priority = section.Priority,
|
||||
MaxItems = section.MaxItems,
|
||||
SortOrder = section.SortOrder,
|
||||
SortBy = (Jellyfin.Database.Implementations.Enums.SortOrder)section.SortBy
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public HomeSectionOptions? GetHomeSection(Guid userId, Guid sectionId)
|
||||
{
|
||||
var section = _dbContext.UserHomeSections
|
||||
.FirstOrDefault(section => section.UserId.Equals(userId) && section.SectionId.Equals(sectionId));
|
||||
|
||||
if (section == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new HomeSectionOptions
|
||||
{
|
||||
Name = section.Name,
|
||||
SectionType = section.SectionType,
|
||||
Priority = section.Priority,
|
||||
MaxItems = section.MaxItems,
|
||||
SortOrder = section.SortOrder,
|
||||
SortBy = (Jellyfin.Database.Implementations.Enums.SortOrder)section.SortBy
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid CreateHomeSection(Guid userId, HomeSectionOptions options)
|
||||
{
|
||||
var sectionId = Guid.NewGuid();
|
||||
var section = new UserHomeSection
|
||||
{
|
||||
UserId = userId,
|
||||
SectionId = sectionId,
|
||||
Name = options.Name,
|
||||
SectionType = options.SectionType,
|
||||
Priority = options.Priority,
|
||||
MaxItems = options.MaxItems,
|
||||
SortOrder = options.SortOrder,
|
||||
SortBy = (int)options.SortBy
|
||||
};
|
||||
|
||||
_dbContext.UserHomeSections.Add(section);
|
||||
return sectionId;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool UpdateHomeSection(Guid userId, Guid sectionId, HomeSectionOptions options)
|
||||
{
|
||||
var section = _dbContext.UserHomeSections
|
||||
.FirstOrDefault(section => section.UserId.Equals(userId) && section.SectionId.Equals(sectionId));
|
||||
|
||||
if (section == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
section.Name = options.Name;
|
||||
section.SectionType = options.SectionType;
|
||||
section.Priority = options.Priority;
|
||||
section.MaxItems = options.MaxItems;
|
||||
section.SortOrder = options.SortOrder;
|
||||
section.SortBy = (int)options.SortBy;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool DeleteHomeSection(Guid userId, Guid sectionId)
|
||||
{
|
||||
var section = _dbContext.UserHomeSections
|
||||
.FirstOrDefault(section => section.UserId.Equals(userId) && section.SectionId.Equals(sectionId));
|
||||
|
||||
if (section == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_dbContext.UserHomeSections.Remove(section);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveChanges()
|
||||
{
|
||||
_dbContext.SaveChanges();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _dbContext.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
||||
namespace MediaBrowser.Controller
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for managing home sections.
|
||||
/// </summary>
|
||||
public interface IHomeSectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all home sections for a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <returns>A list of home section options.</returns>
|
||||
IList<HomeSectionOptions> GetHomeSections(Guid userId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a specific home section for a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="sectionId">The section id.</param>
|
||||
/// <returns>The home section options, or null if not found.</returns>
|
||||
HomeSectionOptions? GetHomeSection(Guid userId, Guid sectionId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new home section for a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="options">The home section options.</param>
|
||||
/// <returns>The id of the newly created section.</returns>
|
||||
Guid CreateHomeSection(Guid userId, HomeSectionOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a home section for a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="sectionId">The section id.</param>
|
||||
/// <param name="options">The updated home section options.</param>
|
||||
/// <returns>True if the section was updated, false if it was not found.</returns>
|
||||
bool UpdateHomeSection(Guid userId, Guid sectionId, HomeSectionOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a home section for a user.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user id.</param>
|
||||
/// <param name="sectionId">The section id.</param>
|
||||
/// <returns>True if the section was deleted, false if it was not found.</returns>
|
||||
bool DeleteHomeSection(Guid userId, Guid sectionId);
|
||||
|
||||
/// <summary>
|
||||
/// Saves changes to the database.
|
||||
/// </summary>
|
||||
void SaveChanges();
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.ComponentModel;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
|
||||
namespace MediaBrowser.Model.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Options for a specific home section.
|
||||
/// </summary>
|
||||
public class HomeSectionOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HomeSectionOptions"/> class.
|
||||
/// </summary>
|
||||
public HomeSectionOptions()
|
||||
{
|
||||
Name = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the section.
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the section.
|
||||
/// </summary>
|
||||
public HomeSectionType SectionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the priority/order of this section (lower numbers appear first).
|
||||
/// </summary>
|
||||
[DefaultValue(0)]
|
||||
public int Priority { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of items to display in the section.
|
||||
/// </summary>
|
||||
[DefaultValue(10)]
|
||||
public int MaxItems { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sort order for items in this section.
|
||||
/// </summary>
|
||||
[DefaultValue(SortOrder.Ascending)]
|
||||
public SortOrder SortOrder { get; set; } = SortOrder.Ascending;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how items should be sorted in this section.
|
||||
/// </summary>
|
||||
[DefaultValue(SortOrder.Ascending)]
|
||||
public SortOrder SortBy { get; set; } = SortOrder.Ascending;
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
|
||||
namespace Jellyfin.Database.Implementations.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// An entity representing a user's home section.
|
||||
/// </summary>
|
||||
public class UserHomeSection
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the Id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Identity. Required.
|
||||
/// </remarks>
|
||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||
public int Id { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user Id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Required.
|
||||
/// </remarks>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the section Id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Required.
|
||||
/// </remarks>
|
||||
public Guid SectionId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the section.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Required. Max Length = 64.
|
||||
/// </remarks>
|
||||
[MaxLength(64)]
|
||||
[StringLength(64)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the section.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Required.
|
||||
/// </remarks>
|
||||
public HomeSectionType SectionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the priority/order of this section.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Required.
|
||||
/// </remarks>
|
||||
public int Priority { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of items to display in the section.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Required.
|
||||
/// </remarks>
|
||||
public int MaxItems { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sort order for items in this section.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Required.
|
||||
/// </remarks>
|
||||
public SortOrder SortOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets how items should be sorted in this section.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Required.
|
||||
/// </remarks>
|
||||
public int SortBy { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Jellyfin.Database.Implementations.ModelConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Configuration for the UserHomeSection entity.
|
||||
/// </summary>
|
||||
public class UserHomeSectionConfiguration : IEntityTypeConfiguration<UserHomeSection>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Configure(EntityTypeBuilder<UserHomeSection> builder)
|
||||
{
|
||||
builder.ToTable("UserHomeSections");
|
||||
|
||||
builder.HasKey(e => e.Id);
|
||||
|
||||
builder.Property(e => e.UserId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.SectionId)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.Name)
|
||||
.IsRequired()
|
||||
.HasMaxLength(64);
|
||||
|
||||
builder.Property(e => e.SectionType)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.Priority)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.MaxItems)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.SortOrder)
|
||||
.IsRequired();
|
||||
|
||||
builder.Property(e => e.SortBy)
|
||||
.IsRequired();
|
||||
|
||||
// Create a unique index on UserId + SectionId
|
||||
builder.HasIndex(e => new { e.UserId, e.SectionId })
|
||||
.IsUnique();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Jellyfin.Database.Providers.Sqlite.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Migration to add UserHomeSections table.
|
||||
/// </summary>
|
||||
public partial class AddUserHomeSections : Migration
|
||||
{
|
||||
/// <summary>
|
||||
/// Builds the operations that will migrate the database 'up'.
|
||||
/// </summary>
|
||||
/// <param name="migrationBuilder">The migration builder.</param>
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserHomeSections",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
UserId = table.Column<string>(nullable: false),
|
||||
SectionId = table.Column<string>(nullable: false),
|
||||
Name = table.Column<string>(nullable: false),
|
||||
SectionType = table.Column<int>(nullable: false),
|
||||
Priority = table.Column<int>(nullable: false),
|
||||
MaxItems = table.Column<int>(nullable: false),
|
||||
SortOrder = table.Column<int>(nullable: false),
|
||||
SortBy = table.Column<int>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserHomeSections", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserHomeSections_UserId_SectionId",
|
||||
table: "UserHomeSections",
|
||||
columns: new[] { "UserId", "SectionId" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the operations that will migrate the database 'down'.
|
||||
/// </summary>
|
||||
/// <param name="migrationBuilder">The migration builder.</param>
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserHomeSections");
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,264 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Api.Controllers;
|
||||
using Jellyfin.Api.Models.HomeSectionDto;
|
||||
using Jellyfin.Api.Results;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Api.Tests.Controllers
|
||||
{
|
||||
public class HomeSectionControllerTests
|
||||
{
|
||||
private readonly Mock<IHomeSectionManager> _mockHomeSectionManager;
|
||||
private readonly Mock<IUserManager> _mockUserManager;
|
||||
private readonly Mock<ILibraryManager> _mockLibraryManager;
|
||||
private readonly Mock<IDtoService> _mockDtoService;
|
||||
private readonly HomeSectionController _controller;
|
||||
private readonly Guid _userId = Guid.NewGuid();
|
||||
|
||||
public HomeSectionControllerTests()
|
||||
{
|
||||
_mockHomeSectionManager = new Mock<IHomeSectionManager>();
|
||||
_mockUserManager = new Mock<IUserManager>();
|
||||
_mockLibraryManager = new Mock<ILibraryManager>();
|
||||
_mockDtoService = new Mock<IDtoService>();
|
||||
|
||||
_controller = new HomeSectionController(
|
||||
_mockHomeSectionManager.Object,
|
||||
_mockUserManager.Object,
|
||||
_mockLibraryManager.Object,
|
||||
_mockDtoService.Object);
|
||||
|
||||
// Setup user manager to return a non-null user for the test user ID
|
||||
var mockUser = new Mock<User>();
|
||||
_mockUserManager.Setup(m => m.GetUserById(_userId))
|
||||
.Returns(mockUser.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHomeSections_ReturnsOkResult_WithListOfSections()
|
||||
{
|
||||
// Arrange
|
||||
var sections = new List<HomeSectionOptions>
|
||||
{
|
||||
new HomeSectionOptions
|
||||
{
|
||||
Name = "Test Section 1",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 1,
|
||||
MaxItems = 10,
|
||||
SortOrder = SortOrder.Descending,
|
||||
SortBy = SortOrder.Ascending
|
||||
},
|
||||
new HomeSectionOptions
|
||||
{
|
||||
Name = "Test Section 2",
|
||||
SectionType = HomeSectionType.NextUp,
|
||||
Priority = 2,
|
||||
MaxItems = 5,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = SortOrder.Descending
|
||||
}
|
||||
};
|
||||
|
||||
_mockHomeSectionManager.Setup(m => m.GetHomeSections(_userId))
|
||||
.Returns(sections);
|
||||
|
||||
// Act
|
||||
var result = _controller.GetHomeSections(_userId);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<OkObjectResult>(result.Result);
|
||||
var returnValue = Assert.IsType<List<EnrichedHomeSectionDto>>(okResult.Value);
|
||||
Assert.Equal(2, returnValue.Count);
|
||||
Assert.Equal("Test Section 1", returnValue[0].SectionOptions.Name);
|
||||
Assert.Equal("Test Section 2", returnValue[1].SectionOptions.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHomeSection_WithValidId_ReturnsOkResult()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
var section = new HomeSectionOptions
|
||||
{
|
||||
Name = "Test Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 1,
|
||||
MaxItems = 10,
|
||||
SortOrder = SortOrder.Descending,
|
||||
SortBy = SortOrder.Ascending
|
||||
};
|
||||
|
||||
_mockHomeSectionManager.Setup(m => m.GetHomeSection(_userId, sectionId))
|
||||
.Returns(section);
|
||||
|
||||
// Act
|
||||
var result = _controller.GetHomeSection(_userId, sectionId);
|
||||
|
||||
// Assert
|
||||
var okResult = Assert.IsAssignableFrom<OkObjectResult>(result.Result);
|
||||
var returnValue = Assert.IsType<EnrichedHomeSectionDto>(okResult.Value);
|
||||
Assert.Equal("Test Section", returnValue.SectionOptions.Name);
|
||||
Assert.Equal(sectionId, returnValue.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHomeSection_WithInvalidId_ReturnsNotFound()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
_mockHomeSectionManager.Setup(m => m.GetHomeSection(_userId, sectionId))
|
||||
.Returns((HomeSectionOptions?)null);
|
||||
|
||||
// Act
|
||||
var result = _controller.GetHomeSection(_userId, sectionId);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NotFoundResult>(result.Result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateHomeSection_ReturnsCreatedAtAction()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
var dto = new HomeSectionDto
|
||||
{
|
||||
SectionOptions = new HomeSectionOptions
|
||||
{
|
||||
Name = "New Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 3,
|
||||
MaxItems = 15,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = SortOrder.Ascending
|
||||
}
|
||||
};
|
||||
|
||||
_mockHomeSectionManager.Setup(m => m.CreateHomeSection(_userId, dto.SectionOptions))
|
||||
.Returns(sectionId);
|
||||
|
||||
// Act
|
||||
var result = _controller.CreateHomeSection(_userId, dto);
|
||||
|
||||
// Assert
|
||||
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(result.Result);
|
||||
var returnValue = Assert.IsType<HomeSectionDto>(createdAtActionResult.Value);
|
||||
Assert.Equal("New Section", returnValue.SectionOptions.Name);
|
||||
Assert.Equal(sectionId, returnValue.Id);
|
||||
Assert.Equal("GetHomeSection", createdAtActionResult.ActionName);
|
||||
|
||||
// Check if RouteValues is not null before accessing its elements
|
||||
Assert.NotNull(createdAtActionResult.RouteValues);
|
||||
if (createdAtActionResult.RouteValues != null)
|
||||
{
|
||||
Assert.Equal(_userId, createdAtActionResult.RouteValues["userId"]);
|
||||
Assert.Equal(sectionId, createdAtActionResult.RouteValues["sectionId"]);
|
||||
}
|
||||
|
||||
_mockHomeSectionManager.Verify(m => m.SaveChanges(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateHomeSection_WithValidId_ReturnsNoContent()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
var dto = new HomeSectionDto
|
||||
{
|
||||
SectionOptions = new HomeSectionOptions
|
||||
{
|
||||
Name = "Updated Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 3,
|
||||
MaxItems = 15,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = SortOrder.Ascending
|
||||
}
|
||||
};
|
||||
|
||||
_mockHomeSectionManager.Setup(m => m.UpdateHomeSection(_userId, sectionId, dto.SectionOptions))
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = _controller.UpdateHomeSection(_userId, sectionId, dto);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
_mockHomeSectionManager.Verify(m => m.SaveChanges(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateHomeSection_WithInvalidId_ReturnsNotFound()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
var dto = new HomeSectionDto
|
||||
{
|
||||
SectionOptions = new HomeSectionOptions
|
||||
{
|
||||
Name = "Updated Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 3,
|
||||
MaxItems = 15,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = SortOrder.Ascending
|
||||
}
|
||||
};
|
||||
|
||||
_mockHomeSectionManager.Setup(m => m.UpdateHomeSection(_userId, sectionId, dto.SectionOptions))
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = _controller.UpdateHomeSection(_userId, sectionId, dto);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NotFoundResult>(result);
|
||||
_mockHomeSectionManager.Verify(m => m.SaveChanges(), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteHomeSection_WithValidId_ReturnsNoContent()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
_mockHomeSectionManager.Setup(m => m.DeleteHomeSection(_userId, sectionId))
|
||||
.Returns(true);
|
||||
|
||||
// Act
|
||||
var result = _controller.DeleteHomeSection(_userId, sectionId);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NoContentResult>(result);
|
||||
_mockHomeSectionManager.Verify(m => m.SaveChanges(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteHomeSection_WithInvalidId_ReturnsNotFound()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
_mockHomeSectionManager.Setup(m => m.DeleteHomeSection(_userId, sectionId))
|
||||
.Returns(false);
|
||||
|
||||
// Act
|
||||
var result = _controller.DeleteHomeSection(_userId, sectionId);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<NotFoundResult>(result);
|
||||
_mockHomeSectionManager.Verify(m => m.SaveChanges(), Times.Never);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Models.HomeSectionDto;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Api.Tests.Integration
|
||||
{
|
||||
/// <summary>
|
||||
/// Integration tests for the Home Section API.
|
||||
/// These tests require a running Jellyfin server and should be run in a controlled environment.
|
||||
/// </summary>
|
||||
public sealed class HomeSectionApiTests : IDisposable
|
||||
{
|
||||
private readonly HttpClient _client;
|
||||
private readonly Guid _userId = Guid.Parse("38a9e9be-b2a6-4790-85a3-62a01ca06dec"); // Test user ID
|
||||
private readonly List<Guid> _createdSectionIds = new List<Guid>();
|
||||
|
||||
public HomeSectionApiTests()
|
||||
{
|
||||
// Setup HttpClient with base address pointing to your test server
|
||||
_client = new HttpClient
|
||||
{
|
||||
BaseAddress = new Uri("http://localhost:8096/")
|
||||
};
|
||||
}
|
||||
|
||||
[Fact(Skip = "Integration test - requires running server")]
|
||||
public async Task GetHomeSections_ReturnsSuccessStatusCode()
|
||||
{
|
||||
// Act
|
||||
var response = await _client.GetAsync($"Users/{_userId}/HomeSections");
|
||||
|
||||
// Assert
|
||||
response.EnsureSuccessStatusCode();
|
||||
var sections = await response.Content.ReadFromJsonAsync<List<HomeSectionDto>>();
|
||||
Assert.NotNull(sections);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Integration test - requires running server")]
|
||||
public async Task CreateAndGetHomeSection_ReturnsCreatedSection()
|
||||
{
|
||||
// Arrange
|
||||
var newSection = new HomeSectionDto
|
||||
{
|
||||
SectionOptions = new HomeSectionOptions
|
||||
{
|
||||
Name = "Integration Test Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 100,
|
||||
MaxItems = 8,
|
||||
SortOrder = SortOrder.Descending,
|
||||
SortBy = SortOrder.Ascending
|
||||
}
|
||||
};
|
||||
|
||||
// Act - Create
|
||||
var createResponse = await _client.PostAsJsonAsync($"Users/{_userId}/HomeSections", newSection);
|
||||
|
||||
// Assert - Create
|
||||
createResponse.EnsureSuccessStatusCode();
|
||||
Assert.Equal(HttpStatusCode.Created, createResponse.StatusCode);
|
||||
|
||||
var createdSection = await createResponse.Content.ReadFromJsonAsync<HomeSectionDto>();
|
||||
Assert.NotNull(createdSection);
|
||||
Assert.NotNull(createdSection.Id);
|
||||
_createdSectionIds.Add(createdSection.Id.Value);
|
||||
|
||||
// Act - Get
|
||||
var getResponse = await _client.GetAsync($"Users/{_userId}/HomeSections/{createdSection.Id}");
|
||||
|
||||
// Assert - Get
|
||||
getResponse.EnsureSuccessStatusCode();
|
||||
var retrievedSection = await getResponse.Content.ReadFromJsonAsync<HomeSectionDto>();
|
||||
|
||||
Assert.NotNull(retrievedSection);
|
||||
Assert.Equal(createdSection.Id, retrievedSection.Id);
|
||||
Assert.Equal("Integration Test Section", retrievedSection.SectionOptions.Name);
|
||||
Assert.Equal(HomeSectionType.LatestMedia, retrievedSection.SectionOptions.SectionType);
|
||||
Assert.Equal(100, retrievedSection.SectionOptions.Priority);
|
||||
Assert.Equal(8, retrievedSection.SectionOptions.MaxItems);
|
||||
Assert.Equal(SortOrder.Descending, retrievedSection.SectionOptions.SortOrder);
|
||||
Assert.Equal(SortOrder.Ascending, retrievedSection.SectionOptions.SortBy);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Integration test - requires running server")]
|
||||
public async Task UpdateHomeSection_ReturnsNoContent()
|
||||
{
|
||||
// Arrange - Create a section first
|
||||
var newSection = new HomeSectionDto
|
||||
{
|
||||
SectionOptions = new HomeSectionOptions
|
||||
{
|
||||
Name = "Section To Update",
|
||||
SectionType = HomeSectionType.NextUp,
|
||||
Priority = 50,
|
||||
MaxItems = 5,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = SortOrder.Ascending
|
||||
}
|
||||
};
|
||||
|
||||
var createResponse = await _client.PostAsJsonAsync($"Users/{_userId}/HomeSections", newSection);
|
||||
createResponse.EnsureSuccessStatusCode();
|
||||
var createdSection = await createResponse.Content.ReadFromJsonAsync<HomeSectionDto>();
|
||||
Assert.NotNull(createdSection);
|
||||
Assert.NotNull(createdSection.Id);
|
||||
_createdSectionIds.Add(createdSection.Id.Value);
|
||||
|
||||
// Arrange - Update data
|
||||
var updateSection = new HomeSectionDto
|
||||
{
|
||||
SectionOptions = new HomeSectionOptions
|
||||
{
|
||||
Name = "Updated Section Name",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 25,
|
||||
MaxItems = 12,
|
||||
SortOrder = SortOrder.Descending,
|
||||
SortBy = SortOrder.Descending
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var updateResponse = await _client.PutAsJsonAsync($"Users/{_userId}/HomeSections/{createdSection.Id}", updateSection);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NoContent, updateResponse.StatusCode);
|
||||
|
||||
// Verify the update
|
||||
var getResponse = await _client.GetAsync($"Users/{_userId}/HomeSections/{createdSection.Id}");
|
||||
getResponse.EnsureSuccessStatusCode();
|
||||
var retrievedSection = await getResponse.Content.ReadFromJsonAsync<HomeSectionDto>();
|
||||
|
||||
Assert.NotNull(retrievedSection);
|
||||
Assert.Equal("Updated Section Name", retrievedSection.SectionOptions.Name);
|
||||
Assert.Equal(HomeSectionType.LatestMedia, retrievedSection.SectionOptions.SectionType);
|
||||
Assert.Equal(25, retrievedSection.SectionOptions.Priority);
|
||||
Assert.Equal(12, retrievedSection.SectionOptions.MaxItems);
|
||||
Assert.Equal(SortOrder.Descending, retrievedSection.SectionOptions.SortOrder);
|
||||
Assert.Equal(SortOrder.Descending, retrievedSection.SectionOptions.SortBy);
|
||||
}
|
||||
|
||||
[Fact(Skip = "Integration test - requires running server")]
|
||||
public async Task DeleteHomeSection_ReturnsNoContent()
|
||||
{
|
||||
// Arrange - Create a section first
|
||||
var newSection = new HomeSectionDto
|
||||
{
|
||||
SectionOptions = new HomeSectionOptions
|
||||
{
|
||||
Name = "Section To Delete",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 75,
|
||||
MaxItems = 3,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = SortOrder.Descending
|
||||
}
|
||||
};
|
||||
|
||||
var createResponse = await _client.PostAsJsonAsync($"Users/{_userId}/HomeSections", newSection);
|
||||
createResponse.EnsureSuccessStatusCode();
|
||||
var createdSection = await createResponse.Content.ReadFromJsonAsync<HomeSectionDto>();
|
||||
Assert.NotNull(createdSection);
|
||||
Assert.NotNull(createdSection.Id);
|
||||
|
||||
// Act
|
||||
var deleteResponse = await _client.DeleteAsync($"Users/{_userId}/HomeSections/{createdSection.Id}");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(HttpStatusCode.NoContent, deleteResponse.StatusCode);
|
||||
|
||||
// Verify it's gone
|
||||
var getResponse = await _client.GetAsync($"Users/{_userId}/HomeSections/{createdSection.Id}");
|
||||
Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Clean up any sections created during tests
|
||||
foreach (var sectionId in _createdSectionIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
_client.DeleteAsync($"Users/{_userId}/HomeSections/{sectionId}").Wait();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
}
|
||||
|
||||
_client.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Jellyfin.Api.Models.HomeSectionDto;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Api.Tests.Models.HomeSectionDto
|
||||
{
|
||||
public class HomeSectionDtoTests
|
||||
{
|
||||
[Fact]
|
||||
public void HomeSectionDto_DefaultConstructor_InitializesProperties()
|
||||
{
|
||||
// Act
|
||||
var dto = new Jellyfin.Api.Models.HomeSectionDto.HomeSectionDto();
|
||||
|
||||
// Assert
|
||||
Assert.Null(dto.Id);
|
||||
Assert.NotNull(dto.SectionOptions);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HomeSectionDto_WithValues_StoresCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var id = Guid.NewGuid();
|
||||
var options = new HomeSectionOptions
|
||||
{
|
||||
Name = "Test Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 1,
|
||||
MaxItems = 10,
|
||||
SortOrder = SortOrder.Descending,
|
||||
SortBy = SortOrder.Ascending
|
||||
};
|
||||
|
||||
// Act
|
||||
var dto = new Jellyfin.Api.Models.HomeSectionDto.HomeSectionDto
|
||||
{
|
||||
Id = id,
|
||||
SectionOptions = options
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal(id, dto.Id);
|
||||
Assert.Same(options, dto.SectionOptions);
|
||||
Assert.Equal("Test Section", dto.SectionOptions.Name);
|
||||
Assert.Equal(HomeSectionType.LatestMedia, dto.SectionOptions.SectionType);
|
||||
Assert.Equal(1, dto.SectionOptions.Priority);
|
||||
Assert.Equal(10, dto.SectionOptions.MaxItems);
|
||||
Assert.Equal(SortOrder.Descending, dto.SectionOptions.SortOrder);
|
||||
Assert.Equal(SortOrder.Ascending, dto.SectionOptions.SortBy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HomeSectionDto_SectionOptionsRequired_ValidationFails()
|
||||
{
|
||||
// Arrange
|
||||
var dto = new Jellyfin.Api.Models.HomeSectionDto.HomeSectionDto
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
SectionOptions = new HomeSectionOptions() // Use empty options instead of null
|
||||
};
|
||||
|
||||
// Set SectionOptions to null for validation test
|
||||
// This is a workaround for non-nullable reference types
|
||||
var validationContext = new ValidationContext(dto);
|
||||
var validationResults = new List<ValidationResult>();
|
||||
|
||||
// Use reflection to set the SectionOptions to null for validation testing
|
||||
var propertyInfo = dto.GetType().GetProperty("SectionOptions");
|
||||
propertyInfo?.SetValue(dto, null);
|
||||
|
||||
// Act
|
||||
var isValid = Validator.TryValidateObject(dto, validationContext, validationResults, true);
|
||||
|
||||
// Assert
|
||||
Assert.False(isValid);
|
||||
Assert.Single(validationResults);
|
||||
Assert.Contains("SectionOptions", validationResults[0].MemberNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HomeSectionOptions_DefaultConstructor_InitializesProperties()
|
||||
{
|
||||
// Act
|
||||
var options = new HomeSectionOptions();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(string.Empty, options.Name);
|
||||
Assert.Equal(HomeSectionType.None, options.SectionType);
|
||||
Assert.Equal(0, options.Priority);
|
||||
Assert.Equal(10, options.MaxItems);
|
||||
Assert.Equal(SortOrder.Ascending, options.SortOrder);
|
||||
Assert.Equal(SortOrder.Ascending, options.SortBy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HomeSectionOptions_WithValues_StoresCorrectly()
|
||||
{
|
||||
// Act
|
||||
var options = new HomeSectionOptions
|
||||
{
|
||||
Name = "Custom Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 5,
|
||||
MaxItems = 20,
|
||||
SortOrder = SortOrder.Descending,
|
||||
SortBy = SortOrder.Descending
|
||||
};
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Custom Section", options.Name);
|
||||
Assert.Equal(HomeSectionType.LatestMedia, options.SectionType);
|
||||
Assert.Equal(5, options.Priority);
|
||||
Assert.Equal(20, options.MaxItems);
|
||||
Assert.Equal(SortOrder.Descending, options.SortOrder);
|
||||
Assert.Equal(SortOrder.Descending, options.SortBy);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,320 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Server.Implementations.Users;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Implementations.Tests.Users
|
||||
{
|
||||
public sealed class HomeSectionManagerTests : IAsyncDisposable
|
||||
{
|
||||
private readonly Mock<IDbContextFactory<JellyfinDbContext>> _mockDbContextFactory;
|
||||
private readonly Mock<JellyfinDbContext> _mockDbContext;
|
||||
private readonly HomeSectionManager _manager;
|
||||
private readonly Guid _userId = Guid.NewGuid();
|
||||
private readonly List<UserHomeSection> _homeSections;
|
||||
private readonly Mock<DbSet<UserHomeSection>> _mockDbSet;
|
||||
|
||||
public HomeSectionManagerTests()
|
||||
{
|
||||
_homeSections = new List<UserHomeSection>();
|
||||
|
||||
// Setup mock DbSet for UserHomeSections
|
||||
_mockDbSet = CreateMockDbSet(_homeSections);
|
||||
|
||||
// Setup mock DbContext
|
||||
var mockLogger = new Mock<ILogger<JellyfinDbContext>>();
|
||||
var mockProvider = new Mock<IJellyfinDatabaseProvider>();
|
||||
_mockDbContext = new Mock<JellyfinDbContext>(
|
||||
new DbContextOptions<JellyfinDbContext>(),
|
||||
mockLogger.Object,
|
||||
mockProvider.Object);
|
||||
|
||||
// Setup the property to return our mock DbSet
|
||||
_mockDbContext.Setup(c => c.Set<UserHomeSection>()).Returns(_mockDbSet.Object);
|
||||
|
||||
// Setup mock DbContextFactory
|
||||
_mockDbContextFactory = new Mock<IDbContextFactory<JellyfinDbContext>>();
|
||||
_mockDbContextFactory.Setup(f => f.CreateDbContext()).Returns(_mockDbContext.Object);
|
||||
|
||||
_manager = new HomeSectionManager(_mockDbContextFactory.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHomeSections_ReturnsAllSectionsForUser()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId1 = Guid.NewGuid();
|
||||
var sectionId2 = Guid.NewGuid();
|
||||
|
||||
_homeSections.AddRange(new[]
|
||||
{
|
||||
new UserHomeSection
|
||||
{
|
||||
UserId = _userId,
|
||||
SectionId = sectionId1,
|
||||
Name = "Test Section 1",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 1,
|
||||
MaxItems = 10,
|
||||
SortOrder = SortOrder.Descending,
|
||||
SortBy = (int)SortOrder.Ascending
|
||||
},
|
||||
new UserHomeSection
|
||||
{
|
||||
UserId = _userId,
|
||||
SectionId = sectionId2,
|
||||
Name = "Test Section 2",
|
||||
SectionType = HomeSectionType.NextUp,
|
||||
Priority = 2,
|
||||
MaxItems = 5,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = (int)SortOrder.Descending
|
||||
},
|
||||
new UserHomeSection
|
||||
{
|
||||
UserId = Guid.NewGuid(), // Different user
|
||||
SectionId = Guid.NewGuid(),
|
||||
Name = "Other User Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 1,
|
||||
MaxItems = 15,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = (int)SortOrder.Ascending
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = _manager.GetHomeSections(_userId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal("Test Section 1", result[0].Name);
|
||||
Assert.Equal("Test Section 2", result[1].Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHomeSection_WithValidId_ReturnsSection()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
|
||||
_homeSections.Add(new UserHomeSection
|
||||
{
|
||||
UserId = _userId,
|
||||
SectionId = sectionId,
|
||||
Name = "Test Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 1,
|
||||
MaxItems = 10,
|
||||
SortOrder = SortOrder.Descending,
|
||||
SortBy = (int)SortOrder.Ascending
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = _manager.GetHomeSection(_userId, sectionId);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("Test Section", result.Name);
|
||||
Assert.Equal(HomeSectionType.LatestMedia, result.SectionType);
|
||||
Assert.Equal(1, result.Priority);
|
||||
Assert.Equal(10, result.MaxItems);
|
||||
Assert.Equal(SortOrder.Descending, result.SortOrder);
|
||||
Assert.Equal(SortOrder.Ascending, result.SortBy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetHomeSection_WithInvalidId_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var result = _manager.GetHomeSection(_userId, sectionId);
|
||||
|
||||
// Assert
|
||||
Assert.Null(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateHomeSection_AddsNewSectionToDatabase()
|
||||
{
|
||||
// Arrange
|
||||
var options = new HomeSectionOptions
|
||||
{
|
||||
Name = "New Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 3,
|
||||
MaxItems = 15,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = SortOrder.Ascending
|
||||
};
|
||||
|
||||
// Act
|
||||
var sectionId = _manager.CreateHomeSection(_userId, options);
|
||||
|
||||
// Assert
|
||||
_mockDbSet.Verify(
|
||||
m => m.Add(
|
||||
It.Is<UserHomeSection>(s =>
|
||||
s.UserId.Equals(_userId) &&
|
||||
s.SectionId.Equals(sectionId) &&
|
||||
s.Name == "New Section" &&
|
||||
s.SectionType == HomeSectionType.LatestMedia &&
|
||||
s.Priority == 3 &&
|
||||
s.MaxItems == 15 &&
|
||||
s.SortOrder == SortOrder.Ascending &&
|
||||
s.SortBy == (int)SortOrder.Ascending)),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateHomeSection_WithValidId_UpdatesSection()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
|
||||
_homeSections.Add(new UserHomeSection
|
||||
{
|
||||
UserId = _userId,
|
||||
SectionId = sectionId,
|
||||
Name = "Original Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 1,
|
||||
MaxItems = 10,
|
||||
SortOrder = SortOrder.Descending,
|
||||
SortBy = (int)SortOrder.Ascending
|
||||
});
|
||||
|
||||
var options = new HomeSectionOptions
|
||||
{
|
||||
Name = "Updated Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 3,
|
||||
MaxItems = 15,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = SortOrder.Descending
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _manager.UpdateHomeSection(_userId, sectionId, options);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
var section = _homeSections.First(s => s.SectionId.Equals(sectionId));
|
||||
Assert.Equal("Updated Section", section.Name);
|
||||
Assert.Equal(HomeSectionType.LatestMedia, section.SectionType);
|
||||
Assert.Equal(3, section.Priority);
|
||||
Assert.Equal(15, section.MaxItems);
|
||||
Assert.Equal(SortOrder.Ascending, section.SortOrder);
|
||||
Assert.Equal((int)SortOrder.Descending, section.SortBy);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UpdateHomeSection_WithInvalidId_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
|
||||
var options = new HomeSectionOptions
|
||||
{
|
||||
Name = "Updated Section",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 3,
|
||||
MaxItems = 15,
|
||||
SortOrder = SortOrder.Ascending,
|
||||
SortBy = SortOrder.Descending
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = _manager.UpdateHomeSection(_userId, sectionId, options);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteHomeSection_WithValidId_RemovesSection()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
|
||||
var section = new UserHomeSection
|
||||
{
|
||||
UserId = _userId,
|
||||
SectionId = sectionId,
|
||||
Name = "Section to Delete",
|
||||
SectionType = HomeSectionType.LatestMedia,
|
||||
Priority = 1,
|
||||
MaxItems = 10,
|
||||
SortOrder = SortOrder.Descending,
|
||||
SortBy = (int)SortOrder.Ascending
|
||||
};
|
||||
|
||||
_homeSections.Add(section);
|
||||
|
||||
// Act
|
||||
var result = _manager.DeleteHomeSection(_userId, sectionId);
|
||||
|
||||
// Assert
|
||||
Assert.True(result);
|
||||
_mockDbSet.Verify(m => m.Remove(section), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DeleteHomeSection_WithInvalidId_ReturnsFalse()
|
||||
{
|
||||
// Arrange
|
||||
var sectionId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
var result = _manager.DeleteHomeSection(_userId, sectionId);
|
||||
|
||||
// Assert
|
||||
Assert.False(result);
|
||||
_mockDbSet.Verify(m => m.Remove(It.IsAny<UserHomeSection>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveChanges_CallsSaveChangesOnDbContext()
|
||||
{
|
||||
// Act
|
||||
_manager.SaveChanges();
|
||||
|
||||
// Assert
|
||||
_mockDbContext.Verify(c => c.SaveChanges(), Times.Once);
|
||||
}
|
||||
|
||||
private static Mock<DbSet<T>> CreateMockDbSet<T>(List<T> data)
|
||||
where T : class
|
||||
{
|
||||
var queryable = data.AsQueryable();
|
||||
var mockDbSet = new Mock<DbSet<T>>();
|
||||
|
||||
mockDbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
|
||||
mockDbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
|
||||
mockDbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
|
||||
mockDbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
|
||||
|
||||
mockDbSet.Setup(m => m.Add(It.IsAny<T>())).Callback<T>(data.Add);
|
||||
mockDbSet.Setup(m => m.Remove(It.IsAny<T>())).Callback<T>(item => data.Remove(item));
|
||||
|
||||
return mockDbSet;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _manager.DisposeAsync().ConfigureAwait(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue