Compare commits

...

5 Commits
main ... tests

Author SHA1 Message Date
OLUWADAMILOLA OKUSANYA fe8c87de18 Added unit and integration tests 2023-12-25 03:56:48 -05:00
OLUWADAMILOLA OKUSANYA f3dede1e33 Moved everything to internal 2023-11-24 21:17:09 -05:00
OLUWADAMILOLA OKUSANYA cb52ddab05 Adding command line options 2023-06-30 15:09:57 -04:00
OLUWADAMILOLA OKUSANYA e3e81130ba Adding tags command.
Refactoring main and app.go to cmd. Refactoring to adding logrus instead of log. Refactoring handlers to add a global logger.
2023-06-17 18:41:07 -04:00
OLUWADAMILOLA OKUSANYA 234b593c26 Publish, View and Tar commands added
Adding middlewares. Adding a filesystem service. Adding types to represent the put request and the index.json that is stored at the root of the package. Adding conf.go to pull config variables from json
2023-06-17 00:08:50 -04:00
58 changed files with 4213 additions and 146 deletions

36
.gitignore vendored Normal file
View File

@ -0,0 +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
e2e
site/
.vscode/
.idea/
.DS_Store
__rd*
tests/out

View File

@ -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

View File

@ -10,6 +10,22 @@ TODO...
TODO...
# Work List
# TODO List
TODO...
- [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
```

28
app.go
View File

@ -1,28 +0,0 @@
package main
import (
"log"
"github.com/gorilla/mux"
"gosimplenpm/handler"
)
type application struct {
logger *log.Logger
}
func (app *application) Routes() *mux.Router {
m := mux.NewRouter()
// main handler
m.HandleFunc("/{name}", handler.Get).Methods("GET")
m.HandleFunc("/{name}", 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")
return m
}

26
cmd/gosimplenpm/conf.go Normal file
View File

@ -0,0 +1,26 @@
package gosimplenpm
import (
"fmt"
"gosimplenpm/internal/config"
"os"
"github.com/spf13/cobra"
)
var configCmd = &cobra.Command{
Use: "config",
Aliases: []string{"conf"},
Short: "Display the config file",
Run: func(_ *cobra.Command, _ []string) {
err := config.PrintConfigFile()
if err != nil {
fmt.Printf("Error printing config: %+v\n", err)
os.Exit(1)
}
},
}
func init() {
rootCmd.AddCommand(configCmd)
}

84
cmd/gosimplenpm/root.go Normal file
View File

@ -0,0 +1,84 @@
package gosimplenpm
import (
"fmt"
"gosimplenpm/internal/config"
"gosimplenpm/internal/handler"
"gosimplenpm/internal/storage"
"os"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "gosimplenpm",
Short: "Gosimplenpm is an implementation of npm registry server",
Long: `Gosimplenpm is an implemenation of npm registry server.
It creates the repository to host all your private npm repos. It can
serve the npm packages offline.
Documentation about the npm private registry:
https://docs.npmjs.com/packages-and-modules
`,
Run: func(_ *cobra.Command, _ []string) {
err := config.VerifyConfig()
if err != nil {
fmt.Printf("Error verifying config: %+v\n", err)
os.Exit(1)
}
var cfg config.Config
err = config.LoadOrCreateConfig(&cfg)
if err != nil {
fmt.Printf("Error loading config: %+v\n", err)
os.Exit(1)
}
lvl, err := logrus.ParseLevel(cfg.LogLevel)
if err != nil {
fmt.Printf("%+v", err)
os.Exit(1)
}
log := &logrus.Logger{
Out: os.Stdout,
Level: lvl,
Formatter: &logrus.TextFormatter{
FullTimestamp: true,
TimestampFormat: "2009-01-02 15:15:15",
},
}
app := &handler.Application{
Conf: cfg,
Logger: log,
FSStorage: &storage.FSStorage{},
}
fmt.Println("\n Server is starting....")
err = app.Start()
if err != nil {
fmt.Printf("Server start up error: %+v\n", err)
os.Exit(1)
}
},
}
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
rootCmd.CompletionOptions.HiddenDefaultCmd = true
// hide help command
rootCmd.SetHelpCommand(&cobra.Command{Hidden: true})
// hide help flag
rootCmd.PersistentFlags().BoolP("help", "h", false, "This help")
rootCmd.PersistentFlags().Lookup("help").Hidden = true
// TODO: get some variables from flags
rootCmd.Flags().StringVarP(&config.HTTPListen, "listen", "l", config.HTTPListen, "HTTP bind interface and port for server")
_ = rootCmd.MarkFlagRequired("listen")
rootCmd.Flags().BoolVarP(&config.CanLog, "verbose", "v", config.CanLog, "Logging level for server")
rootCmd.Flags().StringVar(&config.NpmRepoDir, "repodir", config.NpmRepoDir, "Repo dir to house published packages")
}

18
go.mod
View File

@ -2,6 +2,20 @@ 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
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
)
// replace gosimplenpm/handler => ./handler
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
)

30
go.sum
View File

@ -1,2 +1,32 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=
golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
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 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=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,29 +0,0 @@
package handler
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
"gosimplenpm/storage"
)
func Get(w http.ResponseWriter, r *http.Request) {
packageName := mux.Vars(r)["name"]
fileToServe, err := storage.GetPackageFromStore(packageName)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if fileToServe == "" {
ret := fmt.Sprintf("Package not found: %s", packageName)
http.Error(w, ret, http.StatusNotFound)
return
}
// serve file
http.ServeFile(w, r, fileToServe)
}

View File

@ -1,9 +0,0 @@
package handler
import (
"net/http"
)
func Publish(w http.ResponseWriter, r *http.Request) {
}

View File

@ -1,9 +0,0 @@
package handler
import (
"net/http"
)
func DistTagDelete(w http.ResponseWriter, r *http.Request) {
}

View File

@ -1,9 +0,0 @@
package handler
import (
"net/http"
)
func DistTagGet(w http.ResponseWriter, r *http.Request) {
}

View File

@ -1,9 +0,0 @@
package handler
import (
"net/http"
)
func DistTagPut(w http.ResponseWriter, r *http.Request) {
}

View File

@ -1,9 +0,0 @@
package handler
import (
"net/http"
)
func Tar(w http.ResponseWriter, r *http.Request) {
}

318
internal/config/conf.go Normal file
View File

@ -0,0 +1,318 @@
package config
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"os"
"path"
"golang.org/x/crypto/bcrypt"
)
var (
// File to store the app configuration, like username, password, token, repo dir, logging level
ConfigFilePath string
// Repo dir
NpmRepoDir string
// HTTPListen
HTTPListen string
// Set Verbose Logging
CanLog bool
// Logging Level
LoggingLvl string
// Username
RegUser string
// Password
RegPwd string
)
type Config struct {
Token string `json:"token"`
RepoDir string `json:"repoDir"`
IpAddress string `json:"ipAddress"`
LogLevel string `json:"logLevel"`
}
func checkIfCorrectIPPort(s string) bool {
host, port, _ := net.SplitHostPort(s)
if host == "" || port == "" {
return false
}
if net.ParseIP(host) != nil {
return true
}
_, err := net.ResolveIPAddr("ip", host)
return err == nil
}
func VerifyConfig() error {
if !checkIfCorrectIPPort(HTTPListen) {
return errors.New("ip address should be in the format of <ip>:<port>")
}
// Check if config file exists
dirname, err := os.UserHomeDir()
if err != nil {
return err
}
configDirPath, err := checkOrCreateConfigDir(dirname, true)
if err != nil {
return err
}
// TODO: Create a logging file (it should be located at .gosimplenpm/applogs)
ConfigFilePath = path.Join(configDirPath, "config.json")
if NpmRepoDir == "" {
NpmRepoDir = path.Join(dirname, ".gosimplenpm", "registry")
err := checkOrCreateRepoDir(NpmRepoDir)
if err != nil {
return err
}
} else if NpmRepoDir != "" {
err := checkOrCreateRepoDir(NpmRepoDir)
if err != nil {
return err
}
}
if CanLog {
LoggingLvl = "DEBUG"
fmt.Println("\n Enabled debug logging")
} else {
LoggingLvl = "INFO"
}
return nil
}
func checkOrCreateConfigDir(fp string, canCreate bool) (string, error) {
configDirPath := path.Join(fp, ".gosimplenpm", "config")
ok := isDir(configDirPath)
if !ok && canCreate {
err := os.MkdirAll(configDirPath, os.ModePerm)
if err != nil {
return "", err
}
}
if !ok && !canCreate {
return "", nil
}
return configDirPath, nil
}
func checkOrCreateRepoDir(repoDirPath string) error {
ok := isDir(repoDirPath)
if !ok {
err := os.MkdirAll(repoDirPath, os.ModePerm)
if err != nil {
return err
}
}
return nil
}
func createConfig(cfg *Config, recreate bool) error {
var scanner *bufio.Scanner
if recreate {
fmt.Println("\nNew config variables. Saving...")
} else {
fmt.Println("\nConfig file is not found. Creating...")
}
configFile, err := os.Create(ConfigFilePath)
if err != nil {
return err
}
defer configFile.Close()
cfg.IpAddress = HTTPListen
cfg.LogLevel = LoggingLvl
// Get username
if cfg.Token == "" {
fmt.Println("Enter your username: ")
scanner = bufio.NewScanner(os.Stdin)
scanner.Scan()
err = scanner.Err()
if err != nil {
return err
}
RegUser = scanner.Text()
fmt.Println("Enter your password: ")
scanner = bufio.NewScanner(os.Stdin)
scanner.Scan()
err = scanner.Err()
if err != nil {
return err
}
RegPwd = scanner.Text()
token, err := generateAuthToken()
if err != nil {
return err
}
cfg.Token = token
}
fmt.Printf("The npm authToken is %s.\n", cfg.Token)
err = json.NewEncoder(configFile).Encode(cfg)
if err != nil {
return err
}
return nil
}
func loadConfig(cfg *Config) error {
configFile, err := os.Open(ConfigFilePath)
if err != nil {
return err
}
defer configFile.Close()
err = json.NewDecoder(configFile).Decode(cfg)
if err != nil {
return err
}
return nil
}
func LoadOrCreateConfig(cfg *Config) error {
var err error
ok := isFile(ConfigFilePath)
// If file is not found
if !ok {
err = createConfig(cfg, false)
if err != nil {
return err
}
}
if ok {
// File is found
err = loadConfig(cfg)
if err != nil {
return err
}
if cfg.Token == "" || cfg.IpAddress != HTTPListen || cfg.LogLevel != LoggingLvl || cfg.RepoDir != NpmRepoDir {
// recreate the config file
err = createConfig(cfg, true)
if err != nil {
return err
}
}
}
return err
}
func isFile(fp string) bool {
info, err := os.Stat(fp)
if os.IsNotExist(err) || !info.Mode().IsRegular() {
return false
}
return true
}
func isDir(fp string) bool {
info, err := os.Stat(fp)
if os.IsNotExist(err) || !info.IsDir() {
return false
}
return true
}
// Hash password
func hashPassword(password string) (string, error) {
// Convert password string to byte slice
var passwordBytes = []byte(password)
// Hash password with Bcrypt's min cost
hashedPasswordBytes, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.MinCost)
return string(hashedPasswordBytes), err
}
// Check if two passwords match using Bcrypt's CompareHashAndPassword
// which return nil on success and an error on failure.
// func doPasswordsMatch(hashedPassword, currPassword string) bool {
// err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(currPassword))
// return err == nil
// }
func generateAuthToken() (string, error) {
hashed, err := hashPassword(RegPwd)
if err != nil {
return "", err
}
token := fmt.Sprintf("%s::%s", RegUser, hashed)
return token, nil
}
func PrintConfigFile() error {
// Check if config file exists
dirname, err := os.UserHomeDir()
if err != nil {
return err
}
configDirPath, err := checkOrCreateConfigDir(dirname, false)
if err != nil {
return err
}
if configDirPath == "" {
return errors.New("config dir is not found")
}
ConfigFilePath = path.Join(configDirPath, "config.json")
ok := isFile(ConfigFilePath)
if !ok {
return errors.New("config file is not found")
}
configFile, err := os.Open(ConfigFilePath)
if err != nil {
return err
}
defer configFile.Close()
b, err := io.ReadAll(configFile)
if err != nil {
return err
}
var result map[string]interface{}
err = json.Unmarshal([]byte(b), &result)
if err != nil {
return err
}
// Pretty-print the result
marshaled, err := json.MarshalIndent(result, "", " ")
if err != nil {
return err
}
fmt.Printf("Printing config located at %s: \n %s\n", ConfigFilePath, (marshaled))
return nil
}

50
internal/handler/app.go Normal file
View File

@ -0,0 +1,50 @@
package handler
import (
"gosimplenpm/internal/config"
"gosimplenpm/internal/middlewares"
"gosimplenpm/internal/storage"
"net/http"
"time"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
type Application struct {
Logger *logrus.Logger
Conf config.Config
Mux *mux.Router
FSStorage storage.Storage
}
func (app *Application) Routes() {
// 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}", 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, app.FSStorage)).Methods("GET")
// tag handlers
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
}
func (app *Application) Start() error {
app.Routes()
server := &http.Server{
Addr: app.Conf.IpAddress,
Handler: app.Mux,
ReadTimeout: 4 * time.Second,
WriteTimeout: 4 * time.Second,
}
return server.ListenAndServe()
}

38
internal/handler/get.go Normal file
View File

@ -0,0 +1,38 @@
package handler
import (
"fmt"
"net/http"
"net/url"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"gosimplenpm/internal/config"
"gosimplenpm/internal/storage"
)
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)
lg.WithFields(logrus.Fields{
"function": "get-package",
}).Debugf("Package name => %s\n", packageName)
fileToServe, found, err := stg.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
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
}
// serve file
http.ServeFile(w, r, fileToServe)
}
}

View File

@ -0,0 +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)
}

View File

@ -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}}}

View File

@ -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"
}
}

View File

@ -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}}}

View File

@ -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"
}
}

View File

@ -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}}}

View File

@ -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"
}
}

View File

@ -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}}}

View File

@ -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"
}
}

View File

@ -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}}}

View File

@ -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"
}
}

View File

@ -0,0 +1,10 @@
package handler
import (
"net/http"
)
// This handler is executed when the router cannot match any route
func NotFound(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Invalid url", http.StatusBadRequest)
}

177
internal/handler/publish.go Normal file
View File

@ -0,0 +1,177 @@
package handler
import (
"encoding/json"
"fmt"
"gosimplenpm/internal/config"
"gosimplenpm/internal/serviceidos"
"gosimplenpm/internal/storage"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
type NPMClientPutRequest struct {
Request serviceidos.IndexJson
}
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.
// (a) if it does, ckeck if it is the same version. If it is, return error. Else modify index.json from (2)
// (b) If it does not, add the latest tag to the new index.json
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugf("Package name => %s\n", packageName)
var cr NPMClientPutRequest
// Parse json body
err := json.NewDecoder(r.Body).Decode(&cr.Request)
if err != nil {
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugf("Error unmarshaling put request: %+v\n", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Extract relevant data from index.json
fmt.Printf("cRequest => %+v\n", cr)
index := 0
var tag string
var version string
var versionData serviceidos.IndexJsonVersions
// TODO: Fix this as the order is not guaranteed
for key, value := range cr.Request.DistTags {
if index == 0 {
tag = key
version = value
break
}
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 := stg.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var jsonFile serviceidos.IndexJson
if !found {
// 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 = stg.ReadIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Checking that you are not publishing over a pervious published version
if jsonFile.Versions[version].Version == version {
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugf("Version %s of package %s already exists!!\n", version, packageName)
http.Error(w, fmt.Sprintf("Version %s of package %s already exists!!\n", version, packageName), http.StatusBadRequest)
return
}
// Rewrite attachments
jsonFile.DistTags[tag] = version
nAttachments := make(map[string]serviceidos.IndexJsonAttachments)
nAttachments[fmt.Sprintf("%s-%s.tgz", packageName, version)] = cr.Request.Attachments[fmt.Sprintf("%s-%s.tgz", packageName, version)]
jsonFile.Attachments = nAttachments
// 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{
"function": "publish",
}).Debugln("FiletoServe ==> ", fileToServe)
// Write index.json
err = stg.WriteIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
lg.WithFields(logrus.Fields{
"function": "publish",
}).Debugln("Package path => ", packageFilePath)
// Write bundled package
packageData := jsonFile.Attachments[fmt.Sprintf("%s-%s.tgz", packageName, version)].Data
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)
}
}

View File

@ -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))
}

View File

@ -0,0 +1,84 @@
package handler
import (
"encoding/json"
"fmt"
"gosimplenpm/internal/config"
"gosimplenpm/internal/serviceidos"
"gosimplenpm/internal/storage"
"net/http"
"net/url"
"strconv"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"golang.org/x/mod/semver"
)
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)
lg.WithFields(logrus.Fields{
"function": "dist-tags-delete",
}).Debugf("Package name => %s\n", packageName)
escapedName = mux.Vars(r)["tag"]
tag, _ := url.PathUnescape(escapedName)
lg.WithFields(logrus.Fields{
"function": "dist-tags-delete",
}).Debugf("Tag => %s\n", tag)
if semver.IsValid(tag) {
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)
return
}
fileToServe, found, err := stg.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
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 = stg.ReadIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
delete(jsonFile.DistTags, tag)
// Write index.json
err = stg.WriteIndexJson(fileToServe, &jsonFile, lg)
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)
}
}

View File

@ -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")
}

View File

@ -0,0 +1,50 @@
package handler
import (
"encoding/json"
"fmt"
"gosimplenpm/internal/config"
"gosimplenpm/internal/serviceidos"
"gosimplenpm/internal/storage"
"net/http"
"net/url"
"strconv"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
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)
lg.WithFields(logrus.Fields{
"function": "dist-tags-get",
}).Debugf("Package name => %s\n", packageName)
fileToServe, found, err := stg.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
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 = stg.ReadIndexJson(fileToServe, &jsonFile, lg)
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)
}
}

View File

@ -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"])
}

View File

@ -0,0 +1,92 @@
package handler
import (
"encoding/json"
"fmt"
"gosimplenpm/internal/config"
"gosimplenpm/internal/serviceidos"
"gosimplenpm/internal/storage"
"io"
"net/http"
"net/url"
"strconv"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"golang.org/x/mod/semver"
)
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)
lg.WithFields(logrus.Fields{
"function": "dist-tags-put",
}).Debugf("Package name => %s\n", packageName)
escapedName = mux.Vars(r)["tag"]
tag, _ := url.PathUnescape(escapedName)
lg.WithFields(logrus.Fields{
"function": "dist-tags-put",
}).Debugf("Tag => %s\n", tag)
if semver.IsValid(tag) {
http.Error(w, fmt.Sprintf("Tag %s cannot be a semver version", tag), http.StatusBadRequest)
return
}
if tag == "latest" {
http.Error(w, "Cannot modify the latest tag", http.StatusBadRequest)
return
}
body, _ := io.ReadAll(r.Body)
var version string
_ = json.Unmarshal(body, &version)
lg.WithFields(logrus.Fields{
"function": "dist-tags-put",
}).Debugf("Body => %s", version)
fileToServe, found, err := stg.GetIndexJsonFromStore(packageName, cfg.RepoDir, lg)
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 = stg.ReadIndexJson(fileToServe, &jsonFile, lg)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
jsonFile.DistTags[tag] = version
// Write index.json
err = stg.WriteIndexJson(fileToServe, &jsonFile, lg)
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)
}
}

View File

@ -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")
}

58
internal/handler/tar.go Normal file
View File

@ -0,0 +1,58 @@
package handler
import (
"bytes"
"fmt"
"gosimplenpm/internal/config"
"gosimplenpm/internal/storage"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
)
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
// dist
// .tarball: https://registry.npmjs.org/react/-/react-18.2.0.tgz
// LocalHost
// dist
// .tarball: http://localhost:4000/@ookusanya/package1/-/package1-0.2.0.tgz
escapedName := mux.Vars(r)["name"]
packageName, _ := url.PathUnescape(escapedName)
lg.WithFields(logrus.Fields{
"function": "get-tar",
}).Debugf("Package name => %s\n", packageName)
escapedName = mux.Vars(r)["tar"]
tarFileNameWithScope, _ := url.PathUnescape(escapedName)
lg.WithFields(logrus.Fields{
"function": "get-tar",
}).Debugf("Tarfile name => %s\n", tarFileNameWithScope)
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
}
// Sending the tar as a base64 string
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Length", strconv.Itoa(len([]byte(fileAsString))))
io.Copy(w, bytes.NewReader([]byte(fileAsString)))
}
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -0,0 +1,28 @@
package middlewares
import (
"gosimplenpm/internal/config"
"net/http"
"strings"
)
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
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
}
next(w, r)
}
}
}

View File

@ -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")
})
}

View File

@ -0,0 +1,29 @@
package middlewares
import (
"net/http"
"net/http/httputil"
"github.com/sirupsen/logrus"
)
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
}
requestDump, err := httputil.DumpRequest(r, hasBody)
if err != nil {
lg.Debugln(err)
}
lg.Debugln("RequestDump: ", string(requestDump))
next.ServeHTTP(w, r)
})
}
}

View File

@ -0,0 +1,118 @@
package serviceidos
type IndexJsonAttachments struct {
ContentType string `json:"content_type"`
Data string `json:"data"`
Length int `json:"length"`
}
type IndexJsonDist struct {
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"`
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"`
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 {
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"`
}
type PublishPutResponse struct {
Ok bool `json:"ok"`
Name string `json:"package_name"`
}

180
internal/storage/fs.go Normal file
View File

@ -0,0 +1,180 @@
package storage
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/fs"
"os"
"path"
"strings"
"gosimplenpm/internal/serviceidos"
"path/filepath"
"github.com/sirupsen/logrus"
)
type FSStorage struct{}
func (f *FSStorage) GetIndexJsonFromStore(packageName string, registryPath string, log *logrus.Logger) (string, bool, error) {
fileToServe := ""
found := false
err := filepath.WalkDir(registryPath, func(fp string, info fs.DirEntry, e error) error {
if strings.Contains(fp, path.Join(packageName, "index.json")) {
fileToServe = fp
found = true
}
return e
})
if err != nil {
log.WithFields(logrus.Fields{
"function": "get-index-json-from-store",
}).Errorf("List files error: +%v\n", err)
return fileToServe, found, err
}
if fileToServe == "" && !found {
fileToServe = path.Join(registryPath, packageName, "index.json")
}
return fileToServe, found, nil
}
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 {
if strings.Contains(fp, path.Join(packageName, tarFileName)) {
fileToServe = fp
}
return e
})
if err != nil {
log.WithFields(logrus.Fields{
"function": "get-tar-from-store",
}).Errorf("List files error: +%v\n", err)
return fileToServe, err
}
if fileToServe == "" {
return fileToServe, fmt.Errorf("file %s is not found for package %s", tarFileName, packageName)
}
file, err := os.Open(fileToServe)
if err != nil {
log.WithFields(logrus.Fields{
"function": "get-tar-from-store",
}).Errorf("Open error: %s\n", fileToServe)
return "", err
}
defer file.Close()
bs, err := io.ReadAll(file)
if err != nil {
log.WithFields(logrus.Fields{
"function": "get-tar-from-store",
}).Errorf("File Read error: %s\n", fileToServe)
return "", err
}
return base64.StdEncoding.EncodeToString(bs), err
}
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",
}).Errorf("File Not found: %s\n", fPath)
return err
}
defer jsonFile.Close()
err = json.NewDecoder(jsonFile).Decode(res)
if err != nil {
log.WithFields(logrus.Fields{
"function": "read-index-json",
}).Errorf("Unmarshalerror: %+v\n", err)
return err
}
return nil
}
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",
}).Errorf("Folder (%s) creation failed.\n", fPath)
return err
}
// Create the file
jsonFile, err := os.Create(fPath)
if err != nil {
log.WithFields(logrus.Fields{
"function": "write-index-json",
}).Errorf("Creation error for path(%s): %+v\n ", fPath, err)
return err
}
defer jsonFile.Close()
err = json.NewEncoder(jsonFile).Encode(res)
if err != nil {
log.WithFields(logrus.Fields{
"function": "write-index-json",
}).Debugf("Marshalerror: %+v\n", err)
return err
}
return nil
}
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",
}).Errorf("Base64 Decode error: %+v\n", err)
return err
}
dataFile, err := os.Create(fPath)
if err != nil {
log.WithFields(logrus.Fields{
"function": "write-package-to-store",
}).Errorf("Creation error: %s\n", fPath)
return err
}
defer dataFile.Close()
_, err = dataFile.Write(dec)
if err != nil {
log.WithFields(logrus.Fields{
"function": "write-package-to-store",
}).Errorf("Write error: %s\n", fPath)
return err
}
err = dataFile.Sync()
if err != nil {
log.WithFields(logrus.Fields{
"function": "write-package-to-store",
}).Errorf("Sync error: %s\n", fPath)
return err
}
return nil
}

View File

@ -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)
}

View File

@ -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
}

34
main.go
View File

@ -1,13 +1,35 @@
package main
import (
"log"
"net/http"
"gosimplenpm/cmd/gosimplenpm"
)
func main() {
app := new(application)
log.Print("Starting server on port 4000")
err := http.ListenAndServe(":4000", app.Routes())
log.Fatal(err)
gosimplenpm.Execute()
}
// 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 := &gosimplenpm.Application{
// Conf: cfg,
// Logger: log,
// }
// log.Infoln("Starting server on port 4000")
// err = http.ListenAndServe(":4000", app.Routes())
// log.Fatal(err)
// }

View File

@ -1,33 +0,0 @@
package storage
import (
"fmt"
"io/fs"
"path"
"strings"
"path/filepath"
)
func GetPackageFromStore(packageName string) (string, error) {
fileToServe := ""
searchDir, err := filepath.Abs("./examples")
if err != nil {
fmt.Printf("File repo not found: +%v/n", err)
return "", err
}
err = filepath.WalkDir(searchDir, func(fp string, info fs.DirEntry, e error) error {
if strings.Contains(fp, path.Join(packageName, "index.json")) {
fileToServe = fp
}
return e
})
if err != nil {
fmt.Printf("List files error: +%v/n", err)
return "", err
}
return fileToServe, nil
}

View File

@ -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}}}