diff --git a/app.go b/app.go deleted file mode 100644 index 1ebac57..0000000 --- a/app.go +++ /dev/null @@ -1,36 +0,0 @@ -package main - -import ( - "gosimplenpm/config" - "gosimplenpm/handler" - "gosimplenpm/middlewares" - "log" - "net/http" - - "github.com/gorilla/mux" -) - -type application struct { - logger *log.Logger - conf config.Config -} - -func (app *application) Routes() *mux.Router { - - // Need to use UseEncodedPath as shown here https://github.com/gorilla/mux/blob/master/mux.go#L269 - m := mux.NewRouter().StrictSlash(true).UseEncodedPath() - - m.Use(middlewares.LogMiddleware) - - // main handler - m.HandleFunc("/{name}", middlewares.AuthMiddleware(app.conf)(handler.Get)).Methods("GET") - m.HandleFunc("/{name}", middlewares.AuthMiddleware(app.conf)(handler.Publish)).Methods("PUT") - // tar handlers - m.HandleFunc("/{name}/-/{tar}", handler.Tar).Methods("GET") - // tag handlers - m.HandleFunc("/-/package/{name}/dist-tags/{tag}", handler.DistTagDelete).Methods("DELETE") - m.HandleFunc("/-/package/{name}/dist-tags/{tag}", handler.DistTagPut).Methods("PUT") - m.HandleFunc("/-/package/{name}/dist-tags", handler.DistTagGet).Methods("GET") - m.NotFoundHandler = http.HandlerFunc(handler.NotFound) - return m -} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..a320867 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,36 @@ +package main + +import ( + "gosimplenpm/cmd/npmregserver" + "gosimplenpm/config" + "net/http" + "os" + + "github.com/sirupsen/logrus" +) + +func main() { + log := &logrus.Logger{ + Out: os.Stdout, + Level: logrus.DebugLevel, + Formatter: &logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2009-01-02 15:15:15", + }, + } + + var cfg config.Config + err := config.LoadConfiguration("userdata/config.json", &cfg) + if err != nil { + log.Fatalf("Config is not loaded: %+v\n", err) + } + + app := &npmregserver.Application{ + Conf: cfg, + Logger: log, + } + + log.Infoln("Starting server on port 4000") + err = http.ListenAndServe(":4000", app.Routes()) + log.Fatal(err) +} diff --git a/cmd/npmregserver/app.go b/cmd/npmregserver/app.go new file mode 100644 index 0000000..501b6e7 --- /dev/null +++ b/cmd/npmregserver/app.go @@ -0,0 +1,36 @@ +package npmregserver + +import ( + "gosimplenpm/config" + "gosimplenpm/handler" + "gosimplenpm/middlewares" + "net/http" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" +) + +type Application struct { + Logger *logrus.Logger + Conf config.Config +} + +func (app *Application) Routes() *mux.Router { + + // Need to use UseEncodedPath as shown here https://github.com/gorilla/mux/blob/master/mux.go#L269 + m := mux.NewRouter().StrictSlash(true).UseEncodedPath() + + m.Use(middlewares.LogMiddleware(app.Logger)) + + // main handler + m.HandleFunc("/{name}", handler.Get).Methods("GET") + m.HandleFunc("/{name}", middlewares.AuthMiddleware(app.Conf)(handler.Publish)).Methods("PUT") + // tar handlers + m.HandleFunc("/{name}/-/{tar}", handler.Tar).Methods("GET") + // tag handlers + m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(handler.DistTagDelete(app.Logger))).Methods("DELETE") + m.HandleFunc("/-/package/{name}/dist-tags/{tag}", middlewares.AuthMiddleware(app.Conf)(handler.DistTagPut(app.Logger))).Methods("PUT") + m.HandleFunc("/-/package/{name}/dist-tags", handler.DistTagGet(app.Logger)).Methods("GET") + m.NotFoundHandler = http.HandlerFunc(handler.NotFound) + return m +} diff --git a/go.mod b/go.mod index a111b6f..4366655 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,12 @@ module gosimplenpm go 1.20 -require github.com/gorilla/mux v1.8.0 +require ( + github.com/gorilla/mux v1.8.0 + github.com/sirupsen/logrus v1.9.3 + golang.org/x/mod v0.11.0 +) + +require golang.org/x/sys v0.9.0 // indirect // replace gosimplenpm/handler => ./handler diff --git a/go.sum b/go.sum index 5350288..a5b6272 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,20 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handler/notfound.go b/handler/notfound.go index e5f6da8..5a29505 100644 --- a/handler/notfound.go +++ b/handler/notfound.go @@ -1,11 +1,10 @@ package handler import ( - "fmt" "net/http" ) // This handler is executed when the router cannot match any route func NotFound(w http.ResponseWriter, r *http.Request) { - fmt.Printf("%s - %s - %s\n", r.Method, r.URL, r.Host) + http.Error(w, "Invalid url", http.StatusBadRequest) } diff --git a/handler/tagdelete.go b/handler/tagdelete.go index bfa906a..41a09b9 100644 --- a/handler/tagdelete.go +++ b/handler/tagdelete.go @@ -1,9 +1,79 @@ package handler import ( + "encoding/json" + "fmt" + "gosimplenpm/serviceidos" + "gosimplenpm/storage" "net/http" + "net/url" + "strconv" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + "golang.org/x/mod/semver" ) -func DistTagDelete(w http.ResponseWriter, r *http.Request) { +func DistTagDelete(lg *logrus.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + escapedName := mux.Vars(r)["name"] + packageName, _ := url.PathUnescape(escapedName) + lg.Printf("Package name => %s\n", packageName) + escapedName = mux.Vars(r)["tag"] + tag, _ := url.PathUnescape(escapedName) + lg.Printf("Tag => %s\n", tag) + + if semver.IsValid(tag) { + http.Error(w, "Tag cannot be a semver version", http.StatusBadRequest) + return + } + + if tag == "latest" { + http.Error(w, "Cannot delete the latest tag", http.StatusBadRequest) + return + } + + fileToServe, found, err := storage.GetIndexJsonFromStore(packageName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if !found { + ret := fmt.Sprintf("Package not found: %s", packageName) + http.Error(w, ret, http.StatusNotFound) + return + } + + var jsonFile serviceidos.IndexJson + err = storage.ReadIndexJson(fileToServe, &jsonFile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + delete(jsonFile.DistTags, tag) + + // Write index.json + err = storage.WriteIndexJson(fileToServe, &jsonFile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + distTagsString, _ := json.Marshal(jsonFile.DistTags) + + response := serviceidos.TagPutResponse{ + Ok: true, + ID: escapedName, + DistTags: string(distTagsString), + } + + jsonString, _ := json.Marshal(response) + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", strconv.Itoa(len(jsonString))) + w.Write(jsonString) + } } diff --git a/handler/tagget.go b/handler/tagget.go index 568c2af..5f5d38e 100644 --- a/handler/tagget.go +++ b/handler/tagget.go @@ -1,9 +1,47 @@ package handler import ( + "encoding/json" + "fmt" + "gosimplenpm/serviceidos" + "gosimplenpm/storage" "net/http" + "net/url" + "strconv" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" ) -func DistTagGet(w http.ResponseWriter, r *http.Request) { +func DistTagGet(lg *logrus.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + escapedName := mux.Vars(r)["name"] + packageName, _ := url.PathUnescape(escapedName) + lg.Debugf("Package name => %s\n", packageName) + fileToServe, found, err := storage.GetIndexJsonFromStore(packageName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if !found { + ret := fmt.Sprintf("Package not found: %s", packageName) + http.Error(w, ret, http.StatusNotFound) + return + } + + var jsonFile serviceidos.IndexJson + err = storage.ReadIndexJson(fileToServe, &jsonFile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + jsonString, _ := json.Marshal(jsonFile.DistTags) + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", strconv.Itoa(len(jsonString))) + w.Write(jsonString) + } } diff --git a/handler/tagput.go b/handler/tagput.go index b050b3d..07a4cea 100644 --- a/handler/tagput.go +++ b/handler/tagput.go @@ -1,9 +1,85 @@ package handler import ( + "encoding/json" + "fmt" + "gosimplenpm/serviceidos" + "gosimplenpm/storage" + "io" "net/http" + "net/url" + "strconv" + + "github.com/gorilla/mux" + "github.com/sirupsen/logrus" + "golang.org/x/mod/semver" ) -func DistTagPut(w http.ResponseWriter, r *http.Request) { +func DistTagPut(lg *logrus.Logger) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + escapedName := mux.Vars(r)["name"] + packageName, _ := url.PathUnescape(escapedName) + lg.Printf("Package name => %s\n", packageName) + escapedName = mux.Vars(r)["tag"] + tag, _ := url.PathUnescape(escapedName) + lg.Printf("Tag => %s\n", tag) + + if semver.IsValid(tag) { + http.Error(w, "Tag cannot be a semver version", http.StatusBadRequest) + return + } + + if tag == "latest" { + http.Error(w, "Cannot delete the latest tag", http.StatusBadRequest) + return + } + + body, _ := io.ReadAll(r.Body) + var version string + _ = json.Unmarshal(body, &version) + lg.Printf("Body => %s", version) + + fileToServe, found, err := storage.GetIndexJsonFromStore(packageName) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if !found { + ret := fmt.Sprintf("Package not found: %s", packageName) + http.Error(w, ret, http.StatusNotFound) + return + } + + var jsonFile serviceidos.IndexJson + err = storage.ReadIndexJson(fileToServe, &jsonFile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + jsonFile.DistTags[tag] = version + + // Write index.json + err = storage.WriteIndexJson(fileToServe, &jsonFile) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + distTagsString, _ := json.Marshal(jsonFile.DistTags) + + response := serviceidos.TagPutResponse{ + Ok: true, + ID: escapedName, + DistTags: string(distTagsString), + } + + jsonString, _ := json.Marshal(response) + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", strconv.Itoa(len(jsonString))) + w.Write(jsonString) + } } diff --git a/main.go b/main.go deleted file mode 100644 index f004808..0000000 --- a/main.go +++ /dev/null @@ -1,45 +0,0 @@ -package main - -import ( - "gosimplenpm/config" - "log" - "net/http" -) - -func main() { - var cfg config.Config - err := config.LoadConfiguration("userdata/config.json", &cfg) - if err != nil { - log.Fatalf("Config is not loaded: %+v\n", err) - } - - app := &application{ - conf: cfg, - } - log.Print("Starting server on port 4000") - err = http.ListenAndServe(":4000", app.Routes()) - log.Fatal(err) -} - -// func main() { -// router := mux.NewRouter() -// router.NewRoute().Path("/{name}").Methods("PUT") -// router.NewRoute().Path("/{name}").Methods("GET") - -// rMatch := &mux.RouteMatch{} - -// u := url.URL{Path: "/@ookusanya%2fsimplepackone"} -// req := http.Request{Method: "GET", URL: &u} - -// x := router.Match(&req, rMatch) -// fmt.Println("Is Matched: ", x) - -// reqt := http.Request{Method: "PUT", URL: &u} -// g := router.Match(&reqt, rMatch) -// fmt.Println("Is Matched: ", g) - -// ut := url.URL{Path: "/@ookusanya%2fsimplepackone/-/simplepackone-1.0.0.tgz"} -// rt := http.Request{Method: "PUT", URL: &ut} -// gt := router.Match(&rt, rMatch) -// fmt.Println("Is Matched: ", gt) -// } diff --git a/middlewares/auth.go b/middlewares/auth.go index dcd4efb..cc3cf91 100644 --- a/middlewares/auth.go +++ b/middlewares/auth.go @@ -6,7 +6,7 @@ import ( "strings" ) -func AuthMiddleware(cfg config.Config) Middleware { +func AuthMiddleware(cfg config.Config) func(http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // get headers @@ -26,30 +26,3 @@ func AuthMiddleware(cfg config.Config) Middleware { } } } - -// func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc { -// return func(w http.ResponseWriter, r *http.Request) { -// if cfg == nil { -// log.Println("Config load error") -// http.Error(w, "Config load error", http.StatusInternalServerError) -// return -// } - -// log.Println("Config was loaded") - -// // get headers -// authHeader := r.Header.Get("Authorization") -// authFields := strings.Fields(authHeader) -// if len(authFields) != 2 || strings.ToLower(authFields[0]) != "bearer" { -// http.Error(w, "Authentication Error", http.StatusForbidden) -// return -// } -// token := authFields[1] -// if token != cfg.Token { -// http.Error(w, "Authentication Error", http.StatusForbidden) -// return -// } -// fmt.Println("Authorized") -// next(w, r) -// } -// } diff --git a/middlewares/logRequest.go b/middlewares/logRequest.go index 52e1cb5..6a9cc72 100644 --- a/middlewares/logRequest.go +++ b/middlewares/logRequest.go @@ -1,33 +1,29 @@ package middlewares import ( - "fmt" - "log" "net/http" "net/http/httputil" + + "github.com/sirupsen/logrus" ) -func LogMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Printf("%s - %s - %s", r.Method, r.URL, r.Host) +func LogMiddleware(lg *logrus.Logger) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + lg.Debugf("%s - %s - %s", r.Method, r.URL, r.Host) - hasBody := false - if r.Method == "PUT" { - hasBody = true - } + hasBody := false + if r.Method == "PUT" { + hasBody = true + } - requestDump, err := httputil.DumpRequest(r, hasBody) - if err != nil { - fmt.Println(err) - } - fmt.Println("RequestDump: ", string(requestDump)) + requestDump, err := httputil.DumpRequest(r, hasBody) + if err != nil { + lg.Debugln(err) + } + lg.Debugln("RequestDump: ", string(requestDump)) - // fmt.Println("Printing headers") - // for name, values := range r.Header { - // for _, value := range values { - // fmt.Printf("%s:%s\n", name, value) - // } - // } - next.ServeHTTP(w, r) - }) + next.ServeHTTP(w, r) + }) + } } diff --git a/middlewares/middlewaretype.go b/middlewares/middlewaretype.go deleted file mode 100644 index a160084..0000000 --- a/middlewares/middlewaretype.go +++ /dev/null @@ -1,7 +0,0 @@ -package middlewares - -import ( - "net/http" -) - -type Middleware func(http.HandlerFunc) http.HandlerFunc diff --git a/serviceidos/responseidos.go b/serviceidos/responseidos.go index 08b82e7..820b01e 100644 --- a/serviceidos/responseidos.go +++ b/serviceidos/responseidos.go @@ -40,3 +40,15 @@ type IndexJson struct { Access string `json:"access"` Attachments map[string]IndexJsonAttachments `json:"_attachments"` } + +type TagPutResponse struct { + Ok bool `json:"ok"` + ID string `json:"id"` + DistTags string `json:"dist-tags"` +} + +type TagDeleteResponse struct { + Ok bool `json:"ok"` + ID string `json:"id"` + DistTags string `json:"dist-tags"` +} diff --git a/tests/data/@ookusanya/simplepackone/index.json b/tests/data/@ookusanya/simplepackone/index.json new file mode 100644 index 0000000..ab0402b --- /dev/null +++ b/tests/data/@ookusanya/simplepackone/index.json @@ -0,0 +1 @@ +{"_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.0.0":{"name":"@ookusanya/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","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.0.0","_nodeVersion":"16.20.0","_npmVersion":"8.19.4","dist":{"integrity":"sha512-x9cAEHDPM0dLhdyd8baWLTNYskr41QXLNv5HS5WNDg1CVzwcMaAU+N9lYVuOvLf/xPHLielWmX+wLXEtRWQvGw==","shasum":"545a7737b59b1a6f064339a4ebe056e8a9b1511d","tarball":"http://localhost:4000/@ookusanya/simplepackone/-/@ookusanya/simplepackone-1.0.0.tgz"},"dependencies":{"eslint":"^7.x"}},"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\" \u0026\u0026 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}}} diff --git a/tests/data/@ookusanya/simplepackone/simplepackone-1.0.0.tgz b/tests/data/@ookusanya/simplepackone/simplepackone-1.0.0.tgz new file mode 100644 index 0000000..e5fae89 Binary files /dev/null and b/tests/data/@ookusanya/simplepackone/simplepackone-1.0.0.tgz differ diff --git a/tests/data/@ookusanya/simplepackone/simplepackone-1.2.0.tgz b/tests/data/@ookusanya/simplepackone/simplepackone-1.2.0.tgz new file mode 100644 index 0000000..c0cfe49 Binary files /dev/null and b/tests/data/@ookusanya/simplepackone/simplepackone-1.2.0.tgz differ