Sfoglia il codice sorgente

all: unwrap `database.AccessTokensStore` interface (#7670)

Joe Chen 1 anno fa
parent
commit
8054ffc12f

+ 5 - 5
.github/workflows/go.yml

@@ -62,7 +62,7 @@ jobs:
     name: Test
     strategy:
       matrix:
-        go-version: [ 1.21.x, 1.22.x ]
+        go-version: [ 1.22.x ]
         platform: [ ubuntu-latest, macos-latest ]
     runs-on: ${{ matrix.platform }}
     steps:
@@ -102,7 +102,7 @@ jobs:
     name: Test Windows
     strategy:
       matrix:
-        go-version: [ 1.21.x, 1.22.x ]
+        go-version: [ 1.22.x ]
         platform: [ windows-latest ]
     runs-on: ${{ matrix.platform }}
     steps:
@@ -140,7 +140,7 @@ jobs:
     name: Postgres
     strategy:
       matrix:
-        go-version: [ 1.21.x, 1.22.x ]
+        go-version: [ 1.22.x ]
         platform: [ ubuntu-latest ]
     runs-on: ${{ matrix.platform }}
     services:
@@ -176,7 +176,7 @@ jobs:
     name: MySQL
     strategy:
       matrix:
-        go-version: [ 1.21.x, 1.22.x ]
+        go-version: [ 1.22.x ]
         platform: [ ubuntu-20.04 ]
     runs-on: ${{ matrix.platform }}
     steps:
@@ -201,7 +201,7 @@ jobs:
     name: SQLite - Go
     strategy:
       matrix:
-        go-version: [ 1.21.x, 1.22.x ]
+        go-version: [ 1.22.x ]
         platform: [ ubuntu-latest ]
     runs-on: ${{ matrix.platform }}
     steps:

+ 7 - 5
internal/cmd/web.go

@@ -237,9 +237,11 @@ func runWeb(c *cli.Context) error {
 				m.Get("", user.SettingsOrganizations)
 				m.Post("/leave", user.SettingsLeaveOrganization)
 			})
-			m.Combo("/applications").Get(user.SettingsApplications).
-				Post(bindIgnErr(form.NewAccessToken{}), user.SettingsApplicationsPost)
-			m.Post("/applications/delete", user.SettingsDeleteApplication)
+
+			settingsHandler := user.NewSettingsHandler(user.NewSettingsStore())
+			m.Combo("/applications").Get(settingsHandler.Applications()).
+				Post(bindIgnErr(form.NewAccessToken{}), settingsHandler.ApplicationsPost())
+			m.Post("/applications/delete", settingsHandler.DeleteApplication())
 			m.Route("/delete", "GET,POST", user.SettingsDelete)
 		}, reqSignIn, func(c *context.Context) {
 			c.Data["PageIsUserSettings"] = true
@@ -652,7 +654,7 @@ func runWeb(c *cli.Context) error {
 			SetCookie:      true,
 			Secure:         conf.Server.URL.Scheme == "https",
 		}),
-		context.Contexter(),
+		context.Contexter(context.NewStore()),
 	)
 
 	// ***************************
@@ -666,7 +668,7 @@ func runWeb(c *cli.Context) error {
 			lfs.RegisterRoutes(m.Router)
 		})
 
-		m.Route("/*", "GET,POST,OPTIONS", context.ServeGoGet(), repo.HTTPContexter(), repo.HTTP)
+		m.Route("/*", "GET,POST,OPTIONS", context.ServeGoGet(), repo.HTTPContexter(repo.NewStore()), repo.HTTP)
 	})
 
 	// ***************************

+ 17 - 8
internal/context/auth.go

@@ -106,9 +106,18 @@ func isAPIPath(url string) bool {
 	return strings.HasPrefix(url, "/api/")
 }
 
+type AuthStore interface {
+	// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
+	// database.ErrAccessTokenNotExist when not found.
+	GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error)
+	// TouchAccessTokenByID updates the updated time of the given access token to
+	// the current time.
+	TouchAccessTokenByID(ctx context.Context, id int64) error
+}
+
 // authenticatedUserID returns the ID of the authenticated user, along with a bool value
 // which indicates whether the user uses token authentication.
-func authenticatedUserID(c *macaron.Context, sess session.Store) (_ int64, isTokenAuth bool) {
+func authenticatedUserID(store AuthStore, c *macaron.Context, sess session.Store) (_ int64, isTokenAuth bool) {
 	if !database.HasEngine {
 		return 0, false
 	}
@@ -132,14 +141,14 @@ func authenticatedUserID(c *macaron.Context, sess session.Store) (_ int64, isTok
 
 		// Let's see if token is valid.
 		if len(tokenSHA) > 0 {
-			t, err := database.AccessTokens.GetBySHA1(c.Req.Context(), tokenSHA)
+			t, err := store.GetAccessTokenBySHA1(c.Req.Context(), tokenSHA)
 			if err != nil {
 				if !database.IsErrAccessTokenNotExist(err) {
 					log.Error("GetAccessTokenBySHA: %v", err)
 				}
 				return 0, false
 			}
-			if err = database.AccessTokens.Touch(c.Req.Context(), t.ID); err != nil {
+			if err = store.TouchAccessTokenByID(c.Req.Context(), t.ID); err != nil {
 				log.Error("Failed to touch access token: %v", err)
 			}
 			return t.UserID, true
@@ -165,12 +174,12 @@ func authenticatedUserID(c *macaron.Context, sess session.Store) (_ int64, isTok
 
 // authenticatedUser returns the user object of the authenticated user, along with two bool values
 // which indicate whether the user uses HTTP Basic Authentication or token authentication respectively.
-func authenticatedUser(ctx *macaron.Context, sess session.Store) (_ *database.User, isBasicAuth, isTokenAuth bool) {
+func authenticatedUser(store AuthStore, ctx *macaron.Context, sess session.Store) (_ *database.User, isBasicAuth, isTokenAuth bool) {
 	if !database.HasEngine {
 		return nil, false, false
 	}
 
-	uid, isTokenAuth := authenticatedUserID(ctx, sess)
+	uid, isTokenAuth := authenticatedUserID(store, ctx, sess)
 
 	if uid <= 0 {
 		if conf.Auth.EnableReverseProxyAuthentication {
@@ -235,12 +244,12 @@ func authenticatedUser(ctx *macaron.Context, sess session.Store) (_ *database.Us
 // AuthenticateByToken attempts to authenticate a user by the given access
 // token. It returns database.ErrAccessTokenNotExist when the access token does not
 // exist.
-func AuthenticateByToken(ctx context.Context, token string) (*database.User, error) {
-	t, err := database.AccessTokens.GetBySHA1(ctx, token)
+func AuthenticateByToken(store AuthStore, ctx context.Context, token string) (*database.User, error) {
+	t, err := store.GetAccessTokenBySHA1(ctx, token)
 	if err != nil {
 		return nil, errors.Wrap(err, "get access token by SHA1")
 	}
-	if err = database.AccessTokens.Touch(ctx, t.ID); err != nil {
+	if err = store.TouchAccessTokenByID(ctx, t.ID); err != nil {
 		// NOTE: There is no need to fail the auth flow if we can't touch the token.
 		log.Error("Failed to touch access token [id: %d]: %v", t.ID, err)
 	}

+ 2 - 2
internal/context/context.go

@@ -235,7 +235,7 @@ func (c *Context) ServeContent(name string, r io.ReadSeeker, params ...any) {
 var csrfTokenExcludePattern = lazyregexp.New(`[^a-zA-Z0-9-_].*`)
 
 // Contexter initializes a classic context for a request.
-func Contexter() macaron.Handler {
+func Contexter(store Store) macaron.Handler {
 	return func(ctx *macaron.Context, l i18n.Locale, cache cache.Cache, sess session.Store, f *session.Flash, x csrf.CSRF) {
 		c := &Context{
 			Context: ctx,
@@ -260,7 +260,7 @@ func Contexter() macaron.Handler {
 		}
 
 		// Get user from session or header when possible
-		c.User, c.IsBasicAuth, c.IsTokenAuth = authenticatedUser(c.Context, c.Session)
+		c.User, c.IsBasicAuth, c.IsTokenAuth = authenticatedUser(store, c.Context, c.Session)
 
 		if c.User != nil {
 			c.IsLogged = true

+ 34 - 0
internal/context/store.go

@@ -0,0 +1,34 @@
+package context
+
+import (
+	"context"
+
+	"gogs.io/gogs/internal/database"
+)
+
+// Store is the data layer carrier for context middleware. This interface is
+// meant to abstract away and limit the exposure of the underlying data layer to
+// the handler through a thin-wrapper.
+type Store interface {
+	// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
+	// database.ErrAccessTokenNotExist when not found.
+	GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error)
+	// TouchAccessTokenByID updates the updated time of the given access token to
+	// the current time.
+	TouchAccessTokenByID(ctx context.Context, id int64) error
+}
+
+type store struct{}
+
+// NewStore returns a new Store using the global database handle.
+func NewStore() Store {
+	return &store{}
+}
+
+func (*store) GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error) {
+	return database.Handle.AccessTokens().GetBySHA1(ctx, sha1)
+}
+
+func (*store) TouchAccessTokenByID(ctx context.Context, id int64) error {
+	return database.Handle.AccessTokens().Touch(ctx, id)
+}

+ 35 - 46
internal/database/access_tokens.go

@@ -17,28 +17,6 @@ import (
 	"gogs.io/gogs/internal/errutil"
 )
 
-// AccessTokensStore is the persistent interface for access tokens.
-type AccessTokensStore interface {
-	// Create creates a new access token and persist to database. It returns
-	// ErrAccessTokenAlreadyExist when an access token with same name already exists
-	// for the user.
-	Create(ctx context.Context, userID int64, name string) (*AccessToken, error)
-	// DeleteByID deletes the access token by given ID.
-	//
-	// 🚨 SECURITY: The "userID" is required to prevent attacker deletes arbitrary
-	// access token that belongs to another user.
-	DeleteByID(ctx context.Context, userID, id int64) error
-	// GetBySHA1 returns the access token with given SHA1. It returns
-	// ErrAccessTokenNotExist when not found.
-	GetBySHA1(ctx context.Context, sha1 string) (*AccessToken, error)
-	// List returns all access tokens belongs to given user.
-	List(ctx context.Context, userID int64) ([]*AccessToken, error)
-	// Touch updates the updated time of the given access token to the current time.
-	Touch(ctx context.Context, id int64) error
-}
-
-var AccessTokens AccessTokensStore
-
 // AccessToken is a personal access token.
 type AccessToken struct {
 	ID     int64 `gorm:"primarykey"`
@@ -74,10 +52,13 @@ func (t *AccessToken) AfterFind(tx *gorm.DB) error {
 	return nil
 }
 
-var _ AccessTokensStore = (*accessTokensStore)(nil)
+// AccessTokensStore is the storage layer for access tokens.
+type AccessTokensStore struct {
+	db *gorm.DB
+}
 
-type accessTokensStore struct {
-	*gorm.DB
+func newAccessTokensStore(db *gorm.DB) *AccessTokensStore {
+	return &AccessTokensStore{db}
 }
 
 type ErrAccessTokenAlreadyExist struct {
@@ -85,19 +66,21 @@ type ErrAccessTokenAlreadyExist struct {
 }
 
 func IsErrAccessTokenAlreadyExist(err error) bool {
-	_, ok := err.(ErrAccessTokenAlreadyExist)
-	return ok
+	return errors.As(err, &ErrAccessTokenAlreadyExist{})
 }
 
 func (err ErrAccessTokenAlreadyExist) Error() string {
 	return fmt.Sprintf("access token already exists: %v", err.args)
 }
 
-func (s *accessTokensStore) Create(ctx context.Context, userID int64, name string) (*AccessToken, error) {
-	err := s.WithContext(ctx).Where("uid = ? AND name = ?", userID, name).First(new(AccessToken)).Error
+// Create creates a new access token and persist to database. It returns
+// ErrAccessTokenAlreadyExist when an access token with same name already exists
+// for the user.
+func (s *AccessTokensStore) Create(ctx context.Context, userID int64, name string) (*AccessToken, error) {
+	err := s.db.WithContext(ctx).Where("uid = ? AND name = ?", userID, name).First(new(AccessToken)).Error
 	if err == nil {
 		return nil, ErrAccessTokenAlreadyExist{args: errutil.Args{"userID": userID, "name": name}}
-	} else if err != gorm.ErrRecordNotFound {
+	} else if !errors.Is(err, gorm.ErrRecordNotFound) {
 		return nil, err
 	}
 
@@ -110,7 +93,7 @@ func (s *accessTokensStore) Create(ctx context.Context, userID int64, name strin
 		Sha1:   sha256[:40], // To pass the column unique constraint, keep the length of SHA1.
 		SHA256: sha256,
 	}
-	if err = s.WithContext(ctx).Create(accessToken).Error; err != nil {
+	if err = s.db.WithContext(ctx).Create(accessToken).Error; err != nil {
 		return nil, err
 	}
 
@@ -119,8 +102,12 @@ func (s *accessTokensStore) Create(ctx context.Context, userID int64, name strin
 	return accessToken, nil
 }
 
-func (s *accessTokensStore) DeleteByID(ctx context.Context, userID, id int64) error {
-	return s.WithContext(ctx).Where("id = ? AND uid = ?", id, userID).Delete(new(AccessToken)).Error
+// DeleteByID deletes the access token by given ID.
+//
+// 🚨 SECURITY: The "userID" is required to prevent attacker deletes arbitrary
+// access token that belongs to another user.
+func (s *AccessTokensStore) DeleteByID(ctx context.Context, userID, id int64) error {
+	return s.db.WithContext(ctx).Where("id = ? AND uid = ?", id, userID).Delete(new(AccessToken)).Error
 }
 
 var _ errutil.NotFound = (*ErrAccessTokenNotExist)(nil)
@@ -132,8 +119,7 @@ type ErrAccessTokenNotExist struct {
 // IsErrAccessTokenNotExist returns true if the underlying error has the type
 // ErrAccessTokenNotExist.
 func IsErrAccessTokenNotExist(err error) bool {
-	_, ok := errors.Cause(err).(ErrAccessTokenNotExist)
-	return ok
+	return errors.As(errors.Cause(err), &ErrAccessTokenNotExist{})
 }
 
 func (err ErrAccessTokenNotExist) Error() string {
@@ -144,7 +130,9 @@ func (ErrAccessTokenNotExist) NotFound() bool {
 	return true
 }
 
-func (s *accessTokensStore) GetBySHA1(ctx context.Context, sha1 string) (*AccessToken, error) {
+// GetBySHA1 returns the access token with given SHA1. It returns
+// ErrAccessTokenNotExist when not found.
+func (s *AccessTokensStore) GetBySHA1(ctx context.Context, sha1 string) (*AccessToken, error) {
 	// No need to waste a query for an empty SHA1.
 	if sha1 == "" {
 		return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}}
@@ -152,25 +140,26 @@ func (s *accessTokensStore) GetBySHA1(ctx context.Context, sha1 string) (*Access
 
 	sha256 := cryptoutil.SHA256(sha1)
 	token := new(AccessToken)
-	err := s.WithContext(ctx).Where("sha256 = ?", sha256).First(token).Error
-	if err != nil {
-		if err == gorm.ErrRecordNotFound {
-			return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}}
-		}
+	err := s.db.WithContext(ctx).Where("sha256 = ?", sha256).First(token).Error
+	if errors.Is(err, gorm.ErrRecordNotFound) {
+		return nil, ErrAccessTokenNotExist{args: errutil.Args{"sha": sha1}}
+	} else if err != nil {
 		return nil, err
 	}
 	return token, nil
 }
 
-func (s *accessTokensStore) List(ctx context.Context, userID int64) ([]*AccessToken, error) {
+// List returns all access tokens belongs to given user.
+func (s *AccessTokensStore) List(ctx context.Context, userID int64) ([]*AccessToken, error) {
 	var tokens []*AccessToken
-	return tokens, s.WithContext(ctx).Where("uid = ?", userID).Order("id ASC").Find(&tokens).Error
+	return tokens, s.db.WithContext(ctx).Where("uid = ?", userID).Order("id ASC").Find(&tokens).Error
 }
 
-func (s *accessTokensStore) Touch(ctx context.Context, id int64) error {
-	return s.WithContext(ctx).
+// Touch updates the updated time of the given access token to the current time.
+func (s *AccessTokensStore) Touch(ctx context.Context, id int64) error {
+	return s.db.WithContext(ctx).
 		Model(new(AccessToken)).
 		Where("id = ?", id).
-		UpdateColumn("updated_unix", s.NowFunc().Unix()).
+		UpdateColumn("updated_unix", s.db.NowFunc().Unix()).
 		Error
 }

+ 31 - 31
internal/database/access_tokens_test.go

@@ -98,13 +98,13 @@ func TestAccessTokens(t *testing.T) {
 	t.Parallel()
 
 	ctx := context.Background()
-	db := &accessTokensStore{
-		DB: newTestDB(t, "accessTokensStore"),
+	s := &AccessTokensStore{
+		db: newTestDB(t, "AccessTokensStore"),
 	}
 
 	for _, tc := range []struct {
 		name string
-		test func(t *testing.T, ctx context.Context, db *accessTokensStore)
+		test func(t *testing.T, ctx context.Context, s *AccessTokensStore)
 	}{
 		{"Create", accessTokensCreate},
 		{"DeleteByID", accessTokensDeleteByID},
@@ -114,10 +114,10 @@ func TestAccessTokens(t *testing.T) {
 	} {
 		t.Run(tc.name, func(t *testing.T) {
 			t.Cleanup(func() {
-				err := clearTables(t, db.DB)
+				err := clearTables(t, s.db)
 				require.NoError(t, err)
 			})
-			tc.test(t, ctx, db)
+			tc.test(t, ctx, s)
 		})
 		if t.Failed() {
 			break
@@ -125,9 +125,9 @@ func TestAccessTokens(t *testing.T) {
 	}
 }
 
-func accessTokensCreate(t *testing.T, ctx context.Context, db *accessTokensStore) {
+func accessTokensCreate(t *testing.T, ctx context.Context, s *AccessTokensStore) {
 	// Create first access token with name "Test"
-	token, err := db.Create(ctx, 1, "Test")
+	token, err := s.Create(ctx, 1, "Test")
 	require.NoError(t, err)
 
 	assert.Equal(t, int64(1), token.UserID)
@@ -135,12 +135,12 @@ func accessTokensCreate(t *testing.T, ctx context.Context, db *accessTokensStore
 	assert.Equal(t, 40, len(token.Sha1), "sha1 length")
 
 	// Get it back and check the Created field
-	token, err = db.GetBySHA1(ctx, token.Sha1)
+	token, err = s.GetBySHA1(ctx, token.Sha1)
 	require.NoError(t, err)
-	assert.Equal(t, db.NowFunc().Format(time.RFC3339), token.Created.UTC().Format(time.RFC3339))
+	assert.Equal(t, s.db.NowFunc().Format(time.RFC3339), token.Created.UTC().Format(time.RFC3339))
 
 	// Try create second access token with same name should fail
-	_, err = db.Create(ctx, token.UserID, token.Name)
+	_, err = s.Create(ctx, token.UserID, token.Name)
 	wantErr := ErrAccessTokenAlreadyExist{
 		args: errutil.Args{
 			"userID": token.UserID,
@@ -150,25 +150,25 @@ func accessTokensCreate(t *testing.T, ctx context.Context, db *accessTokensStore
 	assert.Equal(t, wantErr, err)
 }
 
-func accessTokensDeleteByID(t *testing.T, ctx context.Context, db *accessTokensStore) {
+func accessTokensDeleteByID(t *testing.T, ctx context.Context, s *AccessTokensStore) {
 	// Create an access token with name "Test"
-	token, err := db.Create(ctx, 1, "Test")
+	token, err := s.Create(ctx, 1, "Test")
 	require.NoError(t, err)
 
 	// Delete a token with mismatched user ID is noop
-	err = db.DeleteByID(ctx, 2, token.ID)
+	err = s.DeleteByID(ctx, 2, token.ID)
 	require.NoError(t, err)
 
 	// We should be able to get it back
-	_, err = db.GetBySHA1(ctx, token.Sha1)
+	_, err = s.GetBySHA1(ctx, token.Sha1)
 	require.NoError(t, err)
 
 	// Now delete this token with correct user ID
-	err = db.DeleteByID(ctx, token.UserID, token.ID)
+	err = s.DeleteByID(ctx, token.UserID, token.ID)
 	require.NoError(t, err)
 
 	// We should get token not found error
-	_, err = db.GetBySHA1(ctx, token.Sha1)
+	_, err = s.GetBySHA1(ctx, token.Sha1)
 	wantErr := ErrAccessTokenNotExist{
 		args: errutil.Args{
 			"sha": token.Sha1,
@@ -177,17 +177,17 @@ func accessTokensDeleteByID(t *testing.T, ctx context.Context, db *accessTokensS
 	assert.Equal(t, wantErr, err)
 }
 
-func accessTokensGetBySHA(t *testing.T, ctx context.Context, db *accessTokensStore) {
+func accessTokensGetBySHA(t *testing.T, ctx context.Context, s *AccessTokensStore) {
 	// Create an access token with name "Test"
-	token, err := db.Create(ctx, 1, "Test")
+	token, err := s.Create(ctx, 1, "Test")
 	require.NoError(t, err)
 
 	// We should be able to get it back
-	_, err = db.GetBySHA1(ctx, token.Sha1)
+	_, err = s.GetBySHA1(ctx, token.Sha1)
 	require.NoError(t, err)
 
 	// Try to get a non-existent token
-	_, err = db.GetBySHA1(ctx, "bad_sha")
+	_, err = s.GetBySHA1(ctx, "bad_sha")
 	wantErr := ErrAccessTokenNotExist{
 		args: errutil.Args{
 			"sha": "bad_sha",
@@ -196,21 +196,21 @@ func accessTokensGetBySHA(t *testing.T, ctx context.Context, db *accessTokensSto
 	assert.Equal(t, wantErr, err)
 }
 
-func accessTokensList(t *testing.T, ctx context.Context, db *accessTokensStore) {
+func accessTokensList(t *testing.T, ctx context.Context, s *AccessTokensStore) {
 	// Create two access tokens for user 1
-	_, err := db.Create(ctx, 1, "user1_1")
+	_, err := s.Create(ctx, 1, "user1_1")
 	require.NoError(t, err)
-	_, err = db.Create(ctx, 1, "user1_2")
+	_, err = s.Create(ctx, 1, "user1_2")
 	require.NoError(t, err)
 
 	// Create one access token for user 2
-	_, err = db.Create(ctx, 2, "user2_1")
+	_, err = s.Create(ctx, 2, "user2_1")
 	require.NoError(t, err)
 
 	// List all access tokens for user 1
-	tokens, err := db.List(ctx, 1)
+	tokens, err := s.List(ctx, 1)
 	require.NoError(t, err)
-	assert.Equal(t, 2, len(tokens), "number of tokens")
+	require.Equal(t, 2, len(tokens), "number of tokens")
 
 	assert.Equal(t, int64(1), tokens[0].UserID)
 	assert.Equal(t, "user1_1", tokens[0].Name)
@@ -219,19 +219,19 @@ func accessTokensList(t *testing.T, ctx context.Context, db *accessTokensStore)
 	assert.Equal(t, "user1_2", tokens[1].Name)
 }
 
-func accessTokensTouch(t *testing.T, ctx context.Context, db *accessTokensStore) {
+func accessTokensTouch(t *testing.T, ctx context.Context, s *AccessTokensStore) {
 	// Create an access token with name "Test"
-	token, err := db.Create(ctx, 1, "Test")
+	token, err := s.Create(ctx, 1, "Test")
 	require.NoError(t, err)
 
 	// Updated field is zero now
 	assert.True(t, token.Updated.IsZero())
 
-	err = db.Touch(ctx, token.ID)
+	err = s.Touch(ctx, token.ID)
 	require.NoError(t, err)
 
 	// Get back from DB should have Updated set
-	token, err = db.GetBySHA1(ctx, token.Sha1)
+	token, err = s.GetBySHA1(ctx, token.Sha1)
 	require.NoError(t, err)
-	assert.Equal(t, db.NowFunc().Format(time.RFC3339), token.Updated.UTC().Format(time.RFC3339))
+	assert.Equal(t, s.db.NowFunc().Format(time.RFC3339), token.Updated.UTC().Format(time.RFC3339))
 }

+ 27 - 3
internal/database/database.go

@@ -50,8 +50,8 @@ var Tables = []any{
 	new(Notice),
 }
 
-// Init initializes the database with given logger.
-func Init(w logger.Writer) (*gorm.DB, error) {
+// NewConnection returns a new database connection with the given logger.
+func NewConnection(w logger.Writer) (*gorm.DB, error) {
 	level := logger.Info
 	if conf.IsProdMode() {
 		level = logger.Warn
@@ -123,7 +123,6 @@ func Init(w logger.Writer) (*gorm.DB, error) {
 	}
 
 	// Initialize stores, sorted in alphabetical order.
-	AccessTokens = &accessTokensStore{DB: db}
 	Actions = NewActionsStore(db)
 	LoginSources = &loginSourcesStore{DB: db, files: sourceFiles}
 	LFS = &lfsStore{DB: db}
@@ -136,3 +135,28 @@ func Init(w logger.Writer) (*gorm.DB, error) {
 
 	return db, nil
 }
+
+type DB struct {
+	db *gorm.DB
+}
+
+// Handle is the global database handle. It could be `nil` during the
+// installation mode.
+//
+// NOTE: Because we need to register all the routes even during the installation
+// mode (which initially has no database configuration), we have to use a global
+// variable since we can't pass a database handler around before it's available.
+//
+// NOTE: It is not guarded by a mutex because it is only written once either
+// during the service start or during the installation process (which is a
+// single-thread process).
+var Handle *DB
+
+// SetHandle updates the global database handle with the given connection.
+func SetHandle(db *gorm.DB) {
+	Handle = &DB{db: db}
+}
+
+func (db *DB) AccessTokens() *AccessTokensStore {
+	return newAccessTokensStore(db.db)
+}

+ 0 - 8
internal/database/mocks.go

@@ -8,14 +8,6 @@ import (
 	"testing"
 )
 
-func SetMockAccessTokensStore(t *testing.T, mock AccessTokensStore) {
-	before := AccessTokens
-	AccessTokens = mock
-	t.Cleanup(func() {
-		AccessTokens = before
-	})
-}
-
 func SetMockLFSStore(t *testing.T, mock LFSStore) {
 	before := LFS
 	LFS = mock

+ 6 - 6
internal/database/models.go

@@ -178,24 +178,24 @@ func SetEngine() (*gorm.DB, error) {
 			return nil, errors.Wrap(err, "new log writer")
 		}
 	}
-	return Init(gormLogger)
+	return NewConnection(gormLogger)
 }
 
-func NewEngine() (err error) {
+func NewEngine() (*gorm.DB, error) {
 	db, err := SetEngine()
 	if err != nil {
-		return err
+		return nil, err
 	}
 
 	if err = migrations.Migrate(db); err != nil {
-		return fmt.Errorf("migrate: %v", err)
+		return nil, fmt.Errorf("migrate: %v", err)
 	}
 
 	if err = x.StoreEngine("InnoDB").Sync2(legacyTables...); err != nil {
-		return errors.Wrap(err, "sync tables")
+		return nil, errors.Wrap(err, "sync tables")
 	}
 
-	return nil
+	return db, nil
 }
 
 type Statistic struct {

+ 3 - 2
internal/route/api/v1/api.go

@@ -186,9 +186,10 @@ func RegisterRoutes(m *macaron.Macaron) {
 				m.Get("", user.GetInfo)
 
 				m.Group("/tokens", func() {
+					accessTokensHandler := user.NewAccessTokensHandler(user.NewAccessTokensStore())
 					m.Combo("").
-						Get(user.ListAccessTokens).
-						Post(bind(api.CreateAccessTokenOption{}), user.CreateAccessToken)
+						Get(accessTokensHandler.List()).
+						Post(bind(api.CreateAccessTokenOption{}), accessTokensHandler.Create())
 				}, reqBasicAuth())
 			})
 		})

+ 88 - 0
internal/route/api/v1/user/access_tokens.go

@@ -0,0 +1,88 @@
+// Copyright 2014 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 user
+
+import (
+	gocontext "context"
+	"net/http"
+
+	api "github.com/gogs/go-gogs-client"
+	"gopkg.in/macaron.v1"
+
+	"gogs.io/gogs/internal/context"
+	"gogs.io/gogs/internal/database"
+)
+
+// AccessTokensHandler is the handler for users access tokens API endpoints.
+type AccessTokensHandler struct {
+	store AccessTokensStore
+}
+
+// NewAccessTokensHandler returns a new AccessTokensHandler for users access
+// tokens API endpoints.
+func NewAccessTokensHandler(s AccessTokensStore) *AccessTokensHandler {
+	return &AccessTokensHandler{
+		store: s,
+	}
+}
+
+func (h *AccessTokensHandler) List() macaron.Handler {
+	return func(c *context.APIContext) {
+		tokens, err := h.store.ListAccessTokens(c.Req.Context(), c.User.ID)
+		if err != nil {
+			c.Error(err, "list access tokens")
+			return
+		}
+
+		apiTokens := make([]*api.AccessToken, len(tokens))
+		for i := range tokens {
+			apiTokens[i] = &api.AccessToken{Name: tokens[i].Name, Sha1: tokens[i].Sha1}
+		}
+		c.JSONSuccess(&apiTokens)
+	}
+}
+
+func (h *AccessTokensHandler) Create() macaron.Handler {
+	return func(c *context.APIContext, form api.CreateAccessTokenOption) {
+		t, err := h.store.CreateAccessToken(c.Req.Context(), c.User.ID, form.Name)
+		if err != nil {
+			if database.IsErrAccessTokenAlreadyExist(err) {
+				c.ErrorStatus(http.StatusUnprocessableEntity, err)
+			} else {
+				c.Error(err, "new access token")
+			}
+			return
+		}
+		c.JSON(http.StatusCreated, &api.AccessToken{Name: t.Name, Sha1: t.Sha1})
+	}
+}
+
+// AccessTokensStore is the data layer carrier for user access tokens API
+// endpoints. This interface is meant to abstract away and limit the exposure of
+// the underlying data layer to the handler through a thin-wrapper.
+type AccessTokensStore interface {
+	// CreateAccessToken creates a new access token and persist to database. It
+	// returns database.ErrAccessTokenAlreadyExist when an access token with same
+	// name already exists for the user.
+	CreateAccessToken(ctx gocontext.Context, userID int64, name string) (*database.AccessToken, error)
+	// ListAccessTokens returns all access tokens belongs to given user.
+	ListAccessTokens(ctx gocontext.Context, userID int64) ([]*database.AccessToken, error)
+}
+
+type accessTokensStore struct{}
+
+// NewAccessTokensStore returns a new AccessTokensStore using the global
+// database handle.
+func NewAccessTokensStore() AccessTokensStore {
+	return &accessTokensStore{}
+}
+
+func (*accessTokensStore) CreateAccessToken(ctx gocontext.Context, userID int64, name string) (*database.AccessToken, error) {
+	return database.Handle.AccessTokens().Create(ctx, userID, name)
+}
+
+func (*accessTokensStore) ListAccessTokens(ctx gocontext.Context, userID int64) ([]*database.AccessToken, error) {
+	return database.Handle.AccessTokens().List(ctx, userID)
+}

+ 0 - 41
internal/route/api/v1/user/app.go

@@ -1,41 +0,0 @@
-// Copyright 2014 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 user
-
-import (
-	"net/http"
-
-	api "github.com/gogs/go-gogs-client"
-
-	"gogs.io/gogs/internal/context"
-	"gogs.io/gogs/internal/database"
-)
-
-func ListAccessTokens(c *context.APIContext) {
-	tokens, err := database.AccessTokens.List(c.Req.Context(), c.User.ID)
-	if err != nil {
-		c.Error(err, "list access tokens")
-		return
-	}
-
-	apiTokens := make([]*api.AccessToken, len(tokens))
-	for i := range tokens {
-		apiTokens[i] = &api.AccessToken{Name: tokens[i].Name, Sha1: tokens[i].Sha1}
-	}
-	c.JSONSuccess(&apiTokens)
-}
-
-func CreateAccessToken(c *context.APIContext, form api.CreateAccessTokenOption) {
-	t, err := database.AccessTokens.Create(c.Req.Context(), c.User.ID, form.Name)
-	if err != nil {
-		if database.IsErrAccessTokenAlreadyExist(err) {
-			c.ErrorStatus(http.StatusUnprocessableEntity, err)
-		} else {
-			c.Error(err, "new access token")
-		}
-		return
-	}
-	c.JSON(http.StatusCreated, &api.AccessToken{Name: t.Name, Sha1: t.Sha1})
-}

+ 3 - 1
internal/route/install.go

@@ -71,9 +71,11 @@ func GlobalInit(customConf string) error {
 	if conf.Security.InstallLock {
 		highlight.NewContext()
 		markup.NewSanitizer()
-		if err := database.NewEngine(); err != nil {
+		db, err := database.NewEngine()
+		if err != nil {
 			log.Fatal("Failed to initialize ORM engine: %v", err)
 		}
+		database.SetHandle(db)
 		database.HasEngine = true
 
 		database.LoadRepoConfig()

+ 271 - 651
internal/route/lfs/mocks_test.go

@@ -14,657 +14,6 @@ import (
 	lfsutil "gogs.io/gogs/internal/lfsutil"
 )
 
-// MockAccessTokensStore is a mock implementation of the AccessTokensStore
-// interface (from the package gogs.io/gogs/internal/database) used for unit
-// testing.
-type MockAccessTokensStore struct {
-	// CreateFunc is an instance of a mock function object controlling the
-	// behavior of the method Create.
-	CreateFunc *AccessTokensStoreCreateFunc
-	// DeleteByIDFunc is an instance of a mock function object controlling
-	// the behavior of the method DeleteByID.
-	DeleteByIDFunc *AccessTokensStoreDeleteByIDFunc
-	// GetBySHA1Func is an instance of a mock function object controlling
-	// the behavior of the method GetBySHA1.
-	GetBySHA1Func *AccessTokensStoreGetBySHA1Func
-	// ListFunc is an instance of a mock function object controlling the
-	// behavior of the method List.
-	ListFunc *AccessTokensStoreListFunc
-	// TouchFunc is an instance of a mock function object controlling the
-	// behavior of the method Touch.
-	TouchFunc *AccessTokensStoreTouchFunc
-}
-
-// NewMockAccessTokensStore creates a new mock of the AccessTokensStore
-// interface. All methods return zero values for all results, unless
-// overwritten.
-func NewMockAccessTokensStore() *MockAccessTokensStore {
-	return &MockAccessTokensStore{
-		CreateFunc: &AccessTokensStoreCreateFunc{
-			defaultHook: func(context.Context, int64, string) (r0 *database.AccessToken, r1 error) {
-				return
-			},
-		},
-		DeleteByIDFunc: &AccessTokensStoreDeleteByIDFunc{
-			defaultHook: func(context.Context, int64, int64) (r0 error) {
-				return
-			},
-		},
-		GetBySHA1Func: &AccessTokensStoreGetBySHA1Func{
-			defaultHook: func(context.Context, string) (r0 *database.AccessToken, r1 error) {
-				return
-			},
-		},
-		ListFunc: &AccessTokensStoreListFunc{
-			defaultHook: func(context.Context, int64) (r0 []*database.AccessToken, r1 error) {
-				return
-			},
-		},
-		TouchFunc: &AccessTokensStoreTouchFunc{
-			defaultHook: func(context.Context, int64) (r0 error) {
-				return
-			},
-		},
-	}
-}
-
-// NewStrictMockAccessTokensStore creates a new mock of the
-// AccessTokensStore interface. All methods panic on invocation, unless
-// overwritten.
-func NewStrictMockAccessTokensStore() *MockAccessTokensStore {
-	return &MockAccessTokensStore{
-		CreateFunc: &AccessTokensStoreCreateFunc{
-			defaultHook: func(context.Context, int64, string) (*database.AccessToken, error) {
-				panic("unexpected invocation of MockAccessTokensStore.Create")
-			},
-		},
-		DeleteByIDFunc: &AccessTokensStoreDeleteByIDFunc{
-			defaultHook: func(context.Context, int64, int64) error {
-				panic("unexpected invocation of MockAccessTokensStore.DeleteByID")
-			},
-		},
-		GetBySHA1Func: &AccessTokensStoreGetBySHA1Func{
-			defaultHook: func(context.Context, string) (*database.AccessToken, error) {
-				panic("unexpected invocation of MockAccessTokensStore.GetBySHA1")
-			},
-		},
-		ListFunc: &AccessTokensStoreListFunc{
-			defaultHook: func(context.Context, int64) ([]*database.AccessToken, error) {
-				panic("unexpected invocation of MockAccessTokensStore.List")
-			},
-		},
-		TouchFunc: &AccessTokensStoreTouchFunc{
-			defaultHook: func(context.Context, int64) error {
-				panic("unexpected invocation of MockAccessTokensStore.Touch")
-			},
-		},
-	}
-}
-
-// NewMockAccessTokensStoreFrom creates a new mock of the
-// MockAccessTokensStore interface. All methods delegate to the given
-// implementation, unless overwritten.
-func NewMockAccessTokensStoreFrom(i database.AccessTokensStore) *MockAccessTokensStore {
-	return &MockAccessTokensStore{
-		CreateFunc: &AccessTokensStoreCreateFunc{
-			defaultHook: i.Create,
-		},
-		DeleteByIDFunc: &AccessTokensStoreDeleteByIDFunc{
-			defaultHook: i.DeleteByID,
-		},
-		GetBySHA1Func: &AccessTokensStoreGetBySHA1Func{
-			defaultHook: i.GetBySHA1,
-		},
-		ListFunc: &AccessTokensStoreListFunc{
-			defaultHook: i.List,
-		},
-		TouchFunc: &AccessTokensStoreTouchFunc{
-			defaultHook: i.Touch,
-		},
-	}
-}
-
-// AccessTokensStoreCreateFunc describes the behavior when the Create method
-// of the parent MockAccessTokensStore instance is invoked.
-type AccessTokensStoreCreateFunc struct {
-	defaultHook func(context.Context, int64, string) (*database.AccessToken, error)
-	hooks       []func(context.Context, int64, string) (*database.AccessToken, error)
-	history     []AccessTokensStoreCreateFuncCall
-	mutex       sync.Mutex
-}
-
-// Create delegates to the next hook function in the queue and stores the
-// parameter and result values of this invocation.
-func (m *MockAccessTokensStore) Create(v0 context.Context, v1 int64, v2 string) (*database.AccessToken, error) {
-	r0, r1 := m.CreateFunc.nextHook()(v0, v1, v2)
-	m.CreateFunc.appendCall(AccessTokensStoreCreateFuncCall{v0, v1, v2, r0, r1})
-	return r0, r1
-}
-
-// SetDefaultHook sets function that is called when the Create method of the
-// parent MockAccessTokensStore instance is invoked and the hook queue is
-// empty.
-func (f *AccessTokensStoreCreateFunc) SetDefaultHook(hook func(context.Context, int64, string) (*database.AccessToken, error)) {
-	f.defaultHook = hook
-}
-
-// PushHook adds a function to the end of hook queue. Each invocation of the
-// Create method of the parent MockAccessTokensStore instance invokes the
-// hook at the front of the queue and discards it. After the queue is empty,
-// the default hook function is invoked for any future action.
-func (f *AccessTokensStoreCreateFunc) PushHook(hook func(context.Context, int64, string) (*database.AccessToken, error)) {
-	f.mutex.Lock()
-	f.hooks = append(f.hooks, hook)
-	f.mutex.Unlock()
-}
-
-// SetDefaultReturn calls SetDefaultHook with a function that returns the
-// given values.
-func (f *AccessTokensStoreCreateFunc) SetDefaultReturn(r0 *database.AccessToken, r1 error) {
-	f.SetDefaultHook(func(context.Context, int64, string) (*database.AccessToken, error) {
-		return r0, r1
-	})
-}
-
-// PushReturn calls PushHook with a function that returns the given values.
-func (f *AccessTokensStoreCreateFunc) PushReturn(r0 *database.AccessToken, r1 error) {
-	f.PushHook(func(context.Context, int64, string) (*database.AccessToken, error) {
-		return r0, r1
-	})
-}
-
-func (f *AccessTokensStoreCreateFunc) nextHook() func(context.Context, int64, string) (*database.AccessToken, error) {
-	f.mutex.Lock()
-	defer f.mutex.Unlock()
-
-	if len(f.hooks) == 0 {
-		return f.defaultHook
-	}
-
-	hook := f.hooks[0]
-	f.hooks = f.hooks[1:]
-	return hook
-}
-
-func (f *AccessTokensStoreCreateFunc) appendCall(r0 AccessTokensStoreCreateFuncCall) {
-	f.mutex.Lock()
-	f.history = append(f.history, r0)
-	f.mutex.Unlock()
-}
-
-// History returns a sequence of AccessTokensStoreCreateFuncCall objects
-// describing the invocations of this function.
-func (f *AccessTokensStoreCreateFunc) History() []AccessTokensStoreCreateFuncCall {
-	f.mutex.Lock()
-	history := make([]AccessTokensStoreCreateFuncCall, len(f.history))
-	copy(history, f.history)
-	f.mutex.Unlock()
-
-	return history
-}
-
-// AccessTokensStoreCreateFuncCall is an object that describes an invocation
-// of method Create on an instance of MockAccessTokensStore.
-type AccessTokensStoreCreateFuncCall struct {
-	// Arg0 is the value of the 1st argument passed to this method
-	// invocation.
-	Arg0 context.Context
-	// Arg1 is the value of the 2nd argument passed to this method
-	// invocation.
-	Arg1 int64
-	// Arg2 is the value of the 3rd argument passed to this method
-	// invocation.
-	Arg2 string
-	// Result0 is the value of the 1st result returned from this method
-	// invocation.
-	Result0 *database.AccessToken
-	// Result1 is the value of the 2nd result returned from this method
-	// invocation.
-	Result1 error
-}
-
-// Args returns an interface slice containing the arguments of this
-// invocation.
-func (c AccessTokensStoreCreateFuncCall) Args() []interface{} {
-	return []interface{}{c.Arg0, c.Arg1, c.Arg2}
-}
-
-// Results returns an interface slice containing the results of this
-// invocation.
-func (c AccessTokensStoreCreateFuncCall) Results() []interface{} {
-	return []interface{}{c.Result0, c.Result1}
-}
-
-// AccessTokensStoreDeleteByIDFunc describes the behavior when the
-// DeleteByID method of the parent MockAccessTokensStore instance is
-// invoked.
-type AccessTokensStoreDeleteByIDFunc struct {
-	defaultHook func(context.Context, int64, int64) error
-	hooks       []func(context.Context, int64, int64) error
-	history     []AccessTokensStoreDeleteByIDFuncCall
-	mutex       sync.Mutex
-}
-
-// DeleteByID delegates to the next hook function in the queue and stores
-// the parameter and result values of this invocation.
-func (m *MockAccessTokensStore) DeleteByID(v0 context.Context, v1 int64, v2 int64) error {
-	r0 := m.DeleteByIDFunc.nextHook()(v0, v1, v2)
-	m.DeleteByIDFunc.appendCall(AccessTokensStoreDeleteByIDFuncCall{v0, v1, v2, r0})
-	return r0
-}
-
-// SetDefaultHook sets function that is called when the DeleteByID method of
-// the parent MockAccessTokensStore instance is invoked and the hook queue
-// is empty.
-func (f *AccessTokensStoreDeleteByIDFunc) SetDefaultHook(hook func(context.Context, int64, int64) error) {
-	f.defaultHook = hook
-}
-
-// PushHook adds a function to the end of hook queue. Each invocation of the
-// DeleteByID method of the parent MockAccessTokensStore instance invokes
-// the hook at the front of the queue and discards it. After the queue is
-// empty, the default hook function is invoked for any future action.
-func (f *AccessTokensStoreDeleteByIDFunc) PushHook(hook func(context.Context, int64, int64) error) {
-	f.mutex.Lock()
-	f.hooks = append(f.hooks, hook)
-	f.mutex.Unlock()
-}
-
-// SetDefaultReturn calls SetDefaultHook with a function that returns the
-// given values.
-func (f *AccessTokensStoreDeleteByIDFunc) SetDefaultReturn(r0 error) {
-	f.SetDefaultHook(func(context.Context, int64, int64) error {
-		return r0
-	})
-}
-
-// PushReturn calls PushHook with a function that returns the given values.
-func (f *AccessTokensStoreDeleteByIDFunc) PushReturn(r0 error) {
-	f.PushHook(func(context.Context, int64, int64) error {
-		return r0
-	})
-}
-
-func (f *AccessTokensStoreDeleteByIDFunc) nextHook() func(context.Context, int64, int64) error {
-	f.mutex.Lock()
-	defer f.mutex.Unlock()
-
-	if len(f.hooks) == 0 {
-		return f.defaultHook
-	}
-
-	hook := f.hooks[0]
-	f.hooks = f.hooks[1:]
-	return hook
-}
-
-func (f *AccessTokensStoreDeleteByIDFunc) appendCall(r0 AccessTokensStoreDeleteByIDFuncCall) {
-	f.mutex.Lock()
-	f.history = append(f.history, r0)
-	f.mutex.Unlock()
-}
-
-// History returns a sequence of AccessTokensStoreDeleteByIDFuncCall objects
-// describing the invocations of this function.
-func (f *AccessTokensStoreDeleteByIDFunc) History() []AccessTokensStoreDeleteByIDFuncCall {
-	f.mutex.Lock()
-	history := make([]AccessTokensStoreDeleteByIDFuncCall, len(f.history))
-	copy(history, f.history)
-	f.mutex.Unlock()
-
-	return history
-}
-
-// AccessTokensStoreDeleteByIDFuncCall is an object that describes an
-// invocation of method DeleteByID on an instance of MockAccessTokensStore.
-type AccessTokensStoreDeleteByIDFuncCall struct {
-	// Arg0 is the value of the 1st argument passed to this method
-	// invocation.
-	Arg0 context.Context
-	// Arg1 is the value of the 2nd argument passed to this method
-	// invocation.
-	Arg1 int64
-	// Arg2 is the value of the 3rd argument passed to this method
-	// invocation.
-	Arg2 int64
-	// Result0 is the value of the 1st result returned from this method
-	// invocation.
-	Result0 error
-}
-
-// Args returns an interface slice containing the arguments of this
-// invocation.
-func (c AccessTokensStoreDeleteByIDFuncCall) Args() []interface{} {
-	return []interface{}{c.Arg0, c.Arg1, c.Arg2}
-}
-
-// Results returns an interface slice containing the results of this
-// invocation.
-func (c AccessTokensStoreDeleteByIDFuncCall) Results() []interface{} {
-	return []interface{}{c.Result0}
-}
-
-// AccessTokensStoreGetBySHA1Func describes the behavior when the GetBySHA1
-// method of the parent MockAccessTokensStore instance is invoked.
-type AccessTokensStoreGetBySHA1Func struct {
-	defaultHook func(context.Context, string) (*database.AccessToken, error)
-	hooks       []func(context.Context, string) (*database.AccessToken, error)
-	history     []AccessTokensStoreGetBySHA1FuncCall
-	mutex       sync.Mutex
-}
-
-// GetBySHA1 delegates to the next hook function in the queue and stores the
-// parameter and result values of this invocation.
-func (m *MockAccessTokensStore) GetBySHA1(v0 context.Context, v1 string) (*database.AccessToken, error) {
-	r0, r1 := m.GetBySHA1Func.nextHook()(v0, v1)
-	m.GetBySHA1Func.appendCall(AccessTokensStoreGetBySHA1FuncCall{v0, v1, r0, r1})
-	return r0, r1
-}
-
-// SetDefaultHook sets function that is called when the GetBySHA1 method of
-// the parent MockAccessTokensStore instance is invoked and the hook queue
-// is empty.
-func (f *AccessTokensStoreGetBySHA1Func) SetDefaultHook(hook func(context.Context, string) (*database.AccessToken, error)) {
-	f.defaultHook = hook
-}
-
-// PushHook adds a function to the end of hook queue. Each invocation of the
-// GetBySHA1 method of the parent MockAccessTokensStore instance invokes the
-// hook at the front of the queue and discards it. After the queue is empty,
-// the default hook function is invoked for any future action.
-func (f *AccessTokensStoreGetBySHA1Func) PushHook(hook func(context.Context, string) (*database.AccessToken, error)) {
-	f.mutex.Lock()
-	f.hooks = append(f.hooks, hook)
-	f.mutex.Unlock()
-}
-
-// SetDefaultReturn calls SetDefaultHook with a function that returns the
-// given values.
-func (f *AccessTokensStoreGetBySHA1Func) SetDefaultReturn(r0 *database.AccessToken, r1 error) {
-	f.SetDefaultHook(func(context.Context, string) (*database.AccessToken, error) {
-		return r0, r1
-	})
-}
-
-// PushReturn calls PushHook with a function that returns the given values.
-func (f *AccessTokensStoreGetBySHA1Func) PushReturn(r0 *database.AccessToken, r1 error) {
-	f.PushHook(func(context.Context, string) (*database.AccessToken, error) {
-		return r0, r1
-	})
-}
-
-func (f *AccessTokensStoreGetBySHA1Func) nextHook() func(context.Context, string) (*database.AccessToken, error) {
-	f.mutex.Lock()
-	defer f.mutex.Unlock()
-
-	if len(f.hooks) == 0 {
-		return f.defaultHook
-	}
-
-	hook := f.hooks[0]
-	f.hooks = f.hooks[1:]
-	return hook
-}
-
-func (f *AccessTokensStoreGetBySHA1Func) appendCall(r0 AccessTokensStoreGetBySHA1FuncCall) {
-	f.mutex.Lock()
-	f.history = append(f.history, r0)
-	f.mutex.Unlock()
-}
-
-// History returns a sequence of AccessTokensStoreGetBySHA1FuncCall objects
-// describing the invocations of this function.
-func (f *AccessTokensStoreGetBySHA1Func) History() []AccessTokensStoreGetBySHA1FuncCall {
-	f.mutex.Lock()
-	history := make([]AccessTokensStoreGetBySHA1FuncCall, len(f.history))
-	copy(history, f.history)
-	f.mutex.Unlock()
-
-	return history
-}
-
-// AccessTokensStoreGetBySHA1FuncCall is an object that describes an
-// invocation of method GetBySHA1 on an instance of MockAccessTokensStore.
-type AccessTokensStoreGetBySHA1FuncCall struct {
-	// Arg0 is the value of the 1st argument passed to this method
-	// invocation.
-	Arg0 context.Context
-	// Arg1 is the value of the 2nd argument passed to this method
-	// invocation.
-	Arg1 string
-	// Result0 is the value of the 1st result returned from this method
-	// invocation.
-	Result0 *database.AccessToken
-	// Result1 is the value of the 2nd result returned from this method
-	// invocation.
-	Result1 error
-}
-
-// Args returns an interface slice containing the arguments of this
-// invocation.
-func (c AccessTokensStoreGetBySHA1FuncCall) Args() []interface{} {
-	return []interface{}{c.Arg0, c.Arg1}
-}
-
-// Results returns an interface slice containing the results of this
-// invocation.
-func (c AccessTokensStoreGetBySHA1FuncCall) Results() []interface{} {
-	return []interface{}{c.Result0, c.Result1}
-}
-
-// AccessTokensStoreListFunc describes the behavior when the List method of
-// the parent MockAccessTokensStore instance is invoked.
-type AccessTokensStoreListFunc struct {
-	defaultHook func(context.Context, int64) ([]*database.AccessToken, error)
-	hooks       []func(context.Context, int64) ([]*database.AccessToken, error)
-	history     []AccessTokensStoreListFuncCall
-	mutex       sync.Mutex
-}
-
-// List delegates to the next hook function in the queue and stores the
-// parameter and result values of this invocation.
-func (m *MockAccessTokensStore) List(v0 context.Context, v1 int64) ([]*database.AccessToken, error) {
-	r0, r1 := m.ListFunc.nextHook()(v0, v1)
-	m.ListFunc.appendCall(AccessTokensStoreListFuncCall{v0, v1, r0, r1})
-	return r0, r1
-}
-
-// SetDefaultHook sets function that is called when the List method of the
-// parent MockAccessTokensStore instance is invoked and the hook queue is
-// empty.
-func (f *AccessTokensStoreListFunc) SetDefaultHook(hook func(context.Context, int64) ([]*database.AccessToken, error)) {
-	f.defaultHook = hook
-}
-
-// PushHook adds a function to the end of hook queue. Each invocation of the
-// List method of the parent MockAccessTokensStore instance invokes the hook
-// at the front of the queue and discards it. After the queue is empty, the
-// default hook function is invoked for any future action.
-func (f *AccessTokensStoreListFunc) PushHook(hook func(context.Context, int64) ([]*database.AccessToken, error)) {
-	f.mutex.Lock()
-	f.hooks = append(f.hooks, hook)
-	f.mutex.Unlock()
-}
-
-// SetDefaultReturn calls SetDefaultHook with a function that returns the
-// given values.
-func (f *AccessTokensStoreListFunc) SetDefaultReturn(r0 []*database.AccessToken, r1 error) {
-	f.SetDefaultHook(func(context.Context, int64) ([]*database.AccessToken, error) {
-		return r0, r1
-	})
-}
-
-// PushReturn calls PushHook with a function that returns the given values.
-func (f *AccessTokensStoreListFunc) PushReturn(r0 []*database.AccessToken, r1 error) {
-	f.PushHook(func(context.Context, int64) ([]*database.AccessToken, error) {
-		return r0, r1
-	})
-}
-
-func (f *AccessTokensStoreListFunc) nextHook() func(context.Context, int64) ([]*database.AccessToken, error) {
-	f.mutex.Lock()
-	defer f.mutex.Unlock()
-
-	if len(f.hooks) == 0 {
-		return f.defaultHook
-	}
-
-	hook := f.hooks[0]
-	f.hooks = f.hooks[1:]
-	return hook
-}
-
-func (f *AccessTokensStoreListFunc) appendCall(r0 AccessTokensStoreListFuncCall) {
-	f.mutex.Lock()
-	f.history = append(f.history, r0)
-	f.mutex.Unlock()
-}
-
-// History returns a sequence of AccessTokensStoreListFuncCall objects
-// describing the invocations of this function.
-func (f *AccessTokensStoreListFunc) History() []AccessTokensStoreListFuncCall {
-	f.mutex.Lock()
-	history := make([]AccessTokensStoreListFuncCall, len(f.history))
-	copy(history, f.history)
-	f.mutex.Unlock()
-
-	return history
-}
-
-// AccessTokensStoreListFuncCall is an object that describes an invocation
-// of method List on an instance of MockAccessTokensStore.
-type AccessTokensStoreListFuncCall struct {
-	// Arg0 is the value of the 1st argument passed to this method
-	// invocation.
-	Arg0 context.Context
-	// Arg1 is the value of the 2nd argument passed to this method
-	// invocation.
-	Arg1 int64
-	// Result0 is the value of the 1st result returned from this method
-	// invocation.
-	Result0 []*database.AccessToken
-	// Result1 is the value of the 2nd result returned from this method
-	// invocation.
-	Result1 error
-}
-
-// Args returns an interface slice containing the arguments of this
-// invocation.
-func (c AccessTokensStoreListFuncCall) Args() []interface{} {
-	return []interface{}{c.Arg0, c.Arg1}
-}
-
-// Results returns an interface slice containing the results of this
-// invocation.
-func (c AccessTokensStoreListFuncCall) Results() []interface{} {
-	return []interface{}{c.Result0, c.Result1}
-}
-
-// AccessTokensStoreTouchFunc describes the behavior when the Touch method
-// of the parent MockAccessTokensStore instance is invoked.
-type AccessTokensStoreTouchFunc struct {
-	defaultHook func(context.Context, int64) error
-	hooks       []func(context.Context, int64) error
-	history     []AccessTokensStoreTouchFuncCall
-	mutex       sync.Mutex
-}
-
-// Touch delegates to the next hook function in the queue and stores the
-// parameter and result values of this invocation.
-func (m *MockAccessTokensStore) Touch(v0 context.Context, v1 int64) error {
-	r0 := m.TouchFunc.nextHook()(v0, v1)
-	m.TouchFunc.appendCall(AccessTokensStoreTouchFuncCall{v0, v1, r0})
-	return r0
-}
-
-// SetDefaultHook sets function that is called when the Touch method of the
-// parent MockAccessTokensStore instance is invoked and the hook queue is
-// empty.
-func (f *AccessTokensStoreTouchFunc) SetDefaultHook(hook func(context.Context, int64) error) {
-	f.defaultHook = hook
-}
-
-// PushHook adds a function to the end of hook queue. Each invocation of the
-// Touch method of the parent MockAccessTokensStore instance invokes the
-// hook at the front of the queue and discards it. After the queue is empty,
-// the default hook function is invoked for any future action.
-func (f *AccessTokensStoreTouchFunc) PushHook(hook func(context.Context, int64) error) {
-	f.mutex.Lock()
-	f.hooks = append(f.hooks, hook)
-	f.mutex.Unlock()
-}
-
-// SetDefaultReturn calls SetDefaultHook with a function that returns the
-// given values.
-func (f *AccessTokensStoreTouchFunc) SetDefaultReturn(r0 error) {
-	f.SetDefaultHook(func(context.Context, int64) error {
-		return r0
-	})
-}
-
-// PushReturn calls PushHook with a function that returns the given values.
-func (f *AccessTokensStoreTouchFunc) PushReturn(r0 error) {
-	f.PushHook(func(context.Context, int64) error {
-		return r0
-	})
-}
-
-func (f *AccessTokensStoreTouchFunc) nextHook() func(context.Context, int64) error {
-	f.mutex.Lock()
-	defer f.mutex.Unlock()
-
-	if len(f.hooks) == 0 {
-		return f.defaultHook
-	}
-
-	hook := f.hooks[0]
-	f.hooks = f.hooks[1:]
-	return hook
-}
-
-func (f *AccessTokensStoreTouchFunc) appendCall(r0 AccessTokensStoreTouchFuncCall) {
-	f.mutex.Lock()
-	f.history = append(f.history, r0)
-	f.mutex.Unlock()
-}
-
-// History returns a sequence of AccessTokensStoreTouchFuncCall objects
-// describing the invocations of this function.
-func (f *AccessTokensStoreTouchFunc) History() []AccessTokensStoreTouchFuncCall {
-	f.mutex.Lock()
-	history := make([]AccessTokensStoreTouchFuncCall, len(f.history))
-	copy(history, f.history)
-	f.mutex.Unlock()
-
-	return history
-}
-
-// AccessTokensStoreTouchFuncCall is an object that describes an invocation
-// of method Touch on an instance of MockAccessTokensStore.
-type AccessTokensStoreTouchFuncCall struct {
-	// Arg0 is the value of the 1st argument passed to this method
-	// invocation.
-	Arg0 context.Context
-	// Arg1 is the value of the 2nd argument passed to this method
-	// invocation.
-	Arg1 int64
-	// Result0 is the value of the 1st result returned from this method
-	// invocation.
-	Result0 error
-}
-
-// Args returns an interface slice containing the arguments of this
-// invocation.
-func (c AccessTokensStoreTouchFuncCall) Args() []interface{} {
-	return []interface{}{c.Arg0, c.Arg1}
-}
-
-// Results returns an interface slice containing the results of this
-// invocation.
-func (c AccessTokensStoreTouchFuncCall) Results() []interface{} {
-	return []interface{}{c.Result0}
-}
-
 // MockLFSStore is a mock implementation of the LFSStore interface (from the
 // package gogs.io/gogs/internal/database) used for unit testing.
 type MockLFSStore struct {
@@ -6698,3 +6047,274 @@ func (c UsersStoreUseCustomAvatarFuncCall) Args() []interface{} {
 func (c UsersStoreUseCustomAvatarFuncCall) Results() []interface{} {
 	return []interface{}{c.Result0}
 }
+
+// MockStore is a mock implementation of the Store interface (from the
+// package gogs.io/gogs/internal/route/lfs) used for unit testing.
+type MockStore struct {
+	// GetAccessTokenBySHA1Func is an instance of a mock function object
+	// controlling the behavior of the method GetAccessTokenBySHA1.
+	GetAccessTokenBySHA1Func *StoreGetAccessTokenBySHA1Func
+	// TouchAccessTokenByIDFunc is an instance of a mock function object
+	// controlling the behavior of the method TouchAccessTokenByID.
+	TouchAccessTokenByIDFunc *StoreTouchAccessTokenByIDFunc
+}
+
+// NewMockStore creates a new mock of the Store interface. All methods
+// return zero values for all results, unless overwritten.
+func NewMockStore() *MockStore {
+	return &MockStore{
+		GetAccessTokenBySHA1Func: &StoreGetAccessTokenBySHA1Func{
+			defaultHook: func(context.Context, string) (r0 *database.AccessToken, r1 error) {
+				return
+			},
+		},
+		TouchAccessTokenByIDFunc: &StoreTouchAccessTokenByIDFunc{
+			defaultHook: func(context.Context, int64) (r0 error) {
+				return
+			},
+		},
+	}
+}
+
+// NewStrictMockStore creates a new mock of the Store interface. All methods
+// panic on invocation, unless overwritten.
+func NewStrictMockStore() *MockStore {
+	return &MockStore{
+		GetAccessTokenBySHA1Func: &StoreGetAccessTokenBySHA1Func{
+			defaultHook: func(context.Context, string) (*database.AccessToken, error) {
+				panic("unexpected invocation of MockStore.GetAccessTokenBySHA1")
+			},
+		},
+		TouchAccessTokenByIDFunc: &StoreTouchAccessTokenByIDFunc{
+			defaultHook: func(context.Context, int64) error {
+				panic("unexpected invocation of MockStore.TouchAccessTokenByID")
+			},
+		},
+	}
+}
+
+// NewMockStoreFrom creates a new mock of the MockStore interface. All
+// methods delegate to the given implementation, unless overwritten.
+func NewMockStoreFrom(i Store) *MockStore {
+	return &MockStore{
+		GetAccessTokenBySHA1Func: &StoreGetAccessTokenBySHA1Func{
+			defaultHook: i.GetAccessTokenBySHA1,
+		},
+		TouchAccessTokenByIDFunc: &StoreTouchAccessTokenByIDFunc{
+			defaultHook: i.TouchAccessTokenByID,
+		},
+	}
+}
+
+// StoreGetAccessTokenBySHA1Func describes the behavior when the
+// GetAccessTokenBySHA1 method of the parent MockStore instance is invoked.
+type StoreGetAccessTokenBySHA1Func struct {
+	defaultHook func(context.Context, string) (*database.AccessToken, error)
+	hooks       []func(context.Context, string) (*database.AccessToken, error)
+	history     []StoreGetAccessTokenBySHA1FuncCall
+	mutex       sync.Mutex
+}
+
+// GetAccessTokenBySHA1 delegates to the next hook function in the queue and
+// stores the parameter and result values of this invocation.
+func (m *MockStore) GetAccessTokenBySHA1(v0 context.Context, v1 string) (*database.AccessToken, error) {
+	r0, r1 := m.GetAccessTokenBySHA1Func.nextHook()(v0, v1)
+	m.GetAccessTokenBySHA1Func.appendCall(StoreGetAccessTokenBySHA1FuncCall{v0, v1, r0, r1})
+	return r0, r1
+}
+
+// SetDefaultHook sets function that is called when the GetAccessTokenBySHA1
+// method of the parent MockStore instance is invoked and the hook queue is
+// empty.
+func (f *StoreGetAccessTokenBySHA1Func) SetDefaultHook(hook func(context.Context, string) (*database.AccessToken, error)) {
+	f.defaultHook = hook
+}
+
+// PushHook adds a function to the end of hook queue. Each invocation of the
+// GetAccessTokenBySHA1 method of the parent MockStore instance invokes the
+// hook at the front of the queue and discards it. After the queue is empty,
+// the default hook function is invoked for any future action.
+func (f *StoreGetAccessTokenBySHA1Func) PushHook(hook func(context.Context, string) (*database.AccessToken, error)) {
+	f.mutex.Lock()
+	f.hooks = append(f.hooks, hook)
+	f.mutex.Unlock()
+}
+
+// SetDefaultReturn calls SetDefaultHook with a function that returns the
+// given values.
+func (f *StoreGetAccessTokenBySHA1Func) SetDefaultReturn(r0 *database.AccessToken, r1 error) {
+	f.SetDefaultHook(func(context.Context, string) (*database.AccessToken, error) {
+		return r0, r1
+	})
+}
+
+// PushReturn calls PushHook with a function that returns the given values.
+func (f *StoreGetAccessTokenBySHA1Func) PushReturn(r0 *database.AccessToken, r1 error) {
+	f.PushHook(func(context.Context, string) (*database.AccessToken, error) {
+		return r0, r1
+	})
+}
+
+func (f *StoreGetAccessTokenBySHA1Func) nextHook() func(context.Context, string) (*database.AccessToken, error) {
+	f.mutex.Lock()
+	defer f.mutex.Unlock()
+
+	if len(f.hooks) == 0 {
+		return f.defaultHook
+	}
+
+	hook := f.hooks[0]
+	f.hooks = f.hooks[1:]
+	return hook
+}
+
+func (f *StoreGetAccessTokenBySHA1Func) appendCall(r0 StoreGetAccessTokenBySHA1FuncCall) {
+	f.mutex.Lock()
+	f.history = append(f.history, r0)
+	f.mutex.Unlock()
+}
+
+// History returns a sequence of StoreGetAccessTokenBySHA1FuncCall objects
+// describing the invocations of this function.
+func (f *StoreGetAccessTokenBySHA1Func) History() []StoreGetAccessTokenBySHA1FuncCall {
+	f.mutex.Lock()
+	history := make([]StoreGetAccessTokenBySHA1FuncCall, len(f.history))
+	copy(history, f.history)
+	f.mutex.Unlock()
+
+	return history
+}
+
+// StoreGetAccessTokenBySHA1FuncCall is an object that describes an
+// invocation of method GetAccessTokenBySHA1 on an instance of MockStore.
+type StoreGetAccessTokenBySHA1FuncCall struct {
+	// Arg0 is the value of the 1st argument passed to this method
+	// invocation.
+	Arg0 context.Context
+	// Arg1 is the value of the 2nd argument passed to this method
+	// invocation.
+	Arg1 string
+	// Result0 is the value of the 1st result returned from this method
+	// invocation.
+	Result0 *database.AccessToken
+	// Result1 is the value of the 2nd result returned from this method
+	// invocation.
+	Result1 error
+}
+
+// Args returns an interface slice containing the arguments of this
+// invocation.
+func (c StoreGetAccessTokenBySHA1FuncCall) Args() []interface{} {
+	return []interface{}{c.Arg0, c.Arg1}
+}
+
+// Results returns an interface slice containing the results of this
+// invocation.
+func (c StoreGetAccessTokenBySHA1FuncCall) Results() []interface{} {
+	return []interface{}{c.Result0, c.Result1}
+}
+
+// StoreTouchAccessTokenByIDFunc describes the behavior when the
+// TouchAccessTokenByID method of the parent MockStore instance is invoked.
+type StoreTouchAccessTokenByIDFunc struct {
+	defaultHook func(context.Context, int64) error
+	hooks       []func(context.Context, int64) error
+	history     []StoreTouchAccessTokenByIDFuncCall
+	mutex       sync.Mutex
+}
+
+// TouchAccessTokenByID delegates to the next hook function in the queue and
+// stores the parameter and result values of this invocation.
+func (m *MockStore) TouchAccessTokenByID(v0 context.Context, v1 int64) error {
+	r0 := m.TouchAccessTokenByIDFunc.nextHook()(v0, v1)
+	m.TouchAccessTokenByIDFunc.appendCall(StoreTouchAccessTokenByIDFuncCall{v0, v1, r0})
+	return r0
+}
+
+// SetDefaultHook sets function that is called when the TouchAccessTokenByID
+// method of the parent MockStore instance is invoked and the hook queue is
+// empty.
+func (f *StoreTouchAccessTokenByIDFunc) SetDefaultHook(hook func(context.Context, int64) error) {
+	f.defaultHook = hook
+}
+
+// PushHook adds a function to the end of hook queue. Each invocation of the
+// TouchAccessTokenByID method of the parent MockStore instance invokes the
+// hook at the front of the queue and discards it. After the queue is empty,
+// the default hook function is invoked for any future action.
+func (f *StoreTouchAccessTokenByIDFunc) PushHook(hook func(context.Context, int64) error) {
+	f.mutex.Lock()
+	f.hooks = append(f.hooks, hook)
+	f.mutex.Unlock()
+}
+
+// SetDefaultReturn calls SetDefaultHook with a function that returns the
+// given values.
+func (f *StoreTouchAccessTokenByIDFunc) SetDefaultReturn(r0 error) {
+	f.SetDefaultHook(func(context.Context, int64) error {
+		return r0
+	})
+}
+
+// PushReturn calls PushHook with a function that returns the given values.
+func (f *StoreTouchAccessTokenByIDFunc) PushReturn(r0 error) {
+	f.PushHook(func(context.Context, int64) error {
+		return r0
+	})
+}
+
+func (f *StoreTouchAccessTokenByIDFunc) nextHook() func(context.Context, int64) error {
+	f.mutex.Lock()
+	defer f.mutex.Unlock()
+
+	if len(f.hooks) == 0 {
+		return f.defaultHook
+	}
+
+	hook := f.hooks[0]
+	f.hooks = f.hooks[1:]
+	return hook
+}
+
+func (f *StoreTouchAccessTokenByIDFunc) appendCall(r0 StoreTouchAccessTokenByIDFuncCall) {
+	f.mutex.Lock()
+	f.history = append(f.history, r0)
+	f.mutex.Unlock()
+}
+
+// History returns a sequence of StoreTouchAccessTokenByIDFuncCall objects
+// describing the invocations of this function.
+func (f *StoreTouchAccessTokenByIDFunc) History() []StoreTouchAccessTokenByIDFuncCall {
+	f.mutex.Lock()
+	history := make([]StoreTouchAccessTokenByIDFuncCall, len(f.history))
+	copy(history, f.history)
+	f.mutex.Unlock()
+
+	return history
+}
+
+// StoreTouchAccessTokenByIDFuncCall is an object that describes an
+// invocation of method TouchAccessTokenByID on an instance of MockStore.
+type StoreTouchAccessTokenByIDFuncCall struct {
+	// Arg0 is the value of the 1st argument passed to this method
+	// invocation.
+	Arg0 context.Context
+	// Arg1 is the value of the 2nd argument passed to this method
+	// invocation.
+	Arg1 int64
+	// Result0 is the value of the 1st result returned from this method
+	// invocation.
+	Result0 error
+}
+
+// Args returns an interface slice containing the arguments of this
+// invocation.
+func (c StoreTouchAccessTokenByIDFuncCall) Args() []interface{} {
+	return []interface{}{c.Arg0, c.Arg1}
+}
+
+// Results returns an interface slice containing the results of this
+// invocation.
+func (c StoreTouchAccessTokenByIDFuncCall) Results() []interface{} {
+	return []interface{}{c.Result0}
+}

+ 7 - 5
internal/route/lfs/route.go

@@ -19,12 +19,14 @@ import (
 	"gogs.io/gogs/internal/lfsutil"
 )
 
-// RegisterRoutes registers LFS routes using given router, and inherits all groups and middleware.
+// RegisterRoutes registers LFS routes using given router, and inherits all
+// groups and middleware.
 func RegisterRoutes(r *macaron.Router) {
 	verifyAccept := verifyHeader("Accept", contentType, http.StatusNotAcceptable)
 	verifyContentTypeJSON := verifyHeader("Content-Type", contentType, http.StatusBadRequest)
 	verifyContentTypeStream := verifyHeader("Content-Type", "application/octet-stream", http.StatusBadRequest)
 
+	store := NewStore()
 	r.Group("", func() {
 		r.Post("/objects/batch", authorize(database.AccessModeRead), verifyAccept, verifyContentTypeJSON, serveBatch)
 		r.Group("/objects/basic", func() {
@@ -39,12 +41,12 @@ func RegisterRoutes(r *macaron.Router) {
 				Put(authorize(database.AccessModeWrite), verifyContentTypeStream, basic.serveUpload)
 			r.Post("/verify", authorize(database.AccessModeWrite), verifyAccept, verifyContentTypeJSON, basic.serveVerify)
 		})
-	}, authenticate())
+	}, authenticate(store))
 }
 
 // authenticate tries to authenticate user via HTTP Basic Auth. It first tries to authenticate
 // as plain username and password, then use username as access token if previous step failed.
-func authenticate() macaron.Handler {
+func authenticate(store Store) macaron.Handler {
 	askCredentials := func(w http.ResponseWriter) {
 		w.Header().Set("Lfs-Authenticate", `Basic realm="Git LFS"`)
 		responseJSON(w, http.StatusUnauthorized, responseError{
@@ -74,14 +76,14 @@ func authenticate() macaron.Handler {
 		// If username and password combination failed, try again using either username
 		// or password as the token.
 		if auth.IsErrBadCredentials(err) {
-			user, err = context.AuthenticateByToken(c.Req.Context(), username)
+			user, err = context.AuthenticateByToken(store, c.Req.Context(), username)
 			if err != nil && !database.IsErrAccessTokenNotExist(err) {
 				internalServerError(c.Resp)
 				log.Error("Failed to authenticate by access token via username: %v", err)
 				return
 			} else if database.IsErrAccessTokenNotExist(err) {
 				// Try again using the password field as the token.
-				user, err = context.AuthenticateByToken(c.Req.Context(), password)
+				user, err = context.AuthenticateByToken(store, c.Req.Context(), password)
 				if err != nil {
 					if database.IsErrAccessTokenNotExist(err) {
 						askCredentials(c.Resp)

+ 29 - 29
internal/route/lfs/route_test.go

@@ -20,22 +20,16 @@ import (
 	"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 *database.User) {
-		_, _ = fmt.Fprintf(w, "ID: %d, Name: %s", user.ID, user.Name)
-	})
-
+func TestAuthenticate(t *testing.T) {
 	tests := []struct {
-		name                  string
-		header                http.Header
-		mockUsersStore        func() database.UsersStore
-		mockTwoFactorsStore   func() database.TwoFactorsStore
-		mockAccessTokensStore func() database.AccessTokensStore
-		expStatusCode         int
-		expHeader             http.Header
-		expBody               string
+		name                string
+		header              http.Header
+		mockUsersStore      func() database.UsersStore
+		mockTwoFactorsStore func() database.TwoFactorsStore
+		mockStore           func() *MockStore
+		expStatusCode       int
+		expHeader           http.Header
+		expBody             string
 	}{
 		{
 			name:          "no authorization",
@@ -75,10 +69,10 @@ func Test_authenticate(t *testing.T) {
 				mock.AuthenticateFunc.SetDefaultReturn(nil, auth.ErrBadCredentials{})
 				return mock
 			},
-			mockAccessTokensStore: func() database.AccessTokensStore {
-				mock := NewMockAccessTokensStore()
-				mock.GetBySHA1Func.SetDefaultReturn(nil, database.ErrAccessTokenNotExist{})
-				return mock
+			mockStore: func() *MockStore {
+				mockStore := NewMockStore()
+				mockStore.GetAccessTokenBySHA1Func.SetDefaultReturn(nil, database.ErrAccessTokenNotExist{})
+				return mockStore
 			},
 			expStatusCode: http.StatusUnauthorized,
 			expHeader: http.Header{
@@ -118,10 +112,10 @@ func Test_authenticate(t *testing.T) {
 				mock.GetByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
 				return mock
 			},
-			mockAccessTokensStore: func() database.AccessTokensStore {
-				mock := NewMockAccessTokensStore()
-				mock.GetBySHA1Func.SetDefaultReturn(&database.AccessToken{}, nil)
-				return mock
+			mockStore: func() *MockStore {
+				mockStore := NewMockStore()
+				mockStore.GetAccessTokenBySHA1Func.SetDefaultReturn(&database.AccessToken{}, nil)
+				return mockStore
 			},
 			expStatusCode: http.StatusOK,
 			expHeader:     http.Header{},
@@ -138,15 +132,15 @@ func Test_authenticate(t *testing.T) {
 				mock.GetByIDFunc.SetDefaultReturn(&database.User{ID: 1, Name: "unknwon"}, nil)
 				return mock
 			},
-			mockAccessTokensStore: func() database.AccessTokensStore {
-				mock := NewMockAccessTokensStore()
-				mock.GetBySHA1Func.SetDefaultHook(func(ctx context.Context, sha1 string) (*database.AccessToken, error) {
+			mockStore: func() *MockStore {
+				mockStore := NewMockStore()
+				mockStore.GetAccessTokenBySHA1Func.SetDefaultHook(func(_ context.Context, sha1 string) (*database.AccessToken, error) {
 					if sha1 == "password" {
 						return &database.AccessToken{}, nil
 					}
 					return nil, database.ErrAccessTokenNotExist{}
 				})
-				return mock
+				return mockStore
 			},
 			expStatusCode: http.StatusOK,
 			expHeader:     http.Header{},
@@ -161,10 +155,16 @@ func Test_authenticate(t *testing.T) {
 			if test.mockTwoFactorsStore != nil {
 				database.SetMockTwoFactorsStore(t, test.mockTwoFactorsStore())
 			}
-			if test.mockAccessTokensStore != nil {
-				database.SetMockAccessTokensStore(t, test.mockAccessTokensStore())
+			if test.mockStore == nil {
+				test.mockStore = NewMockStore
 			}
 
+			m := macaron.New()
+			m.Use(macaron.Renderer())
+			m.Get("/", authenticate(test.mockStore()), func(w http.ResponseWriter, user *database.User) {
+				_, _ = fmt.Fprintf(w, "ID: %d, Name: %s", user.ID, user.Name)
+			})
+
 			r, err := http.NewRequest("GET", "/", nil)
 			if err != nil {
 				t.Fatal(err)

+ 34 - 0
internal/route/lfs/store.go

@@ -0,0 +1,34 @@
+package lfs
+
+import (
+	"context"
+
+	"gogs.io/gogs/internal/database"
+)
+
+// Store is the data layer carrier for LFS endpoints. This interface is meant to
+// abstract away and limit the exposure of the underlying data layer to the
+// handler through a thin-wrapper.
+type Store interface {
+	// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
+	// database.ErrAccessTokenNotExist when not found.
+	GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error)
+	// TouchAccessTokenByID updates the updated time of the given access token to
+	// the current time.
+	TouchAccessTokenByID(ctx context.Context, id int64) error
+}
+
+type store struct{}
+
+// NewStore returns a new Store using the global database handle.
+func NewStore() Store {
+	return &store{}
+}
+
+func (*store) GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error) {
+	return database.Handle.AccessTokens().GetBySHA1(ctx, sha1)
+}
+
+func (*store) TouchAccessTokenByID(ctx context.Context, id int64) error {
+	return database.Handle.AccessTokens().Touch(ctx, id)
+}

+ 3 - 3
internal/route/repo/http.go

@@ -44,7 +44,7 @@ func askCredentials(c *macaron.Context, status int, text string) {
 	c.Error(status, text)
 }
 
-func HTTPContexter() macaron.Handler {
+func HTTPContexter(store Store) macaron.Handler {
 	return func(c *macaron.Context) {
 		if len(conf.HTTP.AccessControlAllowOrigin) > 0 {
 			// Set CORS headers for browser-based git clients
@@ -134,14 +134,14 @@ func HTTPContexter() macaron.Handler {
 		// If username and password combination failed, try again using either username
 		// or password as the token.
 		if authUser == nil {
-			authUser, err = context.AuthenticateByToken(c.Req.Context(), authUsername)
+			authUser, err = context.AuthenticateByToken(store, c.Req.Context(), authUsername)
 			if err != nil && !database.IsErrAccessTokenNotExist(err) {
 				c.Status(http.StatusInternalServerError)
 				log.Error("Failed to authenticate by access token via username: %v", err)
 				return
 			} else if database.IsErrAccessTokenNotExist(err) {
 				// Try again using the password field as the token.
-				authUser, err = context.AuthenticateByToken(c.Req.Context(), authPassword)
+				authUser, err = context.AuthenticateByToken(store, c.Req.Context(), authPassword)
 				if err != nil {
 					if database.IsErrAccessTokenNotExist(err) {
 						askCredentials(c, http.StatusUnauthorized, "")

+ 34 - 0
internal/route/repo/store.go

@@ -0,0 +1,34 @@
+package repo
+
+import (
+	"context"
+
+	"gogs.io/gogs/internal/database"
+)
+
+// Store is the data layer carrier for context middleware. This interface is
+// meant to abstract away and limit the exposure of the underlying data layer to
+// the handler through a thin-wrapper.
+type Store interface {
+	// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
+	// database.ErrAccessTokenNotExist when not found.
+	GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error)
+	// TouchAccessTokenByID updates the updated time of the given access token to
+	// the current time.
+	TouchAccessTokenByID(ctx context.Context, id int64) error
+}
+
+type store struct{}
+
+// NewStore returns a new Store using the global database handle.
+func NewStore() Store {
+	return &store{}
+}
+
+func (*store) GetAccessTokenBySHA1(ctx context.Context, sha1 string) (*database.AccessToken, error) {
+	return database.Handle.AccessTokens().GetBySHA1(ctx, sha1)
+}
+
+func (*store) TouchAccessTokenByID(ctx context.Context, id int64) error {
+	return database.Handle.AccessTokens().Touch(ctx, id)
+}

+ 109 - 41
internal/route/user/setting.go

@@ -6,6 +6,7 @@ package user
 
 import (
 	"bytes"
+	gocontext "context"
 	"encoding/base64"
 	"fmt"
 	"html/template"
@@ -15,6 +16,7 @@ import (
 	"github.com/pkg/errors"
 	"github.com/pquerna/otp"
 	"github.com/pquerna/otp/totp"
+	"gopkg.in/macaron.v1"
 	log "unknwon.dev/clog/v2"
 
 	"gogs.io/gogs/internal/auth"
@@ -28,6 +30,18 @@ import (
 	"gogs.io/gogs/internal/userutil"
 )
 
+// SettingsHandler is the handler for users settings endpoints.
+type SettingsHandler struct {
+	store SettingsStore
+}
+
+// NewSettingsHandler returns a new SettingsHandler for users settings endpoints.
+func NewSettingsHandler(s SettingsStore) *SettingsHandler {
+	return &SettingsHandler{
+		store: s,
+	}
+}
+
 const (
 	SETTINGS_PROFILE                   = "user/settings/profile"
 	SETTINGS_AVATAR                    = "user/settings/avatar"
@@ -580,62 +594,68 @@ func SettingsLeaveOrganization(c *context.Context) {
 	})
 }
 
-func SettingsApplications(c *context.Context) {
-	c.Title("settings.applications")
-	c.PageIs("SettingsApplications")
+func (h *SettingsHandler) Applications() macaron.Handler {
+	return func(c *context.Context) {
+		c.Title("settings.applications")
+		c.PageIs("SettingsApplications")
 
-	tokens, err := database.AccessTokens.List(c.Req.Context(), c.User.ID)
-	if err != nil {
-		c.Errorf(err, "list access tokens")
-		return
-	}
-	c.Data["Tokens"] = tokens
+		tokens, err := h.store.ListAccessTokens(c.Req.Context(), c.User.ID)
+		if err != nil {
+			c.Errorf(err, "list access tokens")
+			return
+		}
+		c.Data["Tokens"] = tokens
 
-	c.Success(SETTINGS_APPLICATIONS)
+		c.Success(SETTINGS_APPLICATIONS)
+	}
 }
 
-func SettingsApplicationsPost(c *context.Context, f form.NewAccessToken) {
-	c.Title("settings.applications")
-	c.PageIs("SettingsApplications")
+func (h *SettingsHandler) ApplicationsPost() macaron.Handler {
+	return func(c *context.Context, f form.NewAccessToken) {
+		c.Title("settings.applications")
+		c.PageIs("SettingsApplications")
 
-	if c.HasError() {
-		tokens, err := database.AccessTokens.List(c.Req.Context(), c.User.ID)
+		if c.HasError() {
+			tokens, err := h.store.ListAccessTokens(c.Req.Context(), c.User.ID)
+			if err != nil {
+				c.Errorf(err, "list access tokens")
+				return
+			}
+
+			c.Data["Tokens"] = tokens
+			c.Success(SETTINGS_APPLICATIONS)
+			return
+		}
+
+		t, err := h.store.CreateAccessToken(c.Req.Context(), c.User.ID, f.Name)
 		if err != nil {
-			c.Errorf(err, "list access tokens")
+			if database.IsErrAccessTokenAlreadyExist(err) {
+				c.Flash.Error(c.Tr("settings.token_name_exists"))
+				c.RedirectSubpath("/user/settings/applications")
+			} else {
+				c.Errorf(err, "new access token")
+			}
 			return
 		}
 
-		c.Data["Tokens"] = tokens
-		c.Success(SETTINGS_APPLICATIONS)
-		return
+		c.Flash.Success(c.Tr("settings.generate_token_succees"))
+		c.Flash.Info(t.Sha1)
+		c.RedirectSubpath("/user/settings/applications")
 	}
+}
 
-	t, err := database.AccessTokens.Create(c.Req.Context(), c.User.ID, f.Name)
-	if err != nil {
-		if database.IsErrAccessTokenAlreadyExist(err) {
-			c.Flash.Error(c.Tr("settings.token_name_exists"))
-			c.RedirectSubpath("/user/settings/applications")
+func (h *SettingsHandler) DeleteApplication() macaron.Handler {
+	return func(c *context.Context) {
+		if err := h.store.DeleteAccessTokenByID(c.Req.Context(), c.User.ID, c.QueryInt64("id")); err != nil {
+			c.Flash.Error("DeleteAccessTokenByID: " + err.Error())
 		} else {
-			c.Errorf(err, "new access token")
+			c.Flash.Success(c.Tr("settings.delete_token_success"))
 		}
-		return
-	}
-
-	c.Flash.Success(c.Tr("settings.generate_token_succees"))
-	c.Flash.Info(t.Sha1)
-	c.RedirectSubpath("/user/settings/applications")
-}
 
-func SettingsDeleteApplication(c *context.Context) {
-	if err := database.AccessTokens.DeleteByID(c.Req.Context(), c.User.ID, c.QueryInt64("id")); err != nil {
-		c.Flash.Error("DeleteAccessTokenByID: " + err.Error())
-	} else {
-		c.Flash.Success(c.Tr("settings.delete_token_success"))
+		c.JSONSuccess(map[string]any{
+			"redirect": conf.Server.Subpath + "/user/settings/applications",
+		})
 	}
-
-	c.JSONSuccess(map[string]any{
-		"redirect": conf.Server.Subpath + "/user/settings/applications",
-	})
 }
 
 func SettingsDelete(c *context.Context) {
@@ -672,3 +692,51 @@ func SettingsDelete(c *context.Context) {
 
 	c.Success(SETTINGS_DELETE)
 }
+
+// SettingsStore is the data layer carrier for user settings endpoints. This
+// interface is meant to abstract away and limit the exposure of the underlying
+// data layer to the handler through a thin-wrapper.
+type SettingsStore interface {
+	// CreateAccessToken creates a new access token and persist to database. It
+	// returns database.ErrAccessTokenAlreadyExist when an access token with same
+	// name already exists for the user.
+	CreateAccessToken(ctx gocontext.Context, userID int64, name string) (*database.AccessToken, error)
+	// GetAccessTokenBySHA1 returns the access token with given SHA1. It returns
+	// database.ErrAccessTokenNotExist when not found.
+	GetAccessTokenBySHA1(ctx gocontext.Context, sha1 string) (*database.AccessToken, error)
+	// TouchAccessTokenByID updates the updated time of the given access token to
+	// the current time.
+	TouchAccessTokenByID(ctx gocontext.Context, id int64) error
+	// ListAccessTokens returns all access tokens belongs to given user.
+	ListAccessTokens(ctx gocontext.Context, userID int64) ([]*database.AccessToken, error)
+	// DeleteAccessTokenByID deletes the access token by given ID.
+	DeleteAccessTokenByID(ctx gocontext.Context, userID, id int64) error
+}
+
+type settingsStore struct{}
+
+// NewSettingsStore returns a new SettingsStore using the global database
+// handle.
+func NewSettingsStore() SettingsStore {
+	return &settingsStore{}
+}
+
+func (*settingsStore) CreateAccessToken(ctx gocontext.Context, userID int64, name string) (*database.AccessToken, error) {
+	return database.Handle.AccessTokens().Create(ctx, userID, name)
+}
+
+func (*settingsStore) GetAccessTokenBySHA1(ctx gocontext.Context, sha1 string) (*database.AccessToken, error) {
+	return database.Handle.AccessTokens().GetBySHA1(ctx, sha1)
+}
+
+func (*settingsStore) TouchAccessTokenByID(ctx gocontext.Context, id int64) error {
+	return database.Handle.AccessTokens().Touch(ctx, id)
+}
+
+func (*settingsStore) ListAccessTokens(ctx gocontext.Context, userID int64) ([]*database.AccessToken, error) {
+	return database.Handle.AccessTokens().List(ctx, userID)
+}
+
+func (*settingsStore) DeleteAccessTokenByID(ctx gocontext.Context, userID, id int64) error {
+	return database.Handle.AccessTokens().DeleteByID(ctx, userID, id)
+}

+ 3 - 1
mockgen.yaml

@@ -39,6 +39,8 @@ mocks:
           - LFSStore
           - UsersStore
           - TwoFactorsStore
-          - AccessTokensStore
           - ReposStore
           - PermsStore
+      - path: gogs.io/gogs/internal/route/lfs
+        interfaces:
+          - Store