diff --git a/apis/project.go b/apis/project.go index 4c10276..dd7263b 100644 --- a/apis/project.go +++ b/apis/project.go @@ -1,232 +1,232 @@ /* Copyright © 2017-2019 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package apis import ( "net/http" "strings" - "anongit.kde.org/sysadmin/projects-api.git/models" + "invent.kde.org/sysadmin/projects-api.git/models" "github.com/gin-gonic/gin" ) type projectService interface { Get(path string) (*models.Project, error) Find(filter *models.ProjectFilter) ([]string, error) List(path string) ([]string, error) } type projectResource struct { service projectService } // ServeProjectResource creates a new API resource. func ServeProjectResource(rg *gin.RouterGroup, service projectService) { r := &projectResource{service} rg.GET("/repo/*path", r.repo) rg.GET("/project/*path", r.get) rg.GET("/find", r.find) rg.GET("/projects", r.projects) rg.GET("/projects/*path", r.projects) } /** * @apiDefine ProjectSuccessExample * @apiSuccessExample {json} Success-Response: * { * "i18n": { * "stable": "none", * "stableKF5": "krita/3.1", * "trunk": "none", * "trunkKF5": "master", * "component": "calligra" * }, * "repo": "krita" * "path": "calligra/krita" * } */ /** * @api {get} /project/:path Get (by Project) * * @apiVersion 1.0.0 * @apiGroup Project * @apiName project * * @apiDescription Gets the metadata of the project identified by path. * * @apiUse ProjectSuccessExample * * @apiError Forbidden Path may not be accessed. */ func (r *projectResource) get(c *gin.Context) { path := strings.TrimLeft(c.Param("path"), "/") if strings.Contains(path, "/..") || strings.Contains(path, "../") { c.AbortWithStatus(http.StatusForbidden) return } response, err := r.service.Get(path) if err != nil { panic(err) } if response == nil { c.AbortWithStatus(http.StatusNotFound) return } c.JSON(http.StatusOK, response) } /** * @api {get} /repo/:path Get (by Repository) * @apiParam {String} path Repository path as seen in git clone kde:path/is/this. * * @apiVersion 1.0.0 * @apiGroup Project * @apiName repo * * @apiDescription Gets the metadata of the project identified by a query. * * @apiUse ProjectSuccessExample * * @apiError NotFound Couldn't find a project associated with this repo. * @apiError MultipleChoices Couldn't find a unique project for this repo. * You'll need to pick a project and get it via the /project/ * endpoint * @apiErrorExample {json} MultipleChoices-Response: * [ * "books/kf5book", * "books/kf5book-duplcate" * ] */ func (r *projectResource) repo(c *gin.Context) { path := strings.TrimLeft(c.Param("path"), "/") matches, err := r.service.Find(&models.ProjectFilter{RepoPath: path}) if err != nil { panic(err) } if len(matches) < 1 { c.AbortWithStatus(http.StatusNotFound) return } if len(matches) > 1 { c.JSON(http.StatusMultipleChoices, matches) return } response, err := r.service.Get(matches[0]) if err != nil { panic(err) } c.JSON(http.StatusOK, response) } /** * @api {get} /find Find * @apiParam {String} id Identifier (basename) of the project to find. * @apiParam {String} repo repo attribute of the project * to find. * @apiParam {Bool} active Whether to find only projects marked active (inactive * projects can be skipped by code that wants to iterate worthwhile code * projects only e.g. sysadmin repos are usually inactive). * This defaults to false, giving you all repos. * @apiParam {Bool} inactive Whether to find only inactive projects (see active) * This defaults to false, giving you all repos. * * @apiVersion 1.0.0 * @apiGroup Project * @apiName find * * @apiDescription Finds matching projects by a combination of filter params or * none to list all projects. * * @apiError NotFound Nothing matched the filter criteria * * @apiSuccessExample {json} Success-Response: * [ * "books", * "books/kf5book", * "calligra", * ... * ] */ func (r *projectResource) find(c *gin.Context) { var filter models.ProjectFilter c.BindQuery(&filter) matches, err := r.service.Find(&filter) if len(matches) == 0 || err != nil { c.AbortWithStatus(http.StatusNotFound) return } c.JSON(http.StatusOK, matches) } /** * @api {get} /projects/:prefix List * @apiParam {String} prefix Prefix path of the project. This will usually be * the module/components the project is sorted under. * * @apiVersion 1.0.0 * @apiGroup Project * @apiName projects * * @apiDescription Lists all *projects* underneath the prefix. If the prefix is * a project itself it will be included in the list. If the prefix is not a * project only its "children" will be returned. * * @apiSuccessExample {json} Project (/projects/calligra/krita): * [ * "calligra/krita" * ] * * @apiSuccessExample {json} Project with Children (/projects/papa): * [ * "aba", * "aba/bubbale1", * "aba/bubbale2" * ] * * @apiSuccessExample {json} Component (/projects/frameworks) * [ * "frameworks/solid", * "frameworks/ki18n", * "..." * ] */ func (r *projectResource) projects(c *gin.Context) { path := strings.TrimLeft(c.Param("path"), "/") matches, err := r.service.List(path) if len(matches) == 0 || err != nil { c.AbortWithStatus(http.StatusNotFound) return } c.JSON(http.StatusOK, matches) } diff --git a/apis/project_test.go b/apis/project_test.go index 22de06b..04c1474 100644 --- a/apis/project_test.go +++ b/apis/project_test.go @@ -1,99 +1,99 @@ /* Copyright © 2017-2019 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package apis import ( "errors" "net/http" "testing" - "anongit.kde.org/sysadmin/projects-api.git/models" + "invent.kde.org/sysadmin/projects-api.git/models" ) // Test Double type ProjectService struct { } func NewProjectService() *ProjectService { return &ProjectService{} } func (s *ProjectService) Get(path string) (*models.Project, error) { project := models.Project{} if path == "calligra/krita" { project.Path = "calligra/krita" project.Repo = "krita" return &project, nil } if path == "calligra" { return nil, nil } return &project, errors.New("unexpected path " + path) } func (s *ProjectService) Find(filter *models.ProjectFilter) ([]string, error) { projects := []string{"calligra/krita"} if filter.ID == "krita" && filter.RepoPath == "" { return projects, nil } if filter.ID == "" && filter.RepoPath == "krita" { return projects, nil } if filter.ID == "" && filter.InactiveOnly { return []string{"kde/hole"}, nil } if filter.ID == "" && filter.RepoPath == "" { return append(projects, "frameworks/solid"), nil } panic("unexpected query " + filter.ID + " " + filter.RepoPath) } func (s *ProjectService) List(path string) ([]string, error) { if path == "" { return []string{"calligra/krita", "frameworks/solid"}, nil } if path == "calligra" { return []string{"calligra/krita"}, nil } panic("unexpected query " + path) } func init() { v1 := router.Group("/v1") { ServeProjectResource(v1, NewProjectService()) } } func TestProject(t *testing.T) { kritaObj := `{"i18n":{"trunkKF5":"", "component":"", "stable":"", "stableKF5":"", "trunk":""}, "repo":"krita", "path":"calligra/krita"}` runAPITests(t, []apiTestCase{ {"get a project", "GET", "/v1/project/calligra/krita", "", http.StatusOK, kritaObj}, {"get nil project", "GET", "/v1/project/calligra", "", http.StatusNotFound, ""}, {"get by repopath", "GET", "/v1/repo/krita", "", http.StatusOK, kritaObj}, {"find by id", "GET", "/v1/find?id=krita", "", http.StatusOK, `["calligra/krita"]`}, {"find by repopath", "GET", "/v1/find?repo=krita", "", http.StatusOK, `["calligra/krita"]`}, {"find by only inactive", "GET", "/v1/find?inactive=true", "", http.StatusOK, `["kde/hole"]`}, {"find all", "GET", "/v1/find", "", http.StatusOK, `["calligra/krita", "frameworks/solid"]`}, {"list", "GET", "/v1/projects", "", http.StatusOK, `["calligra/krita", "frameworks/solid"]`}, {"list component", "GET", "/v1/projects/calligra", "", http.StatusOK, `["calligra/krita"]`}, }) } diff --git a/daos/git.go b/daos/git.go index 3e55c5f..5f1806b 100644 --- a/daos/git.go +++ b/daos/git.go @@ -1,334 +1,334 @@ /* Copyright © 2017 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package daos import ( "encoding/json" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "sync" "time" - "anongit.kde.org/sysadmin/projects-api.git/models" + "invent.kde.org/sysadmin/projects-api.git/models" "github.com/danwakefield/fnmatch" "gopkg.in/yaml.v2" ) type i18nDefaultsData []map[string]models.I18nData type _Cache struct { path map[string]*models.Project list []string i18nDefaults i18nDefaultsData mutex sync.RWMutex } func (c *_Cache) Reset() { defer c.Locker()() fmt.Println("RESET CACHE") c.path = map[string]*models.Project{} c.list = []string{} c.i18nDefaults = i18nDefaultsData{} } func (c *_Cache) RLocker() func() { c.mutex.RLock() return func() { c.mutex.RUnlock() } } func (c *_Cache) Locker() func() { c.mutex.Lock() return func() { c.mutex.Unlock() } } func (c *_Cache) Project(path string) *models.Project { defer c.RLocker()() return c.path[path] } func (c *_Cache) AddProject(path string, project *models.Project) { defer c.Locker()() c.path[path] = project } func (c *_Cache) List() []string { defer c.RLocker()() return c.list } func (c *_Cache) SetList(list []string) { defer c.Locker()() c.list = list } func (c *_Cache) I18nDefaults() i18nDefaultsData { defer c.RLocker()() return c.i18nDefaults } func (c *_Cache) SetI18nDefaults(i18nDefaults i18nDefaultsData) { defer c.Locker()() c.i18nDefaults = i18nDefaults } // GitDAO is a wrapper around repo-metadata git repo. type GitDAO struct { cache _Cache revSHA string lastPoll time.Time updateMutex sync.Mutex } // NewGitDAO creates a new data access object for the repo-metadata git repo. func NewGitDAO() *GitDAO { return newGitDAOInternal(true) } func newGitDAOInternal(autoUpdate bool) *GitDAO { dao := &GitDAO{} dao.maybeResetCache() // Always true here ;) if !autoUpdate { return dao } updateTicker := time.NewTicker(4 * time.Minute) go func() { for { dao.UpdateClone() <-updateTicker.C } }() return dao } // ResetCache resets internal caching. func (dao *GitDAO) ResetCache() { dao.cache.Reset() } func (dao *GitDAO) revParse() (string, error) { cmd := exec.Command("git", "rev-parse", "--verify", "HEAD") cmd.Dir = "repo-metadata" stdoutStderr, err := cmd.CombinedOutput() if err != nil { return "", err } return string(stdoutStderr), nil } func (dao *GitDAO) maybeResetCache() { sha, err := dao.revParse() if err != nil { dao.ResetCache() return } if sha != dao.revSHA { dao.revSHA = sha dao.ResetCache() } } // UpdateClone clones (if applicable), pulls, restes caches (if applicable). func (dao *GitDAO) UpdateClone() string { dao.updateMutex.Lock() // Make sure we have consistent rev values. defer dao.updateMutex.Unlock() dao.lastPoll = time.Now() dao.revSHA, _ = dao.revParse() // So we definitely know where we were at. dao.clone() ret := dao.update() dao.maybeResetCache() return ret } // Age returns the age of the underlying repository (time since last poll) func (dao *GitDAO) Age() time.Duration { return time.Since(dao.lastPoll) } // Get returns a new Project entity. Either from cache or from the data layer. func (dao *GitDAO) Get(path string) (*models.Project, error) { if path[0] == '/' { panic("expect path to NOT start with slash") } project := dao.cache.Project(path) if project != nil { return project, nil } project, err := dao.newProject(path) if err == nil { // TODO: maybe should cache pointers, foot print small enough to not matter // really, but deep copy runtime implications are meh. dao.cache.AddProject(path, project) } return project, err } func isEntity(path string) bool { _, err := os.Stat(path + "/metadata.yaml") return err == nil } func (dao *GitDAO) List() []string { if len(dao.cache.List()) <= 0 { dao.cache.SetList(dao.list()) } return dao.cache.List() } func (dao *GitDAO) list() []string { matches := []string{} filepath.Walk("repo-metadata/projects/", func(path string, info os.FileInfo, err error) error { if err != nil { panic(err) } if info.IsDir() && isEntity(path) { rel, err := filepath.Rel("repo-metadata/projects/", path) if err != nil { panic(err) } matches = append(matches, rel) } return nil }) return matches } func (dao *GitDAO) i18nDefaults() i18nDefaultsData { if len(dao.cache.I18nDefaults()) <= 0 { dao.cache.SetI18nDefaults(dao.i18nDefaultsLoad()) } return dao.cache.I18nDefaults() } func (dao *GitDAO) i18nDefaultsLoad() i18nDefaultsData { obj := i18nDefaultsData{} // NOTE: Documentation of repo-metadata says the entires in the json must // be ordered so we'll rely on this here. // Ruby implicitly preserves key order when parsing objects, use it to parse // and then convert to actually ordered array of objects. cmd := exec.Command("ruby", "-e", ` require 'json' hash = JSON.parse(File.read('repo-metadata/config/i18n_defaults.json')) puts JSON.dump(hash.map {|k,v| { k => v } }) `) stdoutStderr, err := cmd.CombinedOutput() if err != nil { panic(err) } err = json.Unmarshal(stdoutStderr, &obj) if err != nil { panic(err) } // err = json.NewDecoder(i18nFile).Decode(&obj) return obj } func (dao *GitDAO) newProject(path string) (*models.Project, error) { data, err := ioutil.ReadFile("repo-metadata/projects/" + path + "/metadata.yaml") if err != nil { return nil, err } var project models.Project if err = yaml.Unmarshal([]byte(data), &project); err != nil { panic(err) } if project.Repo == "" { return nil, nil } // TODO: cache the bloody defaults // First use default values foundDefault := false for _, rule := range dao.i18nDefaults() { for pattern, values := range rule { if !fnmatch.Match(pattern, path, 0) { continue } project.I18n = values foundDefault = true break } if foundDefault { break } } // Then patch sepcific project data in if available. This cascades the // attributes, e.g. if there's x and y in the defaults, the specific data may // specify only y to override y but leave x at the default. i18nFile, err := os.Open("repo-metadata/projects/" + path + "/i18n.json") if err == nil { // Components and the like have no i18n data. var overrideData models.I18nData if err = json.NewDecoder(i18nFile).Decode(&overrideData); err != nil { panic(err) } project.I18n.Merge(overrideData) } project.I18n.Infer(&project) // TODO: not cascading urls_gitrepo or urls_webaccess, useless. // This data patching is too depressing for me. return &project, nil } func (dao *GitDAO) clone() { _, err := os.Stat("repo-metadata") if err == nil { return // exists already } cmd := exec.Command("git", "clone", "--depth=1", "https://anongit.kde.org/sysadmin/repo-metadata.git") stdoutStderr, err := cmd.CombinedOutput() if err != nil { fmt.Println(string(stdoutStderr)) panic(err) } } func (dao *GitDAO) update() string { cmd := exec.Command("git", "pull") cmd.Dir = "repo-metadata" stdoutStderr, err := cmd.CombinedOutput() if err != nil { panic(err) } return string(stdoutStderr) } diff --git a/go.mod b/go.mod index fba75ca..a29f154 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ -module anongit.kde.org/sysadmin/projects-api.git +module invent.kde.org/sysadmin/projects-api.git go 1.14 require ( github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 github.com/gin-gonic/gin v1.6.3 github.com/stretchr/testify v1.5.1 gopkg.in/yaml.v2 v2.2.8 ) diff --git a/main.go b/main.go index 9cecfe6..dbcc453 100644 --- a/main.go +++ b/main.go @@ -1,100 +1,100 @@ /* Copyright © 2017 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package main import ( "context" "flag" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" - "anongit.kde.org/sysadmin/projects-api.git/apis" - "anongit.kde.org/sysadmin/projects-api.git/daos" - "anongit.kde.org/sysadmin/projects-api.git/services" + "invent.kde.org/sysadmin/projects-api.git/apis" + "invent.kde.org/sysadmin/projects-api.git/daos" + "invent.kde.org/sysadmin/projects-api.git/services" "github.com/coreos/go-systemd/activation" "github.com/gin-gonic/gin" ) func main() { flag.Parse() fmt.Println("Ready to rumble...") router := gin.Default() router.GET("/", func(c *gin.Context) { c.Redirect(http.StatusMovedPermanently, "http://"+c.Request.Host+"/doc") }) router.StaticFS("/doc", http.Dir("doc")) router.GET("/ping", func(c *gin.Context) { c.String(http.StatusOK, "OK") }) v1 := router.Group("/v1") { gitDAO := daos.NewGitDAO() apis.ServeGitResource(v1, services.NewGitService(gitDAO)) apis.ServeProjectResource(v1, services.NewProjectService(gitDAO)) } listeners, err := activation.Listeners() if err != nil { panic(err) } log.Println("starting servers") var servers []*http.Server for _, listener := range listeners { server := &http.Server{Handler: router} go server.Serve(listener) servers = append(servers, server) } quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) // Wait for some quit cause. // This could be INT, TERM, QUIT or the db update trigger. // We'll then do a zero downtime shutdown. // This relies on systemd managing the socket and us doing graceful listener // shutdown. Once we are no longer listening, the system starts backlogging // the socket until we get restarted and listen again. // Ideally this results in zero dropped connections. <-quit log.Println("servers are shutting down") for _, srv := range servers { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) defer cancel() srv.SetKeepAlivesEnabled(false) if err := srv.Shutdown(ctx); err != nil { log.Fatalf("Server Shutdown: %s", err) } } log.Println("Server exiting") } diff --git a/services/git.go b/services/git.go index 8695102..51ab393 100644 --- a/services/git.go +++ b/services/git.go @@ -1,50 +1,50 @@ /* Copyright © 2017 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package services import ( "time" - "anongit.kde.org/sysadmin/projects-api.git/models" + "invent.kde.org/sysadmin/projects-api.git/models" ) type gitDAO interface { UpdateClone() string Age() time.Duration Get(path string) (*models.Project, error) List() []string } type GitService struct { dao gitDAO } func NewGitService(dao gitDAO) *GitService { return &GitService{dao} } func (s *GitService) UpdateClone() string { return s.dao.UpdateClone() } func (s *GitService) Age() time.Duration { return s.dao.Age() } diff --git a/services/project.go b/services/project.go index 2fdb74f..879fa5f 100644 --- a/services/project.go +++ b/services/project.go @@ -1,94 +1,94 @@ /* Copyright © 2017-2019 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package services import ( "path/filepath" "strings" - "anongit.kde.org/sysadmin/projects-api.git/models" + "invent.kde.org/sysadmin/projects-api.git/models" ) type ProjectService struct { dao gitDAO } func NewProjectService(dao gitDAO) *ProjectService { return &ProjectService{dao} } func (s *ProjectService) Get(path string) (*models.Project, error) { return s.dao.Get(path) } func (s *ProjectService) isProject(path string) bool { model, err := s.Get(path) if err != nil { panic(err) } return model != nil && model.Repo != "" } func (s *ProjectService) Find(filter *models.ProjectFilter) ([]string, error) { matches := []string{} for _, path := range s.dao.List() { if !s.isProject(path) { continue } if len(filter.ID) != 0 && filter.ID != filepath.Base(path) { continue } model, _ := s.Get(path) if len(filter.RepoPath) != 0 { if model.Repo != filter.RepoPath { continue // doesn't match repopath constraint } } if filter.ActiveOnly && !model.Active { continue } if filter.InactiveOnly && model.Active { continue } matches = append(matches, path) } return matches, nil } func (s *ProjectService) List(prefix string) ([]string, error) { matches := []string{} for _, path := range s.dao.List() { if !s.isProject(path) { continue } if len(prefix) == 0 || path == prefix || strings.HasPrefix(path, prefix+"/") { matches = append(matches, path) } } return matches, nil } diff --git a/services/project_test.go b/services/project_test.go index fffad6d..ebbd764 100644 --- a/services/project_test.go +++ b/services/project_test.go @@ -1,124 +1,124 @@ /* Copyright © 2017-2019 Harald Sitter This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package services import ( "testing" "time" - "anongit.kde.org/sysadmin/projects-api.git/models" + "invent.kde.org/sysadmin/projects-api.git/models" "github.com/stretchr/testify/assert" ) type GitDAO struct { } func NewGitDAO() *GitDAO { return &GitDAO{} } func (d *GitDAO) Age() time.Duration { return 0 } func (d *GitDAO) Get(path string) (*models.Project, error) { if path == "calligra/krita" { return &models.Project{ Repo: "kritarepo", }, nil } if path == "calligra/krita-extensions/krita-analogies" { return &models.Project{ Repo: "krita-analogiesrepo", Active: true, }, nil } if path == "frameworks/solid" { return &models.Project{ Repo: "solidrepo", }, nil } if path == "calligra" { return nil, nil } panic("unknown path requested " + path) } func (d *GitDAO) List() []string { return []string{"calligra", "calligra/krita", "calligra/krita-extensions/krita-analogies", "frameworks/solid"} } func (d *GitDAO) UpdateClone() string { return "" } func TestProjectFind(t *testing.T) { s := NewProjectService(NewGitDAO()) t.Run("no filter", func(t *testing.T) { ret, err := s.Find(&models.ProjectFilter{}) assert.NoError(t, err) assert.Equal(t, []string{"calligra/krita", "calligra/krita-extensions/krita-analogies", "frameworks/solid"}, ret) }) t.Run("filter by id", func(t *testing.T) { ret, err := s.Find(&models.ProjectFilter{ID: "krita"}) assert.NoError(t, err) assert.Equal(t, []string{"calligra/krita"}, ret) }) t.Run("filter by repo", func(t *testing.T) { ret, err := s.Find(&models.ProjectFilter{RepoPath: "kritarepo"}) assert.NoError(t, err) assert.Equal(t, []string{"calligra/krita"}, ret) }) t.Run("filter by active", func(t *testing.T) { ret, err := s.Find(&models.ProjectFilter{ActiveOnly: true}) assert.NoError(t, err) assert.Equal(t, []string{"calligra/krita-extensions/krita-analogies"}, ret) }) } func TestProjectList(t *testing.T) { s := NewProjectService(NewGitDAO()) t.Run("root", func(t *testing.T) { ret, err := s.List("") assert.NoError(t, err) assert.Equal(t, []string{"calligra/krita", "calligra/krita-extensions/krita-analogies", "frameworks/solid"}, ret) }) t.Run("component", func(t *testing.T) { ret, err := s.List("calligra") assert.NoError(t, err) assert.Equal(t, []string{"calligra/krita", "calligra/krita-extensions/krita-analogies"}, ret) }) t.Run("project", func(t *testing.T) { ret, err := s.List("calligra/krita") assert.NoError(t, err) // This must not include krita-extensions as it is not a child, it happens // to have the same stringy prefix though. assert.Equal(t, []string{"calligra/krita"}, ret) }) }