111 lines
4.6 KiB
Go
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)
|
|
}
|
|
}
|