Logon Workshop
Setup
Run our legacy project
git clone
https://ptrstpp950@bitbucket.org/ptrstpp950/logonworkshop.git
Who am I?

The story
Long long time ago, far far away in our company somebody created a login page :)
It has small feature: when user 3 times in a row enters bad credentials, the login is blocked for 1 minute
The users frequently create service calls about blocked account
The boss said: Do something with it!!
And now your are here looking into the code
Let's create a Unit test project
- Right click on solution -> Add -> New project
- Visual C# -> Test
- Name it as you wish
References
- Add reference to LogonWorkshop project
- Add reference to System.Web
- Install System.Web.Mvc package from solution
Our first test method
[TestMethod]
public void TestMethodSuccessLogin()
{
var password = "123";
var user = "test";
var controller = new HomeController();
controller.Index(new LoginModel() {User = user, Password = password});
}
[TestMethod]
public void TestMethodSuccessLogin()
{
var password = "123";
var user = "test";
var controller = new HomeController();
controller.Index(new LoginModel() {User = user, Password = password});
}
Connection string problem
We need to add it in the constructor of HomeController
private string connectionString;
public HomeController() : this
(ConfigurationManager.ConnectionStrings["ProjectDB"].ConnectionString)
{
}
public HomeController(string connectionString)
{
this.connectionString = connectionString;
}
private string connectionString;
public HomeController() : this
(ConfigurationManager.ConnectionStrings["ProjectDB"].ConnectionString)
{
}
public HomeController(string connectionString)
{
this.connectionString = connectionString;
}
Fix unit test
Options
- Pass something in constructor
- Use database in unit test - NO!! until there is no other option
- Mock/stub database - this needs some refactoring
Extract SQL methods
- Select code and press CTRL+R,M (or in context menu select Refactor->Extract method)
- Change them from
private static
topublic
- Move them to new class like
SQLrepository
- Fix errors
- If you are lost just checkout step1 branch from git with:
git checkout step1
Eliminate SQLConnection from HomeController
This is dangerous part
- Make
SqlRepository
asIDisposable
- Move
SqlConnection
intoSqlRepository
- Remove
SqlConnection conn
form extracted methods inSqlRepository
- Fill
Dispose
method - Implement
Open
method - Replace
SqlConnection
withSqlRepository
SqlRepository code
public SqlConnection conn;
public SqlRepository(string connectionString)
{
conn = new SqlConnection(connectionString);
}
public void Dispose()
{
if (conn != null)
conn.Dispose();
}
public void Open()
{
conn.Open();
}
Interface is easier to stub/mock
To convert SqlRepository
just use Visual Studio right-click menu
Refactor->Extract interface
Remember to make this interface inherit from IDisposable
If you are lost
If you are lost just checkout step2 branch from git with:
git checkout step2
git checkout step2
Use Func Luke!
The last step is to eliminate
using (var sqlRepository = new SqlRepository(connectionString))
Easy conversion is to use Func like:
public Func CreateSqlRepository;
public HomeController(string connectionString)
{
this.connectionString = connectionString;
CreateSqlRepository = () => new SqlRepository(connectionString);
}
Let's write stub
public class SqlRepositoryStub : ISqlRepository
{
public class UserInDbStub
{
public int UserId;
public string Username;
public string Password;
public DateTime LastBadLogin = new DateTime();
public int BadAttempt = 0;
}
public List Users = new List();
public int GetUserId(LoginModel model)
{
var user =
Users.FirstOrDefault(x =>
x.Username == model.User);
if (user == null)
{
return -1;
}
return user.UserId;
}
public void TryLogin(LoginModel model, int userId,
out int badAttempt, out DateTime lastBadLogin,
out bool successLogin)
{
var user =
Users.FirstOrDefault(x =>
x.UserId == userId
&& x.Password == model.Password);
if (user == null)
{
lastBadLogin = DateTime.MinValue;
badAttempt = 0;
successLogin = false;
}
lastBadLogin = user.LastBadLogin;
badAttempt = user.BadAttempt;
successLogin = true;
}
public void UpdateBadAttemps(int userId)
{
var user =
Users.First(x => x.UserId == userId);
user.BadAttempt += 1;
user.LastBadLogin = DateTime.Now;
}
public void Open()
{
}
public void Dispose()
{
}
}
Our first test method
var password = "123";
var user = "test";
var controller = new HomeController("");
controller.CreateSqlRepository = () =>
{
var sqlRepositoryStub = new SqlRepositoryStub();
sqlRepositoryStub.Users.Add(
new SqlRepositoryStub.UserInDbStub()
{
BadAttempt = 0,
UserId = 1,
Username = user,
Password = password
});
return sqlRepositoryStub;
};
var result = controller
.Index(
new LoginModel() {User = user, Password = password})
as RedirectToRouteResult;
Assert.IsNotNull(result);
Assert.AreEqual("LoginSuccess", result.RouteValues["action"]);
Now is Your turn
The code has two errors - find them wit Unit Test
- Implement bad login test
- Implement lock account test
- Implement login after lock with more than 1 minute delay
- !!Good luck!!