Merge pull request #134 from NicolasConstant/topic_add-proper-exceptions

Topic add proper exceptions handling
This commit is contained in:
Nicolas Constant 2022-02-07 19:45:49 -05:00 committed by GitHub
commit b0e7601333
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 576 additions and 83 deletions

View file

@ -9,6 +9,7 @@ using BirdsiteLive.Moderation.Actions;
using BirdsiteLive.Pipeline.Contracts;
using BirdsiteLive.Pipeline.Models;
using BirdsiteLive.Twitter;
using BirdsiteLive.Twitter.Models;
namespace BirdsiteLive.Pipeline.Processors
{
@ -35,26 +36,61 @@ namespace BirdsiteLive.Pipeline.Processors
foreach (var user in syncTwitterUsers)
{
var userView = _twitterUserService.GetUser(user.Acct);
if (userView == null)
{
await AnalyseFailingUserAsync(user);
}
else if (!userView.Protected)
{
user.FetchingErrorCount = 0;
var userWtData = new UserWithDataToSync
{
User = user
};
usersWtData.Add(userWtData);
}
}
TwitterUser userView = null;
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();
}
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);
dbUser.FetchingErrorCount++;
@ -68,9 +104,6 @@ namespace BirdsiteLive.Pipeline.Processors
{
await _twitterUserDal.UpdateTwitterUserAsync(dbUser);
}
// Purge
_twitterUserService.PurgeUser(user.Acct);
}
}
}

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

@ -6,6 +6,7 @@ using BirdsiteLive.Twitter.Models;
using BirdsiteLive.Twitter.Tools;
using Microsoft.Extensions.Logging;
using Tweetinvi;
using Tweetinvi.Exceptions;
using Tweetinvi.Models;
namespace BirdsiteLive.Twitter
@ -45,17 +46,34 @@ namespace BirdsiteLive.Twitter
try
{
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);
return null;
throw new UserHasBeenSuspendedException();
}
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)
{
_logger.LogError(e, "Error retrieving user {Username}", username);
return null;
throw;
}
finally
{
_statisticsHandler.CalledUserApi();
}
// Expand URLs

View file

@ -66,13 +66,42 @@ namespace BirdsiteLive.Controllers
id = id.Trim(new[] { ' ', '@' }).ToLowerInvariant();
TwitterUser user = null;
var isSaturated = false;
var notFound = false;
// Ensure valid username
// 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)
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 isSaturated = _twitterUserService.IsUserApiRateLimited();
var acceptHeaders = Request.Headers["Accept"];
if (acceptHeaders.Any())
@ -80,17 +109,17 @@ namespace BirdsiteLive.Controllers
var r = acceptHeaders.First();
if (r.Contains("application/activity+json"))
{
if (user == null && isSaturated) return new ObjectResult("Too Many Requests") { StatusCode = 429 };
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 jsonApUser = JsonConvert.SerializeObject(apUser);
return Content(jsonApUser, "application/activity+json; charset=utf-8");
}
}
if (user == null && isSaturated) return View("ApiSaturated");
if (user == null) return View("UserNotFound");
if (isSaturated) return View("ApiSaturated");
if (notFound) return View("UserNotFound");
var displayableUser = new DisplayTwitterUser
{
Name = user.Name,
@ -138,46 +167,61 @@ namespace BirdsiteLive.Controllers
[HttpPost]
public async Task<IActionResult> Inbox()
{
var r = Request;
using (var reader = new StreamReader(Request.Body))
try
{
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)
var r = Request;
using (var reader = new StreamReader(Request.Body))
{
case "Follow":
{
var succeeded = await _userService.FollowRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityFollow, body);
if (succeeded) return Accepted();
else return Unauthorized();
}
case "Undo":
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.DeleteRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityDelete, body);
if (succeeded) return Accepted();
else return Unauthorized();
}
default:
return Accepted();
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,
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityFollow, body);
if (succeeded) return Accepted();
else return Unauthorized();
}
case "Undo":
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.DeleteRequestedAsync(signature, r.Method, r.Path,
r.QueryString.ToString(), HeaderHandler.RequestHeaders(r.Headers), activity as ActivityDelete, body);
if (succeeded) return Accepted();
else return Unauthorized();
}
default:
return Accepted();
}
}
}
catch (UserNotFoundException)
{
return NotFound();
}
catch (UserHasBeenSuspendedException)
{
return NotFound();
}
catch (RateLimitExceededException)
{
return new ObjectResult("Too Many Requests") { StatusCode = 429 };
}
}
[Route("/users/{id}/followers")]

View file

@ -12,6 +12,7 @@ using BirdsiteLive.Models;
using BirdsiteLive.Models.WellKnownModels;
using BirdsiteLive.Twitter;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace BirdsiteLive.Controllers
@ -23,13 +24,15 @@ namespace BirdsiteLive.Controllers
private readonly ITwitterUserService _twitterUserService;
private readonly ITwitterUserDal _twitterUserDal;
private readonly InstanceSettings _settings;
private readonly ILogger<WellKnownController> _logger;
#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;
_twitterUserDal = twitterUserDal;
_moderationRepository = moderationRepository;
_logger = logger;
_settings = settings;
}
#endregion
@ -174,9 +177,27 @@ namespace BirdsiteLive.Controllers
if (!string.IsNullOrWhiteSpace(domain) && domain != _settings.Domain)
return NotFound();
var user = _twitterUserService.GetUser(name);
if (user == null)
try
{
_twitterUserService.GetUser(name);
}
catch (UserNotFoundException)
{
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);

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -159,11 +160,136 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
twitterUserServiceMock
.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
.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);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
@ -194,7 +320,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
}
[TestMethod]
public async Task ProcessAsync_Unfound_OverThreshold_Test()
public async Task ProcessAsync_Error_Test()
{
#region Stubs
var userId1 = 1;
@ -235,10 +361,79 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
twitterUserServiceMock
.Setup(x => x.GetUser(It.Is<string>(y => y == acct2)))
.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
.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);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct2)))
@ -312,8 +507,20 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
{
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 = 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
@ -331,7 +538,81 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
}
[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
var userId1 = 1;
@ -361,9 +642,6 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
.Setup(x => x.GetUser(It.Is<string>(y => y == acct1)))
.Returns((TwitterUser)null);
twitterUserServiceMock
.Setup(x => x.PurgeUser(It.Is<string>(y => y == acct1)));
var twitterUserDalMock = new Mock<ITwitterUserDal>(MockBehavior.Strict);
twitterUserDalMock
.Setup(x => x.GetTwitterUserAsync(It.Is<string>(y => y == acct1)))
@ -388,5 +666,77 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
removeTwitterAccountActionMock.VerifyAll();
#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
}
}
}