From fe674ac88b0e85c0da70666bda04f7277f0f5a7a Mon Sep 17 00:00:00 2001 From: Thomas Miceli <27960254+thomiceli@users.noreply.github.com> Date: Sun, 17 Sep 2023 00:59:47 +0200 Subject: [PATCH] Add git, auth and gists tests (#97) --- go.mod | 4 + go.sum | 6 + internal/db/db.go | 17 +- internal/db/gist.go | 4 +- internal/git/commands.go | 21 +- internal/git/commands_test.go | 301 +++++++++++++++++++++++++++++ internal/web/{run.go => server.go} | 56 +++--- internal/web/test/auth_test.go | 91 +++++++++ internal/web/test/gist_test.go | 200 +++++++++++++++++++ internal/web/test/server.go | 162 ++++++++++++++++ opengist.go | 4 +- 11 files changed, 831 insertions(+), 35 deletions(-) create mode 100644 internal/git/commands_test.go rename internal/web/{run.go => server.go} (92%) create mode 100644 internal/web/test/auth_test.go create mode 100644 internal/web/test/gist_test.go create mode 100644 internal/web/test/server.go diff --git a/go.mod b/go.mod index 43c7dbc..8ce05be 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect @@ -34,6 +35,9 @@ require ( github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.5.0 // indirect + github.com/stretchr/testify v1.8.4 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/net v0.7.0 // indirect diff --git a/go.sum b/go.sum index 72f129a..0bc637a 100644 --- a/go.sum +++ b/go.sum @@ -195,12 +195,18 @@ github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= diff --git a/internal/db/db.go b/internal/db/db.go index c995e36..79394f6 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -14,7 +14,7 @@ import ( var db *gorm.DB -func Setup(dbPath string) error { +func Setup(dbPath string, sharedCache bool) error { var err error journalMode := strings.ToUpper(config.C.SqliteJournalMode) @@ -22,7 +22,12 @@ func Setup(dbPath string) error { log.Warn().Msg("Invalid SQLite journal mode: " + journalMode) } - if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode), &gorm.Config{ + sharedCacheStr := "" + if sharedCache { + sharedCacheStr = "&cache=shared" + } + + if db, err = gorm.Open(sqlite.Open(dbPath+"?_fk=true&_journal_mode="+journalMode+sharedCacheStr), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }); err != nil { return err @@ -51,6 +56,14 @@ func Setup(dbPath string) error { }) } +func Close() error { + sqlDB, err := db.DB() + if err != nil { + return err + } + return sqlDB.Close() +} + func CountAll(table interface{}) (int64, error) { var count int64 err := db.Model(table).Count(&count).Error diff --git a/internal/db/gist.go b/internal/db/gist.go index 4ac2787..de12d58 100644 --- a/internal/db/gist.go +++ b/internal/db/gist.go @@ -317,7 +317,7 @@ func (gist *Gist) Log(skip int) ([]*git.Commit, error) { } func (gist *Gist) NbCommits() (string, error) { - return git.GetNumberOfCommitsOfRepository(gist.User.Username, gist.Uuid) + return git.CountCommits(gist.User.Username, gist.Uuid) } func (gist *Gist) AddAndCommitFiles(files *[]FileDTO) error { @@ -391,6 +391,8 @@ type GistDTO struct { Description string `validate:"max=150" form:"description"` Private int `validate:"number,min=0,max=2" form:"private"` Files []FileDTO `validate:"min=1,dive"` + Name []string `form:"name"` + Content []string `form:"content"` } type FileDTO struct { diff --git a/internal/git/commands.go b/internal/git/commands.go index f3306bd..c3a94ff 100644 --- a/internal/git/commands.go +++ b/internal/git/commands.go @@ -14,8 +14,14 @@ import ( "strings" ) +var ( + ReposDirectory = "repos" +) + +const truncateLimit = 2 << 18 + func RepositoryPath(user string, gist string) string { - return filepath.Join(config.GetHomeDir(), "repos", strings.ToLower(user), gist) + return filepath.Join(config.GetHomeDir(), ReposDirectory, strings.ToLower(user), gist) } func RepositoryUrl(ctx echo.Context, user string, gist string) string { @@ -54,8 +60,7 @@ func InitRepository(user string, gist string) error { repositoryPath, ) - err := cmd.Run() - if err != nil { + if err := cmd.Run(); err != nil { return err } @@ -72,7 +77,7 @@ func InitRepositoryViaNewPush(user string, gist string, ctx echo.Context) error return createDotGitHookFile(repositoryPath, "post-receive", fmt.Sprintf(postReceive, repositoryUrl, repositoryUrl)) } -func GetNumberOfCommitsOfRepository(user string, gist string) (string, error) { +func CountCommits(user string, gist string) (string, error) { repositoryPath := RepositoryPath(user, gist) cmd := exec.Command( @@ -113,7 +118,7 @@ func GetFileContent(user string, gist string, revision string, filename string, var maxBytes int64 = -1 if truncate { - maxBytes = 2 << 18 + maxBytes = truncateLimit } cmd := exec.Command( @@ -167,7 +172,7 @@ func GetLog(user string, gist string, skip int) ([]*Commit, error) { } defer cmd.Wait() - return parseLog(stdout, 2<<18), nil + return parseLog(stdout, truncateLimit), nil } func CloneTmp(user string, gist string, gistTmpId string, email string) error { @@ -294,7 +299,7 @@ func RPC(user string, gist string, service string) ([]byte, error) { } func GcRepos() error { - subdirs, err := os.ReadDir(filepath.Join(config.GetHomeDir(), "repos")) + subdirs, err := os.ReadDir(filepath.Join(config.GetHomeDir(), ReposDirectory)) if err != nil { return err } @@ -304,7 +309,7 @@ func GcRepos() error { continue } - subRoot := filepath.Join(config.GetHomeDir(), "repos", subdir.Name()) + subRoot := filepath.Join(config.GetHomeDir(), ReposDirectory, subdir.Name()) gitRepos, err := os.ReadDir(subRoot) if err != nil { diff --git a/internal/git/commands_test.go b/internal/git/commands_test.go new file mode 100644 index 0000000..cc510a0 --- /dev/null +++ b/internal/git/commands_test.go @@ -0,0 +1,301 @@ +package git + +import ( + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/thomiceli/opengist/internal/config" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "testing" +) + +func setup(t *testing.T) { + err := config.InitConfig("") + require.NoError(t, err, "Could not init config") + + err = os.MkdirAll(path.Join(config.GetHomeDir(), "tests"), 0755) + ReposDirectory = path.Join("tests") + require.NoError(t, err) + + err = os.MkdirAll(filepath.Join(config.GetHomeDir(), "tmp", "repos"), 0755) + require.NoError(t, err) + + err = InitRepository("thomas", "gist1") + require.NoError(t, err) +} + +func teardown(t *testing.T) { + err := os.RemoveAll(path.Join(config.C.OpengistHome, "tests")) + require.NoError(t, err, "Could not remove repos directory") +} + +func TestInitDeleteRepository(t *testing.T) { + setup(t) + defer teardown(t) + + cmd := exec.Command("git", "rev-parse", "--is-bare-repository") + cmd.Dir = RepositoryPath("thomas", "gist1") + out, err := cmd.Output() + require.NoError(t, err, "Could not run git command") + require.Equal(t, "true", strings.TrimSpace(string(out)), "Repository is not bare") + + _, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "hooks", "pre-receive")) + require.NoError(t, err, "pre-receive hook not found") + + _, err = os.Stat(path.Join(RepositoryPath("thomas", "gist1"), "git-daemon-export-ok")) + require.NoError(t, err, "git-daemon-export-ok file not found") + + err = DeleteRepository("thomas", "gist1") + require.NoError(t, err, "Could not delete repository") + require.NoDirExists(t, RepositoryPath("thomas", "gist1"), "Repository should not exist") +} + +func TestCommits(t *testing.T) { + setup(t) + defer teardown(t) + + hasNoCommits, err := HasNoCommits("thomas", "gist1") + require.NoError(t, err, "Could not check if repository has no commits") + require.True(t, hasNoCommits, "Repository should have no commits") + + commitToBare(t, "thomas", "gist1", nil) + + hasNoCommits, err = HasNoCommits("thomas", "gist1") + require.NoError(t, err, "Could not check if repository has no commits") + require.False(t, hasNoCommits, "Repository should have commits") + + nbCommits, err := CountCommits("thomas", "gist1") + require.NoError(t, err, "Could not count commits") + require.Equal(t, "1", nbCommits, "Repository should have 1 commit") + + commitToBare(t, "thomas", "gist1", nil) + nbCommits, err = CountCommits("thomas", "gist1") + require.NoError(t, err, "Could not count commits") + require.Equal(t, "2", nbCommits, "Repository should have 2 commits") +} + +func TestContent(t *testing.T) { + setup(t) + defer teardown(t) + + commitToBare(t, "thomas", "gist1", map[string]string{ + "my_file.txt": "I love Opengist\n", + "my_other_file.txt": `I really +hate Opengist`, + "rip.txt": "byebye", + }) + + files, err := GetFilesOfRepository("thomas", "gist1", "HEAD") + require.NoError(t, err, "Could not get files of repository") + require.Subset(t, []string{"my_file.txt", "my_other_file.txt", "rip.txt"}, files, "Files are not correct") + + content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", false) + require.NoError(t, err, "Could not get content") + require.False(t, truncated, "Content should not be truncated") + require.Equal(t, "I love Opengist\n", content, "Content is not correct") + + content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false) + require.NoError(t, err, "Could not get content") + require.False(t, truncated, "Content should not be truncated") + require.Equal(t, "I really\nhate Opengist", content, "Content is not correct") + + commitToBare(t, "thomas", "gist1", map[string]string{ + "my_renamed_file.txt": "I love Opengist\n", + "my_other_file.txt": `I really +like Opengist actually`, + "new_file.txt": "Wait now there is a new file", + }) + + files, err = GetFilesOfRepository("thomas", "gist1", "HEAD") + require.NoError(t, err, "Could not get files of repository") + require.Subset(t, []string{"my_renamed_file.txt", "my_other_file.txt", "new_file.txt"}, files, "Files are not correct") + + content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_other_file.txt", false) + require.NoError(t, err, "Could not get content") + require.False(t, truncated, "Content should not be truncated") + require.Equal(t, "I really\nlike Opengist actually", content, "Content is not correct") + + commits, err := GetLog("thomas", "gist1", 0) + require.NoError(t, err, "Could not get log") + require.Equal(t, 2, len(commits), "Commits count are not correct") + require.Regexp(t, "[a-f0-9]{40}", commits[0].Hash, "Commit ID is not correct") + require.Regexp(t, "[0-9]{10}", commits[0].Timestamp, "Commit timestamp is not correct") + require.Equal(t, "thomas", commits[0].AuthorName, "Commit author name is not correct") + require.Equal(t, "thomas@mail.com", commits[0].AuthorEmail, "Commit author email is not correct") + require.Equal(t, "4 files changed, 2 insertions, 2 deletions", commits[0].Changed, "Commit author name is not correct") + + require.Contains(t, commits[0].Files, File{ + Filename: "my_renamed_file.txt", + OldFilename: "my_file.txt", + Content: "", + Truncated: false, + IsCreated: false, + IsDeleted: false, + }, "File my_renamed_file.txt is not correct") + + require.Contains(t, commits[0].Files, File{ + Filename: "rip.txt", + OldFilename: "", + Content: `@@ -1 +0,0 @@ +-byebye +\ No newline at end of file +`, + Truncated: false, + IsCreated: false, + IsDeleted: true, + }, "File rip.txt is not correct") + + require.Contains(t, commits[0].Files, File{ + Filename: "my_other_file.txt", + OldFilename: "", + Content: `@@ -1,2 +1,2 @@ + I really +-hate Opengist +\ No newline at end of file ++like Opengist actually +\ No newline at end of file +`, + Truncated: false, + IsCreated: false, + IsDeleted: false, + }, "File my_other_file.txt is not correct") + + require.Contains(t, commits[0].Files, File{ + Filename: "new_file.txt", + OldFilename: "", + Content: `@@ -0,0 +1 @@ ++Wait now there is a new file +\ No newline at end of file +`, + Truncated: false, + IsCreated: true, + IsDeleted: false, + }, "File new_file.txt is not correct") + + commitsSkip1, err := GetLog("thomas", "gist1", 1) + require.NoError(t, err, "Could not get log") + require.Equal(t, commitsSkip1[0], commits[1], "Commits skips are not correct") +} + +func TestGitGc(t *testing.T) { + setup(t) + defer teardown(t) + + err := GcRepos() + require.NoError(t, err, "Could not run git gc") +} + +func TestFork(t *testing.T) { + setup(t) + defer teardown(t) + + commitToBare(t, "thomas", "gist1", map[string]string{ + "my_file.txt": "I love Opengist\n", + }) + + err := ForkClone("thomas", "gist1", "thomas", "gist2") + require.NoError(t, err, "Could not fork repository") + + files1, err := GetFilesOfRepository("thomas", "gist1", "HEAD") + require.NoError(t, err, "Could not get files of repository") + files2, err := GetFilesOfRepository("thomas", "gist2", "HEAD") + require.NoError(t, err, "Could not get files of repository") + + require.Equal(t, files1, files2, "Files are not the same") + +} + +func TestTruncate(t *testing.T) { + setup(t) + defer teardown(t) + + commitToBare(t, "thomas", "gist1", map[string]string{ + "my_file.txt": "A", + }) + + content, truncated, err := GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true) + require.NoError(t, err, "Could not get content") + require.False(t, truncated, "Content should not be truncated") + require.Equal(t, 1, len(content), "Content size is not correct") + + var builder strings.Builder + for i := 0; i < truncateLimit+10; i++ { + builder.WriteString("A") + } + str := builder.String() + commitToBare(t, "thomas", "gist1", map[string]string{ + "my_file.txt": str, + }) + + content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true) + require.NoError(t, err, "Could not get content") + require.True(t, truncated, "Content should be truncated") + require.Equal(t, truncateLimit, len(content), "Content size should be at truncate limit") + + commitToBare(t, "thomas", "gist1", map[string]string{ + "my_file.txt": "AA\n" + str, + }) + + content, truncated, err = GetFileContent("thomas", "gist1", "HEAD", "my_file.txt", true) + require.NoError(t, err, "Could not get content") + require.True(t, truncated, "Content should be truncated") + require.Equal(t, 2, len(content), "Content size is not correct") +} + +func TestInitViaNewPush(t *testing.T) { + setup(t) + defer teardown(t) + + e := echo.New() + + // Create a mock HTTP request + req := httptest.NewRequest(http.MethodPost, "/", nil) + + // Create a mock HTTP response recorder + rec := httptest.NewRecorder() + + // Create a new Echo context + c := e.NewContext(req, rec) + + // Define your user and gist + user := "testUser" + gist := "testGist" + + // Call InitRepositoryViaNewPush + err := InitRepositoryViaNewPush(user, gist, c) + + // Perform assertions + assert.NoError(t, err) +} + +func commitToBare(t *testing.T, user string, gist string, files map[string]string) { + err := CloneTmp(user, gist, gist, "thomas@mail.com") + require.NoError(t, err, "Could not commit to repository") + + if len(files) > 0 { + for filename, content := range files { + if err := SetFileContent(gist, filename, content); err != nil { + require.NoError(t, err, "Could not commit to repository") + } + + if err := AddAll(gist); err != nil { + require.NoError(t, err, "Could not commit to repository") + } + } + + } + + if err := CommitRepository(gist, user, "thomas@mail.com"); err != nil { + require.NoError(t, err, "Could not commit to repository") + } + + if err := Push(gist); err != nil { + require.NoError(t, err, "Could not commit to repository") + } +} diff --git a/internal/web/run.go b/internal/web/server.go similarity index 92% rename from internal/web/run.go rename to internal/web/server.go index a003dc1..a7d8be0 100644 --- a/internal/web/run.go +++ b/internal/web/server.go @@ -17,7 +17,6 @@ import ( "html/template" "io" "net/http" - "os" "path/filepath" "regexp" "strconv" @@ -25,7 +24,7 @@ import ( "time" ) -var dev = os.Getenv("OG_DEV") == "1" +var dev bool var store *sessions.CookieStore var re = regexp.MustCompile("[^a-z0-9]+") var fm = template.FuncMap{ @@ -116,7 +115,13 @@ func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Con return t.templates.ExecuteTemplate(w, name, data) } -func Start() { +type Server struct { + echo *echo.Echo + dev bool +} + +func NewServer(isDev bool) *Server { + dev = isDev store = sessions.NewCookieStore([]byte("opengist")) gothic.Store = store @@ -172,13 +177,15 @@ func Start() { // Web based routes g1 := e.Group("") { - g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ - TokenLookup: "form:_csrf", - CookiePath: "/", - CookieHTTPOnly: true, - CookieSameSite: http.SameSiteStrictMode, - })) - g1.Use(csrfInit) + if !dev { + g1.Use(middleware.CSRFWithConfig(middleware.CSRFConfig{ + TokenLookup: "form:_csrf", + CookiePath: "/", + CookieHTTPOnly: true, + CookieSameSite: http.SameSiteStrictMode, + })) + g1.Use(csrfInit) + } g1.GET("/", create, logged) g1.POST("/", processCreate, logged) @@ -242,30 +249,35 @@ func Start() { } } - debugStr := "" // Git HTTP routes if config.C.HttpGit { e.Any("/:user/:gistname/*", gitHttp, gistSoftInit) - debugStr = " (with Git over HTTP)" } e.Any("/*", noRouteFound) + return &Server{echo: e, dev: dev} +} + +func (s *Server) Start() { addr := config.C.HttpHost + ":" + config.C.HttpPort - if config.C.HttpTLSEnabled { - log.Info().Msg("Starting HTTPS server on https://" + addr + debugStr) - if err := e.StartTLS(addr, config.C.HttpCertFile, config.C.HttpKeyFile); err != nil { - log.Fatal().Err(err).Msg("Failed to start HTTPS server") - } - } else { - log.Info().Msg("Starting HTTP server on http://" + addr + debugStr) - if err := e.Start(addr); err != nil { - log.Fatal().Err(err).Msg("Failed to start HTTP server") - } + log.Info().Msg("Starting HTTP server on http://" + addr) + if err := s.echo.Start(addr); err != nil && err != http.ErrServerClosed { + log.Fatal().Err(err).Msg("Failed to start HTTP server") } } +func (s *Server) Stop() { + if err := s.echo.Close(); err != nil { + log.Fatal().Err(err).Msg("Failed to stop HTTP server") + } +} + +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.echo.ServeHTTP(w, r) +} + func dataInit(next echo.HandlerFunc) echo.HandlerFunc { return func(ctx echo.Context) error { ctxValue := context.WithValue(ctx.Request().Context(), dataKey, echo.Map{}) diff --git a/internal/web/test/auth_test.go b/internal/web/test/auth_test.go new file mode 100644 index 0000000..73b7b9d --- /dev/null +++ b/internal/web/test/auth_test.go @@ -0,0 +1,91 @@ +package test + +import ( + "github.com/stretchr/testify/require" + "github.com/thomiceli/opengist/internal/db" + "testing" +) + +func TestRegister(t *testing.T) { + setup(t) + s, err := newTestServer() + require.NoError(t, err, "Failed to create test server") + defer teardown(t, s) + + err = s.request("GET", "/", nil, 302) + require.NoError(t, err) + + err = s.request("GET", "/register", nil, 200) + require.NoError(t, err) + + user1 := db.UserDTO{Username: "thomas", Password: "thomas"} + register(t, s, user1) + + user1db, err := db.GetUserById(1) + require.NoError(t, err) + require.Equal(t, user1.Username, user1db.Username) + require.True(t, user1db.IsAdmin) + + err = s.request("GET", "/", nil, 200) + require.NoError(t, err) + + s.sessionCookie = "" + + user2 := db.UserDTO{Username: "thomas", Password: "azeaze"} + err = s.request("POST", "/register", user2, 200) + require.Error(t, err) + + user3 := db.UserDTO{Username: "kaguya", Password: "kaguya"} + register(t, s, user3) + + user3db, err := db.GetUserById(2) + require.NoError(t, err) + require.False(t, user3db.IsAdmin) + + s.sessionCookie = "" + + count, err := db.CountAll(db.User{}) + require.NoError(t, err) + require.Equal(t, int64(2), count) +} + +func TestLogin(t *testing.T) { + setup(t) + s, err := newTestServer() + require.NoError(t, err, "Failed to create test server") + defer teardown(t, s) + + err = s.request("GET", "/login", nil, 200) + require.NoError(t, err) + + user1 := db.UserDTO{Username: "thomas", Password: "thomas"} + register(t, s, user1) + + s.sessionCookie = "" + + login(t, s, user1) + require.NotEmpty(t, s.sessionCookie) + + s.sessionCookie = "" + + user2 := db.UserDTO{Username: "thomas", Password: "azeaze"} + user3 := db.UserDTO{Username: "azeaze", Password: ""} + + err = s.request("POST", "/login", user2, 302) + require.Empty(t, s.sessionCookie) + require.Error(t, err) + + err = s.request("POST", "/login", user3, 302) + require.Empty(t, s.sessionCookie) + require.Error(t, err) +} + +func register(t *testing.T, s *testServer, user db.UserDTO) { + err := s.request("POST", "/register", user, 302) + require.NoError(t, err) +} + +func login(t *testing.T, s *testServer, user db.UserDTO) { + err := s.request("POST", "/login", user, 302) + require.NoError(t, err) +} diff --git a/internal/web/test/gist_test.go b/internal/web/test/gist_test.go new file mode 100644 index 0000000..b477e0e --- /dev/null +++ b/internal/web/test/gist_test.go @@ -0,0 +1,200 @@ +package test + +import ( + "github.com/stretchr/testify/require" + "github.com/thomiceli/opengist/internal/db" + "github.com/thomiceli/opengist/internal/git" + "testing" +) + +func TestGists(t *testing.T) { + setup(t) + s, err := newTestServer() + require.NoError(t, err, "Failed to create test server") + defer teardown(t, s) + + err = s.request("GET", "/", nil, 302) + require.NoError(t, err) + + user1 := db.UserDTO{Username: "thomas", Password: "thomas"} + register(t, s, user1) + + err = s.request("GET", "/all", nil, 200) + require.NoError(t, err) + + err = s.request("POST", "/", nil, 200) + require.NoError(t, err) + + gist1 := db.GistDTO{ + Title: "gist1", + Description: "my first gist", + Private: 0, + Name: []string{"gist1.txt", "gist2.txt", "gist3.txt"}, + Content: []string{"yeah", "yeah\ncool", "yeah\ncool gist actually"}, + } + err = s.request("POST", "/", gist1, 302) + require.NoError(t, err) + + gist1db, err := db.GetGistByID("1") + require.NoError(t, err) + require.Equal(t, uint(1), gist1db.ID) + require.Equal(t, gist1.Title, gist1db.Title) + require.Equal(t, gist1.Description, gist1db.Description) + require.Regexp(t, "[a-f0-9]{32}", gist1db.Uuid) + require.Equal(t, user1.Username, gist1db.User.Username) + + err = s.request("GET", "/"+gist1db.User.Username+"/"+gist1db.Uuid, nil, 200) + require.NoError(t, err) + + gist1files, err := git.GetFilesOfRepository(gist1db.User.Username, gist1db.Uuid, "HEAD") + require.NoError(t, err) + require.Equal(t, 3, len(gist1files)) + + gist1fileContent, _, err := git.GetFileContent(gist1db.User.Username, gist1db.Uuid, "HEAD", gist1.Name[0], false) + require.NoError(t, err) + require.Equal(t, gist1.Content[0], gist1fileContent) + + gist2 := db.GistDTO{ + Title: "gist2", + Description: "my second gist", + Private: 0, + Name: []string{"", "gist2.txt", "gist3.txt"}, + Content: []string{"", "yeah\ncool", "yeah\ncool gist actually"}, + } + err = s.request("POST", "/", gist2, 200) + require.NoError(t, err) + + gist3 := db.GistDTO{ + Title: "gist3", + Description: "my third gist", + Private: 0, + Name: []string{""}, + Content: []string{"yeah"}, + } + err = s.request("POST", "/", gist3, 302) + require.NoError(t, err) + + gist3db, err := db.GetGistByID("2") + require.NoError(t, err) + + gist3files, err := git.GetFilesOfRepository(gist3db.User.Username, gist3db.Uuid, "HEAD") + require.NoError(t, err) + require.Equal(t, "gistfile1.txt", gist3files[0]) + + err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", nil, 200) + require.NoError(t, err) + + gist1.Name = []string{"gist1.txt"} + gist1.Content = []string{"only want one gist"} + + err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/edit", gist1, 302) + require.NoError(t, err) + + gist1files, err = git.GetFilesOfRepository(gist1db.User.Username, gist1db.Uuid, "HEAD") + require.NoError(t, err) + require.Equal(t, 1, len(gist1files)) + + err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/delete", nil, 302) + require.NoError(t, err) +} + +func TestVisibility(t *testing.T) { + setup(t) + s, err := newTestServer() + require.NoError(t, err, "Failed to create test server") + defer teardown(t, s) + + user1 := db.UserDTO{Username: "thomas", Password: "thomas"} + register(t, s, user1) + + gist1 := db.GistDTO{ + Title: "gist1", + Description: "my first gist", + Private: 1, + Name: []string{""}, + Content: []string{"yeah"}, + } + err = s.request("POST", "/", gist1, 302) + require.NoError(t, err) + + gist1db, err := db.GetGistByID("1") + require.NoError(t, err) + require.Equal(t, 1, gist1db.Private) + + err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302) + require.NoError(t, err) + gist1db, err = db.GetGistByID("1") + require.NoError(t, err) + require.Equal(t, 2, gist1db.Private) + + err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302) + require.NoError(t, err) + gist1db, err = db.GetGistByID("1") + require.NoError(t, err) + require.Equal(t, 0, gist1db.Private) + + err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/visibility", nil, 302) + require.NoError(t, err) + gist1db, err = db.GetGistByID("1") + require.NoError(t, err) + require.Equal(t, 1, gist1db.Private) +} + +func TestLikeFork(t *testing.T) { + setup(t) + s, err := newTestServer() + require.NoError(t, err, "Failed to create test server") + defer teardown(t, s) + + user1 := db.UserDTO{Username: "thomas", Password: "thomas"} + register(t, s, user1) + + gist1 := db.GistDTO{ + Title: "gist1", + Description: "my first gist", + Private: 1, + Name: []string{""}, + Content: []string{"yeah"}, + } + err = s.request("POST", "/", gist1, 302) + require.NoError(t, err) + + s.sessionCookie = "" + + user2 := db.UserDTO{Username: "kaguya", Password: "kaguya"} + register(t, s, user2) + + gist1db, err := db.GetGistByID("1") + require.NoError(t, err) + require.Equal(t, 0, gist1db.NbLikes) + likeCount, err := db.CountAll(db.Like{}) + require.NoError(t, err) + require.Equal(t, int64(0), likeCount) + + err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/like", nil, 302) + require.NoError(t, err) + gist1db, err = db.GetGistByID("1") + require.NoError(t, err) + require.Equal(t, 1, gist1db.NbLikes) + likeCount, err = db.CountAll(db.Like{}) + require.NoError(t, err) + require.Equal(t, int64(1), likeCount) + + err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/like", nil, 302) + require.NoError(t, err) + gist1db, err = db.GetGistByID("1") + require.NoError(t, err) + require.Equal(t, 0, gist1db.NbLikes) + likeCount, err = db.CountAll(db.Like{}) + require.NoError(t, err) + require.Equal(t, int64(0), likeCount) + + err = s.request("POST", "/"+gist1db.User.Username+"/"+gist1db.Uuid+"/fork", nil, 302) + require.NoError(t, err) + gist2db, err := db.GetGistByID("2") + require.NoError(t, err) + require.Equal(t, gist1db.Title, gist2db.Title) + require.Equal(t, gist1db.Description, gist2db.Description) + require.Equal(t, gist1db.Private, gist2db.Private) + require.Equal(t, user2.Username, gist2db.User.Username) +} diff --git a/internal/web/test/server.go b/internal/web/test/server.go new file mode 100644 index 0000000..a6853c1 --- /dev/null +++ b/internal/web/test/server.go @@ -0,0 +1,162 @@ +package test + +import ( + "errors" + "fmt" + "github.com/rs/zerolog/log" + "github.com/stretchr/testify/require" + "github.com/thomiceli/opengist/internal/config" + "github.com/thomiceli/opengist/internal/db" + "github.com/thomiceli/opengist/internal/git" + "github.com/thomiceli/opengist/internal/memdb" + "github.com/thomiceli/opengist/internal/web" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path" + "path/filepath" + "reflect" + "strconv" + "strings" + "testing" +) + +type testServer struct { + server *web.Server + sessionCookie string +} + +func newTestServer() (*testServer, error) { + s := &testServer{ + server: web.NewServer(true), + } + + go s.start() + return s, nil +} + +func (s *testServer) start() { + s.server.Start() +} + +func (s *testServer) stop() { + s.server.Stop() +} + +func (s *testServer) request(method, uri string, data interface{}, expectedCode int) error { + var bodyReader io.Reader + if method == http.MethodPost || method == http.MethodPut { + values := structToURLValues(data) + bodyReader = strings.NewReader(values.Encode()) + } + + req := httptest.NewRequest(method, "http://localhost:6157"+uri, bodyReader) + w := httptest.NewRecorder() + + if method == http.MethodPost || method == http.MethodPut { + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + } + + if s.sessionCookie != "" { + req.AddCookie(&http.Cookie{Name: "session", Value: s.sessionCookie}) + } + + s.server.ServeHTTP(w, req) + + if w.Code != expectedCode { + return fmt.Errorf("unexpected status code %d, expected %d", w.Code, expectedCode) + } + + if method == http.MethodPost { + if strings.Contains(uri, "/login") || strings.Contains(uri, "/register") { + cookie := "" + h := w.Header().Get("Set-Cookie") + parts := strings.Split(h, "; ") + for _, p := range parts { + if strings.HasPrefix(p, "session=") { + cookie = p + break + } + } + if cookie == "" { + return errors.New("unable to find access session token in response headers") + } + s.sessionCookie = strings.TrimPrefix(cookie, "session=") + } else if strings.Contains(uri, "/logout") { + s.sessionCookie = "" + } + } + + return nil +} + +func structToURLValues(s interface{}) url.Values { + v := url.Values{} + if s == nil { + return v + } + + rValue := reflect.ValueOf(s) + if rValue.Kind() != reflect.Struct { + return v + } + + for i := 0; i < rValue.NumField(); i++ { + field := rValue.Type().Field(i) + tag := field.Tag.Get("form") + if tag != "" { + if field.Type.Kind() == reflect.Int { + fieldValue := rValue.Field(i).Int() + v.Add(tag, strconv.FormatInt(fieldValue, 10)) + } else if field.Type.Kind() == reflect.Slice { + fieldValue := rValue.Field(i).Interface().([]string) + for _, va := range fieldValue { + v.Add(tag, va) + } + } else { + fieldValue := rValue.Field(i).String() + v.Add(tag, fieldValue) + } + } + } + return v +} + +func setup(t *testing.T) { + err := config.InitConfig("") + require.NoError(t, err, "Could not init config") + + err = os.MkdirAll(filepath.Join(config.GetHomeDir()), 0755) + require.NoError(t, err, "Could not create Opengist home directory") + + git.ReposDirectory = path.Join("tests") + + config.InitLog() + + homePath := config.GetHomeDir() + log.Info().Msg("Data directory: " + homePath) + + err = os.MkdirAll(filepath.Join(homePath, "repos"), 0755) + require.NoError(t, err, "Could not create repos directory") + + err = os.MkdirAll(filepath.Join(homePath, "tmp", "repos"), 0755) + require.NoError(t, err, "Could not create tmp repos directory") + + err = db.Setup("file::memory:", true) + require.NoError(t, err, "Could not initialize database") + + err = memdb.Setup() + require.NoError(t, err, "Could not initialize in memory database") +} + +func teardown(t *testing.T, s *testServer) { + s.stop() + + err := db.Close() + require.NoError(t, err, "Could not close database") + + err = os.RemoveAll(path.Join(config.C.OpengistHome, "tests")) + require.NoError(t, err, "Could not remove repos directory") +} diff --git a/opengist.go b/opengist.go index d93db90..eb2a5a1 100644 --- a/opengist.go +++ b/opengist.go @@ -52,7 +52,7 @@ func initialize() { } log.Info().Msg("Database file: " + filepath.Join(homePath, config.C.DBFilename)) - if err := db.Setup(filepath.Join(homePath, config.C.DBFilename)); err != nil { + if err := db.Setup(filepath.Join(homePath, config.C.DBFilename), false); err != nil { log.Fatal().Err(err).Msg("Failed to initialize database") } @@ -64,7 +64,7 @@ func initialize() { func main() { initialize() - go web.Start() + go web.NewServer(os.Getenv("OG_DEV") == "1").Start() go ssh.Start() select {}