/* Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ package versionsapi import ( "bytes" "context" "encoding/json" "io" "net/http" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" ) func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } func TestValidate(t *testing.T) { testCases := map[string]struct { listFunc func() *List overrideFunc func(list *List) wantErr bool }{ "valid major list": { listFunc: majorList, }, "valid minor list": { listFunc: minorList, }, "invalid stream": { listFunc: majorList, overrideFunc: func(list *List) { list.Stream = "invalid" }, wantErr: true, }, "invalid granularity": { listFunc: majorList, overrideFunc: func(list *List) { list.Granularity = "invalid" }, wantErr: true, }, "invalid kind": { listFunc: majorList, overrideFunc: func(list *List) { list.Kind = "invalid" }, wantErr: true, }, "base ver is not semantic version": { listFunc: majorList, overrideFunc: func(list *List) { list.Base = "invalid" }, wantErr: true, }, "base ver does not reflect major granularity": { listFunc: majorList, overrideFunc: func(list *List) { list.Base = "v1.0" }, wantErr: true, }, "base ver does not reflect minor granularity": { listFunc: minorList, overrideFunc: func(list *List) { list.Base = "v1" }, wantErr: true, }, "version in list is not semantic version": { listFunc: majorList, overrideFunc: func(list *List) { list.Versions[0] = "invalid" }, wantErr: true, }, "version in list is not sub version of base": { listFunc: majorList, overrideFunc: func(list *List) { list.Versions[0] = "v2.1" }, wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) require := require.New(t) list := tc.listFunc() if tc.overrideFunc != nil { tc.overrideFunc(list) } err := list.Validate() if tc.wantErr { assert.Error(err) return } require.NoError(err) }) } } func TestList(t *testing.T) { majorListJSON, err := json.Marshal(majorList()) require.NoError(t, err) minorListJSON, err := json.Marshal(minorList()) require.NoError(t, err) inconsistentList := majorList() inconsistentList.Base = "v2" inconsistentListJSON, err := json.Marshal(inconsistentList) require.NoError(t, err) client := newTestClient(func(req *http.Request) *http.Response { switch req.URL.Path { case "/constellation/v1/ref/test-ref/stream/nightly/versions/major/v1/image.json": return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBuffer(majorListJSON)), Header: make(http.Header), } case "/constellation/v1/ref/test-ref/stream/nightly/versions/minor/v1.1/image.json": return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBuffer(minorListJSON)), Header: make(http.Header), } case "/constellation/v1/ref/test-ref/stream/nightly/versions/major/v1/500.json": // 500 error return &http.Response{ StatusCode: http.StatusInternalServerError, Body: io.NopCloser(bytes.NewBufferString("Server Error.")), Header: make(http.Header), } case "/constellation/v1/ref/test-ref/stream/nightly/versions/major/v1/nojson.json": // invalid format return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBufferString("not json")), Header: make(http.Header), } case "/constellation/v1/ref/test-ref/stream/nightly/versions/major/v2/image.json": // inconsistent list return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBuffer(inconsistentListJSON)), Header: make(http.Header), } case "/constellation/v1/ref/test-ref/stream/nightly/versions/major/v3/image.json": // does not match requested version return &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(bytes.NewBuffer(minorListJSON)), Header: make(http.Header), } } return &http.Response{ StatusCode: http.StatusNotFound, Body: io.NopCloser(bytes.NewBufferString("Not found.")), Header: make(http.Header), } }) testCases := map[string]struct { ref, stream, granularity, base, kind string overrideFile string wantList List wantErr bool }{ "major list fetched remotely": { wantList: *majorList(), }, "minor list fetched remotely": { granularity: "minor", base: "v1.1", wantList: *minorList(), }, "list does not exist": { stream: "unknown", wantErr: true, }, "unexpected error code": { kind: "500", wantErr: true, }, "invalid json returned": { kind: "nojson", wantErr: true, }, "invalid list returned": { base: "v2", wantErr: true, }, "response does not match request": { base: "v3", wantErr: true, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { assert := assert.New(t) require := require.New(t) ref := "test-ref" stream := "nightly" granularity := "major" base := "v1" kind := "image" if tc.stream != "" { stream = tc.stream } if tc.granularity != "" { granularity = tc.granularity } if tc.base != "" { base = tc.base } if tc.kind != "" { kind = tc.kind } fetcher := &Fetcher{ httpc: client, } list, err := fetcher.list(context.Background(), ref, stream, granularity, base, kind) if tc.wantErr { assert.Error(err) return } require.NoError(err) assert.Equal(tc.wantList, *list) }) } } // roundTripFunc . type roundTripFunc func(req *http.Request) *http.Response // RoundTrip . func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { return f(req), nil } // newTestClient returns *http.Client with Transport replaced to avoid making real calls. func newTestClient(fn roundTripFunc) *http.Client { return &http.Client{ Transport: fn, } } func majorList() *List { return &List{ Ref: "test-ref", Stream: "nightly", Granularity: "major", Base: "v1", Kind: "image", Versions: []string{ "v1.0", "v1.1", "v1.2", }, } } func minorList() *List { return &List{ Ref: "test-ref", Stream: "nightly", Granularity: "minor", Base: "v1.1", Kind: "image", Versions: []string{ "v1.1.0", "v1.1.1", "v1.1.2", }, } } func TestIsValidRef(t *testing.T) { testCases := map[string]bool{ "feat/foo": false, "feat-foo": true, "feat$foo": false, "3234": true, "feat foo": false, "refs-heads-feat-foo": false, "": false, } for ref, want := range testCases { t.Run(ref, func(t *testing.T) { assert := assert.New(t) assert.Equal(want, IsValidRef(ref)) }) } } func TestCanonicalRef(t *testing.T) { testCases := map[string]string{ "feat/foo": "feat-foo", "feat-foo": "feat-foo", "feat$foo": "feat-foo", "3234": "3234", "feat foo": "feat-foo", } for ref, want := range testCases { t.Run(ref, func(t *testing.T) { assert := assert.New(t) assert.Equal(want, CanonicalRef(ref)) }) } } func TestIsValidStream(t *testing.T) { testCases := []struct { branch string stream string want bool }{ {branch: "-", stream: "stable", want: true}, {branch: "-", stream: "debug", want: true}, {branch: "-", stream: "nightly", want: false}, {branch: "-", stream: "console", want: true}, {branch: "main", stream: "stable", want: false}, {branch: "main", stream: "debug", want: true}, {branch: "main", stream: "nightly", want: true}, {branch: "main", stream: "console", want: true}, {branch: "foo-branch", stream: "nightly", want: true}, {branch: "foo-branch", stream: "console", want: true}, {branch: "foo-branch", stream: "debug", want: true}, {branch: "foo-branch", stream: "stable", want: false}, } for _, tc := range testCases { t.Run(tc.branch+"+"+tc.stream, func(t *testing.T) { assert := assert.New(t) assert.Equal(tc.want, IsValidStream(tc.branch, tc.stream)) }) } }