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?>(new Dictionary { { "alice", 1 } }), ExpiredSubscriptionsHandler = (_, _) => Task.FromResult?>(new Dictionary { { "bob", 2 } }), ListsHandler = _ => Task.FromResult?>(new Dictionary { { "ignored", 10 } }), ListUsersHandler = _ => Task.FromResult?>(["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?>(new Dictionary { { "alice", 1 } }), ListsHandler = _ => Task.FromResult?>(new Dictionary()) }; 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?>(["bob"]) }; DownloadOrchestrationService service = new(apiService, configService, new OrchestrationDownloadServiceStub(), new UserTrackingDbService()); Dictionary allUsers = new() { { "alice", 1 }, { "bob", 2 } }; Dictionary lists = new() { { "mylist", 5 } }; Dictionary 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(), 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 { { 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(), 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 { { 1, "https://example.com/preview.jpg" } }, SingleMessages = new Dictionary { { 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(), true, true, eventHandler); Assert.Contains(eventHandler.ContentFound, entry => entry.contentType == "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?>( new Dictionary { { 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(), 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(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(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? 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; } }