hub/internal/handlers/repo/handlers_test.go

702 lines
19 KiB
Go

package repo
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"time"
"github.com/artifacthub/hub/internal/handlers/helpers"
"github.com/artifacthub/hub/internal/hub"
"github.com/artifacthub/hub/internal/repo"
"github.com/artifacthub/hub/internal/tests"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestMain(m *testing.M) {
zerolog.SetGlobalLevel(zerolog.Disabled)
os.Exit(m.Run())
}
func TestAdd(t *testing.T) {
rctx := &chi.Context{
URLParams: chi.RouteParams{
Keys: []string{"orgName"},
Values: []string{"org1"},
},
}
t.Run("invalid input", func(t *testing.T) {
testCases := []struct {
description string
repoJSON string
rmErr error
}{
{
"no repository provided",
"",
nil,
},
{
"invalid json",
"-",
nil,
},
{
"missing name",
`{"url": "https://repo1.url"}`,
hub.ErrInvalidInput,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("POST", "/", strings.NewReader(tc.repoJSON))
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
hw := newHandlersWrapper()
if tc.rmErr != nil {
hw.rm.On("Add", r.Context(), "org1", mock.Anything).Return(tc.rmErr)
}
hw.h.Add(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
}
})
t.Run("valid repository provided", func(t *testing.T) {
repoJSON := `
{
"name": "repo1",
"display_name": "Repository 1",
"url": "https://repo1.url"
}
`
repo := &hub.Repository{}
_ = json.Unmarshal([]byte(repoJSON), &repo)
testCases := []struct {
description string
err error
expectedStatusCode int
}{
{
"add repository succeeded",
nil,
http.StatusCreated,
},
{
"error adding repository (insufficient privilege)",
hub.ErrInsufficientPrivilege,
http.StatusForbidden,
},
{
"error adding repository (db error)",
tests.ErrFakeDB,
http.StatusInternalServerError,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("POST", "/", strings.NewReader(repoJSON))
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
hw := newHandlersWrapper()
hw.rm.On("Add", r.Context(), "org1", repo).Return(tc.err)
hw.h.Add(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
}
})
}
func TestBadge(t *testing.T) {
t.Run("badge info returned successfully", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
rctx := &chi.Context{
URLParams: chi.RouteParams{
Keys: []string{"repoName"},
Values: []string{"artifact-hub"},
},
}
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
hw := newHandlersWrapper()
hw.h.Badge(w, r)
resp := w.Result()
defer resp.Body.Close()
h := resp.Header
data, _ := io.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, "application/json", h.Get("Content-Type"))
assert.Equal(t, helpers.BuildCacheControlHeader(helpers.DefaultAPICacheMaxAge), h.Get("Cache-Control"))
assert.Equal(t, []byte(`{"color":"2D4857","label":"Artifact Hub","labelColor":"417598","logoSvg":"\u003csvg xmlns=\"http://www.w3.org/2000/svg\" width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#ffffff\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class=\"feather feather-hexagon\"\u003e\u003cpath d=\"M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z\"\u003e\u003c/path\u003e\u003c/svg\u003e","logoWidth":18,"message":"artifact-hub","schemaVersion":1,"style":"flat"}`), data)
hw.rm.AssertExpectations(t)
})
}
func TestCheckAvailability(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("HEAD", "/?v=value", nil)
rctx := &chi.Context{
URLParams: chi.RouteParams{
Keys: []string{"resourceKind"},
Values: []string{"invalid"},
},
}
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
hw := newHandlersWrapper()
hw.rm.On("CheckAvailability", r.Context(), "invalid", "value").Return(false, hub.ErrInvalidInput)
hw.h.CheckAvailability(w, r)
resp := w.Result()
defer resp.Body.Close()
h := resp.Header
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
assert.Equal(t, helpers.BuildCacheControlHeader(0), h.Get("Cache-Control"))
hw.rm.AssertExpectations(t)
})
t.Run("valid input", func(t *testing.T) {
t.Run("check availability succeeded", func(t *testing.T) {
testCases := []struct {
resourceKind string
available bool
}{
{
"repositoryName",
true,
},
{
"repositoryURL",
false,
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("resource kind: %s", tc.resourceKind), func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("HEAD", "/?v=value", nil)
rctx := &chi.Context{
URLParams: chi.RouteParams{
Keys: []string{"resourceKind"},
Values: []string{tc.resourceKind},
},
}
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
hw := newHandlersWrapper()
hw.rm.On("CheckAvailability", r.Context(), tc.resourceKind, "value").
Return(tc.available, nil)
hw.h.CheckAvailability(w, r)
resp := w.Result()
defer resp.Body.Close()
h := resp.Header
if tc.available {
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
} else {
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
}
assert.Equal(t, helpers.BuildCacheControlHeader(0), h.Get("Cache-Control"))
hw.rm.AssertExpectations(t)
})
}
})
t.Run("check availability failed", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("HEAD", "/?v=value", nil)
rctx := &chi.Context{
URLParams: chi.RouteParams{
Keys: []string{"resourceKind"},
Values: []string{"repositoryName"},
},
}
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
hw := newHandlersWrapper()
hw.rm.On("CheckAvailability", r.Context(), "repositoryName", "value").
Return(false, tests.ErrFakeDB)
hw.h.CheckAvailability(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
})
}
func TestClaimOwnership(t *testing.T) {
t.Run("invalid input - missing repo name", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("PUT", "/", nil)
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
hw := newHandlersWrapper()
hw.rm.On("ClaimOwnership", r.Context(), "", "").Return(hub.ErrInvalidInput)
hw.h.ClaimOwnership(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
t.Run("valid input", func(t *testing.T) {
testCases := []struct {
description string
err error
expectedStatusCode int
}{
{
"repository ownership claim succeeded",
nil,
http.StatusNoContent,
},
{
"error claiming repository ownership (insufficient privilege)",
hub.ErrInsufficientPrivilege,
http.StatusForbidden,
},
{
"error claiming repository ownership (db error)",
tests.ErrFakeDB,
http.StatusInternalServerError,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("PUT", "/?org=org1", nil)
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
rctx := &chi.Context{
URLParams: chi.RouteParams{
Keys: []string{"repoName"},
Values: []string{"repo1"},
},
}
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
hw := newHandlersWrapper()
hw.rm.On("ClaimOwnership", r.Context(), "repo1", "org1").Return(tc.err)
hw.h.ClaimOwnership(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
}
})
}
func TestDelete(t *testing.T) {
rctx := &chi.Context{
URLParams: chi.RouteParams{
Keys: []string{"repoName"},
Values: []string{"repo1"},
},
}
t.Run("delete repository succeeded", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("DELETE", "/", nil)
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
hw := newHandlersWrapper()
hw.rm.On("Delete", r.Context(), "repo1").Return(nil)
hw.h.Delete(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusNoContent, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
t.Run("error deleting repository", func(t *testing.T) {
testCases := []struct {
rmErr error
expectedStatusCode int
}{
{
hub.ErrInvalidInput,
http.StatusBadRequest,
},
{
hub.ErrInsufficientPrivilege,
http.StatusForbidden,
},
{
tests.ErrFakeDB,
http.StatusInternalServerError,
},
}
for _, tc := range testCases {
t.Run(tc.rmErr.Error(), func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("DELETE", "/", nil)
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
hw := newHandlersWrapper()
hw.rm.On("Delete", r.Context(), "repo1").Return(tc.rmErr)
hw.h.Delete(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
}
})
}
func TestSearch(t *testing.T) {
t.Run("invalid request params", func(t *testing.T) {
testCases := []struct {
desc string
params string
}{
{"invalid limit", "limit=z"},
{"invalid limit", "limit=100"},
{"invalid offset", "offset=z"},
{"invalid kind", "kind=z"},
{"invalid kind (one of them)", "kind=0&kind=z"},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s: %s", tc.desc, tc.params), func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/?"+tc.params, nil)
hw := newHandlersWrapper()
hw.h.Search(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
}
})
t.Run("invalid search input", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
hw := newHandlersWrapper()
hw.rm.On("SearchJSON", r.Context(), mock.Anything).Return(nil, hub.ErrInvalidInput)
hw.h.Search(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
t.Run("valid authenticated request, search succeeded", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
v := url.Values{}
v.Set("name", "n1")
v.Add("user", "u1")
v.Add("user", "u2")
v.Add("org", "o1")
v.Add("org", "o2")
v.Add("kind", "0")
v.Add("kind", "3")
v.Set("limit", "10")
v.Set("offset", "10")
r, _ := http.NewRequest("GET", "/?"+v.Encode(), nil)
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
hw := newHandlersWrapper()
hw.rm.On("SearchJSON", r.Context(), &hub.SearchRepositoryInput{
Name: "n1",
Kinds: []hub.RepositoryKind{hub.Helm, hub.OLM},
Orgs: []string{"o1", "o2"},
Users: []string{"u1", "u2"},
IncludeCredentials: false,
Limit: 10,
Offset: 10,
}).Return(&hub.JSONQueryResult{
Data: []byte("dataJSON"),
TotalCount: 1,
}, nil)
hw.h.Search(w, r)
resp := w.Result()
defer resp.Body.Close()
h := resp.Header
data, _ := io.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, h.Get(helpers.PaginationTotalCount), "1")
assert.Equal(t, helpers.BuildCacheControlHeader(0), h.Get("Cache-Control"))
assert.Equal(t, "application/json", h.Get("Content-Type"))
assert.Equal(t, []byte("dataJSON"), data)
hw.rm.AssertExpectations(t)
})
t.Run("valid unauthenticated request, search succeeded", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
hw := newHandlersWrapper()
hw.rm.On("SearchJSON", r.Context(), &hub.SearchRepositoryInput{
Kinds: []hub.RepositoryKind{},
Limit: 20,
Offset: 0,
}).Return(&hub.JSONQueryResult{
Data: []byte("dataJSON"),
TotalCount: 1,
}, nil)
hw.h.Search(w, r)
resp := w.Result()
defer resp.Body.Close()
h := resp.Header
data, _ := io.ReadAll(resp.Body)
assert.Equal(t, http.StatusOK, resp.StatusCode)
assert.Equal(t, h.Get(helpers.PaginationTotalCount), "1")
assert.Equal(t, helpers.BuildCacheControlHeader(1*time.Hour), h.Get("Cache-Control"))
assert.Equal(t, "application/json", h.Get("Content-Type"))
assert.Equal(t, []byte("dataJSON"), data)
hw.rm.AssertExpectations(t)
})
t.Run("error searching repositories", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("GET", "/", nil)
hw := newHandlersWrapper()
hw.rm.On("SearchJSON", r.Context(), mock.Anything).Return(nil, tests.ErrFakeDB)
hw.h.Search(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
}
func TestTransfer(t *testing.T) {
t.Run("invalid input - missing repo name", func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("PUT", "/", nil)
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
hw := newHandlersWrapper()
hw.rm.On("Transfer", r.Context(), "", "", false).Return(hub.ErrInvalidInput)
hw.h.Transfer(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
t.Run("valid input", func(t *testing.T) {
testCases := []struct {
description string
err error
expectedStatusCode int
}{
{
"repository transfer succeeded",
nil,
http.StatusNoContent,
},
{
"error transferring repository (insufficient privilege)",
hub.ErrInsufficientPrivilege,
http.StatusForbidden,
},
{
"error transferring repository (db error)",
tests.ErrFakeDB,
http.StatusInternalServerError,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("PUT", "/?org=org1", nil)
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
rctx := &chi.Context{
URLParams: chi.RouteParams{
Keys: []string{"repoName"},
Values: []string{"repo1"},
},
}
r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, rctx))
hw := newHandlersWrapper()
hw.rm.On("Transfer", r.Context(), "repo1", "org1", false).Return(tc.err)
hw.h.Transfer(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
}
})
}
func TestUpdate(t *testing.T) {
t.Run("invalid input", func(t *testing.T) {
testCases := []struct {
description string
repoJSON string
rmErr error
}{
{
"no repository provided",
"",
nil,
},
{
"invalid json",
"-",
nil,
},
{
"missing name",
`{"url": "https://repo1.url"}`,
hub.ErrInvalidInput,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("PUT", "/", strings.NewReader(tc.repoJSON))
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
hw := newHandlersWrapper()
if tc.rmErr != nil {
hw.rm.On("Update", r.Context(), mock.Anything).Return(tc.rmErr)
}
hw.h.Update(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
}
})
t.Run("valid repository provided", func(t *testing.T) {
repoJSON := `
{
"display_name": "Repository 1 updated",
"url": "https://repo1.url/updated"
}
`
repo := &hub.Repository{}
_ = json.Unmarshal([]byte(repoJSON), &repo)
testCases := []struct {
description string
err error
expectedStatusCode int
}{
{
"repository update succeeded",
nil,
http.StatusNoContent,
},
{
"error updating repository (insufficient privilege)",
hub.ErrInsufficientPrivilege,
http.StatusForbidden,
},
{
"error updating repository (db error)",
tests.ErrFakeDB,
http.StatusInternalServerError,
},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
t.Parallel()
w := httptest.NewRecorder()
r, _ := http.NewRequest("PUT", "/", strings.NewReader(repoJSON))
r = r.WithContext(context.WithValue(r.Context(), hub.UserIDKey, "userID"))
hw := newHandlersWrapper()
hw.rm.On("Update", r.Context(), repo).Return(tc.err)
hw.h.Update(w, r)
resp := w.Result()
defer resp.Body.Close()
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
hw.rm.AssertExpectations(t)
})
}
})
}
type handlersWrapper struct {
cfg *viper.Viper
rm *repo.ManagerMock
h *Handlers
}
func newHandlersWrapper() *handlersWrapper {
cfg := viper.New()
cfg.Set("theme.colors.primary", "#417598")
cfg.Set("theme.colors.secondary", "#2D4857")
cfg.Set("theme.siteName", "Artifact Hub")
rm := &repo.ManagerMock{}
return &handlersWrapper{
cfg: cfg,
rm: rm,
h: NewHandlers(cfg, rm),
}
}