package repository import ( "encoding/json" "errors" "testing" "github.com/DATA-DOG/go-sqlmock" "github.com/wmms/wmms/internal/model" "github.com/wmms/wmms/pkg/database" ) func TestUserOrganizationReplacement(t *testing.T) { for _, tc := range []struct { name string ids []int64 def string active int valid bool }{ {"deduplicates", []int64{2, 2}, "2", 1, true}, {"disabled", []int64{2}, "2", 0, false}, {"missing", []int64{9}, "9", 0, false}, {"default outside set", []int64{2}, "3", 1, false}, {"nonempty without default", []int64{2}, "null", 1, false}, {"invalid id", []int64{-1}, "null", 0, false}, {"revoke all", []int64{}, "null", 0, true}, {"empty with default", []int64{}, "2", 0, false}, } { t.Run(tc.name, func(t *testing.T) { db, mock, _ := sqlmock.New() defer db.Close() mock.ExpectBegin() tx, _ := db.Begin() if tc.name != "invalid id" { mock.ExpectQuery("SELECT organization_id, is_default FROM user_organizations").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"organization_id", "is_default"}).AddRow(1, true)) mock.ExpectQuery("SELECT EXISTS").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"admin"}).AddRow(false)) if len(tc.ids) > 0 && tc.name != "default outside set" && tc.name != "nonempty without default" { mock.ExpectQuery("SELECT id, status FROM organizations").WithArgs(tc.ids[0]).WillReturnRows(func() *sqlmock.Rows { rows := sqlmock.NewRows([]string{"id", "status"}) if tc.active > 0 { rows.AddRow(tc.ids[0], "active") } return rows }()) } } if tc.valid { mock.ExpectExec("DELETE FROM user_organizations").WithArgs(int64(7)).WillReturnResult(sqlmock.NewResult(0, 1)) if len(tc.ids) > 0 { mock.ExpectExec("INSERT INTO user_organizations").WithArgs(int64(7), int64(2), true).WillReturnResult(sqlmock.NewResult(0, 1)) } mock.ExpectExec("INSERT INTO organization_audit_logs").WithArgs(int64(99), nil, "organization_access_changed", sqlmock.AnyArg(), sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1)) } err := replaceUserOrganizationsTx(tx, 7, model.UserOrganizationInput{OrganizationIDs: &tc.ids, DefaultOrganizationID: json.RawMessage(tc.def), ActorUserID: 99}, false) if tc.valid && err != nil { t.Fatal(err) } if !tc.valid && !errors.Is(err, ErrInvalidOrganizations) { t.Fatalf("want invalid organizations, got %v", err) } mock.ExpectRollback() _ = tx.Rollback() if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }) } } func TestUserOrganizationOmittedPreservesAccess(t *testing.T) { db, mock, _ := sqlmock.New() defer db.Close() mock.ExpectBegin() tx, _ := db.Begin() if err := replaceUserOrganizationsTx(tx, 7, model.UserOrganizationInput{ActorUserID: 99}, false); err != nil { t.Fatal(err) } mock.ExpectRollback() _ = tx.Rollback() if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestUserOrganizationDefaultChangeAuditsOriginalDefault(t *testing.T) { db, mock, err := sqlmock.New() if err != nil { t.Fatal(err) } defer db.Close() mock.ExpectBegin() tx, err := db.Begin() if err != nil { t.Fatal(err) } mock.ExpectQuery("SELECT organization_id, is_default FROM user_organizations").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"organization_id", "is_default"}).AddRow(1, false).AddRow(2, true)) mock.ExpectQuery("SELECT EXISTS").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"admin"}).AddRow(false)) mock.ExpectQuery("SELECT id, status FROM organizations").WithArgs(int64(1), int64(2)).WillReturnRows(sqlmock.NewRows([]string{"id", "status"}).AddRow(1, "active").AddRow(2, "active")) mock.ExpectExec("DELETE FROM user_organizations").WithArgs(int64(7)).WillReturnResult(sqlmock.NewResult(0, 2)) mock.ExpectExec("INSERT INTO user_organizations").WithArgs(int64(7), int64(1), true).WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("INSERT INTO user_organizations").WithArgs(int64(7), int64(2), false).WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("INSERT INTO organization_audit_logs").WithArgs(int64(99), nil, "organization_access_changed", `{"user_id":7,"organization_ids":[1,2],"default_organization_id":2}`, `{"user_id":7,"organization_ids":[1,2],"default_organization_id":1}`).WillReturnResult(sqlmock.NewResult(1, 1)) if err := replaceUserOrganizationsTx(tx, 7, model.UserOrganizationInput{DefaultOrganizationID: json.RawMessage("1"), ActorUserID: 99}, false); err != nil { t.Fatal(err) } mock.ExpectRollback() _ = tx.Rollback() if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } } func TestUserOrganizationAuditFailureRollsBackUserAndRoles(t *testing.T) { db, mock, _ := sqlmock.New() defer db.Close() database.DB = db defer func() { database.DB = nil }() mock.ExpectBegin() mock.ExpectQuery("SELECT COUNT.*FROM roles").WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1)) mock.ExpectQuery("SELECT id FROM roles WHERE code").WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(1)) mock.ExpectQuery("SELECT COUNT.*FROM user_roles WHERE user_id").WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) mock.ExpectQuery("SELECT id FROM users WHERE id.*FOR UPDATE").WithArgs(int64(7)).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow(7)) mock.ExpectExec("UPDATE users").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("DELETE FROM user_roles").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("INSERT INTO user_roles").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectQuery("SELECT organization_id, is_default FROM user_organizations").WillReturnRows(sqlmock.NewRows([]string{"organization_id", "is_default"}).AddRow(2, true)) mock.ExpectQuery("SELECT EXISTS").WillReturnRows(sqlmock.NewRows([]string{"admin"}).AddRow(false)) mock.ExpectExec("DELETE FROM user_organizations").WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("INSERT INTO organization_audit_logs").WillReturnError(errors.New("audit unavailable")) mock.ExpectRollback() ids := []int64{} err := NewUserRepository().Update(&model.User{ID: 7}, []int64{2}, model.UserOrganizationInput{OrganizationIDs: &ids, DefaultOrganizationID: json.RawMessage("null"), ActorUserID: 99}) if err == nil || err.Error() != "audit unavailable" { t.Fatalf("got %v", err) } if err := mock.ExpectationsWereMet(); err != nil { t.Fatal(err) } }