go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luci_notify/internal/alerts/alerts_test.go (about) 1 // Copyright 2024 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package alerts 16 17 import ( 18 "context" 19 "testing" 20 "time" 21 22 "cloud.google.com/go/spanner" 23 24 "go.chromium.org/luci/server/span" 25 26 "go.chromium.org/luci/luci_notify/internal/testutil" 27 28 . "github.com/smartystreets/goconvey/convey" 29 . "go.chromium.org/luci/common/testing/assertions" 30 ) 31 32 func TestValidation(t *testing.T) { 33 Convey("Validate", t, func() { 34 Convey("valid", func() { 35 err := Validate(NewAlertBuilder().Build()) 36 So(err, ShouldBeNil) 37 }) 38 Convey("bug", func() { 39 Convey("must not be negative", func() { 40 err := Validate(NewAlertBuilder().WithBug(-1).Build()) 41 So(err, ShouldErrLike, "bug: must be zero or positive") 42 }) 43 Convey("zero is valid", func() { 44 err := Validate(NewAlertBuilder().WithBug(0).Build()) 45 So(err, ShouldBeNil) 46 }) 47 }) 48 Convey("SilenceUntil", func() { 49 Convey("must not be negative", func() { 50 err := Validate(NewAlertBuilder().WithSilenceUntil(-1).Build()) 51 So(err, ShouldErrLike, "silence_until: must be zero or positive") 52 }) 53 Convey("zero is valid", func() { 54 err := Validate(NewAlertBuilder().WithSilenceUntil(0).Build()) 55 So(err, ShouldBeNil) 56 }) 57 }) 58 }) 59 } 60 61 func TestStatusTable(t *testing.T) { 62 Convey("Put", t, func() { 63 ctx := testutil.SpannerTestContext(t) 64 alert := NewAlertBuilder().WithBug(10).Build() 65 66 m, err := Put(alert) 67 So(err, ShouldBeNil) 68 ts, err := span.Apply(ctx, []*spanner.Mutation{m}) 69 alert.ModifyTime = ts.UTC() 70 71 So(err, ShouldBeNil) 72 fetched, err := ReadBatch(span.Single(ctx), []string{alert.AlertKey}) 73 So(err, ShouldBeNil) 74 So(fetched, ShouldEqual, []*Alert{alert}) 75 }) 76 Convey("Put deletes when all info is zero", t, func() { 77 ctx := testutil.SpannerTestContext(t) 78 alert := NewAlertBuilder().CreateInDB(ctx) 79 80 m, err := Put(alert) 81 So(err, ShouldBeNil) 82 ts, err := span.Apply(ctx, []*spanner.Mutation{m}) 83 alert.ModifyTime = ts.UTC() 84 85 So(err, ShouldBeNil) 86 // When we read a non-existent entry, a fake entry will be returned, so the only 87 // way to tell it was actually deleted is a zero timestamp. 88 fetched, err := ReadBatch(span.Single(ctx), []string{alert.AlertKey}) 89 So(err, ShouldBeNil) 90 So(len(fetched), ShouldEqual, 1) 91 So(fetched[0].ModifyTime, ShouldEqual, time.Time{}) 92 }) 93 94 Convey("ReadBatch", t, func() { 95 Convey("Single", func() { 96 ctx := testutil.SpannerTestContext(t) 97 alert := NewAlertBuilder().CreateInDB(ctx) 98 99 fetched, err := ReadBatch(span.Single(ctx), []string{alert.AlertKey}) 100 101 So(err, ShouldBeNil) 102 So(fetched, ShouldEqual, []*Alert{alert}) 103 }) 104 105 Convey("NotPresent returns empty fields", func() { 106 ctx := testutil.SpannerTestContext(t) 107 _ = NewAlertBuilder().CreateInDB(ctx) 108 109 fetched, err := ReadBatch(span.Single(ctx), []string{"not-present"}) 110 111 So(err, ShouldBeNil) 112 So(fetched, ShouldEqual, []*Alert{{ 113 AlertKey: "not-present", 114 Bug: 0, 115 SilenceUntil: 0, 116 ModifyTime: time.Time{}, 117 }}) 118 }) 119 Convey("Multiple returned in order", func() { 120 ctx := testutil.SpannerTestContext(t) 121 alert1 := NewAlertBuilder().WithAlertKey("alert1").WithBug(1).CreateInDB(ctx) 122 alert2 := NewAlertBuilder().WithAlertKey("alert2").WithBug(2).CreateInDB(ctx) 123 124 forwards, err := ReadBatch(span.Single(ctx), []string{"alert1", "alert2"}) 125 backwards, err2 := ReadBatch(span.Single(ctx), []string{"alert2", "alert1"}) 126 127 So(err, ShouldBeNil) 128 So(err2, ShouldBeNil) 129 So(forwards, ShouldEqual, []*Alert{alert1, alert2}) 130 So(backwards, ShouldEqual, []*Alert{alert2, alert1}) 131 }) 132 }) 133 } 134 135 type AlertBuilder struct { 136 alert Alert 137 } 138 139 func NewAlertBuilder() *AlertBuilder { 140 return &AlertBuilder{alert: Alert{ 141 AlertKey: "test-alert", 142 Bug: 0, 143 SilenceUntil: 0, 144 ModifyTime: spanner.CommitTimestamp, 145 }} 146 } 147 148 func (b *AlertBuilder) WithAlertKey(alertKey string) *AlertBuilder { 149 b.alert.AlertKey = alertKey 150 return b 151 } 152 153 func (b *AlertBuilder) WithBug(bug int64) *AlertBuilder { 154 b.alert.Bug = bug 155 return b 156 } 157 158 func (b *AlertBuilder) WithSilenceUntil(silenceUntil int64) *AlertBuilder { 159 b.alert.SilenceUntil = silenceUntil 160 return b 161 } 162 163 func (b *AlertBuilder) WithModifyTime(modifyTime time.Time) *AlertBuilder { 164 b.alert.ModifyTime = modifyTime 165 return b 166 } 167 168 func (b *AlertBuilder) Build() *Alert { 169 s := b.alert 170 return &s 171 } 172 173 func (b *AlertBuilder) CreateInDB(ctx context.Context) *Alert { 174 s := b.Build() 175 row := map[string]any{ 176 "AlertKey": s.AlertKey, 177 "Bug": s.Bug, 178 "SilenceUntil": s.SilenceUntil, 179 "ModifyTime": s.ModifyTime, 180 } 181 m := spanner.InsertOrUpdateMap("Alerts", row) 182 ts, err := span.Apply(ctx, []*spanner.Mutation{m}) 183 So(err, ShouldBeNil) 184 if s.ModifyTime == spanner.CommitTimestamp { 185 s.ModifyTime = ts.UTC() 186 } 187 return s 188 }