feat: add fnOS packaging, storage workflows and release pipeline
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
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<InvalidOperationException>(() => DouyinUserLookupService.NormalizeDouyinNo(input));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NormalizeDouyinNo_RejectsMoreThan64Characters()
|
||||
{
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
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<DouyinCookie, DouyinFollowed>();
|
||||
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<DouyinCookie, DouyinFollowed>();
|
||||
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<HttpResponseMessage> 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")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user