| package sqlalertstore |
| |
| import ( |
| "context" |
| "database/sql" |
| "encoding/json" |
| "testing" |
| "time" |
| |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
| "go.skia.org/infra/go/sql/pool" |
| "go.skia.org/infra/perf/go/alerts" |
| "go.skia.org/infra/perf/go/sql/sqltest" |
| ) |
| |
| func setUp(t *testing.T) (alerts.Store, pool.Pool) { |
| db := sqltest.NewSpannerDBForTests(t, "alertstore") |
| store, err := New(db) |
| require.NoError(t, err) |
| |
| return store, db |
| } |
| |
| // Tests a hypothetical pipeline of Store. |
| func TestStore_SaveListDelete(t *testing.T) { |
| ctx := context.Background() |
| store, _ := setUp(t) |
| |
| // TODO(jcgregorio) Break up into finer grained tests. |
| cfg := alerts.NewConfig() |
| cfg.Query = "source_type=svg" |
| cfg.DisplayName = "bar" |
| err := store.Save(ctx, &alerts.SaveRequest{ |
| Cfg: cfg, |
| SubKey: nil, |
| }) |
| assert.NoError(t, err) |
| require.NotEqual(t, alerts.BadAlertIDAsAsString, cfg.IDAsString) |
| |
| // Confirm it appears in the list. |
| cfgs, err := store.List(ctx, false) |
| require.NoError(t, err) |
| require.Len(t, cfgs, 1) |
| |
| // Delete it. |
| err = store.Delete(ctx, int(cfgs[0].IDAsStringToInt())) |
| assert.NoError(t, err) |
| |
| // Confirm it is still there if we list deleted configs. |
| cfgs, err = store.List(ctx, true) |
| assert.NoError(t, err) |
| assert.Len(t, cfgs, 1) |
| require.NotEqual(t, alerts.BadAlertIDAsAsString, cfgs[0].IDAsString) |
| |
| // Confirm it is not there if we don't list deleted configs. |
| cfgs, err = store.List(ctx, false) |
| assert.NoError(t, err) |
| assert.Len(t, cfgs, 0) |
| |
| // Store a second config. |
| cfg = alerts.NewConfig() |
| cfg.Query = "source_type=skp" |
| cfg.DisplayName = "foo" |
| err = store.Save(ctx, &alerts.SaveRequest{ |
| Cfg: cfg, |
| SubKey: nil, |
| }) |
| assert.NoError(t, err) |
| |
| // Confirm they are both listed when including deleted configs, and they are |
| // ordered by DisplayName. |
| cfgs, err = store.List(ctx, true) |
| assert.NoError(t, err) |
| assert.Len(t, cfgs, 2) |
| assert.Equal(t, "bar", cfgs[0].DisplayName) |
| assert.Equal(t, "foo", cfgs[1].DisplayName) |
| } |
| |
| // Tests we can list two active Alerts. |
| func TestStoreList_ListActiveAlerts(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| |
| cfg1 := alerts.NewConfig() |
| cfg1.SetIDFromInt64(1) |
| cfg1.Query = "source_type=svg" |
| cfg1.DisplayName = "bar" |
| insertAlertToDb(t, ctx, db, cfg1, nil) |
| |
| cfg2 := alerts.NewConfig() |
| cfg2.SetIDFromInt64(2) |
| cfg2.Query = "source_type=skp" |
| cfg2.DisplayName = "foo" |
| insertAlertToDb(t, ctx, db, cfg2, nil) |
| |
| cfgs, err := store.List(ctx, true) |
| assert.NoError(t, err) |
| assert.Len(t, cfgs, 2) |
| assert.Equal(t, "bar", cfgs[0].DisplayName) |
| assert.Equal(t, "foo", cfgs[1].DisplayName) |
| assert.Equal(t, "source_type=svg", cfgs[0].Query) |
| assert.Equal(t, "source_type=skp", cfgs[1].Query) |
| assert.Equal(t, "1", cfgs[0].IDAsString) |
| assert.Equal(t, "2", cfgs[1].IDAsString) |
| |
| // Confirm all alerts are active. |
| cfgs2, err := store.List(ctx, false) |
| assert.Equal(t, cfgs, cfgs2) |
| } |
| |
| // Tests we can list one deleted Alert. |
| func TestStoreList_ListDeletedAlert(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| |
| cfg1 := alerts.NewConfig() |
| cfg1.SetIDFromInt64(1) |
| cfg1.Query = "source_type=svg" |
| cfg1.DisplayName = "bar" |
| insertAlertToDb(t, ctx, db, cfg1, nil) |
| |
| cfg2 := alerts.NewConfig() |
| cfg2.SetIDFromInt64(2) |
| cfg2.Query = "source_type=skp" |
| cfg2.DisplayName = "foo" |
| cfg2.StateAsString = alerts.DELETED |
| insertAlertToDb(t, ctx, db, cfg2, nil) |
| |
| cfgs, err := store.List(ctx, true) |
| assert.NoError(t, err) |
| assert.Len(t, cfgs, 2) |
| assert.Equal(t, "bar", cfgs[0].DisplayName) |
| assert.Equal(t, "foo", cfgs[1].DisplayName) |
| assert.Equal(t, "source_type=svg", cfgs[0].Query) |
| assert.Equal(t, "source_type=skp", cfgs[1].Query) |
| assert.Equal(t, "1", cfgs[0].IDAsString) |
| assert.Equal(t, "2", cfgs[1].IDAsString) |
| |
| // Confirm one alert is active |
| cfgs2, err := store.List(ctx, false) |
| assert.NotEqual(t, cfgs, cfgs2) |
| assert.Len(t, cfgs2, 1) |
| assert.Equal(t, "bar", cfgs2[0].DisplayName) |
| assert.Equal(t, "source_type=svg", cfgs2[0].Query) |
| assert.Equal(t, "1", cfgs2[0].IDAsString) |
| } |
| |
| // Tests we can mark one Alert as deleted using Delete |
| func TestStoreDelete_DeleteOneAlert(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| |
| cfg := alerts.NewConfig() |
| cfg.SetIDFromInt64(2) |
| cfg.Query = "source_type=svg" |
| cfg.DisplayName = "bar" |
| insertAlertToDb(t, ctx, db, cfg, nil) |
| |
| _, subKey1, configState1 := getAlertFromDb(t, ctx, db, 2) |
| assert.Nil(t, subKey1) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.ACTIVE), configState1) |
| |
| err := store.Delete(ctx, 2) |
| require.NoError(t, err) |
| |
| cfg2, subKey2, configState2 := getAlertFromDb(t, ctx, db, 2) |
| assert.Nil(t, subKey2) |
| assert.Equal(t, "bar", cfg2.DisplayName) |
| assert.Equal(t, "source_type=svg", cfg2.Query) |
| assert.Equal(t, "2", cfg2.IDAsString) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.DELETED), configState2) |
| } |
| |
| // Tests that inserting an ID with -1 ID, generates a new ID for the Alert. |
| func TestStoreSave_SaveWithBadID(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| |
| cfg := alerts.NewConfig() |
| cfg.SetIDFromInt64(alerts.BadAlertID) |
| cfg.Query = "source_type=svg" |
| cfg.DisplayName = "bar" |
| err := store.Save(ctx, &alerts.SaveRequest{ |
| Cfg: cfg, |
| SubKey: nil, |
| }) |
| require.NoError(t, err) |
| |
| _, subKey1, configState1 := getAlertFromDb(t, ctx, db, cfg.IDAsStringToInt()) |
| assert.Nil(t, subKey1) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.ACTIVE), configState1) |
| assert.NotEqual(t, alerts.BadAlertID, cfg.IDAsStringToInt()) |
| } |
| |
| // Tests inserting an Alert with valid ID. |
| func TestStoreSave_SaveWithValidID(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| |
| cfg := alerts.NewConfig() |
| cfg.SetIDFromInt64(1) |
| cfg.Query = "source_type=svg" |
| cfg.DisplayName = "bar" |
| err := store.Save(ctx, &alerts.SaveRequest{ |
| Cfg: cfg, |
| SubKey: nil, |
| }) |
| require.NoError(t, err) |
| _, subKey, configState := getAlertFromDb(t, ctx, db, 1) |
| assert.Nil(t, subKey) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.ACTIVE), configState) |
| } |
| |
| // Tests inserting an Alert with an associated Subscription |
| func TestStoreSave_SaveWithSubscription(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| |
| cfg := alerts.NewConfig() |
| cfg.SetIDFromInt64(1) |
| cfg.Query = "source_type=svg" |
| cfg.DisplayName = "bar" |
| err := store.Save(ctx, &alerts.SaveRequest{ |
| Cfg: cfg, |
| SubKey: &alerts.SubKey{ |
| SubName: "Test Subscription", |
| SubRevision: "abcd", |
| }, |
| }) |
| require.NoError(t, err) |
| |
| _, subKey, configState := getAlertFromDb(t, ctx, db, 1) |
| assert.Equal(t, "Test Subscription", subKey.SubName) |
| assert.Equal(t, "abcd", subKey.SubRevision) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.ACTIVE), configState) |
| } |
| |
| func TestStoreReplaceAll_EmptyAlerts(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| oldAlert := &alerts.SaveRequest{ |
| Cfg: &alerts.Alert{ |
| IDAsString: "1", |
| DisplayName: "Alert A", |
| }, |
| SubKey: &alerts.SubKey{ |
| SubName: "a", |
| SubRevision: "abcd", |
| }, |
| } |
| |
| // First populate DB with 1 alert and confirm that it's set to active. |
| insertAlertToDb(t, ctx, db, oldAlert.Cfg, oldAlert.SubKey) |
| _, _, configState := getAlertFromDb(t, ctx, db, 1) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.ACTIVE), configState) |
| |
| newAlerts := []*alerts.SaveRequest{} |
| |
| tx, err := db.Begin(ctx) |
| require.NoError(t, err) |
| |
| err = store.ReplaceAll(ctx, newAlerts, tx) |
| require.NoError(t, err) |
| |
| err = tx.Commit(ctx) |
| require.NoError(t, err) |
| |
| // Now check that old alert is inactive and new alerts are active |
| _, _, configState = getAlertFromDb(t, ctx, db, 1) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.DELETED), configState) |
| |
| activeAlerts := listAllAlertsInDb(t, ctx, db, alerts.ConfigStateToInt(alerts.ACTIVE)) |
| assert.Len(t, activeAlerts, 0) |
| } |
| |
| func TestStoreReplaceAll_ValidAlerts(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| oldAlert := &alerts.SaveRequest{ |
| Cfg: &alerts.Alert{ |
| IDAsString: "1", |
| DisplayName: "Alert A", |
| }, |
| SubKey: &alerts.SubKey{ |
| SubName: "a", |
| SubRevision: "abcd", |
| }, |
| } |
| |
| // First populate DB with 1 alert and confirm that it's set to active. |
| insertAlertToDb(t, ctx, db, oldAlert.Cfg, oldAlert.SubKey) |
| _, _, configState := getAlertFromDb(t, ctx, db, 1) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.ACTIVE), configState) |
| |
| newAlerts := []*alerts.SaveRequest{ |
| { |
| Cfg: &alerts.Alert{ |
| IDAsString: "2", |
| DisplayName: "Alert B", |
| }, |
| SubKey: &alerts.SubKey{ |
| SubName: "b", |
| SubRevision: "abcde", |
| }, |
| }, |
| { |
| Cfg: &alerts.Alert{ |
| IDAsString: "3", |
| DisplayName: "Alert C", |
| }, |
| SubKey: &alerts.SubKey{ |
| SubName: "b", |
| SubRevision: "abcde", |
| }, |
| }, |
| } |
| |
| tx, err := db.Begin(ctx) |
| require.NoError(t, err) |
| |
| err = store.ReplaceAll(ctx, newAlerts, tx) |
| require.NoError(t, err) |
| |
| err = tx.Commit(ctx) |
| require.NoError(t, err) |
| |
| // Now check that old alert is inactive and new alerts are active |
| _, _, configState = getAlertFromDb(t, ctx, db, 1) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.DELETED), configState) |
| |
| activeAlerts := listAllAlertsInDb(t, ctx, db, alerts.ConfigStateToInt(alerts.ACTIVE)) |
| assert.Len(t, activeAlerts, 2) |
| for _, alert := range activeAlerts { |
| assert.True(t, alert.IDAsString == "2" || alert.IDAsString == "3") |
| } |
| } |
| |
| func TestStoreReplaceAll_DuplicateAlerts(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| oldAlert := &alerts.SaveRequest{ |
| Cfg: &alerts.Alert{ |
| IDAsString: "1", |
| DisplayName: "Alert A", |
| }, |
| SubKey: &alerts.SubKey{ |
| SubName: "a", |
| SubRevision: "abcd", |
| }, |
| } |
| |
| // First populate DB with 1 alert and confirm that it's set to active. |
| insertAlertToDb(t, ctx, db, oldAlert.Cfg, oldAlert.SubKey) |
| _, _, configState := getAlertFromDb(t, ctx, db, 1) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.ACTIVE), configState) |
| |
| newAlerts := []*alerts.SaveRequest{ |
| { |
| Cfg: &alerts.Alert{ |
| IDAsString: "2", |
| DisplayName: "Alert B", |
| }, |
| SubKey: &alerts.SubKey{ |
| SubName: "b", |
| SubRevision: "abcde", |
| }, |
| }, |
| { |
| Cfg: &alerts.Alert{ |
| IDAsString: "3", |
| DisplayName: "Alert C", |
| }, |
| SubKey: &alerts.SubKey{ |
| SubName: "b", |
| SubRevision: "abcde", |
| }, |
| }, |
| } |
| |
| tx, err := db.Begin(ctx) |
| require.NoError(t, err) |
| |
| err = store.ReplaceAll(ctx, newAlerts, tx) |
| require.NoError(t, err) |
| |
| err = tx.Commit(ctx) |
| require.NoError(t, err) |
| |
| // Now check that old alert is inactive and new alerts are active |
| _, _, configState = getAlertFromDb(t, ctx, db, 1) |
| assert.Equal(t, alerts.ConfigStateToInt(alerts.DELETED), configState) |
| |
| activeAlerts := listAllAlertsInDb(t, ctx, db, alerts.ConfigStateToInt(alerts.ACTIVE)) |
| assert.Len(t, activeAlerts, 2) |
| for _, alert := range activeAlerts { |
| assert.True(t, alert.IDAsString == "2" || alert.IDAsString == "3") |
| } |
| } |
| |
| // insertAlertToDb inserts the given Alert into the database. |
| func insertAlertToDb(t *testing.T, ctx context.Context, db pool.Pool, cfg *alerts.Alert, subKey *alerts.SubKey) { |
| b, err := json.Marshal(cfg) |
| require.NoError(t, err) |
| nameOrNull := sql.NullString{Valid: false} |
| revisionOrNull := sql.NullString{Valid: false} |
| |
| if subKey != nil { |
| nameOrNull.String = subKey.SubName |
| nameOrNull.Valid = true |
| revisionOrNull.String = subKey.SubRevision |
| revisionOrNull.Valid = true |
| } |
| const query = `INSERT INTO Alerts |
| (id, alert, config_state, last_modified, sub_name, sub_revision) |
| VALUES ($1,$2,$3,$4,$5,$6)` |
| if _, err := db.Exec(ctx, query, cfg.IDAsStringToInt(), string(b), cfg.StateToInt(), time.Now().Unix(), nameOrNull, revisionOrNull); err != nil { |
| require.NoError(t, err) |
| } |
| } |
| |
| func TestStore_ListForSubscription_Empty(t *testing.T) { |
| ctx := context.Background() |
| store, _ := setUp(t) |
| |
| cfgs, err := store.ListForSubscription(ctx, "test-subscription") |
| require.NoError(t, err) |
| assert.Len(t, cfgs, 0) |
| } |
| |
| func TestStore_ListForSubscription_SingleMatch(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| |
| cfg1 := alerts.NewConfig() |
| cfg1.SetIDFromInt64(1) |
| cfg1.Query = "source_type=svg" |
| cfg1.DisplayName = "bar" |
| subKey1 := &alerts.SubKey{SubName: "test-subscription", SubRevision: "rev1"} |
| insertAlertToDb(t, ctx, db, cfg1, subKey1) |
| |
| cfgs, err := store.ListForSubscription(ctx, "test-subscription") |
| require.NoError(t, err) |
| assert.Len(t, cfgs, 1) |
| assert.Equal(t, "bar", cfgs[0].DisplayName) |
| assert.Equal(t, "source_type=svg", cfgs[0].Query) |
| assert.Equal(t, int64(1), cfgs[0].IDAsStringToInt()) |
| } |
| |
| func TestStore_ListForSubscription_MultipleMatches(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| |
| cfg1 := alerts.NewConfig() |
| cfg1.SetIDFromInt64(1) |
| cfg1.Query = "source_type=svg" |
| cfg1.DisplayName = "bar" |
| subKey1 := &alerts.SubKey{SubName: "test-subscription", SubRevision: "rev1"} |
| insertAlertToDb(t, ctx, db, cfg1, subKey1) |
| |
| cfg2 := alerts.NewConfig() |
| cfg2.SetIDFromInt64(2) |
| cfg2.Query = "source_type=skp" |
| cfg2.DisplayName = "foo" |
| subKey2 := &alerts.SubKey{SubName: "test-subscription", SubRevision: "rev2"} |
| insertAlertToDb(t, ctx, db, cfg2, subKey2) |
| |
| cfg3 := alerts.NewConfig() |
| cfg3.SetIDFromInt64(3) |
| cfg3.Query = "source_type=other" |
| cfg3.DisplayName = "baz" |
| subKey3 := &alerts.SubKey{SubName: "other-subscription", SubRevision: "rev3"} |
| insertAlertToDb(t, ctx, db, cfg3, subKey3) |
| |
| cfgs, err := store.ListForSubscription(ctx, "test-subscription") |
| require.NoError(t, err) |
| assert.Len(t, cfgs, 2) |
| assert.Equal(t, "bar", cfgs[0].DisplayName) |
| assert.Equal(t, "foo", cfgs[1].DisplayName) |
| assert.Equal(t, "source_type=svg", cfgs[0].Query) |
| assert.Equal(t, "source_type=skp", cfgs[1].Query) |
| assert.Equal(t, int64(1), cfgs[0].IDAsStringToInt()) |
| assert.Equal(t, int64(2), cfgs[1].IDAsStringToInt()) |
| } |
| |
| func TestStore_ListForSubscription_NoMatch(t *testing.T) { |
| ctx := context.Background() |
| store, db := setUp(t) |
| |
| cfg1 := alerts.NewConfig() |
| cfg1.SetIDFromInt64(1) |
| cfg1.Query = "source_type=svg" |
| cfg1.DisplayName = "bar" |
| subKey1 := &alerts.SubKey{SubName: "other-subscription", SubRevision: "rev1"} |
| insertAlertToDb(t, ctx, db, cfg1, subKey1) |
| |
| cfgs, err := store.ListForSubscription(ctx, "test-subscription") |
| require.NoError(t, err) |
| assert.Len(t, cfgs, 0) |
| } |
| |
| func getAlertFromDb(t *testing.T, ctx context.Context, db pool.Pool, id int64) (*alerts.Alert, *alerts.SubKey, int) { |
| alert := &alerts.Alert{} |
| var serializedAlert string |
| |
| var nameOrNull sql.NullString |
| var revisionOrNull sql.NullString |
| var configState int |
| err := db.QueryRow(ctx, "SELECT alert, sub_name, sub_revision, config_state FROM Alerts WHERE id = $1", id).Scan( |
| &serializedAlert, |
| &nameOrNull, |
| &revisionOrNull, |
| &configState, |
| ) |
| require.NoError(t, err) |
| |
| err = json.Unmarshal([]byte(serializedAlert), alert) |
| require.NoError(t, err) |
| |
| if !nameOrNull.Valid || !revisionOrNull.Valid { |
| return alert, nil, configState |
| } |
| |
| return alert, &alerts.SubKey{ |
| SubName: nameOrNull.String, |
| SubRevision: revisionOrNull.String, |
| }, configState |
| } |
| |
| // List all Alerts that match the given configState |
| func listAllAlertsInDb(t *testing.T, ctx context.Context, db pool.Pool, configState int) []*alerts.Alert { |
| rows, err := db.Query(ctx, "SELECT alert FROM Alerts WHERE config_state = $1", configState) |
| require.NoError(t, err) |
| ret := []*alerts.Alert{} |
| for rows.Next() { |
| var serializedAlert string |
| if err := rows.Scan(&serializedAlert); err != nil { |
| require.NoError(t, err) |
| } |
| a := &alerts.Alert{} |
| if err := json.Unmarshal([]byte(serializedAlert), a); err != nil { |
| require.NoError(t, err) |
| } |
| ret = append(ret, a) |
| } |
| return ret |
| } |