using System.Net; using System.Text; using dy.net.model.dto; using dy.net.model.entity; using dy.net.repository; using dy.net.service; using dy.net.Tests.TestInfrastructure; using dy.net.utils; using SqlSugar; namespace dy.net.Tests; public class DouyinUserLookupTests { [Fact] public void ABogusSigner_MatchesMaintainedProtocolFixture() { const string query = "device_platform=webapp&aid=6383&keyword=test&count=10"; const string expected = "E7mhBdLkdD2kDDyh56KLfY3q65EVYhxI0SVkMD2f--dPqL39HMYh9exoIBGvXY8jwG/-Ieujy4hbT3ohrQ2y0Hwf9W0L/25ksDSkKl5Q5xSSs1X9eghgJ04qmkt5SMx2RvB-rOXmqhZHKRbp09oHmhK4bIOwu3GMbE=="; var actual = new DouyinABogusSigner().Sign( query, "GET", 1700000000000, 1700000000005, 1234.5, 2345.5, 3456.5); Assert.Equal(expected, actual); } [Theory] [InlineData("abc_123", "abc_123")] [InlineData(" @abc_123 ", "abc_123")] [InlineData("抖音号:abc_123 IP属地:上海", "abc_123")] [InlineData("Douyin: creator-01", "creator-01")] public void NormalizeDouyinNo_AcceptsCommonCopiedFormats(string input, string expected) { Assert.Equal(expected, DouyinUserLookupService.NormalizeDouyinNo(input)); } [Theory] [InlineData("")] [InlineData(" ")] [InlineData("@")] [InlineData("抖音号:")] [InlineData("abc 123")] public void NormalizeDouyinNo_RejectsInvalidValues(string input) { Assert.Throws(() => DouyinUserLookupService.NormalizeDouyinNo(input)); } [Fact] public void NormalizeDouyinNo_RejectsMoreThan64Characters() { Assert.Throws(() => DouyinUserLookupService.NormalizeDouyinNo(new string('a', 65))); } [Fact] public async Task SearchAsync_ParsesCandidatesAndPrefersUniqueIdOverShortId() { var handler = new SearchResponseHandler(); using var factory = new StubHttpClientFactory(handler, "https://www.douyin.com"); var client = new DouyinUserSearchClient(factory); var candidates = await client.SearchAsync("creator_name", "msToken=token-value; UIFID=ui-value"); var candidate = Assert.Single(candidates); Assert.Equal("sec-user-1", candidate.SecUid); Assert.Equal("uid-1", candidate.UperId); Assert.Equal("creator_name", candidate.UniqueId); Assert.Equal("123456", candidate.ShortId); Assert.Equal("creator_name", candidate.DouyinNo); Assert.Equal("测试博主", candidate.UperName); Assert.Equal("https://example.test/avatar.jpg", candidate.UperAvatar); Assert.Equal(12345, candidate.FollowerCount); Assert.NotNull(handler.Request); Assert.Equal("/aweme/v1/web/discover/search/", handler.Request.RequestUri.AbsolutePath); Assert.Contains("search_channel=aweme_user_web", handler.Request.RequestUri.Query); Assert.Contains("keyword=creator_name", handler.Request.RequestUri.Query); Assert.Contains("a_bogus=", handler.Request.RequestUri.Query); Assert.Contains("msToken=token-value", handler.Request.RequestUri.Query); Assert.Equal("https://www.douyin.com/root/search/creator_name?type=user", handler.Request.Headers.Referrer?.ToString()); Assert.Equal("msToken=token-value; UIFID=ui-value", handler.Cookie); } [Fact] public async Task ResolveAsync_MarksExactMatchAndDuplicateForSelectedAccount() { using var temporary = new TemporaryDirectory(); using var db = CreateDatabase(Path.Combine(temporary.Path, "lookup.sqlite")); db.CodeFirst.InitTables(); db.Insertable(new DouyinCookie { Id = "cookie-1", MyUserId = "my-user-1", UserName = "我的账号", Cookies = "msToken=token-value", Status = 1, StatusCode = 0 }).ExecuteCommand(); db.Insertable(new DouyinFollowed { Id = "follow-1", mySelfId = "my-user-1", SecUid = "sec-user-1", UperId = "uid-1", UperName = "已有博主", LastSyncTime = DateTime.UtcNow }).ExecuteCommand(); using var factory = new StubHttpClientFactory(new SearchResponseHandler(), "https://www.douyin.com"); var service = new DouyinUserLookupService(db, new DouyinUserSearchClient(factory)); var result = await service.ResolveAsync(new DouyinFollowLookupRequest { CookieId = "cookie-1", DouyinNo = "@creator_name" }); var candidate = Assert.Single(result.Candidates); Assert.Equal("creator_name", result.Query); Assert.True(candidate.ExactMatch); Assert.True(candidate.AlreadyExists); } [Fact] public async Task FollowGroups_IncludeEnabledCookieWithNoFollowRows() { using var temporary = new TemporaryDirectory(); using var db = CreateDatabase(Path.Combine(temporary.Path, "groups.sqlite")); db.CodeFirst.InitTables(); db.Insertable(new DouyinCookie { Id = "cookie-empty", MyUserId = "my-empty-user", UserName = "零关注账号", Cookies = "msToken=token-value", Status = 1, StatusCode = 0, StatusMsg = "正常" }).ExecuteCommand(); var groups = await new DouyinFollowRepository(db).GetDouyinFollowGroup(); var group = Assert.Single(groups); Assert.Equal("cookie-empty", group.CookieId); Assert.Equal("my-empty-user", group.Key); Assert.Equal(0, group.Total); Assert.Equal(1, group.Status); } private static SqlSugarClient CreateDatabase(string path) => new(new ConnectionConfig { ConnectionString = $"Data Source={path}", DbType = DbType.Sqlite, IsAutoCloseConnection = true, InitKeyType = InitKeyType.Attribute }); private sealed class SearchResponseHandler : HttpMessageHandler { public HttpRequestMessage Request { get; private set; } public string Cookie { get; private set; } protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { Request = request; Cookie = request.Headers.TryGetValues("Cookie", out var values) ? values.Single() : string.Empty; const string json = @" { ""status_code"": 0, ""user_list"": [ { ""user_info"": { ""sec_uid"": ""sec-user-1"", ""uid"": ""uid-1"", ""unique_id"": ""creator_name"", ""short_id"": ""123456"", ""nickname"": ""测试博主"", ""signature"": ""测试签名"", ""enterprise_verify_reason"": ""认证信息"", ""follower_count"": 12345, ""avatar_thumb"": { ""url_list"": [""https://example.test/avatar.jpg""] } } }, { ""user_info"": { ""sec_uid"": ""sec-user-1"", ""uid"": ""duplicate"", ""short_id"": ""999"" } } ] }"; return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(json, Encoding.UTF8, "application/json") }); } } }