From 662f97e53c1efb5595aecc35c2531e49921868d6 Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Mon, 7 Feb 2022 18:48:10 -0500 Subject: [PATCH 1/4] added proper exception in user retrieval --- .../RefreshTwitterUserStatusProcessor.cs | 68 ++++++--- .../Exceptions/RateLimitExceededException.cs | 9 ++ .../UserHasBeenSuspendedException.cs | 9 ++ .../Exceptions/UserNotFoundException.cs | 9 ++ .../TwitterUserService.cs | 28 +++- .../Controllers/UsersController.cs | 132 ++++++++++++------ .../Controllers/WellKnownController.cs | 29 +++- 7 files changed, 212 insertions(+), 72 deletions(-) create mode 100644 src/BirdsiteLive.Twitter/Exceptions/RateLimitExceededException.cs create mode 100644 src/BirdsiteLive.Twitter/Exceptions/UserHasBeenSuspendedException.cs create mode 100644 src/BirdsiteLive.Twitter/Exceptions/UserNotFoundException.cs diff --git a/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs b/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs index 3a36be3..3d29d67 100644 --- a/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs +++ b/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs @@ -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,58 @@ 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); + } + catch (UserHasBeenSuspendedException) + { + await ProcessNotFoundUserAsync(user); + } + catch (RateLimitExceededException) + { + await ProcessRateLimitExceededAsync(user); + } + 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 +101,6 @@ namespace BirdsiteLive.Pipeline.Processors { await _twitterUserDal.UpdateTwitterUserAsync(dbUser); } - - // Purge - _twitterUserService.PurgeUser(user.Acct); } } } \ No newline at end of file diff --git a/src/BirdsiteLive.Twitter/Exceptions/RateLimitExceededException.cs b/src/BirdsiteLive.Twitter/Exceptions/RateLimitExceededException.cs new file mode 100644 index 0000000..93a093a --- /dev/null +++ b/src/BirdsiteLive.Twitter/Exceptions/RateLimitExceededException.cs @@ -0,0 +1,9 @@ +using System; + +namespace BirdsiteLive.Twitter +{ + public class RateLimitExceededException : Exception + { + + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.Twitter/Exceptions/UserHasBeenSuspendedException.cs b/src/BirdsiteLive.Twitter/Exceptions/UserHasBeenSuspendedException.cs new file mode 100644 index 0000000..03bd835 --- /dev/null +++ b/src/BirdsiteLive.Twitter/Exceptions/UserHasBeenSuspendedException.cs @@ -0,0 +1,9 @@ +using System; + +namespace BirdsiteLive.Twitter +{ + public class UserHasBeenSuspendedException : Exception + { + + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.Twitter/Exceptions/UserNotFoundException.cs b/src/BirdsiteLive.Twitter/Exceptions/UserNotFoundException.cs new file mode 100644 index 0000000..1dffc72 --- /dev/null +++ b/src/BirdsiteLive.Twitter/Exceptions/UserNotFoundException.cs @@ -0,0 +1,9 @@ +using System; + +namespace BirdsiteLive.Twitter +{ + public class UserNotFoundException : Exception + { + + } +} \ No newline at end of file diff --git a/src/BirdsiteLive.Twitter/TwitterUserService.cs b/src/BirdsiteLive.Twitter/TwitterUserService.cs index adc8d6b..7fb6439 100644 --- a/src/BirdsiteLive.Twitter/TwitterUserService.cs +++ b/src/BirdsiteLive.Twitter/TwitterUserService.cs @@ -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 diff --git a/src/BirdsiteLive/Controllers/UsersController.cs b/src/BirdsiteLive/Controllers/UsersController.cs index 965f988..f59ae21 100644 --- a/src/BirdsiteLive/Controllers/UsersController.cs +++ b/src/BirdsiteLive/Controllers/UsersController.cs @@ -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 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")] diff --git a/src/BirdsiteLive/Controllers/WellKnownController.cs b/src/BirdsiteLive/Controllers/WellKnownController.cs index 501f783..272d789 100644 --- a/src/BirdsiteLive/Controllers/WellKnownController.cs +++ b/src/BirdsiteLive/Controllers/WellKnownController.cs @@ -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 _logger; + #region Ctor - public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository) + public WellKnownController(InstanceSettings settings, ITwitterUserService twitterUserService, ITwitterUserDal twitterUserDal, IModerationRepository moderationRepository, ILogger 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); From d1c5a592471857bbd592000c54762a107cd0804f Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Mon, 7 Feb 2022 19:33:08 -0500 Subject: [PATCH 2/4] fix tests --- .../RefreshTwitterUserStatusProcessor.cs | 3 + .../RefreshTwitterUserStatusProcessorTests.cs | 323 ++++++++++++++++-- 2 files changed, 306 insertions(+), 20 deletions(-) diff --git a/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs b/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs index 3d29d67..739d50b 100644 --- a/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs +++ b/src/BirdsiteLive.Pipeline/Processors/RefreshTwitterUserStatusProcessor.cs @@ -45,14 +45,17 @@ namespace BirdsiteLive.Pipeline.Processors catch (UserNotFoundException) { await ProcessNotFoundUserAsync(user); + continue; } catch (UserHasBeenSuspendedException) { await ProcessNotFoundUserAsync(user); + continue; } catch (RateLimitExceededException) { await ProcessRateLimitExceededAsync(user); + continue; } catch (Exception) { diff --git a/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs b/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs index d5fbeef..ae4994f 100644 --- a/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs +++ b/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs @@ -159,25 +159,14 @@ namespace BirdsiteLive.Pipeline.Tests.Processors twitterUserServiceMock .Setup(x => x.GetUser(It.Is(y => y == acct2))) - .Returns((TwitterUser) null); - - twitterUserServiceMock - .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); + .Throws(new UserNotFoundException()); var twitterUserDalMock = new Mock(MockBehavior.Strict); - twitterUserDalMock - .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) - .ReturnsAsync(new SyncTwitterUser - { - Id = userId2, - FetchingErrorCount = 0 - }); - - twitterUserDalMock - .Setup(x => x.UpdateTwitterUserAsync(It.Is(y => y.Id == userId2 && y.FetchingErrorCount == 1))) - .Returns(Task.CompletedTask); var removeTwitterAccountActionMock = new Mock(MockBehavior.Strict); + removeTwitterAccountActionMock + .Setup(x => x.ProcessAsync(It.Is(y => y.Acct == acct2))) + .Returns(Task.CompletedTask); #endregion var processor = new RefreshTwitterUserStatusProcessor(twitterUserServiceMock.Object, twitterUserDalMock.Object, removeTwitterAccountActionMock.Object, settings); @@ -194,7 +183,71 @@ namespace BirdsiteLive.Pipeline.Tests.Processors } [TestMethod] - public async Task ProcessAsync_Unfound_OverThreshold_Test() + public async Task ProcessAsync_Suspended_Test() + { + #region Stubs + var userId1 = 1; + var acct1 = "user1"; + + var userId2 = 2; + var acct2 = "user2"; + + var users = new List + { + 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(MockBehavior.Strict); + twitterUserServiceMock + .Setup(x => x.GetUser(It.Is(y => y == acct1))) + .Returns(new TwitterUser + { + Protected = false + }); + + twitterUserServiceMock + .Setup(x => x.GetUser(It.Is(y => y == acct2))) + .Throws(new UserHasBeenSuspendedException()); + + var twitterUserDalMock = new Mock(MockBehavior.Strict); + + var removeTwitterAccountActionMock = new Mock(MockBehavior.Strict); + removeTwitterAccountActionMock + .Setup(x => x.ProcessAsync(It.Is(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_Error_Test() { #region Stubs var userId1 = 1; @@ -236,8 +289,83 @@ namespace BirdsiteLive.Pipeline.Tests.Processors .Setup(x => x.GetUser(It.Is(y => y == acct2))) .Returns((TwitterUser)null); + //twitterUserServiceMock + // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); + + var twitterUserDalMock = new Mock(MockBehavior.Strict); + twitterUserDalMock + .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) + .ReturnsAsync(new SyncTwitterUser + { + Id = userId2, + FetchingErrorCount = 0 + }); + + twitterUserDalMock + .Setup(x => x.UpdateTwitterUserAsync(It.Is(y => y.Id == userId2 && y.FetchingErrorCount == 1))) + .Returns(Task.CompletedTask); + + var removeTwitterAccountActionMock = new Mock(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 + { + 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(MockBehavior.Strict); twitterUserServiceMock - .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); + .Setup(x => x.GetUser(It.Is(y => y == acct1))) + .Returns(new TwitterUser + { + Protected = false + }); + + twitterUserServiceMock + .Setup(x => x.GetUser(It.Is(y => y == acct2))) + .Returns((TwitterUser)null); + + //twitterUserServiceMock + // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); var twitterUserDalMock = new Mock(MockBehavior.Strict); twitterUserDalMock @@ -313,7 +441,22 @@ namespace BirdsiteLive.Pipeline.Tests.Processors Protected = true }); + //twitterUserServiceMock + // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); + var twitterUserDalMock = new Mock(MockBehavior.Strict); + twitterUserDalMock + .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) + .ReturnsAsync(new SyncTwitterUser + { + Id = userId2, + FetchingErrorCount = 0 + }); + + twitterUserDalMock + .Setup(x => x.UpdateTwitterUserAsync(It.Is(y => y.Id == userId2 && y.FetchingErrorCount == 1))) + .Returns(Task.CompletedTask); + var removeTwitterAccountActionMock = new Mock(MockBehavior.Strict); #endregion @@ -331,7 +474,147 @@ 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 + { + 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(MockBehavior.Strict); + twitterUserServiceMock + .Setup(x => x.GetUser(It.Is(y => y == acct1))) + .Returns(new TwitterUser + { + Protected = false + }); + + twitterUserServiceMock + .Setup(x => x.GetUser(It.Is(y => y == acct2))) + .Returns(new TwitterUser + { + Protected = true + }); + + //twitterUserServiceMock + // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); + + var twitterUserDalMock = new Mock(MockBehavior.Strict); + twitterUserDalMock + .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) + .ReturnsAsync(new SyncTwitterUser + { + Id = userId2, + FetchingErrorCount = 500 + }); + + var removeTwitterAccountActionMock = new Mock(MockBehavior.Strict); + removeTwitterAccountActionMock + .Setup(x => x.ProcessAsync(It.Is(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_Protected_Test() + //{ + // #region Stubs + // var userId1 = 1; + // var acct1 = "user1"; + + // var userId2 = 2; + // var acct2 = "user2"; + + // var users = new List + // { + // 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(MockBehavior.Strict); + // twitterUserServiceMock + // .Setup(x => x.GetUser(It.Is(y => y == acct1))) + // .Returns(new TwitterUser + // { + // Protected = false + // }); + + // twitterUserServiceMock + // .Setup(x => x.GetUser(It.Is(y => y == acct2))) + // .Returns(new TwitterUser + // { + // Protected = true + // }); + + // var twitterUserDalMock = new Mock(MockBehavior.Strict); + // var removeTwitterAccountActionMock = new Mock(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_NotInit_Test() { #region Stubs var userId1 = 1; @@ -361,8 +644,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors .Setup(x => x.GetUser(It.Is(y => y == acct1))) .Returns((TwitterUser)null); - twitterUserServiceMock - .Setup(x => x.PurgeUser(It.Is(y => y == acct1))); + //twitterUserServiceMock + // .Setup(x => x.PurgeUser(It.Is(y => y == acct1))); var twitterUserDalMock = new Mock(MockBehavior.Strict); twitterUserDalMock From 420d8867e7ea8377bcc569e66b08817ba93ee2f6 Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Mon, 7 Feb 2022 19:36:19 -0500 Subject: [PATCH 3/4] added test for new behavior --- .../RefreshTwitterUserStatusProcessorTests.cs | 153 +++++++++++++++++- 1 file changed, 152 insertions(+), 1 deletion(-) diff --git a/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs b/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs index ae4994f..be342c0 100644 --- a/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs +++ b/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -246,6 +247,81 @@ namespace BirdsiteLive.Pipeline.Tests.Processors #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 + { + 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(MockBehavior.Strict); + twitterUserServiceMock + .Setup(x => x.GetUser(It.Is(y => y == acct1))) + .Returns(new TwitterUser + { + Protected = false + }); + + twitterUserServiceMock + .Setup(x => x.GetUser(It.Is(y => y == acct2))) + .Throws(new Exception()); + + //twitterUserServiceMock + // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); + + var twitterUserDalMock = new Mock(MockBehavior.Strict); + twitterUserDalMock + .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) + .ReturnsAsync(new SyncTwitterUser + { + Id = userId2, + FetchingErrorCount = 0 + }); + + twitterUserDalMock + .Setup(x => x.UpdateTwitterUserAsync(It.Is(y => y.Id == userId2 && y.FetchingErrorCount == 1))) + .Returns(Task.CompletedTask); + + var removeTwitterAccountActionMock = new Mock(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_Test() { @@ -671,5 +747,80 @@ 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 + { + 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(MockBehavior.Strict); + twitterUserServiceMock + .Setup(x => x.GetUser(It.Is(y => y == acct1))) + .Returns(new TwitterUser + { + Protected = false, + }); + + twitterUserServiceMock + .Setup(x => x.GetUser(It.Is(y => y == acct2))) + .Throws(new RateLimitExceededException()); + + //twitterUserServiceMock + // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); + + var twitterUserDalMock = new Mock(MockBehavior.Strict); + twitterUserDalMock + .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) + .ReturnsAsync(new SyncTwitterUser + { + Id = userId2, + FetchingErrorCount = 20 + }); + + twitterUserDalMock + .Setup(x => x.UpdateTwitterUserAsync(It.Is(y => y.Id == userId2 && y.FetchingErrorCount == 20))) + .Returns(Task.CompletedTask); + + var removeTwitterAccountActionMock = new Mock(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 + } } } \ No newline at end of file From 9f9f88aab78410bc672cfcd04b8814855a7f1cd3 Mon Sep 17 00:00:00 2001 From: Nicolas Constant Date: Mon, 7 Feb 2022 19:37:28 -0500 Subject: [PATCH 4/4] clean up --- .../RefreshTwitterUserStatusProcessorTests.cs | 98 ++----------------- 1 file changed, 7 insertions(+), 91 deletions(-) diff --git a/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs b/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs index be342c0..cd2d116 100644 --- a/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs +++ b/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/RefreshTwitterUserStatusProcessorTests.cs @@ -289,10 +289,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors twitterUserServiceMock .Setup(x => x.GetUser(It.Is(y => y == acct2))) .Throws(new Exception()); - - //twitterUserServiceMock - // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); - + var twitterUserDalMock = new Mock(MockBehavior.Strict); twitterUserDalMock .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) @@ -364,10 +361,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors twitterUserServiceMock .Setup(x => x.GetUser(It.Is(y => y == acct2))) .Returns((TwitterUser)null); - - //twitterUserServiceMock - // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); - + var twitterUserDalMock = new Mock(MockBehavior.Strict); twitterUserDalMock .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) @@ -439,10 +433,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors twitterUserServiceMock .Setup(x => x.GetUser(It.Is(y => y == acct2))) .Returns((TwitterUser)null); - - //twitterUserServiceMock - // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); - + var twitterUserDalMock = new Mock(MockBehavior.Strict); twitterUserDalMock .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) @@ -516,10 +507,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors { Protected = true }); - - //twitterUserServiceMock - // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); - + var twitterUserDalMock = new Mock(MockBehavior.Strict); twitterUserDalMock .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) @@ -594,10 +582,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors { Protected = true }); - - //twitterUserServiceMock - // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); - + var twitterUserDalMock = new Mock(MockBehavior.Strict); twitterUserDalMock .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2))) @@ -625,70 +610,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors removeTwitterAccountActionMock.VerifyAll(); #endregion } - - //[TestMethod] - //public async Task ProcessAsync_Protected_Test() - //{ - // #region Stubs - // var userId1 = 1; - // var acct1 = "user1"; - - // var userId2 = 2; - // var acct2 = "user2"; - - // var users = new List - // { - // 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(MockBehavior.Strict); - // twitterUserServiceMock - // .Setup(x => x.GetUser(It.Is(y => y == acct1))) - // .Returns(new TwitterUser - // { - // Protected = false - // }); - - // twitterUserServiceMock - // .Setup(x => x.GetUser(It.Is(y => y == acct2))) - // .Returns(new TwitterUser - // { - // Protected = true - // }); - - // var twitterUserDalMock = new Mock(MockBehavior.Strict); - // var removeTwitterAccountActionMock = new Mock(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_NotInit_Test() { @@ -720,9 +642,6 @@ namespace BirdsiteLive.Pipeline.Tests.Processors .Setup(x => x.GetUser(It.Is(y => y == acct1))) .Returns((TwitterUser)null); - //twitterUserServiceMock - // .Setup(x => x.PurgeUser(It.Is(y => y == acct1))); - var twitterUserDalMock = new Mock(MockBehavior.Strict); twitterUserDalMock .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct1))) @@ -790,10 +709,7 @@ namespace BirdsiteLive.Pipeline.Tests.Processors twitterUserServiceMock .Setup(x => x.GetUser(It.Is(y => y == acct2))) .Throws(new RateLimitExceededException()); - - //twitterUserServiceMock - // .Setup(x => x.PurgeUser(It.Is(y => y == acct2))); - + var twitterUserDalMock = new Mock(MockBehavior.Strict); twitterUserDalMock .Setup(x => x.GetTwitterUserAsync(It.Is(y => y == acct2)))