added deletion workflow

This commit is contained in:
Nicolas Constant 2022-12-13 22:55:22 -05:00
parent 4fb04c16b8
commit 6f8a2c0373
No known key found for this signature in database
GPG key ID: 1E9F677FB01A5688
15 changed files with 239 additions and 54 deletions

View file

@ -15,4 +15,8 @@
<ProjectReference Include="..\DataAccessLayers\BirdsiteLive.DAL\BirdsiteLive.DAL.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Enum\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,9 @@
namespace BirdsiteLive.Domain.Enum
{
public enum MigrationTypeEnum
{
Unknown = 0,
Migration = 1,
Deletion = 2
}
}

View file

@ -9,6 +9,8 @@ using BirdsiteLive.ActivityPub.Models;
using BirdsiteLive.DAL.Contracts;
using BirdsiteLive.ActivityPub.Converters;
using BirdsiteLive.Common.Settings;
using BirdsiteLive.DAL.Models;
using BirdsiteLive.Domain.Enum;
namespace BirdsiteLive.Domain
{
@ -37,9 +39,21 @@ namespace BirdsiteLive.Domain
return $"[[BirdsiteLIVE-MigrationCode|{hash.Substring(0, 10)}]]";
}
public bool ValidateTweet(string acct, string tweetId)
public string GetDeletionCode(string acct)
{
var code = GetMigrationCode(acct);
var hash = GetHashString(acct);
return $"[[BirdsiteLIVE-DeletionCode|{hash.Substring(0, 10)}]]";
}
public bool ValidateTweet(string acct, string tweetId, MigrationTypeEnum type)
{
string code;
if (type == MigrationTypeEnum.Migration)
code = GetMigrationCode(acct);
else if (type == MigrationTypeEnum.Deletion)
code = GetDeletionCode(acct);
else
throw new NotImplementedException();
var castedTweetId = ExtractedTweetId(tweetId);
var tweet = _twitterTweetsService.GetTweet(castedTweetId);
@ -47,10 +61,10 @@ namespace BirdsiteLive.Domain
if (tweet == null)
throw new Exception("Tweet not found");
if (tweet.CreatorName.Trim().ToLowerInvariant() != acct.Trim().ToLowerInvariant())
if (tweet.CreatorName.Trim().ToLowerInvariant() != acct.Trim().ToLowerInvariant())
throw new Exception($"Tweet not published by @{acct}");
if (!tweet.MessageContent.Contains(code))
if (!tweet.MessageContent.Contains(code))
throw new Exception("Tweet don't have migration code");
return true;
@ -74,7 +88,7 @@ namespace BirdsiteLive.Domain
if (string.IsNullOrWhiteSpace(fediverseAcct))
throw new ArgumentException("Please provide Fediverse account");
if( !fediverseAcct.Contains('@') || !fediverseAcct.StartsWith("@") || fediverseAcct.Trim('@').Split('@').Length != 2)
if (!fediverseAcct.Contains('@') || !fediverseAcct.StartsWith("@") || fediverseAcct.Trim('@').Split('@').Length != 2)
throw new ArgumentException("Please provide valid Fediverse handle");
var objectId = await _activityPubService.GetUserIdAsync(fediverseAcct);
@ -98,15 +112,23 @@ namespace BirdsiteLive.Domain
if (twitterAccount == null)
{
await _twitterUserDal.CreateTwitterUserAsync(acct, -1, validatedUser.ObjectId, validatedUser.FediverseAcct);
}
else
{
twitterAccount.MovedTo = validatedUser.ObjectId;
twitterAccount.MovedToAcct = validatedUser.FediverseAcct;
await _twitterUserDal.UpdateTwitterUserAsync(twitterAccount);
twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
}
twitterAccount.MovedTo = validatedUser.ObjectId;
twitterAccount.MovedToAcct = validatedUser.FediverseAcct;
await _twitterUserDal.UpdateTwitterUserAsync(twitterAccount);
// Notify Followers
var message = $@"<p>[BSL MIRROR SERVICE NOTIFICATION]<br/>
This bot has been disabled by it's original owner.<br/>
It has been redirected to {validatedUser.FediverseAcct}.
</p>";
NotifyFollowers(acct, twitterAccount, message);
}
private void NotifyFollowers(string acct, SyncTwitterUser twitterAccount, string message)
{
var t = Task.Run(async () =>
{
var followers = await _followersDal.GetFollowersAsync(twitterAccount.Id);
@ -118,7 +140,8 @@ namespace BirdsiteLive.Domain
var actorUrl = UrlFactory.GetActorUrl(_instanceSettings.Domain, acct);
var noteUrl = UrlFactory.GetNoteUrl(_instanceSettings.Domain, acct, noteId);
var to = validatedUser.ObjectId;
//var to = validatedUser.ObjectId;
var to = follower.ActorId;
var cc = new string[0];
var note = new Note
@ -132,13 +155,11 @@ namespace BirdsiteLive.Domain
to = new[] { to },
cc = cc,
content = $@"<p>[MIRROR SERVICE NOTIFICATION]<br/>
This bot has been disabled by it's original owner.<br/>
It has been redirected to {validatedUser.FediverseAcct}.
</p>"
content = message
};
await _activityPubService.PostNewNoteActivity(note, acct, Guid.NewGuid().ToString(), follower.Host, follower.InboxRoute);
await _activityPubService.PostNewNoteActivity(note, acct, Guid.NewGuid().ToString(), follower.Host,
follower.InboxRoute);
}
catch (Exception e)
{
@ -148,11 +169,37 @@ namespace BirdsiteLive.Domain
});
}
public async Task DeleteAccountAsync(string acct)
{
// Apply moved to
var twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
if (twitterAccount == null)
{
await _twitterUserDal.CreateTwitterUserAsync(acct, -1);
twitterAccount = await _twitterUserDal.GetTwitterUserAsync(acct);
}
twitterAccount.Deleted = true;
await _twitterUserDal.UpdateTwitterUserAsync(twitterAccount);
// Notify Followers
var message = $@"<p>[BSL MIRROR SERVICE NOTIFICATION]<br/>
This bot has been deleted by it's original owner.<br/>
</p>";
NotifyFollowers(acct, twitterAccount, message);
}
public async Task TriggerRemoteMigrationAsync(string id, string tweetid, string handle)
{
//TODO
}
public async Task TriggerRemoteDeleteAsync(string id, string tweetid)
{
//TODO
}
private byte[] GetHash(string inputString)
{
using (HashAlgorithm algorithm = SHA256.Create())

View file

@ -49,12 +49,12 @@ namespace BirdsiteLive.Pipeline.Processors
{
var tweetId = tweets.Last().Id;
var now = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(user.Id, tweetId, tweetId, user.FetchingErrorCount, now, user.MovedTo, user.MovedToAcct);
await _twitterUserDal.UpdateTwitterUserAsync(user.Id, tweetId, tweetId, user.FetchingErrorCount, now, user.MovedTo, user.MovedToAcct, user.Deleted);
}
else
{
var now = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, user.FetchingErrorCount, now, user.MovedTo, user.MovedToAcct);
await _twitterUserDal.UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, user.FetchingErrorCount, now, user.MovedTo, user.MovedToAcct, user.Deleted);
}
}

View file

@ -48,7 +48,7 @@ namespace BirdsiteLive.Pipeline.Processors
var lastPostedTweet = userWithTweetsToSync.Tweets.Select(x => x.Id).Max();
var minimumSync = followingSyncStatuses.Min();
var now = DateTime.UtcNow;
await _twitterUserDal.UpdateTwitterUserAsync(userId, lastPostedTweet, minimumSync, userWithTweetsToSync.User.FetchingErrorCount, now, userWithTweetsToSync.User.MovedTo, userWithTweetsToSync.User.MovedToAcct);
await _twitterUserDal.UpdateTwitterUserAsync(userId, lastPostedTweet, minimumSync, userWithTweetsToSync.User.FetchingErrorCount, now, userWithTweetsToSync.User.MovedTo, userWithTweetsToSync.User.MovedToAcct, userWithTweetsToSync.User.Deleted);
}
catch (Exception e)
{

View file

@ -5,6 +5,7 @@ using System.Text;
using System.Threading.Tasks;
using Npgsql.TypeHandlers;
using BirdsiteLive.Domain;
using BirdsiteLive.Domain.Enum;
namespace BirdsiteLive.Controllers
{
@ -20,8 +21,8 @@ namespace BirdsiteLive.Controllers
#endregion
[HttpGet]
[Route("/migration/{id}")]
public IActionResult Index(string id)
[Route("/migration/move/{id}")]
public IActionResult IndexMove(string id)
{
var migrationCode = _migrationService.GetMigrationCode(id);
var data = new MigrationData()
@ -30,12 +31,26 @@ namespace BirdsiteLive.Controllers
MigrationCode = migrationCode
};
return View(data);
return View("Index", data);
}
[HttpGet]
[Route("/migration/delete/{id}")]
public IActionResult IndexDelete(string id)
{
var migrationCode = _migrationService.GetDeletionCode(id);
var data = new MigrationData()
{
Acct = id,
MigrationCode = migrationCode
};
return View("Delete", data);
}
[HttpPost]
[Route("/migration/{id}")]
public async Task<IActionResult> Migrate(string id, string tweetid, string handle)
[Route("/migration/move/{id}")]
public async Task<IActionResult> MigrateMove(string id, string tweetid, string handle)
{
var migrationCode = _migrationService.GetMigrationCode(id);
@ -56,7 +71,7 @@ namespace BirdsiteLive.Controllers
try
{
fediverseUserValidation = await _migrationService.ValidateFediverseAcctAsync(handle);
var isTweetValid = _migrationService.ValidateTweet(id, tweetid);
var isTweetValid = _migrationService.ValidateTweet(id, tweetid, MigrationTypeEnum.Migration);
data.IsAcctValid = fediverseUserValidation.IsValid;
data.IsTweetValid = isTweetValid;
@ -85,11 +100,55 @@ namespace BirdsiteLive.Controllers
}
[HttpPost]
[Route("/migration/{id}/{tweetid}/{handle}")]
public async Task<IActionResult> RemoteMigrate(string id, string tweetid, string handle)
[Route("/migration/delete/{id}")]
public async Task<IActionResult> MigrateDelete(string id, string tweetid)
{
var migrationCode = _migrationService.GetMigrationCode(id);
var data = new MigrationData()
{
Acct = id,
MigrationCode = migrationCode,
IsTweetProvided = !string.IsNullOrWhiteSpace(tweetid),
TweetId = tweetid
};
try
{
var isTweetValid = _migrationService.ValidateTweet(id, tweetid, MigrationTypeEnum.Migration);
data.IsTweetValid = isTweetValid;
}
catch (Exception e)
{
data.ErrorMessage = e.Message;
}
if (data.IsTweetValid)
{
try
{
await _migrationService.DeleteAccountAsync(id);
await _migrationService.TriggerRemoteDeleteAsync(id, tweetid);
data.MigrationSuccess = true;
}
catch (Exception e)
{
Console.WriteLine(e);
data.ErrorMessage = e.Message;
}
}
return View("Index", data);
}
[HttpPost]
[Route("/migration/move/{id}/{tweetid}/{handle}")]
public async Task<IActionResult> RemoteMigrateMove(string id, string tweetid, string handle)
{
var fediverseUserValidation = await _migrationService.ValidateFediverseAcctAsync(handle);
var isTweetValid = _migrationService.ValidateTweet(id, tweetid);
var isTweetValid = _migrationService.ValidateTweet(id, tweetid, MigrationTypeEnum.Deletion);
if (fediverseUserValidation.IsValid && isTweetValid)
{
@ -99,6 +158,13 @@ namespace BirdsiteLive.Controllers
return StatusCode(500);
}
[HttpPost]
[Route("/migration/delete/{id}/{tweetid}/{handle}")]
public async Task<IActionResult> RemoteDeleteMove(string id, string tweetid, string handle)
{
throw new NotImplementedException();
}
}

View file

@ -0,0 +1,44 @@
@model BirdsiteLive.Controllers.MigrationData
@{
ViewData["Title"] = "Migration";
}
<div class="col-12 col-sm-10 col-md-8 col-lg-6 mx-auto">
@if (!string.IsNullOrWhiteSpace(ViewData.Model.ErrorMessage))
{
<div class="alert alert-danger" role="alert">
@ViewData.Model.ErrorMessage
</div>
}
<h1 class="display-4 migration__title">Delete @@@ViewData.Model.Acct mirror</h1>
@if (!ViewData.Model.IsTweetProvided)
{
<h2 class="display-4 migration__subtitle">What is needed?</h2>
<p>You'll need access to the Twitter account to provide proof of ownership.</p>
<h2 class="display-4 migration__subtitle">What will deletion do?</h2>
<p>
Deletion will remove all followers, delete the account and will be blacklisted so that it can't be recreated.<br />
</p>
}
<h2 class="display-4 migration__subtitle">Start the deletion!</h2>
<p>Please copy and post this string in a public Tweet (the string must be untampered, but you can write anything you want before or after it):</p>
<input type="text" name="textbox" value="@ViewData.Model.MigrationCode" onclick="this.select()" class="form-control" readonly />
<br />
<h2 class="display-4 migration__subtitle">Provide deletion information:</h2>
<form method="POST">
<div class="form-group">
<label for="tweetid">Tweet URL</label>
<input type="text" class="form-control" id="tweetid" name="tweetid" autocomplete="off" placeholder="https://twitter.com/<username>/status/<tweet id>" value="@ViewData.Model.TweetId">
</div>
<button type="submit" class="btn btn-primary">Delete!</button>
</form>
</div>

View file

@ -11,7 +11,7 @@
</div>
}
<h1 class="display-4 migration__title">Migrate @@@ViewData.Model.Acct</h1>
<h1 class="display-4 migration__title">Migrate @@@ViewData.Model.Acct mirror to my Fediverse account</h1>
@if (!ViewData.Model.IsAcctProvided && !ViewData.Model.IsTweetProvided)
{
@ -21,16 +21,17 @@
<h2 class="display-4 migration__subtitle">What will migration do?</h2>
<p>Migration will transfer the followers to the provided account.<br/>
After a week, the account will be deleted. It will also be blacklisted so that it can't be recreated.</p>
<p>
Migration will notify followers of the migration of the mirror account to your fediverse account and will be disabled after that.<br />
</p>
}
<h2 class="display-4 migration__subtitle">Start the migration!</h2>
<p>Please copy and post this string in a Tweet (the string must be untampered, but you can write anything you want before or after it):</p>
<p>Please copy and post this string in a public Tweet (the string must be untampered, but you can write anything you want before or after it):</p>
<input type="text" name="textbox" value="@ViewData.Model.MigrationCode" onclick="this.select()" class="form-control" readonly/>
<br/>
<input type="text" name="textbox" value="@ViewData.Model.MigrationCode" onclick="this.select()" class="form-control" readonly />
<br />
<h2 class="display-4 migration__subtitle">Provide migration information:</h2>
<form method="POST">
@ -49,4 +50,10 @@
</div>
<button type="submit" class="btn btn-primary">Migrate!</button>
</form>
<br />
<br />
<br />
<div class="user-owner">
<a href="/migration/delete/@ViewData.Model.Acct">I don't have a fediverse account and I'd like to delete this mirror.</a>
</div>
</div>

View file

@ -47,6 +47,6 @@
}
<div class="user-owner">
<a href="/migration/@ViewData.Model.Acct">I'm the owner of this account and I'm now on the Fediverse</a>
<a href="/migration/move/@ViewData.Model.Acct">I'm the owner of this account and I would like to take control of this mirror.</a>
</div>
</div>

View file

@ -119,14 +119,14 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
}
}
public async Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, int fetchingErrorCount, DateTime lastSync, string movedTo, string movedToAcct)
public async Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, int fetchingErrorCount, DateTime lastSync, string movedTo, string movedToAcct, bool deleted)
{
if(id == default) throw new ArgumentException("id");
if(lastTweetPostedId == default) throw new ArgumentException("lastTweetPostedId");
if(lastTweetSynchronizedForAllFollowersId == default) throw new ArgumentException("lastTweetSynchronizedForAllFollowersId");
if(lastSync == default) throw new ArgumentException("lastSync");
var query = $"UPDATE {_settings.TwitterUserTableName} SET lastTweetPostedId = @lastTweetPostedId, lastTweetSynchronizedForAllFollowersId = @lastTweetSynchronizedForAllFollowersId, fetchingErrorCount = @fetchingErrorCount, lastSync = @lastSync, movedTo = @movedTo, movedToAcct = @movedToAcct WHERE id = @id";
var query = $"UPDATE {_settings.TwitterUserTableName} SET lastTweetPostedId = @lastTweetPostedId, lastTweetSynchronizedForAllFollowersId = @lastTweetSynchronizedForAllFollowersId, fetchingErrorCount = @fetchingErrorCount, lastSync = @lastSync, movedTo = @movedTo, movedToAcct = @movedToAcct, deleted = @deleted WHERE id = @id";
using (var dbConnection = Connection)
{
@ -140,14 +140,15 @@ namespace BirdsiteLive.DAL.Postgres.DataAccessLayers
fetchingErrorCount,
lastSync = lastSync.ToUniversalTime(),
movedTo,
movedToAcct
movedToAcct,
deleted
});
}
}
public async Task UpdateTwitterUserAsync(SyncTwitterUser user)
{
await UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, user.FetchingErrorCount, user.LastSync, user.MovedTo, user.MovedToAcct);
await UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, user.FetchingErrorCount, user.LastSync, user.MovedTo, user.MovedToAcct, user.Deleted);
}
public async Task DeleteTwitterUserAsync(string acct)

View file

@ -12,7 +12,7 @@ namespace BirdsiteLive.DAL.Contracts
Task<SyncTwitterUser> GetTwitterUserAsync(int id);
Task<SyncTwitterUser[]> GetAllTwitterUsersAsync(int maxNumber);
Task<SyncTwitterUser[]> GetAllTwitterUsersAsync();
Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, int fetchingErrorCount, DateTime lastSync, string movedTo, string movedToAcct);
Task UpdateTwitterUserAsync(int id, long lastTweetPostedId, long lastTweetSynchronizedForAllFollowersId, int fetchingErrorCount, DateTime lastSync, string movedTo, string movedToAcct, bool deleted);
Task UpdateTwitterUserAsync(SyncTwitterUser user);
Task DeleteTwitterUserAsync(string acct);
Task DeleteTwitterUserAsync(int id);

View file

@ -16,5 +16,7 @@ namespace BirdsiteLive.DAL.Models
public string MovedTo { get; set; }
public string MovedToAcct { get; set; }
public bool Deleted { get; set; } //TODO: update DAL
}
}

View file

@ -109,7 +109,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
var updatedLastSyncId = 1550L;
var now = DateTime.Now;
var errors = 15;
await dal.UpdateTwitterUserAsync(result.Id, updatedLastTweetId, updatedLastSyncId, errors, now, null, null);
await dal.UpdateTwitterUserAsync(result.Id, updatedLastTweetId, updatedLastSyncId, errors, now, null, null, false);
result = await dal.GetTwitterUserAsync(acct);
@ -140,7 +140,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
var errors = 15;
var movedTo = "https://";
var movedToAcct = "@account@instance";
await dal.UpdateTwitterUserAsync(result.Id, updatedLastTweetId, updatedLastSyncId, errors, now, movedTo, movedToAcct);
await dal.UpdateTwitterUserAsync(result.Id, updatedLastTweetId, updatedLastSyncId, errors, now, movedTo, movedToAcct, false);
result = await dal.GetTwitterUserAsync(acct);
@ -222,7 +222,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
public async Task Update_NoId()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(default, default, default, default, DateTime.UtcNow, null, null);
await dal.UpdateTwitterUserAsync(default, default, default, default, DateTime.UtcNow, null, null, false);
}
[TestMethod]
@ -230,7 +230,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
public async Task Update_NoLastTweetPostedId()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(12, default, default, default, DateTime.UtcNow, null, null);
await dal.UpdateTwitterUserAsync(12, default, default, default, DateTime.UtcNow, null, null, false);
}
[TestMethod]
@ -238,7 +238,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
public async Task Update_NoLastTweetSynchronizedForAllFollowersId()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(12, 9556, default, default, DateTime.UtcNow, null, null);
await dal.UpdateTwitterUserAsync(12, 9556, default, default, DateTime.UtcNow, null, null, false);
}
[TestMethod]
@ -246,7 +246,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
public async Task Update_NoLastSync()
{
var dal = new TwitterUserPostgresDal(_settings);
await dal.UpdateTwitterUserAsync(12, 9556, 65, default, default, null, null);
await dal.UpdateTwitterUserAsync(12, 9556, 65, default, default, null, null, false);
}
[TestMethod]
@ -381,7 +381,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
{
var user = allUsers[i];
var date = i % 2 == 0 ? oldest : newest;
await dal.UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, 0, date, null, null);
await dal.UpdateTwitterUserAsync(user.Id, user.LastTweetPostedId, user.LastTweetSynchronizedForAllFollowersId, 0, date, null, null, false);
}
var result = await dal.GetAllTwitterUsersAsync(10);
@ -453,7 +453,7 @@ namespace BirdsiteLive.DAL.Postgres.Tests.DataAccessLayers
if (i == 0 || i == 2 || i == 3)
{
var t = await dal.GetTwitterUserAsync(acct);
await dal.UpdateTwitterUserAsync(t.Id ,1L,2L, 50+i*2, DateTime.Now, null, null);
await dal.UpdateTwitterUserAsync(t.Id ,1L,2L, 50+i*2, DateTime.Now, null, null, false);
}
}

View file

@ -66,7 +66,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
It.Is<int>(y => y == 0),
It.IsAny<DateTime>(),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null)
It.Is<string>(y => y == null),
It.Is<bool>(y => y == false)
))
.Returns(Task.CompletedTask);

View file

@ -68,7 +68,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
It.Is<int>(y => y == 0),
It.IsAny<DateTime>(),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null)
It.Is<string>(y => y == null),
It.Is<bool>(y => y == false)
))
.Returns(Task.CompletedTask);
@ -137,7 +138,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
It.Is<int>(y => y == 0),
It.IsAny<DateTime>(),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null)
It.Is<string>(y => y == null),
It.Is<bool>(y => y == false)
))
.Throws(new ArgumentException());
@ -208,7 +210,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
It.Is<int>(y => y == 0),
It.IsAny<DateTime>(),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null)
It.Is<string>(y => y == null),
It.Is<bool>(y => y == false)
))
.Returns(Task.CompletedTask);
@ -289,7 +292,8 @@ namespace BirdsiteLive.Pipeline.Tests.Processors
It.Is<int>(y => y == 0),
It.IsAny<DateTime>(),
It.Is<string>(y => y == null),
It.Is<string>(y => y == null)
It.Is<string>(y => y == null),
It.Is<bool>(y => y == false)
))
.Returns(Task.CompletedTask);