From d9825ae62b9518a2c3097398091cd0ce3b78b429 Mon Sep 17 00:00:00 2001 From: whimsical-c4lic0 Date: Tue, 10 Feb 2026 12:44:52 -0600 Subject: [PATCH] Add AI generated tests for ApiService and DownloadService --- OF DL.Core/Models/Config/Config.cs | 38 ++-- OF DL.Core/Services/StartupService.cs | 8 +- OF DL.Core/Utils/XmlUtils.cs | 4 +- OF DL.Tests/Services/ApiServiceTests.cs | 151 ++++++++++++++ OF DL.Tests/Services/DownloadServiceTests.cs | 159 ++++++++++++++ OF DL.Tests/Services/TestDoubles.cs | 208 +++++++++++++++++++ 6 files changed, 541 insertions(+), 27 deletions(-) create mode 100644 OF DL.Tests/Services/ApiServiceTests.cs create mode 100644 OF DL.Tests/Services/DownloadServiceTests.cs create mode 100644 OF DL.Tests/Services/TestDoubles.cs diff --git a/OF DL.Core/Models/Config/Config.cs b/OF DL.Core/Models/Config/Config.cs index 50323d8..c6d53fc 100644 --- a/OF DL.Core/Models/Config/Config.cs +++ b/OF DL.Core/Models/Config/Config.cs @@ -31,32 +31,32 @@ public class Config : IFileNameFormatConfig [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; } = ""; - [ToggleableConfig] public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } = false; + [ToggleableConfig] public bool RenameExistingFilesWhenCustomFormatIsSelected { get; set; } 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; // 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. [JsonConverter(typeof(StringEnumConverter))] @@ -66,37 +66,37 @@ public class Config : IFileNameFormatConfig [JsonConverter(typeof(ShortDateConverter))] 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; } = ""; - [ToggleableConfig] public bool NonInteractiveModePurchasedTab { get; set; } = false; + [ToggleableConfig] public bool NonInteractiveModePurchasedTab { get; set; } public string? FFmpegPath { get; set; } = ""; - [ToggleableConfig] public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } = false; + [ToggleableConfig] public bool BypassContentForCreatorsWhoNoLongerExist { get; set; } public Dictionary CreatorConfigs { get; set; } = new(); - [ToggleableConfig] public bool DownloadDuplicatedMedia { get; set; } = false; + [ToggleableConfig] public bool DownloadDuplicatedMedia { get; set; } public string IgnoredUsersListName { get; set; } = ""; [JsonConverter(typeof(StringEnumConverter))] 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))] public VideoResolution DownloadVideoResolution { get; set; } = VideoResolution.source; // 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? PostFileNameFormat { get; set; } = ""; diff --git a/OF DL.Core/Services/StartupService.cs b/OF DL.Core/Services/StartupService.cs index ae0e856..c5e4570 100644 --- a/OF DL.Core/Services/StartupService.cs +++ b/OF DL.Core/Services/StartupService.cs @@ -131,6 +131,7 @@ public class StartupService(IConfigService configService, IAuthService authServi Log.Error("Error checking latest release on GitHub. {Message}", e.Message); } #else + await Task.CompletedTask; Log.Debug("Running in Debug/Local mode. Version check skipped."); result.IsUpToDate = true; #endif @@ -221,12 +222,7 @@ public class StartupService(IConfigService configService, IAuthService authServi private static bool ValidateFilePath(string path) { char[] invalidChars = Path.GetInvalidPathChars(); - if (path.Any(c => invalidChars.Contains(c))) - { - return false; - } - - return File.Exists(path); + return !path.Any(c => invalidChars.Contains(c)) && File.Exists(path); } private static string? GetFullPath(string filename) diff --git a/OF DL.Core/Utils/XmlUtils.cs b/OF DL.Core/Utils/XmlUtils.cs index 4f9bbf6..3b57c0f 100644 --- a/OF DL.Core/Utils/XmlUtils.cs +++ b/OF DL.Core/Utils/XmlUtils.cs @@ -4,8 +4,8 @@ namespace OF_DL.Utils; internal static class XmlUtils { - // When true, return original text without parsing/stripping. - public static bool Passthrough { get; set; } = false; + // When true, return the original text without parsing/stripping. + public static bool Passthrough { get; set; } public static string EvaluateInnerText(string xmlValue) { diff --git a/OF DL.Tests/Services/ApiServiceTests.cs b/OF DL.Tests/Services/ApiServiceTests.cs new file mode 100644 index 0000000..af711bf --- /dev/null +++ b/OF DL.Tests/Services/ApiServiceTests.cs @@ -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 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(() => 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(() => 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); + } + } +} diff --git a/OF DL.Tests/Services/DownloadServiceTests.cs b/OF DL.Tests/Services/DownloadServiceTests.cs new file mode 100644 index 0000000..87d4a38 --- /dev/null +++ b/OF DL.Tests/Services/DownloadServiceTests.cs @@ -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() }; + DownloadService service = + CreateService(new FakeConfigService(new Config()), new FakeDbService(), apiService); + + DownloadResult result = await service.DownloadHighlights("user", 1, "/tmp/creator", new HashSet(), + 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 + { + { 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(), 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("\\", "/"); +} diff --git a/OF DL.Tests/Services/TestDoubles.cs b/OF DL.Tests/Services/TestDoubles.cs new file mode 100644 index 0000000..0cfce8b --- /dev/null +++ b/OF DL.Tests/Services/TestDoubles.cs @@ -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 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 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 GetStoredFileSize(string folder, long mediaId, string apiType) => + Task.FromResult(StoredFileSize); + + public Task 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 users) => throw new NotImplementedException(); + + public Task CheckUsername(KeyValuePair 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 GetMostRecentPostDate(string folder) => throw new NotImplementedException(); +} + +internal sealed class FakeApiService : IApiService +{ + public Dictionary? 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 GetDrmMpdPssh(string mpdUrl, string policy, string signature, string kvp) => + Task.FromResult("pssh"); + + public Task GetDrmMpdLastModified(string mpdUrl, string policy, string signature, string kvp) => + Task.FromResult(LastModifiedToReturn); + + public Task GetDecryptionKeyOfdl(Dictionary drmHeaders, string licenceUrl, string pssh) + { + OfdlCalled = true; + return Task.FromResult("ofdl-key"); + } + + public Task GetDecryptionKeyCdm(Dictionary drmHeaders, string licenceUrl, string pssh) + { + CdmCalled = true; + return Task.FromResult("cdm-key"); + } + + public Dictionary GetDynamicHeaders(string path, string queryParam) => + new() { { "X-Test", "value" } }; + + public Task?> GetMedia(MediaType mediaType, string endpoint, string? username, + string folder) => Task.FromResult(MediaToReturn); + + public Task?> GetLists(string endpoint) => throw new NotImplementedException(); + + public Task?> GetListUsers(string endpoint) => throw new NotImplementedException(); + + public Task GetPaidPosts(string endpoint, string folder, + string username, List paidPostIds, IStatusReporter statusReporter) => + throw new NotImplementedException(); + + public Task GetPosts(string endpoint, string folder, + List paidPostIds, IStatusReporter statusReporter) => throw new NotImplementedException(); + + public Task GetPost(string endpoint, string folder) => + throw new NotImplementedException(); + + public Task GetStreams(string endpoint, string folder, + List paidPostIds, IStatusReporter statusReporter) => throw new NotImplementedException(); + + public Task GetArchived(string endpoint, string folder, + IStatusReporter statusReporter) => throw new NotImplementedException(); + + public Task GetMessages(string endpoint, string folder, + IStatusReporter statusReporter) => throw new NotImplementedException(); + + public Task GetPaidMessages(string endpoint, + string folder, string username, IStatusReporter statusReporter) => throw new NotImplementedException(); + + public Task GetPaidMessage(string endpoint, + string folder) => throw new NotImplementedException(); + + public Task> GetPurchasedTabUsers(string endpoint, Dictionary users) => + throw new NotImplementedException(); + + public Task> GetPurchasedTab(string endpoint, + string folder, Dictionary users) => throw new NotImplementedException(); + + public Task GetUserInfo(string endpoint) => + throw new NotImplementedException(); + + public Task GetUserInfoById(string endpoint) => + throw new NotImplementedException(); + + public Task?> GetActiveSubscriptions(string endpoint, + bool includeRestrictedSubscriptions) => throw new NotImplementedException(); + + public Task?> GetExpiredSubscriptions(string endpoint, + bool includeRestrictedSubscriptions) => throw new NotImplementedException(); +} + +internal sealed class FakeFileNameService : IFileNameService +{ + public Task BuildFilename(string fileFormat, Dictionary values) => + throw new NotImplementedException(); + + public Task> GetFilename(object info, object media, object author, + List selectedProperties, string username, Dictionary? users = null) => + throw new NotImplementedException(); +} + +internal sealed class FakeAuthService : IAuthService +{ + public Auth? CurrentAuth { get; set; } + + public Task LoadFromFileAsync(string filePath = "auth.json") => throw new NotImplementedException(); + + public Task LoadFromBrowserAsync() => throw new NotImplementedException(); + + public Task SaveToFileAsync(string filePath = "auth.json") => throw new NotImplementedException(); + + public void ValidateCookieString() => throw new NotImplementedException(); + + public Task ValidateAuthAsync() => throw new NotImplementedException(); + + public void Logout() => throw new NotImplementedException(); +}