gogs/internal/route/lfs/route_test.go

396 lines
11 KiB
Go

// Copyright 2020 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package lfs
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"gopkg.in/macaron.v1"
"gogs.io/gogs/internal/auth"
"gogs.io/gogs/internal/db"
"gogs.io/gogs/internal/lfsutil"
)
func Test_authenticate(t *testing.T) {
m := macaron.New()
m.Use(macaron.Renderer())
m.Get("/", authenticate(), func(w http.ResponseWriter, user *db.User) {
fmt.Fprintf(w, "ID: %d, Name: %s", user.ID, user.Name)
})
tests := []struct {
name string
header http.Header
mockUsersStore func() db.UsersStore
mockTwoFactorsStore func() db.TwoFactorsStore
mockAccessTokensStore func() db.AccessTokensStore
expStatusCode int
expHeader http.Header
expBody string
}{
{
name: "no authorization",
expStatusCode: http.StatusUnauthorized,
expHeader: http.Header{
"Lfs-Authenticate": []string{`Basic realm="Git LFS"`},
"Content-Type": []string{"application/vnd.git-lfs+json"},
},
expBody: `{"message":"Credentials needed"}` + "\n",
},
{
name: "user has 2FA enabled",
header: http.Header{
"Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
},
mockUsersStore: func() db.UsersStore {
mock := db.NewMockUsersStore()
mock.AuthenticateFunc.SetDefaultReturn(&db.User{}, nil)
return mock
},
mockTwoFactorsStore: func() db.TwoFactorsStore {
mock := db.NewMockTwoFactorsStore()
mock.IsUserEnabledFunc.SetDefaultReturn(true)
return mock
},
expStatusCode: http.StatusBadRequest,
expHeader: http.Header{},
expBody: "Users with 2FA enabled are not allowed to authenticate via username and password.",
},
{
name: "both user and access token do not exist",
header: http.Header{
"Authorization": []string{"Basic dXNlcm5hbWU="},
},
mockUsersStore: func() db.UsersStore {
mock := db.NewMockUsersStore()
mock.AuthenticateFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
return mock
},
mockAccessTokensStore: func() db.AccessTokensStore {
mock := db.NewMockAccessTokensStore()
mock.GetBySHA1Func.SetDefaultReturn(nil, db.ErrAccessTokenNotExist{})
return mock
},
expStatusCode: http.StatusUnauthorized,
expHeader: http.Header{
"Lfs-Authenticate": []string{`Basic realm="Git LFS"`},
"Content-Type": []string{"application/vnd.git-lfs+json"},
},
expBody: `{"message":"Credentials needed"}` + "\n",
},
{
name: "authenticated by username and password",
header: http.Header{
"Authorization": []string{"Basic dXNlcm5hbWU6cGFzc3dvcmQ="},
},
mockUsersStore: func() db.UsersStore {
mock := db.NewMockUsersStore()
mock.AuthenticateFunc.SetDefaultReturn(&db.User{ID: 1, Name: "unknwon"}, nil)
return mock
},
mockTwoFactorsStore: func() db.TwoFactorsStore {
mock := db.NewMockTwoFactorsStore()
mock.IsUserEnabledFunc.SetDefaultReturn(false)
return mock
},
expStatusCode: http.StatusOK,
expHeader: http.Header{},
expBody: "ID: 1, Name: unknwon",
},
{
name: "authenticate by access token",
header: http.Header{
"Authorization": []string{"Basic dXNlcm5hbWU="},
},
mockUsersStore: func() db.UsersStore {
mock := db.NewMockUsersStore()
mock.AuthenticateFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
mock.GetByIDFunc.SetDefaultReturn(&db.User{ID: 1, Name: "unknwon"}, nil)
return mock
},
mockAccessTokensStore: func() db.AccessTokensStore {
mock := db.NewMockAccessTokensStore()
mock.GetBySHA1Func.SetDefaultReturn(&db.AccessToken{}, nil)
return mock
},
expStatusCode: http.StatusOK,
expHeader: http.Header{},
expBody: "ID: 1, Name: unknwon",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.mockUsersStore != nil {
db.SetMockUsersStore(t, test.mockUsersStore())
}
if test.mockTwoFactorsStore != nil {
db.SetMockTwoFactorsStore(t, test.mockTwoFactorsStore())
}
if test.mockAccessTokensStore != nil {
db.SetMockAccessTokensStore(t, test.mockAccessTokensStore())
}
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
r.Header = test.header
rr := httptest.NewRecorder()
m.ServeHTTP(rr, r)
resp := rr.Result()
assert.Equal(t, test.expStatusCode, resp.StatusCode)
assert.Equal(t, test.expHeader, resp.Header)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.expBody, string(body))
})
}
}
func Test_authorize(t *testing.T) {
tests := []struct {
name string
authroize macaron.Handler
mockUsersStore func() db.UsersStore
mockReposStore *db.MockReposStore
mockPermsStore func() db.PermsStore
expStatusCode int
expBody string
}{
{
name: "user does not exist",
authroize: authorize(db.AccessModeNone),
mockUsersStore: func() db.UsersStore {
mock := db.NewMockUsersStore()
mock.GetByUsernameFunc.SetDefaultReturn(nil, db.ErrUserNotExist{})
return mock
},
expStatusCode: http.StatusNotFound,
},
{
name: "repository does not exist",
authroize: authorize(db.AccessModeNone),
mockUsersStore: func() db.UsersStore {
mock := db.NewMockUsersStore()
mock.GetByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*db.User, error) {
return &db.User{Name: username}, nil
})
return mock
},
mockReposStore: &db.MockReposStore{
MockGetByName: func(ownerID int64, name string) (*db.Repository, error) {
return nil, db.ErrRepoNotExist{}
},
},
expStatusCode: http.StatusNotFound,
},
{
name: "actor is not authorized",
authroize: authorize(db.AccessModeWrite),
mockUsersStore: func() db.UsersStore {
mock := db.NewMockUsersStore()
mock.GetByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*db.User, error) {
return &db.User{Name: username}, nil
})
return mock
},
mockReposStore: &db.MockReposStore{
MockGetByName: func(ownerID int64, name string) (*db.Repository, error) {
return &db.Repository{Name: name}, nil
},
},
mockPermsStore: func() db.PermsStore {
mock := db.NewMockPermsStore()
mock.AuthorizeFunc.SetDefaultHook(func(ctx context.Context, userID int64, repoID int64, desired db.AccessMode, opts db.AccessModeOptions) bool {
return desired <= db.AccessModeRead
})
return mock
},
expStatusCode: http.StatusNotFound,
},
{
name: "actor is authorized",
authroize: authorize(db.AccessModeRead),
mockUsersStore: func() db.UsersStore {
mock := db.NewMockUsersStore()
mock.GetByUsernameFunc.SetDefaultHook(func(ctx context.Context, username string) (*db.User, error) {
return &db.User{Name: username}, nil
})
return mock
},
mockReposStore: &db.MockReposStore{
MockGetByName: func(ownerID int64, name string) (*db.Repository, error) {
return &db.Repository{Name: name}, nil
},
},
mockPermsStore: func() db.PermsStore {
mock := db.NewMockPermsStore()
mock.AuthorizeFunc.SetDefaultHook(func(ctx context.Context, userID int64, repoID int64, desired db.AccessMode, opts db.AccessModeOptions) bool {
return desired <= db.AccessModeRead
})
return mock
},
expStatusCode: http.StatusOK,
expBody: "owner.Name: owner, repo.Name: repo",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if test.mockUsersStore != nil {
db.SetMockUsersStore(t, test.mockUsersStore())
}
db.SetMockReposStore(t, test.mockReposStore)
if test.mockPermsStore != nil {
db.SetMockPermsStore(t, test.mockPermsStore())
}
m := macaron.New()
m.Use(macaron.Renderer())
m.Use(func(c *macaron.Context) {
c.Map(&db.User{})
})
m.Get("/:username/:reponame", test.authroize, func(w http.ResponseWriter, owner *db.User, repo *db.Repository) {
fmt.Fprintf(w, "owner.Name: %s, repo.Name: %s", owner.Name, repo.Name)
})
r, err := http.NewRequest("GET", "/owner/repo", nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
m.ServeHTTP(rr, r)
resp := rr.Result()
assert.Equal(t, test.expStatusCode, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.expBody, string(body))
})
}
}
func Test_verifyHeader(t *testing.T) {
tests := []struct {
name string
verifyHeader macaron.Handler
header http.Header
expStatusCode int
}{
{
name: "header not found",
verifyHeader: verifyHeader("Accept", contentType, http.StatusNotAcceptable),
expStatusCode: http.StatusNotAcceptable,
},
{
name: "header found",
verifyHeader: verifyHeader("Accept", "application/vnd.git-lfs+json", http.StatusNotAcceptable),
header: http.Header{
"Accept": []string{"application/vnd.git-lfs+json; charset=utf-8"},
},
expStatusCode: http.StatusOK,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
m := macaron.New()
m.Use(macaron.Renderer())
m.Get("/", test.verifyHeader)
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
r.Header = test.header
rr := httptest.NewRecorder()
m.ServeHTTP(rr, r)
resp := rr.Result()
assert.Equal(t, test.expStatusCode, resp.StatusCode)
})
}
}
func Test_verifyOID(t *testing.T) {
m := macaron.New()
m.Get("/:oid", verifyOID(), func(w http.ResponseWriter, oid lfsutil.OID) {
fmt.Fprintf(w, "oid: %s", oid)
})
tests := []struct {
name string
url string
expStatusCode int
expBody string
}{
{
name: "bad oid",
url: "/bad_oid",
expStatusCode: http.StatusBadRequest,
expBody: `{"message":"Invalid oid"}` + "\n",
},
{
name: "good oid",
url: "/ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
expStatusCode: http.StatusOK,
expBody: "oid: ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
r, err := http.NewRequest("GET", test.url, nil)
if err != nil {
t.Fatal(err)
}
rr := httptest.NewRecorder()
m.ServeHTTP(rr, r)
resp := rr.Result()
assert.Equal(t, test.expStatusCode, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, test.expBody, string(body))
})
}
}
func Test_internalServerError(t *testing.T) {
rr := httptest.NewRecorder()
internalServerError(rr)
resp := rr.Result()
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, `{"message":"Internal server error"}`+"\n", string(body))
}