111 lines
4.6 KiB
Go

package externaladmin
import (
"testing"
"github.com/DATA-DOG/go-sqlmock"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func TestDisableAccountRevokesEveryActiveSession(t *testing.T) {
service, adminMock, userMock, cleanup := newAccountMutationFixture(t)
defer cleanup()
adminMock.ExpectBegin()
adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE id = \\? AND app_code = \\?").
WithArgs(uint64(7), "fami", 1).WillReturnRows(externalAccountRows())
adminMock.ExpectExec("UPDATE `external_admin_accounts` SET .*status.* WHERE `id` = \\?").
WillReturnResult(sqlmock.NewResult(0, 1))
// No session id predicate is allowed here: disabling an account must revoke
// every still-active session for that account, including other devices.
adminMock.ExpectExec("UPDATE `external_admin_sessions` SET .*revoke_reason.*revoked_at_ms.* WHERE account_id = \\? AND revoked_at_ms = 0").
WithArgs("account_disabled", sqlmock.AnyArg(), uint64(7)).
WillReturnResult(sqlmock.NewResult(0, 3))
adminMock.ExpectCommit()
expectReloadedAccountAndLinkedUser(adminMock, userMock)
account, err := service.SetStatus(t.Context(), "fami", 7, "disabled")
if err != nil {
t.Fatalf("disable account: %v", err)
}
if account.Status != "disabled" {
t.Fatalf("status = %q", account.Status)
}
assertAccountMutationExpectations(t, adminMock, userMock)
}
func TestResetPasswordRevokesEveryActiveSession(t *testing.T) {
service, adminMock, userMock, cleanup := newAccountMutationFixture(t)
defer cleanup()
adminMock.ExpectBegin()
adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE id = \\? AND app_code = \\?").
WithArgs(uint64(7), "fami", 1).WillReturnRows(externalAccountRows())
adminMock.ExpectExec("UPDATE `external_admin_accounts` SET .*password_change_required.*password_hash.* WHERE `id` = \\?").
WillReturnResult(sqlmock.NewResult(0, 1))
// A reset is an administrator credential replacement, so even the currently
// active browser must re-authenticate with the temporary password.
adminMock.ExpectExec("UPDATE `external_admin_sessions` SET .*revoke_reason.*revoked_at_ms.* WHERE account_id = \\? AND revoked_at_ms = 0").
WithArgs("password_reset", sqlmock.AnyArg(), uint64(7)).
WillReturnResult(sqlmock.NewResult(0, 3))
adminMock.ExpectCommit()
expectReloadedAccountAndLinkedUser(adminMock, userMock)
account, err := service.ResetPassword(t.Context(), "fami", 7, "replacement-password")
if err != nil {
t.Fatalf("reset password: %v", err)
}
if account.LinkedUser.UserID != 9001 {
t.Fatalf("linked user = %d", account.LinkedUser.UserID)
}
assertAccountMutationExpectations(t, adminMock, userMock)
}
func newAccountMutationFixture(t *testing.T) (*Service, sqlmock.Sqlmock, sqlmock.Sqlmock, func()) {
t.Helper()
adminSQL, adminMock, err := sqlmock.New()
if err != nil {
t.Fatalf("create admin sql mock: %v", err)
}
adminDB, err := gorm.Open(mysql.New(mysql.Config{Conn: adminSQL, SkipInitializeWithVersion: true}), &gorm.Config{})
if err != nil {
adminSQL.Close()
t.Fatalf("create admin gorm: %v", err)
}
userDB, userMock, err := sqlmock.New()
if err != nil {
adminSQL.Close()
t.Fatalf("create user sql mock: %v", err)
}
return NewService(adminDB, userDB, Config{}), adminMock, userMock, func() {
_ = userDB.Close()
_ = adminSQL.Close()
}
}
func externalAccountRows() *sqlmock.Rows {
return sqlmock.NewRows([]string{
"id", "app_code", "linked_app_user_id", "username", "password_hash", "permissions_json", "status",
"password_change_required", "failed_login_count", "locked_until_ms", "created_by_admin_id", "created_at_ms", "updated_at_ms",
}).AddRow(7, "fami", 9001, "operator", "hash", `[]`, "disabled", true, 0, 0, 1, 1, 1)
}
func expectReloadedAccountAndLinkedUser(adminMock sqlmock.Sqlmock, userMock sqlmock.Sqlmock) {
adminMock.ExpectQuery("SELECT \\* FROM `external_admin_accounts` WHERE id = \\?").
WithArgs(uint64(7), uint64(7), 1).WillReturnRows(externalAccountRows())
userMock.ExpectQuery("SELECT u.user_id,").
WithArgs(sqlmock.AnyArg(), sqlmock.AnyArg(), "fami", int64(9001)).
WillReturnRows(sqlmock.NewRows([]string{
"user_id", "current_display_user_id", "default_display_user_id", "pretty_display_user_id", "pretty_id", "username", "avatar", "status",
}).AddRow(9001, "123456", "654321", "8888", "8888", "linked-user", "avatar", "active"))
}
func assertAccountMutationExpectations(t *testing.T, adminMock sqlmock.Sqlmock, userMock sqlmock.Sqlmock) {
t.Helper()
if err := adminMock.ExpectationsWereMet(); err != nil {
t.Fatalf("admin sql expectations: %v", err)
}
if err := userMock.ExpectationsWereMet(); err != nil {
t.Fatalf("user sql expectations: %v", err)
}
}