diff --git a/Dockerfile b/Dockerfile index 11a4422..5f69f30 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/aspnet:3.1-buster-slim AS base +FROM mcr.microsoft.com/dotnet/aspnet:3.1-alpine AS base WORKDIR /app EXPOSE 80 EXPOSE 443 diff --git a/src/BSLManager/App.cs b/src/BSLManager/App.cs index 0e48262..37697cc 100644 --- a/src/BSLManager/App.cs +++ b/src/BSLManager/App.cs @@ -146,23 +146,31 @@ namespace BSLManager Width = Dim.Fill(), Height = 1 }; - var inbox = new Label($"Inbox: {follower.InboxRoute}") + var errors = new Label($"Posting Errors: {follower.PostingErrorCount}") { X = 1, Y = 4, Width = Dim.Fill(), Height = 1 }; - var sharedInbox = new Label($"Shared Inbox: {follower.SharedInboxRoute}") + var inbox = new Label($"Inbox: {follower.InboxRoute}") { X = 1, Y = 5, Width = Dim.Fill(), Height = 1 }; + var sharedInbox = new Label($"Shared Inbox: {follower.SharedInboxRoute}") + { + X = 1, + Y = 6, + Width = Dim.Fill(), + Height = 1 + }; dialog.Add(name); dialog.Add(following); + dialog.Add(errors); dialog.Add(inbox); dialog.Add(sharedInbox); dialog.Add(close); diff --git a/src/BSLManager/Domain/FollowersListState.cs b/src/BSLManager/Domain/FollowersListState.cs index f33acb8..02c2151 100644 --- a/src/BSLManager/Domain/FollowersListState.cs +++ b/src/BSLManager/Domain/FollowersListState.cs @@ -26,7 +26,7 @@ namespace BSLManager.Domain foreach (var follower in _sourceUserList) { - var displayedUser = $"{GetFullHandle(follower)} ({follower.Followings.Count})"; + var displayedUser = $"{GetFullHandle(follower)} ({follower.Followings.Count}) (err:{follower.PostingErrorCount})"; _filteredDisplayableUserList.Add(displayedUser); } } diff --git a/src/BirdsiteLive.Pipeline/Processors/SendTweetsToFollowersProcessor.cs b/src/BirdsiteLive.Pipeline/Processors/SendTweetsToFollowersProcessor.cs index cb1efb6..65f9610 100644 --- a/src/BirdsiteLive.Pipeline/Processors/SendTweetsToFollowersProcessor.cs +++ b/src/BirdsiteLive.Pipeline/Processors/SendTweetsToFollowersProcessor.cs @@ -22,14 +22,16 @@ namespace BirdsiteLive.Pipeline.Processors { private readonly ISendTweetsToInboxTask _sendTweetsToInboxTask; private readonly ISendTweetsToSharedInboxTask _sendTweetsToSharedInbox; + private readonly IFollowersDal _followersDal; private readonly ILogger _logger; #region Ctor - public SendTweetsToFollowersProcessor(ISendTweetsToInboxTask sendTweetsToInboxTask, ISendTweetsToSharedInboxTask sendTweetsToSharedInbox, ILogger logger) + public SendTweetsToFollowersProcessor(ISendTweetsToInboxTask sendTweetsToInboxTask, ISendTweetsToSharedInboxTask sendTweetsToSharedInbox, IFollowersDal followersDal, ILogger logger) { _sendTweetsToInboxTask = sendTweetsToInboxTask; _sendTweetsToSharedInbox = sendTweetsToSharedInbox; _logger = logger; + _followersDal = followersDal; } #endregion @@ -41,18 +43,18 @@ namespace BirdsiteLive.Pipeline.Processors var followersWtSharedInbox = userWithTweetsToSync.Followers .Where(x => !string.IsNullOrWhiteSpace(x.SharedInboxRoute)) .ToList(); - await ProcessFollowersWithSharedInbox(userWithTweetsToSync.Tweets, followersWtSharedInbox, user); + await ProcessFollowersWithSharedInboxAsync(userWithTweetsToSync.Tweets, followersWtSharedInbox, user); // Process Inbox var followerWtInbox = userWithTweetsToSync.Followers .Where(x => string.IsNullOrWhiteSpace(x.SharedInboxRoute)) .ToList(); - await ProcessFollowersWithInbox(userWithTweetsToSync.Tweets, followerWtInbox, user); + await ProcessFollowersWithInboxAsync(userWithTweetsToSync.Tweets, followerWtInbox, user); return userWithTweetsToSync; } - private async Task ProcessFollowersWithSharedInbox(ExtractedTweet[] tweets, List followers, SyncTwitterUser user) + private async Task ProcessFollowersWithSharedInboxAsync(ExtractedTweet[] tweets, List followers, SyncTwitterUser user) { var followersPerInstances = followers.GroupBy(x => x.Host); @@ -61,28 +63,51 @@ namespace BirdsiteLive.Pipeline.Processors try { await _sendTweetsToSharedInbox.ExecuteAsync(tweets, user, followersPerInstance.Key, followersPerInstance.ToArray()); + + foreach (var f in followersPerInstance) + await ProcessWorkingUserAsync(f); } catch (Exception e) { var follower = followersPerInstance.First(); _logger.LogError(e, "Posting to {Host}{Route} failed", follower.Host, follower.SharedInboxRoute); + + foreach (var f in followersPerInstance) + await ProcessFailingUserAsync(f); } } } - private async Task ProcessFollowersWithInbox(ExtractedTweet[] tweets, List followerWtInbox, SyncTwitterUser user) + private async Task ProcessFollowersWithInboxAsync(ExtractedTweet[] tweets, List followerWtInbox, SyncTwitterUser user) { foreach (var follower in followerWtInbox) { try { await _sendTweetsToInboxTask.ExecuteAsync(tweets, follower, user); + await ProcessWorkingUserAsync(follower); } catch (Exception e) { _logger.LogError(e, "Posting to {Host}{Route} failed", follower.Host, follower.InboxRoute); + await ProcessFailingUserAsync(follower); } } } + + private async Task ProcessWorkingUserAsync(Follower follower) + { + if (follower.PostingErrorCount > 0) + { + follower.PostingErrorCount = 0; + await _followersDal.UpdateFollowerAsync(follower); + } + } + + private async Task ProcessFailingUserAsync(Follower follower) + { + follower.PostingErrorCount++; + await _followersDal.UpdateFollowerAsync(follower); + } } } \ No newline at end of file diff --git a/src/BirdsiteLive/BirdsiteLive.csproj b/src/BirdsiteLive/BirdsiteLive.csproj index 3b07184..7385e77 100644 --- a/src/BirdsiteLive/BirdsiteLive.csproj +++ b/src/BirdsiteLive/BirdsiteLive.csproj @@ -4,7 +4,7 @@ netcoreapp3.1 d21486de-a812-47eb-a419-05682bb68856 Linux - 0.18.2 + 0.18.3 diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/DbInitializerPostgresDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/DbInitializerPostgresDal.cs index 0d656a7..2e3acea 100644 --- a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/DbInitializerPostgresDal.cs +++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/DbInitializerPostgresDal.cs @@ -23,7 +23,7 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers public class DbInitializerPostgresDal : PostgresBase, IDbInitializerDal { private readonly PostgresTools _tools; - private readonly Version _currentVersion = new Version(2, 2); + private readonly Version _currentVersion = new Version(2, 3); private const string DbVersionType = "db-version"; #region Ctor @@ -133,7 +133,8 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers { new Tuple(new Version(1,0), new Version(2,0)), new Tuple(new Version(2,0), new Version(2,1)), - new Tuple(new Version(2,1), new Version(2,2)) + new Tuple(new Version(2,1), new Version(2,2)), + new Tuple(new Version(2,2), new Version(2,3)) }; } @@ -157,6 +158,11 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers var addLastSync = $@"ALTER TABLE {_settings.TwitterUserTableName} ADD fetchingErrorCount SMALLINT"; await _tools.ExecuteRequestAsync(addLastSync); } + else if (from == new Version(2, 2) && to == new Version(2, 3)) + { + var addPostingError = $@"ALTER TABLE {_settings.FollowersTableName} ADD postingErrorCount SMALLINT"; + await _tools.ExecuteRequestAsync(addPostingError); + } else { throw new NotImplementedException(); diff --git a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs index ec031d4..bcdda0f 100644 --- a/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs +++ b/src/DataAccessLayers/BirdsiteLive.DAL.Postgres/DataAccessLayers/FollowersPostgresDal.cs @@ -103,13 +103,13 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers if (follower.Id == default) throw new ArgumentException("id"); var serializedDic = JsonConvert.SerializeObject(follower.FollowingsSyncStatus); - var query = $"UPDATE {_settings.FollowersTableName} SET followings = @followings, followingsSyncStatus = CAST(@followingsSyncStatus as json) WHERE id = @id"; + var query = $"UPDATE {_settings.FollowersTableName} SET followings = @followings, followingsSyncStatus = CAST(@followingsSyncStatus as json), postingErrorCount = @postingErrorCount WHERE id = @id"; using (var dbConnection = Connection) { dbConnection.Open(); - await dbConnection.QueryAsync(query, new { follower.Id, follower.Followings, followingsSyncStatus = serializedDic }); + await dbConnection.QueryAsync(query, new { follower.Id, follower.Followings, followingsSyncStatus = serializedDic, postingErrorCount = follower.PostingErrorCount }); } } @@ -158,7 +158,8 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers ActorId = follower.ActorId, SharedInboxRoute = follower.SharedInboxRoute, Followings = follower.Followings.ToList(), - FollowingsSyncStatus = JsonConvert.DeserializeObject>(follower.FollowingsSyncStatus) + FollowingsSyncStatus = JsonConvert.DeserializeObject>(follower.FollowingsSyncStatus), + PostingErrorCount = follower.PostingErrorCount }; } } @@ -174,5 +175,6 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers public string InboxRoute { get; set; } public string SharedInboxRoute { get; set; } public string ActorId { get; set; } + public int PostingErrorCount { get; set; } } } \ No newline at end of file diff --git a/src/DataAccessLayers/BirdsiteLive.DAL/Models/Follower.cs b/src/DataAccessLayers/BirdsiteLive.DAL/Models/Follower.cs index 274852b..357e32e 100644 --- a/src/DataAccessLayers/BirdsiteLive.DAL/Models/Follower.cs +++ b/src/DataAccessLayers/BirdsiteLive.DAL/Models/Follower.cs @@ -14,5 +14,7 @@ namespace BirdsiteLive.DAL.Models public string Host { get; set; } public string InboxRoute { get; set; } public string SharedInboxRoute { get; set; } + + public int PostingErrorCount { get; set; } } } \ No newline at end of file diff --git a/src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/FollowersPostgresDalTests.cs b/src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/FollowersPostgresDalTests.cs index cbaeb72..62cb3e6 100644 --- a/src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/FollowersPostgresDalTests.cs +++ b/src/Tests/BirdsiteLive.DAL.Postgres.Tests/DataAccessLayers/FollowersPostgresDalTests.cs @@ -54,6 +54,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers Assert.AreEqual(inboxRoute, result.InboxRoute); Assert.AreEqual(sharedInboxRoute, result.SharedInboxRoute); Assert.AreEqual(actorId, result.ActorId); + Assert.AreEqual(0, result.PostingErrorCount); Assert.AreEqual(following.Length, result.Followings.Count); Assert.AreEqual(following[0], result.Followings[0]); Assert.AreEqual(followingSync.Count, result.FollowingsSyncStatus.Count); @@ -83,6 +84,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers Assert.AreEqual(sharedInboxRoute, result.SharedInboxRoute); Assert.AreEqual(0, result.Followings.Count); Assert.AreEqual(0, result.FollowingsSyncStatus.Count); + Assert.AreEqual(0, result.PostingErrorCount); } [TestMethod] @@ -125,6 +127,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers Assert.AreEqual(followingSync.Count, result.FollowingsSyncStatus.Count); Assert.AreEqual(followingSync.First().Key, result.FollowingsSyncStatus.First().Key); Assert.AreEqual(followingSync.First().Value, result.FollowingsSyncStatus.First().Value); + Assert.AreEqual(0, result.PostingErrorCount); } [TestMethod] @@ -276,8 +279,8 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers }; result.Followings = updatedFollowing.ToList(); result.FollowingsSyncStatus = updatedFollowingSync; - - + result.PostingErrorCount = 10; + await dal.UpdateFollowerAsync(result); result = await dal.GetFollowerAsync(acct, host); @@ -286,6 +289,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers 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(10, result.PostingErrorCount); } [TestMethod] @@ -316,6 +320,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers }; result.Followings = updatedFollowing.ToList(); result.FollowingsSyncStatus = updatedFollowingSync; + result.PostingErrorCount = 5; await dal.UpdateFollowerAsync(result); result = await dal.GetFollowerAsync(acct, host); @@ -325,6 +330,41 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers 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(5, result.PostingErrorCount); + } + + [TestMethod] + public async Task CreateUpdateAndGetFollower_ResetErrorCount() + { + var acct = "myhandle"; + var host = "domain.ext"; + var following = new[] { 12, 19, 23 }; + var followingSync = new Dictionary() + { + {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); + Assert.AreEqual(0, result.PostingErrorCount); + + result.PostingErrorCount = 5; + + await dal.UpdateFollowerAsync(result); + result = await dal.GetFollowerAsync(acct, host); + Assert.AreEqual(5, result.PostingErrorCount); + + result.PostingErrorCount = 0; + + await dal.UpdateFollowerAsync(result); + result = await dal.GetFollowerAsync(acct, host); + Assert.AreEqual(0, result.PostingErrorCount); } [TestMethod] diff --git a/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/SendTweetsToFollowersProcessorTests.cs b/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/SendTweetsToFollowersProcessorTests.cs index 7715342..53aa12a 100644 --- a/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/SendTweetsToFollowersProcessorTests.cs +++ b/src/Tests/BirdsiteLive.Pipeline.Tests/Processors/SendTweetsToFollowersProcessorTests.cs @@ -1,6 +1,8 @@ using System; using System.Threading; using System.Threading.Tasks; +using System.Xml; +using BirdsiteLive.DAL.Contracts; using BirdsiteLive.DAL.Models; using BirdsiteLive.Pipeline.Models; using BirdsiteLive.Pipeline.Processors; @@ -69,15 +71,18 @@ namespace BirdsiteLive.Pipeline.Tests.Processors It.Is(y => y.Length == 2))) .Returns(Task.CompletedTask); + var followersDalMock = new Mock(MockBehavior.Strict); + var loggerMock = new Mock>(); #endregion - var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, loggerMock.Object); + var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); #region Validations sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll(); + followersDalMock.VerifyAll(); #endregion } @@ -139,15 +144,18 @@ namespace BirdsiteLive.Pipeline.Tests.Processors .Returns(Task.CompletedTask); } + var followersDalMock = new Mock(MockBehavior.Strict); + var loggerMock = new Mock>(); #endregion - var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, loggerMock.Object); + var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); #region Validations sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll(); + followersDalMock.VerifyAll(); #endregion } @@ -214,15 +222,193 @@ namespace BirdsiteLive.Pipeline.Tests.Processors It.Is(y => y.Length == 1))) .Throws(new Exception()); + var followersDalMock = new Mock(MockBehavior.Strict); + + followersDalMock + .Setup(x => x.UpdateFollowerAsync(It.Is(y => y.Id == userId2 && y.PostingErrorCount == 1))) + .Returns(Task.CompletedTask); + var loggerMock = new Mock>(); #endregion - var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, loggerMock.Object); + var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); #region Validations sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll(); + followersDalMock.VerifyAll(); + #endregion + } + + [TestMethod] + public async Task ProcessAsync_MultiInstances_SharedInbox_OneTweet_ErrorReset_Test() + { + #region Stubs + var tweetId = 1; + var host1 = "domain1.ext"; + var host2 = "domain2.ext"; + var sharedInbox = "/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, + SharedInboxRoute = sharedInbox + }, + new Follower + { + Id = userId2, + Host = host2, + SharedInboxRoute = sharedInbox, + PostingErrorCount = 50 + }, + } + }; + #endregion + + #region Mocks + var sendTweetsToInboxTaskMock = new Mock(MockBehavior.Strict); + + var sendTweetsToSharedInboxTaskMock = new Mock(MockBehavior.Strict); + sendTweetsToSharedInboxTaskMock + .Setup(x => x.ExecuteAsync( + It.Is(y => y.Length == 1), + It.Is(y => y.Acct == userAcct), + It.Is(y => y == host1), + It.Is(y => y.Length == 1))) + .Returns(Task.CompletedTask); + + sendTweetsToSharedInboxTaskMock + .Setup(x => x.ExecuteAsync( + It.Is(y => y.Length == 1), + It.Is(y => y.Acct == userAcct), + It.Is(y => y == host2), + It.Is(y => y.Length == 1))) + .Returns(Task.CompletedTask); + + var followersDalMock = new Mock(MockBehavior.Strict); + + followersDalMock + .Setup(x => x.UpdateFollowerAsync(It.Is(y => y.Id == userId2 && y.PostingErrorCount == 0))) + .Returns(Task.CompletedTask); + + var loggerMock = new Mock>(); + #endregion + + var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); + var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); + + #region Validations + sendTweetsToInboxTaskMock.VerifyAll(); + sendTweetsToSharedInboxTaskMock.VerifyAll(); + followersDalMock.VerifyAll(); + #endregion + } + + [TestMethod] + public async Task ProcessAsync_MultiInstances_SharedInbox_OneTweet_ErrorAndReset_Test() + { + #region Stubs + var tweetId = 1; + var host1 = "domain1.ext"; + var host2 = "domain2.ext"; + var sharedInbox = "/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, + SharedInboxRoute = sharedInbox, + PostingErrorCount = 50 + }, + new Follower + { + Id = userId2, + Host = host2, + SharedInboxRoute = sharedInbox, + PostingErrorCount = 50 + }, + } + }; + #endregion + + #region Mocks + var sendTweetsToInboxTaskMock = new Mock(MockBehavior.Strict); + + var sendTweetsToSharedInboxTaskMock = new Mock(MockBehavior.Strict); + sendTweetsToSharedInboxTaskMock + .Setup(x => x.ExecuteAsync( + It.Is(y => y.Length == 1), + It.Is(y => y.Acct == userAcct), + It.Is(y => y == host1), + It.Is(y => y.Length == 1))) + .Returns(Task.CompletedTask); + + sendTweetsToSharedInboxTaskMock + .Setup(x => x.ExecuteAsync( + It.Is(y => y.Length == 1), + It.Is(y => y.Acct == userAcct), + It.Is(y => y == host2), + It.Is(y => y.Length == 1))) + .Throws(new Exception()); + + var followersDalMock = new Mock(MockBehavior.Strict); + + followersDalMock + .Setup(x => x.UpdateFollowerAsync(It.Is(y => y.Id == userId1 && y.PostingErrorCount == 0))) + .Returns(Task.CompletedTask); + + followersDalMock + .Setup(x => x.UpdateFollowerAsync(It.Is(y => y.Id == userId2 && y.PostingErrorCount == 51))) + .Returns(Task.CompletedTask); + + var loggerMock = new Mock>(); + #endregion + + var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); + var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); + + #region Validations + sendTweetsToInboxTaskMock.VerifyAll(); + sendTweetsToSharedInboxTaskMock.VerifyAll(); + followersDalMock.VerifyAll(); #endregion } @@ -282,15 +468,18 @@ namespace BirdsiteLive.Pipeline.Tests.Processors var sendTweetsToSharedInboxTaskMock = new Mock(MockBehavior.Strict); + var followersDalMock = new Mock(MockBehavior.Strict); + var loggerMock = new Mock>(); #endregion - var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, loggerMock.Object); + var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); #region Validations sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll(); + followersDalMock.VerifyAll(); #endregion } @@ -351,15 +540,18 @@ namespace BirdsiteLive.Pipeline.Tests.Processors var sendTweetsToSharedInboxTaskMock = new Mock(MockBehavior.Strict); + var followersDalMock = new Mock(MockBehavior.Strict); + var loggerMock = new Mock>(); #endregion - var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, loggerMock.Object); + var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); #region Validations sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll(); + followersDalMock.VerifyAll(); #endregion } @@ -424,15 +616,189 @@ namespace BirdsiteLive.Pipeline.Tests.Processors var sendTweetsToSharedInboxTaskMock = new Mock(MockBehavior.Strict); + var followersDalMock = new Mock(MockBehavior.Strict); + + followersDalMock + .Setup(x => x.UpdateFollowerAsync(It.Is(y => y.Id == userId2 && y.PostingErrorCount == 1))) + .Returns(Task.CompletedTask); + var loggerMock = new Mock>(); #endregion - var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, loggerMock.Object); + var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); #region Validations sendTweetsToInboxTaskMock.VerifyAll(); sendTweetsToSharedInboxTaskMock.VerifyAll(); + followersDalMock.VerifyAll(); + #endregion + } + + [TestMethod] + public async Task ProcessAsync_MultiInstances_Inbox_OneTweet_ErrorReset_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 = 50 + }, + } + }; + #endregion + + #region Mocks + var sendTweetsToInboxTaskMock = new Mock(MockBehavior.Strict); + sendTweetsToInboxTaskMock + .Setup(x => x.ExecuteAsync( + It.Is(y => y.Length == 1), + It.Is(y => y.Id == userId1), + It.Is(y => y.Acct == userAcct))) + .Returns(Task.CompletedTask); + + sendTweetsToInboxTaskMock + .Setup(x => x.ExecuteAsync( + It.Is(y => y.Length == 1), + It.Is(y => y.Id == userId2), + It.Is(y => y.Acct == userAcct))) + .Returns(Task.CompletedTask); + + var sendTweetsToSharedInboxTaskMock = new Mock(MockBehavior.Strict); + + var followersDalMock = new Mock(MockBehavior.Strict); + + followersDalMock + .Setup(x => x.UpdateFollowerAsync(It.Is(y => y.Id == userId2 && y.PostingErrorCount == 0))) + .Returns(Task.CompletedTask); + + var loggerMock = new Mock>(); + #endregion + + var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); + var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); + + #region Validations + sendTweetsToInboxTaskMock.VerifyAll(); + sendTweetsToSharedInboxTaskMock.VerifyAll(); + followersDalMock.VerifyAll(); + #endregion + } + + [TestMethod] + public async Task ProcessAsync_MultiInstances_Inbox_OneTweet_ErrorAndReset_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, + PostingErrorCount = 50 + }, + new Follower + { + Id = userId2, + Host = host2, + InboxRoute = inbox, + PostingErrorCount = 50 + }, + } + }; + #endregion + + #region Mocks + var sendTweetsToInboxTaskMock = new Mock(MockBehavior.Strict); + sendTweetsToInboxTaskMock + .Setup(x => x.ExecuteAsync( + It.Is(y => y.Length == 1), + It.Is(y => y.Id == userId1), + It.Is(y => y.Acct == userAcct))) + .Returns(Task.CompletedTask); + + sendTweetsToInboxTaskMock + .Setup(x => x.ExecuteAsync( + It.Is(y => y.Length == 1), + It.Is(y => y.Id == userId2), + It.Is(y => y.Acct == userAcct))) + .Throws(new Exception()); + + var sendTweetsToSharedInboxTaskMock = new Mock(MockBehavior.Strict); + + var followersDalMock = new Mock(MockBehavior.Strict); + + followersDalMock + .Setup(x => x.UpdateFollowerAsync(It.Is(y => y.Id == userId1 && y.PostingErrorCount == 0))) + .Returns(Task.CompletedTask); + + followersDalMock + .Setup(x => x.UpdateFollowerAsync(It.Is(y => y.Id == userId2 && y.PostingErrorCount == 51))) + .Returns(Task.CompletedTask); + + var loggerMock = new Mock>(); + #endregion + + var processor = new SendTweetsToFollowersProcessor(sendTweetsToInboxTaskMock.Object, sendTweetsToSharedInboxTaskMock.Object, followersDalMock.Object, loggerMock.Object); + var result = await processor.ProcessAsync(userWithTweets, CancellationToken.None); + + #region Validations + sendTweetsToInboxTaskMock.VerifyAll(); + sendTweetsToSharedInboxTaskMock.VerifyAll(); + followersDalMock.VerifyAll(); #endregion } }