Ctrl K
ComunidadGuías…

api/handler/cleanup_rollback_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handler

import (
	"errors"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	apimodel "github.com/goodrain/rainbond/api/model"
	"github.com/goodrain/rainbond/db"
	"github.com/goodrain/rainbond/db/dao"
	dbmodel "github.com/goodrain/rainbond/db/model"
	gclient "github.com/goodrain/rainbond/mq/client"
	dmodel "github.com/goodrain/rainbond/worker/discover/model"
	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/mysql"
)

type cleanupRollbackManager struct {
	db.Manager
	database *gorm.DB
	services *cleanupRollbackServices
}

func (m cleanupRollbackManager) Begin() *gorm.DB                        { return m.database.Begin() }
func (m cleanupRollbackManager) TenantServiceDao() dao.TenantServiceDao { return m.services }

func (m cleanupRollbackManager) VersionInfoDao() dao.VersionInfoDao {
	return &cleanupRollbackVersions{}
}

type cleanupRollbackVersions struct{ dao.VersionInfoDao }

func (*cleanupRollbackVersions) GetVersionByDeployVersion(string, string) (*dbmodel.VersionInfo, error) {
	return &dbmodel.VersionInfo{BuildVersion: "old", FinalStatus: "success"}, nil
}

type cleanupRollbackServices struct {
	dao.TenantServiceDao
	writes int
}

func (s *cleanupRollbackServices) GetServiceByID(string) (*dbmodel.TenantServices, error) {
	return &dbmodel.TenantServices{ServiceID: "service", TenantID: "tenant", DeployVersion: "current"}, nil
}
func (s *cleanupRollbackServices) UpdateModel(dbmodel.Interface) error {
	s.writes++
	return errors.New("unguarded version update")
}

// capability_id: rainbond.cleanup.version-activation-fence
func TestLegacyRollbackCannotActivateRetiredVersion(t *testing.T) {
	raw, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	database, err := gorm.Open("mysql", raw)
	if err != nil {
		t.Fatal(err)
	}
	defer database.Close()
	database.LogMode(false)
	services := &cleanupRollbackServices{}
	db.SetTestManager(cleanupRollbackManager{database: database, services: services})
	defer db.SetTestManager(nil)
	mock.ExpectBegin()
	mock.ExpectExec("UPDATE .*cleanup_storage").WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectQuery("SELECT .*cleanup_storage.*FOR UPDATE").WillReturnRows(sqlmock.NewRows([]string{"storage_id", "generation", "mode"}))
	mock.ExpectQuery("SELECT .*tenant_services.*FOR UPDATE").WithArgs("service", "tenant").WillReturnRows(sqlmock.NewRows([]string{"service_id", "tenant_id", "deploy_version"}).AddRow("service", "tenant", "current"))
	mock.ExpectQuery("SELECT .*tenant_service_version.*FOR UPDATE").WithArgs("service", "old").WillReturnRows(sqlmock.NewRows([]string{"ID"}))
	mock.ExpectRollback()
	err = (&ServiceAction{}).RollBack(&apimodel.RollbackStruct{ServiceID: "service", TenantID: "tenant", DeployVersion: "old", EventID: "rollback-event"})
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Fatalf("retired target was not rejected by the transaction guard: %v", err)
	}
	if services.writes != 0 {
		t.Fatal("legacy rollback bypassed guarded selection")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

// capability_id: rainbond.cleanup.version-activation-fence
func TestExplicitUpgradeRechecksVersionUnderRetirementLock(t *testing.T) {
	raw, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	database, err := gorm.Open("mysql", raw)
	if err != nil {
		t.Fatal(err)
	}
	defer database.Close()
	database.LogMode(false)
	services := &cleanupRollbackServices{}
	db.SetTestManager(cleanupRollbackManager{database: database, services: services})
	defer db.SetTestManager(nil)
	mock.ExpectBegin()
	mock.ExpectExec("UPDATE .*cleanup_storage").WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectQuery("SELECT .*cleanup_storage.*FOR UPDATE").WillReturnRows(sqlmock.NewRows([]string{"storage_id", "generation", "mode"}))
	mock.ExpectQuery("SELECT .*tenant_services.*FOR UPDATE").WithArgs("service", "tenant").WillReturnRows(sqlmock.NewRows([]string{"service_id", "tenant_id", "deploy_version"}).AddRow("service", "tenant", "current"))
	// The earlier lookup was successful, but retirement won before activation.
	mock.ExpectQuery("SELECT .*tenant_service_version.*FOR UPDATE").WithArgs("service", "old").WillReturnRows(sqlmock.NewRows([]string{"ID"}))
	mock.ExpectRollback()
	err = (&OperationHandler{}).upgrade(&apimodel.ComponentUpgradeReq{ComponentOpGeneralReq: apimodel.ComponentOpGeneralReq{ServiceID: "service", EventID: "upgrade-event"}, UpgradeVersion: "old"})
	if !errors.Is(err, gorm.ErrRecordNotFound) {
		t.Fatalf("upgrade activated a retired version: %v", err)
	}
	if services.writes != 0 {
		t.Fatal("upgrade used unguarded full service update")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

type cleanupFailedMQ struct {
	gclient.MQClient
	calls   int
	failure error
	last    gclient.TaskStruct
}

func (m *cleanupFailedMQ) SendBuilderTopic(task gclient.TaskStruct) error {
	m.last = task
	m.calls++
	return m.failure
}

// capability_id: rainbond.cleanup.version-activation-fence
func TestUpgradeQueueFailureCannotOverwriteNewerDeployment(t *testing.T) {
	raw, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	database, err := gorm.Open("mysql", raw)
	if err != nil {
		t.Fatal(err)
	}
	defer database.Close()
	database.LogMode(false)
	services := &cleanupRollbackServices{}
	db.SetTestManager(cleanupRollbackManager{database: database, services: services})
	defer db.SetTestManager(nil)
	mock.ExpectBegin()
	mock.ExpectExec("UPDATE .*cleanup_storage").WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectQuery("SELECT .*cleanup_storage.*FOR UPDATE").WillReturnRows(sqlmock.NewRows([]string{"storage_id", "generation", "mode"}))
	mock.ExpectQuery("SELECT .*tenant_services.*FOR UPDATE").WithArgs("service", "tenant").WillReturnRows(sqlmock.NewRows([]string{"service_id", "tenant_id", "deploy_version"}).AddRow("service", "tenant", "current"))
	mock.ExpectQuery("SELECT .*tenant_service_version.*FOR UPDATE").WithArgs("service", "old").WillReturnRows(sqlmock.NewRows([]string{"ID", "final_status"}).AddRow(7, "success"))
	mock.ExpectExec("UPDATE .*tenant_service_version.*activation_revision").WithArgs("upgrade-event", "service", "old").WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE .*tenant_services.*deploy_version").WithArgs("old", "service", "tenant").WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()
	mock.ExpectBegin()
	mock.ExpectExec("UPDATE .*cleanup_storage").WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectQuery("SELECT .*cleanup_storage.*FOR UPDATE").WillReturnRows(sqlmock.NewRows([]string{"storage_id", "generation", "mode"}))
	mock.ExpectQuery("SELECT .*tenant_services.*FOR UPDATE").WithArgs("service", "tenant").WillReturnRows(sqlmock.NewRows([]string{"service_id", "tenant_id", "deploy_version"}).AddRow("service", "tenant", "newer"))
	mock.ExpectRollback()
	failure := errors.New("queue unavailable")
	queue := &cleanupFailedMQ{failure: failure}
	err = (&OperationHandler{mqCli: queue}).upgrade(&apimodel.ComponentUpgradeReq{ComponentOpGeneralReq: apimodel.ComponentOpGeneralReq{ServiceID: "service", EventID: "upgrade-event"}, UpgradeVersion: "old"})
	if !errors.Is(err, failure) || queue.calls != 1 || services.writes != 0 {
		t.Fatal("compensation overwrote another operation", err, queue.calls, services.writes)
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}

// capability_id: rainbond.cleanup.version-activation-fence
func TestLegacyRollbackRejectsWrongTenant(t *testing.T) {
	services := &cleanupRollbackServices{}
	db.SetTestManager(cleanupRollbackManager{services: services})
	defer db.SetTestManager(nil)
	if err := (&ServiceAction{}).RollBack(&apimodel.RollbackStruct{ServiceID: "service", TenantID: "other", DeployVersion: "old", EventID: "event"}); err == nil {
		t.Fatal("cross-tenant rollback accepted")
	}
	if services.writes != 0 {
		t.Fatal("cross-tenant request mutated service")
	}
}

// capability_id: rainbond.cleanup.version-activation-fence
func TestExplicitUpgradeEnqueuesValidatedVersion(t *testing.T) {
	raw, mock, err := sqlmock.New()
	if err != nil {
		t.Fatal(err)
	}
	database, err := gorm.Open("mysql", raw)
	if err != nil {
		t.Fatal(err)
	}
	defer database.Close()
	database.LogMode(false)
	services := &cleanupRollbackServices{}
	db.SetTestManager(cleanupRollbackManager{database: database, services: services})
	defer db.SetTestManager(nil)
	mock.ExpectBegin()
	mock.ExpectExec("UPDATE .*cleanup_storage").WillReturnResult(sqlmock.NewResult(0, 0))
	mock.ExpectQuery("SELECT .*cleanup_storage.*FOR UPDATE").WillReturnRows(sqlmock.NewRows([]string{"storage_id", "generation", "mode"}))
	mock.ExpectQuery("SELECT .*tenant_services.*FOR UPDATE").WithArgs("service", "tenant").WillReturnRows(sqlmock.NewRows([]string{"service_id", "tenant_id", "deploy_version"}).AddRow("service", "tenant", "current"))
	mock.ExpectQuery("SELECT .*tenant_service_version.*FOR UPDATE").WithArgs("service", "old").WillReturnRows(sqlmock.NewRows([]string{"ID", "final_status"}).AddRow(7, "success"))
	mock.ExpectExec("UPDATE .*tenant_service_version.*activation_revision").WithArgs("upgrade-event", "service", "old").WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectExec("UPDATE .*tenant_services.*deploy_version").WithArgs("old", "service", "tenant").WillReturnResult(sqlmock.NewResult(0, 1))
	mock.ExpectCommit()
	queue := &cleanupFailedMQ{}
	err = (&OperationHandler{mqCli: queue}).upgrade(&apimodel.ComponentUpgradeReq{ComponentOpGeneralReq: apimodel.ComponentOpGeneralReq{ServiceID: "service", EventID: "upgrade-event"}, UpgradeVersion: "old"})
	if err != nil || queue.calls != 1 || services.writes != 0 {
		t.Fatal("validated upgrade failed", err)
	}
	body, ok := queue.last.TaskBody.(*dmodel.RollingUpgradeTaskBody)
	if !ok || body.NewDeployVersion != "old" || body.TenantID != "tenant" || body.ServiceID != "service" {
		t.Fatal("wrong deployment enqueued")
	}
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Fatal(err)
	}
}