iris/middleware/basicauth/user_auth_test.go

288 lines
6.5 KiB
Go
Raw Normal View History

package basicauth
import (
"errors"
"io/ioutil"
"os"
"reflect"
"testing"
"github.com/kataras/iris/v12/context"
"golang.org/x/crypto/bcrypt"
"gopkg.in/yaml.v3"
)
type IUserRepository interface {
GetByUsernameAndPassword(dest interface{}, username, password string) error
}
// Test a custom implementation of AuthFunc with a user repository.
// This is a usage example of custom AuthFunc implementation.
func UserRepository(repo IUserRepository, newUserPtr func() interface{}) AuthFunc {
return func(ctx *context.Context, username, password string) (interface{}, bool) {
dest := newUserPtr()
err := repo.GetByUsernameAndPassword(dest, username, password)
if err == nil {
return dest, true
}
return nil, false
}
}
type testUser struct {
username string
password string
email string // custom field.
}
// GetUsername & Getpassword complete the User interface (optional but useful on Context.User()).
func (u *testUser) GetUsername() string {
return u.username
}
func (u *testUser) GetPassword() string {
return u.password
}
type testRepo struct {
entries []testUser
}
// Implements IUserRepository interface.
func (r *testRepo) GetByUsernameAndPassword(dest interface{}, username, password string) error {
for _, e := range r.entries {
if e.username == username && e.password == password {
*dest.(*testUser) = e
return nil
}
}
return errors.New("invalid credentials")
}
func TestAllowUserRepository(t *testing.T) {
repo := &testRepo{
entries: []testUser{
{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
},
}
allow := UserRepository(repo, func() interface{} {
return new(testUser)
})
var tests = []struct {
username string
password string
ok bool
user *testUser
}{
{
username: "kataras",
password: "kataras_pass",
ok: true,
user: &testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
},
{
username: "makis",
password: "makis_password",
ok: false,
},
}
for i, tt := range tests {
v, ok := allow(nil, tt.username, tt.password)
if tt.ok != ok {
t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s)", i, tt.ok, ok, tt.username, tt.password)
}
if !ok {
continue
}
u, ok := v.(*testUser)
if !ok {
t.Fatalf("[%d] a user should be type of *testUser but got: %#+v (%T)", i, v, v)
}
if !reflect.DeepEqual(tt.user, u) {
t.Fatalf("[%d] expected user:\n%#+v\nbut got:\n%#+v", i, tt.user, u)
}
}
}
func TestAllowUsers(t *testing.T) {
users := []User{
&testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
}
allow := AllowUsers(users)
var tests = []struct {
username string
password string
ok bool
user *testUser
}{
{
username: "kataras",
password: "kataras_pass",
ok: true,
user: &testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
},
{
username: "makis",
password: "makis_password",
ok: false,
},
}
for i, tt := range tests {
v, ok := allow(nil, tt.username, tt.password)
if tt.ok != ok {
t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s)", i, tt.ok, ok, tt.username, tt.password)
}
if !ok {
continue
}
u, ok := v.(*testUser)
if !ok {
t.Fatalf("[%d] a user should be type of *testUser but got: %#+v (%T)", i, v, v)
}
if !reflect.DeepEqual(tt.user, u) {
t.Fatalf("[%d] expected user:\n%#+v\nbut got:\n%#+v", i, tt.user, u)
}
}
}
// Test YAML user loading with b-encrypted passwords.
func TestAllowUsersFile(t *testing.T) {
f, err := ioutil.TempFile("", "*users.yml")
if err != nil {
t.Fatal(err)
}
defer func() {
f.Close()
os.Remove(f.Name())
}()
// f.WriteString(`
// - username: kataras
// password: kataras_pass
// age: 27
// role: admin
// - username: makis
// password: makis_password
// `)
// This form is supported too, although its features are limited (no custom fields):
// f.WriteString(`
// kataras: kataras_pass
// makis: makis_password
// `)
var tests = []struct {
username string
password string // hashed, auto-filled later on.
inputPassword string
ok bool
user context.Map
}{
{
username: "kataras",
inputPassword: "kataras_pass",
ok: true,
user: context.Map{"age": 27, "role": "admin"}, // username and password are auto-filled in our tests below.
},
{
username: "makis",
inputPassword: "makis_password",
ok: true,
user: context.Map{},
},
{
username: "invalid",
password: "invalid_pass",
ok: false,
},
{
username: "notvalid",
password: "",
ok: false,
},
}
// Write the tests to the users YAML file.
var usersToWrite []context.Map
for _, tt := range tests {
if tt.ok {
// store the hashed password.
tt.password = mustGeneratePassword(t, tt.inputPassword)
// store and write the username and hashed password.
tt.user["username"] = tt.username
tt.user["password"] = tt.password
// cannot write it as a stream, write it as a slice.
// enc.Encode(tt.user)
usersToWrite = append(usersToWrite, tt.user)
}
// bcrypt.GenerateFromPassword([]byte("kataras_pass"), bcrypt.DefaultCost)
}
fileContents, err := yaml.Marshal(usersToWrite)
if err != nil {
t.Fatal(err)
}
f.Write(fileContents)
// Build the authentication func.
allow := AllowUsersFile(f.Name(), BCRYPT)
for i, tt := range tests {
v, ok := allow(nil, tt.username, tt.inputPassword)
if tt.ok != ok {
t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s,user=%#+v)", i, tt.ok, ok, tt.username, tt.inputPassword, v)
}
if !ok {
continue
}
if len(tt.user) == 0 { // when username: password form.
continue
}
u, ok := v.(context.Map)
if !ok {
t.Fatalf("[%d] a user loaded from external source or file should be alway type of map[string]interface{} but got: %#+v (%T)", i, v, v)
}
if expected, got := len(tt.user), len(u); expected != got {
t.Fatalf("[%d] expected user map length to be equal, expected: %d but got: %d\n%#+v\n%#+v", i, expected, got, tt.user, u)
}
for k, v := range tt.user {
if u[k] != v {
t.Fatalf("[%d] expected user map %q to be %q but got: %q", i, k, v, u[k])
}
}
}
}
func mustGeneratePassword(t *testing.T, userPassword string) string {
t.Helper()
hashed, err := bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)
if err != nil {
t.Fatal(err)
}
return string(hashed)
}