Merge pull request #138 from NicolasConstant/develop

0.20.0 PR
This commit is contained in:
Nicolas Constant 2022-02-09 18:43:51 -05:00 committed by GitHub
commit ed3faab924
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1336 additions and 187 deletions

View file

@ -49,6 +49,8 @@ If both whitelisting and blacklisting are set, only the whitelisting will be act
* `Instance:UnlistedTwitterAccounts` (default: null) to enable unlisted publication for selected twitter accounts, separated by `;` (please limit this to brands and other public profiles). * `Instance:UnlistedTwitterAccounts` (default: null) to enable unlisted publication for selected twitter accounts, separated by `;` (please limit this to brands and other public profiles).
* `Instance:SensitiveTwitterAccounts` (default: null) mark all media from given accounts as sensitive by default, separated by `;`. * `Instance:SensitiveTwitterAccounts` (default: null) mark all media from given accounts as sensitive by default, separated by `;`.
* `Instance:FailingTwitterUserCleanUpThreshold` (default: 700) set the max allowed errors (due to a banned/deleted/private account) from a Twitter Account retrieval before auto-removal. (by default an account is called every 15 mins) * `Instance:FailingTwitterUserCleanUpThreshold` (default: 700) set the max allowed errors (due to a banned/deleted/private account) from a Twitter Account retrieval before auto-removal. (by default an account is called every 15 mins)
* `Instance:FailingFollowerCleanUpThreshold` (default: 30000) set the max allowed errors from a Follower (Fediverse) Account before auto-removal. (often due to account suppression, instance issues, etc)
* `Instance:UserCacheCapacity` (default: 10000) set the caching limit of the Twitter User retrieval. Must be higher than the number of synchronized accounts on the instance.
# Docker Compose full example # Docker Compose full example

View file

@ -1,4 +1,5 @@
using System; using System;
using BirdsiteLive.ActivityPub.Models;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub namespace BirdsiteLive.ActivityPub
@ -19,6 +20,8 @@ namespace BirdsiteLive.ActivityPub
if(a.apObject.type == "Follow") if(a.apObject.type == "Follow")
return JsonConvert.DeserializeObject<ActivityUndoFollow>(json); return JsonConvert.DeserializeObject<ActivityUndoFollow>(json);
break; break;
case "Delete":
return JsonConvert.DeserializeObject<ActivityDelete>(json);
case "Accept": case "Accept":
var accept = JsonConvert.DeserializeObject<ActivityAccept>(json); var accept = JsonConvert.DeserializeObject<ActivityAccept>(json);
//var acceptType = JsonConvert.DeserializeObject<Activity>(accept.apObject); //var acceptType = JsonConvert.DeserializeObject<Activity>(accept.apObject);

View file

@ -0,0 +1,10 @@
using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub.Models
{
public class ActivityDelete : Activity
{
[JsonProperty("object")]
public object apObject { get; set; }
}
}

View file

@ -13,5 +13,8 @@
public string SensitiveTwitterAccounts { get; set; } public string SensitiveTwitterAccounts { get; set; }
public int FailingTwitterUserCleanUpThreshold { get; set; } public int FailingTwitterUserCleanUpThreshold { get; set; }
public int FailingFollowerCleanUpThreshold { get; set; } = -1;
public int UserCacheCapacity { get; set; }
} }
} }

View file

@ -11,7 +11,6 @@ using BirdsiteLive.ActivityPub.Models;
using BirdsiteLive.Common.Settings; using BirdsiteLive.Common.Settings;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
using Org.BouncyCastle.Bcpg;
namespace BirdsiteLive.Domain namespace BirdsiteLive.Domain
{ {
@ -45,6 +44,12 @@ namespace BirdsiteLive.Domain
var httpClient = _httpClientFactory.CreateClient(); var httpClient = _httpClientFactory.CreateClient();
httpClient.DefaultRequestHeaders.Add("Accept", "application/activity+json"); httpClient.DefaultRequestHeaders.Add("Accept", "application/activity+json");
var result = await httpClient.GetAsync(objectId); var result = await httpClient.GetAsync(objectId);
if (result.StatusCode == HttpStatusCode.Gone)
throw new FollowerIsGoneException();
result.EnsureSuccessStatusCode();
var content = await result.Content.ReadAsStringAsync(); var content = await result.Content.ReadAsStringAsync();
var actor = JsonConvert.DeserializeObject<Actor>(content); var actor = JsonConvert.DeserializeObject<Actor>(content);

View file

@ -0,0 +1,51 @@
using System.Linq;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
namespace BirdsiteLive.Domain.BusinessUseCases
{
public interface IProcessDeleteUser
{
Task ExecuteAsync(Follower follower);
Task ExecuteAsync(string followerUsername, string followerDomain);
}
public class ProcessDeleteUser : IProcessDeleteUser
{
private readonly IFollowersDal _followersDal;
private readonly ITwitterUserDal _twitterUserDal;
#region Ctor
public ProcessDeleteUser(IFollowersDal followersDal, ITwitterUserDal twitterUserDal)
{
_followersDal = followersDal;
_twitterUserDal = twitterUserDal;
}
#endregion
public async Task ExecuteAsync(string followerUsername, string followerDomain)
{
// Get Follower and Twitter Users
var follower = await _followersDal.GetFollowerAsync(followerUsername, followerDomain);
if (follower == null) return;
await ExecuteAsync(follower);
}
public async Task ExecuteAsync(Follower follower)
{
// Remove twitter users if no more followers
var followings = follower.Followings;
foreach (var following in followings)
{
var followers = await _followersDal.GetFollowersAsync(following);
if (followers.Length == 1 && followers.First().Id == follower.Id)
await _twitterUserDal.DeleteTwitterUserAsync(following);
}
// Remove follower from DB
await _followersDal.DeleteFollowerAsync(follower.Id);
}
}
}

View file

@ -0,0 +1,8 @@
using System;
namespace BirdsiteLive.Domain
{
public class FollowerIsGoneException : Exception
{
}
}

View file

@ -0,0 +1,22 @@
using System.Linq;
namespace BirdsiteLive.Domain.Tools
{
public class SigValidationResultExtractor
{
public static string GetUserName(SignatureValidationResult result)
{
return result.User.preferredUsername.ToLowerInvariant().Trim();
}
public static string GetHost(SignatureValidationResult result)
{
return result.User.url.Replace("https://", string.Empty).Split('/').First();
}
public static string GetSharedInbox(SignatureValidationResult result)
{
return result.User?.endpoints?.sharedInbox;
}
}
}

View file

@ -7,6 +7,7 @@ using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using BirdsiteLive.ActivityPub; using BirdsiteLive.ActivityPub;
using BirdsiteLive.ActivityPub.Converters; using BirdsiteLive.ActivityPub.Converters;
using BirdsiteLive.ActivityPub.Models;
using BirdsiteLive.Common.Regexes; using BirdsiteLive.Common.Regexes;
using BirdsiteLive.Common.Settings; using BirdsiteLive.Common.Settings;
using BirdsiteLive.Cryptography; using BirdsiteLive.Cryptography;
@ -28,10 +29,12 @@ namespace BirdsiteLive.Domain
Task<bool> UndoFollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityUndoFollow activity, string body); Task<bool> UndoFollowRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityUndoFollow activity, string body);
Task<bool> SendRejectFollowAsync(ActivityFollow activity, string followerHost); Task<bool> SendRejectFollowAsync(ActivityFollow activity, string followerHost);
Task<bool> DeleteRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders, ActivityDelete activity, string body);
} }
public class UserService : IUserService public class UserService : IUserService
{ {
private readonly IProcessDeleteUser _processDeleteUser;
private readonly IProcessFollowUser _processFollowUser; private readonly IProcessFollowUser _processFollowUser;
private readonly IProcessUndoFollowUser _processUndoFollowUser; private readonly IProcessUndoFollowUser _processUndoFollowUser;
@ -46,7 +49,7 @@ namespace BirdsiteLive.Domain
private readonly IModerationRepository _moderationRepository; private readonly IModerationRepository _moderationRepository;
#region Ctor #region Ctor
public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser, IProcessUndoFollowUser processUndoFollowUser, IStatusExtractor statusExtractor, IExtractionStatisticsHandler statisticsHandler, ITwitterUserService twitterUserService, IModerationRepository moderationRepository) public UserService(InstanceSettings instanceSettings, ICryptoService cryptoService, IActivityPubService activityPubService, IProcessFollowUser processFollowUser, IProcessUndoFollowUser processUndoFollowUser, IStatusExtractor statusExtractor, IExtractionStatisticsHandler statisticsHandler, ITwitterUserService twitterUserService, IModerationRepository moderationRepository, IProcessDeleteUser processDeleteUser)
{ {
_instanceSettings = instanceSettings; _instanceSettings = instanceSettings;
_cryptoService = cryptoService; _cryptoService = cryptoService;
@ -57,6 +60,7 @@ namespace BirdsiteLive.Domain
_statisticsHandler = statisticsHandler; _statisticsHandler = statisticsHandler;
_twitterUserService = twitterUserService; _twitterUserService = twitterUserService;
_moderationRepository = moderationRepository; _moderationRepository = moderationRepository;
_processDeleteUser = processDeleteUser;
} }
#endregion #endregion
@ -126,10 +130,10 @@ namespace BirdsiteLive.Domain
if (!sigValidation.SignatureIsValidated) return false; if (!sigValidation.SignatureIsValidated) return false;
// Prepare data // Prepare data
var followerUserName = sigValidation.User.preferredUsername.ToLowerInvariant().Trim(); var followerUserName = SigValidationResultExtractor.GetUserName(sigValidation);
var followerHost = sigValidation.User.url.Replace("https://", string.Empty).Split('/').First(); var followerHost = SigValidationResultExtractor.GetHost(sigValidation);
var followerInbox = sigValidation.User.inbox; var followerInbox = sigValidation.User.inbox;
var followerSharedInbox = sigValidation.User?.endpoints?.sharedInbox; var followerSharedInbox = SigValidationResultExtractor.GetSharedInbox(sigValidation);
var twitterUser = activity.apObject.Split('/').Last().Replace("@", string.Empty).ToLowerInvariant().Trim(); var twitterUser = activity.apObject.Split('/').Last().Replace("@", string.Empty).ToLowerInvariant().Trim();
// Make sure to only keep routes // Make sure to only keep routes
@ -213,7 +217,7 @@ namespace BirdsiteLive.Domain
return result == HttpStatusCode.Accepted || return result == HttpStatusCode.Accepted ||
result == HttpStatusCode.OK; //TODO: revamp this for better error handling result == HttpStatusCode.OK; //TODO: revamp this for better error handling
} }
private string OnlyKeepRoute(string inbox, string host) private string OnlyKeepRoute(string inbox, string host)
{ {
if (string.IsNullOrWhiteSpace(inbox)) if (string.IsNullOrWhiteSpace(inbox))
@ -258,6 +262,22 @@ namespace BirdsiteLive.Domain
return result == HttpStatusCode.Accepted || result == HttpStatusCode.OK; //TODO: revamp this for better error handling return result == HttpStatusCode.Accepted || result == HttpStatusCode.OK; //TODO: revamp this for better error handling
} }
public async Task<bool> DeleteRequestedAsync(string signature, string method, string path, string queryString, Dictionary<string, string> requestHeaders,
ActivityDelete activity, string body)
{
// Validate
var sigValidation = await ValidateSignature(activity.actor, signature, method, path, queryString, requestHeaders, body);
if (!sigValidation.SignatureIsValidated) return false;
// Remove user and followings
var followerUserName = SigValidationResultExtractor.GetUserName(sigValidation);
var followerHost = SigValidationResultExtractor.GetHost(sigValidation);
await _processDeleteUser.ExecuteAsync(followerUserName, followerHost);
return true;
}
private async Task<SignatureValidationResult> ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary<string, string> requestHeaders, string body) private async Task<SignatureValidationResult> ValidateSignature(string actor, string rawSig, string method, string path, string queryString, Dictionary<string, string> requestHeaders, string body)
{ {
//Check Date Validity //Check Date Validity

View file

@ -1,12 +1,6 @@
using System; using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.ActivityPub.Converters;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models; using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain; using BirdsiteLive.Domain.BusinessUseCases;
namespace BirdsiteLive.Moderation.Actions namespace BirdsiteLive.Moderation.Actions
{ {
@ -17,16 +11,14 @@ namespace BirdsiteLive.Moderation.Actions
public class RemoveFollowerAction : IRemoveFollowerAction public class RemoveFollowerAction : IRemoveFollowerAction
{ {
private readonly IFollowersDal _followersDal;
private readonly ITwitterUserDal _twitterUserDal;
private readonly IRejectAllFollowingsAction _rejectAllFollowingsAction; private readonly IRejectAllFollowingsAction _rejectAllFollowingsAction;
private readonly IProcessDeleteUser _processDeleteUser;
#region Ctor #region Ctor
public RemoveFollowerAction(IFollowersDal followersDal, ITwitterUserDal twitterUserDal, IRejectAllFollowingsAction rejectAllFollowingsAction) public RemoveFollowerAction(IRejectAllFollowingsAction rejectAllFollowingsAction, IProcessDeleteUser processDeleteUser)
{ {
_followersDal = followersDal;
_twitterUserDal = twitterUserDal;
_rejectAllFollowingsAction = rejectAllFollowingsAction; _rejectAllFollowingsAction = rejectAllFollowingsAction;
_processDeleteUser = processDeleteUser;
} }
#endregion #endregion
@ -36,16 +28,7 @@ namespace BirdsiteLive.Moderation.Actions
await _rejectAllFollowingsAction.ProcessAsync(follower); await _rejectAllFollowingsAction.ProcessAsync(follower);
// Remove twitter users if no more followers // Remove twitter users if no more followers
var followings = follower.Followings; await _processDeleteUser.ExecuteAsync(follower);
foreach (var following in followings)
{
var followers = await _followersDal.GetFollowersAsync(following);
if (followers.Length == 1 && followers.First().Id == follower.Id)
await _twitterUserDal.DeleteTwitterUserAsync(following);
}
// Remove follower from DB
await _followersDal.DeleteFollowerAsync(follower.Id);
} }
} }
} }

View file

@ -9,6 +9,7 @@ using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Pipeline.Contracts; using BirdsiteLive.Pipeline.Contracts;
using BirdsiteLive.Pipeline.Models; using BirdsiteLive.Pipeline.Models;
using BirdsiteLive.Twitter; using BirdsiteLive.Twitter;
using BirdsiteLive.Twitter.Models;
namespace BirdsiteLive.Pipeline.Processors namespace BirdsiteLive.Pipeline.Processors
{ {
@ -35,26 +36,61 @@ namespace BirdsiteLive.Pipeline.Processors
foreach (var user in syncTwitterUsers) foreach (var user in syncTwitterUsers)
{ {
var userView = _twitterUserService.GetUser(user.Acct); TwitterUser userView = null;
if (userView == null)
{
await AnalyseFailingUserAsync(user);
}
else if (!userView.Protected)
{
user.FetchingErrorCount = 0;
var userWtData = new UserWithDataToSync
{
User = user
};
usersWtData.Add(userWtData);
}
}
try
{
userView = _twitterUserService.GetUser(user.Acct);
}
catch (UserNotFoundException)
{
await ProcessNotFoundUserAsync(user);
continue;
}
catch (UserHasBeenSuspendedException)
{
await ProcessNotFoundUserAsync(user);
continue;
}
catch (RateLimitExceededException)
{
await ProcessRateLimitExceededAsync(user);
continue;
}
catch (Exception)
{
// ignored
}
if (userView == null || userView.Protected)
{
await ProcessFailingUserAsync(user);
continue;
}
user.FetchingErrorCount = 0;
var userWtData = new UserWithDataToSync
{
User = user
};
usersWtData.Add(userWtData);
}
return usersWtData.ToArray(); return usersWtData.ToArray();
} }
private async Task AnalyseFailingUserAsync(SyncTwitterUser user) private async Task ProcessRateLimitExceededAsync(SyncTwitterUser user)
{
var dbUser = await _twitterUserDal.GetTwitterUserAsync(user.Acct);
dbUser.LastSync = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(dbUser);
}
private async Task ProcessNotFoundUserAsync(SyncTwitterUser user)
{
await _removeTwitterAccountAction.ProcessAsync(user);
}
private async Task ProcessFailingUserAsync(SyncTwitterUser user)
{ {
var dbUser = await _twitterUserDal.GetTwitterUserAsync(user.Acct); var dbUser = await _twitterUserDal.GetTwitterUserAsync(user.Acct);
dbUser.FetchingErrorCount++; dbUser.FetchingErrorCount++;
@ -68,9 +104,6 @@ namespace BirdsiteLive.Pipeline.Processors
{ {
await _twitterUserDal.UpdateTwitterUserAsync(dbUser); await _twitterUserDal.UpdateTwitterUserAsync(dbUser);
} }
// Purge
_twitterUserService.PurgeUser(user.Acct);
} }
} }
} }

View file

@ -5,9 +5,11 @@ using System.Net;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Contracts; using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models; using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain; using BirdsiteLive.Domain;
using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Pipeline.Contracts; using BirdsiteLive.Pipeline.Contracts;
using BirdsiteLive.Pipeline.Models; using BirdsiteLive.Pipeline.Models;
using BirdsiteLive.Pipeline.Processors.SubTasks; using BirdsiteLive.Pipeline.Processors.SubTasks;
@ -23,14 +25,18 @@ namespace BirdsiteLive.Pipeline.Processors
private readonly ISendTweetsToInboxTask _sendTweetsToInboxTask; private readonly ISendTweetsToInboxTask _sendTweetsToInboxTask;
private readonly ISendTweetsToSharedInboxTask _sendTweetsToSharedInbox; private readonly ISendTweetsToSharedInboxTask _sendTweetsToSharedInbox;
private readonly IFollowersDal _followersDal; private readonly IFollowersDal _followersDal;
private readonly InstanceSettings _instanceSettings;
private readonly ILogger<SendTweetsToFollowersProcessor> _logger; private readonly ILogger<SendTweetsToFollowersProcessor> _logger;
private readonly IRemoveFollowerAction _removeFollowerAction;
#region Ctor #region Ctor
public SendTweetsToFollowersProcessor(ISendTweetsToInboxTask sendTweetsToInboxTask, ISendTweetsToSharedInboxTask sendTweetsToSharedInbox, IFollowersDal followersDal, ILogger<SendTweetsToFollowersProcessor> logger) public SendTweetsToFollowersProcessor(ISendTweetsToInboxTask sendTweetsToInboxTask, ISendTweetsToSharedInboxTask sendTweetsToSharedInbox, IFollowersDal followersDal, ILogger<SendTweetsToFollowersProcessor> logger, InstanceSettings instanceSettings, IRemoveFollowerAction removeFollowerAction)
{ {
_sendTweetsToInboxTask = sendTweetsToInboxTask; _sendTweetsToInboxTask = sendTweetsToInboxTask;
_sendTweetsToSharedInbox = sendTweetsToSharedInbox; _sendTweetsToSharedInbox = sendTweetsToSharedInbox;
_logger = logger; _logger = logger;
_instanceSettings = instanceSettings;
_removeFollowerAction = removeFollowerAction;
_followersDal = followersDal; _followersDal = followersDal;
} }
#endregion #endregion
@ -107,7 +113,17 @@ namespace BirdsiteLive.Pipeline.Processors
private async Task ProcessFailingUserAsync(Follower follower) private async Task ProcessFailingUserAsync(Follower follower)
{ {
follower.PostingErrorCount++; follower.PostingErrorCount++;
await _followersDal.UpdateFollowerAsync(follower);
if (follower.PostingErrorCount > _instanceSettings.FailingFollowerCleanUpThreshold
&& _instanceSettings.FailingFollowerCleanUpThreshold > 0
|| follower.PostingErrorCount > 2147483600)
{
await _removeFollowerAction.ProcessAsync(follower);
}
else
{
await _followersDal.UpdateFollowerAsync(follower);
}
} }
} }
} }

View file

@ -1,4 +1,5 @@
using System; using System;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.Twitter.Models; using BirdsiteLive.Twitter.Models;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
@ -13,11 +14,8 @@ namespace BirdsiteLive.Twitter
{ {
private readonly ITwitterUserService _twitterService; private readonly ITwitterUserService _twitterService;
private MemoryCache _userCache = new MemoryCache(new MemoryCacheOptions() private readonly MemoryCache _userCache;
{ private readonly MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions()
SizeLimit = 5000
});
private MemoryCacheEntryOptions _cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSize(1)//Size amount .SetSize(1)//Size amount
//Priority on removing when reaching size limit (memory pressure) //Priority on removing when reaching size limit (memory pressure)
.SetPriority(CacheItemPriority.High) .SetPriority(CacheItemPriority.High)
@ -27,9 +25,14 @@ namespace BirdsiteLive.Twitter
.SetAbsoluteExpiration(TimeSpan.FromDays(7)); .SetAbsoluteExpiration(TimeSpan.FromDays(7));
#region Ctor #region Ctor
public CachedTwitterUserService(ITwitterUserService twitterService) public CachedTwitterUserService(ITwitterUserService twitterService, InstanceSettings settings)
{ {
_twitterService = twitterService; _twitterService = twitterService;
_userCache = new MemoryCache(new MemoryCacheOptions()
{
SizeLimit = settings.UserCacheCapacity
});
} }
#endregion #endregion
@ -44,6 +47,11 @@ namespace BirdsiteLive.Twitter
return user; return user;
} }
public bool IsUserApiRateLimited()
{
return _twitterService.IsUserApiRateLimited();
}
public void PurgeUser(string username) public void PurgeUser(string username)
{ {
_userCache.Remove(username); _userCache.Remove(username);

View file

@ -0,0 +1,9 @@
using System;
namespace BirdsiteLive.Twitter
{
public class RateLimitExceededException : Exception
{
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace BirdsiteLive.Twitter
{
public class UserHasBeenSuspendedException : Exception
{
}
}

View file

@ -0,0 +1,9 @@
using System;
namespace BirdsiteLive.Twitter
{
public class UserNotFoundException : Exception
{
}
}

View file

@ -13,6 +13,8 @@ namespace BirdsiteLive.Statistics.Domain
void CalledTweetApi(); void CalledTweetApi();
void CalledTimelineApi(); void CalledTimelineApi();
ApiStatistics GetStatistics(); ApiStatistics GetStatistics();
int GetCurrentUserCalls();
} }
//Rate limits: https://developer.twitter.com/en/docs/twitter-api/v1/rate-limits //Rate limits: https://developer.twitter.com/en/docs/twitter-api/v1/rate-limits
@ -60,7 +62,12 @@ namespace BirdsiteLive.Statistics.Domain
foreach (var old in oldSnapshots) _snapshots.TryRemove(old, out var data); foreach (var old in oldSnapshots) _snapshots.TryRemove(old, out var data);
} }
public void CalledUserApi() //GET users/show - 900/15mins public int GetCurrentUserCalls()
{
return _userCalls;
}
public void CalledUserApi() //GET users/show - 300/15mins
{ {
Interlocked.Increment(ref _userCalls); Interlocked.Increment(ref _userCalls);
} }

View file

@ -6,6 +6,7 @@ using BirdsiteLive.Twitter.Models;
using BirdsiteLive.Twitter.Tools; using BirdsiteLive.Twitter.Tools;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Tweetinvi; using Tweetinvi;
using Tweetinvi.Exceptions;
using Tweetinvi.Models; using Tweetinvi.Models;
namespace BirdsiteLive.Twitter namespace BirdsiteLive.Twitter
@ -13,6 +14,7 @@ namespace BirdsiteLive.Twitter
public interface ITwitterUserService public interface ITwitterUserService
{ {
TwitterUser GetUser(string username); TwitterUser GetUser(string username);
bool IsUserApiRateLimited();
} }
public class TwitterUserService : ITwitterUserService public class TwitterUserService : ITwitterUserService
@ -32,27 +34,46 @@ namespace BirdsiteLive.Twitter
public TwitterUser GetUser(string username) public TwitterUser GetUser(string username)
{ {
//Check if API is saturated
if (IsUserApiRateLimited()) throw new RateLimitExceededException();
//Proceed to account retrieval
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized(); _twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
ExceptionHandler.SwallowWebExceptions = false; ExceptionHandler.SwallowWebExceptions = false;
RateLimit.RateLimitTrackerMode = RateLimitTrackerMode.TrackOnly;
IUser user; IUser user;
try try
{ {
user = User.GetUserFromScreenName(username); user = User.GetUserFromScreenName(username);
_statisticsHandler.CalledUserApi(); }
if (user == null) catch (TwitterException e)
{
if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("User has been suspended".ToLowerInvariant())))
{ {
_logger.LogWarning("User {username} not found", username); throw new UserHasBeenSuspendedException();
return null; }
else if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("User not found".ToLowerInvariant())))
{
throw new UserNotFoundException();
}
else if (e.TwitterExceptionInfos.Any(x => x.Message.ToLowerInvariant().Contains("Rate limit exceeded".ToLowerInvariant())))
{
throw new RateLimitExceededException();
}
else
{
throw;
} }
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, "Error retrieving user {Username}", username); _logger.LogError(e, "Error retrieving user {Username}", username);
throw;
// TODO keep track of error, see where to remove user if too much errors }
finally
return null; {
_statisticsHandler.CalledUserApi();
} }
// Expand URLs // Expand URLs
@ -73,5 +94,32 @@ namespace BirdsiteLive.Twitter
Protected = user.Protected Protected = user.Protected
}; };
} }
public bool IsUserApiRateLimited()
{
// Retrieve limit from tooling
_twitterAuthenticationInitializer.EnsureAuthenticationIsInitialized();
ExceptionHandler.SwallowWebExceptions = false;
RateLimit.RateLimitTrackerMode = RateLimitTrackerMode.TrackOnly;
try
{
var queryRateLimits = RateLimit.GetQueryRateLimit("https://api.twitter.com/1.1/users/show.json?screen_name=mastodon");
if (queryRateLimits != null)
{
return queryRateLimits.Remaining <= 0;
}
}
catch (Exception e)
{
_logger.LogError(e, "Error retrieving rate limits");
}
// Fallback
var currentCalls = _statisticsHandler.GetCurrentUserCalls();
var maxCalls = _statisticsHandler.GetStatistics().UserCallsMax;
return currentCalls >= maxCalls;
}
} }
} }

View file

@ -4,7 +4,7 @@
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<UserSecretsId>d21486de-a812-47eb-a419-05682bb68856</UserSecretsId> <UserSecretsId>d21486de-a812-47eb-a419-05682bb68856</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS> <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<Version>0.19.1</Version> <Version>0.20.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -14,6 +14,7 @@ using Newtonsoft.Json;
namespace BirdsiteLive.Controllers namespace BirdsiteLive.Controllers
{ {
#if DEBUG
public class DebugingController : Controller public class DebugingController : Controller
{ {
private readonly InstanceSettings _instanceSettings; private readonly InstanceSettings _instanceSettings;
@ -67,7 +68,7 @@ namespace BirdsiteLive.Controllers
var noteGuid = Guid.NewGuid(); var noteGuid = Guid.NewGuid();
var noteId = $"https://{_instanceSettings.Domain}/users/{username}/statuses/{noteGuid}"; var noteId = $"https://{_instanceSettings.Domain}/users/{username}/statuses/{noteGuid}";
var noteUrl = $"https://{_instanceSettings.Domain}/@{username}/{noteGuid}"; var noteUrl = $"https://{_instanceSettings.Domain}/@{username}/{noteGuid}";
var to = $"{actor}/followers"; var to = $"{actor}/followers";
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
@ -80,12 +81,12 @@ namespace BirdsiteLive.Controllers
type = "Create", type = "Create",
actor = actor, actor = actor,
published = nowString, published = nowString,
to = new []{ to }, to = new[] { to },
//cc = new [] { "https://www.w3.org/ns/activitystreams#Public" }, //cc = new [] { "https://www.w3.org/ns/activitystreams#Public" },
apObject = new Note() apObject = new Note()
{ {
id = noteId, id = noteId,
summary = null, summary = null,
inReplyTo = null, inReplyTo = null,
published = nowString, published = nowString,
url = noteUrl, url = noteUrl,
@ -93,7 +94,7 @@ namespace BirdsiteLive.Controllers
// Unlisted // Unlisted
to = new[] { to }, to = new[] { to },
cc = new [] { "https://www.w3.org/ns/activitystreams#Public" }, cc = new[] { "https://www.w3.org/ns/activitystreams#Public" },
//// Public //// Public
//to = new[] { "https://www.w3.org/ns/activitystreams#Public" }, //to = new[] { "https://www.w3.org/ns/activitystreams#Public" },
@ -125,6 +126,7 @@ namespace BirdsiteLive.Controllers
return View("Index"); return View("Index");
} }
} }
#endif
public static class HtmlHelperExtensions public static class HtmlHelperExtensions
{ {

View file

@ -3,6 +3,10 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using BirdsiteLive.ActivityPub;
using BirdsiteLive.ActivityPub.Models;
using BirdsiteLive.Domain;
using BirdsiteLive.Tools;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -13,11 +17,13 @@ namespace BirdsiteLive.Controllers
public class InboxController : ControllerBase public class InboxController : ControllerBase
{ {
private readonly ILogger<InboxController> _logger; private readonly ILogger<InboxController> _logger;
private readonly IUserService _userService;
#region Ctor #region Ctor
public InboxController(ILogger<InboxController> logger) public InboxController(ILogger<InboxController> logger, IUserService userService)
{ {
_logger = logger; _logger = logger;
_userService = userService;
} }
#endregion #endregion
@ -25,15 +31,32 @@ namespace BirdsiteLive.Controllers
[HttpPost] [HttpPost]
public async Task<IActionResult> Inbox() public async Task<IActionResult> Inbox()
{ {
var r = Request; try
using (var reader = new StreamReader(Request.Body))
{ {
var body = await reader.ReadToEndAsync(); var r = Request;
using (var reader = new StreamReader(Request.Body))
{
var body = await reader.ReadToEndAsync();
_logger.LogTrace("Inbox: {Body}", body); _logger.LogTrace("Inbox: {Body}", body);
//System.IO.File.WriteAllText($@"C:\apdebug\inbox\{Guid.NewGuid()}.json", body); //System.IO.File.WriteAllText($@"C:\apdebug\inbox\{Guid.NewGuid()}.json", body);
var activity = ApDeserializer.ProcessActivity(body);
var signature = r.Headers["Signature"].First();
switch (activity?.type)
{
case "Delete":
{
var succeeded = await _userService.DeleteRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityDelete, body);
if (succeeded) return Accepted();
else return Unauthorized();
}
}
}
} }
catch (FollowerIsGoneException) { } //TODO: check if user in DB
return Accepted(); return Accepted();
} }

View file

@ -13,6 +13,7 @@ using BirdsiteLive.Common.Regexes;
using BirdsiteLive.Common.Settings; using BirdsiteLive.Common.Settings;
using BirdsiteLive.Domain; using BirdsiteLive.Domain;
using BirdsiteLive.Models; using BirdsiteLive.Models;
using BirdsiteLive.Tools;
using BirdsiteLive.Twitter; using BirdsiteLive.Twitter;
using BirdsiteLive.Twitter.Models; using BirdsiteLive.Twitter.Models;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -65,11 +66,42 @@ namespace BirdsiteLive.Controllers
id = id.Trim(new[] { ' ', '@' }).ToLowerInvariant(); id = id.Trim(new[] { ' ', '@' }).ToLowerInvariant();
TwitterUser user = null;
var isSaturated = false;
var notFound = false;
// Ensure valid username // Ensure valid username
// https://help.twitter.com/en/managing-your-account/twitter-username-rules // https://help.twitter.com/en/managing-your-account/twitter-username-rules
TwitterUser user = null;
if (!string.IsNullOrWhiteSpace(id) && UserRegexes.TwitterAccount.IsMatch(id) && id.Length <= 15) if (!string.IsNullOrWhiteSpace(id) && UserRegexes.TwitterAccount.IsMatch(id) && id.Length <= 15)
user = _twitterUserService.GetUser(id); {
try
{
user = _twitterUserService.GetUser(id);
}
catch (UserNotFoundException)
{
notFound = true;
}
catch (UserHasBeenSuspendedException)
{
notFound = true;
}
catch (RateLimitExceededException)
{
isSaturated = true;
}
catch (Exception e)
{
_logger.LogError(e, "Exception getting {Id}", id);
throw;
}
}
else
{
notFound = true;
}
//var isSaturated = _twitterUserService.IsUserApiRateLimited();
var acceptHeaders = Request.Headers["Accept"]; var acceptHeaders = Request.Headers["Accept"];
if (acceptHeaders.Any()) if (acceptHeaders.Any())
@ -77,14 +109,16 @@ namespace BirdsiteLive.Controllers
var r = acceptHeaders.First(); var r = acceptHeaders.First();
if (r.Contains("application/activity+json")) if (r.Contains("application/activity+json"))
{ {
if (user == null) return NotFound(); if (isSaturated) return new ObjectResult("Too Many Requests") { StatusCode = 429 };
if (notFound) return NotFound();
var apUser = _userService.GetUser(user); var apUser = _userService.GetUser(user);
var jsonApUser = JsonConvert.SerializeObject(apUser); var jsonApUser = JsonConvert.SerializeObject(apUser);
return Content(jsonApUser, "application/activity+json; charset=utf-8"); return Content(jsonApUser, "application/activity+json; charset=utf-8");
} }
} }
if (user == null) return View("UserNotFound"); if (isSaturated) return View("ApiSaturated");
if (notFound) return View("UserNotFound");
var displayableUser = new DisplayTwitterUser var displayableUser = new DisplayTwitterUser
{ {
@ -133,40 +167,69 @@ namespace BirdsiteLive.Controllers
[HttpPost] [HttpPost]
public async Task<IActionResult> Inbox() public async Task<IActionResult> Inbox()
{ {
var r = Request; try
using (var reader = new StreamReader(Request.Body))
{ {
var body = await reader.ReadToEndAsync(); var r = Request;
using (var reader = new StreamReader(Request.Body))
_logger.LogTrace("User Inbox: {Body}", body);
//System.IO.File.WriteAllText($@"C:\apdebug\{Guid.NewGuid()}.json", body);
var activity = ApDeserializer.ProcessActivity(body);
// Do something
var signature = r.Headers["Signature"].First();
switch (activity?.type)
{ {
case "Follow": var body = await reader.ReadToEndAsync();
_logger.LogTrace("User Inbox: {Body}", body);
//System.IO.File.WriteAllText($@"C:\apdebug\{Guid.NewGuid()}.json", body);
var activity = ApDeserializer.ProcessActivity(body);
var signature = r.Headers["Signature"].First();
switch (activity?.type)
{
case "Follow":
{ {
var succeeded = await _userService.FollowRequestedAsync(signature, r.Method, r.Path, var succeeded = await _userService.FollowRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityFollow, body); r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers),
activity as ActivityFollow, body);
if (succeeded) return Accepted(); if (succeeded) return Accepted();
else return Unauthorized(); else return Unauthorized();
} }
case "Undo": case "Undo":
if (activity is ActivityUndoFollow) if (activity is ActivityUndoFollow)
{
var succeeded = await _userService.UndoFollowRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers),
activity as ActivityUndoFollow, body);
if (succeeded) return Accepted();
else return Unauthorized();
}
return Accepted();
case "Delete":
{ {
var succeeded = await _userService.UndoFollowRequestedAsync(signature, r.Method, r.Path, var succeeded = await _userService.DeleteRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), RequestHeaders(r.Headers), activity as ActivityUndoFollow, body); r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers),
activity as ActivityDelete, body);
if (succeeded) return Accepted(); if (succeeded) return Accepted();
else return Unauthorized(); else return Unauthorized();
} }
return Accepted(); default:
default: return Accepted();
return Accepted(); }
} }
} }
catch (FollowerIsGoneException) //TODO: check if user in DB
{
return Accepted();
}
catch (UserNotFoundException)
{
return NotFound();
}
catch (UserHasBeenSuspendedException)
{
return NotFound();
}
catch (RateLimitExceededException)
{
return new ObjectResult("Too Many Requests") { StatusCode = 429 };
}
} }
[Route("/users/{id}/followers")] [Route("/users/{id}/followers")]
@ -183,10 +246,5 @@ namespace BirdsiteLive.Controllers
var jsonApUser = JsonConvert.SerializeObject(followers); var jsonApUser = JsonConvert.SerializeObject(followers);
return Content(jsonApUser, "application/activity+json; charset=utf-8"); return Content(jsonApUser, "application/activity+json; charset=utf-8");
} }
private Dictionary<string, string> RequestHeaders(IHeaderDictionary header)
{
return header.ToDictionary<KeyValuePair<string, StringValues>, string, string>(h => h.Key.ToLowerInvariant(), h => h.Value);
}
} }
} }

View file

@ -12,6 +12,7 @@ using BirdsiteLive.Models;
using BirdsiteLive.Models.WellKnownModels; using BirdsiteLive.Models.WellKnownModels;
using BirdsiteLive.Twitter; using BirdsiteLive.Twitter;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace BirdsiteLive.Controllers namespace BirdsiteLive.Controllers
@ -23,13 +24,15 @@ namespace BirdsiteLive.Controllers
private readonly ITwitterUserService _twitterUserService; private readonly ITwitterUserService _twitterUserService;
private readonly ITwitterUserDal _twitterUserDal; private readonly ITwitterUserDal _twitterUserDal;
private readonly InstanceSettings _settings; private readonly InstanceSettings _settings;
private readonly ILogger<WellKnownController> _logger;
#region Ctor #region Ctor
public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository) public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository, ILogger<WellKnownController> logger)
{ {
_twitterUserService = twitterUserService; _twitterUserService = twitterUserService;
_twitterUserDal = twitterUserDal; _twitterUserDal = twitterUserDal;
_moderationRepository = moderationRepository; _moderationRepository = moderationRepository;
_logger = logger;
_settings = settings; _settings = settings;
} }
#endregion #endregion
@ -141,30 +144,54 @@ namespace BirdsiteLive.Controllers
[Route("/.well-known/webfinger")] [Route("/.well-known/webfinger")]
public IActionResult Webfinger(string resource = null) public IActionResult Webfinger(string resource = null)
{ {
var acct = resource.Split("acct:")[1].Trim(); if (string.IsNullOrWhiteSpace(resource))
return BadRequest();
string name = null; string name = null;
string domain = null; string domain = null;
var splitAcct = acct.Split('@', StringSplitOptions.RemoveEmptyEntries); if (resource.StartsWith("acct:"))
{
var acct = resource.Split("acct:")[1].Trim();
var splitAcct = acct.Split('@', StringSplitOptions.RemoveEmptyEntries);
var atCount = acct.Count(x => x == '@'); var atCount = acct.Count(x => x == '@');
if (atCount == 1 && acct.StartsWith('@')) if (atCount == 1 && acct.StartsWith('@'))
{ {
name = splitAcct[1]; name = splitAcct[1];
}
else if (atCount == 1 || atCount == 2)
{
name = splitAcct[0];
domain = splitAcct[1];
}
else
{
return BadRequest();
}
} }
else if (atCount == 1 || atCount == 2) else if (resource.StartsWith("https://"))
{ {
name = splitAcct[0]; try
domain = splitAcct[1]; {
name = resource.Split('/').Last().Trim();
domain = resource.Split("https://", StringSplitOptions.RemoveEmptyEntries)[0].Split('/')[0].Trim();
}
catch (Exception e)
{
_logger.LogError(e, "Error parsing {Resource}", resource);
throw new NotImplementedException();
}
} }
else else
{ {
return BadRequest(); _logger.LogError("Error parsing {Resource}", resource);
throw new NotImplementedException();
} }
// Ensure lowercase // Ensure lowercase
name = name.ToLowerInvariant(); name = name.ToLowerInvariant();
domain = domain?.ToLowerInvariant();
// Ensure valid username // Ensure valid username
// https://help.twitter.com/en/managing-your-account/twitter-username-rules // https://help.twitter.com/en/managing-your-account/twitter-username-rules
@ -174,9 +201,27 @@ namespace BirdsiteLive.Controllers
if (!string.IsNullOrWhiteSpace(domain) && domain != _settings.Domain) if (!string.IsNullOrWhiteSpace(domain) && domain != _settings.Domain)
return NotFound(); return NotFound();
var user = _twitterUserService.GetUser(name); try
if (user == null) {
_twitterUserService.GetUser(name);
}
catch (UserNotFoundException)
{
return NotFound(); return NotFound();
}
catch (UserHasBeenSuspendedException)
{
return NotFound();
}
catch (RateLimitExceededException)
{
return new ObjectResult("Too Many Requests") { StatusCode = 429 };
}
catch (Exception e)
{
_logger.LogError(e, "Exception getting {Name}", name);
throw;
}
var actorUrl = UrlFactory.GetActorUrl(_settings.Domain, name); var actorUrl = UrlFactory.GetActorUrl(_settings.Domain, name);

View file

@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
namespace BirdsiteLive.Tools
{
public class HeaderHandler
{
public static Dictionary<string, string> RequestHeaders(IHeaderDictionary header)
{
return header.ToDictionary<KeyValuePair<string, StringValues>, string, string>(h => h.Key.ToLowerInvariant(), h => h.Value);
}
}
}

View file

@ -0,0 +1,13 @@
@using BirdsiteLive.Controllers;
@{
ViewData["Title"] = "Api Saturated";
}
<div class="text-center">
<h1 class="display-4">429 Too Many Requests</h1>
<p>
<br />
The API is saturated.<br/>
Please consider using another instance.
</p>
</div>

View file

@ -23,7 +23,9 @@
"MaxUsersCapacity": 1000, "MaxUsersCapacity": 1000,
"UnlistedTwitterAccounts": null, "UnlistedTwitterAccounts": null,
"SensitiveTwitterAccounts": null, "SensitiveTwitterAccounts": null,
"FailingTwitterUserCleanUpThreshold": 700 "FailingTwitterUserCleanUpThreshold": 700,
"FailingFollowerCleanUpThreshold": 30000,
"UserCacheCapacity": 10000
}, },
"Db": { "Db": {
"Type": "postgres", "Type": "postgres",

View file

@ -23,7 +23,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
public class DbInitializerPostgresDal : PostgresBase, IDbInitializerDal public class DbInitializerPostgresDal : PostgresBase, IDbInitializerDal
{ {
private readonly PostgresTools _tools; private readonly PostgresTools _tools;
private readonly Version _currentVersion = new Version(2, 3); private readonly Version _currentVersion = new Version(2, 4);
private const string DbVersionType = "db-version"; private const string DbVersionType = "db-version";
#region Ctor #region Ctor
@ -134,7 +134,8 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
new Tuple<Version, Version>(new Version(1,0), new Version(2,0)), new Tuple<Version, Version>(new Version(1,0), new Version(2,0)),
new Tuple<Version, Version>(new Version(2,0), new Version(2,1)), new Tuple<Version, Version>(new Version(2,0), new Version(2,1)),
new Tuple<Version, Version>(new Version(2,1), new Version(2,2)), new Tuple<Version, Version>(new Version(2,1), new Version(2,2)),
new Tuple<Version, Version>(new Version(2,2), new Version(2,3)) new Tuple<Version, Version>(new Version(2,2), new Version(2,3)),
new Tuple<Version, Version>(new Version(2,3), new Version(2,4))
}; };
} }
@ -163,6 +164,14 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
var addPostingError = $@"ALTER TABLE {_settings.FollowersTableName} ADD postingErrorCount SMALLINT"; var addPostingError = $@"ALTER TABLE {_settings.FollowersTableName} ADD postingErrorCount SMALLINT";
await _tools.ExecuteRequestAsync(addPostingError); await _tools.ExecuteRequestAsync(addPostingError);
} }
else if (from == new Version(2, 3) && to == new Version(2, 4))
{
var alterLastSync = $@"ALTER TABLE {_settings.TwitterUserTableName} ALTER COLUMN fetchingErrorCount TYPE INTEGER";
await _tools.ExecuteRequestAsync(alterLastSync);
var alterPostingError = $@"ALTER TABLE {_settings.FollowersTableName} ALTER COLUMN postingErrorCount TYPE INTEGER";
await _tools.ExecuteRequestAsync(alterPostingError);
}
else else
{ {
throw new NotImplementedException(); throw new NotImplementedException();

View file

@ -1,4 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting; using BirdsiteLive.ActivityPub.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace BirdsiteLive.ActivityPub.Tests namespace BirdsiteLive.ActivityPub.Tests
@ -48,6 +49,20 @@ namespace BirdsiteLive.ActivityPub.Tests
Assert.AreEqual("https://mamot.fr/users/testtest", data.apObject.apObject); Assert.AreEqual("https://mamot.fr/users/testtest", data.apObject.apObject);
} }
[TestMethod]
public void DeleteDeserializationTest()
{
var json =
"{\"@context\": \"https://www.w3.org/ns/activitystreams\", \"id\": \"https://mastodon.technology/users/deleteduser#delete\", \"type\": \"Delete\", \"actor\": \"https://mastodon.technology/users/deleteduser\", \"to\": [\"https://www.w3.org/ns/activitystreams#Public\"],\"object\": \"https://mastodon.technology/users/deleteduser\",\"signature\": {\"type\": \"RsaSignature2017\",\"creator\": \"https://mastodon.technology/users/deleteduser#main-key\",\"created\": \"2020-11-19T22:43:01Z\",\"signatureValue\": \"peksQao4v5N+sMZgHXZ6xZnGaZrd0s+LqZimu63cnp7O5NBJM6gY9AAu/vKUgrh4C50r66f9OQdHg5yChQhc4ViE+yLR/3/e59YQimelmXJPpcC99Nt0YLU/iTRLsBehY3cDdC6+ogJKgpkToQvB6tG2KrPdrkreYh4Il4eXLKMfiQhgdKluOvenLnl2erPWfE02hIu/jpuljyxSuvJunMdU4yQVSZHTtk/I8q3jjzIzhgyb7ICWU5Hkx0H/47Q24ztsvOgiTWNgO+v6l9vA7qIhztENiRPhzGP5RCCzUKRAe6bcSu1Wfa3NKWqB9BeJ7s+2y2bD7ubPbiEE1MQV7Q==\"}}";
var data = ApDeserializer.ProcessActivity(json) as ActivityDelete;
Assert.AreEqual("https://mastodon.technology/users/deleteduser#delete", data.id);
Assert.AreEqual("Delete", data.type);
Assert.AreEqual("https://mastodon.technology/users/deleteduser", data.actor);
Assert.AreEqual("https://mastodon.technology/users/deleteduser", data.apObject);
}
//[TestMethod] //[TestMethod]
//public void NoteDeserializationTest() //public void NoteDeserializationTest()
//{ //{

View file

@ -340,6 +340,48 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.AreEqual(10, result.PostingErrorCount); Assert.AreEqual(10, result.PostingErrorCount);
} }
[TestMethod]
public async Task CreateUpdateAndGetFollower_Integer()
{
var acct = "myhandle";
var host = "domain.ext";
var following = new[] { 12, 19, 23 };
var followingSync = new Dictionary<int, long>()
{
{12, 165L},
{19, 166L},
{23, 167L}
};
var inboxRoute = "/myhandle/inbox";
var sharedInboxRoute = "/inbox";
var actorId = $"https://{host}/{acct}";
var dal = new FollowersPostgresDal(_settings);
await dal.CreateFollowerAsync(acct, host, inboxRoute, sharedInboxRoute, actorId, following, followingSync);
var result = await dal.GetFollowerAsync(acct, host);
var updatedFollowing = new List<int> { 12, 19, 23, 24 };
var updatedFollowingSync = new Dictionary<int, long>(){
{12, 170L},
{19, 171L},
{23, 172L},
{24, 173L}
};
result.Followings = updatedFollowing.ToList();
result.FollowingsSyncStatus = updatedFollowingSync;
result.PostingErrorCount = 32768;
await dal.UpdateFollowerAsync(result);
result = await dal.GetFollowerAsync(acct, host);
Assert.AreEqual(updatedFollowing.Count, result.Followings.Count);
Assert.AreEqual(updatedFollowing[0], result.Followings[0]);
Assert.AreEqual(updatedFollowingSync.Count, result.FollowingsSyncStatus.Count);
Assert.AreEqual(updatedFollowingSync.First().Key, result.FollowingsSyncStatus.First().Key);
Assert.AreEqual(updatedFollowingSync.First().Value, result.FollowingsSyncStatus.First().Value);
Assert.AreEqual(32768, result.PostingErrorCount);
}
[TestMethod] [TestMethod]
public async Task CreateUpdateAndGetFollower_Remove() public async Task CreateUpdateAndGetFollower_Remove()
{ {

View file

@ -130,6 +130,38 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100); Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
} }
[TestMethod]
public async Task CreateUpdate3AndGetUser()
{
var acct = "myid";
var lastTweetId = 1548L;
var dal = new TwitterUserPostgresDal(_settings);
await dal.CreateTwitterUserAsync(acct, lastTweetId);
var result = await dal.GetTwitterUserAsync(acct);
var updatedLastTweetId = 1600L;
var updatedLastSyncId = 1550L;
var now = DateTime.Now;
var errors = 32768;
result.LastTweetPostedId = updatedLastTweetId;
result.LastTweetSynchronizedForAllFollowersId = updatedLastSyncId;
result.FetchingErrorCount = errors;
result.LastSync = now;
await dal.UpdateTwitterUserAsync(result);
result = await dal.GetTwitterUserAsync(acct);
Assert.AreEqual(acct, result.Acct);
Assert.AreEqual(updatedLastTweetId, result.LastTweetPostedId);
Assert.AreEqual(updatedLastSyncId, result.LastTweetSynchronizedForAllFollowersId);
Assert.AreEqual(errors, result.FetchingErrorCount);
Assert.IsTrue(Math.Abs((now.ToUniversalTime() - result.LastSync).Milliseconds) < 100);
}
[TestMethod] [TestMethod]
[ExpectedException(typeof(ArgumentException))] [ExpectedException(typeof(ArgumentException))]
public async Task Update_NoId() public async Task Update_NoId()

View file

@ -0,0 +1,97 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain.BusinessUseCases;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace BirdsiteLive.Domain.Tests.BusinessUseCases
{
[TestClass]
public class ProcessDeleteUserTests
{
[TestMethod]
public async Task ExecuteAsync_NoMoreFollowings()
{
#region Stubs
var follower = new Follower
{
Id = 12,
Followings = new List<int> { 1 }
};
#endregion
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetFollowersAsync(
It.Is<int>(y => y == 1)))
.ReturnsAsync(new[] { follower });
followersDalMock
.Setup(x => x.DeleteFollowerAsync(
It.Is<int>(y => y == 12)))
.Returns(Task.CompletedTask);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.DeleteTwitterUserAsync(
It.Is<int>(y => y == 1)))
.Returns(Task.CompletedTask);
#endregion
var action = new ProcessDeleteUser(followersDalMock.Object, twitterUserDalMock.Object);
await action.ExecuteAsync(follower);
#region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ExecuteAsync_HaveFollowings()
{
#region Stubs
var follower = new Follower
{
Id = 12,
Followings = new List<int> { 1 }
};
var followers = new List<Follower>
{
follower,
new Follower
{
Id = 11
}
};
#endregion
#region Mocks
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
followersDalMock
.Setup(x => x.GetFollowersAsync(
It.Is<int>(y => y == 1)))
.ReturnsAsync(followers.ToArray());
followersDalMock
.Setup(x => x.DeleteFollowerAsync(
It.Is<int>(y => y == 12)))
.Returns(Task.CompletedTask);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
#endregion
var action = new ProcessDeleteUser(followersDalMock.Object, twitterUserDalMock.Object);
await action.ExecuteAsync(follower);
#region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
#endregion
}
}
}

View file

@ -2,6 +2,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using BirdsiteLive.DAL.Contracts; using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models; using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain.BusinessUseCases;
using BirdsiteLive.Moderation.Actions; using BirdsiteLive.Moderation.Actions;
using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq; using Moq;
@ -29,31 +30,19 @@ namespace BirdsiteLive.Moderation.Tests.Actions
It.Is<Follower>(y => y.Id == follower.Id))) It.Is<Follower>(y => y.Id == follower.Id)))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict); var processDeleteUserMock = new Mock<IProcessDeleteUser>(MockBehavior.Strict);
followersDalMock processDeleteUserMock
.Setup(x => x.GetFollowersAsync( .Setup(x => x.ExecuteAsync(
It.Is<int>(y => y == 1))) It.Is<Follower>(y => y.Id == follower.Id)))
.ReturnsAsync(new[] {follower});
followersDalMock
.Setup(x => x.DeleteFollowerAsync(
It.Is<int>(y => y == 12)))
.Returns(Task.CompletedTask);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.DeleteTwitterUserAsync(
It.Is<int>(y => y == 1)))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
#endregion #endregion
var action = new RemoveFollowerAction(followersDalMock.Object, twitterUserDalMock.Object, rejectAllFollowingsActionMock.Object); var action = new RemoveFollowerAction(rejectAllFollowingsActionMock.Object, processDeleteUserMock.Object);
await action.ProcessAsync(follower); await action.ProcessAsync(follower);
#region Validations #region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
rejectAllFollowingsActionMock.VerifyAll(); rejectAllFollowingsActionMock.VerifyAll();
processDeleteUserMock.VerifyAll();
#endregion #endregion
} }
@ -66,15 +55,6 @@ namespace BirdsiteLive.Moderation.Tests.Actions
Id = 12, Id = 12,
Followings = new List<int> { 1 } Followings = new List<int> { 1 }
}; };
var followers = new List<Follower>
{
follower,
new Follower
{
Id = 11
}
};
#endregion #endregion
#region Mocks #region Mocks
@ -84,27 +64,19 @@ namespace BirdsiteLive.Moderation.Tests.Actions
It.Is<Follower>(y => y.Id == follower.Id))) It.Is<Follower>(y => y.Id == follower.Id)))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict); var processDeleteUserMock = new Mock<IProcessDeleteUser>(MockBehavior.Strict);
followersDalMock processDeleteUserMock
.Setup(x => x.GetFollowersAsync( .Setup(x => x.ExecuteAsync(
It.Is<int>(y => y == 1))) It.Is<Follower>(y => y.Id == follower.Id)))
.ReturnsAsync(followers.ToArray());
followersDalMock
.Setup(x => x.DeleteFollowerAsync(
It.Is<int>(y => y == 12)))
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
#endregion #endregion
var action = new RemoveFollowerAction(followersDalMock.Object, twitterUserDalMock.Object, rejectAllFollowingsActionMock.Object); var action = new RemoveFollowerAction(rejectAllFollowingsActionMock.Object, processDeleteUserMock.Object);
await action.ProcessAsync(follower); await action.ProcessAsync(follower);
#region Validations #region Validations
followersDalMock.VerifyAll();
twitterUserDalMock.VerifyAll();
rejectAllFollowingsActionMock.VerifyAll(); rejectAllFollowingsActionMock.VerifyAll();
processDeleteUserMock.VerifyAll();
#endregion #endregion
} }
} }

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -159,11 +160,136 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
twitterUserServiceMock twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2))) .Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Returns((TwitterUser) null); .Throws(new UserNotFoundException());
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
removeTwitterAccountActionMock
.Setup(x => x.ProcessAsync(It.Is<SyncTwitterUser>(y => y.Acct == acct2)))
.Returns(Task.CompletedTask);
#endregion
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
twitterUserServiceMock.VerifyAll();
twitterUserDalMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_Suspended_Test()
{
#region Stubs
var userId1 = 1;
var acct1 = "user1";
var userId2 = 2;
var acct2 = "user2";
var users = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Id = userId1,
Acct = acct1
},
new SyncTwitterUser
{
Id = userId2,
Acct = acct2
}
};
var settings = new InstanceSettings
{
FailingTwitterUserCleanUpThreshold = 300
};
#endregion
#region Mocks
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns(new TwitterUser
{
Protected = false
});
twitterUserServiceMock twitterUserServiceMock
.Setup(x => x.PurgeUser(It.Is<string>(y => y == acct2))); .Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Throws(new UserHasBeenSuspendedException());
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
removeTwitterAccountActionMock
.Setup(x => x.ProcessAsync(It.Is<SyncTwitterUser>(y => y.Acct == acct2)))
.Returns(Task.CompletedTask);
#endregion
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
twitterUserServiceMock.VerifyAll();
twitterUserDalMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_Exception_Test()
{
#region Stubs
var userId1 = 1;
var acct1 = "user1";
var userId2 = 2;
var acct2 = "user2";
var users = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Id = userId1,
Acct = acct1
},
new SyncTwitterUser
{
Id = userId2,
Acct = acct2
}
};
var settings = new InstanceSettings
{
FailingTwitterUserCleanUpThreshold = 300
};
#endregion
#region Mocks
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns(new TwitterUser
{
Protected = false
});
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Throws(new Exception());
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict); var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2))) .Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
@ -194,7 +320,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
} }
[TestMethod] [TestMethod]
public async Task ProcessAsync_Unfound_OverThreshold_Test() public async Task ProcessAsync_Error_Test()
{ {
#region Stubs #region Stubs
var userId1 = 1; var userId1 = 1;
@ -235,10 +361,79 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
twitterUserServiceMock twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2))) .Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Returns((TwitterUser)null); .Returns((TwitterUser)null);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
.ReturnsAsync(new SyncTwitterUser
{
Id = userId2,
FetchingErrorCount = 0
});
twitterUserDalMock
.Setup(x => x.UpdateTwitterUserAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2 && y.FetchingErrorCount == 1)))
.Returns(Task.CompletedTask);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
twitterUserServiceMock.VerifyAll();
twitterUserDalMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_Error_OverThreshold_Test()
{
#region Stubs
var userId1 = 1;
var acct1 = "user1";
var userId2 = 2;
var acct2 = "user2";
var users = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Id = userId1,
Acct = acct1
},
new SyncTwitterUser
{
Id = userId2,
Acct = acct2
}
};
var settings = new InstanceSettings
{
FailingTwitterUserCleanUpThreshold = 300
};
#endregion
#region Mocks
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns(new TwitterUser
{
Protected = false
});
twitterUserServiceMock twitterUserServiceMock
.Setup(x => x.PurgeUser(It.Is<string>(y => y == acct2))); .Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Returns((TwitterUser)null);
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict); var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2))) .Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
@ -312,8 +507,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
{ {
Protected = true Protected = true
}); });
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict); var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
.ReturnsAsync(new SyncTwitterUser
{
Id = userId2,
FetchingErrorCount = 0
});
twitterUserDalMock
.Setup(x => x.UpdateTwitterUserAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2 && y.FetchingErrorCount == 1)))
.Returns(Task.CompletedTask);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict); var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion #endregion
@ -331,7 +538,81 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
} }
[TestMethod] [TestMethod]
public async Task ProcessAsync_Unfound_NotInit_Test() public async Task ProcessAsync_Protected_OverThreshold_Test()
{
#region Stubs
var userId1 = 1;
var acct1 = "user1";
var userId2 = 2;
var acct2 = "user2";
var users = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Id = userId1,
Acct = acct1
},
new SyncTwitterUser
{
Id = userId2,
Acct = acct2
}
};
var settings = new InstanceSettings
{
FailingTwitterUserCleanUpThreshold = 300
};
#endregion
#region Mocks
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns(new TwitterUser
{
Protected = false
});
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Returns(new TwitterUser
{
Protected = true
});
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
.ReturnsAsync(new SyncTwitterUser
{
Id = userId2,
FetchingErrorCount = 500
});
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
removeTwitterAccountActionMock
.Setup(x => x.ProcessAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2)))
.Returns(Task.CompletedTask);
#endregion
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
twitterUserServiceMock.VerifyAll();
twitterUserDalMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_Error_NotInit_Test()
{ {
#region Stubs #region Stubs
var userId1 = 1; var userId1 = 1;
@ -361,9 +642,6 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1))) .Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns((TwitterUser)null); .Returns((TwitterUser)null);
twitterUserServiceMock
.Setup(x => x.PurgeUser(It.Is<string>(y => y == acct1)));
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict); var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct1))) .Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct1)))
@ -388,5 +666,77 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
removeTwitterAccountActionMock.VerifyAll(); removeTwitterAccountActionMock.VerifyAll();
#endregion #endregion
} }
[TestMethod]
public async Task ProcessAsync_RateLimited_Test()
{
#region Stubs
var userId1 = 1;
var acct1 = "user1";
var userId2 = 2;
var acct2 = "user2";
var users = new List<SyncTwitterUser>
{
new SyncTwitterUser
{
Id = userId1,
Acct = acct1
},
new SyncTwitterUser
{
Id = userId2,
Acct = acct2
}
};
var settings = new InstanceSettings
{
FailingTwitterUserCleanUpThreshold = 300
};
#endregion
#region Mocks
var twitterUserServiceMock = new Mock<ICachedTwitterUserService>(MockBehavior.Strict);
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns(new TwitterUser
{
Protected = false,
});
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.Throws(new RateLimitExceededException());
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
.ReturnsAsync(new SyncTwitterUser
{
Id = userId2,
FetchingErrorCount = 20
});
twitterUserDalMock
.Setup(x => x.UpdateTwitterUserAsync(It.Is<SyncTwitterUser>(y => y.Id == userId2 && y.FetchingErrorCount == 20)))
.Returns(Task.CompletedTask);
var removeTwitterAccountActionMock = new Mock<IRemoveTwitterAccountAction>(MockBehavior.Strict);
#endregion
var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings);
var result = await processor.ProcessAsync(users.ToArray(), CancellationToken.None);
#region Validations
Assert.AreEqual(1, result.Length);
Assert.IsTrue(result.Any(x => x.User.Id == userId1));
twitterUserServiceMock.VerifyAll();
twitterUserDalMock.VerifyAll();
removeTwitterAccountActionMock.VerifyAll();
#endregion
}
} }
} }

View file

@ -2,8 +2,10 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Contracts; using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.DAL.Models; using BirdsiteLive.DAL.Models;
using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Pipeline.Models; using BirdsiteLive.Pipeline.Models;
using BirdsiteLive.Pipeline.Processors; using BirdsiteLive.Pipeline.Processors;
using BirdsiteLive.Pipeline.Processors.SubTasks; using BirdsiteLive.Pipeline.Processors.SubTasks;
@ -72,17 +74,22 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict); var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>(); var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion #endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations #region Validations
sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll(); followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion #endregion
} }
@ -147,15 +154,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict); var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>(); var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion #endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations #region Validations
sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll(); followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion #endregion
} }
@ -229,15 +241,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>(); var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion #endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations #region Validations
sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll(); followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion #endregion
} }
@ -312,15 +329,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>(); var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion #endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations #region Validations
sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll(); followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion #endregion
} }
@ -400,15 +422,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>(); var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion #endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations #region Validations
sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll(); followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion #endregion
} }
@ -471,15 +498,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict); var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>(); var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion #endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations #region Validations
sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll(); followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion #endregion
} }
@ -543,15 +575,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict); var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>(); var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion #endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations #region Validations
sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll(); followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion #endregion
} }
@ -623,15 +660,196 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>(); var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion #endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations #region Validations
sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll(); followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_MultiInstances_Inbox_OneTweet_Error_SettingsThreshold_Test()
{
#region Stubs
var tweetId = 1;
var host1 = "domain1.ext";
var host2 = "domain2.ext";
var inbox = "/user/inbox";
var userId1 = 2;
var userId2 = 3;
var userAcct = "user";
var userWithTweets = new UserWithDataToSync()
{
Tweets = new[]
{
new ExtractedTweet
{
Id = tweetId
}
},
User = new SyncTwitterUser
{
Acct = userAcct
},
Followers = new[]
{
new Follower
{
Id = userId1,
Host = host1,
InboxRoute = inbox
},
new Follower
{
Id = userId2,
Host = host2,
InboxRoute = inbox,
PostingErrorCount = 42
},
}
};
#endregion
#region Mocks
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
sendTweetsToInboxTaskMock
.Setup(x => x.ExecuteAsync(
It.Is<ExtractedTweet[]>(y => y.Length == 1),
It.Is<Follower>(y => y.Id == userId1),
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
.Returns(Task.CompletedTask);
sendTweetsToInboxTaskMock
.Setup(x => x.ExecuteAsync(
It.Is<ExtractedTweet[]>(y => y.Length == 1),
It.Is<Follower>(y => y.Id == userId2),
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
.Throws(new Exception());
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings
{
FailingFollowerCleanUpThreshold = 10
};
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
removeFollowerMock
.Setup(x => x.ProcessAsync(It.Is<Follower>(y => y.Id == userId2)))
.Returns(Task.CompletedTask);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion
}
[TestMethod]
public async Task ProcessAsync_MultiInstances_Inbox_OneTweet_Error_MaxThreshold_Test()
{
#region Stubs
var tweetId = 1;
var host1 = "domain1.ext";
var host2 = "domain2.ext";
var inbox = "/user/inbox";
var userId1 = 2;
var userId2 = 3;
var userAcct = "user";
var userWithTweets = new UserWithDataToSync()
{
Tweets = new[]
{
new ExtractedTweet
{
Id = tweetId
}
},
User = new SyncTwitterUser
{
Acct = userAcct
},
Followers = new[]
{
new Follower
{
Id = userId1,
Host = host1,
InboxRoute = inbox
},
new Follower
{
Id = userId2,
Host = host2,
InboxRoute = inbox,
PostingErrorCount = 2147483600
},
}
};
#endregion
#region Mocks
var sendTweetsToInboxTaskMock = new Mock<ISendTweetsToInboxTask>(MockBehavior.Strict);
sendTweetsToInboxTaskMock
.Setup(x => x.ExecuteAsync(
It.Is<ExtractedTweet[]>(y => y.Length == 1),
It.Is<Follower>(y => y.Id == userId1),
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
.Returns(Task.CompletedTask);
sendTweetsToInboxTaskMock
.Setup(x => x.ExecuteAsync(
It.Is<ExtractedTweet[]>(y => y.Length == 1),
It.Is<Follower>(y => y.Id == userId2),
It.Is<SyncTwitterUser>(y => y.Acct == userAcct)))
.Throws(new Exception());
var sendTweetsToSharedInboxTaskMock = new Mock<ISendTweetsToSharedInboxTask>(MockBehavior.Strict);
var followersDalMock = new Mock<IFollowersDal>(MockBehavior.Strict);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings
{
FailingFollowerCleanUpThreshold = 0
};
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
removeFollowerMock
.Setup(x => x.ProcessAsync(It.Is<Follower>(y => y.Id == userId2)))
.Returns(Task.CompletedTask);
#endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations
sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion #endregion
} }
@ -704,15 +922,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>(); var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion #endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations #region Validations
sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll(); followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion #endregion
} }
@ -790,15 +1013,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Returns(Task.CompletedTask); .Returns(Task.CompletedTask);
var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>(); var loggerMock = new Mock<ILogger<SendTweetsToFollowersProcessor>>();
var settings = new InstanceSettings();
var removeFollowerMock = new Mock<IRemoveFollowerAction>(MockBehavior.Strict);
#endregion #endregion
var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object, settings, removeFollowerMock.Object);
var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None);
#region Validations #region Validations
sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToInboxTaskMock.VerifyAll();
sendTweetsToSharedInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll();
followersDalMock.VerifyAll(); followersDalMock.VerifyAll();
removeFollowerMock.VerifyAll();
#endregion #endregion
} }
} }