From f352c0b447a609163ed5869b39ebd4a6896b8fb5 Mon Sep 17 00:00:00 2001 From: g-harel Date: Thu, 6 Jun 2019 15:54:52 -0400 Subject: [PATCH] add sanity checks for base router --- handlers/compare.go | 134 +++++++++++++++--------------- handlers/directory.go | 116 +++++++++++++------------- handlers/file.go | 84 ++++++++++--------- handlers/versions.go | 38 +++++---- internal/diff/compare.go | 4 +- internal/mock/registry.go | 81 ++++++++++++++++++ internal/paths/break_test.go | 96 +++++++++++----------- internal/semver/semver_test.go | 17 ++-- main.go | 45 +++++----- main_test.go | 110 +++++++++++++++++++++++++ templates/pages/compare.html | 146 +++++++++++++++++---------------- templates/templates.go | 4 +- 12 files changed, 539 insertions(+), 336 deletions(-) create mode 100644 internal/mock/registry.go create mode 100644 main_test.go diff --git a/handlers/compare.go b/handlers/compare.go index 0e82aaf..b9202b5 100644 --- a/handlers/compare.go +++ b/handlers/compare.go @@ -17,84 +17,86 @@ import ( ) // Compare handler displays a diff between two package versions. -func Compare(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - name := vars["name"] - versionA := vars["a"] - versionB := vars["b"] +func Compare(ry registry.Registry) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + versionA := vars["a"] + versionB := vars["b"] - if versionA == versionB { - http.NotFound(w, r) - return - } + if versionA == versionB { + http.NotFound(w, r) + return + } - // Download both package version contents to a temporary directory in parallel. - type downloadedDir struct { - version string - dir string - err error - } - dirChan := make(chan downloadedDir) - for _, version := range []string{versionA, versionB} { - go func(v string) { - // Create temporary working directory. - dir, err := ioutil.TempDir("", "") - if err != nil { - dirChan <- downloadedDir{v, "", fmt.Errorf("create temp dir: %v", err)} - return - } + // Download both package version contents to a temporary directory in parallel. + type downloadedDir struct { + version string + dir string + err error + } + dirChan := make(chan downloadedDir) + for _, version := range []string{versionA, versionB} { + go func(v string) { + // Create temporary working directory. + dir, err := ioutil.TempDir("", "") + if err != nil { + dirChan <- downloadedDir{v, "", fmt.Errorf("create temp dir: %v", err)} + return + } + + // Fetch package contents for given version. + pkg, err := ry.PackageContents(name, v) + if err != nil { + // Error not wrapped so it can be checked against "registry.ErrNotFound". + dirChan <- downloadedDir{v, "", err} + return + } + defer pkg.Close() + + // Write package contents to directory. + err = tarball.Extract(pkg, tarball.Downloader(func(name string) string { + return path.Join(dir, strings.TrimPrefix(name, "package")) + })) + if err != nil { + dirChan <- downloadedDir{v, "", fmt.Errorf("download contents: %v", err)} + return + } + + dirChan <- downloadedDir{v, dir, nil} + }(version) + } - // Fetch package contents for given version. - pkg, err := registry.NPM.PackageContents(name, v) - if err != nil { - // Error not wrapped so it can be checked against "registry.ErrNotFound". - dirChan <- downloadedDir{v, "", err} + // Wait for both version's contents to be downloaded. + dirs := map[string]string{} + for i := 0; i < 2; i++ { + dir := <-dirChan + if dir.err == registry.ErrNotFound { + http.NotFound(w, r) return } - defer pkg.Close() - - // Write package contents to directory. - err = tarball.Extract(pkg, tarball.Downloader(func(name string) string { - return path.Join(dir, strings.TrimPrefix(name, "package")) - })) - if err != nil { - dirChan <- downloadedDir{v, "", fmt.Errorf("download contents: %v", err)} + if dir.err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + log.Printf("ERROR download package '%v': %v", dir.version, dir.err) return } - - dirChan <- downloadedDir{v, dir, nil} - }(version) - } - - // Wait for both version's contents to be downloaded. - dirs := map[string]string{} - for i := 0; i < 2; i++ { - dir := <-dirChan - if dir.err == registry.ErrNotFound { - http.NotFound(w, r) - return + dirs[dir.version] = dir.dir } - if dir.err != nil { + + // Compare contents. + patches, err := diff.Compare(dirs[versionA], dirs[versionB]) + if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - log.Printf("ERROR download package '%v': %v", dir.version, dir.err) + log.Printf("ERROR compare package contents: %v", err) return } - dirs[dir.version] = dir.dir - } - // Compare contents. - patches, err := diff.Compare(dirs[versionA], dirs[versionB]) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - log.Printf("ERROR compare package contents: %v", err) - return - } + // Cleanup created directories. + for _, path := range dirs { + _ = os.RemoveAll(path) + } - // Cleanup created directories. - for _, path := range dirs { - _ = os.RemoveAll(path) + // Render page template. + templates.PageCompare(name, versionA, versionB, patches).Handler(w, r) } - - // Render page template. - templates.PageCompare(name, versionA, versionB, patches).Render(w) } diff --git a/handlers/directory.go b/handlers/directory.go index f90e1cb..1abb402 100644 --- a/handlers/directory.go +++ b/handlers/directory.go @@ -15,69 +15,71 @@ import ( ) // Directory handler displays a directory view of package contents at the provided path. -func Directory(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - name := vars["name"] - version := vars["version"] - path := vars["path"] +func Directory(ry registry.Registry) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + version := vars["version"] + path := vars["path"] - // Fetch package contents. - pkg, err := registry.NPM.PackageContents(name, version) - if err == registry.ErrNotFound { - http.NotFound(w, r) - return - } - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - log.Printf("ERROR fetch package contents: %v", err) - return - } - defer pkg.Close() + // Fetch package contents. + pkg, err := ry.PackageContents(name, version) + if err == registry.ErrNotFound { + http.NotFound(w, r) + return + } + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + log.Printf("ERROR fetch package contents: %v", err) + return + } + defer pkg.Close() - // Extract files and directories at the given path. - dirs := []string{} - files := []string{} - err = tarball.Extract(pkg, func(name string, contents io.Reader) error { - filePath := strings.TrimPrefix(name, "package/") - if strings.HasPrefix(filePath, path) { - filePath := strings.TrimPrefix(filePath, path) - pathParts := strings.Split(filePath, "/") - if len(pathParts) == 1 { - files = append(files, pathParts[0]) - } else { - dirs = append(dirs, pathParts[0]) + // Extract files and directories at the given path. + dirs := []string{} + files := []string{} + err = tarball.Extract(pkg, func(name string, contents io.Reader) error { + filePath := strings.TrimPrefix(name, "package/") + if strings.HasPrefix(filePath, path) { + filePath := strings.TrimPrefix(filePath, path) + pathParts := strings.Split(filePath, "/") + if len(pathParts) == 1 { + files = append(files, pathParts[0]) + } else { + dirs = append(dirs, pathParts[0]) + } } + return nil + }) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + log.Printf("ERROR extract files from package contents: %v", err) + return } - return nil - }) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - log.Printf("ERROR extract files from package contents: %v", err) - return - } - if len(dirs) == 0 && len(files) == 0 { - http.NotFound(w, r) - return - } - - // Sort and de-duplicate input slice. - cleanup := func(s []string) []string { - m := map[string]interface{}{} - for _, item := range s { - m[item] = true + if len(dirs) == 0 && len(files) == 0 { + http.NotFound(w, r) + return } - out := []string{} - for key := range m { - out = append(out, key) + + // Sort and de-duplicate input slice. + cleanup := func(s []string) []string { + m := map[string]interface{}{} + for _, item := range s { + m[item] = true + } + out := []string{} + for key := range m { + out = append(out, key) + } + sort.Strings(out) + return out } - sort.Strings(out) - return out - } - dirs = cleanup(dirs) - files = cleanup(files) - parts, links := paths.BreakRelative(path) + dirs = cleanup(dirs) + files = cleanup(files) + parts, links := paths.BreakRelative(path) - // Render page template. - templates.PageDirectory(name, version, parts, links, dirs, files).Render(w) + // Render page template. + templates.PageDirectory(name, version, parts, links, dirs, files).Handler(w, r) + } } diff --git a/handlers/file.go b/handlers/file.go index 1e9e5e8..f31dc68 100644 --- a/handlers/file.go +++ b/handlers/file.go @@ -15,51 +15,53 @@ import ( ) // File handler displays a file view of package contents at the provided path. -func File(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - name := vars["name"] - version := vars["version"] - path := vars["path"] +func File(ry registry.Registry) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + version := vars["version"] + path := vars["path"] - // Fetch package contents. - pkg, err := registry.NPM.PackageContents(name, version) - if err == registry.ErrNotFound { - http.NotFound(w, r) - return - } - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - log.Printf("ERROR fetch package contents: %v", err) - return - } - defer pkg.Close() + // Fetch package contents. + pkg, err := ry.PackageContents(name, version) + if err == registry.ErrNotFound { + http.NotFound(w, r) + return + } + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + log.Printf("ERROR fetch package contents: %v", err) + return + } + defer pkg.Close() - // Find file contents to use in response. - // Contents must be written to a buffer to be used in a template. - var file *bytes.Buffer - err = tarball.Extract(pkg, func(name string, contents io.Reader) error { - if strings.TrimPrefix(name, "package/") == path { - file = new(bytes.Buffer) - _, err := file.ReadFrom(contents) - if err != nil { - log.Printf("ERROR copy contents: %v", err) + // Find file contents to use in response. + // Contents must be written to a buffer to be used in a template. + var file *bytes.Buffer + err = tarball.Extract(pkg, func(name string, contents io.Reader) error { + if strings.TrimPrefix(name, "package/") == path { + file = new(bytes.Buffer) + _, err := file.ReadFrom(contents) + if err != nil { + log.Printf("ERROR copy contents: %v", err) + } } + return nil + }) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + log.Printf("ERROR extract files from package contents: %v", err) + return + } + if file == nil { + http.NotFound(w, r) + return } - return nil - }) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - log.Printf("ERROR extract files from package contents: %v", err) - return - } - if file == nil { - http.NotFound(w, r) - return - } - parts, links := paths.BreakRelative(path) - lines := strings.Split(file.String(), "\n") + parts, links := paths.BreakRelative(path) + lines := strings.Split(file.String(), "\n") - // Render page template. - templates.PageFile(name, version, parts, links, lines).Render(w) + // Render page template. + templates.PageFile(name, version, parts, links, lines).Handler(w, r) + } } diff --git a/handlers/versions.go b/handlers/versions.go index f6faab0..aa3440b 100644 --- a/handlers/versions.go +++ b/handlers/versions.go @@ -12,24 +12,26 @@ import ( ) // Versions handler displays all available package versions. -func Versions(w http.ResponseWriter, r *http.Request) { - vars := mux.Vars(r) - name := vars["name"] - disabled := vars["disabled"] +func Versions(ry registry.Registry) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + disabled := vars["disabled"] - // Fetch and sort version list. - versions, latest, err := registry.NPM.PackageVersions(name) - if err == registry.ErrNotFound { - http.NotFound(w, r) - return - } - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - log.Printf("ERROR fetch package versions: %v", err) - return - } - sort.Sort(semver.Sort(versions)) + // Fetch and sort version list. + versions, latest, err := ry.PackageVersions(name) + if err == registry.ErrNotFound { + http.NotFound(w, r) + return + } + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + log.Printf("ERROR fetch package versions: %v", err) + return + } + sort.Sort(semver.Sort(versions)) - // Render page template. - templates.PageVersions(name, latest, disabled, versions).Render(w) + // Render page template. + templates.PageVersions(name, latest, disabled, versions).Handler(w, r) + } } diff --git a/internal/diff/compare.go b/internal/diff/compare.go index a89fdd3..ceafb57 100644 --- a/internal/diff/compare.go +++ b/internal/diff/compare.go @@ -64,7 +64,7 @@ func Compare(a, b string) ([]*Patch, error) { if err != nil { return nil, err } - _, err = execGit(dir, "commit", "-m", a) + _, err = execGit(dir, "commit", "--allow-empty", "-m", a) if err != nil { return nil, err } @@ -86,7 +86,7 @@ func Compare(a, b string) ([]*Patch, error) { if err != nil { return nil, err } - _, err = execGit(dir, "commit", "-m", b) + _, err = execGit(dir, "commit", "--allow-empty", "-m", b) if err != nil { return nil, err } diff --git a/internal/mock/registry.go b/internal/mock/registry.go new file mode 100644 index 0000000..b96af68 --- /dev/null +++ b/internal/mock/registry.go @@ -0,0 +1,81 @@ +package mock + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + + "github.com/g-harel/npmfs/internal/registry" +) + +// Registry is a mock implementation of the registry.Registry interface. +type Registry struct { + Contents map[string]map[string]string + Latest string + PackageVersionsErr error + PackageContentsErr error +} + +var _ registry.Registry = &Registry{} + +// PackageVersions returns all versions listed in the contents and the specified latest value. +// Package name is ignored. +func (ry *Registry) PackageVersions(name string) ([]string, string, error) { + if ry.PackageVersionsErr != nil { + return nil, "", ry.PackageVersionsErr + } + + versions := []string{} + for v := range ry.Contents { + versions = append(versions, v) + } + return versions, ry.Latest, ry.PackageVersionsErr +} + +// PackageContents returns a gzipped tarball of the contents of the specified version. +// Package name is ignored. +func (ry *Registry) PackageContents(name, version string) (io.ReadCloser, error) { + if ry.PackageContentsErr != nil { + return nil, ry.PackageContentsErr + } + + versionContents, ok := ry.Contents[version] + if !ok { + return nil, registry.ErrNotFound + } + + // Create gzip/tar writer chain. + buf := &bytes.Buffer{} + gw := gzip.NewWriter(buf) + tw := tar.NewWriter(gw) + + // Write version's contents. + for name, content := range versionContents { + hdr := &tar.Header{ + Name: name, + Mode: 0600, + Size: int64(len(content)), + } + err := tw.WriteHeader(hdr) + if err != nil { + return nil, fmt.Errorf("write file header: %v", err) + } + _, err = tw.Write([]byte(content)) + if err != nil { + return nil, fmt.Errorf("write file content: %v", err) + } + } + err := tw.Close() + if err != nil { + return nil, fmt.Errorf("close tarball writer: %v", err) + } + err = gw.Close() + if err != nil { + return nil, fmt.Errorf("close gzip writer: %v", err) + } + + return ioutil.NopCloser(buf), ry.PackageContentsErr +} diff --git a/internal/paths/break_test.go b/internal/paths/break_test.go index b373695..1c9dde7 100644 --- a/internal/paths/break_test.go +++ b/internal/paths/break_test.go @@ -6,80 +6,78 @@ import ( "github.com/g-harel/npmfs/internal/paths" ) +func sliceEqual(t *testing.T, expected, received []string) { + if len(expected) != len(received) { + t.Fatalf("expected/received do not match\n%v\n%v", expected, received) + } + for i := range expected { + if expected[i] != received[i] { + t.Fatalf("expected/received do not match\n%v\n%v", expected, received) + } + } +} + func TestBreakRelative(t *testing.T) { - cases := map[string]struct { + tt := map[string]struct { + Path string Parts []string Links []string }{ - "": { - Parts: []string{}, - Links: []string{}, - }, - "/": { + "root": { + Path: "", Parts: []string{}, Links: []string{}, }, - "img.jpg": { + "root file": { + Path: "img.jpg", Parts: []string{"img.jpg"}, Links: []string{""}, }, - "/img.jpg": { - Parts: []string{"img.jpg"}, - Links: []string{""}, - }, - "test/": { + "single dir": { + Path: "test/", Parts: []string{"test"}, Links: []string{""}, }, - "/test/": { - Parts: []string{"test"}, - Links: []string{""}, - }, - "test/img.jpg": { - Parts: []string{"test", "img.jpg"}, - Links: []string{"./", ""}, - }, - "/test/img.jpg": { + "nested file": { + Path: "test/img.jpg", Parts: []string{"test", "img.jpg"}, Links: []string{"./", ""}, }, - "test/path/": { - Parts: []string{"test", "path"}, - Links: []string{"../", ""}, - }, - "/test/path/": { + "nested dir": { + Path: "test/path/", Parts: []string{"test", "path"}, Links: []string{"../", ""}, }, - "test/path/img.jpg": { - Parts: []string{"test", "path", "img.jpg"}, - Links: []string{"../", "./", ""}, - }, - "/test/path/img.jpg": { + "deeply nested file": { + Path: "test/path/img.jpg", Parts: []string{"test", "path", "img.jpg"}, Links: []string{"../", "./", ""}, }, } - for path, result := range cases { - parts, links := paths.BreakRelative(path) + for name, tc := range tt { + t.Run(name, func(t *testing.T) { + parts, links := paths.BreakRelative(tc.Path) - if len(parts) != len(result.Parts) { - t.Fatalf("expected/received parts do not match \n%v\n%v\n%v", path, parts, result.Parts) - } - for i := range parts { - if parts[i] != result.Parts[i] { - t.Fatalf("expected/received parts do not match \n%v\n%v\n%v", path, parts, result.Parts) - } - } + t.Run("parts", func(t *testing.T) { + sliceEqual(t, tc.Parts, parts) + }) - if len(links) != len(result.Links) { - t.Fatalf("expected/received links do not match \n%v\n%v\n%v", path, links, result.Links) - } - for i := range links { - if links[i] != result.Links[i] { - t.Fatalf("expected/received links do not match \n%v\n%v\n%v", path, links, result.Links) - } - } + t.Run("links", func(t *testing.T) { + sliceEqual(t, tc.Links, links) + }) + + t.Run("leading slash ignored", func(t *testing.T) { + parts, links := paths.BreakRelative("/" + tc.Path) + + t.Run("parts", func(t *testing.T) { + sliceEqual(t, tc.Parts, parts) + }) + + t.Run("links", func(t *testing.T) { + sliceEqual(t, tc.Links, links) + }) + }) + }) } } diff --git a/internal/semver/semver_test.go b/internal/semver/semver_test.go index 39e92f4..e412dba 100644 --- a/internal/semver/semver_test.go +++ b/internal/semver/semver_test.go @@ -2,29 +2,28 @@ package semver_test import ( "sort" - "strings" "testing" "github.com/g-harel/npmfs/internal/semver" ) func TestSort(t *testing.T) { - cases := [][]string{ + cases := map[string][]string{ // https://semver.org/#semantic-versioning-specification-semver - {"1.11.0", "1.10.0", "1.9.0"}, - {"2.1.1", "2.1.0", "2.0.0", "1.0.0"}, - {"1.0.0", "1.0.0-alpha"}, - {"1.0.0", "1.0.0-rc.1", "1.0.0-beta.11", "1.0.0-beta.2", "1.0.0-beta", "1.0.0-alpha.beta", "1.0.0-alpha.1", "1.0.0-alpha"}, + "single element": {"1.11.0", "1.10.0", "1.9.0"}, + "element priority": {"2.1.1", "2.1.0", "2.0.0", "1.0.0"}, + "pre-release": {"1.0.0", "1.0.0-alpha"}, + "pre-release identifiers": {"1.0.0", "1.0.0-rc.1", "1.0.0-beta.11", "1.0.0-beta.2", "1.0.0-beta", "1.0.0-alpha.beta", "1.0.0-alpha.1", "1.0.0-alpha"}, } - for _, input := range cases { - t.Run(strings.Join(input, ", "), func(t *testing.T) { + for name, input := range cases { + t.Run(name, func(t *testing.T) { output := append([]string{}, input...) sort.Sort(semver.Sort(output)) for i := range input { if input[i] != output[i] { - t.Fatalf("expected/received do not match \n%v\n%v", input, output) + t.Fatalf("expected/received do not match\n%v\n%v", input, output) } } }) diff --git a/main.go b/main.go index aedfde4..d8ab391 100644 --- a/main.go +++ b/main.go @@ -8,20 +8,11 @@ import ( "github.com/NYTimes/gziphandler" "github.com/g-harel/npmfs/handlers" + "github.com/g-harel/npmfs/internal/registry" "github.com/g-harel/npmfs/templates" "github.com/gorilla/mux" ) -// Name pattern matches with simple and org-scoped names. -// (ex. "lodash", "react", "@types/express") -const namePattern = "{name:(?:@[^/]+\\/)?[^/]+}" - -// Directory path pattern matches everything that ends with a path separator. -const dirPattern = "{path:(?:.+/)?$}" - -// File path pattern matches everything that does not end in a path separator. -const filePattern = "{path:.*[^/]$}" - // Redirect responds with a temporary redirect after adding the pre and postfix. func redirect(pre, post string) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -29,33 +20,42 @@ func redirect(pre, post string) http.HandlerFunc { } } -func main() { +// Routes returns an http handler with all the routes/handlers attached. +func routes(ry registry.Registry) http.Handler { r := mux.NewRouter() // Add gzip middleware to all handlers. r.Use(gziphandler.GzipHandler) // Show homepage. - r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - templates.PageHome().Render(w) - }) + r.HandleFunc("/", templates.PageHome().Handler) + + var ( + // Name pattern matches with simple and org-scoped names. + // (ex. "lodash", "react", "@types/express") + namePattern = "{name:(?:@[^/]+\\/)?[^/]+}" + // Directory path pattern matches everything that ends with a path separator. + dirPattern = "{path:(?:.+/)?$}" + // File path pattern matches everything that does not end in a path separator. + filePattern = "{path:.*[^/]$}" + ) // Show package versions. r.HandleFunc("/package/"+namePattern+"", redirect("", "/")) - r.HandleFunc("/package/"+namePattern+"/", handlers.Versions) + r.HandleFunc("/package/"+namePattern+"/", handlers.Versions(ry)) // Show package contents. r.HandleFunc("/package/"+namePattern+"/{version}", redirect("", "/")) - r.PathPrefix("/package/" + namePattern + "/{version}/" + dirPattern).HandlerFunc(handlers.Directory) - r.PathPrefix("/package/" + namePattern + "/{version}/" + filePattern).HandlerFunc(handlers.File) + r.PathPrefix("/package/" + namePattern + "/{version}/" + dirPattern).HandlerFunc(handlers.Directory(ry)) + r.PathPrefix("/package/" + namePattern + "/{version}/" + filePattern).HandlerFunc(handlers.File(ry)) // Pick second version to compare to. r.HandleFunc("/compare/"+namePattern+"/{disabled}", redirect("", "/")) - r.HandleFunc("/compare/"+namePattern+"/{disabled}/", handlers.Versions) + r.HandleFunc("/compare/"+namePattern+"/{disabled}/", handlers.Versions(ry)) // Compare package versions. r.HandleFunc("/compare/"+namePattern+"/{a}/{b}", redirect("", "/")) - r.HandleFunc("/compare/"+namePattern+"/{a}/{b}/", handlers.Compare) + r.HandleFunc("/compare/"+namePattern+"/{a}/{b}/", handlers.Compare(ry)) // Static assets. assets := http.FileServer(http.Dir("assets")) @@ -68,12 +68,17 @@ func main() { r.HandleFunc("/"+namePattern, redirect("/package", "/")) r.HandleFunc("/"+namePattern+"/", redirect("/package", "")) + return r +} + +func main() { // Take port number from environment if provided. + // https://cloud.google.com/run/docs/reference/container-contract port := os.Getenv("PORT") if port == "" { port = "8080" } log.Printf("accepting connections at :%v", port) - log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), r)) + log.Fatal(http.ListenAndServe(fmt.Sprintf(":%v", port), routes(registry.NPM))) } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..b16f5e9 --- /dev/null +++ b/main_test.go @@ -0,0 +1,110 @@ +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/g-harel/npmfs/internal/mock" +) + +func TestRoutes(t *testing.T) { + registry := &mock.Registry{ + Latest: "1.1.1", + Contents: map[string]map[string]string{ + "0.0.0": { + "README.md": "", + }, + "1.1.1": { + "README.md": "", + }, + }, + } + + tt := map[string]struct { + Path string + Status int + }{ + "home": { + Path: "/", + Status: http.StatusOK, + }, + "static favicon": { + Path: "/favicon.ico", + Status: http.StatusOK, + }, + "static robots.txt": { + Path: "/robots.txt", + Status: http.StatusOK, + }, + "static icon": { + Path: "/assets/icon-package.svg", + Status: http.StatusOK, + }, + "package versions shortcut": { + Path: "/test", + Status: http.StatusOK, + }, + "namespaced package versions shortcut": { + Path: "/@test/test", + Status: http.StatusOK, + }, + "package versions": { + Path: "/package/test", + Status: http.StatusOK, + }, + "namespaced package versions": { + Path: "/package/@test/test", + Status: http.StatusOK, + }, + "package contents": { + Path: "/package/test/0.0.0", + Status: http.StatusOK, + }, + "namespaced package contents": { + Path: "/package/@test/test/0.0.0", + Status: http.StatusOK, + }, + "package file": { + Path: "/package/test/0.0.0/README.md", + Status: http.StatusOK, + }, + "namespaced package file": { + Path: "/package/@test/test/0.0.0/README.md", + Status: http.StatusOK, + }, + "package compare picker": { + Path: "/compare/test/0.0.0", + Status: http.StatusOK, + }, + "namespaced package compare picker": { + Path: "/compare/@test/test/0.0.0", + Status: http.StatusOK, + }, + "package compare": { + Path: "/compare/test/0.0.0/1.1.1", + Status: http.StatusOK, + }, + "namespaced package compare": { + Path: "/compare/@test/test/0.0.0/1.1.1", + Status: http.StatusOK, + }, + } + + srv := httptest.NewServer(routes(registry)) + defer srv.Close() + + for name, tc := range tt { + t.Run(name, func(t *testing.T) { + res, err := http.Get(fmt.Sprintf("%v%v", srv.URL, tc.Path)) + if err != nil { + t.Fatalf("send GET request: %v", err) + } + + if res.StatusCode != tc.Status { + t.Fatalf("expected/received do not match\n%v\n%v", tc.Status, res.StatusCode) + } + }) + } +} diff --git a/templates/pages/compare.html b/templates/pages/compare.html index f6cb492..2d967ee 100644 --- a/templates/pages/compare.html +++ b/templates/pages/compare.html @@ -80,80 +80,82 @@ {{- $versionA := .VersionA -}} {{- $versionB := .VersionB -}} {{- range $i, $patch := .Patches -}} - -
-
- {{- $packageA := print "/package/" $package "/" $versionA "/" -}} - {{- $packageB := print "/package/" $package "/" $versionB "/" -}} - - {{- $package -}} - @ - {{- $versionA -}} - .. - {{- $versionB -}} - - - - {{ $linkA := print $packageA $patch.PathA -}} - {{- $linkB := print $packageB $patch.PathB -}} - - {{- if and (eq $patch.PathA "") (ne $patch.PathB "") -}} - create - {{- $patch.PathB -}} - {{- else if and (ne $patch.PathA "") (eq $patch.PathB "") -}} - delete - {{- $patch.PathA -}} - {{- else if eq $patch.PathA $patch.PathB -}} - {{- $patch.PathB -}} - {{- else -}} - rename - {{- $patch.PathB -}} - 🡒 - {{- $patch.PathA -}} - {{- end -}} -
-
- {{- range $j, $line := $patch.Lines -}} - - - {{- if and (eq $line.LineA 0) (eq $line.LineB 0) -}} -
-
-
-
{{- $line.Content -}}
-
- -
- {{- else if and (ne $line.LineA 0) (ne $line.LineB 0) -}} - -
- {{- $line.LineA -}} - {{- $line.LineB -}} -
{{- $line.Content -}}
-
- -
- {{- else if and (ne $line.LineA 0) (eq $line.LineB 0) -}} - -
- {{- $line.LineA -}} -
-
-
{{- $line.Content -}}
-
- -
- {{- else if and (eq $line.LineA 0) (ne $line.LineB 0) -}} - -
-
+
- {{- $line.LineB -}} -
{{- $line.Content -}}
-
- -
+ {{- if gt (len $patch.Lines) 0 -}} + +
+
+ {{- $packageA := print "/package/" $package "/" $versionA "/" -}} + {{- $packageB := print "/package/" $package "/" $versionB "/" -}} + + {{- $package -}} + @ + {{- $versionA -}} + .. + {{- $versionB -}} + - + + {{ $linkA := print $packageA $patch.PathA -}} + {{- $linkB := print $packageB $patch.PathB -}} + + {{- if and (eq $patch.PathA "") (ne $patch.PathB "") -}} + create + {{- $patch.PathB -}} + {{- else if and (ne $patch.PathA "") (eq $patch.PathB "") -}} + delete + {{- $patch.PathA -}} + {{- else if eq $patch.PathA $patch.PathB -}} + {{- $patch.PathB -}} + {{- else -}} + rename + {{- $patch.PathB -}} + 🡒 + {{- $patch.PathA -}} {{- end -}} +
+
+ {{- range $j, $line := $patch.Lines -}} + + + {{- if and (eq $line.LineA 0) (eq $line.LineB 0) -}} +
+
+
+
{{- $line.Content -}}
+
+ +
+ {{- else if and (ne $line.LineA 0) (ne $line.LineB 0) -}} + +
+ {{- $line.LineA -}} + {{- $line.LineB -}} +
{{- $line.Content -}}
+
+ +
+ {{- else if and (ne $line.LineA 0) (eq $line.LineB 0) -}} + +
+ {{- $line.LineA -}} +
-
+
{{- $line.Content -}}
+
+ +
+ {{- else if and (eq $line.LineA 0) (ne $line.LineB 0) -}} + +
+
+
+ {{- $line.LineB -}} +
{{- $line.Content -}}
+
+ +
+ {{- end -}} - {{- end -}} + {{- end -}} +
-
+ {{- end -}} {{- end -}} {{- end -}} diff --git a/templates/templates.go b/templates/templates.go index 84892ac..936d6d7 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -12,8 +12,8 @@ type Renderer struct { context interface{} } -// Render is an application-aware helper to execute templates. -func (r *Renderer) Render(w http.ResponseWriter) { +// Handler executes the templates and handles errors. +func (r *Renderer) Handler(w http.ResponseWriter, _ *http.Request) { tmpl, err := template.ParseFiles(r.filenames...) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError)