package queryupdate import ( "context" "errors" "fmt" "testing" "queryorchestration/internal/database/repository" "queryorchestration/internal/query" resultprocessor "queryorchestration/internal/query/result/processor" "queryorchestration/internal/serviceconfig" "queryorchestration/internal/serviceconfig/queue/queryversionsync" queuemock "queryorchestration/mocks/queue" "github.com/stretchr/testify/require" "github.com/aws/aws-sdk-go-v2/service/sqs" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/pashagolub/pgxmock/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) type QueryUpdateConfig struct { serviceconfig.BaseConfig queryversionsync.QueryVersionSyncConfig } func TestGetUpdator(t *testing.T) { pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) queryType := resultprocessor.Type(resultprocessor.TypeContextFull) updator, err := svc.getUpdator(queryType) require.NoError(t, err) assert.NotNil(t, updator) queryType = resultprocessor.Type(resultprocessor.TypeJsonExtractor) updator, err = svc.getUpdator(queryType) require.NoError(t, err) assert.NotNil(t, updator) queryType = resultprocessor.Type(-1) _, err = svc.getUpdator(queryType) assert.Error(t, err) } func TestSubmitUpdate(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) config := "{\"path\":\"example_path\"}" q := query.Query{ ID: uuid.New(), RequiredQueryIDs: &[]uuid.UUID{ uuid.New(), }, Config: &config, ActiveVersion: int32(1), LatestVersion: int32(2), } aV := int32(10) update := &resultprocessor.Update{ ID: q.ID, RequiredQueryIDs: &[]uuid.UUID{ uuid.New(), }, Config: q.Config, ActiveVersion: &aV, } pool.ExpectBeginTx(pgx.TxOptions{}) pool.ExpectQuery("name: AddLatestQueryVersion :one").WithArgs(q.ID).WillReturnRows( pgxmock.NewRows([]string{"version"}). AddRow(int32(3)), ) pool.ExpectExec("name: AddActiveQueryVersion :exec").WithArgs(q.ID, aV). WillReturnResult(pgxmock.NewResult("", 1)) pool.ExpectExec("name: AddRequiredQuery :exec").WithArgs(update.ID, (*update.RequiredQueryIDs)[0], pgxmock.AnyArg()). WillReturnResult(pgxmock.NewResult("", 1)) pool.ExpectExec("name: RemoveRequiredQuery :exec").WithArgs(pgxmock.AnyArg(), (*q.RequiredQueryIDs)[0], update.ID). WillReturnResult(pgxmock.NewResult("", 1)) pool.ExpectExec("name: SetQueryConfig :exec").WithArgs(update.ID, []byte(*update.Config), int32(3)). WillReturnResult(pgxmock.NewResult("", 1)) pool.ExpectCommit() err = svc.submitUpdate(ctx, &q, update) require.NoError(t, err) } func TestSubmitUpdateRollback(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) q := query.Query{ ID: uuid.New(), ActiveVersion: int32(1), LatestVersion: int32(2), } aV := int32(10) update := &resultprocessor.Update{ ID: q.ID, ActiveVersion: &aV, } pool.ExpectBeginTx(pgx.TxOptions{}) msg := "database failure" pool.ExpectExec("name: AddActiveQueryVersion :exec").WithArgs(q.ID, aV). WillReturnError(errors.New(msg)) pool.ExpectCommit() err = svc.submitUpdate(ctx, &q, update) assert.EqualError(t, err, msg) } func TestSubmitUpdateRequiredQueries(t *testing.T) { t.Run("all fields", func(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) config := "{\"path\":\"example_path\"}" q := query.Query{ ID: uuid.New(), RequiredQueryIDs: &[]uuid.UUID{ uuid.New(), uuid.New(), uuid.New(), }, Config: &config, ActiveVersion: int32(1), LatestVersion: int32(2), } update := &resultprocessor.Update{ ID: q.ID, RequiredQueryIDs: &[]uuid.UUID{ uuid.New(), uuid.New(), (*q.RequiredQueryIDs)[0], }, } pool.ExpectBeginTx(pgx.TxOptions{}) pool.ExpectQuery("name: AddLatestQueryVersion :one").WithArgs(update.ID).WillReturnRows( pgxmock.NewRows([]string{"version"}). AddRow(int32(1)), ) pool.ExpectExec("name: AddRequiredQuery :exec").WithArgs(update.ID, (*update.RequiredQueryIDs)[0], pgxmock.AnyArg()). WillReturnResult(pgxmock.NewResult("", 1)) pool.ExpectExec("name: AddRequiredQuery :exec").WithArgs(update.ID, (*update.RequiredQueryIDs)[1], pgxmock.AnyArg()). WillReturnResult(pgxmock.NewResult("", 1)) pool.ExpectExec("name: RemoveRequiredQuery :exec").WithArgs(pgxmock.AnyArg(), (*q.RequiredQueryIDs)[1], update.ID). WillReturnResult(pgxmock.NewResult("", 1)) pool.ExpectExec("name: RemoveRequiredQuery :exec").WithArgs(pgxmock.AnyArg(), (*q.RequiredQueryIDs)[2], update.ID). WillReturnResult(pgxmock.NewResult("", 1)) pool.ExpectCommit() err = svc.submitUpdate(ctx, &q, update) require.NoError(t, err) }) t.Run("all nil", func(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) config := "{\"path\":\"example_path\"}" q := query.Query{ ID: uuid.New(), RequiredQueryIDs: &[]uuid.UUID{ uuid.New(), uuid.New(), uuid.New(), }, Config: &config, ActiveVersion: 1, LatestVersion: 2, } av := int32(2) update := &resultprocessor.Update{ ID: q.ID, ActiveVersion: &av, } pool.ExpectBeginTx(pgx.TxOptions{}) pool.ExpectExec("name: AddActiveQueryVersion :exec").WithArgs(q.ID, av). WillReturnResult(pgxmock.NewResult("", 1)) pool.ExpectCommit() err = svc.submitUpdate(ctx, &q, update) require.NoError(t, err) }) } func TestSubmitUpdateActiveVersion(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) q := query.Query{ ID: uuid.New(), ActiveVersion: int32(1), LatestVersion: int32(2), } aV := int32(10) update := &resultprocessor.Update{ ID: q.ID, ActiveVersion: &aV, } pool.ExpectBeginTx(pgx.TxOptions{}) pool.ExpectExec("name: AddActiveQueryVersion :exec").WithArgs(q.ID, aV). WillReturnResult(pgxmock.NewResult("", 1)) pool.ExpectCommit() err = svc.submitUpdate(ctx, &q, update) require.NoError(t, err) } func TestGetSetDifference(t *testing.T) { commonUUID := uuid.New() listA := []uuid.UUID{uuid.New(), commonUUID} listB := []uuid.UUID{uuid.New(), commonUUID} assert.ElementsMatch(t, []uuid.UUID{}, getSetDifference(nil, nil)) assert.ElementsMatch(t, listA, getSetDifference(&listA, nil)) assert.ElementsMatch(t, []uuid.UUID{}, getSetDifference(nil, &listB)) assert.ElementsMatch(t, []uuid.UUID{listA[0]}, getSetDifference(&listA, &listB)) assert.ElementsMatch(t, []uuid.UUID{listB[0]}, getSetDifference(&listB, &listA)) assert.ElementsMatch(t, []uuid.UUID{}, getSetDifference(&listA, &listA)) } func TestNormalizeUpdate(t *testing.T) { t.Run("all fields to change", func(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) current := &query.Query{ ID: uuid.New(), ActiveVersion: int32(1), LatestVersion: int32(2), Type: resultprocessor.TypeJsonExtractor, } qcfg := "{}" aV := int32(2) update := &resultprocessor.Update{ ID: current.ID, Config: &qcfg, ActiveVersion: &aV, RequiredQueryIDs: &[]uuid.UUID{}, } err = svc.normalizeUpdate(ctx, current, update) require.NoError(t, err) assert.EqualExportedValues(t, resultprocessor.Update{ ID: current.ID, ActiveVersion: &aV, Config: &qcfg, RequiredQueryIDs: nil, }, *update) }) t.Run("no changes", func(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) current := &query.Query{ ID: uuid.New(), ActiveVersion: int32(1), LatestVersion: int32(2), Type: resultprocessor.TypeJsonExtractor, } update := &resultprocessor.Update{ ID: current.ID, } err = svc.normalizeUpdate(ctx, current, update) assert.EqualError(t, err, "no changes") }) t.Run("same active version", func(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) version := int32(1) current := &query.Query{ ID: uuid.New(), ActiveVersion: version, LatestVersion: int32(2), Type: resultprocessor.TypeJsonExtractor, } update := &resultprocessor.Update{ ID: current.ID, ActiveVersion: &version, } err = svc.normalizeUpdate(ctx, current, update) assert.EqualError(t, err, "no changes") }) t.Run("ONLY active version to next latest", func(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) current := &query.Query{ ID: uuid.New(), ActiveVersion: 1, LatestVersion: 2, Type: resultprocessor.TypeJsonExtractor, } version := int32(3) update := &resultprocessor.Update{ ID: current.ID, ActiveVersion: &version, } err = svc.normalizeUpdate(ctx, current, update) assert.EqualError(t, err, "no changes") }) } func TestNormalizeUpdateRequiredQueryIDs(t *testing.T) { t.Run("Empty Array", func(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) current := &query.Query{ ID: uuid.New(), Type: resultprocessor.TypeJsonExtractor, } update := &resultprocessor.Update{ ID: current.ID, RequiredQueryIDs: &[]uuid.UUID{}, } err = svc.normalizeUpdateRequiredQueryIDs(ctx, current, update) require.NoError(t, err) assert.EqualExportedValues(t, resultprocessor.Update{ ID: current.ID, }, *update) }) t.Run("entry doesn't exist", func(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) current := &query.Query{ ID: uuid.New(), Type: resultprocessor.TypeJsonExtractor, } update := &resultprocessor.Update{ ID: current.ID, RequiredQueryIDs: &[]uuid.UUID{uuid.New()}, } dbids := *update.RequiredQueryIDs pool.ExpectQuery("name: AllQueriesExist :one").WithArgs(dbids).WillReturnRows( pgxmock.NewRows([]string{"all_exist"}). AddRow(false), ) err = svc.normalizeUpdateRequiredQueryIDs(ctx, current, update) assert.Error(t, err) }) t.Run("in dependency tree", func(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) current := &query.Query{ ID: uuid.New(), Type: resultprocessor.TypeJsonExtractor, } update := &resultprocessor.Update{ ID: current.ID, RequiredQueryIDs: &[]uuid.UUID{uuid.New()}, } dbids := *update.RequiredQueryIDs pool.ExpectQuery("name: AllQueriesExist :one").WithArgs(dbids).WillReturnRows( pgxmock.NewRows([]string{"all_exist"}). AddRow(true), ) pool.ExpectQuery("name: IsQueryInDependencyTree :one").WithArgs(dbids, current.ID).WillReturnRows( pgxmock.NewRows([]string{"all_exist"}). AddRow(true), ) err = svc.normalizeUpdateRequiredQueryIDs(ctx, current, update) assert.Error(t, err) }) t.Run("same update and current", func(t *testing.T) { ctx := context.Background() pool, err := pgxmock.NewPool() require.NoError(t, err) cfg := &QueryUpdateConfig{} cfg.DBPool = pool cfg.DBQueries = repository.New(pool) svc := New(cfg, &Services{ Query: query.New(cfg), }) current := &query.Query{ ID: uuid.New(), Type: resultprocessor.TypeJsonExtractor, } update := &resultprocessor.Update{ ID: current.ID, } ids := []uuid.UUID{uuid.New()} update.RequiredQueryIDs = &ids current.RequiredQueryIDs = &ids dbids := *update.RequiredQueryIDs pool.ExpectQuery("name: AllQueriesExist :one").WithArgs(dbids).WillReturnRows( pgxmock.NewRows([]string{"all_exist"}). AddRow(true), ) pool.ExpectQuery("name: IsQueryInDependencyTree :one").WithArgs(dbids, ¤t.ID).WillReturnRows( pgxmock.NewRows([]string{"all_exist"}). AddRow(false), ) err = svc.normalizeUpdateRequiredQueryIDs(ctx, current, update) require.NoError(t, err) assert.EqualExportedValues(t, resultprocessor.Update{ ID: current.ID, }, *update) }) } func TestInformUpdate(t *testing.T) { t.Run("nil active version", func(t *testing.T) { ctx := context.Background() cfg := &QueryUpdateConfig{} mockSQS := queuemock.NewMockSQSClient(t) cfg.QueueClient = mockSQS cfg.QueryVersionSyncURL = "/here" svc := Service{ cfg: cfg, } entity := &resultprocessor.Update{ ID: uuid.New(), ActiveVersion: nil, } err := svc.informUpdate(ctx, entity) require.NoError(t, err) }) t.Run("update active version", func(t *testing.T) { ctx := context.Background() cfg := &QueryUpdateConfig{} mockSQS := queuemock.NewMockSQSClient(t) cfg.QueueClient = mockSQS cfg.QueryVersionSyncURL = "/here" svc := Service{ cfg: cfg, } av := int32(2) entity := &resultprocessor.Update{ ID: uuid.New(), ActiveVersion: &av, } mockSQS.EXPECT(). SendMessage( mock.Anything, mock.MatchedBy(func(in *sqs.SendMessageInput) bool { return *in.QueueUrl == cfg.QueryVersionSyncURL && *in.MessageBody == fmt.Sprintf("{\"id\":\"%s\"}", entity.ID.String()) }), mock.Anything, ). Return(&sqs.SendMessageOutput{}, nil) err := svc.informUpdate(ctx, entity) require.NoError(t, err) }) }