forked from sim0n00ps/OF-DL
Add additional AI generated unit tests
This commit is contained in:
parent
e7fd0ee138
commit
94e135f168
@ -1,10 +1,15 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OF_DL.Enumerations;
|
||||||
using OF_DL.Models;
|
using OF_DL.Models;
|
||||||
using OF_DL.Models.Config;
|
using OF_DL.Models.Config;
|
||||||
|
using OF_DL.Models.Entities.Common;
|
||||||
using OF_DL.Models.OfdlApi;
|
using OF_DL.Models.OfdlApi;
|
||||||
using OF_DL.Services;
|
using OF_DL.Services;
|
||||||
|
using UserEntities = OF_DL.Models.Entities.Users;
|
||||||
|
|
||||||
namespace OF_DL.Tests.Services;
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
@ -101,8 +106,405 @@ public class ApiServiceTests
|
|||||||
Assert.Contains("Auth service is missing required fields", ex.Message);
|
Assert.Contains("Auth service is missing required fields", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildHttpRequestMessage_BuildsUrlAndAddsHeaders()
|
||||||
|
{
|
||||||
|
FakeAuthService authService = new()
|
||||||
|
{
|
||||||
|
CurrentAuth = new Auth
|
||||||
|
{
|
||||||
|
UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_cookie=abc;"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ApiService service = CreateService(authService);
|
||||||
|
DynamicRules rules = new()
|
||||||
|
{
|
||||||
|
AppToken = "app-token",
|
||||||
|
StaticParam = "static",
|
||||||
|
Prefix = "prefix",
|
||||||
|
Suffix = "suffix",
|
||||||
|
ChecksumConstant = 7,
|
||||||
|
ChecksumIndexes = [0]
|
||||||
|
};
|
||||||
|
|
||||||
|
using DynamicRulesCacheScope _ = new(rules);
|
||||||
|
|
||||||
|
Dictionary<string, string> getParams = new() { { "limit", "10" }, { "offset", "5" } };
|
||||||
|
HttpRequestMessage request = await InvokeBuildHttpRequestMessage(service, getParams, "/users");
|
||||||
|
|
||||||
|
Assert.Equal("https://onlyfans.com/api2/v2/users?limit=10&offset=5", request.RequestUri?.ToString());
|
||||||
|
Assert.True(request.Headers.Contains("app-token"));
|
||||||
|
Assert.True(request.Headers.Contains("sign"));
|
||||||
|
Assert.True(request.Headers.Contains("user-id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeserializeJson_ReturnsDefaultForWhitespace()
|
||||||
|
{
|
||||||
|
object? result = InvokeDeserializeJson(typeof(Dictionary<string, int>), " ", null);
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeserializeJson_ParsesValidJson()
|
||||||
|
{
|
||||||
|
object? result = InvokeDeserializeJson(typeof(Dictionary<string, int>), "{\"a\":1}", null);
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Dictionary<string, int> dict = Assert.IsType<Dictionary<string, int>>(result);
|
||||||
|
Assert.Equal(1, dict["a"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateGetParamsForDateSelection_BeforeAddsBeforePublishTime()
|
||||||
|
{
|
||||||
|
Dictionary<string, string> getParams = new();
|
||||||
|
|
||||||
|
InvokeUpdateGetParamsForDateSelection(DownloadDateSelection.before, getParams, "123.000000");
|
||||||
|
|
||||||
|
Assert.Equal("123.000000", getParams["beforePublishTime"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void UpdateGetParamsForDateSelection_AfterAddsAfterPublishTimeAndOrder()
|
||||||
|
{
|
||||||
|
Dictionary<string, string> getParams = new();
|
||||||
|
|
||||||
|
InvokeUpdateGetParamsForDateSelection(DownloadDateSelection.after, getParams, "456.000000");
|
||||||
|
|
||||||
|
Assert.Equal("publish_date_asc", getParams["order"]);
|
||||||
|
Assert.Equal("456.000000", getParams["afterPublishTime"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("photo", "Images")]
|
||||||
|
[InlineData("video", "Videos")]
|
||||||
|
[InlineData("gif", "Videos")]
|
||||||
|
[InlineData("audio", "Audios")]
|
||||||
|
[InlineData("unknown", null)]
|
||||||
|
public void ResolveMediaType_ReturnsExpectedValue(string input, string? expected)
|
||||||
|
{
|
||||||
|
string? result = InvokeResolveMediaType(input);
|
||||||
|
|
||||||
|
Assert.Equal(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsMediaTypeDownloadEnabled_RespectsConfigFlags()
|
||||||
|
{
|
||||||
|
Config config = new() { DownloadImages = false, DownloadVideos = false, DownloadAudios = false };
|
||||||
|
ApiService service = new(new FakeAuthService(), new FakeConfigService(config), new MediaTrackingDbService());
|
||||||
|
|
||||||
|
Assert.False(InvokeIsMediaTypeDownloadEnabled(service, "photo"));
|
||||||
|
Assert.False(InvokeIsMediaTypeDownloadEnabled(service, "video"));
|
||||||
|
Assert.False(InvokeIsMediaTypeDownloadEnabled(service, "audio"));
|
||||||
|
Assert.True(InvokeIsMediaTypeDownloadEnabled(service, "other"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryGetDrmInfo_ReturnsTrueWhenComplete()
|
||||||
|
{
|
||||||
|
Files files = new()
|
||||||
|
{
|
||||||
|
Drm = new Drm
|
||||||
|
{
|
||||||
|
Manifest = new Manifest { Dash = "dash" },
|
||||||
|
Signature = new Signature
|
||||||
|
{
|
||||||
|
Dash = new Dash
|
||||||
|
{
|
||||||
|
CloudFrontPolicy = "policy",
|
||||||
|
CloudFrontSignature = "signature",
|
||||||
|
CloudFrontKeyPairId = "kvp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool result = InvokeTryGetDrmInfo(files, out string manifestDash, out string policy,
|
||||||
|
out string signature, out string kvp);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.Equal("dash", manifestDash);
|
||||||
|
Assert.Equal("policy", policy);
|
||||||
|
Assert.Equal("signature", signature);
|
||||||
|
Assert.Equal("kvp", kvp);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TryGetDrmInfo_ReturnsFalseWhenMissingFields()
|
||||||
|
{
|
||||||
|
Files files = new()
|
||||||
|
{
|
||||||
|
Drm = new Drm
|
||||||
|
{
|
||||||
|
Manifest = new Manifest { Dash = null },
|
||||||
|
Signature = new Signature { Dash = new Dash { CloudFrontPolicy = "policy" } }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
bool result = InvokeTryGetDrmInfo(files, out _, out _, out _, out _);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCurrentUserIdOrDefault_ReturnsMinValueWhenMissingOrInvalid()
|
||||||
|
{
|
||||||
|
ApiService serviceMissing =
|
||||||
|
new(new FakeAuthService(), new FakeConfigService(new Config()), new MediaTrackingDbService());
|
||||||
|
Assert.Equal(int.MinValue, InvokeGetCurrentUserIdOrDefault(serviceMissing));
|
||||||
|
|
||||||
|
FakeAuthService authService = new() { CurrentAuth = new Auth { UserId = "not-a-number" } };
|
||||||
|
ApiService serviceInvalid = new(authService, new FakeConfigService(new Config()), new MediaTrackingDbService());
|
||||||
|
Assert.Equal(int.MinValue, InvokeGetCurrentUserIdOrDefault(serviceInvalid));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetCurrentUserIdOrDefault_ReturnsParsedUserId()
|
||||||
|
{
|
||||||
|
FakeAuthService authService = new() { CurrentAuth = new Auth { UserId = "42" } };
|
||||||
|
ApiService service = new(authService, new FakeConfigService(new Config()), new MediaTrackingDbService());
|
||||||
|
|
||||||
|
Assert.Equal(42, InvokeGetCurrentUserIdOrDefault(service));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConvertToUnixTimestampWithMicrosecondPrecision_ReturnsExpectedSeconds()
|
||||||
|
{
|
||||||
|
double epoch = InvokeConvertToUnixTimestampWithMicrosecondPrecision(
|
||||||
|
new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc));
|
||||||
|
double oneSecond = InvokeConvertToUnixTimestampWithMicrosecondPrecision(
|
||||||
|
new DateTime(1970, 1, 1, 0, 0, 1, DateTimeKind.Utc));
|
||||||
|
|
||||||
|
Assert.Equal(0, epoch, 6);
|
||||||
|
Assert.Equal(1, oneSecond, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDrmMpdPssh_ReturnsSecondPssh()
|
||||||
|
{
|
||||||
|
string mpd = """
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MPD xmlns:cenc="urn:mpeg:cenc:2013">
|
||||||
|
<Period>
|
||||||
|
<ContentProtection>
|
||||||
|
<cenc:pssh>FIRST</cenc:pssh>
|
||||||
|
<cenc:pssh>SECOND</cenc:pssh>
|
||||||
|
</ContentProtection>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
||||||
|
""";
|
||||||
|
using SimpleHttpServer server = new(mpd);
|
||||||
|
FakeAuthService authService = new()
|
||||||
|
{
|
||||||
|
CurrentAuth = new Auth
|
||||||
|
{
|
||||||
|
UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_id=1; sess=2;"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ApiService service = CreateService(authService);
|
||||||
|
|
||||||
|
string pssh = await service.GetDrmMpdPssh(server.Url.ToString(), "policy", "signature", "kvp");
|
||||||
|
await server.Completion;
|
||||||
|
|
||||||
|
Assert.Equal("SECOND", pssh);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDrmMpdLastModified_ReturnsLastModifiedHeader()
|
||||||
|
{
|
||||||
|
DateTime lastModifiedUtc = new(2024, 2, 3, 4, 5, 6, DateTimeKind.Utc);
|
||||||
|
using SimpleHttpServer server = new("<MPD />", lastModifiedUtc);
|
||||||
|
FakeAuthService authService = new()
|
||||||
|
{
|
||||||
|
CurrentAuth = new Auth
|
||||||
|
{
|
||||||
|
UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = "auth_id=1; sess=2;"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ApiService service = CreateService(authService);
|
||||||
|
|
||||||
|
DateTime result =
|
||||||
|
await service.GetDrmMpdLastModified(server.Url.ToString(), "policy", "signature", "kvp");
|
||||||
|
await server.Completion;
|
||||||
|
|
||||||
|
DateTime expectedLocal = lastModifiedUtc.ToLocalTime();
|
||||||
|
Assert.True((result - expectedLocal).Duration() < TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetUserInfo_ReturnsNullWhenAuthMissing()
|
||||||
|
{
|
||||||
|
ApiService service = CreateService(new FakeAuthService());
|
||||||
|
using DynamicRulesCacheScope _ = new(BuildTestRules());
|
||||||
|
|
||||||
|
UserEntities.User? user = await service.GetUserInfo("/users/me");
|
||||||
|
|
||||||
|
Assert.Null(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetUserInfoById_ReturnsNullWhenAuthMissing()
|
||||||
|
{
|
||||||
|
ApiService service = CreateService(new FakeAuthService());
|
||||||
|
using DynamicRulesCacheScope _ = new(BuildTestRules());
|
||||||
|
|
||||||
|
JObject? user = await service.GetUserInfoById("/users/list?x[]=1");
|
||||||
|
|
||||||
|
Assert.Null(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetActiveSubscriptions_ReturnsNullWhenAuthMissing()
|
||||||
|
{
|
||||||
|
ApiService service = CreateService(new FakeAuthService());
|
||||||
|
using DynamicRulesCacheScope _ = new(BuildTestRules());
|
||||||
|
|
||||||
|
Dictionary<string, long>? result = await service.GetActiveSubscriptions("/subscriptions", false);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetExpiredSubscriptions_ReturnsNullWhenAuthMissing()
|
||||||
|
{
|
||||||
|
ApiService service = CreateService(new FakeAuthService());
|
||||||
|
using DynamicRulesCacheScope _ = new(BuildTestRules());
|
||||||
|
|
||||||
|
Dictionary<string, long>? result = await service.GetExpiredSubscriptions("/subscriptions", false);
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetLists_ReturnsNullWhenAuthMissing()
|
||||||
|
{
|
||||||
|
ApiService service = CreateService(new FakeAuthService());
|
||||||
|
using DynamicRulesCacheScope _ = new(BuildTestRules());
|
||||||
|
|
||||||
|
Dictionary<string, long>? result = await service.GetLists("/lists");
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetListUsers_ReturnsNullWhenAuthMissing()
|
||||||
|
{
|
||||||
|
ApiService service = CreateService(new FakeAuthService());
|
||||||
|
using DynamicRulesCacheScope _ = new(BuildTestRules());
|
||||||
|
|
||||||
|
List<string>? result = await service.GetListUsers("/lists/1/users");
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetMedia_ReturnsNullWhenAuthMissing()
|
||||||
|
{
|
||||||
|
ApiService service = CreateService(new FakeAuthService());
|
||||||
|
using DynamicRulesCacheScope _ = new(BuildTestRules());
|
||||||
|
|
||||||
|
Dictionary<long, string>? result = await service.GetMedia(MediaType.Stories, "/users/1/stories", null, "/tmp");
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
private static ApiService CreateService(FakeAuthService authService) =>
|
private static ApiService CreateService(FakeAuthService authService) =>
|
||||||
new(authService, new FakeConfigService(new Config()), new FakeDbService());
|
new(authService, new FakeConfigService(new Config()), new MediaTrackingDbService());
|
||||||
|
|
||||||
|
private static DynamicRules BuildTestRules() =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
AppToken = "app-token",
|
||||||
|
StaticParam = "static",
|
||||||
|
Prefix = "prefix",
|
||||||
|
Suffix = "suffix",
|
||||||
|
ChecksumConstant = 7,
|
||||||
|
ChecksumIndexes = [0]
|
||||||
|
};
|
||||||
|
|
||||||
|
private static async Task<HttpRequestMessage> InvokeBuildHttpRequestMessage(ApiService service,
|
||||||
|
Dictionary<string, string> getParams, string endpoint)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(ApiService).GetMethod("BuildHttpRequestMessage",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Instance)
|
||||||
|
?? throw new InvalidOperationException("BuildHttpRequestMessage not found.");
|
||||||
|
Task<HttpRequestMessage> task = (Task<HttpRequestMessage>)method.Invoke(service,
|
||||||
|
[getParams, endpoint])!;
|
||||||
|
return await task;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static object? InvokeDeserializeJson(Type type, string? body, JsonSerializerSettings? settings)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(ApiService).GetMethod("DeserializeJson",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("DeserializeJson not found.");
|
||||||
|
MethodInfo generic = method.MakeGenericMethod(type);
|
||||||
|
return generic.Invoke(null, [body, settings]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InvokeUpdateGetParamsForDateSelection(DownloadDateSelection selection,
|
||||||
|
Dictionary<string, string> getParams, string? timestamp)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(ApiService).GetMethod("UpdateGetParamsForDateSelection",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static, null,
|
||||||
|
[
|
||||||
|
typeof(DownloadDateSelection), typeof(Dictionary<string, string>).MakeByRefType(),
|
||||||
|
typeof(string)
|
||||||
|
],
|
||||||
|
null)
|
||||||
|
?? throw new InvalidOperationException("UpdateGetParamsForDateSelection not found.");
|
||||||
|
object?[] args = { selection, getParams, timestamp };
|
||||||
|
method.Invoke(null, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? InvokeResolveMediaType(string? type)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(ApiService).GetMethod("ResolveMediaType",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("ResolveMediaType not found.");
|
||||||
|
return (string?)method.Invoke(null, new object?[] { type });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool InvokeIsMediaTypeDownloadEnabled(ApiService service, string? type)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(ApiService).GetMethod("IsMediaTypeDownloadEnabled",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Instance)
|
||||||
|
?? throw new InvalidOperationException("IsMediaTypeDownloadEnabled not found.");
|
||||||
|
return (bool)method.Invoke(service, [type])!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool InvokeTryGetDrmInfo(Files files, out string manifestDash, out string cloudFrontPolicy,
|
||||||
|
out string cloudFrontSignature, out string cloudFrontKeyPairId)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(ApiService).GetMethod("TryGetDrmInfo",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException("TryGetDrmInfo not found.");
|
||||||
|
object?[] args = { files, null, null, null, null };
|
||||||
|
bool result = (bool)method.Invoke(null, args)!;
|
||||||
|
manifestDash = (string)args[1]!;
|
||||||
|
cloudFrontPolicy = (string)args[2]!;
|
||||||
|
cloudFrontSignature = (string)args[3]!;
|
||||||
|
cloudFrontKeyPairId = (string)args[4]!;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int InvokeGetCurrentUserIdOrDefault(ApiService service)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(ApiService).GetMethod("GetCurrentUserIdOrDefault",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Instance)
|
||||||
|
?? throw new InvalidOperationException("GetCurrentUserIdOrDefault not found.");
|
||||||
|
return (int)method.Invoke(service, null)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double InvokeConvertToUnixTimestampWithMicrosecondPrecision(DateTime dateTime)
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(ApiService).GetMethod("ConvertToUnixTimestampWithMicrosecondPrecision",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static)
|
||||||
|
?? throw new InvalidOperationException(
|
||||||
|
"ConvertToUnixTimestampWithMicrosecondPrecision not found.");
|
||||||
|
return (double)method.Invoke(null, [dateTime])!;
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildSign(DynamicRules rules, long timestamp, string pathWithQuery, string userId)
|
private static string BuildSign(DynamicRules rules, long timestamp, string pathWithQuery, string userId)
|
||||||
{
|
{
|
||||||
@ -118,34 +520,4 @@ public class ApiServiceTests
|
|||||||
|
|
||||||
return $"{rules.Prefix}:{hashString}:{checksumHex}:{rules.Suffix}";
|
return $"{rules.Prefix}:{hashString}:{checksumHex}:{rules.Suffix}";
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class DynamicRulesCacheScope : IDisposable
|
|
||||||
{
|
|
||||||
private static readonly FieldInfo s_rulesField =
|
|
||||||
typeof(ApiService).GetField("s_cachedDynamicRules", BindingFlags.NonPublic | BindingFlags.Static) ??
|
|
||||||
throw new InvalidOperationException("Unable to access cached rules field.");
|
|
||||||
|
|
||||||
private static readonly FieldInfo s_expirationField =
|
|
||||||
typeof(ApiService).GetField("s_cachedDynamicRulesExpiration",
|
|
||||||
BindingFlags.NonPublic | BindingFlags.Static) ??
|
|
||||||
throw new InvalidOperationException("Unable to access cached rules expiration field.");
|
|
||||||
|
|
||||||
private readonly object? _priorRules;
|
|
||||||
private readonly DateTime? _priorExpiration;
|
|
||||||
|
|
||||||
public DynamicRulesCacheScope(DynamicRules rules)
|
|
||||||
{
|
|
||||||
_priorRules = s_rulesField.GetValue(null);
|
|
||||||
_priorExpiration = (DateTime?)s_expirationField.GetValue(null);
|
|
||||||
|
|
||||||
s_rulesField.SetValue(null, rules);
|
|
||||||
s_expirationField.SetValue(null, DateTime.UtcNow.AddHours(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
s_rulesField.SetValue(null, _priorRules);
|
|
||||||
s_expirationField.SetValue(null, _priorExpiration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
89
OF DL.Tests/Services/AuthServiceTests.cs
Normal file
89
OF DL.Tests/Services/AuthServiceTests.cs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using OF_DL.Models;
|
||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
|
[Collection("NonParallel")]
|
||||||
|
public class AuthServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadFromFileAsync_ReturnsFalseWhenMissing()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
using CurrentDirectoryScope _ = new(temp.Path);
|
||||||
|
AuthService service = CreateService();
|
||||||
|
|
||||||
|
bool result = await service.LoadFromFileAsync();
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
Assert.Null(service.CurrentAuth);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SaveToFileAsync_WritesAuthFile()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
using CurrentDirectoryScope _ = new(temp.Path);
|
||||||
|
AuthService service = CreateService();
|
||||||
|
service.CurrentAuth = new Auth
|
||||||
|
{
|
||||||
|
UserId = "123",
|
||||||
|
UserAgent = "agent",
|
||||||
|
XBc = "xbc",
|
||||||
|
Cookie = "auth_id=123; sess=abc;"
|
||||||
|
};
|
||||||
|
|
||||||
|
await service.SaveToFileAsync();
|
||||||
|
|
||||||
|
Assert.True(File.Exists("auth.json"));
|
||||||
|
string json = await File.ReadAllTextAsync("auth.json");
|
||||||
|
Auth? saved = JsonConvert.DeserializeObject<Auth>(json);
|
||||||
|
Assert.NotNull(saved);
|
||||||
|
Assert.Equal("123", saved.UserId);
|
||||||
|
Assert.Equal("agent", saved.UserAgent);
|
||||||
|
Assert.Equal("xbc", saved.XBc);
|
||||||
|
Assert.Equal("auth_id=123; sess=abc;", saved.Cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ValidateCookieString_NormalizesAndPersists()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
using CurrentDirectoryScope _ = new(temp.Path);
|
||||||
|
AuthService service = CreateService();
|
||||||
|
service.CurrentAuth = new Auth
|
||||||
|
{
|
||||||
|
Cookie = "auth_id=123; other=1; sess=abc"
|
||||||
|
};
|
||||||
|
|
||||||
|
service.ValidateCookieString();
|
||||||
|
|
||||||
|
Assert.Equal("auth_id=123; sess=abc;", service.CurrentAuth.Cookie);
|
||||||
|
Assert.True(File.Exists("auth.json"));
|
||||||
|
string json = File.ReadAllText("auth.json");
|
||||||
|
Auth? saved = JsonConvert.DeserializeObject<Auth>(json);
|
||||||
|
Assert.NotNull(saved);
|
||||||
|
Assert.Equal("auth_id=123; sess=abc;", saved.Cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Logout_DeletesAuthAndChromeData()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
using CurrentDirectoryScope _ = new(temp.Path);
|
||||||
|
AuthService service = CreateService();
|
||||||
|
Directory.CreateDirectory("chrome-data");
|
||||||
|
File.WriteAllText("chrome-data/test.txt", "x");
|
||||||
|
File.WriteAllText("auth.json", "{}");
|
||||||
|
|
||||||
|
service.Logout();
|
||||||
|
|
||||||
|
Assert.False(Directory.Exists("chrome-data"));
|
||||||
|
Assert.False(File.Exists("auth.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AuthService CreateService() =>
|
||||||
|
new(new ServiceCollection().BuildServiceProvider());
|
||||||
|
}
|
||||||
84
OF DL.Tests/Services/ConfigServiceTests.cs
Normal file
84
OF DL.Tests/Services/ConfigServiceTests.cs
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
using OF_DL.Enumerations;
|
||||||
|
using OF_DL.Models.Config;
|
||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
|
[Collection("NonParallel")]
|
||||||
|
public class ConfigServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadConfigurationAsync_CreatesDefaultConfigWhenMissing()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
using CurrentDirectoryScope _ = new(temp.Path);
|
||||||
|
FakeLoggingService loggingService = new();
|
||||||
|
ConfigService service = new(loggingService);
|
||||||
|
|
||||||
|
bool result = await service.LoadConfigurationAsync([]);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(File.Exists("config.conf"));
|
||||||
|
Assert.True(loggingService.UpdateCount > 0);
|
||||||
|
Assert.Equal(service.CurrentConfig.LoggingLevel, loggingService.LastLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadConfigurationAsync_OverridesNonInteractiveFromCli()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
using CurrentDirectoryScope _ = new(temp.Path);
|
||||||
|
FakeLoggingService loggingService = new();
|
||||||
|
ConfigService service = new(loggingService);
|
||||||
|
await service.SaveConfigurationAsync();
|
||||||
|
|
||||||
|
bool result = await service.LoadConfigurationAsync(["--non-interactive"]);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
Assert.True(service.IsCliNonInteractive);
|
||||||
|
Assert.True(service.CurrentConfig.NonInteractiveMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task LoadConfigurationAsync_ReturnsFalseWhenInvalidFilenameFormat()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
using CurrentDirectoryScope _ = new(temp.Path);
|
||||||
|
FakeLoggingService loggingService = new();
|
||||||
|
ConfigService service = new(loggingService);
|
||||||
|
await service.SaveConfigurationAsync();
|
||||||
|
|
||||||
|
string hocon = await File.ReadAllTextAsync("config.conf");
|
||||||
|
hocon = hocon.Replace("PaidPostFileNameFormat = \"\"",
|
||||||
|
"PaidPostFileNameFormat = \"invalid-format\"");
|
||||||
|
await File.WriteAllTextAsync("config.conf", hocon);
|
||||||
|
|
||||||
|
bool result = await service.LoadConfigurationAsync([]);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ApplyToggleableSelections_UpdatesConfigAndReturnsChange()
|
||||||
|
{
|
||||||
|
FakeLoggingService loggingService = new();
|
||||||
|
ConfigService service = new(loggingService);
|
||||||
|
Config initialConfig = new()
|
||||||
|
{
|
||||||
|
DownloadPosts = true,
|
||||||
|
DownloadMessages = true,
|
||||||
|
DownloadPath = "/downloads",
|
||||||
|
LoggingLevel = LoggingLevel.Warning
|
||||||
|
};
|
||||||
|
service.UpdateConfig(initialConfig);
|
||||||
|
|
||||||
|
bool changed = service.ApplyToggleableSelections(["DownloadPosts"]);
|
||||||
|
|
||||||
|
Assert.True(changed);
|
||||||
|
Assert.True(service.CurrentConfig.DownloadPosts);
|
||||||
|
Assert.False(service.CurrentConfig.DownloadMessages);
|
||||||
|
Assert.Equal("/downloads", service.CurrentConfig.DownloadPath);
|
||||||
|
Assert.Equal(LoggingLevel.Warning, loggingService.LastLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
302
OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs
Normal file
302
OF DL.Tests/Services/DownloadOrchestrationServiceTests.cs
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OF_DL.Models.Config;
|
||||||
|
using OF_DL.Models.Downloads;
|
||||||
|
using OF_DL.Services;
|
||||||
|
using MessageEntities = OF_DL.Models.Entities.Messages;
|
||||||
|
using PostEntities = OF_DL.Models.Entities.Posts;
|
||||||
|
using PurchasedEntities = OF_DL.Models.Entities.Purchased;
|
||||||
|
|
||||||
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
|
public class DownloadOrchestrationServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAvailableUsersAsync_FiltersIgnoredUsers()
|
||||||
|
{
|
||||||
|
Config config = CreateConfig(c =>
|
||||||
|
{
|
||||||
|
c.IncludeExpiredSubscriptions = true;
|
||||||
|
c.IgnoredUsersListName = "ignored";
|
||||||
|
});
|
||||||
|
FakeConfigService configService = new(config);
|
||||||
|
ConfigurableApiService apiService = new()
|
||||||
|
{
|
||||||
|
ActiveSubscriptionsHandler = (_, _) =>
|
||||||
|
Task.FromResult<Dictionary<string, long>?>(new Dictionary<string, long> { { "alice", 1 } }),
|
||||||
|
ExpiredSubscriptionsHandler = (_, _) =>
|
||||||
|
Task.FromResult<Dictionary<string, long>?>(new Dictionary<string, long> { { "bob", 2 } }),
|
||||||
|
ListsHandler =
|
||||||
|
_ => Task.FromResult<Dictionary<string, long>?>(new Dictionary<string, long> { { "ignored", 10 } }),
|
||||||
|
ListUsersHandler = _ => Task.FromResult<List<string>?>(["alice"])
|
||||||
|
};
|
||||||
|
UserTrackingDbService dbService = new();
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(apiService, configService, new OrchestrationDownloadServiceStub(), dbService);
|
||||||
|
|
||||||
|
UserListResult result = await service.GetAvailableUsersAsync();
|
||||||
|
|
||||||
|
Assert.Single(result.Users);
|
||||||
|
Assert.True(result.Users.ContainsKey("bob"));
|
||||||
|
Assert.Null(result.IgnoredListError);
|
||||||
|
Assert.NotNull(dbService.CreatedUsers);
|
||||||
|
Assert.True(dbService.CreatedUsers.ContainsKey("bob"));
|
||||||
|
Assert.False(dbService.CreatedUsers.ContainsKey("alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAvailableUsersAsync_SetsIgnoredListErrorWhenMissing()
|
||||||
|
{
|
||||||
|
Config config = CreateConfig(c =>
|
||||||
|
{
|
||||||
|
c.IncludeExpiredSubscriptions = false;
|
||||||
|
c.IgnoredUsersListName = "missing";
|
||||||
|
});
|
||||||
|
FakeConfigService configService = new(config);
|
||||||
|
ConfigurableApiService apiService = new()
|
||||||
|
{
|
||||||
|
ActiveSubscriptionsHandler = (_, _) =>
|
||||||
|
Task.FromResult<Dictionary<string, long>?>(new Dictionary<string, long> { { "alice", 1 } }),
|
||||||
|
ListsHandler = _ => Task.FromResult<Dictionary<string, long>?>(new Dictionary<string, long>())
|
||||||
|
};
|
||||||
|
UserTrackingDbService dbService = new();
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(apiService, configService, new OrchestrationDownloadServiceStub(), dbService);
|
||||||
|
|
||||||
|
UserListResult result = await service.GetAvailableUsersAsync();
|
||||||
|
|
||||||
|
Assert.NotNull(result.IgnoredListError);
|
||||||
|
Assert.Single(result.Users);
|
||||||
|
Assert.True(result.Users.ContainsKey("alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetUsersForListAsync_ReturnsUsersInList()
|
||||||
|
{
|
||||||
|
FakeConfigService configService = new(CreateConfig());
|
||||||
|
ConfigurableApiService apiService = new() { ListUsersHandler = _ => Task.FromResult<List<string>?>(["bob"]) };
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(apiService, configService, new OrchestrationDownloadServiceStub(), new UserTrackingDbService());
|
||||||
|
Dictionary<string, long> allUsers = new() { { "alice", 1 }, { "bob", 2 } };
|
||||||
|
Dictionary<string, long> lists = new() { { "mylist", 5 } };
|
||||||
|
|
||||||
|
Dictionary<string, long> result = await service.GetUsersForListAsync("mylist", allUsers, lists);
|
||||||
|
|
||||||
|
Assert.Single(result);
|
||||||
|
Assert.Equal(2, result["bob"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveDownloadPath_UsesConfiguredPathWhenSet()
|
||||||
|
{
|
||||||
|
Config config = CreateConfig(c => c.DownloadPath = "C:\\Downloads");
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(new ConfigurableApiService(), new FakeConfigService(config), new OrchestrationDownloadServiceStub(),
|
||||||
|
new UserTrackingDbService());
|
||||||
|
|
||||||
|
string path = service.ResolveDownloadPath("creator");
|
||||||
|
|
||||||
|
Assert.Equal(Path.Combine("C:\\Downloads", "creator"), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResolveDownloadPath_UsesDefaultWhenBlank()
|
||||||
|
{
|
||||||
|
Config config = CreateConfig(c => c.DownloadPath = "");
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(new ConfigurableApiService(), new FakeConfigService(config), new OrchestrationDownloadServiceStub(),
|
||||||
|
new UserTrackingDbService());
|
||||||
|
|
||||||
|
string path = service.ResolveDownloadPath("creator");
|
||||||
|
|
||||||
|
Assert.Equal("__user_data__/sites/OnlyFans/creator", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PrepareUserFolderAsync_CreatesFolderAndDb()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
string userPath = Path.Combine(temp.Path, "creator");
|
||||||
|
UserTrackingDbService dbService = new();
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(new ConfigurableApiService(), new FakeConfigService(CreateConfig()),
|
||||||
|
new OrchestrationDownloadServiceStub(), dbService);
|
||||||
|
|
||||||
|
await service.PrepareUserFolderAsync("creator", 99, userPath);
|
||||||
|
|
||||||
|
Assert.True(Directory.Exists(userPath));
|
||||||
|
Assert.True(dbService.CheckedUser.HasValue);
|
||||||
|
Assert.Equal("creator", dbService.CheckedUser.Value.user.Key);
|
||||||
|
Assert.Equal(99, dbService.CheckedUser.Value.user.Value);
|
||||||
|
Assert.Equal(userPath, dbService.CheckedUser.Value.path);
|
||||||
|
Assert.Contains(userPath, dbService.CreatedDbs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DownloadSinglePostAsync_WhenMissingPost_SendsMessage()
|
||||||
|
{
|
||||||
|
ConfigurableApiService apiService = new()
|
||||||
|
{
|
||||||
|
PostHandler = (_, _) => Task.FromResult(new PostEntities.SinglePostCollection())
|
||||||
|
};
|
||||||
|
OrchestrationDownloadServiceStub downloadService = new();
|
||||||
|
RecordingDownloadEventHandler eventHandler = new();
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(apiService, new FakeConfigService(CreateConfig()), downloadService, new UserTrackingDbService());
|
||||||
|
|
||||||
|
await service.DownloadSinglePostAsync("creator", 42, "/tmp", new Dictionary<string, long>(),
|
||||||
|
true, true, eventHandler);
|
||||||
|
|
||||||
|
Assert.Contains("Getting Post", eventHandler.Messages);
|
||||||
|
Assert.Contains("Couldn't find post", eventHandler.Messages);
|
||||||
|
Assert.False(downloadService.SinglePostCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DownloadSinglePostAsync_WhenDownloaded_SendsDownloadedMessage()
|
||||||
|
{
|
||||||
|
PostEntities.SinglePostCollection collection = new()
|
||||||
|
{
|
||||||
|
SinglePosts = new Dictionary<long, string> { { 1, "https://example.com/post.jpg" } }
|
||||||
|
};
|
||||||
|
ConfigurableApiService apiService = new() { PostHandler = (_, _) => Task.FromResult(collection) };
|
||||||
|
OrchestrationDownloadServiceStub downloadService = new()
|
||||||
|
{
|
||||||
|
SinglePostResult = new DownloadResult { NewDownloads = 1, TotalCount = 1 }
|
||||||
|
};
|
||||||
|
RecordingDownloadEventHandler eventHandler = new();
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(apiService, new FakeConfigService(CreateConfig()), downloadService, new UserTrackingDbService());
|
||||||
|
|
||||||
|
await service.DownloadSinglePostAsync("creator", 99, "/tmp", new Dictionary<string, long>(),
|
||||||
|
true, true, eventHandler);
|
||||||
|
|
||||||
|
Assert.Contains("Post 99 downloaded", eventHandler.Messages);
|
||||||
|
Assert.True(downloadService.SinglePostCalled);
|
||||||
|
Assert.True(eventHandler.ProgressCalls.Count > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DownloadSinglePaidMessageAsync_WithPreviewDownloads()
|
||||||
|
{
|
||||||
|
PurchasedEntities.SinglePaidMessageCollection collection = new()
|
||||||
|
{
|
||||||
|
PreviewSingleMessages = new Dictionary<long, string> { { 1, "https://example.com/preview.jpg" } },
|
||||||
|
SingleMessages = new Dictionary<long, string> { { 2, "https://example.com/full.jpg" } },
|
||||||
|
SingleMessageObjects = [new MessageEntities.SingleMessage()]
|
||||||
|
};
|
||||||
|
ConfigurableApiService apiService = new() { PaidMessageHandler = (_, _) => Task.FromResult(collection) };
|
||||||
|
OrchestrationDownloadServiceStub downloadService = new()
|
||||||
|
{
|
||||||
|
SinglePaidMessageResult = new DownloadResult { TotalCount = 1, NewDownloads = 1 }
|
||||||
|
};
|
||||||
|
RecordingDownloadEventHandler eventHandler = new();
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(apiService, new FakeConfigService(CreateConfig()), downloadService, new UserTrackingDbService());
|
||||||
|
|
||||||
|
await service.DownloadSinglePaidMessageAsync("creator", 5, "/tmp", new Dictionary<string, long>(),
|
||||||
|
true, true, eventHandler);
|
||||||
|
|
||||||
|
Assert.Contains(eventHandler.ContentFound, entry => entry.contentType == "Preview Paid Messages");
|
||||||
|
Assert.True(downloadService.SinglePaidMessageCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DownloadCreatorContentAsync_DownloadsStoriesWhenEnabled()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
string path = Path.Combine(temp.Path, "creator");
|
||||||
|
Config config = CreateConfig(c =>
|
||||||
|
{
|
||||||
|
c.DownloadStories = true;
|
||||||
|
c.ShowScrapeSize = false;
|
||||||
|
});
|
||||||
|
FakeConfigService configService = new(config);
|
||||||
|
ConfigurableApiService apiService = new()
|
||||||
|
{
|
||||||
|
MediaHandler = (_, _, _, _) => Task.FromResult<Dictionary<long, string>?>(
|
||||||
|
new Dictionary<long, string>
|
||||||
|
{
|
||||||
|
{ 1, "https://example.com/one.jpg" }, { 2, "https://example.com/two.jpg" }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
OrchestrationDownloadServiceStub downloadService = new()
|
||||||
|
{
|
||||||
|
StoriesResult = new DownloadResult { TotalCount = 2, NewDownloads = 2 }
|
||||||
|
};
|
||||||
|
RecordingDownloadEventHandler eventHandler = new();
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(apiService, configService, downloadService, new UserTrackingDbService());
|
||||||
|
|
||||||
|
CreatorDownloadResult result = await service.DownloadCreatorContentAsync("creator", 1, path,
|
||||||
|
new Dictionary<string, long>(), true, true, eventHandler);
|
||||||
|
|
||||||
|
Assert.Equal(2, result.StoriesCount);
|
||||||
|
Assert.Contains(eventHandler.ContentFound, entry => entry.contentType == "Stories");
|
||||||
|
Assert.Contains(eventHandler.DownloadCompletes, entry => entry.contentType == "Stories");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveUsernameAsync_ReturnsDeletedPlaceholderWhenMissing()
|
||||||
|
{
|
||||||
|
ConfigurableApiService apiService = new() { UserInfoByIdHandler = _ => Task.FromResult<JObject?>(null) };
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(apiService, new FakeConfigService(CreateConfig()), new OrchestrationDownloadServiceStub(),
|
||||||
|
new UserTrackingDbService());
|
||||||
|
|
||||||
|
string? result = await service.ResolveUsernameAsync(123);
|
||||||
|
|
||||||
|
Assert.Equal("Deleted User - 123", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveUsernameAsync_ReturnsUsernameWhenPresent()
|
||||||
|
{
|
||||||
|
JObject payload = new() { ["5"] = new JObject { ["username"] = "creator" } };
|
||||||
|
ConfigurableApiService apiService = new() { UserInfoByIdHandler = _ => Task.FromResult<JObject?>(payload) };
|
||||||
|
DownloadOrchestrationService service =
|
||||||
|
new(apiService, new FakeConfigService(CreateConfig()), new OrchestrationDownloadServiceStub(),
|
||||||
|
new UserTrackingDbService());
|
||||||
|
|
||||||
|
string? result = await service.ResolveUsernameAsync(5);
|
||||||
|
|
||||||
|
Assert.Equal("creator", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Config CreateConfig(Action<Config>? configure = null)
|
||||||
|
{
|
||||||
|
Config config = new()
|
||||||
|
{
|
||||||
|
DownloadAvatarHeaderPhoto = false,
|
||||||
|
DownloadPaidPosts = false,
|
||||||
|
DownloadPosts = false,
|
||||||
|
DownloadArchived = false,
|
||||||
|
DownloadStreams = false,
|
||||||
|
DownloadStories = false,
|
||||||
|
DownloadHighlights = false,
|
||||||
|
DownloadMessages = false,
|
||||||
|
DownloadPaidMessages = false,
|
||||||
|
DownloadImages = false,
|
||||||
|
DownloadVideos = false,
|
||||||
|
DownloadAudios = false,
|
||||||
|
IncludeExpiredSubscriptions = false,
|
||||||
|
IncludeRestrictedSubscriptions = false,
|
||||||
|
SkipAds = false,
|
||||||
|
IgnoreOwnMessages = false,
|
||||||
|
DownloadPostsIncrementally = false,
|
||||||
|
BypassContentForCreatorsWhoNoLongerExist = false,
|
||||||
|
DownloadDuplicatedMedia = false,
|
||||||
|
DownloadOnlySpecificDates = false,
|
||||||
|
NonInteractiveModePurchasedTab = false,
|
||||||
|
LimitDownloadRate = false,
|
||||||
|
FolderPerPaidPost = false,
|
||||||
|
FolderPerPost = false,
|
||||||
|
FolderPerPaidMessage = false,
|
||||||
|
FolderPerMessage = false,
|
||||||
|
ShowScrapeSize = false,
|
||||||
|
DisableBrowserAuth = false
|
||||||
|
};
|
||||||
|
|
||||||
|
configure?.Invoke(config);
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -19,7 +19,7 @@ public class DownloadServiceTests
|
|||||||
Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException());
|
Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException());
|
||||||
await File.WriteAllTextAsync(serverFilePath, "abc");
|
await File.WriteAllTextAsync(serverFilePath, "abc");
|
||||||
|
|
||||||
FakeDbService dbService = new() { CheckDownloadedResult = false };
|
MediaTrackingDbService dbService = new() { CheckDownloadedResult = false };
|
||||||
FakeConfigService configService = new(new Config { ShowScrapeSize = false });
|
FakeConfigService configService = new(new Config { ShowScrapeSize = false });
|
||||||
DownloadService service = CreateService(configService, dbService);
|
DownloadService service = CreateService(configService, dbService);
|
||||||
ProgressRecorder progress = new();
|
ProgressRecorder progress = new();
|
||||||
@ -51,7 +51,7 @@ public class DownloadServiceTests
|
|||||||
Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException());
|
Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException());
|
||||||
await File.WriteAllTextAsync(serverFilePath, "abc");
|
await File.WriteAllTextAsync(serverFilePath, "abc");
|
||||||
|
|
||||||
FakeDbService dbService = new() { CheckDownloadedResult = true, StoredFileSize = 123 };
|
MediaTrackingDbService dbService = new() { CheckDownloadedResult = true, StoredFileSize = 123 };
|
||||||
FakeConfigService configService =
|
FakeConfigService configService =
|
||||||
new(new Config { ShowScrapeSize = false, RenameExistingFilesWhenCustomFormatIsSelected = true });
|
new(new Config { ShowScrapeSize = false, RenameExistingFilesWhenCustomFormatIsSelected = true });
|
||||||
DownloadService service = CreateService(configService, dbService);
|
DownloadService service = CreateService(configService, dbService);
|
||||||
@ -73,9 +73,9 @@ public class DownloadServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetDecryptionInfo_UsesOfdlWhenCdmMissing()
|
public async Task GetDecryptionInfo_UsesOfdlWhenCdmMissing()
|
||||||
{
|
{
|
||||||
FakeApiService apiService = new();
|
StaticApiService apiService = new();
|
||||||
DownloadService service =
|
DownloadService service =
|
||||||
CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService);
|
CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService);
|
||||||
|
|
||||||
(string decryptionKey, DateTime lastModified)? result = await service.GetDecryptionInfo(
|
(string decryptionKey, DateTime lastModified)? result = await service.GetDecryptionInfo(
|
||||||
"https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
|
"https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
|
||||||
@ -91,9 +91,9 @@ public class DownloadServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetDecryptionInfo_UsesCdmWhenAvailable()
|
public async Task GetDecryptionInfo_UsesCdmWhenAvailable()
|
||||||
{
|
{
|
||||||
FakeApiService apiService = new();
|
StaticApiService apiService = new();
|
||||||
DownloadService service =
|
DownloadService service =
|
||||||
CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService);
|
CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService);
|
||||||
|
|
||||||
(string decryptionKey, DateTime lastModified)? result = await service.GetDecryptionInfo(
|
(string decryptionKey, DateTime lastModified)? result = await service.GetDecryptionInfo(
|
||||||
"https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
|
"https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
|
||||||
@ -109,9 +109,9 @@ public class DownloadServiceTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public async Task DownloadHighlights_ReturnsZeroWhenNoMedia()
|
public async Task DownloadHighlights_ReturnsZeroWhenNoMedia()
|
||||||
{
|
{
|
||||||
FakeApiService apiService = new() { MediaToReturn = new Dictionary<long, string>() };
|
StaticApiService apiService = new() { MediaToReturn = new Dictionary<long, string>() };
|
||||||
DownloadService service =
|
DownloadService service =
|
||||||
CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService);
|
CreateService(new FakeConfigService(new Config()), new MediaTrackingDbService(), apiService);
|
||||||
|
|
||||||
DownloadResult result = await service.DownloadHighlights("user", 1, "/tmp/creator", new HashSet<long>(),
|
DownloadResult result = await service.DownloadHighlights("user", 1, "/tmp/creator", new HashSet<long>(),
|
||||||
new ProgressRecorder());
|
new ProgressRecorder());
|
||||||
@ -128,14 +128,14 @@ public class DownloadServiceTests
|
|||||||
{
|
{
|
||||||
using TempFolder temp = new();
|
using TempFolder temp = new();
|
||||||
string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
|
string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
|
||||||
FakeApiService apiService = new()
|
StaticApiService apiService = new()
|
||||||
{
|
{
|
||||||
MediaToReturn = new Dictionary<long, string>
|
MediaToReturn = new Dictionary<long, string>
|
||||||
{
|
{
|
||||||
{ 1, "https://example.com/one.jpg" }, { 2, "https://example.com/two.jpg" }
|
{ 1, "https://example.com/one.jpg" }, { 2, "https://example.com/two.jpg" }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
FakeDbService dbService = new() { CheckDownloadedResult = true };
|
MediaTrackingDbService dbService = new() { CheckDownloadedResult = true };
|
||||||
FakeConfigService configService = new(new Config { ShowScrapeSize = false });
|
FakeConfigService configService = new(new Config { ShowScrapeSize = false });
|
||||||
ProgressRecorder progress = new();
|
ProgressRecorder progress = new();
|
||||||
DownloadService service = CreateService(configService, dbService, apiService);
|
DownloadService service = CreateService(configService, dbService, apiService);
|
||||||
@ -150,10 +150,11 @@ public class DownloadServiceTests
|
|||||||
Assert.Equal(2, progress.Total);
|
Assert.Equal(2, progress.Total);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DownloadService CreateService(FakeConfigService configService, FakeDbService dbService,
|
private static DownloadService CreateService(FakeConfigService configService, MediaTrackingDbService dbService,
|
||||||
FakeApiService? apiService = null) =>
|
StaticApiService? apiService = null) =>
|
||||||
new(new FakeAuthService(), configService, dbService, new FakeFileNameService(),
|
new(new FakeAuthService(), configService, dbService, new FakeFileNameService(),
|
||||||
apiService ?? new FakeApiService());
|
apiService ?? new StaticApiService());
|
||||||
|
|
||||||
private static string NormalizeFolder(string folder) => folder.Replace("\\", "/");
|
private static string NormalizeFolder(string folder) => folder.Replace("\\", "/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
30
OF DL.Tests/Services/FileNameServiceTestModels.cs
Normal file
30
OF DL.Tests/Services/FileNameServiceTestModels.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
|
internal sealed class TestInfo
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string? Text { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestAuthor
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestMedia
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public TestMediaFiles Files { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestMediaFiles
|
||||||
|
{
|
||||||
|
public TestMediaFull Full { get; set; } = new();
|
||||||
|
public object? Drm { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class TestMediaFull
|
||||||
|
{
|
||||||
|
public string? Url { get; set; }
|
||||||
|
}
|
||||||
79
OF DL.Tests/Services/FileNameServiceTests.cs
Normal file
79
OF DL.Tests/Services/FileNameServiceTests.cs
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
|
public class FileNameServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task GetFilename_ReturnsExpectedValues()
|
||||||
|
{
|
||||||
|
TestInfo info = new() { Id = 7, Text = "<div>hello <b>world</b></div>", CreatedAt = new DateTime(2024, 1, 2) };
|
||||||
|
TestMedia media = new()
|
||||||
|
{
|
||||||
|
Id = 99,
|
||||||
|
Files = new TestMediaFiles
|
||||||
|
{
|
||||||
|
Full = new TestMediaFull { Url = "https://cdn.test/file-name.jpg" }, Drm = new object()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
TestAuthor author = new() { Id = 123 };
|
||||||
|
FileNameService service = new(new FakeAuthService());
|
||||||
|
|
||||||
|
List<string> selectedProperties = ["mediaId", "filename", "username", "text", "createdAt", "id"];
|
||||||
|
Dictionary<string, string> values =
|
||||||
|
await service.GetFilename(info, media, author, selectedProperties, "creator");
|
||||||
|
|
||||||
|
Assert.Equal("99", values["mediaId"]);
|
||||||
|
Assert.Equal("file-name", values["filename"]);
|
||||||
|
Assert.Equal("creator", values["username"]);
|
||||||
|
Assert.Equal("hello world", values["text"]);
|
||||||
|
Assert.Equal("2024-01-02", values["createdAt"]);
|
||||||
|
Assert.Equal("7", values["id"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetFilename_TruncatesTextTo100Chars()
|
||||||
|
{
|
||||||
|
string longText = new('a', 120);
|
||||||
|
TestInfo info = new() { Text = $"<p>{longText}</p>" };
|
||||||
|
TestMedia media = new()
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
Files = new TestMediaFiles
|
||||||
|
{
|
||||||
|
Full = new TestMediaFull { Url = "https://cdn.test/short.jpg" }, Drm = null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
FileNameService service = new(new FakeAuthService());
|
||||||
|
|
||||||
|
Dictionary<string, string> values =
|
||||||
|
await service.GetFilename(info, media, new TestAuthor(), ["text"], "creator");
|
||||||
|
|
||||||
|
Assert.Equal(100, values["text"].Length);
|
||||||
|
Assert.Equal(new string('a', 100), values["text"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetFilename_UsesUserLookupWhenUsernameMissing()
|
||||||
|
{
|
||||||
|
TestAuthor author = new() { Id = 55 };
|
||||||
|
Dictionary<string, long> users = new() { { "mapped", 55 } };
|
||||||
|
FileNameService service = new(new FakeAuthService());
|
||||||
|
|
||||||
|
Dictionary<string, string> values =
|
||||||
|
await service.GetFilename(new TestInfo(), new TestMedia(), author, ["username"], "", users);
|
||||||
|
|
||||||
|
Assert.Equal("mapped", values["username"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildFilename_ReplacesTokensAndRemovesInvalidChars()
|
||||||
|
{
|
||||||
|
FileNameService service = new(new FakeAuthService());
|
||||||
|
Dictionary<string, string> values = new() { { "username", "creator" }, { "mediaId", "99" } };
|
||||||
|
|
||||||
|
string result = await service.BuildFilename("{username}_{mediaId}:*?", values);
|
||||||
|
|
||||||
|
Assert.Equal("creator_99", result);
|
||||||
|
}
|
||||||
|
}
|
||||||
83
OF DL.Tests/Services/SimpleHttpServer.cs
Normal file
83
OF DL.Tests/Services/SimpleHttpServer.cs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
|
internal sealed class SimpleHttpServer : IDisposable
|
||||||
|
{
|
||||||
|
private readonly TcpListener _listener;
|
||||||
|
private readonly Task _handlerTask;
|
||||||
|
private readonly byte[] _responseBytes;
|
||||||
|
|
||||||
|
public SimpleHttpServer(string body, DateTime? lastModifiedUtc = null)
|
||||||
|
{
|
||||||
|
_listener = new TcpListener(IPAddress.Loopback, 0);
|
||||||
|
_listener.Start();
|
||||||
|
int port = ((IPEndPoint)_listener.LocalEndpoint).Port;
|
||||||
|
Url = new Uri($"http://127.0.0.1:{port}/");
|
||||||
|
_responseBytes = BuildResponse(body, lastModifiedUtc);
|
||||||
|
_handlerTask = Task.Run(HandleOnceAsync);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri Url { get; }
|
||||||
|
|
||||||
|
public Task Completion => _handlerTask;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_listener.Stop();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_handlerTask.Wait(TimeSpan.FromSeconds(1));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleOnceAsync()
|
||||||
|
{
|
||||||
|
using TcpClient client = await _listener.AcceptTcpClientAsync();
|
||||||
|
await using NetworkStream stream = client.GetStream();
|
||||||
|
await ReadHeadersAsync(stream);
|
||||||
|
await stream.WriteAsync(_responseBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ReadHeadersAsync(NetworkStream stream)
|
||||||
|
{
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int read;
|
||||||
|
string data = "";
|
||||||
|
while ((read = await stream.ReadAsync(buffer)) > 0)
|
||||||
|
{
|
||||||
|
data += Encoding.ASCII.GetString(buffer, 0, read);
|
||||||
|
if (data.Contains("\r\n\r\n", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] BuildResponse(string body, DateTime? lastModifiedUtc)
|
||||||
|
{
|
||||||
|
byte[] bodyBytes = Encoding.UTF8.GetBytes(body);
|
||||||
|
StringBuilder header = new();
|
||||||
|
header.Append("HTTP/1.1 200 OK\r\n");
|
||||||
|
header.Append("Content-Type: application/xml\r\n");
|
||||||
|
header.Append($"Content-Length: {bodyBytes.Length}\r\n");
|
||||||
|
if (lastModifiedUtc.HasValue)
|
||||||
|
{
|
||||||
|
header.Append($"Last-Modified: {lastModifiedUtc.Value.ToUniversalTime():R}\r\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
header.Append("Connection: close\r\n\r\n");
|
||||||
|
byte[] headerBytes = Encoding.ASCII.GetBytes(header.ToString());
|
||||||
|
|
||||||
|
byte[] response = new byte[headerBytes.Length + bodyBytes.Length];
|
||||||
|
Buffer.BlockCopy(headerBytes, 0, response, 0, headerBytes.Length);
|
||||||
|
Buffer.BlockCopy(bodyBytes, 0, response, headerBytes.Length, bodyBytes.Length);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,8 +2,16 @@ using Newtonsoft.Json.Linq;
|
|||||||
using OF_DL.Enumerations;
|
using OF_DL.Enumerations;
|
||||||
using OF_DL.Models;
|
using OF_DL.Models;
|
||||||
using OF_DL.Models.Config;
|
using OF_DL.Models.Config;
|
||||||
using OF_DL.Models.Entities.Users;
|
using OF_DL.Models.Downloads;
|
||||||
using OF_DL.Services;
|
using OF_DL.Services;
|
||||||
|
using Serilog.Core;
|
||||||
|
using Serilog.Events;
|
||||||
|
using ArchivedEntities = OF_DL.Models.Entities.Archived;
|
||||||
|
using MessageEntities = OF_DL.Models.Entities.Messages;
|
||||||
|
using PostEntities = OF_DL.Models.Entities.Posts;
|
||||||
|
using PurchasedEntities = OF_DL.Models.Entities.Purchased;
|
||||||
|
using StreamEntities = OF_DL.Models.Entities.Streams;
|
||||||
|
using UserEntities = OF_DL.Models.Entities.Users;
|
||||||
|
|
||||||
namespace OF_DL.Tests.Services;
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
@ -54,7 +62,7 @@ internal sealed class FakeConfigService(Config config) : IConfigService
|
|||||||
public bool ApplyToggleableSelections(List<string> selectedNames) => false;
|
public bool ApplyToggleableSelections(List<string> selectedNames) => false;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class FakeDbService : IDbService
|
internal sealed class MediaTrackingDbService : IDbService
|
||||||
{
|
{
|
||||||
public bool CheckDownloadedResult { get; init; }
|
public bool CheckDownloadedResult { get; init; }
|
||||||
|
|
||||||
@ -98,7 +106,7 @@ internal sealed class FakeDbService : IDbService
|
|||||||
public Task<DateTime?> GetMostRecentPostDate(string folder) => throw new NotImplementedException();
|
public Task<DateTime?> GetMostRecentPostDate(string folder) => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal sealed class FakeApiService : IApiService
|
internal sealed class StaticApiService : IApiService
|
||||||
{
|
{
|
||||||
public Dictionary<long, string>? MediaToReturn { get; init; }
|
public Dictionary<long, string>? MediaToReturn { get; init; }
|
||||||
|
|
||||||
@ -167,7 +175,7 @@ internal sealed class FakeApiService : IApiService
|
|||||||
public Task<List<OF_DL.Models.Entities.Purchased.PurchasedTabCollection>> GetPurchasedTab(string endpoint,
|
public Task<List<OF_DL.Models.Entities.Purchased.PurchasedTabCollection>> GetPurchasedTab(string endpoint,
|
||||||
string folder, Dictionary<string, long> users) => throw new NotImplementedException();
|
string folder, Dictionary<string, long> users) => throw new NotImplementedException();
|
||||||
|
|
||||||
public Task<User?> GetUserInfo(string endpoint) =>
|
public Task<UserEntities.User?> GetUserInfo(string endpoint) =>
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
|
|
||||||
public Task<JObject?> GetUserInfoById(string endpoint) =>
|
public Task<JObject?> GetUserInfoById(string endpoint) =>
|
||||||
@ -180,6 +188,290 @@ internal sealed class FakeApiService : IApiService
|
|||||||
bool includeRestrictedSubscriptions) => throw new NotImplementedException();
|
bool includeRestrictedSubscriptions) => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed class ConfigurableApiService : IApiService
|
||||||
|
{
|
||||||
|
public Func<string, bool, Task<Dictionary<string, long>?>>? ActiveSubscriptionsHandler { get; init; }
|
||||||
|
public Func<string, bool, Task<Dictionary<string, long>?>>? ExpiredSubscriptionsHandler { get; init; }
|
||||||
|
public Func<string, Task<Dictionary<string, long>?>>? ListsHandler { get; init; }
|
||||||
|
public Func<string, Task<List<string>?>>? ListUsersHandler { get; init; }
|
||||||
|
public Func<MediaType, string, string?, string, Task<Dictionary<long, string>?>>? MediaHandler { get; init; }
|
||||||
|
public Func<string, string, Task<PostEntities.SinglePostCollection>>? PostHandler { get; init; }
|
||||||
|
public Func<string, string, Task<PurchasedEntities.SinglePaidMessageCollection>>? PaidMessageHandler { get; init; }
|
||||||
|
public Func<string, Task<UserEntities.User?>>? UserInfoHandler { get; init; }
|
||||||
|
public Func<string, Task<JObject?>>? UserInfoByIdHandler { get; init; }
|
||||||
|
|
||||||
|
public Task<Dictionary<string, long>?> GetActiveSubscriptions(string endpoint,
|
||||||
|
bool includeRestrictedSubscriptions) =>
|
||||||
|
ActiveSubscriptionsHandler?.Invoke(endpoint, includeRestrictedSubscriptions) ??
|
||||||
|
Task.FromResult<Dictionary<string, long>?>(null);
|
||||||
|
|
||||||
|
public Task<Dictionary<string, long>?> GetExpiredSubscriptions(string endpoint,
|
||||||
|
bool includeRestrictedSubscriptions) =>
|
||||||
|
ExpiredSubscriptionsHandler?.Invoke(endpoint, includeRestrictedSubscriptions) ??
|
||||||
|
Task.FromResult<Dictionary<string, long>?>(null);
|
||||||
|
|
||||||
|
public Task<Dictionary<string, long>?> GetLists(string endpoint) =>
|
||||||
|
ListsHandler?.Invoke(endpoint) ?? Task.FromResult<Dictionary<string, long>?>(null);
|
||||||
|
|
||||||
|
public Task<List<string>?> GetListUsers(string endpoint) =>
|
||||||
|
ListUsersHandler?.Invoke(endpoint) ?? Task.FromResult<List<string>?>(null);
|
||||||
|
|
||||||
|
public Task<Dictionary<long, string>?> GetMedia(MediaType mediaType, string endpoint, string? username,
|
||||||
|
string folder) =>
|
||||||
|
MediaHandler?.Invoke(mediaType, endpoint, username, folder) ??
|
||||||
|
Task.FromResult<Dictionary<long, string>?>(null);
|
||||||
|
|
||||||
|
public Task<PostEntities.SinglePostCollection> GetPost(string endpoint, string folder) =>
|
||||||
|
PostHandler?.Invoke(endpoint, folder) ?? Task.FromResult(new PostEntities.SinglePostCollection());
|
||||||
|
|
||||||
|
public Task<PurchasedEntities.SinglePaidMessageCollection> GetPaidMessage(string endpoint, string folder) =>
|
||||||
|
PaidMessageHandler?.Invoke(endpoint, folder) ??
|
||||||
|
Task.FromResult(new PurchasedEntities.SinglePaidMessageCollection());
|
||||||
|
|
||||||
|
public Task<UserEntities.User?> GetUserInfo(string endpoint) =>
|
||||||
|
UserInfoHandler?.Invoke(endpoint) ?? Task.FromResult<UserEntities.User?>(null);
|
||||||
|
|
||||||
|
public Task<JObject?> GetUserInfoById(string endpoint) =>
|
||||||
|
UserInfoByIdHandler?.Invoke(endpoint) ?? Task.FromResult<JObject?>(null);
|
||||||
|
|
||||||
|
public Task<PurchasedEntities.PaidPostCollection> GetPaidPosts(string endpoint, string folder, string username,
|
||||||
|
List<long> paidPostIds, IStatusReporter statusReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<PostEntities.PostCollection> GetPosts(string endpoint, string folder, List<long> paidPostIds,
|
||||||
|
IStatusReporter statusReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<StreamEntities.StreamsCollection> GetStreams(string endpoint, string folder, List<long> paidPostIds,
|
||||||
|
IStatusReporter statusReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<ArchivedEntities.ArchivedCollection> GetArchived(string endpoint, string folder,
|
||||||
|
IStatusReporter statusReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<MessageEntities.MessageCollection> GetMessages(string endpoint, string folder,
|
||||||
|
IStatusReporter statusReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<PurchasedEntities.PaidMessageCollection> GetPaidMessages(string endpoint, string folder,
|
||||||
|
string username, IStatusReporter statusReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<List<PurchasedEntities.PurchasedTabCollection>> GetPurchasedTab(string endpoint, string folder,
|
||||||
|
Dictionary<string, long> users) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Dictionary<string, string> GetDynamicHeaders(string path, string queryParam) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<string> GetDecryptionKeyCdm(Dictionary<string, string> drmHeaders, string licenceUrl, string pssh) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<DateTime> GetDrmMpdLastModified(string mpdUrl, string policy, string signature, string kvp) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<string> GetDrmMpdPssh(string mpdUrl, string policy, string signature, string kvp) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<string> GetDecryptionKeyOfdl(Dictionary<string, string> drmHeaders, string licenceUrl, string pssh) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class OrchestrationDownloadServiceStub : IDownloadService
|
||||||
|
{
|
||||||
|
public bool SinglePostCalled { get; private set; }
|
||||||
|
public bool SinglePaidMessageCalled { get; private set; }
|
||||||
|
|
||||||
|
public DownloadResult? SinglePostResult { get; init; }
|
||||||
|
public DownloadResult? SinglePaidMessageResult { get; init; }
|
||||||
|
public DownloadResult? StoriesResult { get; init; }
|
||||||
|
|
||||||
|
public Task<long> CalculateTotalFileSize(List<string> urls) => Task.FromResult((long)urls.Count);
|
||||||
|
|
||||||
|
public Task<bool> ProcessMediaDownload(string folder, long mediaId, string apiType, string url, string path,
|
||||||
|
string serverFileName, string resolvedFileName, string extension, IProgressReporter progressReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<(string decryptionKey, DateTime lastModified)?> GetDecryptionInfo(string mpdUrl, string policy,
|
||||||
|
string signature, string kvp, string mediaId, string contentId, string drmType, bool clientIdBlobMissing,
|
||||||
|
bool devicePrivateKeyMissing) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task DownloadAvatarHeader(string? avatarUrl, string? headerUrl, string folder, string username) =>
|
||||||
|
Task.CompletedTask;
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadHighlights(string username, long userId, string path,
|
||||||
|
HashSet<long> paidPostIds, IProgressReporter progressReporter) =>
|
||||||
|
Task.FromResult(new DownloadResult());
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadStories(string username, long userId, string path,
|
||||||
|
HashSet<long> paidPostIds, IProgressReporter progressReporter) =>
|
||||||
|
Task.FromResult(StoriesResult ?? new DownloadResult());
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadArchived(string username, long userId, string path,
|
||||||
|
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||||
|
ArchivedEntities.ArchivedCollection archived, IProgressReporter progressReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadMessages(string username, long userId, string path,
|
||||||
|
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||||
|
MessageEntities.MessageCollection messages, IProgressReporter progressReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadPaidMessages(string username, string path, Dictionary<string, long> users,
|
||||||
|
bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||||
|
PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadStreams(string username, long userId, string path,
|
||||||
|
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||||
|
StreamEntities.StreamsCollection streams, IProgressReporter progressReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadFreePosts(string username, long userId, string path,
|
||||||
|
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||||
|
PostEntities.PostCollection posts, IProgressReporter progressReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadPaidPosts(string username, long userId, string path,
|
||||||
|
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||||
|
PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadPaidPostsPurchasedTab(string username, string path,
|
||||||
|
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||||
|
PurchasedEntities.PaidPostCollection purchasedPosts, IProgressReporter progressReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadPaidMessagesPurchasedTab(string username, string path,
|
||||||
|
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||||
|
PurchasedEntities.PaidMessageCollection paidMessageCollection, IProgressReporter progressReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadSinglePost(string username, string path, Dictionary<string, long> users,
|
||||||
|
bool clientIdBlobMissing, bool devicePrivateKeyMissing, PostEntities.SinglePostCollection post,
|
||||||
|
IProgressReporter progressReporter)
|
||||||
|
{
|
||||||
|
SinglePostCalled = true;
|
||||||
|
return Task.FromResult(SinglePostResult ?? new DownloadResult());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<DownloadResult> DownloadSinglePaidMessage(string username, string path,
|
||||||
|
Dictionary<string, long> users, bool clientIdBlobMissing, bool devicePrivateKeyMissing,
|
||||||
|
PurchasedEntities.SinglePaidMessageCollection singlePaidMessageCollection,
|
||||||
|
IProgressReporter progressReporter)
|
||||||
|
{
|
||||||
|
SinglePaidMessageCalled = true;
|
||||||
|
return Task.FromResult(SinglePaidMessageResult ?? new DownloadResult());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class UserTrackingDbService : IDbService
|
||||||
|
{
|
||||||
|
public Dictionary<string, long>? CreatedUsers { get; private set; }
|
||||||
|
public List<string> CreatedDbs { get; } = [];
|
||||||
|
public (KeyValuePair<string, long> user, string path)? CheckedUser { get; private set; }
|
||||||
|
|
||||||
|
public Task CreateDb(string folder)
|
||||||
|
{
|
||||||
|
CreatedDbs.Add(folder);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CreateUsersDb(Dictionary<string, long> users)
|
||||||
|
{
|
||||||
|
CreatedUsers = new Dictionary<string, long>(users);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task CheckUsername(KeyValuePair<string, long> user, string path)
|
||||||
|
{
|
||||||
|
CheckedUser = (user, path);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task AddMessage(string folder, long postId, string messageText, string price, bool isPaid,
|
||||||
|
bool isArchived, DateTime createdAt, long userId) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task AddPost(string folder, long postId, string messageText, string price, bool isPaid,
|
||||||
|
bool isArchived, DateTime createdAt) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task AddStory(string folder, long postId, string messageText, string price, bool isPaid,
|
||||||
|
bool isArchived, DateTime createdAt) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task AddMedia(string folder, long mediaId, long postId, string link, string? directory,
|
||||||
|
string? filename, long? size, string apiType, string mediaType, bool preview, bool downloaded,
|
||||||
|
DateTime? createdAt) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task UpdateMedia(string folder, long mediaId, string apiType, string directory, string filename,
|
||||||
|
long size, bool downloaded, DateTime createdAt) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<long> GetStoredFileSize(string folder, long mediaId, string apiType) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<bool> CheckDownloaded(string folder, long mediaId, string apiType) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<DateTime?> GetMostRecentPostDate(string folder) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class RecordingDownloadEventHandler : IDownloadEventHandler
|
||||||
|
{
|
||||||
|
public List<string> Messages { get; } = [];
|
||||||
|
public List<(string contentType, int mediaCount, int objectCount)> ContentFound { get; } = [];
|
||||||
|
public List<string> NoContent { get; } = [];
|
||||||
|
public List<(string contentType, DownloadResult result)> DownloadCompletes { get; } = [];
|
||||||
|
public List<(string description, long maxValue, bool showSize)> ProgressCalls { get; } = [];
|
||||||
|
|
||||||
|
public Task<T> WithStatusAsync<T>(string statusMessage, Func<IStatusReporter, Task<T>> work) =>
|
||||||
|
work(new RecordingStatusReporter(statusMessage));
|
||||||
|
|
||||||
|
public Task<T> WithProgressAsync<T>(string description, long maxValue, bool showSize,
|
||||||
|
Func<IProgressReporter, Task<T>> work)
|
||||||
|
{
|
||||||
|
ProgressCalls.Add((description, maxValue, showSize));
|
||||||
|
return work(new ProgressRecorder());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnContentFound(string contentType, int mediaCount, int objectCount) =>
|
||||||
|
ContentFound.Add((contentType, mediaCount, objectCount));
|
||||||
|
|
||||||
|
public void OnNoContentFound(string contentType) => NoContent.Add(contentType);
|
||||||
|
|
||||||
|
public void OnDownloadComplete(string contentType, DownloadResult result) =>
|
||||||
|
DownloadCompletes.Add((contentType, result));
|
||||||
|
|
||||||
|
public void OnUserStarting(string username) => Messages.Add($"Starting {username}");
|
||||||
|
|
||||||
|
public void OnUserComplete(string username, CreatorDownloadResult result) =>
|
||||||
|
Messages.Add($"Completed {username}");
|
||||||
|
|
||||||
|
public void OnPurchasedTabUserComplete(string username, int paidPostCount, int paidMessagesCount) =>
|
||||||
|
Messages.Add($"Purchased {username}");
|
||||||
|
|
||||||
|
public void OnScrapeComplete(TimeSpan elapsed) => Messages.Add("Scrape complete");
|
||||||
|
|
||||||
|
public void OnMessage(string message) => Messages.Add(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class RecordingStatusReporter : IStatusReporter
|
||||||
|
{
|
||||||
|
private readonly List<string> _statuses;
|
||||||
|
|
||||||
|
public RecordingStatusReporter(string initialStatus)
|
||||||
|
{
|
||||||
|
_statuses = [initialStatus];
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Statuses => _statuses;
|
||||||
|
|
||||||
|
public void ReportStatus(string message) => _statuses.Add(message);
|
||||||
|
}
|
||||||
|
|
||||||
internal sealed class FakeFileNameService : IFileNameService
|
internal sealed class FakeFileNameService : IFileNameService
|
||||||
{
|
{
|
||||||
public Task<string> BuildFilename(string fileFormat, Dictionary<string, string> values) =>
|
public Task<string> BuildFilename(string fileFormat, Dictionary<string, string> values) =>
|
||||||
@ -202,7 +494,23 @@ internal sealed class FakeAuthService : IAuthService
|
|||||||
|
|
||||||
public void ValidateCookieString() => throw new NotImplementedException();
|
public void ValidateCookieString() => throw new NotImplementedException();
|
||||||
|
|
||||||
public Task<User?> ValidateAuthAsync() => throw new NotImplementedException();
|
public Task<UserEntities.User?> ValidateAuthAsync() => throw new NotImplementedException();
|
||||||
|
|
||||||
public void Logout() => throw new NotImplementedException();
|
public void Logout() => throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal sealed class FakeLoggingService : ILoggingService
|
||||||
|
{
|
||||||
|
public LoggingLevelSwitch LevelSwitch { get; } = new();
|
||||||
|
public LoggingLevel LastLevel { get; private set; } = LoggingLevel.Error;
|
||||||
|
public int UpdateCount { get; private set; }
|
||||||
|
|
||||||
|
public void UpdateLoggingLevel(LoggingLevel newLevel)
|
||||||
|
{
|
||||||
|
UpdateCount++;
|
||||||
|
LastLevel = newLevel;
|
||||||
|
LevelSwitch.MinimumLevel = (LogEventLevel)newLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoggingLevel GetCurrentLoggingLevel() => (LoggingLevel)LevelSwitch.MinimumLevel;
|
||||||
|
}
|
||||||
|
|||||||
53
OF DL.Tests/Services/TestScopes.cs
Normal file
53
OF DL.Tests/Services/TestScopes.cs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using OF_DL.Models.OfdlApi;
|
||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
|
[CollectionDefinition("NonParallel", DisableParallelization = true)]
|
||||||
|
public class NonParallelCollection
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class DynamicRulesCacheScope : IDisposable
|
||||||
|
{
|
||||||
|
private static readonly FieldInfo s_rulesField =
|
||||||
|
typeof(ApiService).GetField("s_cachedDynamicRules", BindingFlags.NonPublic | BindingFlags.Static) ??
|
||||||
|
throw new InvalidOperationException("Unable to access cached rules field.");
|
||||||
|
|
||||||
|
private static readonly FieldInfo s_expirationField =
|
||||||
|
typeof(ApiService).GetField("s_cachedDynamicRulesExpiration",
|
||||||
|
BindingFlags.NonPublic | BindingFlags.Static) ??
|
||||||
|
throw new InvalidOperationException("Unable to access cached rules expiration field.");
|
||||||
|
|
||||||
|
private readonly object? _priorRules;
|
||||||
|
private readonly DateTime? _priorExpiration;
|
||||||
|
|
||||||
|
public DynamicRulesCacheScope(DynamicRules rules)
|
||||||
|
{
|
||||||
|
_priorRules = s_rulesField.GetValue(null);
|
||||||
|
_priorExpiration = (DateTime?)s_expirationField.GetValue(null);
|
||||||
|
|
||||||
|
s_rulesField.SetValue(null, rules);
|
||||||
|
s_expirationField.SetValue(null, DateTime.UtcNow.AddHours(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
s_rulesField.SetValue(null, _priorRules);
|
||||||
|
s_expirationField.SetValue(null, _priorExpiration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class CurrentDirectoryScope : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _original;
|
||||||
|
|
||||||
|
public CurrentDirectoryScope(string path)
|
||||||
|
{
|
||||||
|
_original = Environment.CurrentDirectory;
|
||||||
|
Environment.CurrentDirectory = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Environment.CurrentDirectory = _original;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user