86 lines
6.7 KiB
C#
86 lines
6.7 KiB
C#
using Jellyfin.Data;
|
|
using System.Collections.Concurrent;
|
|
using MediaBrowser.Model.Session;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using System;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Security.Claims;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Jellyfin.Database.Implementations.Entities;
|
|
using Jellyfin.Database.Implementations.Enums;
|
|
using Jellyfin.Plugin.Enigma2;
|
|
using MediaBrowser.Controller.Net;
|
|
using MediaBrowser.Controller.Session;
|
|
using Microsoft.AspNetCore.Http;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
|
|
public static class PlaybackFeedbackTests
|
|
{
|
|
public static async Task Run(Action<bool,string> check)
|
|
{
|
|
var user=new User("feedback-user","auth","reset");user.SetPermission(PermissionKind.EnableLiveTvAccess,true);
|
|
var auth=DispatchProxy.Create<IAuthorizationContext,FeedbackAuth>();
|
|
((FeedbackAuth)(object)auth).Info=new AuthorizationInfo{User=user,DeviceId="device-a",Client="Jellyfin Web"};
|
|
var sessions=DispatchProxy.Create<ISessionManager,FeedbackSessions>();
|
|
var contexts=new HttpContextAccessor{HttpContext=new DefaultHttpContext()};
|
|
var feedback=new PlaybackFeedback(contexts,auth,sessions);
|
|
var first=await feedback.Begin("channel-a",CancellationToken.None);
|
|
first.Report("checking_stream");
|
|
string Snapshot(Guid id,string device,string reference)=>JsonSerializer.Serialize(feedback.Snapshot(id,device,reference));
|
|
check(Snapshot(user.Id,"device-a","channel-a").Contains("checking_stream"),"startup feedback exposes actual probe phase");
|
|
check(!Snapshot(user.Id,"device-b","channel-a").Contains("checking_stream")&&!Snapshot(Guid.NewGuid(),"device-a","channel-a").Contains("checking_stream"),"startup progress isolates user and device");
|
|
check(!Snapshot(user.Id,"device-a","channel-b").Contains("checking_stream"),"startup progress isolates channels");
|
|
var second=await feedback.Begin("channel-a",CancellationToken.None);first.Report("failed");second.Report("ready");
|
|
check(Snapshot(user.Id,"device-a","channel-a").Contains("ready")&&!Snapshot(user.Id,"device-a","channel-a").Contains("failed"),"late old attempt cannot replace current progress");
|
|
check(!Snapshot(user.Id,"device-a","channel-a").Contains("device-a")&&!Snapshot(user.Id,"device-a","channel-a").Contains(user.Id.ToString()),"progress response excludes user and device identifiers");
|
|
check(((FeedbackSessions)(object)sessions).Messages==0,"web requests never send unsolicited native messages");
|
|
contexts.HttpContext=null;check(await feedback.Begin("recording",CancellationToken.None)==null,"background recording has no guessed message destination");
|
|
contexts.HttpContext=new DefaultHttpContext();user.SetPermission(PermissionKind.EnableLiveTvAccess,false);
|
|
check(await feedback.Begin("channel-a",CancellationToken.None)==null,"feedback requires live TV permission");
|
|
check(PlaybackFeedback.IsAndroidTv("Jellyfin Android TV")&&!PlaybackFeedback.IsAndroidTv("Jellyfin Android")&&!PlaybackFeedback.IsAndroidTv("Jellyfin Web"),"native messages scoped to Android TV clients");
|
|
user.SetPermission(PermissionKind.EnableLiveTvAccess,true);
|
|
var library=DispatchProxy.Create<MediaBrowser.Controller.Library.ILibraryManager,PolicyLibraryProxy>();
|
|
((PolicyLibraryProxy)(object)library).Current=user;((PolicyLibraryProxy)(object)library).Item=new MediaBrowser.Controller.LiveTv.LiveTvChannel{ServiceName="Enigma2",ExternalId="channel-a"};
|
|
var users=DispatchProxy.Create<MediaBrowser.Controller.Library.IUserManager,PolicyUserProxy>();((PolicyUserProxy)(object)users).Current=user;
|
|
var context=new DefaultHttpContext();context.User=new ClaimsPrincipal(new ClaimsIdentity(new[]{new Claim("Jellyfin-UserId",user.Id.ToString())},"test"));
|
|
var controller=new PlaybackFeedbackController(feedback,library,users,auth){ControllerContext=new ControllerContext{HttpContext=context}};
|
|
check(await controller.Get(Guid.NewGuid()) is OkObjectResult,"progress endpoint uses native user-filtered channel access");
|
|
((PolicyLibraryProxy)(object)library).Item=null;check(await controller.Get(Guid.NewGuid()) is NotFoundResult,"invisible native channel cannot expose startup state");
|
|
context.User=new ClaimsPrincipal();check(await controller.Get(Guid.NewGuid()) is UnauthorizedResult,"progress endpoint rejects absent authentication");
|
|
var manager=(FeedbackSessions)(object)sessions;
|
|
var native=new SessionInfo(sessions,NullLogger.Instance){Id="tv-session",UserId=user.Id,DeviceId="device-a",Client="Jellyfin Android TV",LastActivityDate=DateTime.UtcNow,
|
|
Capabilities=new ClientCapabilities{SupportedCommands=new[]{GeneralCommandType.DisplayMessage}}};
|
|
var other=new SessionInfo(sessions,NullLogger.Instance){Id="other-session",UserId=user.Id,DeviceId="device-b",Client="Jellyfin Android TV",LastActivityDate=DateTime.UtcNow};
|
|
manager.Entries=new[]{native,other}; contexts.HttpContext.Request.Headers.AcceptLanguage="uk-UA";
|
|
check(native.IsActive,"native feedback fixture has an active session");
|
|
var nativeAttempt=await feedback.Begin("native",CancellationToken.None);nativeAttempt.Report("waiting_for_stream");nativeAttempt.Report("ready");nativeAttempt.Report("failed");
|
|
for(var i=0;i<100&&manager.Messages<3;i++)await Task.Delay(10);
|
|
check(manager.Messages==3&&manager.Sent.All(m=>m.Target==native.Id&&m.Message.Header=="Телебачення"),"native localized feedback targets only the requesting user/device and has a three-message ceiling");
|
|
manager.Entries=new[]{native,native};var ambiguous=await feedback.Begin("ambiguous",CancellationToken.None);ambiguous.Report("ready");await Task.Delay(30);
|
|
check(manager.Messages==3,"ambiguous native session never broadcasts a message");
|
|
((FeedbackAuth)(object)auth).Info.IsApiKey=true;
|
|
check(await feedback.Begin("api-key",CancellationToken.None)==null,"server API keys cannot impersonate a native viewer");
|
|
|
|
}
|
|
}
|
|
public class FeedbackAuth:DispatchProxy
|
|
{
|
|
public AuthorizationInfo Info;
|
|
protected override object Invoke(MethodInfo method,object[] args)=>Task.FromResult(Info);
|
|
}
|
|
public class FeedbackSessions:DispatchProxy
|
|
{
|
|
public int Messages;
|
|
public SessionInfo[] Entries=Array.Empty<SessionInfo>();
|
|
public ConcurrentQueue<(string Target,MessageCommand Message)> Sent=new();
|
|
protected override object Invoke(MethodInfo method,object[] args)
|
|
{
|
|
if(method.Name=="get_Sessions")return Entries;
|
|
if(method.Name=="SendMessageCommand"){Sent.Enqueue(((string)args[1],(MessageCommand)args[2]));Interlocked.Increment(ref Messages);return Task.CompletedTask;}
|
|
throw new InvalidOperationException(method.Name);
|
|
}
|
|
}
|