diff --git a/.gitignore b/.gitignore index 704a057..f5fc986 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,36 @@ + + + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin +bin/* +rpm +rpm/* +dist + +tmp/ +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out +coverage +.coverage + + +# ignore the following files/directories **/userdata/* examples -tests \ No newline at end of file +tests +e2e +site/ +.vscode/ +.idea/ +.DS_Store +__rd* +tests/out diff --git a/Makefile b/Makefile index 7749aa3..7ea8ec5 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,52 @@ +# Inspired from https://dustinspecker.com/posts/go-combined-unit-integration-code-coverage/ and https://netdevops.me/2023/test-coverage-for-go-integration-tests/ +BIN_DIR = $(CURDIR)/bin +COVERAGE_DIR = $(CURDIR)/coverage +BINARY = $(BIN_DIR)/gosimplenpm + +.PHONY: clean clean: go clean + +.PHONY: dep dep: go mod tidy + +.PHONY: fmt fmt: go fmt ./... + +.PHONY: lint lint: golangci-lint run + +coverage-unit: + go test ./... -short -covermode=count -coverprofile=./coverage/unit.out + go tool cover -func=./coverage/unit.out +coverage-integration: + go test ./... -run Integration -covermode=count -coverprofile=./coverage/integration.out + go tool cover -func=./coverage/integration.out + +.PHONY: build-debug +build-debug: + mkdir -p $(BIN_DIR) + go build -o $(BINARY) -cover main.go + +.PHONY: test +test: build-debug + rm -rf $(COVERAGE_DIR) + mkdir -p $(COVERAGE_DIR) + go test -cover ./... -args -test.gocoverdir="$(COVERAGE_DIR)" + +.PHONY: coverage-full +coverage-full: test + go tool covdata textfmt -i=$(COVERAGE_DIR) -o $(COVERAGE_DIR)/coverage.out + go tool cover -func=$(COVERAGE_DIR)/coverage.out + +.PHONY: coverage-html +coverage-html: + go tool cover -html=./coverage/coverage.out -o ./coverage/coverage.html + open ./coverage/coverage.html + +.PHONY: lint-all lint-all: golangci-lint run --enable-all -.PHONY: dep lint clean diff --git a/README.md b/README.md index fcb80d6..51e2b90 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,22 @@ TODO... TODO... -# Work List +# TODO List -TODO... \ No newline at end of file +- [x] Adding unit tests +- [x] Adding integration tests +- [ ] Adding e2e tests +- [ ] Adding support for log files, that is, writing logs to a log file +- [ ] Support [abbreviated_package_format](https://github.com/verdaccio/verdaccio/issues/2792) +- [ ] Copy artifactory setup where there is a package.json (current package.json) and there is also an index json for the npm cli. Basically, the structure is like this +- [ ] Add a max bytes for publishing as shown [here](https://stackoverflow.com/questions/28282370/is-it-advisable-to-further-limit-the-size-of-forms-when-using-golang) + +```sh +registry +|-.npm +|-|-@scope1/package1 +|-|-|-index.json +|-@scope1/package1 +|-|-package1-x.x.x.tgz +|-|-package.json +``` diff --git a/cmd/gosimplenpm/root.go b/cmd/gosimplenpm/root.go index 25ba6c2..c1233ab 100644 --- a/cmd/gosimplenpm/root.go +++ b/cmd/gosimplenpm/root.go @@ -4,6 +4,7 @@ import ( "fmt" "gosimplenpm/internal/config" "gosimplenpm/internal/handler" + "gosimplenpm/internal/storage" "os" "github.com/sirupsen/logrus" @@ -47,13 +48,13 @@ Documentation about the npm private registry: }, } app := &handler.Application{ - Conf: cfg, - Logger: log, + Conf: cfg, + Logger: log, + FSStorage: &storage.FSStorage{}, } fmt.Println("\n Server is starting....") err = app.Start() - fmt.Println("Why!") if err != nil { fmt.Printf("Server start up error: %+v\n", err) os.Exit(1) diff --git a/go.mod b/go.mod index ea3aac0..6220a2f 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,16 @@ require ( github.com/gorilla/mux v1.8.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 + github.com/stretchr/testify v1.7.0 golang.org/x/crypto v0.10.0 golang.org/x/mod v0.11.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.9.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 881c27f..c666b1f 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,7 @@ golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/config/conf.go b/internal/config/conf.go index 88c337b..371ffb9 100644 --- a/internal/config/conf.go +++ b/internal/config/conf.go @@ -70,6 +70,8 @@ func VerifyConfig() error { return err } + // TODO: Create a logging file (it should be located at .gosimplenpm/applogs) + ConfigFilePath = path.Join(configDirPath, "config.json") if NpmRepoDir == "" { diff --git a/internal/handler/app.go b/internal/handler/app.go index 26e43ed..e2c0336 100644 --- a/internal/handler/app.go +++ b/internal/handler/app.go @@ -3,6 +3,7 @@ package handler import ( "gosimplenpm/internal/config" "gosimplenpm/internal/middlewares" + "gosimplenpm/internal/storage" "net/http" "time" @@ -11,9 +12,10 @@ import ( ) type Application struct { - Logger *logrus.Logger - Conf config.Config - Mux *mux.Router + Logger *logrus.Logger + Conf config.Config + Mux *mux.Router + FSStorage storage.Storage } func (app *Application) Routes() { @@ -24,14 +26,14 @@ func (app *Application) Routes() { m.Use(middlewares.LogMiddleware(app.Logger)) // main handler - m.HandleFunc("/{name}", GetPackage(app.Logger, app.Conf)).Methods("GET") - m.HandleFunc("/{name}", middlewares.AuthMiddleware(app.Conf)(Publish(app.Logger, app.Conf))).Methods("PUT") + m.HandleFunc("/{name}", GetPackage(app.Logger, app.Conf, app.FSStorage)).Methods("GET") + m.HandleFunc("/{name}", middlewares.AuthMiddleware(app.Conf)(Publish(app.Logger, app.Conf, app.FSStorage))).Methods("PUT") // tar handlers - m.HandleFunc("/{name}/-/{tar}", PackageTarGet(app.Logger, app.Conf)).Methods("GET") + m.HandleFunc("/{name}/-/{tar}", PackageTarGet(app.Logger, app.Conf, app.FSStorage)).Methods("GET") // tag handlers - m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(DistTagDelete(app.Logger, app.Conf))).Methods("DELETE") - m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(DistTagPut(app.Logger, app.Conf))).Methods("PUT") - m.HandleFunc("/-/package/{name}/dist-tags", DistTagGet(app.Logger, app.Conf)).Methods("GET") + m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(DistTagDelete(app.Logger, app.Conf, app.FSStorage))).Methods("DELETE") + m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(DistTagPut(app.Logger, app.Conf, app.FSStorage))).Methods("PUT") + m.HandleFunc("/-/package/{name}/dist-tags", DistTagGet(app.Logger, app.Conf, app.FSStorage)).Methods("GET") m.NotFoundHandler = http.HandlerFunc(NotFound) app.Mux = m } diff --git a/internal/handler/get.go b/internal/handler/get.go index 925aa9e..515c531 100644 --- a/internal/handler/get.go +++ b/internal/handler/get.go @@ -12,7 +12,7 @@ import ( "gosimplenpm/internal/storage" ) -func GetPackage(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { +func GetPackage(lg *logrus.Logger, cfg config.Config, stg storage.Storage) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { escapedName := mux.Vars(r)["name"] packageName, _ := url.PathUnescape(escapedName) @@ -20,7 +20,7 @@ func GetPackage(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { "function": "get-package", }).Debugf("Package name => %s\n", packageName) - fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg) + fileToServe, found, err := stg.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/handler/get_test.go b/internal/handler/get_test.go index abeebd1..4afd873 100644 --- a/internal/handler/get_test.go +++ b/internal/handler/get_test.go @@ -1 +1,218 @@ package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "gosimplenpm/internal/config" + "gosimplenpm/internal/storage" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestUnitGet(t *testing.T) { + t.Run("return `Not Found` error if package is not found", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/{name}", nil) + wrt := httptest.NewRecorder() + + log := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, log *logrus.Logger) (string, bool, error) { + return "", false, nil + } + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + } + + req = mux.SetURLVars(req, vars) + + GetPackage(log, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusNotFound) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Package not found: test-package\n") + }) + + t.Run("return `Internal Server` error if package cannot be retrieved", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/test-package", nil) + wrt := httptest.NewRecorder() + + log := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, log *logrus.Logger) (string, bool, error) { + return "", true, fmt.Errorf("filesystem error") + } + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + } + + req = mux.SetURLVars(req, vars) + + GetPackage(log, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "filesystem error\n") + }) + + t.Run("return a file if package is found", func(t *testing.T) { + tmpDir := t.TempDir() + + f, err := os.CreateTemp(tmpDir, "foo.json") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(tmpDir) + defer f.Close() + + _, err = f.WriteString("{data: \"test data\"}") + if err != nil { + t.Fatal(err) + } + + req := httptest.NewRequest(http.MethodGet, "/test-oackage", nil) + wrt := httptest.NewRecorder() + + log := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, log *logrus.Logger) (string, bool, error) { + return f.Name(), true, nil + } + // mfs.SetRetrieved(true) + // mfs.SetFileToServe(f.Name()) + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + } + + req = mux.SetURLVars(req, vars) + + GetPackage(log, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusOK) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "{data: \"test data\"}") + }) +} + +func TestIntegrationGet(t *testing.T) { + if testing.Short() { + t.Skip("Skipping getPackage integration test") + } + + token := "0N89nr/hmKXoBzG]R{fKH%YE1X" + + tmpDir := t.TempDir() + + t.Logf("Temp Dir: %s", tmpDir) + + // cpFolders(t, "intestdata/@df", fmt.Sprintf("%s/@df", tmpDir)) + + mkDir(t, fmt.Sprintf("%s/@df/simplepackone", tmpDir)) + + indexJsonFp := "intestdata/get/index.json" + tgzFile := "intestdata/get/simplepackone-1.0.0.tgz" + + cpFile(t, indexJsonFp, fmt.Sprintf("%s/@df/simplepackone/index.json", tmpDir)) + cpFile(t, tgzFile, fmt.Sprintf("%s/@df/simplepackone/simplepackone-1.0.0.tgz", tmpDir)) + + listDir(t, fmt.Sprintf("%s/", tmpDir), true) + + cfg := config.Config{ + RepoDir: tmpDir, + Token: token, + } + + app := newTestApp(t, cfg) + app.Routes() + ts := newTestServer(t, app.Mux) + defer ts.Close() + + code, _, body := ts.get(t, fmt.Sprintf("/%s", url.PathEscape("@df/simplepackone"))) + + assert.Equal(t, code, http.StatusOK) + + expected := readTestFile(t, indexJsonFp) + var resultExpected map[string]interface{} + var resultBody map[string]interface{} + json.Unmarshal(expected, &resultExpected) + json.Unmarshal(body, &resultBody) + + assert.Equal(t, resultBody, resultExpected) +} diff --git a/internal/handler/intestdata/get/index.json b/internal/handler/intestdata/get/index.json new file mode 100644 index 0000000..7bff30e --- /dev/null +++ b/internal/handler/intestdata/get/index.json @@ -0,0 +1 @@ +{"_id":"@df/simplepackone","name":"@df/simplepackone","description":"This is a very rough implementation of a private npm registry.","dist-tags":{"latest":"1.0.0"},"time":{"1.0.0":"2023-12-24T11:04:34-05:00","created":"2023-12-24T11:04:34-05:00","modified":"2023-12-24T11:04:34-05:00","unpublished":""},"versions":{"1.0.0":{"name":"@df/simplepackone","version":"1.0.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","repository":{"type":"","url":"","directory":""},"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","readmeFilename":"README.md","_id":"@df/simplepackone@1.0.0","maintainers.omitempty":null,"bugs":{},"bin":null,"engines":{},"_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-9NI+Kqf+4C8Rr5GPKen8o6hhp/LMMeox96du65v6T+W27irVSsxZP0grBHBFyfd9whDXOoSliRWvYBnGnELlnA==","shasum":"a3974e824557b8e7ba00b87fb4ec51e4d132fff2","tarball":"http://localhost:6000/@df/simplepackone/-/@df/simplepackone-1.0.0.tgz","fileCount":0,"unpackedSize":0},"dependencies":{"eslint":"^7.x","mocha":"^10.x","uuid":"^9.x"}}},"access":"","_attachments":{"@df/simplepackone-1.0.0.tgz":{"content_type":"application/octet-stream","data":"H4sIAAAAAAAC/+1XbW/bNhDOZ/2KgwoUCSAospsXLJ+m2HQsTJYMSW4WoCggS7TFVhYFksoLhv73HSknTtp93Dys88EAwXt77o6+I9Xmxdd8TU9ZU9JH94s8+gfI87yLszP4K76h4Qc4+nB+4Q0GFxce6nkD73Lo4Xq0B+qkygWG8jckiQQv63+ECt5IXlO35utje0oFtU+ODvT/oXbb/9sVJwBv9tz/Z+dn3/f/4Px8cOj/fdAfFoDd5BtqX4H9a7k6lWzT1lT/HXhDbUeL76mQjDdaY+B6rtdzSyoLwVq1lfTMTc7M7vk66bm9okSBhkOGolJpNVpUHD7ZRAgurqDhoAUgW1qwFaPlJxvevwf6yBQMbLT8Zrzlnaq42GHWrKCNNAnMguw5uJZiCE3B6CtYKmvWGODPl+6j0dQx86LKDXPg7bhdx0r7yv78C7I0tPXN+pn7PyH+eEbcTbn/+384HPxw/59/GB76fx/0Dm543/JNu7GsrGIS8JcDNv0TCN6tKzDiDW1Urpsd+ArFrWD3uaKAViDomkklnlzLegcTmqtOUIm+4nHsuoZJmipvCuPjreCWi68QovUL93Al/yv9HwYjEqXEVY9qz/0/9M68H+5/7/Ly0P/7IGvE2yfB1pWC4+IEhvpj7M1EmFOxYVLf/3ouVPiFsHyCtcgbRUsHVoJSPRDwBhVr6oDikDdP0OKLQU+KpcL3AGvWVg4F4mhNpSeM5Cv1kAuKyiXkUvKC4TApoeRFtxs0K1ZTCceqopadbi3sEwNS0rwG1qA3Cs8ieGD4MOgUziOcRqzQPhxUKuqu1DE8i2u2YVsEbW6Sl9ppJzEDHacDG16ylV6pSavtljWTlWOVetCxZaeQKTXTPD0cnccpFyBpXWsP+Oroc91FZ3QQxWp1QdW2RAb3oeKbt5lgiVadaBCSliZdjiUziF9oobQXrb7idc0fMDWEbEqmM5JXeoZjXZf8nppc+rNtuMJQ+xD0AbS7U92KZJVj7Etq9QVDXCxv/iodoeGxWRrFsPYtFwbv+zRxqmdTAmk8yW79hECQwjyJPwZjMgbbT3FvO3AbZNN4kQFqJH6U3UE8AT+6g9+CaOxY5Pd5QtIU4gSC2TwMyNiBIBqFi3EQ3cA12kVxBmGAjz10msWgAbeuAoJ2E2tGktEUt/51EAbZnQOTIIu0zwk69WHuJ1kwWoR+AvNFMo9TgvBjK4qjIJokiEJmJMpcREUoIB9xA+nUD0MD5S8w+sTEN4rnd0lwM81gGodjgsxrYoWBfx2SHgqTGoV+MHNg7M/8G2KsYvSSgFbbRnc7JYaFeD7+RlkQRxbWZBRHWYJbB7NMshfT2yAlDvhJkOqCTJIY3etyokVsnKBdRHovutTw5kRQRe8XKdnFMiZ+iL5Sbfxa2bUON+SBDnSgA/2c9CfXO4iiABgAAA==","length":1075}}} diff --git a/internal/handler/intestdata/get/package.json b/internal/handler/intestdata/get/package.json new file mode 100644 index 0000000..311077d --- /dev/null +++ b/internal/handler/intestdata/get/package.json @@ -0,0 +1,16 @@ +{ + "name": "@df/simplepackone", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid":"^9.x" + } +} diff --git a/internal/handler/intestdata/get/simplepackone-1.0.0.tgz b/internal/handler/intestdata/get/simplepackone-1.0.0.tgz new file mode 100644 index 0000000..bfd3bac Binary files /dev/null and b/internal/handler/intestdata/get/simplepackone-1.0.0.tgz differ diff --git a/internal/handler/intestdata/publish/normal/index.json b/internal/handler/intestdata/publish/normal/index.json new file mode 100644 index 0000000..7bff30e --- /dev/null +++ b/internal/handler/intestdata/publish/normal/index.json @@ -0,0 +1 @@ +{"_id":"@df/simplepackone","name":"@df/simplepackone","description":"This is a very rough implementation of a private npm registry.","dist-tags":{"latest":"1.0.0"},"time":{"1.0.0":"2023-12-24T11:04:34-05:00","created":"2023-12-24T11:04:34-05:00","modified":"2023-12-24T11:04:34-05:00","unpublished":""},"versions":{"1.0.0":{"name":"@df/simplepackone","version":"1.0.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","repository":{"type":"","url":"","directory":""},"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","readmeFilename":"README.md","_id":"@df/simplepackone@1.0.0","maintainers.omitempty":null,"bugs":{},"bin":null,"engines":{},"_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-9NI+Kqf+4C8Rr5GPKen8o6hhp/LMMeox96du65v6T+W27irVSsxZP0grBHBFyfd9whDXOoSliRWvYBnGnELlnA==","shasum":"a3974e824557b8e7ba00b87fb4ec51e4d132fff2","tarball":"http://localhost:6000/@df/simplepackone/-/@df/simplepackone-1.0.0.tgz","fileCount":0,"unpackedSize":0},"dependencies":{"eslint":"^7.x","mocha":"^10.x","uuid":"^9.x"}}},"access":"","_attachments":{"@df/simplepackone-1.0.0.tgz":{"content_type":"application/octet-stream","data":"H4sIAAAAAAAC/+1XbW/bNhDOZ/2KgwoUCSAospsXLJ+m2HQsTJYMSW4WoCggS7TFVhYFksoLhv73HSknTtp93Dys88EAwXt77o6+I9Xmxdd8TU9ZU9JH94s8+gfI87yLszP4K76h4Qc4+nB+4Q0GFxce6nkD73Lo4Xq0B+qkygWG8jckiQQv63+ECt5IXlO35utje0oFtU+ODvT/oXbb/9sVJwBv9tz/Z+dn3/f/4Px8cOj/fdAfFoDd5BtqX4H9a7k6lWzT1lT/HXhDbUeL76mQjDdaY+B6rtdzSyoLwVq1lfTMTc7M7vk66bm9okSBhkOGolJpNVpUHD7ZRAgurqDhoAUgW1qwFaPlJxvevwf6yBQMbLT8Zrzlnaq42GHWrKCNNAnMguw5uJZiCE3B6CtYKmvWGODPl+6j0dQx86LKDXPg7bhdx0r7yv78C7I0tPXN+pn7PyH+eEbcTbn/+384HPxw/59/GB76fx/0Dm543/JNu7GsrGIS8JcDNv0TCN6tKzDiDW1Urpsd+ArFrWD3uaKAViDomkklnlzLegcTmqtOUIm+4nHsuoZJmipvCuPjreCWi68QovUL93Al/yv9HwYjEqXEVY9qz/0/9M68H+5/7/Ly0P/7IGvE2yfB1pWC4+IEhvpj7M1EmFOxYVLf/3ouVPiFsHyCtcgbRUsHVoJSPRDwBhVr6oDikDdP0OKLQU+KpcL3AGvWVg4F4mhNpSeM5Cv1kAuKyiXkUvKC4TApoeRFtxs0K1ZTCceqopadbi3sEwNS0rwG1qA3Cs8ieGD4MOgUziOcRqzQPhxUKuqu1DE8i2u2YVsEbW6Sl9ppJzEDHacDG16ylV6pSavtljWTlWOVetCxZaeQKTXTPD0cnccpFyBpXWsP+Oroc91FZ3QQxWp1QdW2RAb3oeKbt5lgiVadaBCSliZdjiUziF9oobQXrb7idc0fMDWEbEqmM5JXeoZjXZf8nppc+rNtuMJQ+xD0AbS7U92KZJVj7Etq9QVDXCxv/iodoeGxWRrFsPYtFwbv+zRxqmdTAmk8yW79hECQwjyJPwZjMgbbT3FvO3AbZNN4kQFqJH6U3UE8AT+6g9+CaOxY5Pd5QtIU4gSC2TwMyNiBIBqFi3EQ3cA12kVxBmGAjz10msWgAbeuAoJ2E2tGktEUt/51EAbZnQOTIIu0zwk69WHuJ1kwWoR+AvNFMo9TgvBjK4qjIJokiEJmJMpcREUoIB9xA+nUD0MD5S8w+sTEN4rnd0lwM81gGodjgsxrYoWBfx2SHgqTGoV+MHNg7M/8G2KsYvSSgFbbRnc7JYaFeD7+RlkQRxbWZBRHWYJbB7NMshfT2yAlDvhJkOqCTJIY3etyokVsnKBdRHovutTw5kRQRe8XKdnFMiZ+iL5Sbfxa2bUON+SBDnSgA/2c9CfXO4iiABgAAA==","length":1075}}} diff --git a/internal/handler/intestdata/publish/normal/package.json b/internal/handler/intestdata/publish/normal/package.json new file mode 100644 index 0000000..311077d --- /dev/null +++ b/internal/handler/intestdata/publish/normal/package.json @@ -0,0 +1,16 @@ +{ + "name": "@df/simplepackone", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid":"^9.x" + } +} diff --git a/internal/handler/intestdata/publish/normal/simplepackone-1.0.0.tgz b/internal/handler/intestdata/publish/normal/simplepackone-1.0.0.tgz new file mode 100644 index 0000000..bfd3bac Binary files /dev/null and b/internal/handler/intestdata/publish/normal/simplepackone-1.0.0.tgz differ diff --git a/internal/handler/intestdata/publish/overwrite/index.json b/internal/handler/intestdata/publish/overwrite/index.json new file mode 100644 index 0000000..df8c59c --- /dev/null +++ b/internal/handler/intestdata/publish/overwrite/index.json @@ -0,0 +1 @@ +{"_id":"@df/simplepackone","name":"@df/simplepackone","description":"This is a very rough implementation of a private npm registry.","dist-tags":{"latest":"1.9.0"},"time":{"1.0.0":"2023-12-24T11:04:34-05:00","1.9.0":"2023-12-24T13:16:16-05:00","created":"2023-12-24T11:04:34-05:00","modified":"2023-12-24T13:16:16-05:00","unpublished":""},"versions":{"1.0.0":{"name":"@df/simplepackone","version":"1.0.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","repository":{"type":"","url":"","directory":""},"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","readmeFilename":"README.md","_id":"@df/simplepackone@1.0.0","maintainers.omitempty":null,"bugs":{},"bin":null,"engines":{},"_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-9NI+Kqf+4C8Rr5GPKen8o6hhp/LMMeox96du65v6T+W27irVSsxZP0grBHBFyfd9whDXOoSliRWvYBnGnELlnA==","shasum":"a3974e824557b8e7ba00b87fb4ec51e4d132fff2","tarball":"http://localhost:6000/@df/simplepackone/-/@df/simplepackone-1.0.0.tgz","fileCount":0,"unpackedSize":0},"dependencies":{"eslint":"^7.x","mocha":"^10.x","uuid":"^9.x"}},"1.9.0":{"name":"@df/simplepackone","version":"1.9.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","repository":{"type":"","url":"","directory":""},"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","readmeFilename":"README.md","_id":"@df/simplepackone@1.9.0","maintainers.omitempty":null,"bugs":{},"bin":null,"engines":{},"_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-TU3UpYnWUeZ4wQmwpMIZGLSPWYkbAP3pOdHmlX2nelmd2zhsCRWHoOLaCyxOrsn6KAmR0M9da5ggHZ9NUnGiCA==","shasum":"047e8445501a52ce658a036f8bff356f8d762762","tarball":"http://localhost:6000/@df%2Fsimplepackone/-/@df%2Fsimplepackone-1.9.0.tgz","fileCount":0,"unpackedSize":0},"dependencies":{"csv":"^6.x","eslint":"^7.x","mocha":"^10.x","uuid":"^9.x"}}},"access":"","_attachments":{"@df/simplepackone-1.9.0.tgz":{"content_type":"application/octet-stream","data":"H4sIAAAAAAAC/+1XbW/bNhDOZ/2KgwoUCSAospM4aD5NselYqCwZktwsQFFAlmiLrSwKJJUXDP3vO9JOnLT7uHlY54MBgvf23B19R6rNi2/5ip6ypqSP7ld59A+Q53mD83P4K76h/hkcnV0MvF5vMPBQz+t5l30P16M9UCdVLjCUvyFJJHhZ/yNU8Ebymro1Xx3bEyqofXJ0oP8Ptdv+3644AXiz5/4/vxz82P+9i4uLQ//vg/6wAOwmX1P7CuzfyuWpZOu2pvrvwBtqO1p8T4VkvNEaPfeD6224JZWFYK3aSjbMdc7M7vk62XA3ihIFGg4Zikql1WhRcfhsEyG4uIKGgxaAbGnBloyWn214/x7oI1PQs9Hyu/GWd6riYodZs4I20iQwDbLn4FqKITQFo69gqaxZY4C/XLqPRlPHzIsqN8yet+N2HSvtK/vLhx2rkPeaM0CODsb6bv06/Z8QfzQl7rrc//3f7/d+uv8vzvqH/t8HvYMbvmn5pl1bVlYxCfjLAZv+CQTvVhUY8Zo2KtfNDnyJ4law+1xRQCsQdMWkEk+uZb2DMc1VJ6hEX/Eodl3DJE2VN4Xx8VZwy8U3CNH6hXu4kv+V/g+DIYlS4qpHtef+73vn3k/3v3d5eej/fZA15O2TYKtKwXFxAn39MfZmIsyoWDOp7389Fyr8Qlg8wUrkjaKlA0tBqR4IeIOKFXVAccibJ2jxxaAnxULhe4A1KyuHAnG0ptITRvKlesgFReUScil5wXCYlFDyotsNmiWrqYRjVVHLTrcW9okBKWleA2vQG4VnETwwfBh0CucRTiNWaB8OKhV1V+oYnsU1W7MtgjY3yUvttJOYgY7TgTUv2VKv1KTVdouaycqxSj3o2KJTyJSaaZ4ejs7jlAuQtK61B3x1bHLdRWd0EMVqdUHVtkQG96Hi67eZYImWnWgQkpYmXY4lM4hfaaG0F62+5HXNHzA1hGxKpjOSV3qGY10X/J6aXDZn23CFoW5C0AfQ7k51K5JVjrEvqLUpGOJiefNX6QgNj83SKIa1b7kweD+miVM9mxBI43F26ycEghRmSfwpGJER2H6Ke9uB2yCbxPMMUCPxo+wO4jH40R18DKKRY5HfZwlJU4gTCKazMCAjB4JoGM5HQXQD12gXxRmEAT720GkWgwbcugoI2o2tKUmGE9z610EYZHcOjIMs0j7H6NSHmZ9kwXAe+gnM5sksTgnCj6wojoJonCAKmZIocxEVoYB8wg2kEz8MDZQ/x+gTE98wnt0lwc0kg0kcjggyr4kVBv51SDZQmNQw9IOpAyN/6t8QYxWjlwS02ja62wkxLMTz8TfMgjiysCbDOMoS3DqYZZK9mN4GKXHAT4JUF2ScxOhelxMtYuME7SKy8aJLDW9OBFX0fp6SXSwj4ofoK9XGr5Vd63BDHuhABzrQr0l/AlppbUsAGAAA","length":1083}}} diff --git a/internal/handler/intestdata/publish/overwrite/package.json b/internal/handler/intestdata/publish/overwrite/package.json new file mode 100644 index 0000000..f61d63f --- /dev/null +++ b/internal/handler/intestdata/publish/overwrite/package.json @@ -0,0 +1,17 @@ +{ + "name": "@df/simplepackone", + "version": "1.9.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid":"^9.x", + "csv":"^6.x" + } +} diff --git a/internal/handler/intestdata/publish/overwrite/simplepackone-1.9.0.tgz b/internal/handler/intestdata/publish/overwrite/simplepackone-1.9.0.tgz new file mode 100644 index 0000000..12da379 Binary files /dev/null and b/internal/handler/intestdata/publish/overwrite/simplepackone-1.9.0.tgz differ diff --git a/internal/handler/intestdata/tags/index.json b/internal/handler/intestdata/tags/index.json new file mode 100644 index 0000000..5afee60 --- /dev/null +++ b/internal/handler/intestdata/tags/index.json @@ -0,0 +1 @@ +{"_id":"@df/simplepackone","name":"@df/simplepackone","description":"This is a very rough implementation of a private npm registry.","dist-tags":{"latest":"1.9.0", "pre-alpha":"1.0.0"},"time":{"1.0.0":"2023-12-24T11:04:34-05:00","1.9.0":"2023-12-24T13:16:16-05:00","created":"2023-12-24T11:04:34-05:00","modified":"2023-12-24T13:16:16-05:00","unpublished":""},"versions":{"1.0.0":{"name":"@df/simplepackone","version":"1.0.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","repository":{"type":"","url":"","directory":""},"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","readmeFilename":"README.md","_id":"@df/simplepackone@1.0.0","maintainers.omitempty":null,"bugs":{},"bin":null,"engines":{},"_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-9NI+Kqf+4C8Rr5GPKen8o6hhp/LMMeox96du65v6T+W27irVSsxZP0grBHBFyfd9whDXOoSliRWvYBnGnELlnA==","shasum":"a3974e824557b8e7ba00b87fb4ec51e4d132fff2","tarball":"http://localhost:6000/@df/simplepackone/-/@df/simplepackone-1.0.0.tgz","fileCount":0,"unpackedSize":0},"dependencies":{"eslint":"^7.x","mocha":"^10.x","uuid":"^9.x"}},"1.9.0":{"name":"@df/simplepackone","version":"1.9.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","repository":{"type":"","url":"","directory":""},"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","readmeFilename":"README.md","_id":"@df/simplepackone@1.9.0","maintainers.omitempty":null,"bugs":{},"bin":null,"engines":{},"_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-TU3UpYnWUeZ4wQmwpMIZGLSPWYkbAP3pOdHmlX2nelmd2zhsCRWHoOLaCyxOrsn6KAmR0M9da5ggHZ9NUnGiCA==","shasum":"047e8445501a52ce658a036f8bff356f8d762762","tarball":"http://localhost:6000/@df%2Fsimplepackone/-/@df%2Fsimplepackone-1.9.0.tgz","fileCount":0,"unpackedSize":0},"dependencies":{"csv":"^6.x","eslint":"^7.x","mocha":"^10.x","uuid":"^9.x"}}},"access":"","_attachments":{"@df/simplepackone-1.9.0.tgz":{"content_type":"application/octet-stream","data":"H4sIAAAAAAAC/+1XbW/bNhDOZ/2KgwoUCSAospM4aD5NselYqCwZktwsQFFAlmiLrSwKJJUXDP3vO9JOnLT7uHlY54MBgvf23B19R6rNi2/5ip6ypqSP7ld59A+Q53mD83P4K76h/hkcnV0MvF5vMPBQz+t5l30P16M9UCdVLjCUvyFJJHhZ/yNU8Ebymro1Xx3bEyqofXJ0oP8Ptdv+3644AXiz5/4/vxz82P+9i4uLQ//vg/6wAOwmX1P7CuzfyuWpZOu2pvrvwBtqO1p8T4VkvNEaPfeD6224JZWFYK3aSjbMdc7M7vk62XA3ihIFGg4Zikql1WhRcfhsEyG4uIKGgxaAbGnBloyWn214/x7oI1PQs9Hyu/GWd6riYodZs4I20iQwDbLn4FqKITQFo69gqaxZY4C/XLqPRlPHzIsqN8yet+N2HSvtK/vLhx2rkPeaM0CODsb6bv06/Z8QfzQl7rrc//3f7/d+uv8vzvqH/t8HvYMbvmn5pl1bVlYxCfjLAZv+CQTvVhUY8Zo2KtfNDnyJ4law+1xRQCsQdMWkEk+uZb2DMc1VJ6hEX/Eodl3DJE2VN4Xx8VZwy8U3CNH6hXu4kv+V/g+DIYlS4qpHtef+73vn3k/3v3d5eej/fZA15O2TYKtKwXFxAn39MfZmIsyoWDOp7389Fyr8Qlg8wUrkjaKlA0tBqR4IeIOKFXVAccibJ2jxxaAnxULhe4A1KyuHAnG0ptITRvKlesgFReUScil5wXCYlFDyotsNmiWrqYRjVVHLTrcW9okBKWleA2vQG4VnETwwfBh0CucRTiNWaB8OKhV1V+oYnsU1W7MtgjY3yUvttJOYgY7TgTUv2VKv1KTVdouaycqxSj3o2KJTyJSaaZ4ejs7jlAuQtK61B3x1bHLdRWd0EMVqdUHVtkQG96Hi67eZYImWnWgQkpYmXY4lM4hfaaG0F62+5HXNHzA1hGxKpjOSV3qGY10X/J6aXDZn23CFoW5C0AfQ7k51K5JVjrEvqLUpGOJiefNX6QgNj83SKIa1b7kweD+miVM9mxBI43F26ycEghRmSfwpGJER2H6Ke9uB2yCbxPMMUCPxo+wO4jH40R18DKKRY5HfZwlJU4gTCKazMCAjB4JoGM5HQXQD12gXxRmEAT720GkWgwbcugoI2o2tKUmGE9z610EYZHcOjIMs0j7H6NSHmZ9kwXAe+gnM5sksTgnCj6wojoJonCAKmZIocxEVoYB8wg2kEz8MDZQ/x+gTE98wnt0lwc0kg0kcjggyr4kVBv51SDZQmNQw9IOpAyN/6t8QYxWjlwS02ja62wkxLMTz8TfMgjiysCbDOMoS3DqYZZK9mN4GKXHAT4JUF2ScxOhelxMtYuME7SKy8aJLDW9OBFX0fp6SXSwj4ofoK9XGr5Vd63BDHuhABzrQr0l/AlppbUsAGAAA","length":1083}}} diff --git a/internal/handler/intestdata/tags/package.json b/internal/handler/intestdata/tags/package.json new file mode 100644 index 0000000..f61d63f --- /dev/null +++ b/internal/handler/intestdata/tags/package.json @@ -0,0 +1,17 @@ +{ + "name": "@df/simplepackone", + "version": "1.9.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid":"^9.x", + "csv":"^6.x" + } +} diff --git a/internal/handler/intestdata/tags/simplepackone-1.0.0.tgz b/internal/handler/intestdata/tags/simplepackone-1.0.0.tgz new file mode 100644 index 0000000..bfd3bac Binary files /dev/null and b/internal/handler/intestdata/tags/simplepackone-1.0.0.tgz differ diff --git a/internal/handler/intestdata/tags/simplepackone-1.9.0.tgz b/internal/handler/intestdata/tags/simplepackone-1.9.0.tgz new file mode 100644 index 0000000..12da379 Binary files /dev/null and b/internal/handler/intestdata/tags/simplepackone-1.9.0.tgz differ diff --git a/internal/handler/intestdata/tar/index.json b/internal/handler/intestdata/tar/index.json new file mode 100644 index 0000000..7bff30e --- /dev/null +++ b/internal/handler/intestdata/tar/index.json @@ -0,0 +1 @@ +{"_id":"@df/simplepackone","name":"@df/simplepackone","description":"This is a very rough implementation of a private npm registry.","dist-tags":{"latest":"1.0.0"},"time":{"1.0.0":"2023-12-24T11:04:34-05:00","created":"2023-12-24T11:04:34-05:00","modified":"2023-12-24T11:04:34-05:00","unpublished":""},"versions":{"1.0.0":{"name":"@df/simplepackone","version":"1.0.0","description":"This is a very rough implementation of a private npm registry.","main":"index.js","scripts":{"test":"echo \"Error: no test specified\" \u0026\u0026 exit 1"},"license":"MIT","repository":{"type":"","url":"","directory":""},"readme":"# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...","readmeFilename":"README.md","_id":"@df/simplepackone@1.0.0","maintainers.omitempty":null,"bugs":{},"bin":null,"engines":{},"_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-9NI+Kqf+4C8Rr5GPKen8o6hhp/LMMeox96du65v6T+W27irVSsxZP0grBHBFyfd9whDXOoSliRWvYBnGnELlnA==","shasum":"a3974e824557b8e7ba00b87fb4ec51e4d132fff2","tarball":"http://localhost:6000/@df/simplepackone/-/@df/simplepackone-1.0.0.tgz","fileCount":0,"unpackedSize":0},"dependencies":{"eslint":"^7.x","mocha":"^10.x","uuid":"^9.x"}}},"access":"","_attachments":{"@df/simplepackone-1.0.0.tgz":{"content_type":"application/octet-stream","data":"H4sIAAAAAAAC/+1XbW/bNhDOZ/2KgwoUCSAospsXLJ+m2HQsTJYMSW4WoCggS7TFVhYFksoLhv73HSknTtp93Dys88EAwXt77o6+I9Xmxdd8TU9ZU9JH94s8+gfI87yLszP4K76h4Qc4+nB+4Q0GFxce6nkD73Lo4Xq0B+qkygWG8jckiQQv63+ECt5IXlO35utje0oFtU+ODvT/oXbb/9sVJwBv9tz/Z+dn3/f/4Px8cOj/fdAfFoDd5BtqX4H9a7k6lWzT1lT/HXhDbUeL76mQjDdaY+B6rtdzSyoLwVq1lfTMTc7M7vk66bm9okSBhkOGolJpNVpUHD7ZRAgurqDhoAUgW1qwFaPlJxvevwf6yBQMbLT8Zrzlnaq42GHWrKCNNAnMguw5uJZiCE3B6CtYKmvWGODPl+6j0dQx86LKDXPg7bhdx0r7yv78C7I0tPXN+pn7PyH+eEbcTbn/+384HPxw/59/GB76fx/0Dm543/JNu7GsrGIS8JcDNv0TCN6tKzDiDW1Urpsd+ArFrWD3uaKAViDomkklnlzLegcTmqtOUIm+4nHsuoZJmipvCuPjreCWi68QovUL93Al/yv9HwYjEqXEVY9qz/0/9M68H+5/7/Ly0P/7IGvE2yfB1pWC4+IEhvpj7M1EmFOxYVLf/3ouVPiFsHyCtcgbRUsHVoJSPRDwBhVr6oDikDdP0OKLQU+KpcL3AGvWVg4F4mhNpSeM5Cv1kAuKyiXkUvKC4TApoeRFtxs0K1ZTCceqopadbi3sEwNS0rwG1qA3Cs8ieGD4MOgUziOcRqzQPhxUKuqu1DE8i2u2YVsEbW6Sl9ppJzEDHacDG16ylV6pSavtljWTlWOVetCxZaeQKTXTPD0cnccpFyBpXWsP+Oroc91FZ3QQxWp1QdW2RAb3oeKbt5lgiVadaBCSliZdjiUziF9oobQXrb7idc0fMDWEbEqmM5JXeoZjXZf8nppc+rNtuMJQ+xD0AbS7U92KZJVj7Etq9QVDXCxv/iodoeGxWRrFsPYtFwbv+zRxqmdTAmk8yW79hECQwjyJPwZjMgbbT3FvO3AbZNN4kQFqJH6U3UE8AT+6g9+CaOxY5Pd5QtIU4gSC2TwMyNiBIBqFi3EQ3cA12kVxBmGAjz10msWgAbeuAoJ2E2tGktEUt/51EAbZnQOTIIu0zwk69WHuJ1kwWoR+AvNFMo9TgvBjK4qjIJokiEJmJMpcREUoIB9xA+nUD0MD5S8w+sTEN4rnd0lwM81gGodjgsxrYoWBfx2SHgqTGoV+MHNg7M/8G2KsYvSSgFbbRnc7JYaFeD7+RlkQRxbWZBRHWYJbB7NMshfT2yAlDvhJkOqCTJIY3etyokVsnKBdRHovutTw5kRQRe8XKdnFMiZ+iL5Sbfxa2bUON+SBDnSgA/2c9CfXO4iiABgAAA==","length":1075}}} diff --git a/internal/handler/intestdata/tar/package.json b/internal/handler/intestdata/tar/package.json new file mode 100644 index 0000000..311077d --- /dev/null +++ b/internal/handler/intestdata/tar/package.json @@ -0,0 +1,16 @@ +{ + "name": "@df/simplepackone", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "MIT", + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid":"^9.x" + } +} diff --git a/internal/handler/intestdata/tar/simplepackone-1.0.0.tgz b/internal/handler/intestdata/tar/simplepackone-1.0.0.tgz new file mode 100644 index 0000000..bfd3bac Binary files /dev/null and b/internal/handler/intestdata/tar/simplepackone-1.0.0.tgz differ diff --git a/internal/handler/publish.go b/internal/handler/publish.go index 27af380..a54812b 100644 --- a/internal/handler/publish.go +++ b/internal/handler/publish.go @@ -9,7 +9,9 @@ import ( "net/http" "net/url" "path" + "strconv" "strings" + "time" "github.com/gorilla/mux" "github.com/sirupsen/logrus" @@ -19,7 +21,7 @@ type NPMClientPutRequest struct { Request serviceidos.IndexJson } -func Publish(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { +func Publish(lg *logrus.Logger, cfg config.Config, stg storage.Storage) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // (1) Parse Json Body // (2) Check if package exists in the folder. @@ -45,6 +47,7 @@ func Publish(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { } // Extract relevant data from index.json + fmt.Printf("cRequest => %+v\n", cr) index := 0 var tag string var version string @@ -59,26 +62,34 @@ func Publish(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { index++ } versionData = cr.Request.Versions[version] + lg.WithFields(logrus.Fields{ "function": "publish", }).Debugf("For version(%s) with tag(%s), versionData => %+v\n", version, tag, versionData) // Rewrite the tarball path tarballFileName := strings.Split(versionData.Dist.Tarball, "/-/")[1] + tarballFileName, _ = url.PathUnescape(tarballFileName) + lg.WithFields(logrus.Fields{ "function": "publish", }).Debugf("TarballName => %s\n", tarballFileName) // versionData.Dist.Tarball = fmt.Sprintf("file://%s", packageFilePath) versionData.Dist.Tarball = fmt.Sprintf("http://%s/%s/-/%s", r.Host, url.PathEscape(packageName), url.PathEscape(tarballFileName)) + lg.WithFields(logrus.Fields{ "function": "publish", }).Debugf("versionData.Dist.Tarball => %s\n", versionData.Dist.Tarball) tarBallFile := strings.Split(tarballFileName, "/")[1] packageFilePath := path.Join(cfg.RepoDir, packageName, tarBallFile) + lg.WithFields(logrus.Fields{ + "function": "publish", + }).Debugf("PackageFilePath => %s\n", packageFilePath) + fmt.Printf("PackageFilePath => %s\n", packageFilePath) // Try to get the index.json from the store - fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg) + fileToServe, found, err := stg.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -89,9 +100,16 @@ func Publish(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { // new package jsonFile = cr.Request jsonFile.DistTags["latest"] = version + curTime := time.Now().Format(time.RFC3339) + jsonFile.TimesPackage = map[string]string{ + version: curTime, + "created": curTime, + "modified": curTime, + "unpublished": "", + } } else { // old package - err = storage.ReadIndexJson(fileToServe, &jsonFile, lg) + err = stg.ReadIndexJson(fileToServe, &jsonFile, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -102,7 +120,7 @@ func Publish(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { lg.WithFields(logrus.Fields{ "function": "publish", }).Debugf("Version %s of package %s already exists!!\n", version, packageName) - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Version %s of package %s already exists!!\n", version, packageName), http.StatusBadRequest) return } @@ -114,6 +132,12 @@ func Publish(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { // Merge in the new version data jsonFile.Versions[version] = versionData + + // Update the time field + timesPackages := jsonFile.TimesPackage + curTime := time.Now().Format(time.RFC3339) + timesPackages["modified"] = curTime + timesPackages[version] = curTime } lg.WithFields(logrus.Fields{ @@ -121,7 +145,7 @@ func Publish(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { }).Debugln("FiletoServe ==> ", fileToServe) // Write index.json - err = storage.WriteIndexJson(fileToServe, &jsonFile, lg) + err = stg.WriteIndexJson(fileToServe, &jsonFile, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -132,11 +156,22 @@ func Publish(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { }).Debugln("Package path => ", packageFilePath) // Write bundled package packageData := jsonFile.Attachments[fmt.Sprintf("%s-%s.tgz", packageName, version)].Data - err = storage.WritePackageToStore(packageFilePath, packageData, lg) + err = stg.WritePackageToStore(packageFilePath, packageData, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } + + response := serviceidos.PublishPutResponse{ + Ok: true, + Name: packageName, + } + jsonString, _ := json.Marshal(response) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + w.Header().Set("Content-Length", strconv.Itoa(len(jsonString))) + w.Write(jsonString) } } diff --git a/internal/handler/publish_test.go b/internal/handler/publish_test.go new file mode 100644 index 0000000..49e3128 --- /dev/null +++ b/internal/handler/publish_test.go @@ -0,0 +1,715 @@ +package handler + +import ( + "bytes" + "fmt" + "gosimplenpm/internal/config" + "gosimplenpm/internal/serviceidos" + "gosimplenpm/internal/storage" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestUnitPublish(t *testing.T) { + t.Run("return `Internal Server` error if index.json cannot be retrieved", func(t *testing.T) { + jsonBody := []byte( + `{ + "_id": "@ookusanya/simplepackone", + "name": "@ookusanya/simplepackone", + "description": "This is a very rough implementation of a private npm registry.", + "dist-tags": { + "latest": "1.2.0" + }, + "versions": { + "1.2.0": { + "name": "@ookusanya/simplepackone", + "version": "1.2.0", + "description": "This is a very rough implementation of a private npm registry.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "MIT", + "files": null, + "readme": "# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...", + "_id": "@ookusanya/simplepackone@1.2.0", + "_nodeVersion": "16.20.0", + "_npmVersion": "8.19.4", + "dist": { + "integrity": "sha512-coOiU+ywV/do/+HwK91mfuei9491yfQUedLWjspAjDa58RniJvQNTF/cXDp/sooVdpjgEbCqKVyLOju6C1i3pw==", + "shasum": "1adafdbe8878372c9f4b554bf30d5bfb1b7896c5", + "tarball": "http://localhost:4000/@ookusanya%2Fsimplepackone/-/@ookusanya%2Fsimplepackone-1.2.0.tgz" + }, + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid": "^9.x" + } + } + }, + "access": "", + "_attachments": { + "@ookusanya/simplepackone-1.2.0.tgz": { + "content_type": "application/octet-stream", + "data": "H4sIAAAAAAAC/+1XbWvjOBDuZ/+KwQtLCyZ10pfl+uncRGnMOnawne0VlgXVVmJtHctIcl849r/fSEmb9nof73LcXoaA0Lw9M6PMSG5pcUeX7Jg3JXvsfVcH/wD5vn9+egp/xbc0OIGDk7Nzv98/P/dRz+/7nwY+rgc7oE5pKjGUvyFJJHhZ/yNUiEaJmvVqsTx0J0wy9+hgT/8fajf9v1lxAohmx/1/ev6u//tnZ/19/++CfncA3IaumHsB7q9C3HWKNk/0WPFVWzPzrxANcz2jdc+k4qIxiv3eoOevuSVTheSt3kjWzBXldvd8q6y5a0WFAoOKDM2UNmqsqAR8dYmUQl5AI8AIQLWs4AvOyq8ufPwI7JFr6Lto+cN6o52uhNxi1rxgjbJ5TMP8ObiWYQhNwdkrWKZq3ljgb596j1bTxCyKilpm399yu46X7oX77RdkGWjnh/NT9n9KgtGU9Fbl7u//waD/7v4/Oxns+38X9AGuxLrXm3blOHnFFeCPAnb7E0jRLSuw4hVrNDVdDmKB4lbye6oZoBVItuRKy6ee43yAMaO6k0yhr2SU9HqWSZqKNoX18VZwLeQdRGj9wt1fyf9K/0fhkMQZ6elHveP+H/hng3f3f9/f9/9OyBmK9knyZaXhsDiCgfkYS+rugZZ0xWtRU0g2TwLHmTG54sq8AcyIqPBj4fYJlpI2mpUeLCRjZjbgLSqXzAMtAM2gxVeDGRq3Gt8EvFk6FAqENJraDBslFvqBSobKJVClRMFxrpRQiqLbzpwFr5mCQ10xx802Fu6RBSkZrYE36I3BswgeOD4OOo2jCQcTL4wPD5WKuitNDM/imq/4BsGY2zoo47RTmIGJ04OVKPnCrMym1Xa3NVeV55Rm5vHbTiNTGaZ9fngmj2MhQbG6Nh7w5bHOdRud1UEUpzUF1ZsSWdyHSqzeZoIlWnSyQUhW2nQFlswifmeFNl6M+kLUtXjA1BCyKbnJSF2YcY51vRX3zOayPuZGaAx1HYI5gHZ7qhuRqijGfsucdcEQF8tLX6UjDTz2TaM51r4V0uL9OU0c8PmEQJaM8+sgJRBmMEuTL+GIjMANMty7HlyH+SSZ54AaaRDnN5CMIYhv4HMYjzyH/DZLSZZBkkI4nUUhGXkQxsNoPgrjK7hEuzjJIQrxwYdO8wQM4MZVSNBu7ExJOpzgNrgMozC/8WAc5rHxOUanAcyCNA+H8yhIYTZPZ0lGEH7kxEkcxuMUUciUxHkPUREKyBfcQDYJoshCBXOMPrXxDZPZTRpeTXKYJNGIIPOSOFEYXEZkDYVJDaMgnHowCqbBFbFWCXpJwahtorueEMtCvAB/wzxMYgdrMkziPMWth1mm+YvpdZgRD4I0zExBxmmC7k050SKxTtAuJmsvptTw5kRQxeznGdnGMiJBhL4yY/xaeX8x72lPe9rTT0V/APlm4rcAGAAA", + "length": 1089 + } + } + }`, + ) + req := httptest.NewRequest(http.MethodPut, "/{name}", bytes.NewReader(jsonBody)) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", false, fmt.Errorf("Filesystem error") + } + + vars := map[string]string{ + "name": "@ookusanya%2Fsimplepackone", + } + + req = mux.SetURLVars(req, vars) + + Publish(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Filesystem error\n") + }) + + t.Run("return `Internal Server` error if index.json cannot be decoded", func(t *testing.T) { + jsonBody := []byte( + `{ + "_id": "@ookusanya/simplepackone", + "name": "@ookusanya/simplepackone", + "description": "This is a very rough implementation of a private npm registry.", + "dist-tags": { + "latest": "1.2.0" + }, + "versions": { + "1.2.0": { + "name": "@ookusanya/simplepackone", + "version": "1.2.0", + "description": "This is a very rough implementation of a private npm registry.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "MIT", + "files": null, + "readme": "# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...", + "_id": "@ookusanya/simplepackone@1.2.0", + "_nodeVersion": "16.20.0", + "_npmVersion": "8.19.4", + "dist": { + "integrity": "sha512-coOiU+ywV/do/+HwK91mfuei9491yfQUedLWjspAjDa58RniJvQNTF/cXDp/sooVdpjgEbCqKVyLOju6C1i3pw==", + "shasum": "1adafdbe8878372c9f4b554bf30d5bfb1b7896c5", + "tarball": "http://localhost:4000/@ookusanya%2Fsimplepackone/-/@ookusanya%2Fsimplepackone-1.2.0.tgz" + }, + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid": "^9.x" + } + } + }, + "access": "", + "_attachments": { + "@ookusanya/simplepackone-1.2.0.tgz": { + "content_type": "application/octet-stream", + "data": "H4sIAAAAAAAC/+1XbWvjOBDuZ/+KwQtLCyZ10pfl+uncRGnMOnawne0VlgXVVmJtHctIcl849r/fSEmb9nof73LcXoaA0Lw9M6PMSG5pcUeX7Jg3JXvsfVcH/wD5vn9+egp/xbc0OIGDk7Nzv98/P/dRz+/7nwY+rgc7oE5pKjGUvyFJJHhZ/yNUiEaJmvVqsTx0J0wy9+hgT/8fajf9v1lxAohmx/1/ev6u//tnZ/19/++CfncA3IaumHsB7q9C3HWKNk/0WPFVWzPzrxANcz2jdc+k4qIxiv3eoOevuSVTheSt3kjWzBXldvd8q6y5a0WFAoOKDM2UNmqsqAR8dYmUQl5AI8AIQLWs4AvOyq8ufPwI7JFr6Lto+cN6o52uhNxi1rxgjbJ5TMP8ObiWYQhNwdkrWKZq3ljgb596j1bTxCyKilpm399yu46X7oX77RdkGWjnh/NT9n9KgtGU9Fbl7u//waD/7v4/Oxns+38X9AGuxLrXm3blOHnFFeCPAnb7E0jRLSuw4hVrNDVdDmKB4lbye6oZoBVItuRKy6ee43yAMaO6k0yhr2SU9HqWSZqKNoX18VZwLeQdRGj9wt1fyf9K/0fhkMQZ6elHveP+H/hng3f3f9/f9/9OyBmK9knyZaXhsDiCgfkYS+rugZZ0xWtRU0g2TwLHmTG54sq8AcyIqPBj4fYJlpI2mpUeLCRjZjbgLSqXzAMtAM2gxVeDGRq3Gt8EvFk6FAqENJraDBslFvqBSobKJVClRMFxrpRQiqLbzpwFr5mCQ10xx802Fu6RBSkZrYE36I3BswgeOD4OOo2jCQcTL4wPD5WKuitNDM/imq/4BsGY2zoo47RTmIGJ04OVKPnCrMym1Xa3NVeV55Rm5vHbTiNTGaZ9fngmj2MhQbG6Nh7w5bHOdRud1UEUpzUF1ZsSWdyHSqzeZoIlWnSyQUhW2nQFlswifmeFNl6M+kLUtXjA1BCyKbnJSF2YcY51vRX3zOayPuZGaAx1HYI5gHZ7qhuRqijGfsucdcEQF8tLX6UjDTz2TaM51r4V0uL9OU0c8PmEQJaM8+sgJRBmMEuTL+GIjMANMty7HlyH+SSZ54AaaRDnN5CMIYhv4HMYjzyH/DZLSZZBkkI4nUUhGXkQxsNoPgrjK7hEuzjJIQrxwYdO8wQM4MZVSNBu7ExJOpzgNrgMozC/8WAc5rHxOUanAcyCNA+H8yhIYTZPZ0lGEH7kxEkcxuMUUciUxHkPUREKyBfcQDYJoshCBXOMPrXxDZPZTRpeTXKYJNGIIPOSOFEYXEZkDYVJDaMgnHowCqbBFbFWCXpJwahtorueEMtCvAB/wzxMYgdrMkziPMWth1mm+YvpdZgRD4I0zExBxmmC7k050SKxTtAuJmsvptTw5kRQxeznGdnGMiJBhL4yY/xaeX8x72lPe9rTT0V/APlm4rcAGAAA", + "length": 1089 + } + } + }`, + ) + req := httptest.NewRequest(http.MethodPut, "/{name}", bytes.NewReader(jsonBody)) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return string(jsonBody), true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return fmt.Errorf("Filesystem error") + } + + vars := map[string]string{ + "name": "@ookusanya%2Fsimplepackone", + } + + req = mux.SetURLVars(req, vars) + + Publish(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Filesystem error\n") + }) + + t.Run("return `Bad Request` error if version to publish already exists", func(t *testing.T) { + jsonBody := []byte( + `{ + "_id": "@ookusanya/simplepackone", + "name": "@ookusanya/simplepackone", + "description": "This is a very rough implementation of a private npm registry.", + "dist-tags": { + "latest": "1.2.0" + }, + "versions": { + "1.2.0": { + "name": "@ookusanya/simplepackone", + "version": "1.2.0", + "description": "This is a very rough implementation of a private npm registry.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "MIT", + "files": null, + "readme": "# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...", + "_id": "@ookusanya/simplepackone@1.2.0", + "_nodeVersion": "16.20.0", + "_npmVersion": "8.19.4", + "dist": { + "integrity": "sha512-coOiU+ywV/do/+HwK91mfuei9491yfQUedLWjspAjDa58RniJvQNTF/cXDp/sooVdpjgEbCqKVyLOju6C1i3pw==", + "shasum": "1adafdbe8878372c9f4b554bf30d5bfb1b7896c5", + "tarball": "http://localhost:4000/@ookusanya%2Fsimplepackone/-/@ookusanya%2Fsimplepackone-1.2.0.tgz" + }, + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid": "^9.x" + } + } + }, + "access": "", + "_attachments": { + "@ookusanya/simplepackone-1.2.0.tgz": { + "content_type": "application/octet-stream", + "data": "H4sIAAAAAAAC/+1XbWvjOBDuZ/+KwQtLCyZ10pfl+uncRGnMOnawne0VlgXVVmJtHctIcl849r/fSEmb9nof73LcXoaA0Lw9M6PMSG5pcUeX7Jg3JXvsfVcH/wD5vn9+egp/xbc0OIGDk7Nzv98/P/dRz+/7nwY+rgc7oE5pKjGUvyFJJHhZ/yNUiEaJmvVqsTx0J0wy9+hgT/8fajf9v1lxAohmx/1/ev6u//tnZ/19/++CfncA3IaumHsB7q9C3HWKNk/0WPFVWzPzrxANcz2jdc+k4qIxiv3eoOevuSVTheSt3kjWzBXldvd8q6y5a0WFAoOKDM2UNmqsqAR8dYmUQl5AI8AIQLWs4AvOyq8ufPwI7JFr6Lto+cN6o52uhNxi1rxgjbJ5TMP8ObiWYQhNwdkrWKZq3ljgb596j1bTxCyKilpm399yu46X7oX77RdkGWjnh/NT9n9KgtGU9Fbl7u//waD/7v4/Oxns+38X9AGuxLrXm3blOHnFFeCPAnb7E0jRLSuw4hVrNDVdDmKB4lbye6oZoBVItuRKy6ee43yAMaO6k0yhr2SU9HqWSZqKNoX18VZwLeQdRGj9wt1fyf9K/0fhkMQZ6elHveP+H/hng3f3f9/f9/9OyBmK9knyZaXhsDiCgfkYS+rugZZ0xWtRU0g2TwLHmTG54sq8AcyIqPBj4fYJlpI2mpUeLCRjZjbgLSqXzAMtAM2gxVeDGRq3Gt8EvFk6FAqENJraDBslFvqBSobKJVClRMFxrpRQiqLbzpwFr5mCQ10xx802Fu6RBSkZrYE36I3BswgeOD4OOo2jCQcTL4wPD5WKuitNDM/imq/4BsGY2zoo47RTmIGJ04OVKPnCrMym1Xa3NVeV55Rm5vHbTiNTGaZ9fngmj2MhQbG6Nh7w5bHOdRud1UEUpzUF1ZsSWdyHSqzeZoIlWnSyQUhW2nQFlswifmeFNl6M+kLUtXjA1BCyKbnJSF2YcY51vRX3zOayPuZGaAx1HYI5gHZ7qhuRqijGfsucdcEQF8tLX6UjDTz2TaM51r4V0uL9OU0c8PmEQJaM8+sgJRBmMEuTL+GIjMANMty7HlyH+SSZ54AaaRDnN5CMIYhv4HMYjzyH/DZLSZZBkkI4nUUhGXkQxsNoPgrjK7hEuzjJIQrxwYdO8wQM4MZVSNBu7ExJOpzgNrgMozC/8WAc5rHxOUanAcyCNA+H8yhIYTZPZ0lGEH7kxEkcxuMUUciUxHkPUREKyBfcQDYJoshCBXOMPrXxDZPZTRpeTXKYJNGIIPOSOFEYXEZkDYVJDaMgnHowCqbBFbFWCXpJwahtorueEMtCvAB/wzxMYgdrMkziPMWth1mm+YvpdZgRD4I0zExBxmmC7k050SKxTtAuJmsvptTw5kRQxeznGdnGMiJBhL4yY/xaeX8x72lPe9rTT0V/APlm4rcAGAAA", + "length": 1089 + } + } + }`, + ) + req := httptest.NewRequest(http.MethodPut, "/{name}", bytes.NewReader(jsonBody)) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return string(jsonBody), true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + temp := serviceidos.IndexJson{ + Versions: map[string]serviceidos.IndexJsonVersions{ + "1.2.0": { + Version: "1.2.0", + Name: "@ookusanya/simplepackone", + }, + }, + } + *res = temp + return nil + } + mfs.WriteIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return fmt.Errorf("Filesystem error") + } + + vars := map[string]string{ + "name": "@ookusanya%2Fsimplepackone", + } + + req = mux.SetURLVars(req, vars) + + Publish(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusBadRequest) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Version 1.2.0 of package @ookusanya/simplepackone already exists!!\n\n") + }) + + t.Run("return `Internal Server` error if writing index.json fails", func(t *testing.T) { + jsonBody := []byte( + `{ + "_id": "@ookusanya/simplepackone", + "name": "@ookusanya/simplepackone", + "description": "This is a very rough implementation of a private npm registry.", + "dist-tags": { + "latest": "1.3.0" + }, + "versions": { + "1.3.0": { + "name": "@ookusanya/simplepackone", + "version": "1.3.0", + "description": "This is a very rough implementation of a private npm registry.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "MIT", + "files": null, + "readme": "# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...", + "_id": "@ookusanya/simplepackone@1.2.0", + "_nodeVersion": "16.20.0", + "_npmVersion": "8.19.4", + "dist": { + "integrity": "sha512-coOiU+ywV/do/+HwK91mfuei9491yfQUedLWjspAjDa58RniJvQNTF/cXDp/sooVdpjgEbCqKVyLOju6C1i3pw==", + "shasum": "1adafdbe8878372c9f4b554bf30d5bfb1b7896c5", + "tarball": "http://localhost:4000/@ookusanya%2Fsimplepackone/-/@ookusanya%2Fsimplepackone-1.2.0.tgz" + }, + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid": "^9.x" + } + } + }, + "access": "", + "_attachments": { + "@ookusanya/simplepackone-1.3.0.tgz": { + "content_type": "application/octet-stream", + "data": "H4sIAAAAAAAC/+1XbWvjOBDuZ/+KwQtLCyZ10pfl+uncRGnMOnawne0VlgXVVmJtHctIcl849r/fSEmb9nof73LcXoaA0Lw9M6PMSG5pcUeX7Jg3JXvsfVcH/wD5vn9+egp/xbc0OIGDk7Nzv98/P/dRz+/7nwY+rgc7oE5pKjGUvyFJJHhZ/yNUiEaJmvVqsTx0J0wy9+hgT/8fajf9v1lxAohmx/1/ev6u//tnZ/19/++CfncA3IaumHsB7q9C3HWKNk/0WPFVWzPzrxANcz2jdc+k4qIxiv3eoOevuSVTheSt3kjWzBXldvd8q6y5a0WFAoOKDM2UNmqsqAR8dYmUQl5AI8AIQLWs4AvOyq8ufPwI7JFr6Lto+cN6o52uhNxi1rxgjbJ5TMP8ObiWYQhNwdkrWKZq3ljgb596j1bTxCyKilpm399yu46X7oX77RdkGWjnh/NT9n9KgtGU9Fbl7u//waD/7v4/Oxns+38X9AGuxLrXm3blOHnFFeCPAnb7E0jRLSuw4hVrNDVdDmKB4lbye6oZoBVItuRKy6ee43yAMaO6k0yhr2SU9HqWSZqKNoX18VZwLeQdRGj9wt1fyf9K/0fhkMQZ6elHveP+H/hng3f3f9/f9/9OyBmK9knyZaXhsDiCgfkYS+rugZZ0xWtRU0g2TwLHmTG54sq8AcyIqPBj4fYJlpI2mpUeLCRjZjbgLSqXzAMtAM2gxVeDGRq3Gt8EvFk6FAqENJraDBslFvqBSobKJVClRMFxrpRQiqLbzpwFr5mCQ10xx802Fu6RBSkZrYE36I3BswgeOD4OOo2jCQcTL4wPD5WKuitNDM/imq/4BsGY2zoo47RTmIGJ04OVKPnCrMym1Xa3NVeV55Rm5vHbTiNTGaZ9fngmj2MhQbG6Nh7w5bHOdRud1UEUpzUF1ZsSWdyHSqzeZoIlWnSyQUhW2nQFlswifmeFNl6M+kLUtXjA1BCyKbnJSF2YcY51vRX3zOayPuZGaAx1HYI5gHZ7qhuRqijGfsucdcEQF8tLX6UjDTz2TaM51r4V0uL9OU0c8PmEQJaM8+sgJRBmMEuTL+GIjMANMty7HlyH+SSZ54AaaRDnN5CMIYhv4HMYjzyH/DZLSZZBkkI4nUUhGXkQxsNoPgrjK7hEuzjJIQrxwYdO8wQM4MZVSNBu7ExJOpzgNrgMozC/8WAc5rHxOUanAcyCNA+H8yhIYTZPZ0lGEH7kxEkcxuMUUciUxHkPUREKyBfcQDYJoshCBXOMPrXxDZPZTRpeTXKYJNGIIPOSOFEYXEZkDYVJDaMgnHowCqbBFbFWCXpJwahtorueEMtCvAB/wzxMYgdrMkziPMWth1mm+YvpdZgRD4I0zExBxmmC7k050SKxTtAuJmsvptTw5kRQxeznGdnGMiJBhL4yY/xaeX8x72lPe9rTT0V/APlm4rcAGAAA", + "length": 1089 + } + } + }`, + ) + req := httptest.NewRequest(http.MethodPut, "/{name}", bytes.NewReader(jsonBody)) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return string(jsonBody), true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + temp := serviceidos.IndexJson{ + Versions: map[string]serviceidos.IndexJsonVersions{ + "1.2.0": { + Version: "1.2.0", + Name: "@ookusanya/simplepackone", + }, + }, + DistTags: map[string]string{ + "latest": "1.2.0", + }, + TimesPackage: map[string]string{ + "modified": "", + "created": "", + }, + } + *res = temp + return nil + } + mfs.WriteIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return fmt.Errorf("Filesystem error") + } + + vars := map[string]string{ + "name": "@ookusanya%2Fsimplepackone", + } + + req = mux.SetURLVars(req, vars) + + Publish(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Filesystem error\n") + }) + + t.Run("return `Internal Server` error if writing tar package fails", func(t *testing.T) { + jsonBody := []byte( + `{ + "_id": "@ookusanya/simplepackone", + "name": "@ookusanya/simplepackone", + "description": "This is a very rough implementation of a private npm registry.", + "dist-tags": { + "latest": "1.3.0" + }, + "versions": { + "1.3.0": { + "name": "@ookusanya/simplepackone", + "version": "1.3.0", + "description": "This is a very rough implementation of a private npm registry.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "MIT", + "files": null, + "readme": "# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...", + "_id": "@ookusanya/simplepackone@1.2.0", + "_nodeVersion": "16.20.0", + "_npmVersion": "8.19.4", + "dist": { + "integrity": "sha512-coOiU+ywV/do/+HwK91mfuei9491yfQUedLWjspAjDa58RniJvQNTF/cXDp/sooVdpjgEbCqKVyLOju6C1i3pw==", + "shasum": "1adafdbe8878372c9f4b554bf30d5bfb1b7896c5", + "tarball": "http://localhost:4000/@ookusanya%2Fsimplepackone/-/@ookusanya%2Fsimplepackone-1.2.0.tgz" + }, + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid": "^9.x" + } + } + }, + "access": "", + "_attachments": { + "@ookusanya/simplepackone-1.3.0.tgz": { + "content_type": "application/octet-stream", + "data": "H4sIAAAAAAAC/+1XbWvjOBDuZ/+KwQtLCyZ10pfl+uncRGnMOnawne0VlgXVVmJtHctIcl849r/fSEmb9nof73LcXoaA0Lw9M6PMSG5pcUeX7Jg3JXvsfVcH/wD5vn9+egp/xbc0OIGDk7Nzv98/P/dRz+/7nwY+rgc7oE5pKjGUvyFJJHhZ/yNUiEaJmvVqsTx0J0wy9+hgT/8fajf9v1lxAohmx/1/ev6u//tnZ/19/++CfncA3IaumHsB7q9C3HWKNk/0WPFVWzPzrxANcz2jdc+k4qIxiv3eoOevuSVTheSt3kjWzBXldvd8q6y5a0WFAoOKDM2UNmqsqAR8dYmUQl5AI8AIQLWs4AvOyq8ufPwI7JFr6Lto+cN6o52uhNxi1rxgjbJ5TMP8ObiWYQhNwdkrWKZq3ljgb596j1bTxCyKilpm399yu46X7oX77RdkGWjnh/NT9n9KgtGU9Fbl7u//waD/7v4/Oxns+38X9AGuxLrXm3blOHnFFeCPAnb7E0jRLSuw4hVrNDVdDmKB4lbye6oZoBVItuRKy6ee43yAMaO6k0yhr2SU9HqWSZqKNoX18VZwLeQdRGj9wt1fyf9K/0fhkMQZ6elHveP+H/hng3f3f9/f9/9OyBmK9knyZaXhsDiCgfkYS+rugZZ0xWtRU0g2TwLHmTG54sq8AcyIqPBj4fYJlpI2mpUeLCRjZjbgLSqXzAMtAM2gxVeDGRq3Gt8EvFk6FAqENJraDBslFvqBSobKJVClRMFxrpRQiqLbzpwFr5mCQ10xx802Fu6RBSkZrYE36I3BswgeOD4OOo2jCQcTL4wPD5WKuitNDM/imq/4BsGY2zoo47RTmIGJ04OVKPnCrMym1Xa3NVeV55Rm5vHbTiNTGaZ9fngmj2MhQbG6Nh7w5bHOdRud1UEUpzUF1ZsSWdyHSqzeZoIlWnSyQUhW2nQFlswifmeFNl6M+kLUtXjA1BCyKbnJSF2YcY51vRX3zOayPuZGaAx1HYI5gHZ7qhuRqijGfsucdcEQF8tLX6UjDTz2TaM51r4V0uL9OU0c8PmEQJaM8+sgJRBmMEuTL+GIjMANMty7HlyH+SSZ54AaaRDnN5CMIYhv4HMYjzyH/DZLSZZBkkI4nUUhGXkQxsNoPgrjK7hEuzjJIQrxwYdO8wQM4MZVSNBu7ExJOpzgNrgMozC/8WAc5rHxOUanAcyCNA+H8yhIYTZPZ0lGEH7kxEkcxuMUUciUxHkPUREKyBfcQDYJoshCBXOMPrXxDZPZTRpeTXKYJNGIIPOSOFEYXEZkDYVJDaMgnHowCqbBFbFWCXpJwahtorueEMtCvAB/wzxMYgdrMkziPMWth1mm+YvpdZgRD4I0zExBxmmC7k050SKxTtAuJmsvptTw5kRQxeznGdnGMiJBhL4yY/xaeX8x72lPe9rTT0V/APlm4rcAGAAA", + "length": 1089 + } + } + }`, + ) + req := httptest.NewRequest(http.MethodPut, "/{name}", bytes.NewReader(jsonBody)) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return string(jsonBody), true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + temp := serviceidos.IndexJson{ + Versions: map[string]serviceidos.IndexJsonVersions{ + "1.2.0": { + Version: "1.2.0", + Name: "@ookusanya/simplepackone", + }, + }, + DistTags: map[string]string{ + "latest": "1.2.0", + }, + TimesPackage: map[string]string{ + "modified": "", + "created": "", + }, + } + *res = temp + return nil + } + mfs.WriteIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return nil + } + mfs.WritePackageToStoreFunc = func(fPath string, data string, lg *logrus.Logger) error { + return fmt.Errorf("Filesystem error") + } + + vars := map[string]string{ + "name": "@ookusanya%2Fsimplepackone", + } + + req = mux.SetURLVars(req, vars) + + Publish(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Filesystem error\n") + }) + + t.Run("return 201 Created if publish is successful", func(t *testing.T) { + jsonBody := []byte( + `{ + "_id": "@ookusanya/simplepackone", + "name": "@ookusanya/simplepackone", + "description": "This is a very rough implementation of a private npm registry.", + "dist-tags": { + "latest": "1.3.0" + }, + "versions": { + "1.3.0": { + "name": "@ookusanya/simplepackone", + "version": "1.3.0", + "description": "This is a very rough implementation of a private npm registry.", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "MIT", + "files": null, + "readme": "# Gosimplenpm\n\nThis is a very rough implementation of a private npm registry.\n\n# Features\n\nTODO...\n\n# Enhancements\n\nTODO...\n\n# Work List\n\nTODO...", + "_id": "@ookusanya/simplepackone@1.2.0", + "_nodeVersion": "16.20.0", + "_npmVersion": "8.19.4", + "dist": { + "integrity": "sha512-coOiU+ywV/do/+HwK91mfuei9491yfQUedLWjspAjDa58RniJvQNTF/cXDp/sooVdpjgEbCqKVyLOju6C1i3pw==", + "shasum": "1adafdbe8878372c9f4b554bf30d5bfb1b7896c5", + "tarball": "http://localhost:4000/@ookusanya%2Fsimplepackone/-/@ookusanya%2Fsimplepackone-1.2.0.tgz" + }, + "dependencies": { + "eslint": "^7.x", + "mocha": "^10.x", + "uuid": "^9.x" + } + } + }, + "access": "", + "_attachments": { + "@ookusanya/simplepackone-1.3.0.tgz": { + "content_type": "application/octet-stream", + "data": "H4sIAAAAAAAC/+1XbWvjOBDuZ/+KwQtLCyZ10pfl+uncRGnMOnawne0VlgXVVmJtHctIcl849r/fSEmb9nof73LcXoaA0Lw9M6PMSG5pcUeX7Jg3JXvsfVcH/wD5vn9+egp/xbc0OIGDk7Nzv98/P/dRz+/7nwY+rgc7oE5pKjGUvyFJJHhZ/yNUiEaJmvVqsTx0J0wy9+hgT/8fajf9v1lxAohmx/1/ev6u//tnZ/19/++CfncA3IaumHsB7q9C3HWKNk/0WPFVWzPzrxANcz2jdc+k4qIxiv3eoOevuSVTheSt3kjWzBXldvd8q6y5a0WFAoOKDM2UNmqsqAR8dYmUQl5AI8AIQLWs4AvOyq8ufPwI7JFr6Lto+cN6o52uhNxi1rxgjbJ5TMP8ObiWYQhNwdkrWKZq3ljgb596j1bTxCyKilpm399yu46X7oX77RdkGWjnh/NT9n9KgtGU9Fbl7u//waD/7v4/Oxns+38X9AGuxLrXm3blOHnFFeCPAnb7E0jRLSuw4hVrNDVdDmKB4lbye6oZoBVItuRKy6ee43yAMaO6k0yhr2SU9HqWSZqKNoX18VZwLeQdRGj9wt1fyf9K/0fhkMQZ6elHveP+H/hng3f3f9/f9/9OyBmK9knyZaXhsDiCgfkYS+rugZZ0xWtRU0g2TwLHmTG54sq8AcyIqPBj4fYJlpI2mpUeLCRjZjbgLSqXzAMtAM2gxVeDGRq3Gt8EvFk6FAqENJraDBslFvqBSobKJVClRMFxrpRQiqLbzpwFr5mCQ10xx802Fu6RBSkZrYE36I3BswgeOD4OOo2jCQcTL4wPD5WKuitNDM/imq/4BsGY2zoo47RTmIGJ04OVKPnCrMym1Xa3NVeV55Rm5vHbTiNTGaZ9fngmj2MhQbG6Nh7w5bHOdRud1UEUpzUF1ZsSWdyHSqzeZoIlWnSyQUhW2nQFlswifmeFNl6M+kLUtXjA1BCyKbnJSF2YcY51vRX3zOayPuZGaAx1HYI5gHZ7qhuRqijGfsucdcEQF8tLX6UjDTz2TaM51r4V0uL9OU0c8PmEQJaM8+sgJRBmMEuTL+GIjMANMty7HlyH+SSZ54AaaRDnN5CMIYhv4HMYjzyH/DZLSZZBkkI4nUUhGXkQxsNoPgrjK7hEuzjJIQrxwYdO8wQM4MZVSNBu7ExJOpzgNrgMozC/8WAc5rHxOUanAcyCNA+H8yhIYTZPZ0lGEH7kxEkcxuMUUciUxHkPUREKyBfcQDYJoshCBXOMPrXxDZPZTRpeTXKYJNGIIPOSOFEYXEZkDYVJDaMgnHowCqbBFbFWCXpJwahtorueEMtCvAB/wzxMYgdrMkziPMWth1mm+YvpdZgRD4I0zExBxmmC7k050SKxTtAuJmsvptTw5kRQxeznGdnGMiJBhL4yY/xaeX8x72lPe9rTT0V/APlm4rcAGAAA", + "length": 1089 + } + } + }`, + ) + req := httptest.NewRequest(http.MethodPut, "/{name}", bytes.NewReader(jsonBody)) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return string(jsonBody), false, nil + } + mfs.WriteIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return nil + } + mfs.WritePackageToStoreFunc = func(fPath string, data string, lg *logrus.Logger) error { + return nil + } + + vars := map[string]string{ + "name": "@ookusanya%2Fsimplepackone", + } + + req = mux.SetURLVars(req, vars) + + Publish(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusCreated) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "{\"ok\":true,\"package_name\":\"@ookusanya/simplepackone\"}") + }) +} + +func TestIntegrationPublishNormal(t *testing.T) { + if testing.Short() { + t.Skip("Skipping publishPackage integration test") + } + + token := "0N89nr/hmKXoBzG]R{fKH%YE1X" + + tmpDir := t.TempDir() + + t.Logf("Temp Dir: %s", tmpDir) + + indexJsonFp := "intestdata/publish/normal/index.json" + + isEmpty := IsDirEmpty(t, tmpDir) + assert.Equal(t, true, isEmpty) + + cfg := config.Config{ + RepoDir: tmpDir, + Token: token, + } + + app := newTestApp(t, cfg) + app.Routes() + ts := newTestServer(t, app.Mux) + defer ts.Close() + + dataToSend := readTestFile(t, indexJsonFp) + code, _, body := ts.put(t, fmt.Sprintf("/%s", url.PathEscape("@df/simplepackone")), token, string(dataToSend)) + + assert.Equal(t, code, http.StatusCreated) + + isEmpty = IsDirEmpty(t, tmpDir) + assert.Equal(t, false, isEmpty) + + // t.Logf("Body ==> %s", string(body)) + + filePaths := listDir(t, tmpDir, false) + assert.Equal(t, true, containsSub(filePaths, "index.json")) + assert.Equal(t, true, containsSub(filePaths, "simplepackone-1.0.0")) + + assert.NotEmpty(t, string(body)) +} + +func TestIntegrationPublishOverwrite(t *testing.T) { + if testing.Short() { + t.Skip("Skipping publishPackage integration test") + } + + token := "0N89nr/hmKXoBzG]R{fKH%YE1X" + + tmpDir := t.TempDir() + + t.Logf("Temp Dir: %s", tmpDir) + + // Copy initial package + indexJsonFp := "intestdata/publish/normal/index.json" + tgzFp := "intestdata/get/simplepackone-1.0.0.tgz" + mkDir(t, fmt.Sprintf("%s/@df/simplepackone", tmpDir)) + cpFile(t, indexJsonFp, fmt.Sprintf("%s/@df/simplepackone/index.json", tmpDir)) + cpFile(t, tgzFp, fmt.Sprintf("%s/@df/simplepackone/simplepackone-1.0.0.tgz", tmpDir)) + + isEmpty := IsDirEmpty(t, tmpDir) + assert.Equal(t, false, isEmpty) + + indexJsonFp = "intestdata/publish/overwrite/index.json" + + cfg := config.Config{ + RepoDir: tmpDir, + Token: token, + } + + app := newTestApp(t, cfg) + app.Routes() + ts := newTestServer(t, app.Mux) + defer ts.Close() + + dataToSend := readTestFile(t, indexJsonFp) + code, _, body := ts.put(t, fmt.Sprintf("/%s", url.PathEscape("@df/simplepackone")), token, string(dataToSend)) + + assert.Equal(t, code, http.StatusCreated) + + filePaths := listDir(t, tmpDir, false) + assert.Equal(t, true, containsSub(filePaths, "index.json")) + assert.Equal(t, true, containsSub(filePaths, "simplepackone-1.0.0")) + assert.Equal(t, true, containsSub(filePaths, "simplepackone-1.9.0")) + + assert.NotEmpty(t, string(body)) +} diff --git a/internal/handler/tagdelete.go b/internal/handler/tagdelete.go index f5f06cd..887d2ce 100644 --- a/internal/handler/tagdelete.go +++ b/internal/handler/tagdelete.go @@ -15,7 +15,7 @@ import ( "golang.org/x/mod/semver" ) -func DistTagDelete(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { +func DistTagDelete(lg *logrus.Logger, cfg config.Config, stg storage.Storage) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { escapedName := mux.Vars(r)["name"] packageName, _ := url.PathUnescape(escapedName) @@ -30,7 +30,7 @@ func DistTagDelete(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { }).Debugf("Tag => %s\n", tag) if semver.IsValid(tag) { - http.Error(w, "Tag cannot be a semver version", http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Tag %s cannot be a semver version", tag), http.StatusBadRequest) return } @@ -39,7 +39,7 @@ func DistTagDelete(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { return } - fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg) + fileToServe, found, err := stg.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -52,7 +52,7 @@ func DistTagDelete(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { } var jsonFile serviceidos.IndexJson - err = storage.ReadIndexJson(fileToServe, &jsonFile, lg) + err = stg.ReadIndexJson(fileToServe, &jsonFile, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -61,7 +61,7 @@ func DistTagDelete(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { delete(jsonFile.DistTags, tag) // Write index.json - err = storage.WriteIndexJson(fileToServe, &jsonFile, lg) + err = stg.WriteIndexJson(fileToServe, &jsonFile, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/handler/tagdelete_test.go b/internal/handler/tagdelete_test.go new file mode 100644 index 0000000..1373fd2 --- /dev/null +++ b/internal/handler/tagdelete_test.go @@ -0,0 +1,429 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "gosimplenpm/internal/config" + "gosimplenpm/internal/serviceidos" + "gosimplenpm/internal/storage" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestUnitTagDelete(t *testing.T) { + t.Run("return `Bad request` error if tag is semver", func(t *testing.T) { + req := httptest.NewRequest(http.MethodDelete, "/-/package/{name}/dist-tags/{tag}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + + tag := "v1.0.0" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagDelete(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusBadRequest) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), fmt.Sprintf("Tag %s cannot be a semver version\n", tag)) + }) + + t.Run("return `Bad request` error if tag is latest", func(t *testing.T) { + req := httptest.NewRequest(http.MethodDelete, "/-/package/{name}/dist-tags/{tag}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + + tag := "latest" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagDelete(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusBadRequest) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Cannot delete the latest tag\n") + }) + + t.Run("return `Internal Server` error if index json cannot be retrieved", func(t *testing.T) { + req := httptest.NewRequest(http.MethodDelete, "/-/package/{name}/dist-tags/{tag}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", false, fmt.Errorf("Filesystem error") + } + + tag := "current" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagDelete(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Filesystem error\n") + }) + + t.Run("return `Not found` error if package cannot be retrieved", func(t *testing.T) { + req := httptest.NewRequest(http.MethodDelete, "/-/package/{name}/dist-tags/{tag}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", false, nil + } + + tag := "current" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagDelete(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusNotFound) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Package not found: test-package\n") + }) + + t.Run("return `Internal Server` error if package.json cannot be parsed", func(t *testing.T) { + req := httptest.NewRequest(http.MethodDelete, "/-/package/{name}/dist-tags/{tag}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return fmt.Errorf("Parsing failed") + } + + tag := "current" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagDelete(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Parsing failed\n") + }) + + t.Run("return `Internal Server` error if package.json cannot be written", func(t *testing.T) { + req := httptest.NewRequest(http.MethodDelete, "/-/package/{name}/dist-tags/{tag}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + temp := serviceidos.IndexJson{ + DistTags: map[string]string{ + "current": "1.2.0", + "latest": "1.2.0", + }, + } + *res = temp + return nil + } + mfs.WriteIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return fmt.Errorf("File cannot be written") + } + + tag := "current" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagDelete(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "File cannot be written\n") + }) + + t.Run("return 200 OK if tag can be deleted", func(t *testing.T) { + req := httptest.NewRequest(http.MethodDelete, "/-/package/{name}/dist-tags/{tag}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + temp := serviceidos.IndexJson{ + DistTags: map[string]string{ + "current": "1.2.0", + "latest": "1.2.0", + }, + } + *res = temp + return nil + } + mfs.WriteIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return nil + } + + tag := "current" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagDelete(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusOK) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "{\"ok\":true,\"id\":\"current\",\"dist-tags\":\"{\\\"latest\\\":\\\"1.2.0\\\"}\"}") + }) +} + +func TestIntegrationTagDelete(t *testing.T) { + if testing.Short() { + t.Skip("Skipping deleteTags integration test") + } + + token := "0N89nr/hmKXoBzG]R{fKH%YE1X" + + tmpDir := t.TempDir() + + mkDir(t, fmt.Sprintf("%s/@df/simplepackone", tmpDir)) + mkDir(t, fmt.Sprintf("%s/output", tmpDir)) + + indexJsonFp := "intestdata/tags/index.json" + oldTgzFp := "intestdata/tags/simplepackone-1.0.0.tgz" + tgzFp := "intestdata/tags/simplepackone-1.9.0.tgz" + + cpFile(t, indexJsonFp, fmt.Sprintf("%s/@df/simplepackone/index.json", tmpDir)) + cpFile(t, oldTgzFp, fmt.Sprintf("%s/@df/simplepackone/simplepackone-1.0.0.tgz", tmpDir)) + cpFile(t, tgzFp, fmt.Sprintf("%s/@df/simplepackone/simplepackone-1.9.0.tgz", tmpDir)) + + cfg := config.Config{ + RepoDir: tmpDir, + Token: token, + } + + app := newTestApp(t, cfg) + app.Routes() + ts := newTestServer(t, app.Mux) + defer ts.Close() + + code, _, body := ts.delete(t, fmt.Sprintf("/-/package/%s/dist-tags/pre-alpha", url.PathEscape("@df/simplepackone")), token) + + assert.Equal(t, code, http.StatusOK) + assert.NotEmpty(t, body) + + old := readTestFile(t, indexJsonFp) + oldRet := make(map[string]interface{}) + json.Unmarshal(old, &oldRet) + + assert.Contains(t, oldRet["dist-tags"], "pre-alpha") + + modified := readTestFile(t, fmt.Sprintf("%s/@df/simplepackone/index.json", tmpDir)) + modifiedRet := make(map[string]interface{}) + json.Unmarshal(modified, &modifiedRet) + + assert.NotContains(t, modifiedRet["dist-tags"], "pre-alpha") +} diff --git a/internal/handler/tagget.go b/internal/handler/tagget.go index 4957334..aedd654 100644 --- a/internal/handler/tagget.go +++ b/internal/handler/tagget.go @@ -14,7 +14,7 @@ import ( "github.com/sirupsen/logrus" ) -func DistTagGet(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { +func DistTagGet(lg *logrus.Logger, cfg config.Config, stg storage.Storage) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { escapedName := mux.Vars(r)["name"] packageName, _ := url.PathUnescape(escapedName) @@ -22,7 +22,7 @@ func DistTagGet(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { "function": "dist-tags-get", }).Debugf("Package name => %s\n", packageName) - fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg) + fileToServe, found, err := stg.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -35,7 +35,7 @@ func DistTagGet(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { } var jsonFile serviceidos.IndexJson - err = storage.ReadIndexJson(fileToServe, &jsonFile, lg) + err = stg.ReadIndexJson(fileToServe, &jsonFile, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/handler/tagget_test.go b/internal/handler/tagget_test.go new file mode 100644 index 0000000..1cbbba5 --- /dev/null +++ b/internal/handler/tagget_test.go @@ -0,0 +1,255 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "gosimplenpm/internal/config" + "gosimplenpm/internal/serviceidos" + "gosimplenpm/internal/storage" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestUnitTagGet(t *testing.T) { + t.Run("return `Internal Server` error if index json cannot be retrieved", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/-/package/{name}/dist-tags", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", false, fmt.Errorf("Filesystem error") + } + + tag := "1.2.4" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagGet(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Filesystem error\n") + }) + + t.Run("return `Not Found` error if package cannot be retrieved", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/-/package/{name}/dist-tags", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", false, nil + } + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + } + + req = mux.SetURLVars(req, vars) + + DistTagGet(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusNotFound) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Package not found: test-package\n") + }) + + t.Run("return `Internal Server` error if package.json cannot be parsed", func(t *testing.T) { + req := httptest.NewRequest(http.MethodDelete, "/-/package/{name}/dist-tags", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return fmt.Errorf("Parsing failed") + } + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + } + + req = mux.SetURLVars(req, vars) + + DistTagGet(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Parsing failed\n") + }) + + t.Run("return 200 OK", func(t *testing.T) { + req := httptest.NewRequest(http.MethodDelete, "/-/package/{name}/dist-tags", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + temp := serviceidos.IndexJson{ + DistTags: map[string]string{ + "current": "1.2.0", + "latest": "1.2.0", + }, + } + *res = temp + return nil + } + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + } + + req = mux.SetURLVars(req, vars) + + DistTagGet(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusOK) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "{\"current\":\"1.2.0\",\"latest\":\"1.2.0\"}") + }) +} + +func TestIntegrationTagGet(t *testing.T) { + if testing.Short() { + t.Skip("Skipping getTags integration test") + } + + token := "0N89nr/hmKXoBzG]R{fKH%YE1X" + + tmpDir := t.TempDir() + + mkDir(t, fmt.Sprintf("%s/@df/simplepackone", tmpDir)) + mkDir(t, fmt.Sprintf("%s/output", tmpDir)) + + indexJsonFp := "intestdata/tags/index.json" + tgzFp := "intestdata/tags/simplepackone-1.9.0.tgz" + + cpFile(t, indexJsonFp, fmt.Sprintf("%s/@df/simplepackone/index.json", tmpDir)) + cpFile(t, tgzFp, fmt.Sprintf("%s/@df/simplepackone/simplepackone-1.9.0.tgz", tmpDir)) + + cfg := config.Config{ + RepoDir: tmpDir, + Token: token, + } + + app := newTestApp(t, cfg) + app.Routes() + ts := newTestServer(t, app.Mux) + defer ts.Close() + + code, _, body := ts.get(t, fmt.Sprintf("/-/package/%s/dist-tags", url.PathEscape("@df/simplepackone"))) + + assert.Equal(t, code, http.StatusOK) + ret := make(map[string]interface{}) + json.Unmarshal(body, &ret) + assert.Contains(t, ret, "latest") + assert.Equal(t, "1.9.0", ret["latest"]) +} diff --git a/internal/handler/tagput.go b/internal/handler/tagput.go index a0fc17b..938a4b6 100644 --- a/internal/handler/tagput.go +++ b/internal/handler/tagput.go @@ -16,7 +16,7 @@ import ( "golang.org/x/mod/semver" ) -func DistTagPut(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { +func DistTagPut(lg *logrus.Logger, cfg config.Config, stg storage.Storage) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { escapedName := mux.Vars(r)["name"] packageName, _ := url.PathUnescape(escapedName) @@ -31,12 +31,12 @@ func DistTagPut(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { }).Debugf("Tag => %s\n", tag) if semver.IsValid(tag) { - http.Error(w, "Tag cannot be a semver version", http.StatusBadRequest) + http.Error(w, fmt.Sprintf("Tag %s cannot be a semver version", tag), http.StatusBadRequest) return } if tag == "latest" { - http.Error(w, "Cannot delete the latest tag", http.StatusBadRequest) + http.Error(w, "Cannot modify the latest tag", http.StatusBadRequest) return } @@ -47,7 +47,7 @@ func DistTagPut(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { "function": "dist-tags-put", }).Debugf("Body => %s", version) - fileToServe, found, err := storage.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg) + fileToServe, found, err := stg.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -60,7 +60,7 @@ func DistTagPut(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { } var jsonFile serviceidos.IndexJson - err = storage.ReadIndexJson(fileToServe, &jsonFile, lg) + err = stg.ReadIndexJson(fileToServe, &jsonFile, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -69,7 +69,7 @@ func DistTagPut(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { jsonFile.DistTags[tag] = version // Write index.json - err = storage.WriteIndexJson(fileToServe, &jsonFile, lg) + err = stg.WriteIndexJson(fileToServe, &jsonFile, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/handler/tagput_test.go b/internal/handler/tagput_test.go new file mode 100644 index 0000000..f464294 --- /dev/null +++ b/internal/handler/tagput_test.go @@ -0,0 +1,436 @@ +package handler + +import ( + "bytes" + "encoding/json" + "fmt" + "gosimplenpm/internal/config" + "gosimplenpm/internal/serviceidos" + "gosimplenpm/internal/storage" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "strings" + "testing" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestUnitTagPut(t *testing.T) { + t.Run("return `Bad request` error if tag is semver", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPut, "/-/package/{name}/dist-tags/{tag}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + + tag := "v1.0.0" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagPut(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusBadRequest) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), fmt.Sprintf("Tag %s cannot be a semver version\n", tag)) + }) + + t.Run("return `Bad request` error if tag is latest", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPut, "/-/package/{name}/dist-tags/{tag}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + + tag := "latest" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagPut(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusBadRequest) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Cannot modify the latest tag\n") + }) + + t.Run("return `Internal Server` error if index json cannot be retrieved", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPut, "/-/package/{name}/dist-tags/{tag}", strings.NewReader("development")) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", false, fmt.Errorf("Filesystem error") + } + + tag := "current" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagPut(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Filesystem error\n") + }) + + t.Run("return `Not found` error if package cannot be retrieved", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPut, "/-/package/{name}/dist-tags/{tag}", strings.NewReader("development")) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", false, nil + } + + tag := "current" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagPut(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusNotFound) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Package not found: test-package\n") + }) + + t.Run("return `Internal Server` error if package.json cannot be parsed", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPut, "/-/package/{name}/dist-tags/{tag}", strings.NewReader("development")) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return fmt.Errorf("Parsing failed") + } + + tag := "current" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagPut(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Parsing failed\n") + }) + + t.Run("return `Internal Server` error if package.json cannot be written", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPut, "/-/package/{name}/dist-tags/{tag}", strings.NewReader("\"3.5.6-rc\"")) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + temp := serviceidos.IndexJson{ + DistTags: map[string]string{ + "current": "1.2.0", + "latest": "1.2.0", + }, + } + *res = temp + return nil + } + mfs.WriteIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return fmt.Errorf("File cannot be written") + } + + tag := "current" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagPut(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "File cannot be written\n") + }) + + t.Run("return 200 OK if tag can be overwritten", func(t *testing.T) { + req := httptest.NewRequest(http.MethodPut, "/-/package/{name}/dist-tags/{tag}", strings.NewReader("\"3.5.6-rc\"")) + + // req := httptest.NewRequest(http.MethodPut, "/-/package/{name}/dist-tags/{tag}", strings.NewReader("{\"version\":\"development\"}")) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetIndexJsonFromStoreFunc = func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + return "", true, nil + } + mfs.ReadIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + temp := serviceidos.IndexJson{ + DistTags: map[string]string{ + "current": "1.2.0", + "latest": "1.2.0", + }, + } + *res = temp + return nil + } + mfs.WriteIndexJsonFunc = func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + return nil + } + + tag := "current" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "test-package", + "tag": tag, + } + + req = mux.SetURLVars(req, vars) + + DistTagPut(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusOK) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "{\"ok\":true,\"id\":\"current\",\"dist-tags\":\"{\\\"current\\\":\\\"3.5.6-rc\\\",\\\"latest\\\":\\\"1.2.0\\\"}\"}") + }) +} + +func TestIntegrationTagPut(t *testing.T) { + if testing.Short() { + t.Skip("Skipping putTags integration test") + } + + token := "0N89nr/hmKXoBzG]R{fKH%YE1X" + + tmpDir := t.TempDir() + + mkDir(t, fmt.Sprintf("%s/@df/simplepackone", tmpDir)) + mkDir(t, fmt.Sprintf("%s/output", tmpDir)) + + indexJsonFp := "intestdata/tags/index.json" + oldTgzFp := "intestdata/tags/simplepackone-1.0.0.tgz" + tgzFp := "intestdata/tags/simplepackone-1.9.0.tgz" + + cpFile(t, indexJsonFp, fmt.Sprintf("%s/@df/simplepackone/index.json", tmpDir)) + cpFile(t, oldTgzFp, fmt.Sprintf("%s/@df/simplepackone/simplepackone-1.0.0.tgz", tmpDir)) + cpFile(t, tgzFp, fmt.Sprintf("%s/@df/simplepackone/simplepackone-1.9.0.tgz", tmpDir)) + + cfg := config.Config{ + RepoDir: tmpDir, + Token: token, + } + + app := newTestApp(t, cfg) + app.Routes() + ts := newTestServer(t, app.Mux) + defer ts.Close() + + code, _, body := ts.put(t, fmt.Sprintf("/-/package/%s/dist-tags/release-candidate", url.PathEscape("@df/simplepackone")), token, "\"1.0.0\"") + + assert.Equal(t, code, http.StatusOK) + + assert.NotEmpty(t, body) + + old := readTestFile(t, indexJsonFp) + oldRet := make(map[string]interface{}) + json.Unmarshal(old, &oldRet) + + assert.NotContains(t, oldRet["dist-tags"], "release-candidate") + + modified := readTestFile(t, fmt.Sprintf("%s/@df/simplepackone/index.json", tmpDir)) + modifiedRet := make(map[string]interface{}) + json.Unmarshal(modified, &modifiedRet) + + assert.Contains(t, modifiedRet["dist-tags"], "release-candidate") + version := modifiedRet["dist-tags"].(map[string]interface{}) + assert.Equal(t, version["release-candidate"], "1.0.0") + +} diff --git a/internal/handler/tar.go b/internal/handler/tar.go index 00c3593..0c02b17 100644 --- a/internal/handler/tar.go +++ b/internal/handler/tar.go @@ -2,6 +2,7 @@ package handler import ( "bytes" + "fmt" "gosimplenpm/internal/config" "gosimplenpm/internal/storage" "io" @@ -14,7 +15,7 @@ import ( "github.com/sirupsen/logrus" ) -func PackageTarGet(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { +func PackageTarGet(lg *logrus.Logger, cfg config.Config, stg storage.Storage) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Sample output of npm view // Public @@ -30,13 +31,20 @@ func PackageTarGet(lg *logrus.Logger, cfg config.Config) http.HandlerFunc { "function": "get-tar", }).Debugf("Package name => %s\n", packageName) escapedName = mux.Vars(r)["tar"] - tarFileName, _ := url.PathUnescape(escapedName) + tarFileNameWithScope, _ := url.PathUnescape(escapedName) lg.WithFields(logrus.Fields{ "function": "get-tar", - }).Debugf("Tarfile name => %s\n", tarFileName) + }).Debugf("Tarfile name => %s\n", tarFileNameWithScope) - versionName := strings.Split(strings.Split(tarFileName, "-")[1], ".tgz")[0] - fileAsString, err := storage.GetTarFromStore(packageName, versionName, cfg.RepoDir, lg) + fmt.Printf("Tarfile name => %s\n", tarFileNameWithScope) + + fragments := strings.Split(tarFileNameWithScope, "/") + tarFileName := fragments[len(fragments)-1] + // fragments := strings.Split(urlFileFragment, "-") + // versionFragment := fragments[len(fragments)-1] + // versionName := strings.Split(versionFragment, ".tgz")[0] + // fmt.Printf("Version name => %s\n", versionName) + fileAsString, err := stg.GetTarFromStore(packageName, tarFileName, cfg.RepoDir, lg) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return diff --git a/internal/handler/tar_test.go b/internal/handler/tar_test.go new file mode 100644 index 0000000..5f5f4a1 --- /dev/null +++ b/internal/handler/tar_test.go @@ -0,0 +1,159 @@ +package handler + +import ( + "bytes" + "fmt" + "gosimplenpm/internal/config" + "gosimplenpm/internal/storage" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "testing" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestUnitTar(t *testing.T) { + t.Run("return `Bad request` error if tar package cannot be read from the filesystem", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/{name}/-/{tar}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetTarFromStoreFunc = func(packageName string, tarFileName string, registryPath string, lg *logrus.Logger) (string, error) { + return "", fmt.Errorf("Filesystem error") + } + + tag := "@test%2Fpackage-1.2.0.tgz" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "@test%2Fpackage", + "tar": tag, + } + + req = mux.SetURLVars(req, vars) + + PackageTarGet(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusInternalServerError) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Filesystem error\n") + }) + + t.Run("return 200 OK if tar is found on the filesystem", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/{name}/-/{tar}", nil) + wrt := httptest.NewRecorder() + + lg := &logrus.Logger{ + Out: os.Stdout, + // Level: "DEBUG", + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + cfg := config.Config{ + RepoDir: "", + } + + mfs := &storage.MockFs{} + mfs.GetTarFromStoreFunc = func(packageName string, tarFileName string, registryPath string, lg *logrus.Logger) (string, error) { + return "OHnFFeCPAnb7E0jRLSuw4hVrNDVdDmKB4lbye6oZoBVItuRKy6ee43yAMaO6k0yhr2SU9HqWSZ", nil + } + + tag := "@test%2Fpackage-1.2.0.tgz" + + //Hack to try to fake gorilla/mux vars + vars := map[string]string{ + "name": "@test%2Fpackage", + "tar": tag, + } + + req = mux.SetURLVars(req, vars) + + PackageTarGet(lg, cfg, mfs)(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusOK) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "OHnFFeCPAnb7E0jRLSuw4hVrNDVdDmKB4lbye6oZoBVItuRKy6ee43yAMaO6k0yhr2SU9HqWSZ") + }) +} + +func TestIntegrationTar(t *testing.T) { + if testing.Short() { + t.Skip("Skipping getTar integration test") + } + + token := "0N89nr/hmKXoBzG]R{fKH%YE1X" + + tmpDir := t.TempDir() + + t.Logf("Temp Dir: %s", tmpDir) + + // cpFolders(t, "intestdata/@df", fmt.Sprintf("%s/@df", tmpDir)) + + mkDir(t, fmt.Sprintf("%s/@df/simplepackone", tmpDir)) + mkDir(t, fmt.Sprintf("%s/output", tmpDir)) + + indexJsonFp := "intestdata/tar/index.json" + tgzFp := "intestdata/tar/simplepackone-1.0.0.tgz" + + cpFile(t, indexJsonFp, fmt.Sprintf("%s/@df/simplepackone/index.json", tmpDir)) + cpFile(t, tgzFp, fmt.Sprintf("%s/@df/simplepackone/simplepackone-1.0.0.tgz", tmpDir)) + + cfg := config.Config{ + RepoDir: tmpDir, + Token: token, + } + + app := newTestApp(t, cfg) + app.Routes() + ts := newTestServer(t, app.Mux) + defer ts.Close() + + tarUrlFragment := url.PathEscape("@df/simplepackone-1.0.0.tgz") + nameUrlFragment := url.PathEscape("@df/simplepackone") + + code, _, body := ts.get(t, fmt.Sprintf("/%s/-/%s", nameUrlFragment, tarUrlFragment)) + assert.Equal(t, code, http.StatusOK) + // assert.NotEmpty(t, body) + + expected := readTestFileAsBase64(t, tgzFp) + assert.Equal(t, string(body), expected) +} diff --git a/internal/handler/testutils_test.go b/internal/handler/testutils_test.go new file mode 100644 index 0000000..a4cd0e4 --- /dev/null +++ b/internal/handler/testutils_test.go @@ -0,0 +1,231 @@ +package handler + +import ( + "bytes" + "encoding/base64" + "fmt" + "gosimplenpm/internal/config" + "gosimplenpm/internal/storage" + "io" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/sirupsen/logrus" +) + +func newTestApp(t *testing.T, cfg config.Config) *Application { + return &Application{ + Conf: cfg, + Logger: &logrus.Logger{ + Out: io.Discard, + // Level: logrus.DebugLevel, + // Formatter: &logrus.TextFormatter{ + // FullTimestamp: true, + // TimestampFormat: "2009-01-02 15:15:15", + // }, + }, + FSStorage: &storage.FSStorage{}, + } +} + +type testServer struct { + *httptest.Server +} + +func newTestServer(t *testing.T, h http.Handler) *testServer { + ts := httptest.NewServer(h) + return &testServer{ts} +} + +func (ts *testServer) get(t *testing.T, urlPath string) (int, http.Header, []byte) { + rs, err := ts.Client().Get(ts.URL + urlPath) + if err != nil { + t.Fatal(err) + } + + defer rs.Body.Close() + + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + + body = bytes.TrimSpace(body) + return rs.StatusCode, rs.Header, body +} + +func (ts *testServer) put(t *testing.T, urlPath string, token string, data string) (int, http.Header, []byte) { + req, err := http.NewRequest(http.MethodPut, ts.URL+urlPath, strings.NewReader(data)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + if err != nil { + t.Fatal(err) + } + rs, err := ts.Client().Do(req) + if err != nil { + t.Fatal(err) + } + + defer rs.Body.Close() + + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + + body = bytes.TrimSpace(body) + return rs.StatusCode, rs.Header, body +} + +func (ts *testServer) delete(t *testing.T, urlPath string, token string) (int, http.Header, []byte) { + req, err := http.NewRequest(http.MethodDelete, ts.URL+urlPath, nil) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + if err != nil { + t.Fatal(err) + } + rs, err := ts.Client().Do(req) + if err != nil { + t.Fatal(err) + } + + defer rs.Body.Close() + + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + + body = bytes.TrimSpace(body) + return rs.StatusCode, rs.Header, body +} + +func cpFile(t *testing.T, src string, dst string) { + var srcfd *os.File + var dstfd *os.File + var err error + var srcinfo os.FileInfo + srcfd, err = os.Open(src) + if err != nil { + t.Fatal(err) + } + defer srcfd.Close() + dstfd, err = os.Create(dst) + if err != nil { + t.Fatal(err) + } + + defer dstfd.Close() + _, err = io.Copy(dstfd, srcfd) + if err != nil { + t.Fatal(err) + } + srcinfo, err = os.Stat(src) + if err != nil { + t.Fatal(err) + } + err = os.Chmod(dst, srcinfo.Mode()) + if err != nil { + t.Fatal(err) + } +} + +// cpFolders - Copy folders recursively +// func cpFolders(t *testing.T, src string, dst string) { +// var err error +// var fds []fs.DirEntry +// var srcinfo os.FileInfo + +// srcinfo, err = os.Stat(src) +// if err != nil { +// t.Fatal(err) +// } + +// err = os.MkdirAll(dst, srcinfo.Mode()) +// if err != nil { +// t.Fatal(err) +// } + +// fds, err = os.ReadDir(src) +// if err != nil { +// t.Fatal(err) +// } + +// for _, fd := range fds { +// srcfp := path.Join(src, fd.Name()) +// dstfp := path.Join(dst, fd.Name()) +// if fd.IsDir() { +// cpFolders(t, srcfp, dstfp) +// } else { +// cpFile(t, srcfp, dstfp) +// } +// } +// } + +func mkDir(t *testing.T, fp string) { + err := os.MkdirAll(fp, os.ModePerm) + if err != nil { + t.Fatal(err) + } +} + +func readTestFile(t *testing.T, fp string) []byte { + f, err := os.ReadFile(fp) + if err != nil { + t.Fatal(err) + } + return f +} + +func readTestFileAsBase64(t *testing.T, fp string) string { + f, err := os.ReadFile(fp) + if err != nil { + t.Fatal(err) + } + + return base64.StdEncoding.EncodeToString(f) +} + +func listDir(t *testing.T, fp string, list bool) []string { + var filePaths []string + err := filepath.Walk(fp, func(path string, info os.FileInfo, err error) error { + if list { + t.Logf("File (Directory: %t)=> %s", info.IsDir(), path) + } else { + if !info.IsDir() { + filePaths = append(filePaths, path) + } + } + return nil + }) + if err != nil { + t.Fatal(err) + } + return filePaths +} + +func IsDirEmpty(t *testing.T, name string) bool { + f, err := os.Open(name) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + // read in ONLY one file + _, err = f.Readdir(1) + + // and if the file is EOF... well, the dir is empty. + return err == io.EOF +} + +func containsSub(s []string, str string) bool { + for _, v := range s { + if strings.Contains(v, str) { + return true + } + } + + return false +} diff --git a/internal/middlewares/auth_test.go b/internal/middlewares/auth_test.go new file mode 100644 index 0000000..8062419 --- /dev/null +++ b/internal/middlewares/auth_test.go @@ -0,0 +1,99 @@ +package middlewares + +import ( + "bytes" + "gosimplenpm/internal/config" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" +) + +func TestUnitMAuthMiddleware(t *testing.T) { + + router := mux.NewRouter() + handlerStr := []byte("Logic\n") + + hFunc := func(w http.ResponseWriter, e *http.Request) { + _, err := w.Write(handlerStr) + if err != nil { + t.Fatalf("Failed writing HTTP response: %v", err) + } + } + + cfg := config.Config{ + RepoDir: "", + Token: "MyToken", + } + + router.HandleFunc("/", AuthMiddleware(cfg)(hFunc)) + + t.Run("return `Status Foribben` if there is no token", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + wrt := httptest.NewRecorder() + + req.Header.Set("Authorization", "") + + router.ServeHTTP(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusForbidden) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Authentication Error\n") + }) + + t.Run("return `Status Foribben` if the Authorization field is not set properly", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + wrt := httptest.NewRecorder() + + req.Header.Set("Authorization", "Secret other") + + router.ServeHTTP(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusForbidden) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Authentication Error\n") + }) + + t.Run("return `Status Foribben` if the token is incorrect", func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/", nil) + wrt := httptest.NewRecorder() + + req.Header.Set("Authorization", "Bearer incorrectToken") + + router.ServeHTTP(wrt, req) + + rs := wrt.Result() + + assert.Equal(t, rs.StatusCode, http.StatusForbidden) + + defer rs.Body.Close() + body, err := io.ReadAll(rs.Body) + if err != nil { + t.Fatal(err) + } + bytes.TrimSpace(body) + + assert.Equal(t, string(body), "Authentication Error\n") + }) +} diff --git a/internal/serviceidos/responseidos.go b/internal/serviceidos/responseidos.go index 820b01e..7a5755e 100644 --- a/internal/serviceidos/responseidos.go +++ b/internal/serviceidos/responseidos.go @@ -7,38 +7,97 @@ type IndexJsonAttachments struct { } type IndexJsonDist struct { - Integrity string `json:"integrity"` - Shasum string `json:"shasum"` - Tarball string `json:"tarball"` + Integrity string `json:"integrity"` + Shasum string `json:"shasum"` + Tarball string `json:"tarball"` + FileCount int `json:"fileCount"` + UnpackedSize int `json:"unpackedSize"` } +type IndexJsonRepository struct { + Type string `json:"type"` + Url string `json:"url"` + // Only used if the package.json is not in the root folder of the url + Directory string `json:"directory"` +} + +type IndexJsonEngines struct { + NodeVersion string `json:"node,omitempty"` + NpmVersion string `json:"npm,omitempty"` +} + +type IndexJsonBugs struct { + Url string `json:"url,omitempty"` + Email string `json:"email,omitempty"` +} + +type IndexJsonAuthor struct { + Url string `json:"url,omitempty"` + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` +} + +// https://docs.npmjs.com/cli/v10/configuring-npm/package-json +// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md type IndexJsonVersions struct { - Name string `json:"name"` - Version string `json:"version"` - Description string `json:"description"` - Main string `json:"main,omitempty"` - Scripts map[string]string `json:"scripts,omitempty"` - Author string `json:"author,omitempty"` - License string `json:"license"` - Files []string `json:"files"` - Readme string `json:"readme,omitempty"` - ID string `json:"_id"` - NodeVersion string `json:"_nodeVersion"` - NpmVersion string `json:"_npmVersion"` - Dist IndexJsonDist `json:"dist"` - Dependencies map[string]string `json:"dependencies,omitempty"` - DevDependencies map[string]string `json:"devDependencies,omitempty"` - Resolutions map[string]string `json:"resolutions,omitempty"` + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Main string `json:"main,omitempty"` + Scripts map[string]string `json:"scripts,omitempty"` + Author string `json:"author,omitempty"` + License string `json:"license"` + Repository IndexJsonRepository `json:"repository,omitempty"` + Files []string `json:"files,omitempty"` + Homepage string `json:"homepage,omitempty"` + Readme string `json:"readme,omitempty"` + ReadmeFilename string `json:"readmeFilename,omitempty"` + Keywords []string `json:"keywords,omitempty"` + ID string `json:"_id"` + Contributors []IndexJsonAuthor `json:"contributors,omitempty"` + Maintainers []IndexJsonAuthor `json:"maintainers.omitempty"` + Bugs IndexJsonBugs `json:"bugs,omitempty"` + Bin map[string]string `json:"bin"` + OperatingSystem []string `json:"os,omitempty"` + Cpu []string `json:"cpu,omitempty"` + Engines IndexJsonEngines `json:"engines,omitempty"` + NodeVersion string `json:"_nodeVersion"` + NpmVersion string `json:"_npmVersion"` + Dist IndexJsonDist `json:"dist"` + Dependencies map[string]string `json:"dependencies,omitempty"` + DevDependencies map[string]string `json:"devDependencies,omitempty"` + PeerDependencies map[string]string `json:"peerDependencies,omitempty"` + PeerDependenciesMeta map[string]map[string]bool `json:"peerDependenciesMeta,omitempty"` + BundleDependencies []string `json:"bundleDependencies,omitempty"` + Resolutions map[string]string `json:"resolutions,omitempty"` } type IndexJson struct { - ID string `json:"_id"` - Name string `json:"name"` - Description string `json:"description"` - DistTags map[string]string `json:"dist-tags"` - Versions map[string]IndexJsonVersions `json:"versions"` - Access string `json:"access"` - Attachments map[string]IndexJsonAttachments `json:"_attachments"` + ID string `json:"_id"` + Name string `json:"name"` + Description string `json:"description"` + Readme string `json:"readme,omitempty"` + ReadmeFilename string `json:"readmeFilename,omitempty"` + DistTags map[string]string `json:"dist-tags"` + TimesPackage map[string]string `json:"time"` + Versions map[string]IndexJsonVersions `json:"versions"` + Access string `json:"access"` + Attachments map[string]IndexJsonAttachments `json:"_attachments"` +} + +type IndexJsonAbridgedVersions struct { + HasShrinkWrap bool `json:"_hasShrinkwrap"` + Dist IndexJsonDist `json:"dist"` + Name string `json:"name"` + Version string `json:"version"` +} + +// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md +type IndexJsonAbridged struct { + DistTags map[string]string `json:"dist-tags"` + Modified string `json:"modified"` + Name string `json:"name"` + Versions map[string]IndexJsonAbridgedVersions `json:"versions"` } type TagPutResponse struct { @@ -52,3 +111,8 @@ type TagDeleteResponse struct { ID string `json:"id"` DistTags string `json:"dist-tags"` } + +type PublishPutResponse struct { + Ok bool `json:"ok"` + Name string `json:"package_name"` +} diff --git a/internal/storage/fs.go b/internal/storage/fs.go index 4459285..3dac426 100644 --- a/internal/storage/fs.go +++ b/internal/storage/fs.go @@ -1,8 +1,6 @@ package storage import ( - "archive/tar" - "compress/gzip" "encoding/base64" "encoding/json" "fmt" @@ -18,7 +16,9 @@ import ( "github.com/sirupsen/logrus" ) -func GetIndexJsonFromStore(packageName string, registryPath string, log *logrus.Logger) (string, bool, error) { +type FSStorage struct{} + +func (f *FSStorage) GetIndexJsonFromStore(packageName string, registryPath string, log *logrus.Logger) (string, bool, error) { fileToServe := "" found := false @@ -33,7 +33,7 @@ func GetIndexJsonFromStore(packageName string, registryPath string, log *logrus. if err != nil { log.WithFields(logrus.Fields{ "function": "get-index-json-from-store", - }).Debugf("List files error: +%v\n", err) + }).Errorf("List files error: +%v\n", err) return fileToServe, found, err } @@ -44,7 +44,7 @@ func GetIndexJsonFromStore(packageName string, registryPath string, log *logrus. return fileToServe, found, nil } -func GetTarFromStore(packageName string, tarFileName string, registryPath string, log *logrus.Logger) (string, error) { +func (f *FSStorage) GetTarFromStore(packageName string, tarFileName string, registryPath string, log *logrus.Logger) (string, error) { fileToServe := "" err := filepath.WalkDir(registryPath, func(fp string, info fs.DirEntry, e error) error { @@ -57,7 +57,7 @@ func GetTarFromStore(packageName string, tarFileName string, registryPath string if err != nil { log.WithFields(logrus.Fields{ "function": "get-tar-from-store", - }).Debugf("List files error: +%v\n", err) + }).Errorf("List files error: +%v\n", err) return fileToServe, err } @@ -69,35 +69,28 @@ func GetTarFromStore(packageName string, tarFileName string, registryPath string if err != nil { log.WithFields(logrus.Fields{ "function": "get-tar-from-store", - }).Debugf("Open error: %s\n", fileToServe) + }).Errorf("Open error: %s\n", fileToServe) return "", err } + defer file.Close() - archive, err := gzip.NewReader(file) + bs, err := io.ReadAll(file) if err != nil { log.WithFields(logrus.Fields{ "function": "get-tar-from-store", - }).Debugf("Archive Open error: %s\n", fileToServe) + }).Errorf("File Read error: %s\n", fileToServe) return "", err } - tr := tar.NewReader(archive) - bs, err := io.ReadAll(tr) - if err != nil { - log.WithFields(logrus.Fields{ - "function": "get-tar-from-store", - }).Debugf("Archive Read error: %s\n", fileToServe) - return "", err - } return base64.StdEncoding.EncodeToString(bs), err } -func ReadIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger) error { +func (f *FSStorage) ReadIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger) error { jsonFile, err := os.Open(fPath) if err != nil { log.WithFields(logrus.Fields{ "function": "read-index-json", - }).Debugf("File Not found: %s\n", fPath) + }).Errorf("File Not found: %s\n", fPath) return err } @@ -107,21 +100,21 @@ func ReadIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger) if err != nil { log.WithFields(logrus.Fields{ "function": "read-index-json", - }).Debugf("Unmarshalerror: %+v\n", err) + }).Errorf("Unmarshalerror: %+v\n", err) return err } return nil } -func WriteIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger) error { +func (f *FSStorage) WriteIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger) error { // Need to create the directory first parent := path.Dir(fPath) err := os.MkdirAll(parent, os.ModePerm) if err != nil { log.WithFields(logrus.Fields{ "function": "write-index-json", - }).Debugf("Folder (%s) creation failed.\n", fPath) + }).Errorf("Folder (%s) creation failed.\n", fPath) return err } @@ -131,7 +124,7 @@ func WriteIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger if err != nil { log.WithFields(logrus.Fields{ "function": "write-index-json", - }).Debugf("Creation error for path(%s): %+v\n ", fPath, err) + }).Errorf("Creation error for path(%s): %+v\n ", fPath, err) return err } @@ -148,12 +141,12 @@ func WriteIndexJson(fPath string, res *serviceidos.IndexJson, log *logrus.Logger return nil } -func WritePackageToStore(fPath string, data string, log *logrus.Logger) error { +func (f *FSStorage) WritePackageToStore(fPath string, data string, log *logrus.Logger) error { dec, err := base64.StdEncoding.DecodeString(data) if err != nil { log.WithFields(logrus.Fields{ "function": "write-package-to-store", - }).Debugf("Base64 Decode error: %+v\n", err) + }).Errorf("Base64 Decode error: %+v\n", err) return err } @@ -161,7 +154,7 @@ func WritePackageToStore(fPath string, data string, log *logrus.Logger) error { if err != nil { log.WithFields(logrus.Fields{ "function": "write-package-to-store", - }).Debugf("Creation error: %s\n", fPath) + }).Errorf("Creation error: %s\n", fPath) return err } @@ -171,7 +164,7 @@ func WritePackageToStore(fPath string, data string, log *logrus.Logger) error { if err != nil { log.WithFields(logrus.Fields{ "function": "write-package-to-store", - }).Debugf("Write error: %s\n", fPath) + }).Errorf("Write error: %s\n", fPath) return err } @@ -179,7 +172,7 @@ func WritePackageToStore(fPath string, data string, log *logrus.Logger) error { if err != nil { log.WithFields(logrus.Fields{ "function": "write-package-to-store", - }).Debugf("Sync error: %s\n", fPath) + }).Errorf("Sync error: %s\n", fPath) return err } diff --git a/internal/storage/mockfs.go b/internal/storage/mockfs.go new file mode 100644 index 0000000..a11193f --- /dev/null +++ b/internal/storage/mockfs.go @@ -0,0 +1,56 @@ +package storage + +import ( + "gosimplenpm/internal/serviceidos" + + "github.com/sirupsen/logrus" +) + +type MockFs struct { + calls map[string]int + GetIndexJsonFromStoreFunc func(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) + GetTarFromStoreFunc func(packageName string, tarFileName string, registryPath string, lg *logrus.Logger) (string, error) + ReadIndexJsonFunc func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error + WriteIndexJsonFunc func(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error + WritePackageToStoreFunc func(fPath string, data string, lg *logrus.Logger) error +} + +func (m *MockFs) GetIndexJsonFromStore(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) { + if len(m.calls) == 0 { + m.calls = make(map[string]int) + } + m.calls["GetIndexJsonFromStore"] += 1 + return m.GetIndexJsonFromStoreFunc(packageName, registryPath, lg) +} + +func (m *MockFs) GetTarFromStore(packageName string, tarFileName string, registryPath string, lg *logrus.Logger) (string, error) { + if len(m.calls) == 0 { + m.calls = make(map[string]int) + } + m.calls["GetTarFromStore"] += 1 + return m.GetTarFromStoreFunc(packageName, tarFileName, registryPath, lg) +} + +func (m *MockFs) ReadIndexJson(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + if len(m.calls) == 0 { + m.calls = make(map[string]int) + } + m.calls["ReadIndexJson"] += 1 + return m.ReadIndexJsonFunc(fPath, res, lg) +} + +func (m *MockFs) WriteIndexJson(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error { + if len(m.calls) == 0 { + m.calls = make(map[string]int) + } + m.calls["WriteIndexJson"] += 1 + return m.WriteIndexJsonFunc(fPath, res, lg) +} + +func (m *MockFs) WritePackageToStore(fPath string, data string, lg *logrus.Logger) error { + if len(m.calls) == 0 { + m.calls = make(map[string]int) + } + m.calls["WritePackageToStore"] += 1 + return m.WritePackageToStoreFunc(fPath, data, lg) +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go new file mode 100644 index 0000000..6bffbee --- /dev/null +++ b/internal/storage/storage.go @@ -0,0 +1,15 @@ +package storage + +import ( + "gosimplenpm/internal/serviceidos" + + "github.com/sirupsen/logrus" +) + +type Storage interface { + GetIndexJsonFromStore(packageName string, registryPath string, lg *logrus.Logger) (string, bool, error) + GetTarFromStore(packageName string, tarFileName string, registryPath string, lg *logrus.Logger) (string, error) + ReadIndexJson(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error + WriteIndexJson(fPath string, res *serviceidos.IndexJson, lg *logrus.Logger) error + WritePackageToStore(fPath string, data string, lg *logrus.Logger) error +}