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) } }