go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tree_status/internal/status/status_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 status 16 17 import ( 18 "context" 19 "strings" 20 "testing" 21 "time" 22 23 "cloud.google.com/go/spanner" 24 25 "go.chromium.org/luci/server/span" 26 27 "go.chromium.org/luci/tree_status/internal/testutil" 28 pb "go.chromium.org/luci/tree_status/proto/v1" 29 30 . "github.com/smartystreets/goconvey/convey" 31 . "go.chromium.org/luci/common/testing/assertions" 32 ) 33 34 func TestValidation(t *testing.T) { 35 Convey("Validate", t, func() { 36 Convey("valid", func() { 37 err := Validate(NewStatusBuilder().Build()) 38 So(err, ShouldBeNil) 39 }) 40 Convey("tree_name", func() { 41 Convey("must be specified", func() { 42 err := Validate(NewStatusBuilder().WithTreeName("").Build()) 43 So(err, ShouldErrLike, "tree: must be specified") 44 }) 45 Convey("must match format", func() { 46 err := Validate(NewStatusBuilder().WithTreeName("INVALID").Build()) 47 So(err, ShouldErrLike, "tree: expected format") 48 }) 49 }) 50 Convey("id", func() { 51 Convey("must be specified", func() { 52 err := Validate(NewStatusBuilder().WithStatusID("").Build()) 53 So(err, ShouldErrLike, "id: must be specified") 54 }) 55 Convey("must match format", func() { 56 err := Validate(NewStatusBuilder().WithStatusID("INVALID").Build()) 57 So(err, ShouldErrLike, "id: expected format") 58 }) 59 }) 60 Convey("general_state", func() { 61 Convey("must be specified", func() { 62 err := Validate(NewStatusBuilder().WithGeneralStatus(pb.GeneralState_GENERAL_STATE_UNSPECIFIED).Build()) 63 So(err, ShouldErrLike, "general_state: must be specified") 64 }) 65 Convey("must be a valid enum value", func() { 66 err := Validate(NewStatusBuilder().WithGeneralStatus(pb.GeneralState(100)).Build()) 67 So(err, ShouldErrLike, "general_state: invalid enum value") 68 }) 69 }) 70 Convey("message", func() { 71 Convey("must be specified", func() { 72 err := Validate(NewStatusBuilder().WithMessage("").Build()) 73 So(err, ShouldErrLike, "message: must be specified") 74 }) 75 Convey("must not exceed length", func() { 76 err := Validate(NewStatusBuilder().WithMessage(strings.Repeat("a", 1025)).Build()) 77 So(err, ShouldErrLike, "message: longer than 1024 bytes") 78 }) 79 Convey("invalid utf-8 string", func() { 80 err := Validate(NewStatusBuilder().WithMessage("\xbd").Build()) 81 So(err, ShouldErrLike, "message: not a valid utf8 string") 82 }) 83 // TODO: unicode tests 84 85 }) 86 }) 87 } 88 89 func TestStatusTable(t *testing.T) { 90 Convey("Create", t, func() { 91 ctx := testutil.SpannerTestContext(t) 92 status := NewStatusBuilder().Build() 93 94 m, err := Create(status, status.CreateUser) 95 So(err, ShouldBeNil) 96 ts, err := span.Apply(ctx, []*spanner.Mutation{m}) 97 status.CreateTime = ts.UTC() 98 99 So(err, ShouldBeNil) 100 fetched, err := ReadLatest(span.Single(ctx), "chromium") 101 So(err, ShouldBeNil) 102 So(fetched, ShouldEqual, status) 103 }) 104 105 Convey("Read", t, func() { 106 Convey("Single", func() { 107 ctx := testutil.SpannerTestContext(t) 108 status := NewStatusBuilder().CreateInDB(ctx) 109 110 fetched, err := Read(span.Single(ctx), "chromium", status.StatusID) 111 112 So(err, ShouldBeNil) 113 So(fetched, ShouldEqual, status) 114 }) 115 116 Convey("NotPresent", func() { 117 ctx := testutil.SpannerTestContext(t) 118 _ = NewStatusBuilder().CreateInDB(ctx) 119 120 _, err := Read(span.Single(ctx), "chromium", "1234") 121 122 So(err, ShouldEqual, NotExistsErr) 123 }) 124 }) 125 126 Convey("ReadLatest", t, func() { 127 Convey("Exists", func() { 128 ctx := testutil.SpannerTestContext(t) 129 _ = NewStatusBuilder().WithMessage("older").CreateInDB(ctx) 130 expected := NewStatusBuilder().WithMessage("newer").CreateInDB(ctx) 131 132 fetched, err := ReadLatest(span.Single(ctx), "chromium") 133 134 So(err, ShouldBeNil) 135 So(fetched, ShouldEqual, expected) 136 }) 137 138 Convey("NotPresent", func() { 139 ctx := testutil.SpannerTestContext(t) 140 141 _, err := ReadLatest(span.Single(ctx), "chromium") 142 143 So(err, ShouldEqual, NotExistsErr) 144 }) 145 }) 146 147 Convey("List", t, func() { 148 Convey("Empty", func() { 149 ctx := testutil.SpannerTestContext(t) 150 151 actual, hasNextPage, err := List(span.Single(ctx), "chromium", nil) 152 153 So(err, ShouldBeNil) 154 So(actual, ShouldHaveLength, 0) 155 So(hasNextPage, ShouldBeFalse) 156 }) 157 158 Convey("Single page", func() { 159 ctx := testutil.SpannerTestContext(t) 160 older := NewStatusBuilder().WithMessage("older").CreateInDB(ctx) 161 newer := NewStatusBuilder().WithMessage("newer").CreateInDB(ctx) 162 163 actual, hasNextPage, err := List(span.Single(ctx), "chromium", nil) 164 165 So(err, ShouldBeNil) 166 So(actual, ShouldHaveLength, 2) 167 So(actual, ShouldEqual, []*Status{newer, older}) 168 So(hasNextPage, ShouldBeFalse) 169 }) 170 171 Convey("Paginated", func() { 172 ctx := testutil.SpannerTestContext(t) 173 older := NewStatusBuilder().WithMessage("older").CreateInDB(ctx) 174 newer := NewStatusBuilder().WithMessage("newer").CreateInDB(ctx) 175 176 firstPage, hasSecondPage, err1 := List(span.Single(ctx), "chromium", &ListOptions{Offset: 0, Limit: 1}) 177 secondPage, hasThirdPage, err2 := List(span.Single(ctx), "chromium", &ListOptions{Offset: 1, Limit: 1}) 178 179 So(err1, ShouldBeNil) 180 So(err2, ShouldBeNil) 181 So(firstPage, ShouldEqual, []*Status{newer}) 182 So(secondPage, ShouldEqual, []*Status{older}) 183 So(hasSecondPage, ShouldBeTrue) 184 So(hasThirdPage, ShouldBeFalse) 185 }) 186 }) 187 } 188 189 type StatusBuilder struct { 190 status Status 191 } 192 193 func NewStatusBuilder() *StatusBuilder { 194 id, err := GenerateID() 195 So(err, ShouldBeNil) 196 return &StatusBuilder{status: Status{ 197 TreeName: "chromium", 198 StatusID: id, 199 GeneralStatus: pb.GeneralState_OPEN, 200 Message: "Tree is open!", 201 CreateUser: "user1", 202 CreateTime: spanner.CommitTimestamp, 203 }} 204 } 205 206 func (b *StatusBuilder) WithTreeName(treeName string) *StatusBuilder { 207 b.status.TreeName = treeName 208 return b 209 } 210 211 func (b *StatusBuilder) WithStatusID(id string) *StatusBuilder { 212 b.status.StatusID = id 213 return b 214 } 215 216 func (b *StatusBuilder) WithGeneralStatus(state pb.GeneralState) *StatusBuilder { 217 b.status.GeneralStatus = state 218 return b 219 } 220 221 func (b *StatusBuilder) WithMessage(message string) *StatusBuilder { 222 b.status.Message = message 223 return b 224 } 225 226 func (b *StatusBuilder) WithCreateTime(createTime time.Time) *StatusBuilder { 227 b.status.CreateTime = createTime 228 return b 229 } 230 231 func (b *StatusBuilder) WithCreateUser(user string) *StatusBuilder { 232 b.status.CreateUser = user 233 return b 234 } 235 236 func (b *StatusBuilder) Build() *Status { 237 s := b.status 238 return &s 239 } 240 241 func (b *StatusBuilder) CreateInDB(ctx context.Context) *Status { 242 s := b.Build() 243 row := map[string]any{ 244 "TreeName": s.TreeName, 245 "StatusId": s.StatusID, 246 "GeneralStatus": int64(s.GeneralStatus), 247 "Message": s.Message, 248 "CreateUser": s.CreateUser, 249 "CreateTime": s.CreateTime, 250 } 251 m := spanner.InsertOrUpdateMap("Status", row) 252 ts, err := span.Apply(ctx, []*spanner.Mutation{m}) 253 So(err, ShouldBeNil) 254 if s.CreateTime == spanner.CommitTimestamp { 255 s.CreateTime = ts.UTC() 256 } 257 return s 258 }