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  }