forked from sim0n00ps/OF-DL
Add AI generated tests for ApiService and DownloadService
This commit is contained in:
parent
9794eacbc9
commit
d9825ae62b
@ -31,32 +31,32 @@ public class Config : IFileNameFormatConfig
|
|||||||
|
|
||||||
[ToggleableConfig] public bool DownloadAudios { get; set; } = true;
|
[ToggleableConfig] public bool DownloadAudios { get; set; } = true;
|
||||||
|
|
||||||
[ToggleableConfig] public bool IncludeExpiredSubscriptions { get; set; } = false;
|
[ToggleableConfig] public bool IncludeExpiredSubscriptions { get; set; }
|
||||||
|
|
||||||
[ToggleableConfig] public bool IncludeRestrictedSubscriptions { get; set; } = false;
|
[ToggleableConfig] public bool IncludeRestrictedSubscriptions { get; set; }
|
||||||
|
|
||||||
[ToggleableConfig] public bool SkipAds { get; set; } = false;
|
[ToggleableConfig] public bool SkipAds { get; set; }
|
||||||
|
|
||||||
public string? DownloadPath { get; set; } = "";
|
public string? DownloadPath { get; set; } = "";
|
||||||
|
|
||||||
[ToggleableConfig] public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } = false;
|
[ToggleableConfig] public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; }
|
||||||
|
|
||||||
public int? Timeout { get; set; } = -1;
|
public int? Timeout { get; set; } = -1;
|
||||||
|
|
||||||
[ToggleableConfig] public bool FolderPerPaidPost { get; set; } = false;
|
[ToggleableConfig] public bool FolderPerPaidPost { get; set; }
|
||||||
|
|
||||||
[ToggleableConfig] public bool FolderPerPost { get; set; } = false;
|
[ToggleableConfig] public bool FolderPerPost { get; set; }
|
||||||
|
|
||||||
[ToggleableConfig] public bool FolderPerPaidMessage { get; set; } = false;
|
[ToggleableConfig] public bool FolderPerPaidMessage { get; set; }
|
||||||
|
|
||||||
[ToggleableConfig] public bool FolderPerMessage { get; set; } = false;
|
[ToggleableConfig] public bool FolderPerMessage { get; set; }
|
||||||
|
|
||||||
[ToggleableConfig] public bool LimitDownloadRate { get; set; } = false;
|
[ToggleableConfig] public bool LimitDownloadRate { get; set; }
|
||||||
|
|
||||||
public int DownloadLimitInMbPerSec { get; set; } = 4;
|
public int DownloadLimitInMbPerSec { get; set; } = 4;
|
||||||
|
|
||||||
// Indicates if you want to download only on specific dates.
|
// Indicates if you want to download only on specific dates.
|
||||||
[ToggleableConfig] public bool DownloadOnlySpecificDates { get; set; } = false;
|
[ToggleableConfig] public bool DownloadOnlySpecificDates { get; set; }
|
||||||
|
|
||||||
// This enum will define if we want data from before or after the CustomDate.
|
// This enum will define if we want data from before or after the CustomDate.
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
@ -66,37 +66,37 @@ public class Config : IFileNameFormatConfig
|
|||||||
[JsonConverter(typeof(ShortDateConverter))]
|
[JsonConverter(typeof(ShortDateConverter))]
|
||||||
public DateTime? CustomDate { get; set; } = null;
|
public DateTime? CustomDate { get; set; } = null;
|
||||||
|
|
||||||
[ToggleableConfig] public bool ShowScrapeSize { get; set; } = false;
|
[ToggleableConfig] public bool ShowScrapeSize { get; set; }
|
||||||
|
|
||||||
[ToggleableConfig] public bool DownloadPostsIncrementally { get; set; } = false;
|
[ToggleableConfig] public bool DownloadPostsIncrementally { get; set; }
|
||||||
|
|
||||||
public bool NonInteractiveMode { get; set; } = false;
|
public bool NonInteractiveMode { get; set; }
|
||||||
public string NonInteractiveModeListName { get; set; } = "";
|
public string NonInteractiveModeListName { get; set; } = "";
|
||||||
|
|
||||||
[ToggleableConfig] public bool NonInteractiveModePurchasedTab { get; set; } = false;
|
[ToggleableConfig] public bool NonInteractiveModePurchasedTab { get; set; }
|
||||||
|
|
||||||
public string? FFmpegPath { get; set; } = "";
|
public string? FFmpegPath { get; set; } = "";
|
||||||
|
|
||||||
[ToggleableConfig] public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } = false;
|
[ToggleableConfig] public bool BypassContentForCreatorsWhoNoLongerExist { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, CreatorConfig> CreatorConfigs { get; set; } = new();
|
public Dictionary<string, CreatorConfig> CreatorConfigs { get; set; } = new();
|
||||||
|
|
||||||
[ToggleableConfig] public bool DownloadDuplicatedMedia { get; set; } = false;
|
[ToggleableConfig] public bool DownloadDuplicatedMedia { get; set; }
|
||||||
|
|
||||||
public string IgnoredUsersListName { get; set; } = "";
|
public string IgnoredUsersListName { get; set; } = "";
|
||||||
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error;
|
public LoggingLevel LoggingLevel { get; set; } = LoggingLevel.Error;
|
||||||
|
|
||||||
[ToggleableConfig] public bool IgnoreOwnMessages { get; set; } = false;
|
[ToggleableConfig] public bool IgnoreOwnMessages { get; set; }
|
||||||
|
|
||||||
[ToggleableConfig] public bool DisableBrowserAuth { get; set; } = false;
|
[ToggleableConfig] public bool DisableBrowserAuth { get; set; }
|
||||||
|
|
||||||
[JsonConverter(typeof(StringEnumConverter))]
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
public VideoResolution DownloadVideoResolution { get; set; } = VideoResolution.source;
|
public VideoResolution DownloadVideoResolution { get; set; } = VideoResolution.source;
|
||||||
|
|
||||||
// When enabled, post/message text is stored as-is without XML stripping.
|
// When enabled, post/message text is stored as-is without XML stripping.
|
||||||
[ToggleableConfig] public bool DisableTextSanitization { get; set; } = false;
|
[ToggleableConfig] public bool DisableTextSanitization { get; set; }
|
||||||
|
|
||||||
public string? PaidPostFileNameFormat { get; set; } = "";
|
public string? PaidPostFileNameFormat { get; set; } = "";
|
||||||
public string? PostFileNameFormat { get; set; } = "";
|
public string? PostFileNameFormat { get; set; } = "";
|
||||||
|
|||||||
@ -131,6 +131,7 @@ public class StartupService(IConfigService configService, IAuthService authServi
|
|||||||
Log.Error("Error checking latest release on GitHub. {Message}", e.Message);
|
Log.Error("Error checking latest release on GitHub. {Message}", e.Message);
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
await Task.CompletedTask;
|
||||||
Log.Debug("Running in Debug/Local mode. Version check skipped.");
|
Log.Debug("Running in Debug/Local mode. Version check skipped.");
|
||||||
result.IsUpToDate = true;
|
result.IsUpToDate = true;
|
||||||
#endif
|
#endif
|
||||||
@ -221,12 +222,7 @@ public class StartupService(IConfigService configService, IAuthService authServi
|
|||||||
private static bool ValidateFilePath(string path)
|
private static bool ValidateFilePath(string path)
|
||||||
{
|
{
|
||||||
char[] invalidChars = Path.GetInvalidPathChars();
|
char[] invalidChars = Path.GetInvalidPathChars();
|
||||||
if (path.Any(c => invalidChars.Contains(c)))
|
return !path.Any(c => invalidChars.Contains(c)) && File.Exists(path);
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return File.Exists(path);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? GetFullPath(string filename)
|
private static string? GetFullPath(string filename)
|
||||||
|
|||||||
@ -4,8 +4,8 @@ namespace OF_DL.Utils;
|
|||||||
|
|
||||||
internal static class XmlUtils
|
internal static class XmlUtils
|
||||||
{
|
{
|
||||||
// When true, return original text without parsing/stripping.
|
// When true, return the original text without parsing/stripping.
|
||||||
public static bool Passthrough { get; set; } = false;
|
public static bool Passthrough { get; set; }
|
||||||
|
|
||||||
public static string EvaluateInnerText(string xmlValue)
|
public static string EvaluateInnerText(string xmlValue)
|
||||||
{
|
{
|
||||||
|
|||||||
151
OF DL.Tests/Services/ApiServiceTests.cs
Normal file
151
OF DL.Tests/Services/ApiServiceTests.cs
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using OF_DL.Models;
|
||||||
|
using OF_DL.Models.Config;
|
||||||
|
using OF_DL.Models.OfdlApi;
|
||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
|
public class ApiServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void GetDynamicHeaders_ReturnsSignedHeaders()
|
||||||
|
{
|
||||||
|
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, 5, 10, 15]
|
||||||
|
};
|
||||||
|
|
||||||
|
using DynamicRulesCacheScope _ = new(rules);
|
||||||
|
|
||||||
|
Dictionary<string, string> headers = service.GetDynamicHeaders("/api2/v2/users", "?limit=1");
|
||||||
|
|
||||||
|
Assert.Equal("application/json, text/plain", headers["accept"]);
|
||||||
|
Assert.Equal("app-token", headers["app-token"]);
|
||||||
|
Assert.Equal("auth_cookie=abc;", headers["cookie"]);
|
||||||
|
Assert.Equal("unit-test-agent", headers["user-agent"]);
|
||||||
|
Assert.Equal("xbc-token", headers["x-bc"]);
|
||||||
|
Assert.Equal("123", headers["user-id"]);
|
||||||
|
Assert.True(long.TryParse(headers["time"], out long timestamp));
|
||||||
|
|
||||||
|
string expectedSign = BuildSign(rules, timestamp, "/api2/v2/users?limit=1", "123");
|
||||||
|
Assert.Equal(expectedSign, headers["sign"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDynamicHeaders_ThrowsWhenRulesInvalid()
|
||||||
|
{
|
||||||
|
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 = null,
|
||||||
|
StaticParam = "static",
|
||||||
|
Prefix = null,
|
||||||
|
Suffix = "suffix",
|
||||||
|
ChecksumConstant = null,
|
||||||
|
ChecksumIndexes = []
|
||||||
|
};
|
||||||
|
|
||||||
|
using DynamicRulesCacheScope _ = new(rules);
|
||||||
|
|
||||||
|
Exception ex = Assert.Throws<Exception>(() => service.GetDynamicHeaders("/api2/v2/users", "?limit=1"));
|
||||||
|
Assert.Contains("Invalid dynamic rules", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetDynamicHeaders_ThrowsWhenAuthMissingFields()
|
||||||
|
{
|
||||||
|
FakeAuthService authService = new()
|
||||||
|
{
|
||||||
|
CurrentAuth = new Auth
|
||||||
|
{
|
||||||
|
UserId = "123", UserAgent = "unit-test-agent", XBc = "xbc-token", Cookie = null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ApiService service = CreateService(authService);
|
||||||
|
DynamicRules rules = new()
|
||||||
|
{
|
||||||
|
AppToken = "app-token",
|
||||||
|
StaticParam = "static",
|
||||||
|
Prefix = "prefix",
|
||||||
|
Suffix = "suffix",
|
||||||
|
ChecksumConstant = 1,
|
||||||
|
ChecksumIndexes = [0]
|
||||||
|
};
|
||||||
|
|
||||||
|
using DynamicRulesCacheScope _ = new(rules);
|
||||||
|
|
||||||
|
Exception ex = Assert.Throws<Exception>(() => service.GetDynamicHeaders("/api2/v2/users", "?limit=1"));
|
||||||
|
Assert.Contains("Auth service is missing required fields", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ApiService CreateService(FakeAuthService authService) =>
|
||||||
|
new(authService, new FakeConfigService(new Config()), new FakeDbService());
|
||||||
|
|
||||||
|
private static string BuildSign(DynamicRules rules, long timestamp, string pathWithQuery, string userId)
|
||||||
|
{
|
||||||
|
string input = $"{rules.StaticParam}\n{timestamp}\n{pathWithQuery}\n{userId}";
|
||||||
|
byte[] hashBytes = SHA1.HashData(Encoding.UTF8.GetBytes(input));
|
||||||
|
string hashString = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||||
|
|
||||||
|
Assert.NotNull(rules.ChecksumConstant);
|
||||||
|
|
||||||
|
int checksum = rules.ChecksumIndexes.Aggregate(0, (current, index) => current + hashString[index]) +
|
||||||
|
rules.ChecksumConstant.Value;
|
||||||
|
string checksumHex = checksum.ToString("X").ToLowerInvariant();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
159
OF DL.Tests/Services/DownloadServiceTests.cs
Normal file
159
OF DL.Tests/Services/DownloadServiceTests.cs
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
using OF_DL.Models.Config;
|
||||||
|
using OF_DL.Models.Downloads;
|
||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
|
public class DownloadServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessMediaDownload_RenamesServerFileAndUpdatesDb_WhenNotDownloadedButServerFileExists()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
|
||||||
|
string path = "/Posts/Free";
|
||||||
|
string url = "https://example.com/image.jpg";
|
||||||
|
string serverFilename = "server";
|
||||||
|
string resolvedFilename = "custom";
|
||||||
|
string serverFilePath = $"{folder}{path}/{serverFilename}.jpg";
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException());
|
||||||
|
await File.WriteAllTextAsync(serverFilePath, "abc");
|
||||||
|
|
||||||
|
FakeDbService dbService = new() { CheckDownloadedResult = false };
|
||||||
|
FakeConfigService configService = new(new Config { ShowScrapeSize = false });
|
||||||
|
DownloadService service = CreateService(configService, dbService);
|
||||||
|
ProgressRecorder progress = new();
|
||||||
|
|
||||||
|
bool isNew = await service.ProcessMediaDownload(folder, 1, "Posts", url, path, serverFilename,
|
||||||
|
resolvedFilename, ".jpg", progress);
|
||||||
|
|
||||||
|
string renamedPath = $"{folder}{path}/{resolvedFilename}.jpg";
|
||||||
|
Assert.False(isNew);
|
||||||
|
Assert.False(File.Exists(serverFilePath));
|
||||||
|
Assert.True(File.Exists(renamedPath));
|
||||||
|
Assert.NotNull(dbService.LastUpdateMedia);
|
||||||
|
Assert.Equal($"{folder}{path}", dbService.LastUpdateMedia.Value.directory);
|
||||||
|
Assert.Equal("custom.jpg", dbService.LastUpdateMedia.Value.filename);
|
||||||
|
Assert.Equal(new FileInfo(renamedPath).Length, dbService.LastUpdateMedia.Value.size);
|
||||||
|
Assert.Equal(1, progress.Total);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ProcessMediaDownload_RenamesExistingFile_WhenDownloadedAndCustomFormatEnabled()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
|
||||||
|
string path = "/Posts/Free";
|
||||||
|
string url = "https://example.com/image.jpg";
|
||||||
|
string serverFilename = "server";
|
||||||
|
string resolvedFilename = "custom";
|
||||||
|
string serverFilePath = $"{folder}{path}/{serverFilename}.jpg";
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(serverFilePath) ?? throw new InvalidOperationException());
|
||||||
|
await File.WriteAllTextAsync(serverFilePath, "abc");
|
||||||
|
|
||||||
|
FakeDbService dbService = new() { CheckDownloadedResult = true, StoredFileSize = 123 };
|
||||||
|
FakeConfigService configService =
|
||||||
|
new(new Config { ShowScrapeSize = false, RenameExistingFilesWhenCustomFormatIsSelected = true });
|
||||||
|
DownloadService service = CreateService(configService, dbService);
|
||||||
|
ProgressRecorder progress = new();
|
||||||
|
|
||||||
|
bool isNew = await service.ProcessMediaDownload(folder, 1, "Posts", url, path, serverFilename,
|
||||||
|
resolvedFilename, ".jpg", progress);
|
||||||
|
|
||||||
|
string renamedPath = $"{folder}{path}/{resolvedFilename}.jpg";
|
||||||
|
Assert.False(isNew);
|
||||||
|
Assert.False(File.Exists(serverFilePath));
|
||||||
|
Assert.True(File.Exists(renamedPath));
|
||||||
|
Assert.NotNull(dbService.LastUpdateMedia);
|
||||||
|
Assert.Equal("custom.jpg", dbService.LastUpdateMedia.Value.filename);
|
||||||
|
Assert.Equal(123, dbService.LastUpdateMedia.Value.size);
|
||||||
|
Assert.Equal(1, progress.Total);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDecryptionInfo_UsesOfdlWhenCdmMissing()
|
||||||
|
{
|
||||||
|
FakeApiService apiService = new();
|
||||||
|
DownloadService service =
|
||||||
|
CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService);
|
||||||
|
|
||||||
|
(string decryptionKey, DateTime lastModified)? result = await service.GetDecryptionInfo(
|
||||||
|
"https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
|
||||||
|
true, false);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("ofdl-key", result.Value.decryptionKey);
|
||||||
|
Assert.Equal(apiService.LastModifiedToReturn, result.Value.lastModified);
|
||||||
|
Assert.True(apiService.OfdlCalled);
|
||||||
|
Assert.False(apiService.CdmCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetDecryptionInfo_UsesCdmWhenAvailable()
|
||||||
|
{
|
||||||
|
FakeApiService apiService = new();
|
||||||
|
DownloadService service =
|
||||||
|
CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService);
|
||||||
|
|
||||||
|
(string decryptionKey, DateTime lastModified)? result = await service.GetDecryptionInfo(
|
||||||
|
"https://example.com/file.mpd", "policy", "signature", "kvp", "1", "2", "post",
|
||||||
|
false, false);
|
||||||
|
|
||||||
|
Assert.NotNull(result);
|
||||||
|
Assert.Equal("cdm-key", result.Value.decryptionKey);
|
||||||
|
Assert.Equal(apiService.LastModifiedToReturn, result.Value.lastModified);
|
||||||
|
Assert.True(apiService.CdmCalled);
|
||||||
|
Assert.False(apiService.OfdlCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DownloadHighlights_ReturnsZeroWhenNoMedia()
|
||||||
|
{
|
||||||
|
FakeApiService apiService = new() { MediaToReturn = new Dictionary<long, string>() };
|
||||||
|
DownloadService service =
|
||||||
|
CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService);
|
||||||
|
|
||||||
|
DownloadResult result = await service.DownloadHighlights("user", 1, "/tmp/creator", new HashSet<long>(),
|
||||||
|
new ProgressRecorder());
|
||||||
|
|
||||||
|
Assert.Equal(0, result.TotalCount);
|
||||||
|
Assert.Equal(0, result.NewDownloads);
|
||||||
|
Assert.Equal(0, result.ExistingDownloads);
|
||||||
|
Assert.Equal("Highlights", result.MediaType);
|
||||||
|
Assert.True(result.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DownloadHighlights_CountsExistingWhenAlreadyDownloaded()
|
||||||
|
{
|
||||||
|
using TempFolder temp = new();
|
||||||
|
string folder = NormalizeFolder(Path.Combine(temp.Path, "creator"));
|
||||||
|
FakeApiService apiService = new()
|
||||||
|
{
|
||||||
|
MediaToReturn = new Dictionary<long, string>
|
||||||
|
{
|
||||||
|
{ 1, "https://example.com/one.jpg" }, { 2, "https://example.com/two.jpg" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
FakeDbService dbService = new() { CheckDownloadedResult = true };
|
||||||
|
FakeConfigService configService = new(new Config { ShowScrapeSize = false });
|
||||||
|
ProgressRecorder progress = new();
|
||||||
|
DownloadService service = CreateService(configService, dbService, apiService);
|
||||||
|
|
||||||
|
DownloadResult result = await service.DownloadHighlights("user", 1, folder, new HashSet<long>(), progress);
|
||||||
|
|
||||||
|
Assert.Equal(2, result.TotalCount);
|
||||||
|
Assert.Equal(0, result.NewDownloads);
|
||||||
|
Assert.Equal(2, result.ExistingDownloads);
|
||||||
|
Assert.Equal("Highlights", result.MediaType);
|
||||||
|
Assert.True(result.Success);
|
||||||
|
Assert.Equal(2, progress.Total);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DownloadService CreateService(FakeConfigService configService, FakeDbService dbService,
|
||||||
|
FakeApiService? apiService = null) =>
|
||||||
|
new(new FakeAuthService(), configService, dbService, new FakeFileNameService(),
|
||||||
|
apiService ?? new FakeApiService());
|
||||||
|
|
||||||
|
private static string NormalizeFolder(string folder) => folder.Replace("\\", "/");
|
||||||
|
}
|
||||||
208
OF DL.Tests/Services/TestDoubles.cs
Normal file
208
OF DL.Tests/Services/TestDoubles.cs
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OF_DL.Enumerations;
|
||||||
|
using OF_DL.Models;
|
||||||
|
using OF_DL.Models.Config;
|
||||||
|
using OF_DL.Models.Entities.Users;
|
||||||
|
using OF_DL.Services;
|
||||||
|
|
||||||
|
namespace OF_DL.Tests.Services;
|
||||||
|
|
||||||
|
internal sealed class TempFolder : IDisposable
|
||||||
|
{
|
||||||
|
public TempFolder()
|
||||||
|
{
|
||||||
|
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "ofdl-tests", Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Path { get; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(Path, true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ProgressRecorder : IProgressReporter
|
||||||
|
{
|
||||||
|
public long Total { get; private set; }
|
||||||
|
|
||||||
|
public void ReportProgress(long increment) => Total += increment;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class FakeConfigService(Config config) : IConfigService
|
||||||
|
{
|
||||||
|
public Config CurrentConfig { get; private set; } = config;
|
||||||
|
|
||||||
|
public bool IsCliNonInteractive { get; } = config.NonInteractiveMode;
|
||||||
|
|
||||||
|
public Task<bool> LoadConfigurationAsync(string[] args) => Task.FromResult(true);
|
||||||
|
|
||||||
|
public Task SaveConfigurationAsync(string filePath = "config.conf") => Task.CompletedTask;
|
||||||
|
|
||||||
|
public void UpdateConfig(Config newConfig) => CurrentConfig = newConfig;
|
||||||
|
|
||||||
|
public List<(string Name, bool Value)> GetToggleableProperties() => [];
|
||||||
|
|
||||||
|
public bool ApplyToggleableSelections(List<string> selectedNames) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class FakeDbService : IDbService
|
||||||
|
{
|
||||||
|
public bool CheckDownloadedResult { get; init; }
|
||||||
|
|
||||||
|
public long StoredFileSize { get; init; }
|
||||||
|
|
||||||
|
public (string folder, long mediaId, string apiType, string directory, string filename, long size,
|
||||||
|
bool downloaded, DateTime createdAt)? LastUpdateMedia { get; private set; }
|
||||||
|
|
||||||
|
public Task UpdateMedia(string folder, long mediaId, string apiType, string directory, string filename,
|
||||||
|
long size, bool downloaded, DateTime createdAt)
|
||||||
|
{
|
||||||
|
LastUpdateMedia = (folder, mediaId, apiType, directory, filename, size, downloaded, createdAt);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<long> GetStoredFileSize(string folder, long mediaId, string apiType) =>
|
||||||
|
Task.FromResult(StoredFileSize);
|
||||||
|
|
||||||
|
public Task<bool> CheckDownloaded(string folder, long mediaId, string apiType) =>
|
||||||
|
Task.FromResult(CheckDownloadedResult);
|
||||||
|
|
||||||
|
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 CreateDb(string folder) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task CreateUsersDb(Dictionary<string, long> users) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task CheckUsername(KeyValuePair<string, long> user, string path) => 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<DateTime?> GetMostRecentPostDate(string folder) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class FakeApiService : IApiService
|
||||||
|
{
|
||||||
|
public Dictionary<long, string>? MediaToReturn { get; init; }
|
||||||
|
|
||||||
|
public DateTime LastModifiedToReturn { get; set; } = new(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
public bool OfdlCalled { get; private set; }
|
||||||
|
|
||||||
|
public bool CdmCalled { get; private set; }
|
||||||
|
|
||||||
|
public Task<string> GetDrmMpdPssh(string mpdUrl, string policy, string signature, string kvp) =>
|
||||||
|
Task.FromResult("pssh");
|
||||||
|
|
||||||
|
public Task<DateTime> GetDrmMpdLastModified(string mpdUrl, string policy, string signature, string kvp) =>
|
||||||
|
Task.FromResult(LastModifiedToReturn);
|
||||||
|
|
||||||
|
public Task<string> GetDecryptionKeyOfdl(Dictionary<string, string> drmHeaders, string licenceUrl, string pssh)
|
||||||
|
{
|
||||||
|
OfdlCalled = true;
|
||||||
|
return Task.FromResult("ofdl-key");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<string> GetDecryptionKeyCdm(Dictionary<string, string> drmHeaders, string licenceUrl, string pssh)
|
||||||
|
{
|
||||||
|
CdmCalled = true;
|
||||||
|
return Task.FromResult("cdm-key");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<string, string> GetDynamicHeaders(string path, string queryParam) =>
|
||||||
|
new() { { "X-Test", "value" } };
|
||||||
|
|
||||||
|
public Task<Dictionary<long, string>?> GetMedia(MediaType mediaType, string endpoint, string? username,
|
||||||
|
string folder) => Task.FromResult(MediaToReturn);
|
||||||
|
|
||||||
|
public Task<Dictionary<string, long>?> GetLists(string endpoint) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<List<string>?> GetListUsers(string endpoint) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<OF_DL.Models.Entities.Purchased.PaidPostCollection> GetPaidPosts(string endpoint, string folder,
|
||||||
|
string username, List<long> paidPostIds, IStatusReporter statusReporter) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<OF_DL.Models.Entities.Posts.PostCollection> GetPosts(string endpoint, string folder,
|
||||||
|
List<long> paidPostIds, IStatusReporter statusReporter) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<OF_DL.Models.Entities.Posts.SinglePostCollection> GetPost(string endpoint, string folder) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<OF_DL.Models.Entities.Streams.StreamsCollection> GetStreams(string endpoint, string folder,
|
||||||
|
List<long> paidPostIds, IStatusReporter statusReporter) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<OF_DL.Models.Entities.Archived.ArchivedCollection> GetArchived(string endpoint, string folder,
|
||||||
|
IStatusReporter statusReporter) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<OF_DL.Models.Entities.Messages.MessageCollection> GetMessages(string endpoint, string folder,
|
||||||
|
IStatusReporter statusReporter) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<OF_DL.Models.Entities.Purchased.PaidMessageCollection> GetPaidMessages(string endpoint,
|
||||||
|
string folder, string username, IStatusReporter statusReporter) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<OF_DL.Models.Entities.Purchased.SinglePaidMessageCollection> GetPaidMessage(string endpoint,
|
||||||
|
string folder) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<Dictionary<string, long>> GetPurchasedTabUsers(string endpoint, Dictionary<string, long> users) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<List<OF_DL.Models.Entities.Purchased.PurchasedTabCollection>> GetPurchasedTab(string endpoint,
|
||||||
|
string folder, Dictionary<string, long> users) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<User?> GetUserInfo(string endpoint) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<JObject?> GetUserInfoById(string endpoint) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<Dictionary<string, long>?> GetActiveSubscriptions(string endpoint,
|
||||||
|
bool includeRestrictedSubscriptions) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<Dictionary<string, long>?> GetExpiredSubscriptions(string endpoint,
|
||||||
|
bool includeRestrictedSubscriptions) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class FakeFileNameService : IFileNameService
|
||||||
|
{
|
||||||
|
public Task<string> BuildFilename(string fileFormat, Dictionary<string, string> values) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<Dictionary<string, string>> GetFilename(object info, object media, object author,
|
||||||
|
List<string> selectedProperties, string username, Dictionary<string, long>? users = null) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class FakeAuthService : IAuthService
|
||||||
|
{
|
||||||
|
public Auth? CurrentAuth { get; set; }
|
||||||
|
|
||||||
|
public Task<bool> LoadFromFileAsync(string filePath = "auth.json") => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<bool> LoadFromBrowserAsync() => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task SaveToFileAsync(string filePath = "auth.json") => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public void ValidateCookieString() => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<User?> ValidateAuthAsync() => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public void Logout() => throw new NotImplementedException();
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user