diff --git a/src/api/artifact/abstractor/abstractor_test.go b/src/api/artifact/abstractor/abstractor_test.go
index 235a4461a..972a41fb2 100644
--- a/src/api/artifact/abstractor/abstractor_test.go
+++ b/src/api/artifact/abstractor/abstractor_test.go
@@ -21,8 +21,8 @@ import (
 	"github.com/goharbor/harbor/src/api/artifact/abstractor/resolver"
 	"github.com/goharbor/harbor/src/common/models"
 	"github.com/goharbor/harbor/src/pkg/artifact"
-	htesting "github.com/goharbor/harbor/src/testing"
 	"github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob"
+	repotesting "github.com/goharbor/harbor/src/testing/pkg/repository"
 	v1 "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/stretchr/testify/suite"
 	"testing"
@@ -212,7 +212,7 @@ type abstractorTestSuite struct {
 	suite.Suite
 	abstractor Abstractor
 	fetcher    *blob.FakeFetcher
-	repoMgr    *htesting.FakeRepositoryManager
+	repoMgr    *repotesting.FakeManager
 }
 
 func (a *abstractorTestSuite) SetupSuite() {
@@ -222,7 +222,7 @@ func (a *abstractorTestSuite) SetupSuite() {
 
 func (a *abstractorTestSuite) SetupTest() {
 	a.fetcher = &blob.FakeFetcher{}
-	a.repoMgr = &htesting.FakeRepositoryManager{}
+	a.repoMgr = &repotesting.FakeManager{}
 	a.abstractor = &abstractor{
 		repoMgr:     a.repoMgr,
 		blobFetcher: a.fetcher,
diff --git a/src/api/artifact/abstractor/blob/fetcher.go b/src/api/artifact/abstractor/blob/fetcher.go
index b03de9c80..8f3f2d43e 100644
--- a/src/api/artifact/abstractor/blob/fetcher.go
+++ b/src/api/artifact/abstractor/blob/fetcher.go
@@ -21,7 +21,6 @@ import (
 	"github.com/goharbor/harbor/src/common/utils/registry"
 	"github.com/goharbor/harbor/src/common/utils/registry/auth"
 	"github.com/goharbor/harbor/src/core/config"
-	coreutils "github.com/goharbor/harbor/src/core/utils"
 	v1 "github.com/opencontainers/image-spec/specs-go/v1"
 	"io/ioutil"
 	"net/http"
@@ -69,7 +68,7 @@ func (f *fetcher) FetchManifest(repository, digest string) (string, []byte, erro
 // TODO re-implement it based on OCI registry driver
 func (f *fetcher) FetchLayer(repository, digest string) ([]byte, error) {
 	// TODO read from cache first
-	client, err := coreutils.NewRepositoryClientForLocal("admin", repository)
+	client, err := newRepositoryClient(repository)
 	if err != nil {
 		return nil, err
 	}
diff --git a/src/api/artifact/abstractor/resolver/chart/chart_test.go b/src/api/artifact/abstractor/resolver/chart/chart_test.go
index 6171084b4..9cf1f91a9 100644
--- a/src/api/artifact/abstractor/resolver/chart/chart_test.go
+++ b/src/api/artifact/abstractor/resolver/chart/chart_test.go
@@ -17,8 +17,8 @@ package chart
 import (
 	"github.com/goharbor/harbor/src/common/models"
 	"github.com/goharbor/harbor/src/pkg/artifact"
-	htesting "github.com/goharbor/harbor/src/testing"
 	"github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob"
+	"github.com/goharbor/harbor/src/testing/pkg/repository"
 	"github.com/stretchr/testify/suite"
 	"testing"
 )
@@ -26,12 +26,12 @@ import (
 type resolverTestSuite struct {
 	suite.Suite
 	resolver    *resolver
-	repoMgr     *htesting.FakeRepositoryManager
+	repoMgr     *repository.FakeManager
 	blobFetcher *blob.FakeFetcher
 }
 
 func (r *resolverTestSuite) SetupTest() {
-	r.repoMgr = &htesting.FakeRepositoryManager{}
+	r.repoMgr = &repository.FakeManager{}
 	r.blobFetcher = &blob.FakeFetcher{}
 	r.resolver = &resolver{
 		repoMgr:     r.repoMgr,
diff --git a/src/api/artifact/abstractor/resolver/image/index_test.go b/src/api/artifact/abstractor/resolver/image/index_test.go
index aae67dc30..292ff76d9 100644
--- a/src/api/artifact/abstractor/resolver/image/index_test.go
+++ b/src/api/artifact/abstractor/resolver/image/index_test.go
@@ -16,7 +16,7 @@ package image
 
 import (
 	"github.com/goharbor/harbor/src/pkg/artifact"
-	htesting "github.com/goharbor/harbor/src/testing"
+	arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact"
 	"github.com/stretchr/testify/suite"
 	"testing"
 )
@@ -24,11 +24,11 @@ import (
 type indexResolverTestSuite struct {
 	suite.Suite
 	resolver *indexResolver
-	artMgr   *htesting.FakeArtifactManager
+	artMgr   *arttesting.FakeManager
 }
 
 func (i *indexResolverTestSuite) SetupTest() {
-	i.artMgr = &htesting.FakeArtifactManager{}
+	i.artMgr = &arttesting.FakeManager{}
 	i.resolver = &indexResolver{
 		artMgr: i.artMgr,
 	}
diff --git a/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go b/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go
index 41c48a9b9..f90348392 100644
--- a/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go
+++ b/src/api/artifact/abstractor/resolver/image/manifest_v2_test.go
@@ -17,8 +17,8 @@ package image
 import (
 	"github.com/goharbor/harbor/src/common/models"
 	"github.com/goharbor/harbor/src/pkg/artifact"
-	htesting "github.com/goharbor/harbor/src/testing"
 	"github.com/goharbor/harbor/src/testing/api/artifact/abstractor/blob"
+	"github.com/goharbor/harbor/src/testing/pkg/repository"
 	"github.com/stretchr/testify/suite"
 	"testing"
 )
@@ -26,12 +26,12 @@ import (
 type manifestV2ResolverTestSuite struct {
 	suite.Suite
 	resolver    *manifestV2Resolver
-	repoMgr     *htesting.FakeRepositoryManager
+	repoMgr     *repository.FakeManager
 	blobFetcher *blob.FakeFetcher
 }
 
 func (m *manifestV2ResolverTestSuite) SetupTest() {
-	m.repoMgr = &htesting.FakeRepositoryManager{}
+	m.repoMgr = &repository.FakeManager{}
 	m.blobFetcher = &blob.FakeFetcher{}
 	m.resolver = &manifestV2Resolver{
 		repoMgr:     m.repoMgr,
diff --git a/src/api/artifact/controller_test.go b/src/api/artifact/controller_test.go
index 8148b51fd..d1a018de6 100644
--- a/src/api/artifact/controller_test.go
+++ b/src/api/artifact/controller_test.go
@@ -21,7 +21,9 @@ import (
 	"github.com/goharbor/harbor/src/pkg/artifact"
 	"github.com/goharbor/harbor/src/pkg/q"
 	"github.com/goharbor/harbor/src/pkg/tag/model/tag"
-	htesting "github.com/goharbor/harbor/src/testing"
+	arttesting "github.com/goharbor/harbor/src/testing/pkg/artifact"
+	repotesting "github.com/goharbor/harbor/src/testing/pkg/repository"
+	tagtesting "github.com/goharbor/harbor/src/testing/pkg/tag"
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/suite"
 	"testing"
@@ -40,16 +42,16 @@ func (f *fakeAbstractor) Abstract(ctx context.Context, artifact *artifact.Artifa
 type controllerTestSuite struct {
 	suite.Suite
 	ctl        *controller
-	repoMgr    *htesting.FakeRepositoryManager
-	artMgr     *htesting.FakeArtifactManager
-	tagMgr     *htesting.FakeTagManager
+	repoMgr    *repotesting.FakeManager
+	artMgr     *arttesting.FakeManager
+	tagMgr     *tagtesting.FakeManager
 	abstractor *fakeAbstractor
 }
 
 func (c *controllerTestSuite) SetupTest() {
-	c.repoMgr = &htesting.FakeRepositoryManager{}
-	c.artMgr = &htesting.FakeArtifactManager{}
-	c.tagMgr = &htesting.FakeTagManager{}
+	c.repoMgr = &repotesting.FakeManager{}
+	c.artMgr = &arttesting.FakeManager{}
+	c.tagMgr = &tagtesting.FakeManager{}
 	c.abstractor = &fakeAbstractor{}
 	c.ctl = &controller{
 		repoMgr:    c.repoMgr,
diff --git a/src/api/repository/controller.go b/src/api/repository/controller.go
index 8c8c82a36..abb416002 100644
--- a/src/api/repository/controller.go
+++ b/src/api/repository/controller.go
@@ -35,8 +35,12 @@ type Controller interface {
 	// The "name" should contain the namespace part. The "created" will be set as true
 	// when the repository is created
 	Ensure(ctx context.Context, name string) (created bool, id int64, err error)
+	// List repositories according to the query
+	List(ctx context.Context, query *q.Query) (total int64, repositories []*models.RepoRecord, err error)
 	// Get the repository specified by ID
 	Get(ctx context.Context, id int64) (repository *models.RepoRecord, err error)
+	// GetByName gets the repository specified by name
+	GetByName(ctx context.Context, name string) (repository *models.RepoRecord, err error)
 }
 
 // NewController creates an instance of the default repository controller
@@ -93,6 +97,14 @@ func (c *controller) Ensure(ctx context.Context, name string) (bool, int64, erro
 	return true, id, nil
 }
 
+func (c *controller) List(ctx context.Context, query *q.Query) (int64, []*models.RepoRecord, error) {
+	return c.repoMgr.List(ctx, query)
+}
+
 func (c *controller) Get(ctx context.Context, id int64) (*models.RepoRecord, error) {
 	return c.repoMgr.Get(ctx, id)
 }
+
+func (c *controller) GetByName(ctx context.Context, name string) (*models.RepoRecord, error) {
+	return c.repoMgr.GetByName(ctx, name)
+}
diff --git a/src/api/repository/controller_test.go b/src/api/repository/controller_test.go
index b18aea370..8e7fe8f1f 100644
--- a/src/api/repository/controller_test.go
+++ b/src/api/repository/controller_test.go
@@ -16,7 +16,8 @@ package repository
 
 import (
 	"github.com/goharbor/harbor/src/common/models"
-	htesting "github.com/goharbor/harbor/src/testing"
+	"github.com/goharbor/harbor/src/testing/pkg/project"
+	"github.com/goharbor/harbor/src/testing/pkg/repository"
 	"github.com/stretchr/testify/suite"
 	"testing"
 )
@@ -24,13 +25,13 @@ import (
 type controllerTestSuite struct {
 	suite.Suite
 	ctl     *controller
-	proMgr  *htesting.FakeProjectManager
-	repoMgr *htesting.FakeRepositoryManager
+	proMgr  *project.FakeManager
+	repoMgr *repository.FakeManager
 }
 
 func (c *controllerTestSuite) SetupTest() {
-	c.proMgr = &htesting.FakeProjectManager{}
-	c.repoMgr = &htesting.FakeRepositoryManager{}
+	c.proMgr = &project.FakeManager{}
+	c.repoMgr = &repository.FakeManager{}
 	c.ctl = &controller{
 		proMgr:  c.proMgr,
 		repoMgr: c.repoMgr,
@@ -69,6 +70,20 @@ func (c *controllerTestSuite) TestEnsure() {
 	c.Equal(int64(1), id)
 }
 
+func (c *controllerTestSuite) TestList() {
+	c.repoMgr.On("List").Return(1, []*models.RepoRecord{
+		{
+			RepositoryID: 1,
+		},
+	}, nil)
+	total, repositories, err := c.ctl.List(nil, nil)
+	c.Require().Nil(err)
+	c.repoMgr.AssertExpectations(c.T())
+	c.Equal(int64(1), total)
+	c.Require().Len(repositories, 1)
+	c.Equal(int64(1), repositories[0].RepositoryID)
+}
+
 func (c *controllerTestSuite) TestGet() {
 	c.repoMgr.On("Get").Return(&models.RepoRecord{
 		RepositoryID: 1,
@@ -79,6 +94,16 @@ func (c *controllerTestSuite) TestGet() {
 	c.Equal(int64(1), repository.RepositoryID)
 }
 
+func (c *controllerTestSuite) TestGetByName() {
+	c.repoMgr.On("GetByName").Return(&models.RepoRecord{
+		RepositoryID: 1,
+	}, nil)
+	repository, err := c.ctl.GetByName(nil, "library/hello-world")
+	c.Require().Nil(err)
+	c.repoMgr.AssertExpectations(c.T())
+	c.Equal(int64(1), repository.RepositoryID)
+}
+
 func TestControllerTestSuite(t *testing.T) {
 	suite.Run(t, &controllerTestSuite{})
 }
diff --git a/src/core/config/config.go b/src/core/config/config.go
index 2b5f945c3..e40d7a7c8 100755
--- a/src/core/config/config.go
+++ b/src/core/config/config.go
@@ -217,7 +217,11 @@ func SelfRegistration() (bool, error) {
 
 // RegistryURL ...
 func RegistryURL() (string, error) {
-	return cfgMgr.Get(common.RegistryURL).GetString(), nil
+	url := os.Getenv("REGISTRY_URL")
+	if len(url) == 0 {
+		url = "http://registry:5000"
+	}
+	return url, nil
 }
 
 // InternalJobServiceURL returns jobservice URL for internal communication between Harbor containers
diff --git a/src/go.mod b/src/go.mod
index f5873c1c7..7a7cf51ce 100644
--- a/src/go.mod
+++ b/src/go.mod
@@ -76,6 +76,7 @@ require (
 	github.com/stretchr/testify v1.4.0
 	github.com/theupdateframework/notary v0.6.1
 	golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56
+	golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297
 	golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421
 	gopkg.in/asn1-ber.v1 v1.0.0-20150924051756-4e86f4367175 // indirect
 	gopkg.in/dancannon/gorethink.v3 v3.0.5 // indirect
diff --git a/src/pkg/retention/controller_test.go b/src/pkg/retention/controller_test.go
index 5c28a7d94..761958b68 100644
--- a/src/pkg/retention/controller_test.go
+++ b/src/pkg/retention/controller_test.go
@@ -1,14 +1,13 @@
 package retention
 
 import (
-	htesting "github.com/goharbor/harbor/src/testing"
-	"strings"
-	"testing"
-
 	"github.com/goharbor/harbor/src/pkg/retention/dep"
 	"github.com/goharbor/harbor/src/pkg/retention/policy"
 	"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
+	"github.com/goharbor/harbor/src/testing/pkg/repository"
 	"github.com/stretchr/testify/suite"
+	"strings"
+	"testing"
 )
 
 type ControllerTestSuite struct {
@@ -29,7 +28,7 @@ func TestController(t *testing.T) {
 
 func (s *ControllerTestSuite) TestPolicy() {
 	projectMgr := &fakeProjectManager{}
-	repositoryMgr := &htesting.FakeRepositoryManager{}
+	repositoryMgr := &repository.FakeManager{}
 	retentionScheduler := &fakeRetentionScheduler{}
 	retentionLauncher := &fakeLauncher{}
 	retentionMgr := NewManager()
@@ -127,7 +126,7 @@ func (s *ControllerTestSuite) TestPolicy() {
 
 func (s *ControllerTestSuite) TestExecution() {
 	projectMgr := &fakeProjectManager{}
-	repositoryMgr := &htesting.FakeRepositoryManager{}
+	repositoryMgr := &repository.FakeManager{}
 	retentionScheduler := &fakeRetentionScheduler{}
 	retentionLauncher := &fakeLauncher{}
 	retentionMgr := NewManager()
diff --git a/src/pkg/retention/launcher_test.go b/src/pkg/retention/launcher_test.go
index 7579129ed..5736689de 100644
--- a/src/pkg/retention/launcher_test.go
+++ b/src/pkg/retention/launcher_test.go
@@ -16,9 +16,6 @@ package retention
 
 import (
 	"fmt"
-	htesting "github.com/goharbor/harbor/src/testing"
-	"testing"
-
 	"github.com/goharbor/harbor/src/common/job"
 	"github.com/goharbor/harbor/src/common/models"
 	_ "github.com/goharbor/harbor/src/pkg/art/selectors/doublestar"
@@ -27,9 +24,11 @@ import (
 	"github.com/goharbor/harbor/src/pkg/retention/policy/rule"
 	"github.com/goharbor/harbor/src/pkg/retention/q"
 	hjob "github.com/goharbor/harbor/src/testing/job"
+	"github.com/goharbor/harbor/src/testing/pkg/repository"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
+	"testing"
 )
 
 type fakeProjectManager struct {
@@ -132,7 +131,7 @@ func (f *fakeRetentionManager) ListHistories(executionID int64, query *q.Query)
 type launchTestSuite struct {
 	suite.Suite
 	projectMgr       project.Manager
-	repositoryMgr    *htesting.FakeRepositoryManager
+	repositoryMgr    *repository.FakeManager
 	retentionMgr     Manager
 	jobserviceClient job.Client
 }
@@ -150,7 +149,7 @@ func (l *launchTestSuite) SetupTest() {
 		projects: []*models.Project{
 			pro1, pro2,
 		}}
-	l.repositoryMgr = &htesting.FakeRepositoryManager{}
+	l.repositoryMgr = &repository.FakeManager{}
 	l.retentionMgr = &fakeRetentionManager{}
 	l.jobserviceClient = &hjob.MockJobClient{
 		JobUUID: []string{"1"},
diff --git a/src/pkg/scheduler/hook/handler_test.go b/src/pkg/scheduler/hook/handler_test.go
index 99875ae5f..f245aabe6 100644
--- a/src/pkg/scheduler/hook/handler_test.go
+++ b/src/pkg/scheduler/hook/handler_test.go
@@ -15,16 +15,15 @@
 package hook
 
 import (
-	"testing"
-
 	"github.com/goharbor/harbor/src/pkg/scheduler"
 	"github.com/goharbor/harbor/src/pkg/scheduler/model"
-	htesting "github.com/goharbor/harbor/src/testing"
+	schedulertesting "github.com/goharbor/harbor/src/testing/pkg/scheduler"
 	"github.com/stretchr/testify/require"
+	"testing"
 )
 
 var h = &controller{
-	manager: &htesting.FakeSchedulerManager{},
+	manager: &schedulertesting.FakeManager{},
 }
 
 func TestUpdateStatus(t *testing.T) {
@@ -33,7 +32,7 @@ func TestUpdateStatus(t *testing.T) {
 	require.NotNil(t, err)
 
 	// pass
-	h.manager.(*htesting.FakeSchedulerManager).Schedules = []*model.Schedule{
+	h.manager.(*schedulertesting.FakeManager).Schedules = []*model.Schedule{
 		{
 			ID:     1,
 			Status: "",
diff --git a/src/pkg/scheduler/scheduler_test.go b/src/pkg/scheduler/scheduler_test.go
index de4bb2a2a..79e6dbbeb 100644
--- a/src/pkg/scheduler/scheduler_test.go
+++ b/src/pkg/scheduler/scheduler_test.go
@@ -15,13 +15,12 @@
 package scheduler
 
 import (
-	"testing"
-
-	htesting "github.com/goharbor/harbor/src/testing"
 	"github.com/goharbor/harbor/src/testing/job"
+	schedulertesting "github.com/goharbor/harbor/src/testing/pkg/scheduler"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/stretchr/testify/suite"
+	"testing"
 )
 
 var sch *scheduler
@@ -41,7 +40,7 @@ func (s *schedulerTestSuite) SetupTest() {
 	// recreate the scheduler object
 	sch = &scheduler{
 		jobserviceClient: &job.MockJobClient{},
-		manager:          &htesting.FakeSchedulerManager{},
+		manager:          &schedulertesting.FakeManager{},
 	}
 }
 
diff --git a/src/server/registry/blob/blob.go b/src/server/registry/blob/blob.go
deleted file mode 100644
index f963c4621..000000000
--- a/src/server/registry/blob/blob.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package blob
-
-import (
-	"net/http"
-	"net/http/httputil"
-)
-
-// NewHandler returns the handler to handler catalog request
-func NewHandler(proxy *httputil.ReverseProxy) http.Handler {
-	return &handler{
-		proxy: proxy,
-	}
-}
-
-type handler struct {
-	proxy *httputil.ReverseProxy
-}
-
-// ServeHTTP ...
-func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
-	h.proxy.ServeHTTP(w, req)
-}
diff --git a/src/server/registry/blob/blob_test.go b/src/server/registry/blob/blob_test.go
deleted file mode 100644
index f3a8cab65..000000000
--- a/src/server/registry/blob/blob_test.go
+++ /dev/null
@@ -1 +0,0 @@
-package blob
diff --git a/src/server/registry/catalog/catalog.go b/src/server/registry/catalog.go
similarity index 79%
rename from src/server/registry/catalog/catalog.go
rename to src/server/registry/catalog.go
index 4e4560de2..e27af4269 100644
--- a/src/server/registry/catalog/catalog.go
+++ b/src/server/registry/catalog.go
@@ -12,13 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package catalog
+package registry
 
 import (
 	"encoding/json"
 	"fmt"
+	"github.com/goharbor/harbor/src/api/repository"
 	ierror "github.com/goharbor/harbor/src/internal/error"
-	"github.com/goharbor/harbor/src/pkg/repository"
 	reg_error "github.com/goharbor/harbor/src/server/registry/error"
 	"github.com/goharbor/harbor/src/server/registry/util"
 	"net/http"
@@ -26,26 +26,17 @@ import (
 	"strconv"
 )
 
-// NewHandler returns the handler to handler catalog request
-func NewHandler(repoMgr repository.Manager) http.Handler {
-	return &handler{
-		repoMgr: repoMgr,
+func newRepositoryHandler() http.Handler {
+	return &repositoryHandler{
+		repoCtl: repository.Ctl,
 	}
 }
 
-type handler struct {
-	repoMgr repository.Manager
+type repositoryHandler struct {
+	repoCtl repository.Controller
 }
 
-// ServeHTTP ...
-func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
-	switch req.Method {
-	case http.MethodGet:
-		h.get(w, req)
-	}
-}
-
-func (h *handler) get(w http.ResponseWriter, req *http.Request) {
+func (r *repositoryHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	var maxEntries int
 	var err error
 
@@ -64,13 +55,13 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) {
 	var repoNames []string
 	// get all repositories
 	// ToDo filter out the untagged repos
-	total, repoRecords, err := h.repoMgr.List(req.Context(), nil)
+	total, repoRecords, err := r.repoCtl.List(req.Context(), nil)
 	if err != nil {
 		reg_error.Handle(w, req, err)
 		return
 	}
 	if total <= 0 {
-		h.sendResponse(w, req, repoNames)
+		r.sendResponse(w, req, repoNames)
 		return
 	}
 	for _, r := range repoRecords {
@@ -78,7 +69,7 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) {
 	}
 	sort.Strings(repoNames)
 	if !withN {
-		h.sendResponse(w, req, repoNames)
+		r.sendResponse(w, req, repoNames)
 		return
 	}
 
@@ -109,12 +100,12 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) {
 		w.Header().Set("Link", urlStr)
 	}
 
-	h.sendResponse(w, req, resRepos)
+	r.sendResponse(w, req, resRepos)
 	return
 }
 
 // sendResponse ...
-func (h *handler) sendResponse(w http.ResponseWriter, req *http.Request, repositoryNames []string) {
+func (r *repositoryHandler) sendResponse(w http.ResponseWriter, req *http.Request, repositoryNames []string) {
 	w.Header().Set("Content-Type", "application/json; charset=utf-8")
 	enc := json.NewEncoder(w)
 	if err := enc.Encode(catalogAPIResponse{
diff --git a/src/server/registry/catalog/catalog_test.go b/src/server/registry/catalog/catalog_test.go
deleted file mode 100644
index e571e24c6..000000000
--- a/src/server/registry/catalog/catalog_test.go
+++ /dev/null
@@ -1 +0,0 @@
-package catalog
diff --git a/src/server/registry/manifest/manifest_test.go b/src/server/registry/catalog_test.go
similarity index 97%
rename from src/server/registry/manifest/manifest_test.go
rename to src/server/registry/catalog_test.go
index b15e65d5f..172fb4596 100644
--- a/src/server/registry/manifest/manifest_test.go
+++ b/src/server/registry/catalog_test.go
@@ -12,6 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package manifest
+package registry
 
 // TODO
diff --git a/src/server/registry/handler.go b/src/server/registry/handler.go
deleted file mode 100644
index c5f875e3e..000000000
--- a/src/server/registry/handler.go
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright Project Harbor Authors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//    http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package registry
-
-import (
-	"github.com/goharbor/harbor/src/server/middleware/contenttrust"
-	"github.com/goharbor/harbor/src/server/middleware/vulnerable"
-
-	"github.com/goharbor/harbor/src/core/config"
-	pkg_repo "github.com/goharbor/harbor/src/pkg/repository"
-	pkg_tag "github.com/goharbor/harbor/src/pkg/tag"
-	"github.com/goharbor/harbor/src/server/middleware"
-	"github.com/goharbor/harbor/src/server/middleware/immutable"
-	"github.com/goharbor/harbor/src/server/middleware/manifestinfo"
-	"github.com/goharbor/harbor/src/server/middleware/readonly"
-	"github.com/goharbor/harbor/src/server/middleware/regtoken"
-	"github.com/goharbor/harbor/src/server/registry/blob"
-	"github.com/goharbor/harbor/src/server/registry/catalog"
-	"github.com/goharbor/harbor/src/server/registry/manifest"
-	"github.com/goharbor/harbor/src/server/registry/tag"
-	"github.com/gorilla/mux"
-	"net/http"
-	"net/http/httputil"
-	"net/url"
-)
-
-// New return the registry instance to handle the registry APIs
-func New(url *url.URL) http.Handler {
-	// TODO customize the reverse proxy to improve the performance?
-	proxy := httputil.NewSingleHostReverseProxy(url)
-	proxy.Director = basicAuthDirector(proxy.Director)
-
-	// create the root rooter
-	rootRouter := mux.NewRouter()
-	rootRouter.StrictSlash(true)
-
-	// handle catalog
-	rootRouter.Path("/v2/_catalog").Methods(http.MethodGet).Handler(catalog.NewHandler(pkg_repo.Mgr))
-
-	// handle list tag
-	rootRouter.Path("/v2/{name:.*}/tags/list").Methods(http.MethodGet).Handler(tag.NewHandler(pkg_repo.Mgr, pkg_tag.Mgr))
-
-	// handle manifest
-	// TODO maybe we should split it into several sub routers based on the method
-	manifestRouter := rootRouter.Path("/v2/{name:.*}/manifests/{reference}").Subrouter()
-	manifestRouter.NewRoute().Methods(http.MethodGet).Handler(middleware.WithMiddlewares(manifest.NewHandler(proxy), manifestinfo.Middleware(), regtoken.Middleware(), contenttrust.Middleware(), vulnerable.Middleware()))
-	manifestRouter.NewRoute().Methods(http.MethodHead).Handler(manifest.NewHandler(proxy))
-	manifestRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(manifest.NewHandler(proxy), readonly.Middleware(), manifestinfo.Middleware(), immutable.MiddlewareDelete()))
-
-	// handle blob
-	// as we need to apply middleware to the blob requests, so create a sub router to handle the blob APIs
-	blobRouter := rootRouter.PathPrefix("/v2/{name:.*}/blobs/").Subrouter()
-	blobRouter.NewRoute().Methods(http.MethodGet).Handler(blob.NewHandler(proxy))
-	blobRouter.NewRoute().Methods(http.MethodHead).Handler(blob.NewHandler(proxy))
-	blobRouter.NewRoute().Methods(http.MethodPost).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware()))
-	blobRouter.NewRoute().Methods(http.MethodPut).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware()))
-	blobRouter.NewRoute().Methods(http.MethodPatch).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware()))
-	blobRouter.NewRoute().Methods(http.MethodDelete).Handler(middleware.WithMiddlewares(blob.NewHandler(proxy), readonly.Middleware()))
-
-	// all other APIs are proxy to the backend docker registry
-	rootRouter.PathPrefix("/").Handler(proxy)
-
-	// register middlewares
-	// TODO add auth middleware
-	// TODO apply the existing middlewares
-	// rootRouter.Use(mux.MiddlewareFunc(middleware))
-
-	return rootRouter
-}
-
-func basicAuthDirector(d func(*http.Request)) func(*http.Request) {
-	return func(r *http.Request) {
-		d(r)
-		if r != nil && !middleware.SkipInjectRegistryCred(r.Context()) {
-			u, p := config.RegistryCredential()
-			r.SetBasicAuth(u, p)
-		}
-	}
-}
diff --git a/src/server/registry/manifest/manifest.go b/src/server/registry/manifest.go
similarity index 61%
rename from src/server/registry/manifest/manifest.go
rename to src/server/registry/manifest.go
index 935c88e76..cdc63d6aa 100644
--- a/src/server/registry/manifest/manifest.go
+++ b/src/server/registry/manifest.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package manifest
+package registry
 
 import (
 	"github.com/goharbor/harbor/src/api/artifact"
@@ -21,45 +21,16 @@ import (
 	"github.com/goharbor/harbor/src/internal"
 	"github.com/goharbor/harbor/src/server/registry/error"
 	"github.com/goharbor/harbor/src/server/router"
-	"github.com/gorilla/mux"
 	"github.com/opencontainers/go-digest"
 	"net/http"
-	"net/http/httputil"
 	"strings"
 )
 
-// NewHandler returns the handler to handler manifest requests
-func NewHandler(proxy *httputil.ReverseProxy) http.Handler {
-	return &handler{
-		repoCtl: repository.Ctl,
-		artCtl:  artifact.Ctl,
-		proxy:   proxy,
-	}
-}
-
-type handler struct {
-	repoCtl repository.Controller
-	artCtl  artifact.Controller
-	proxy   *httputil.ReverseProxy
-}
-
-func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
-	switch req.Method {
-	case http.MethodHead, http.MethodGet:
-		h.get(w, req)
-	case http.MethodDelete:
-		h.delete(w, req)
-	case http.MethodPut:
-		h.put(w, req)
-	}
-}
-
 // make sure the artifact exist before proxying the request to the backend registry
-func (h *handler) get(w http.ResponseWriter, req *http.Request) {
-	// check the existence in the database first
-	vars := mux.Vars(req)
-	reference := vars["reference"]
-	artifact, err := h.artCtl.GetByReference(req.Context(), vars["name"], reference, nil)
+func getManifest(w http.ResponseWriter, req *http.Request) {
+	repository := router.Param(req.Context(), ":splat")
+	reference := router.Param(req.Context(), ":reference")
+	artifact, err := artifact.Ctl.GetByReference(req.Context(), repository, reference, nil)
 	if err != nil {
 		error.Handle(w, req, err)
 		return
@@ -69,23 +40,23 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) {
 	if _, err = digest.Parse(reference); err != nil {
 		req = req.Clone(req.Context())
 		req.URL.Path = strings.TrimSuffix(req.URL.Path, reference) + artifact.Digest
-		req.URL.RawPath = ""
 		req.URL.RawPath = req.URL.EscapedPath()
 	}
-	h.proxy.ServeHTTP(w, req)
+	proxy.ServeHTTP(w, req)
 
 	// TODO fire event(only for GET method), add access log in the event handler
 }
 
-func (h *handler) delete(w http.ResponseWriter, req *http.Request) {
-	// just delete the artifact from database
-	vars := mux.Vars(req)
-	artifact, err := h.artCtl.GetByReference(req.Context(), vars["name"], vars["reference"], nil)
+// just delete the artifact from database
+func deleteManifest(w http.ResponseWriter, req *http.Request) {
+	repository := router.Param(req.Context(), ":splat")
+	reference := router.Param(req.Context(), ":reference")
+	art, err := artifact.Ctl.GetByReference(req.Context(), repository, reference, nil)
 	if err != nil {
 		error.Handle(w, req, err)
 		return
 	}
-	if err = h.artCtl.Delete(req.Context(), artifact.ID); err != nil {
+	if err = artifact.Ctl.Delete(req.Context(), art.ID); err != nil {
 		error.Handle(w, req, err)
 		return
 	}
@@ -94,20 +65,12 @@ func (h *handler) delete(w http.ResponseWriter, req *http.Request) {
 	// TODO fire event, add access log in the event handler
 }
 
-func (h *handler) put(w http.ResponseWriter, req *http.Request) {
-	repository, err := router.Param(req.Context(), ":splat")
-	if err != nil {
-		error.Handle(w, req, err)
-		return
-	}
-	reference, err := router.Param(req.Context(), ":reference")
-	if err != nil {
-		error.Handle(w, req, err)
-		return
-	}
+func putManifest(w http.ResponseWriter, req *http.Request) {
+	repo := router.Param(req.Context(), ":splat")
+	reference := router.Param(req.Context(), ":reference")
 
 	// make sure the repository exist before pushing the manifest
-	_, repositoryID, err := h.repoCtl.Ensure(req.Context(), repository)
+	_, repositoryID, err := repository.Ctl.Ensure(req.Context(), repo)
 	if err != nil {
 		error.Handle(w, req, err)
 		return
@@ -115,7 +78,7 @@ func (h *handler) put(w http.ResponseWriter, req *http.Request) {
 
 	buffer := internal.NewResponseBuffer(w)
 	// proxy the req to the backend docker registry
-	h.proxy.ServeHTTP(buffer, req)
+	proxy.ServeHTTP(buffer, req)
 	if !buffer.Success() {
 		if _, err := buffer.Flush(); err != nil {
 			log.Errorf("failed to flush: %v", err)
@@ -135,7 +98,7 @@ func (h *handler) put(w http.ResponseWriter, req *http.Request) {
 		tags = append(tags, reference)
 	}
 
-	_, _, err = h.artCtl.Ensure(req.Context(), repositoryID, dgt, tags...)
+	_, _, err = artifact.Ctl.Ensure(req.Context(), repositoryID, dgt, tags...)
 	if err != nil {
 		error.Handle(w, req, err)
 		return
diff --git a/src/server/registry/manifest_test.go b/src/server/registry/manifest_test.go
new file mode 100644
index 000000000..cc1cf314d
--- /dev/null
+++ b/src/server/registry/manifest_test.go
@@ -0,0 +1,162 @@
+// Copyright Project Harbor Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry
+
+import (
+	"github.com/goharbor/harbor/src/api/artifact"
+	"github.com/goharbor/harbor/src/api/repository"
+	ierror "github.com/goharbor/harbor/src/internal/error"
+	arttesting "github.com/goharbor/harbor/src/testing/api/artifact"
+	repotesting "github.com/goharbor/harbor/src/testing/api/repository"
+	"github.com/stretchr/testify/suite"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+)
+
+type manifestTestSuite struct {
+	suite.Suite
+	originalRepoCtl repository.Controller
+	originalArtCtl  artifact.Controller
+	originalProxy   http.Handler
+	repoCtl         *repotesting.FakeController
+	artCtl          *arttesting.FakeController
+}
+
+func (m *manifestTestSuite) SetupSuite() {
+	m.originalRepoCtl = repository.Ctl
+	m.originalArtCtl = artifact.Ctl
+	m.originalProxy = proxy
+}
+
+func (m *manifestTestSuite) SetupTest() {
+	m.repoCtl = &repotesting.FakeController{}
+	m.artCtl = &arttesting.FakeController{}
+	repository.Ctl = m.repoCtl
+	artifact.Ctl = m.artCtl
+}
+
+func (m *manifestTestSuite) TearDownTest() {
+	proxy = nil
+}
+
+func (m *manifestTestSuite) TearDownSuite() {
+	repository.Ctl = m.originalRepoCtl
+	artifact.Ctl = m.originalArtCtl
+	proxy = m.originalProxy
+}
+
+func (m *manifestTestSuite) TestGetManifest() {
+	// doesn't exist
+	req := httptest.NewRequest(http.MethodGet, "/v2/library/hello-world/manifests/latest", nil)
+	w := &httptest.ResponseRecorder{}
+
+	m.artCtl.On("GetByReference").Return(nil, ierror.New(nil).WithCode(ierror.NotFoundCode))
+	getManifest(w, req)
+	m.Equal(http.StatusNotFound, w.Code)
+
+	// reset the mock
+	m.SetupTest()
+
+	// exist
+	proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		if req.Method == http.MethodGet && req.URL.Path == "/v2/library/hello-world/manifests/sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180" {
+			w.WriteHeader(http.StatusOK)
+			return
+		}
+		w.WriteHeader(http.StatusNotFound)
+	})
+
+	// as we cannot set the beego input in the context, here the request doesn't carry reference part
+	req = httptest.NewRequest(http.MethodGet, "/v2/library/hello-world/manifests/", nil)
+	w = &httptest.ResponseRecorder{}
+
+	art := &artifact.Artifact{}
+	art.Digest = "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180"
+	m.artCtl.On("GetByReference").Return(art, nil)
+	getManifest(w, req)
+	m.Equal(http.StatusOK, w.Code)
+}
+
+func (m *manifestTestSuite) TestDeleteManifest() {
+	// doesn't exist
+	req := httptest.NewRequest(http.MethodDelete, "/v2/library/hello-world/manifests/latest", nil)
+	w := &httptest.ResponseRecorder{}
+
+	m.artCtl.On("GetByReference").Return(nil, ierror.New(nil).WithCode(ierror.NotFoundCode))
+	deleteManifest(w, req)
+	m.Equal(http.StatusNotFound, w.Code)
+
+	// reset the mock
+	m.SetupTest()
+
+	// reset the mock
+	m.SetupTest()
+
+	// exist
+	proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		if req.Method == http.MethodPut && req.URL.Path == "/v2/library/hello-world/manifests/latest" {
+			w.WriteHeader(http.StatusInternalServerError)
+			w.Header().Set("Docker-Content-Digest", "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180")
+			return
+		}
+		w.WriteHeader(http.StatusOK)
+	})
+	req = httptest.NewRequest(http.MethodDelete, "/v2/library/hello-world/manifests/latest", nil)
+	w = &httptest.ResponseRecorder{}
+	m.artCtl.On("GetByReference").Return(&artifact.Artifact{}, nil)
+	m.artCtl.On("Delete").Return(nil)
+	deleteManifest(w, req)
+	m.Equal(http.StatusAccepted, w.Code)
+}
+
+func (m *manifestTestSuite) TestPutManifest() {
+	// the backend registry response with 500
+	proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		if req.Method == http.MethodPut && req.URL.Path == "/v2/library/hello-world/manifests/latest" {
+			w.WriteHeader(http.StatusInternalServerError)
+			return
+		}
+		w.WriteHeader(http.StatusNotFound)
+	})
+	req := httptest.NewRequest(http.MethodPut, "/v2/library/hello-world/manifests/latest", nil)
+	w := &httptest.ResponseRecorder{}
+	m.repoCtl.On("Ensure").Return(false, 1, nil)
+	putManifest(w, req)
+	m.Equal(http.StatusInternalServerError, w.Code)
+
+	// reset the mock
+	m.SetupTest()
+
+	// // the backend registry serves the request successfully
+	proxy = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+		if req.Method == http.MethodPut && req.URL.Path == "/v2/library/hello-world/manifests/latest" {
+			w.Header().Set("Docker-Content-Digest", "sha256:418fb88ec412e340cdbef913b8ca1bbe8f9e8dc705f9617414c1f2c8db980180")
+			w.WriteHeader(http.StatusCreated)
+			return
+		}
+		w.WriteHeader(http.StatusNotFound)
+	})
+	req = httptest.NewRequest(http.MethodPut, "/v2/library/hello-world/manifests/latest", nil)
+	w = &httptest.ResponseRecorder{}
+	m.repoCtl.On("Ensure").Return(false, 1, nil)
+	m.artCtl.On("Ensure").Return(true, 1, nil)
+	putManifest(w, req)
+	m.Equal(http.StatusCreated, w.Code)
+}
+
+func TestManifestTestSuite(t *testing.T) {
+	suite.Run(t, &manifestTestSuite{})
+}
diff --git a/src/server/registry/proxy.go b/src/server/registry/proxy.go
new file mode 100644
index 000000000..8e68c330c
--- /dev/null
+++ b/src/server/registry/proxy.go
@@ -0,0 +1,47 @@
+// Copyright Project Harbor Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry
+
+import (
+	"fmt"
+	"github.com/goharbor/harbor/src/core/config"
+	"github.com/goharbor/harbor/src/server/middleware"
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+)
+
+var proxy = newProxy()
+
+func newProxy() http.Handler {
+	regURL, _ := config.RegistryURL()
+	url, err := url.Parse(regURL)
+	if err != nil {
+		panic(fmt.Sprintf("failed to parse the URL of registry: %v", err))
+	}
+	proxy := httputil.NewSingleHostReverseProxy(url)
+	proxy.Director = basicAuthDirector(proxy.Director)
+	return proxy
+}
+
+func basicAuthDirector(d func(*http.Request)) func(*http.Request) {
+	return func(r *http.Request) {
+		d(r)
+		if r != nil && !middleware.SkipInjectRegistryCred(r.Context()) {
+			u, p := config.RegistryCredential()
+			r.SetBasicAuth(u, p)
+		}
+	}
+}
diff --git a/src/server/registry/route.go b/src/server/registry/route.go
index bec79ee7e..5ea06a1d4 100644
--- a/src/server/registry/route.go
+++ b/src/server/registry/route.go
@@ -15,39 +15,70 @@
 package registry
 
 import (
-	"github.com/goharbor/harbor/src/core/config"
-	"github.com/goharbor/harbor/src/server/middleware/immutable"
-
 	"github.com/goharbor/harbor/src/server/middleware/artifactinfo"
+	"github.com/goharbor/harbor/src/server/middleware/contenttrust"
+	"github.com/goharbor/harbor/src/server/middleware/immutable"
 	"github.com/goharbor/harbor/src/server/middleware/manifestinfo"
 	"github.com/goharbor/harbor/src/server/middleware/readonly"
+	"github.com/goharbor/harbor/src/server/middleware/regtoken"
 	"github.com/goharbor/harbor/src/server/middleware/v2auth"
-	"github.com/goharbor/harbor/src/server/registry/manifest"
+	"github.com/goharbor/harbor/src/server/middleware/vulnerable"
 	"github.com/goharbor/harbor/src/server/router"
 	"net/http"
-	"net/http/httputil"
-	"net/url"
 )
 
 // RegisterRoutes for OCI registry APIs
 func RegisterRoutes() {
-	// TODO remove
-	regURL, _ := config.RegistryURL()
-	url, _ := url.Parse(regURL)
-	proxy := httputil.NewSingleHostReverseProxy(url)
-	proxy.Director = basicAuthDirector(proxy.Director)
-
-	router.NewRoute().Path("/v2/*").
+	root := router.NewRoute().
+		Path("/v2").
 		Middleware(artifactinfo.Middleware()).
-		Middleware(v2auth.Middleware()).
-		Handler(New(url))
-	router.NewRoute().
+		Middleware(v2auth.Middleware())
+	// catalog
+	root.NewRoute().
+		Method(http.MethodGet).
+		Path("/_catalog").
+		Handler(newRepositoryHandler())
+	// list tags
+	root.NewRoute().
+		Method(http.MethodGet).
+		Path("/*/tags/list").
+		Handler(newTagHandler())
+	// manifest
+	root.NewRoute().
+		Method(http.MethodGet).
+		Path("/*/manifests/:reference").
+		Middleware(manifestinfo.Middleware()).
+		Middleware(regtoken.Middleware()).
+		Middleware(contenttrust.Middleware()).
+		Middleware(vulnerable.Middleware()).
+		HandlerFunc(getManifest)
+	root.NewRoute().
+		Method(http.MethodHead).
+		Path("/*/manifests/:reference").
+		HandlerFunc(getManifest)
+	root.NewRoute().
+		Method(http.MethodDelete).
+		Path("/*/manifests/:reference").
+		Middleware(readonly.Middleware()).
+		Middleware(manifestinfo.Middleware()).
+		Middleware(immutable.MiddlewareDelete()).
+		HandlerFunc(deleteManifest)
+	root.NewRoute().
 		Method(http.MethodPut).
-		Path("/v2/*/manifests/:reference").
-		Middleware(artifactinfo.Middleware()).
-		Middleware(v2auth.Middleware()).
+		Path("/*/manifests/:reference").
 		Middleware(readonly.Middleware()).
 		Middleware(manifestinfo.Middleware()).
 		Middleware(immutable.MiddlewarePush()).
-		Handler(manifest.NewHandler(proxy))
+		HandlerFunc(putManifest)
+	// blob
+	root.NewRoute().
+		Method(http.MethodPost).
+		Method(http.MethodPut).
+		Method(http.MethodPatch).
+		Method(http.MethodDelete).
+		Path("/{name:.*}/blobs/").
+		Middleware(readonly.Middleware()).
+		Handler(proxy)
+	// others
+	root.NewRoute().Path("/*").Handler(proxy)
 }
diff --git a/src/server/registry/tag/tag.go b/src/server/registry/tag.go
similarity index 68%
rename from src/server/registry/tag/tag.go
rename to src/server/registry/tag.go
index 057614130..80744d59a 100644
--- a/src/server/registry/tag/tag.go
+++ b/src/server/registry/tag.go
@@ -12,46 +12,36 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package tag
+package registry
 
 import (
 	"encoding/json"
 	"fmt"
+	"github.com/goharbor/harbor/src/api/artifact"
+	"github.com/goharbor/harbor/src/api/repository"
 	ierror "github.com/goharbor/harbor/src/internal/error"
 	"github.com/goharbor/harbor/src/pkg/q"
-	"github.com/goharbor/harbor/src/pkg/repository"
-	"github.com/goharbor/harbor/src/pkg/tag"
 	reg_error "github.com/goharbor/harbor/src/server/registry/error"
 	"github.com/goharbor/harbor/src/server/registry/util"
-	"github.com/gorilla/mux"
+	"github.com/goharbor/harbor/src/server/router"
 	"net/http"
 	"sort"
 	"strconv"
 )
 
-// NewHandler returns the handler to handle listing tag request
-func NewHandler(repoMgr repository.Manager, tagMgr tag.Manager) http.Handler {
-	return &handler{
-		repoMgr: repoMgr,
-		tagMgr:  tagMgr,
+func newTagHandler() http.Handler {
+	return &tagHandler{
+		repoCtl: repository.Ctl,
+		artCtl:  artifact.Ctl,
 	}
 }
 
-type handler struct {
-	repoMgr repository.Manager
-	tagMgr  tag.Manager
-
+type tagHandler struct {
+	repoCtl        repository.Controller
+	artCtl         artifact.Controller
 	repositoryName string
 }
 
-// ServeHTTP ...
-func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
-	switch req.Method {
-	case http.MethodGet:
-		h.get(w, req)
-	}
-}
-
 // get return the list of tags
 
 // Content-Type: application/json
@@ -64,7 +54,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 //      ...
 //    ]
 // }
-func (h *handler) get(w http.ResponseWriter, req *http.Request) {
+func (t *tagHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	var maxEntries int
 	var err error
 
@@ -80,29 +70,26 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) {
 		}
 	}
 
-	var repoID int64
 	var tagNames []string
-	vars := mux.Vars(req)
-	h.repositoryName = vars["name"]
 
-	repoID, err = h.getRepoID(req)
+	t.repositoryName = router.Param(req.Context(), ":splat")
+	repository, err := t.repoCtl.GetByName(req.Context(), t.repositoryName)
 	if err != nil {
 		reg_error.Handle(w, req, err)
 		return
 	}
 
 	// get tags ...
-	total, tags, err := h.tagMgr.List(req.Context(), &q.Query{
+	total, tags, err := t.artCtl.Tags(req.Context(), &q.Query{
 		Keywords: map[string]interface{}{
-			"RepositoryID": repoID,
-		},
-	})
+			"RepositoryID": repository.RepositoryID,
+		}}, nil)
 	if err != nil {
 		reg_error.Handle(w, req, err)
 		return
 	}
 	if total == 0 {
-		h.sendResponse(w, req, tagNames)
+		t.sendResponse(w, req, tagNames)
 		return
 	}
 
@@ -111,7 +98,7 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) {
 	}
 	sort.Strings(tagNames)
 	if !withN {
-		h.sendResponse(w, req, tagNames)
+		t.sendResponse(w, req, tagNames)
 		return
 	}
 
@@ -141,33 +128,16 @@ func (h *handler) get(w http.ResponseWriter, req *http.Request) {
 		}
 		w.Header().Set("Link", urlStr)
 	}
-	h.sendResponse(w, req, resTags)
+	t.sendResponse(w, req, resTags)
 	return
 }
 
-// getRepoID ...
-func (h *handler) getRepoID(req *http.Request) (int64, error) {
-	total, repoRecord, err := h.repoMgr.List(req.Context(), &q.Query{
-		Keywords: map[string]interface{}{
-			"name": h.repositoryName,
-		},
-	})
-	if err != nil {
-		return 0, err
-	}
-	if total <= 0 {
-		err := ierror.New(nil).WithCode(ierror.NotFoundCode).WithMessage("repositoryNotFound")
-		return 0, err
-	}
-	return repoRecord[0].RepositoryID, nil
-}
-
 // sendResponse ...
-func (h *handler) sendResponse(w http.ResponseWriter, req *http.Request, tagNames []string) {
+func (t *tagHandler) sendResponse(w http.ResponseWriter, req *http.Request, tagNames []string) {
 	w.Header().Set("Content-Type", "application/json; charset=utf-8")
 	enc := json.NewEncoder(w)
 	if err := enc.Encode(tagsAPIResponse{
-		Name: h.repositoryName,
+		Name: t.repositoryName,
 		Tags: tagNames,
 	}); err != nil {
 		reg_error.Handle(w, req, err)
diff --git a/src/server/registry/tag/tag_test.go b/src/server/registry/tag/tag_test.go
deleted file mode 100644
index 686c0dfff..000000000
--- a/src/server/registry/tag/tag_test.go
+++ /dev/null
@@ -1 +0,0 @@
-package tag
diff --git a/src/server/registry/tag_test.go b/src/server/registry/tag_test.go
new file mode 100644
index 000000000..172fb4596
--- /dev/null
+++ b/src/server/registry/tag_test.go
@@ -0,0 +1,17 @@
+// Copyright Project Harbor Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry
+
+// TODO
diff --git a/src/server/router/router.go b/src/server/router/router.go
index 4afcdad18..73cd10383 100644
--- a/src/server/router/router.go
+++ b/src/server/router/router.go
@@ -16,11 +16,11 @@ package router
 
 import (
 	"context"
-	"errors"
 	"github.com/astaxie/beego"
 	beegocontext "github.com/astaxie/beego/context"
 	"github.com/goharbor/harbor/src/server/middleware"
 	"net/http"
+	"path/filepath"
 )
 
 type contextKeyInput struct{}
@@ -32,11 +32,19 @@ func NewRoute() *Route {
 
 // Route stores the information that matches a request
 type Route struct {
+	parent      *Route
 	methods     []string
 	path        string
 	middlewares []middleware.Middleware
 }
 
+// NewRoute returns a sub route based on the current one
+func (r *Route) NewRoute() *Route {
+	return &Route{
+		parent: r,
+	}
+}
+
 // Method sets the method that the route matches
 func (r *Route) Method(method string) *Route {
 	r.methods = append(r.methods, method)
@@ -57,33 +65,49 @@ func (r *Route) Middleware(middleware middleware.Middleware) *Route {
 
 // Handler sets the handler that handles the request
 func (r *Route) Handler(handler http.Handler) {
+	methods := r.methods
+	if len(methods) == 0 && r.parent != nil {
+		methods = r.parent.methods
+	}
+
+	path := r.path
+	if r.parent != nil {
+		path = filepath.Join(r.parent.path, path)
+	}
+
+	var middlewares []middleware.Middleware
+	if r.parent != nil {
+		middlewares = r.parent.middlewares
+	}
+
+	middlewares = append(middlewares, r.middlewares...)
 	filterFunc := beego.FilterFunc(func(ctx *beegocontext.Context) {
 		ctx.Request = ctx.Request.WithContext(
 			context.WithValue(ctx.Request.Context(), contextKeyInput{}, ctx.Input))
 		// TODO remove the WithMiddlewares?
-		middleware.WithMiddlewares(handler, r.middlewares...).
+		middleware.WithMiddlewares(handler, middlewares...).
 			ServeHTTP(ctx.ResponseWriter, ctx.Request)
 	})
-	if len(r.methods) == 0 {
+	if len(methods) == 0 {
 		beego.Any(r.path, filterFunc)
 		return
 	}
-	for _, method := range r.methods {
+	for _, method := range methods {
 		switch method {
 		case http.MethodGet:
-			beego.Get(r.path, filterFunc)
+			beego.Get(path, filterFunc)
 		case http.MethodHead:
-			beego.Head(r.path, filterFunc)
+			beego.Head(path, filterFunc)
 		case http.MethodPut:
-			beego.Put(r.path, filterFunc)
+			beego.Put(path, filterFunc)
 		case http.MethodPatch:
-			beego.Patch(r.path, filterFunc)
+			beego.Patch(path, filterFunc)
 		case http.MethodPost:
-			beego.Post(r.path, filterFunc)
+			beego.Post(path, filterFunc)
 		case http.MethodDelete:
-			beego.Delete(r.path, filterFunc)
+			beego.Delete(path, filterFunc)
 		case http.MethodOptions:
-			beego.Options(r.path, filterFunc)
+			beego.Options(path, filterFunc)
 		}
 	}
 }
@@ -93,28 +117,14 @@ func (r *Route) HandlerFunc(f http.HandlerFunc) {
 	r.Handler(f)
 }
 
-// GetInput returns the input object from the context
-func GetInput(context context.Context) (*beegocontext.BeegoInput, error) {
-	if context == nil {
-		return nil, errors.New("context is nil")
+// Param returns the beego router param by a given key from the context
+func Param(ctx context.Context, key string) string {
+	if ctx == nil {
+		return ""
 	}
-	input, ok := context.Value(contextKeyInput{}).(*beegocontext.BeegoInput)
+	input, ok := ctx.Value(contextKeyInput{}).(*beegocontext.BeegoInput)
 	if !ok {
-		return nil, errors.New("input not found in the context")
+		return ""
 	}
-	return input, nil
-}
-
-// Param returns the router param by a given key from the context
-func Param(ctx context.Context, key string) (string, error) {
-	input, err := GetInput(ctx)
-	if err != nil {
-		return "", err
-	}
-	return input.Param(key), nil
-}
-
-// Middleware registers the global middleware that executed for all requests that match the path
-func Middleware(path string, middleware middleware.Middleware) {
-	// TODO add middleware function to register global middleware after upgrading to the latest version of beego
+	return input.Param(key)
 }
diff --git a/src/server/router/router_test.go b/src/server/router/router_test.go
index 14964df99..b8382d05d 100644
--- a/src/server/router/router_test.go
+++ b/src/server/router/router_test.go
@@ -32,6 +32,12 @@ func (r *routerTestSuite) SetupTest() {
 	r.route = NewRoute()
 }
 
+func (r *routerTestSuite) TestNewRoute() {
+	sub := r.route.Path("/v2").NewRoute()
+	r.Require().NotNil(sub.parent)
+	r.Equal("/v2", sub.parent.path)
+}
+
 func (r *routerTestSuite) TestMethod() {
 	r.route.Method(http.MethodGet)
 	r.Equal(http.MethodGet, r.route.methods[0])
@@ -53,27 +59,19 @@ func (r *routerTestSuite) TestMiddleware() {
 	r.Len(r.route.middlewares, 2)
 }
 
-func (r *routerTestSuite) TestGetInput() {
+func (r *routerTestSuite) TestParam() {
 	// nil context
-	_, err := GetInput(nil)
-	r.Require().NotNil(err)
+	value := Param(nil, "key")
+	r.Empty(value)
 
 	// context contains wrong type input
-	_, err = GetInput(context.WithValue(context.Background(), contextKeyInput{}, &Route{}))
-	r.Require().NotNil(err)
+	value = Param(context.WithValue(context.Background(), contextKeyInput{}, &Route{}), "key")
+	r.Empty(value)
 
-	// context contains input
-	input, err := GetInput(context.WithValue(context.Background(), contextKeyInput{}, &beegocontext.BeegoInput{}))
-	r.Require().Nil(err)
-	r.Assert().NotNil(input)
-}
-
-func (r *routerTestSuite) TestParam() {
+	// success
 	input := &beegocontext.BeegoInput{}
 	input.SetParam("key", "value")
-	value, err := Param(context.WithValue(context.Background(), contextKeyInput{}, input), "key")
-	r.Require().Nil(err)
-	r.Assert().NotNil(input)
+	value = Param(context.WithValue(context.Background(), contextKeyInput{}, input), "key")
 	r.Equal("value", value)
 }
 
diff --git a/src/testing/api/artifact/controller.go b/src/testing/api/artifact/controller.go
new file mode 100644
index 000000000..0f0bac0e4
--- /dev/null
+++ b/src/testing/api/artifact/controller.go
@@ -0,0 +1,102 @@
+// Copyright Project Harbor Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package artifact
+
+import (
+	"context"
+	"github.com/goharbor/harbor/src/api/artifact"
+	"github.com/goharbor/harbor/src/pkg/q"
+	"github.com/stretchr/testify/mock"
+	"time"
+)
+
+// FakeController is a fake artifact controller that implement src/api/artifact.Controller interface
+type FakeController struct {
+	mock.Mock
+}
+
+// Ensure ...
+func (f *FakeController) Ensure(ctx context.Context, repositoryID int64, digest string, tags ...string) (bool, int64, error) {
+	args := f.Called()
+	return args.Bool(0), int64(args.Int(1)), args.Error(2)
+}
+
+// List ...
+func (f *FakeController) List(ctx context.Context, query *q.Query, option *artifact.Option) (int64, []*artifact.Artifact, error) {
+	args := f.Called()
+	var artifacts []*artifact.Artifact
+	if args.Get(1) != nil {
+		artifacts = args.Get(1).([]*artifact.Artifact)
+	}
+	return int64(args.Int(0)), artifacts, args.Error(2)
+}
+
+// Get ...
+func (f *FakeController) Get(ctx context.Context, id int64, option *artifact.Option) (*artifact.Artifact, error) {
+	args := f.Called()
+	var art *artifact.Artifact
+	if args.Get(0) != nil {
+		art = args.Get(0).(*artifact.Artifact)
+	}
+	return art, args.Error(1)
+}
+
+// GetByReference ...
+func (f *FakeController) GetByReference(ctx context.Context, repository, reference string, option *artifact.Option) (*artifact.Artifact, error) {
+	args := f.Called()
+	var art *artifact.Artifact
+	if args.Get(0) != nil {
+		art = args.Get(0).(*artifact.Artifact)
+	}
+	return art, args.Error(1)
+}
+
+// Delete ...
+func (f *FakeController) Delete(ctx context.Context, id int64) (err error) {
+	args := f.Called()
+	return args.Error(0)
+}
+
+// Tags ...
+func (f *FakeController) Tags(ctx context.Context, query *q.Query, option *artifact.TagOption) (int64, []*artifact.Tag, error) {
+	args := f.Called()
+	var tags []*artifact.Tag
+	if args.Get(1) != nil {
+		tags = args.Get(1).([]*artifact.Tag)
+	}
+	return int64(args.Int(0)), tags, args.Error(2)
+}
+
+// DeleteTag ...
+func (f *FakeController) DeleteTag(ctx context.Context, tagID int64) (err error) {
+	args := f.Called()
+	return args.Error(0)
+}
+
+// UpdatePullTime ...
+func (f *FakeController) UpdatePullTime(ctx context.Context, artifactID int64, tagID int64, time time.Time) error {
+	args := f.Called()
+	return args.Error(0)
+}
+
+// GetSubResource ...
+func (f *FakeController) GetSubResource(ctx context.Context, artifactID int64, resource string) (*artifact.Resource, error) {
+	args := f.Called()
+	var res *artifact.Resource
+	if args.Get(0) != nil {
+		res = args.Get(0).(*artifact.Resource)
+	}
+	return res, args.Error(1)
+}
diff --git a/src/testing/api/repository/controller.go b/src/testing/api/repository/controller.go
new file mode 100644
index 000000000..3ab1ecc20
--- /dev/null
+++ b/src/testing/api/repository/controller.go
@@ -0,0 +1,64 @@
+// Copyright Project Harbor Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package repository
+
+import (
+	"context"
+	"github.com/goharbor/harbor/src/common/models"
+	"github.com/goharbor/harbor/src/pkg/q"
+	"github.com/stretchr/testify/mock"
+)
+
+// FakeController is a fake repository controller that implement src/api/repository.Controller interface
+type FakeController struct {
+	mock.Mock
+}
+
+// Ensure ...
+func (f *FakeController) Ensure(ctx context.Context, name string) (bool, int64, error) {
+	args := f.Called()
+	return args.Bool(0), int64(args.Int(1)), args.Error(2)
+}
+
+// List ...
+func (f *FakeController) List(ctx context.Context, query *q.Query) (int64, []*models.RepoRecord, error) {
+	args := f.Called()
+	var repositories []*models.RepoRecord
+	if args.Get(1) != nil {
+		repositories = args.Get(1).([]*models.RepoRecord)
+	}
+	return int64(args.Int(0)), repositories, args.Error(2)
+
+}
+
+// Get ...
+func (f *FakeController) Get(ctx context.Context, id int64) (*models.RepoRecord, error) {
+	args := f.Called()
+	var repository *models.RepoRecord
+	if args.Get(0) != nil {
+		repository = args.Get(0).(*models.RepoRecord)
+	}
+	return repository, args.Error(1)
+}
+
+// GetByName ...
+func (f *FakeController) GetByName(ctx context.Context, name string) (*models.RepoRecord, error) {
+	args := f.Called()
+	var repository *models.RepoRecord
+	if args.Get(0) != nil {
+		repository = args.Get(0).(*models.RepoRecord)
+	}
+	return repository, args.Error(1)
+}
diff --git a/src/testing/artifact_manager.go b/src/testing/pkg/artifact/manager.go
similarity index 67%
rename from src/testing/artifact_manager.go
rename to src/testing/pkg/artifact/manager.go
index aeab562ff..b168651a9 100644
--- a/src/testing/artifact_manager.go
+++ b/src/testing/pkg/artifact/manager.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package testing
+package artifact
 
 import (
 	"context"
@@ -22,13 +22,13 @@ import (
 	"time"
 )
 
-// FakeArtifactManager is a fake artifact manager that implement src/pkg/artifact.Manager interface
-type FakeArtifactManager struct {
+// FakeManager is a fake artifact manager that implement src/pkg/artifact.Manager interface
+type FakeManager struct {
 	mock.Mock
 }
 
 // List ...
-func (f *FakeArtifactManager) List(ctx context.Context, query *q.Query) (int64, []*artifact.Artifact, error) {
+func (f *FakeManager) List(ctx context.Context, query *q.Query) (int64, []*artifact.Artifact, error) {
 	args := f.Called()
 	var artifacts []*artifact.Artifact
 	if args.Get(1) != nil {
@@ -38,7 +38,7 @@ func (f *FakeArtifactManager) List(ctx context.Context, query *q.Query) (int64,
 }
 
 // Get ...
-func (f *FakeArtifactManager) Get(ctx context.Context, id int64) (*artifact.Artifact, error) {
+func (f *FakeManager) Get(ctx context.Context, id int64) (*artifact.Artifact, error) {
 	args := f.Called()
 	var art *artifact.Artifact
 	if args.Get(0) != nil {
@@ -48,19 +48,19 @@ func (f *FakeArtifactManager) Get(ctx context.Context, id int64) (*artifact.Arti
 }
 
 // Create ...
-func (f *FakeArtifactManager) Create(ctx context.Context, artifact *artifact.Artifact) (int64, error) {
+func (f *FakeManager) Create(ctx context.Context, artifact *artifact.Artifact) (int64, error) {
 	args := f.Called()
 	return int64(args.Int(0)), args.Error(1)
 }
 
 // Delete ...
-func (f *FakeArtifactManager) Delete(ctx context.Context, id int64) error {
+func (f *FakeManager) Delete(ctx context.Context, id int64) error {
 	args := f.Called()
 	return args.Error(0)
 }
 
 // UpdatePullTime ...
-func (f *FakeArtifactManager) UpdatePullTime(ctx context.Context, artifactID int64, time time.Time) error {
+func (f *FakeManager) UpdatePullTime(ctx context.Context, artifactID int64, time time.Time) error {
 	args := f.Called()
 	return args.Error(0)
 }
diff --git a/src/testing/project_manager.go b/src/testing/pkg/project/manager.go
similarity index 76%
rename from src/testing/project_manager.go
rename to src/testing/pkg/project/manager.go
index 92dd42ab0..90c51a2c9 100644
--- a/src/testing/project_manager.go
+++ b/src/testing/pkg/project/manager.go
@@ -12,20 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package testing
+package project
 
 import (
 	"github.com/goharbor/harbor/src/common/models"
 	"github.com/stretchr/testify/mock"
 )
 
-// FakeProjectManager is a fake project manager that implement src/pkg/project.Manager interface
-type FakeProjectManager struct {
+// FakeManager is a fake project manager that implement src/pkg/project.Manager interface
+type FakeManager struct {
 	mock.Mock
 }
 
 // List ...
-func (f *FakeProjectManager) List(query ...*models.ProjectQueryParam) ([]*models.Project, error) {
+func (f *FakeManager) List(query ...*models.ProjectQueryParam) ([]*models.Project, error) {
 	args := f.Called()
 	var projects []*models.Project
 	if args.Get(0) != nil {
@@ -35,7 +35,7 @@ func (f *FakeProjectManager) List(query ...*models.ProjectQueryParam) ([]*models
 }
 
 // Get ...
-func (f *FakeProjectManager) Get(interface{}) (*models.Project, error) {
+func (f *FakeManager) Get(interface{}) (*models.Project, error) {
 	args := f.Called()
 	var project *models.Project
 	if args.Get(0) != nil {
diff --git a/src/testing/repository_manager.go b/src/testing/pkg/repository/manager.go
similarity index 66%
rename from src/testing/repository_manager.go
rename to src/testing/pkg/repository/manager.go
index f6994dd94..5ff176c9f 100644
--- a/src/testing/repository_manager.go
+++ b/src/testing/pkg/repository/manager.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package testing
+package repository
 
 import (
 	"context"
@@ -21,13 +21,13 @@ import (
 	"github.com/stretchr/testify/mock"
 )
 
-// FakeRepositoryManager is a fake repository manager that implement src/pkg/repository.Manager interface
-type FakeRepositoryManager struct {
+// FakeManager is a fake repository manager that implement src/pkg/repository.Manager interface
+type FakeManager struct {
 	mock.Mock
 }
 
 // List ...
-func (f *FakeRepositoryManager) List(ctx context.Context, query *q.Query) (int64, []*models.RepoRecord, error) {
+func (f *FakeManager) List(ctx context.Context, query *q.Query) (int64, []*models.RepoRecord, error) {
 	args := f.Called()
 	var repositories []*models.RepoRecord
 	if args.Get(1) != nil {
@@ -37,7 +37,7 @@ func (f *FakeRepositoryManager) List(ctx context.Context, query *q.Query) (int64
 }
 
 // Get ...
-func (f *FakeRepositoryManager) Get(ctx context.Context, id int64) (*models.RepoRecord, error) {
+func (f *FakeManager) Get(ctx context.Context, id int64) (*models.RepoRecord, error) {
 	args := f.Called()
 	var repository *models.RepoRecord
 	if args.Get(0) != nil {
@@ -47,7 +47,7 @@ func (f *FakeRepositoryManager) Get(ctx context.Context, id int64) (*models.Repo
 }
 
 // GetByName ...
-func (f *FakeRepositoryManager) GetByName(ctx context.Context, name string) (*models.RepoRecord, error) {
+func (f *FakeManager) GetByName(ctx context.Context, name string) (*models.RepoRecord, error) {
 	args := f.Called()
 	var repository *models.RepoRecord
 	if args.Get(0) != nil {
@@ -57,19 +57,19 @@ func (f *FakeRepositoryManager) GetByName(ctx context.Context, name string) (*mo
 }
 
 // Delete ...
-func (f *FakeRepositoryManager) Delete(ctx context.Context, id int64) error {
+func (f *FakeManager) Delete(ctx context.Context, id int64) error {
 	args := f.Called()
 	return args.Error(0)
 }
 
 // Create ...
-func (f *FakeRepositoryManager) Create(ctx context.Context, repository *models.RepoRecord) (int64, error) {
+func (f *FakeManager) Create(ctx context.Context, repository *models.RepoRecord) (int64, error) {
 	args := f.Called()
 	return int64(args.Int(0)), args.Error(1)
 }
 
 // Update ...
-func (f *FakeRepositoryManager) Update(ctx context.Context, repository *models.RepoRecord, props ...string) error {
+func (f *FakeManager) Update(ctx context.Context, repository *models.RepoRecord, props ...string) error {
 	args := f.Called()
 	return args.Error(0)
 }
diff --git a/src/testing/scheduler.go b/src/testing/pkg/scheduler/scheduler.go
similarity index 76%
rename from src/testing/scheduler.go
rename to src/testing/pkg/scheduler/scheduler.go
index c7e502e8e..5675d8baf 100644
--- a/src/testing/scheduler.go
+++ b/src/testing/pkg/scheduler/scheduler.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package testing
+package scheduler
 
 import (
 	"fmt"
@@ -20,14 +20,14 @@ import (
 	"github.com/goharbor/harbor/src/pkg/scheduler/model"
 )
 
-// FakeSchedulerManager ...
-type FakeSchedulerManager struct {
+// FakeManager ...
+type FakeManager struct {
 	idCounter int64
 	Schedules []*model.Schedule
 }
 
 // Create ...
-func (f *FakeSchedulerManager) Create(schedule *model.Schedule) (int64, error) {
+func (f *FakeManager) Create(schedule *model.Schedule) (int64, error) {
 	f.idCounter++
 	id := f.idCounter
 	schedule.ID = id
@@ -36,7 +36,7 @@ func (f *FakeSchedulerManager) Create(schedule *model.Schedule) (int64, error) {
 }
 
 // Update ...
-func (f *FakeSchedulerManager) Update(schedule *model.Schedule, props ...string) error {
+func (f *FakeManager) Update(schedule *model.Schedule, props ...string) error {
 	for i, sch := range f.Schedules {
 		if sch.ID == schedule.ID {
 			f.Schedules[i] = schedule
@@ -47,7 +47,7 @@ func (f *FakeSchedulerManager) Update(schedule *model.Schedule, props ...string)
 }
 
 // Delete ...
-func (f *FakeSchedulerManager) Delete(id int64) error {
+func (f *FakeManager) Delete(id int64) error {
 	length := len(f.Schedules)
 	for i, sch := range f.Schedules {
 		if sch.ID == id {
@@ -62,7 +62,7 @@ func (f *FakeSchedulerManager) Delete(id int64) error {
 }
 
 // Get ...
-func (f *FakeSchedulerManager) Get(id int64) (*model.Schedule, error) {
+func (f *FakeManager) Get(id int64) (*model.Schedule, error) {
 	for _, sch := range f.Schedules {
 		if sch.ID == id {
 			return sch, nil
@@ -72,6 +72,6 @@ func (f *FakeSchedulerManager) Get(id int64) (*model.Schedule, error) {
 }
 
 // List ...
-func (f *FakeSchedulerManager) List(...*model.ScheduleQuery) ([]*model.Schedule, error) {
+func (f *FakeManager) List(...*model.ScheduleQuery) ([]*model.Schedule, error) {
 	return f.Schedules, nil
 }
diff --git a/src/testing/tag_manager.go b/src/testing/pkg/tag/manager.go
similarity index 69%
rename from src/testing/tag_manager.go
rename to src/testing/pkg/tag/manager.go
index 7c664eb86..133704390 100644
--- a/src/testing/tag_manager.go
+++ b/src/testing/pkg/tag/manager.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package testing
+package tag
 
 import (
 	"context"
@@ -21,13 +21,13 @@ import (
 	"github.com/stretchr/testify/mock"
 )
 
-// FakeTagManager is a fake tag manager that implement the src/pkg/tag.Manager interface
-type FakeTagManager struct {
+// FakeManager is a fake tag manager that implement the src/pkg/tag.Manager interface
+type FakeManager struct {
 	mock.Mock
 }
 
 // List ...
-func (f *FakeTagManager) List(ctx context.Context, query *q.Query) (int64, []*tag.Tag, error) {
+func (f *FakeManager) List(ctx context.Context, query *q.Query) (int64, []*tag.Tag, error) {
 	args := f.Called()
 	var tags []*tag.Tag
 	if args.Get(1) != nil {
@@ -37,7 +37,7 @@ func (f *FakeTagManager) List(ctx context.Context, query *q.Query) (int64, []*ta
 }
 
 // Get ...
-func (f *FakeTagManager) Get(ctx context.Context, id int64) (*tag.Tag, error) {
+func (f *FakeManager) Get(ctx context.Context, id int64) (*tag.Tag, error) {
 	args := f.Called()
 	var tg *tag.Tag
 	if args.Get(0) != nil {
@@ -47,19 +47,19 @@ func (f *FakeTagManager) Get(ctx context.Context, id int64) (*tag.Tag, error) {
 }
 
 // Create ...
-func (f *FakeTagManager) Create(ctx context.Context, tag *tag.Tag) (int64, error) {
+func (f *FakeManager) Create(ctx context.Context, tag *tag.Tag) (int64, error) {
 	args := f.Called()
 	return int64(args.Int(0)), args.Error(1)
 }
 
 // Update ...
-func (f *FakeTagManager) Update(ctx context.Context, tag *tag.Tag, props ...string) error {
+func (f *FakeManager) Update(ctx context.Context, tag *tag.Tag, props ...string) error {
 	args := f.Called()
 	return args.Error(0)
 }
 
 // Delete ...
-func (f *FakeTagManager) Delete(ctx context.Context, id int64) error {
+func (f *FakeManager) Delete(ctx context.Context, id int64) error {
 	args := f.Called()
 	return args.Error(0)
 }
diff --git a/tests/robot-cases/Group0-BAT/API_DB.robot b/tests/robot-cases/Group0-BAT/API_DB.robot
index 67c47e3ab..f17cb70f0 100644
--- a/tests/robot-cases/Group0-BAT/API_DB.robot
+++ b/tests/robot-cases/Group0-BAT/API_DB.robot
@@ -36,9 +36,8 @@ Test Case - Edit Project Creation
 #    Harbor API Test  ./tests/apitests/python/test_scan_image.py
 Test Case - Manage Project Member
     Harbor API Test  ./tests/apitests/python/test_manage_project_member.py
-# TODO uncomment this after enable content trust middleware
-# Test Case - Project Level Policy Content Trust
-#    Harbor API Test  ./tests/apitests/python/test_project_level_policy_content_trust.py
+Test Case - Project Level Policy Content Trust
+    Harbor API Test  ./tests/apitests/python/test_project_level_policy_content_trust.py
 # TODO uncomment this after we move the accesslog away from registry notificaiton
 # TODO potentially #10602 may also fix this.
 # Test Case - User View Logs
diff --git a/tests/travis/ut_run.sh b/tests/travis/ut_run.sh
index 21b68dd81..f835b3e63 100755
--- a/tests/travis/ut_run.sh
+++ b/tests/travis/ut_run.sh
@@ -4,7 +4,7 @@ set -x
 set -e
 
 export POSTGRESQL_HOST=$1
-export REGISTRY_URL=$1:5000
+export REGISTRY_URL=http://$1:5000
 export CHROME_BIN=chromium-browser
 #export DISPLAY=:99.0
 #sh -e /etc/init.d/xvfb start