@ -0,0 +1,51 @@ | |||
# BSLManager | |||
A CLI is provided in the Docker image so that admins can manage their instance. | |||
## Access to the CLI | |||
Since the CLI is packaged into the docker image, you'll have to open a shell from the container. To do so, list first your running containers: | |||
``` | |||
docker ps | |||
``` | |||
This should display you something equivalent to this: | |||
``` | |||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | |||
3734c41af5a7 postgres:9.6 "docker-entrypoint.s…" 2 weeks ago Up 2 weeks 5432/tcp db_1 | |||
be6870fe103e nicolasconstant/birdsitelive:latest "dotnet BirdsiteLive…" 6 weeks ago Up 2 weeks 443/tcp, 0.0.0.0:5000->80/tcp birdsitelive | |||
``` | |||
Find the BSL container and keep the ID, here it's `be6870fe103e`. And you only need the three first char to identify it, so we'll be using `be6`. | |||
Then open a shell inside the container (change `be6` with your own id): | |||
``` | |||
docker exec -it be6 /bin/bash | |||
``` | |||
And you should now be inside the container, and all you have to do is calling the CLI: | |||
``` | |||
./BSLManager | |||
``` | |||
## Setting up the CLI | |||
The manager will ask you to provide information about the database and the instance. | |||
Those must be same than the ones in the `docker-compose.yml` file. | |||
Provide the information, review it and validate it. Then the CLI UI should shows up. | |||
## Using the CLI | |||
You can navigate between the sections with the arrows and tab keys. | |||
The **filter** permits to filter the list of users with a pattern. | |||
All users have their followings count provided next to them. | |||
You can select any user by using the up/down arrow keys and hitting the `Enter` key, this will display more information about the user. | |||
You can also remove a user and all their followings by hitting the `Del` key. You will be prompted by a confirmation message, and you'll be able to remove this user. | |||
Deleting users having a lots of followings can take some time: after the prompt has closed the process is still running and will update the list after that. Let the software do its thing and it will go through. |
@ -0,0 +1,244 @@ | |||
using System; | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using System.Reflection; | |||
using System.Threading.Tasks; | |||
using BirdsiteLive.DAL.Contracts; | |||
using BirdsiteLive.DAL.Models; | |||
using BirdsiteLive.Moderation.Actions; | |||
using BSLManager.Domain; | |||
using BSLManager.Tools; | |||
using Terminal.Gui; | |||
namespace BSLManager | |||
{ | |||
public class App | |||
{ | |||
private readonly IFollowersDal _followersDal; | |||
private readonly IRemoveFollowerAction _removeFollowerAction; | |||
private readonly FollowersListState _state = new FollowersListState(); | |||
#region Ctor | |||
public App(IFollowersDal followersDal, IRemoveFollowerAction removeFollowerAction) | |||
{ | |||
_followersDal = followersDal; | |||
_removeFollowerAction = removeFollowerAction; | |||
} | |||
#endregion | |||
public void Run() | |||
{ | |||
Application.Init(); | |||
var top = Application.Top; | |||
// Creates the top-level window to show | |||
var win = new Window("BSL Manager") | |||
{ | |||
X = 0, | |||
Y = 1, // Leave one row for the toplevel menu | |||
// By using Dim.Fill(), it will automatically resize without manual intervention | |||
Width = Dim.Fill(), | |||
Height = Dim.Fill() | |||
}; | |||
top.Add(win); | |||
// Creates a menubar, the item "New" has a help menu. | |||
var menu = new MenuBar(new MenuBarItem[] | |||
{ | |||
new MenuBarItem("_File", new MenuItem[] | |||
{ | |||
new MenuItem("_Quit", "", () => | |||
{ | |||
if (Quit()) top.Running = false; | |||
}) | |||
}), | |||
//new MenuBarItem ("_Edit", new MenuItem [] { | |||
// new MenuItem ("_Copy", "", null), | |||
// new MenuItem ("C_ut", "", null), | |||
// new MenuItem ("_Paste", "", null) | |||
//}) | |||
}); | |||
top.Add(menu); | |||
static bool Quit() | |||
{ | |||
var n = MessageBox.Query(50, 7, "Quit BSL Manager", "Are you sure you want to quit?", "Yes", "No"); | |||
return n == 0; | |||
} | |||
RetrieveUserList(); | |||
var list = new ListView(_state.GetDisplayableList()) | |||
{ | |||
X = 1, | |||
Y = 3, | |||
Width = Dim.Fill(), | |||
Height = Dim.Fill() | |||
}; | |||
list.KeyDown += _ => | |||
{ | |||
if (_.KeyEvent.Key == Key.Enter) | |||
{ | |||
OpenFollowerDialog(list.SelectedItem); | |||
} | |||
else if (_.KeyEvent.Key == Key.Delete | |||
|| _.KeyEvent.Key == Key.DeleteChar | |||
|| _.KeyEvent.Key == Key.Backspace | |||
|| _.KeyEvent.Key == Key.D) | |||
{ | |||
OpenDeleteDialog(list.SelectedItem); | |||
} | |||
}; | |||
var listingFollowersLabel = new Label(1, 0, "Listing followers"); | |||
var filterLabel = new Label("Filter: ") { X = 1, Y = 1 }; | |||
var filterText = new TextField("") | |||
{ | |||
X = Pos.Right(filterLabel), | |||
Y = 1, | |||
Width = 40 | |||
}; | |||
filterText.KeyDown += _ => | |||
{ | |||
var text = filterText.Text.ToString(); | |||
if (_.KeyEvent.Key == Key.Enter && !string.IsNullOrWhiteSpace(text)) | |||
{ | |||
_state.FilterBy(text); | |||
ConsoleGui.RefreshUI(); | |||
} | |||
}; | |||
win.Add( | |||
listingFollowersLabel, | |||
filterLabel, | |||
filterText, | |||
list | |||
); | |||
Application.Run(); | |||
} | |||
private void OpenFollowerDialog(int selectedIndex) | |||
{ | |||
var close = new Button(3, 14, "Close"); | |||
close.Clicked += () => Application.RequestStop(); | |||
var dialog = new Dialog("Info", 60, 18, close); | |||
var follower = _state.GetElementAt(selectedIndex); | |||
var name = new Label($"User: @{follower.Acct}@{follower.Host}") | |||
{ | |||
X = 1, | |||
Y = 1, | |||
Width = Dim.Fill(), | |||
Height = 1 | |||
}; | |||
var following = new Label($"Following Count: {follower.Followings.Count}") | |||
{ | |||
X = 1, | |||
Y = 3, | |||
Width = Dim.Fill(), | |||
Height = 1 | |||
}; | |||
var inbox = new Label($"Inbox: {follower.InboxRoute}") | |||
{ | |||
X = 1, | |||
Y = 4, | |||
Width = Dim.Fill(), | |||
Height = 1 | |||
}; | |||
var sharedInbox = new Label($"Shared Inbox: {follower.SharedInboxRoute}") | |||
{ | |||
X = 1, | |||
Y = 5, | |||
Width = Dim.Fill(), | |||
Height = 1 | |||
}; | |||
dialog.Add(name); | |||
dialog.Add(following); | |||
dialog.Add(inbox); | |||
dialog.Add(sharedInbox); | |||
dialog.Add(close); | |||
Application.Run(dialog); | |||
} | |||
private void OpenDeleteDialog(int selectedIndex) | |||
{ | |||
bool okpressed = false; | |||
var ok = new Button(10, 14, "Yes"); | |||
ok.Clicked += () => | |||
{ | |||
Application.RequestStop(); | |||
okpressed = true; | |||
}; | |||
var cancel = new Button(3, 14, "No"); | |||
cancel.Clicked += () => Application.RequestStop(); | |||
var dialog = new Dialog("Delete", 60, 18, cancel, ok); | |||
var follower = _state.GetElementAt(selectedIndex); | |||
var name = new Label($"User: @{follower.Acct}@{follower.Host}") | |||
{ | |||
X = 1, | |||
Y = 1, | |||
Width = Dim.Fill(), | |||
Height = 1 | |||
}; | |||
var entry = new Label("Delete user and remove all their followings?") | |||
{ | |||
X = 1, | |||
Y = 3, | |||
Width = Dim.Fill(), | |||
Height = 1 | |||
}; | |||
dialog.Add(name); | |||
dialog.Add(entry); | |||
Application.Run(dialog); | |||
if (okpressed) | |||
{ | |||
DeleteAndRemoveUser(selectedIndex); | |||
} | |||
} | |||
private void DeleteAndRemoveUser(int el) | |||
{ | |||
Application.MainLoop.Invoke(async () => | |||
{ | |||
try | |||
{ | |||
var userToDelete = _state.GetElementAt(el); | |||
BasicLogger.Log($"Delete {userToDelete.Acct}@{userToDelete.Host}"); | |||
await _removeFollowerAction.ProcessAsync(userToDelete); | |||
BasicLogger.Log($"Remove user from list"); | |||
_state.RemoveAt(el); | |||
} | |||
catch (Exception e) | |||
{ | |||
BasicLogger.Log(e.Message); | |||
} | |||
ConsoleGui.RefreshUI(); | |||
}); | |||
} | |||
private void RetrieveUserList() | |||
{ | |||
Application.MainLoop.Invoke(async () => | |||
{ | |||
var followers = await _followersDal.GetAllFollowersAsync(); | |||
_state.Load(followers.ToList()); | |||
ConsoleGui.RefreshUI(); | |||
}); | |||
} | |||
} | |||
} |
@ -0,0 +1,94 @@ | |||
using System; | |||
using System.Net.Http; | |||
using BirdsiteLive.Common.Settings; | |||
using BirdsiteLive.Common.Structs; | |||
using BirdsiteLive.DAL.Contracts; | |||
using BirdsiteLive.DAL.Postgres.DataAccessLayers; | |||
using BirdsiteLive.DAL.Postgres.Settings; | |||
using Lamar; | |||
using Lamar.Scanning.Conventions; | |||
using Microsoft.Extensions.DependencyInjection; | |||
using Microsoft.Extensions.Logging; | |||
namespace BSLManager | |||
{ | |||
public class Bootstrapper | |||
{ | |||
private readonly DbSettings _dbSettings; | |||
private readonly InstanceSettings _instanceSettings; | |||
#region Ctor | |||
public Bootstrapper(DbSettings dbSettings, InstanceSettings instanceSettings) | |||
{ | |||
_dbSettings = dbSettings; | |||
_instanceSettings = instanceSettings; | |||
} | |||
#endregion | |||
public Container Init() | |||
{ | |||
var container = new Container(x => | |||
{ | |||
x.For<DbSettings>().Use(x => _dbSettings); | |||
x.For<InstanceSettings>().Use(x => _instanceSettings); | |||
if (string.Equals(_dbSettings.Type, DbTypes.Postgres, StringComparison.OrdinalIgnoreCase)) | |||
{ | |||
var connString = $"Host={_dbSettings.Host};Username={_dbSettings.User};Password={_dbSettings.Password};Database={_dbSettings.Name}"; | |||
var postgresSettings = new PostgresSettings | |||
{ | |||
ConnString = connString | |||
}; | |||
x.For<PostgresSettings>().Use(x => postgresSettings); | |||
x.For<ITwitterUserDal>().Use<TwitterUserPostgresDal>().Singleton(); | |||
x.For<IFollowersDal>().Use<FollowersPostgresDal>().Singleton(); | |||
x.For<IDbInitializerDal>().Use<DbInitializerPostgresDal>().Singleton(); | |||
} | |||
else | |||
{ | |||
throw new NotImplementedException($"{_dbSettings.Type} is not supported"); | |||
} | |||
var serviceProvider = new ServiceCollection().AddHttpClient().BuildServiceProvider(); | |||
x.For<IHttpClientFactory>().Use(_ => serviceProvider.GetService<IHttpClientFactory>()); | |||
x.For(typeof(ILogger<>)).Use(typeof(DummyLogger<>)); | |||
x.Scan(_ => | |||
{ | |||
_.Assembly("BirdsiteLive.Twitter"); | |||
_.Assembly("BirdsiteLive.Domain"); | |||
_.Assembly("BirdsiteLive.DAL"); | |||
_.Assembly("BirdsiteLive.DAL.Postgres"); | |||
_.Assembly("BirdsiteLive.Moderation"); | |||
_.TheCallingAssembly(); | |||
_.WithDefaultConventions(); | |||
_.LookForRegistries(); | |||
}); | |||
}); | |||
return container; | |||
} | |||
public class DummyLogger<T> : ILogger<T> | |||
{ | |||
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter) | |||
{ | |||
} | |||
public bool IsEnabled(LogLevel logLevel) | |||
{ | |||
return false; | |||
} | |||
public IDisposable BeginScope<TState>(TState state) | |||
{ | |||
return null; | |||
} | |||
} | |||
} | |||
} |
@ -0,0 +1,81 @@ | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using BirdsiteLive.DAL.Models; | |||
namespace BSLManager.Domain | |||
{ | |||
public class FollowersListState | |||
{ | |||
private readonly List<string> _filteredDisplayableUserList = new List<string>(); | |||
private List<Follower> _sourceUserList = new List<Follower>(); | |||
private List<Follower> _filteredSourceUserList = new List<Follower>(); | |||
public void Load(List<Follower> followers) | |||
{ | |||
_sourceUserList = followers.OrderByDescending(x => x.Followings.Count).ToList(); | |||
ResetLists(); | |||
} | |||
private void ResetLists() | |||
{ | |||
_filteredSourceUserList = _sourceUserList.ToList(); | |||
_filteredDisplayableUserList.Clear(); | |||
foreach (var follower in _sourceUserList) | |||
{ | |||
var displayedUser = $"{GetFullHandle(follower)} ({follower.Followings.Count})"; | |||
_filteredDisplayableUserList.Add(displayedUser); | |||
} | |||
} | |||
public List<string> GetDisplayableList() | |||
{ | |||
return _filteredDisplayableUserList; | |||
} | |||
public void FilterBy(string pattern) | |||
{ | |||
ResetLists(); | |||
if (!string.IsNullOrWhiteSpace(pattern)) | |||
{ | |||
var elToRemove = _filteredSourceUserList | |||
.Where(x => !GetFullHandle(x).Contains(pattern)) | |||
.Select(x => x) | |||
.ToList(); | |||
foreach (var el in elToRemove) | |||
{ | |||
_filteredSourceUserList.Remove(el); | |||
var dElToRemove = _filteredDisplayableUserList.First(x => x.Contains(GetFullHandle(el))); | |||
_filteredDisplayableUserList.Remove(dElToRemove); | |||
} | |||
} | |||
} | |||
private string GetFullHandle(Follower follower) | |||
{ | |||
return $"@{follower.Acct}@{follower.Host}"; | |||
} | |||
public void RemoveAt(int index) | |||
{ | |||
var displayableUser = _filteredDisplayableUserList[index]; | |||
var sourceUser = _filteredSourceUserList[index]; | |||
_filteredDisplayableUserList.Remove(displayableUser); | |||
_filteredSourceUserList.Remove(sourceUser); | |||
_sourceUserList.Remove(sourceUser); | |||
} | |||
public Follower GetElementAt(int index) | |||
{ | |||
return _filteredSourceUserList[index]; | |||
} | |||
} | |||
} |
@ -0,0 +1,13 @@ | |||
using System; | |||
using System.IO; | |||
namespace BSLManager.Tools | |||
{ | |||
public static class BasicLogger | |||
{ | |||
public static void Log(string log) | |||
{ | |||
File.AppendAllLines($"Log-{Guid.NewGuid()}.txt", new []{ log }); | |||
} | |||
} | |||
} |
@ -0,0 +1,15 @@ | |||
using System.Reflection; | |||
using Terminal.Gui; | |||
namespace BSLManager.Tools | |||
{ | |||
public static class ConsoleGui | |||
{ | |||
public static void RefreshUI() | |||
{ | |||
typeof(Application) | |||
.GetMethod("TerminalResized", BindingFlags.Static | BindingFlags.NonPublic) | |||
.Invoke(null, null); | |||
} | |||
} | |||
} |
@ -0,0 +1,124 @@ | |||
using System; | |||
using System.IO; | |||
using System.Runtime.CompilerServices; | |||
using BirdsiteLive.Common.Settings; | |||
using Newtonsoft.Json; | |||
using Org.BouncyCastle.Asn1.IsisMtt.X509; | |||
namespace BSLManager.Tools | |||
{ | |||
public class SettingsManager | |||
{ | |||
private const string LocalFileName = "ManagerSettings.json"; | |||
public (DbSettings dbSettings, InstanceSettings instanceSettings) GetSettings() | |||
{ | |||
var localSettingsData = GetLocalSettingsFile(); | |||
if (localSettingsData != null) return Convert(localSettingsData); | |||
Console.WriteLine("We need to set up the manager"); | |||
Console.WriteLine("Please provide the following information as provided in the docker-compose file"); | |||
LocalSettingsData data; | |||
do | |||
{ | |||
data = GetDataFromUser(); | |||
Console.WriteLine(); | |||
Console.WriteLine("Please check if all is ok:"); | |||
Console.WriteLine(); | |||
Console.WriteLine($"Db Host: {data.DbHost}"); | |||
Console.WriteLine($"Db Name: {data.DbName}"); | |||
Console.WriteLine($"Db User: {data.DbUser}"); | |||
Console.WriteLine($"Db Password: {data.DbPassword}"); | |||
Console.WriteLine($"Instance Domain: {data.InstanceDomain}"); | |||
Console.WriteLine(); | |||
string resp; | |||
do | |||
{ | |||
Console.WriteLine("Is it valid? (yes, no)"); | |||
resp = Console.ReadLine()?.Trim().ToLowerInvariant(); | |||
if (resp == "n" || resp == "no") data = null; | |||
} while (resp != "y" && resp != "yes" && resp != "n" && resp != "no"); | |||
} while (data == null); | |||
SaveLocalSettings(data); | |||
return Convert(data); | |||
} | |||
private LocalSettingsData GetDataFromUser() | |||
{ | |||
var data = new LocalSettingsData(); | |||
Console.WriteLine("Db Host:"); | |||
data.DbHost = Console.ReadLine(); | |||
Console.WriteLine("Db Name:"); | |||
data.DbName = Console.ReadLine(); | |||
Console.WriteLine("Db User:"); | |||
data.DbUser = Console.ReadLine(); | |||
Console.WriteLine("Db Password:"); | |||
data.DbPassword = Console.ReadLine(); | |||
Console.WriteLine("Instance Domain:"); | |||
data.InstanceDomain = Console.ReadLine(); | |||
return data; | |||
} | |||
private (DbSettings dbSettings, InstanceSettings instanceSettings) Convert(LocalSettingsData data) | |||
{ | |||
var dbSettings = new DbSettings | |||
{ | |||
Type = data.DbType, | |||
Host = data.DbHost, | |||
Name = data.DbName, | |||
User = data.DbUser, | |||
Password = data.DbPassword | |||
}; | |||
var instancesSettings = new InstanceSettings | |||
{ | |||
Domain = data.InstanceDomain | |||
}; | |||
return (dbSettings, instancesSettings); | |||
} | |||
private LocalSettingsData GetLocalSettingsFile() | |||
{ | |||
try | |||
{ | |||
if (!File.Exists(LocalFileName)) return null; | |||
var jsonContent = File.ReadAllText(LocalFileName); | |||
var content = JsonConvert.DeserializeObject<LocalSettingsData>(jsonContent); | |||
return content; | |||
} | |||
catch (Exception) | |||
{ | |||
return null; | |||
} | |||
} | |||
private void SaveLocalSettings(LocalSettingsData data) | |||
{ | |||
var jsonContent = JsonConvert.SerializeObject(data); | |||
File.WriteAllText(LocalFileName, jsonContent); | |||
} | |||
} | |||
internal class LocalSettingsData | |||
{ | |||
public string DbType { get; set; } = "postgres"; | |||
public string DbHost { get; set; } | |||
public string DbName { get; set; } | |||
public string DbUser { get; set; } | |||
public string DbPassword { get; set; } | |||
public string InstanceDomain { get; set; } | |||
} | |||
} |
@ -0,0 +1,20 @@ | |||
<Project Sdk="Microsoft.NET.Sdk"> | |||
<PropertyGroup> | |||
<TargetFramework>netcoreapp3.1</TargetFramework> | |||
<IsPackable>false</IsPackable> | |||
</PropertyGroup> | |||
<ItemGroup> | |||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" /> | |||
<PackageReference Include="MSTest.TestAdapter" Version="2.1.1" /> | |||
<PackageReference Include="MSTest.TestFramework" Version="2.1.1" /> | |||
<PackageReference Include="coverlet.collector" Version="1.3.0" /> | |||
</ItemGroup> | |||
<ItemGroup> | |||
<ProjectReference Include="..\..\BSLManager\BSLManager.csproj" /> | |||
</ItemGroup> | |||
</Project> |
@ -0,0 +1,307 @@ | |||
using System.Collections.Generic; | |||
using System.Linq; | |||
using BirdsiteLive.DAL.Models; | |||
using BSLManager.Domain; | |||
using Microsoft.VisualStudio.TestTools.UnitTesting; | |||
namespace BSLManager.Tests | |||
{ | |||
[TestClass] | |||
public class FollowersListStateTests | |||
{ | |||
[TestMethod] | |||
public void FilterBy() | |||
{ | |||
#region Stub | |||
var followers = new List<Follower> | |||
{ | |||
new Follower | |||
{ | |||
Id = 0, | |||
Acct = "test", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 1, | |||
Acct = "test", | |||
Host = "host2", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 2, | |||
Acct = "user1", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 3, | |||
Acct = "user2", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
} | |||
}; | |||
#endregion | |||
var state = new FollowersListState(); | |||
state.Load(followers); | |||
state.FilterBy("test"); | |||
#region Validate | |||
Assert.AreEqual(2, state.GetDisplayableList().Count); | |||
#endregion | |||
} | |||
[TestMethod] | |||
public void FilterBy_GetElement() | |||
{ | |||
#region Stub | |||
var followers = new List<Follower> | |||
{ | |||
new Follower | |||
{ | |||
Id = 0, | |||
Acct = "test", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 1, | |||
Acct = "test", | |||
Host = "host2", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 2, | |||
Acct = "user1", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 3, | |||
Acct = "user2", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
} | |||
}; | |||
#endregion | |||
var state = new FollowersListState(); | |||
state.Load(followers); | |||
state.FilterBy("test"); | |||
var el = state.GetElementAt(1); | |||
#region Validate | |||
Assert.AreEqual(followers[1].Id, el.Id); | |||
#endregion | |||
} | |||
[TestMethod] | |||
public void GetElement() | |||
{ | |||
#region Stub | |||
var followers = new List<Follower> | |||
{ | |||
new Follower | |||
{ | |||
Id = 0, | |||
Acct = "test", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 1, | |||
Acct = "test", | |||
Host = "host2", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 2, | |||
Acct = "user1", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 3, | |||
Acct = "user2", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
} | |||
}; | |||
#endregion | |||
var state = new FollowersListState(); | |||
state.Load(followers); | |||
var el = state.GetElementAt(2); | |||
#region Validate | |||
Assert.AreEqual(followers[2].Id, el.Id); | |||
#endregion | |||
} | |||
[TestMethod] | |||
public void FilterBy_RemoveAt() | |||
{ | |||
#region Stub | |||
var followers = new List<Follower> | |||
{ | |||
new Follower | |||
{ | |||
Id = 0, | |||
Acct = "test", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 1, | |||
Acct = "test", | |||
Host = "host2", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 2, | |||
Acct = "user1", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 3, | |||
Acct = "user2", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
} | |||
}; | |||
#endregion | |||
var state = new FollowersListState(); | |||
state.Load(followers.ToList()); | |||
state.FilterBy("test"); | |||
state.RemoveAt(1); | |||
var list = state.GetDisplayableList(); | |||
#region Validate | |||
Assert.AreEqual(1, list.Count); | |||
Assert.IsTrue(list[0].Contains("@test@host1")); | |||
#endregion | |||
} | |||
[TestMethod] | |||
public void RemoveAt() | |||
{ | |||
#region Stub | |||
var followers = new List<Follower> | |||
{ | |||
new Follower | |||
{ | |||
Id = 0, | |||
Acct = "test", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 1, | |||
Acct = "test", | |||
Host = "host2", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 2, | |||
Acct = "user1", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 3, | |||
Acct = "user2", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
} | |||
}; | |||
#endregion | |||
var state = new FollowersListState(); | |||
state.Load(followers.ToList()); | |||
state.RemoveAt(1); | |||
var list = state.GetDisplayableList(); | |||
#region Validate | |||
Assert.AreEqual(3, list.Count); | |||
Assert.IsTrue(list[0].Contains("@test@host1")); | |||
Assert.IsFalse(list[1].Contains("@test@host2")); | |||
#endregion | |||
} | |||
[TestMethod] | |||
public void FilterBy_ResetFilter() | |||
{ | |||
#region Stub | |||
var followers = new List<Follower> | |||
{ | |||
new Follower | |||
{ | |||
Id = 0, | |||
Acct = "test", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 1, | |||
Acct = "test", | |||
Host = "host2", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 2, | |||
Acct = "user1", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
}, | |||
new Follower | |||
{ | |||
Id = 3, | |||
Acct = "user2", | |||
Host = "host1", | |||
Followings = new List<int>() | |||
} | |||
}; | |||
#endregion | |||
var state = new FollowersListState(); | |||
state.Load(followers.ToList()); | |||
#region Validate | |||
state.FilterBy("data"); | |||
var list = state.GetDisplayableList(); | |||
Assert.AreEqual(0, list.Count); | |||
state.FilterBy(string.Empty); | |||
list = state.GetDisplayableList(); | |||
Assert.AreEqual(4, list.Count); | |||
#endregion | |||
} | |||
} | |||
} |