diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..0775980 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,18 @@ +name: Test and Build +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + - name: Install dependencies + run: go get . + - name: Build + run: make build + - name: Test + run: make test diff --git a/Makefile b/Makefile index 20a1806..982ee8e 100644 --- a/Makefile +++ b/Makefile @@ -8,5 +8,7 @@ build:gentempl esbuild templ generate cat ./style/*.css | ./esbuild --loader=css --minify > ./static/style.css go build -ldflags="-X 'github.com/rramiachraf/dumb/data.Version=$(VERSION)' -s -w" +test: + go test ./... -v fmt: templ fmt . diff --git a/handlers/album.go b/handlers/album.go index e9aa8bc..684ec33 100644 --- a/handlers/album.go +++ b/handlers/album.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" ) -func Album(l *logrus.Logger) http.HandlerFunc { +func album(l *logrus.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { artist := mux.Vars(r)["artist"] albumName := mux.Vars(r)["albumName"] diff --git a/handlers/album_test.go b/handlers/album_test.go new file mode 100644 index 0000000..55a84fd --- /dev/null +++ b/handlers/album_test.go @@ -0,0 +1,39 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/PuerkitoBio/goquery" + "github.com/sirupsen/logrus" +) + +func TestAlbum(t *testing.T) { + url := "/albums/Daft-punk/Random-access-memories" + title := "Give Life Back to Music" + + r, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + l := logrus.New() + m := New(l) + + m.ServeHTTP(rr, r) + + defer rr.Result().Body.Close() + + doc, err := goquery.NewDocumentFromReader(rr.Result().Body) + if err != nil { + t.Fatal(err) + } + + docTitle := doc.Find("#album-tracklist > a > p").First().Text() + + if docTitle != title { + t.Fatalf("expected %q, got %q\n", title, docTitle) + } +} diff --git a/handlers/annotations.go b/handlers/annotations.go index 252f039..8033506 100644 --- a/handlers/annotations.go +++ b/handlers/annotations.go @@ -16,7 +16,7 @@ import ( "github.com/sirupsen/logrus" ) -func Annotations(l *logrus.Logger) http.HandlerFunc { +func annotations(l *logrus.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] if a, err := getCache[data.Annotation]("annotation:" + id); err == nil { diff --git a/handlers/annotations_test.go b/handlers/annotations_test.go new file mode 100644 index 0000000..c7a4c19 --- /dev/null +++ b/handlers/annotations_test.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/sirupsen/logrus" +) + +func TestAnnotations(t *testing.T) { + url := "/61590/Black-star-respiration/The-new-moon-rode-high-in-the-crown-of-the-metropolis/annotations" + + r, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + l := logrus.New() + m := New(l) + + m.ServeHTTP(rr, r) + + defer rr.Result().Body.Close() + + decoder := json.NewDecoder(rr.Result().Body) + annotation := map[string]string{} + + if err := decoder.Decode(&annotation); err != nil { + t.Fatal(err) + } + + if _, exists := annotation["html"]; !exists { + t.Fatalf("html field not found on annotation\n") + } +} diff --git a/handlers/cache_test.go b/handlers/cache_test.go new file mode 100644 index 0000000..3a7b1b5 --- /dev/null +++ b/handlers/cache_test.go @@ -0,0 +1,25 @@ +package handlers + +import ( + "bytes" + "testing" +) + +func TestCache(t *testing.T) { + key := "testkey" + value := []byte("testvalue") + + err := setCache(key, value) + if err != nil { + t.Fatalf("unable to set cache, %q\n", err) + } + + v, err := getCache[[]byte](key) + if err != nil { + t.Fatalf("unable to get cache, %q\n", err) + } + + if !bytes.Equal(v, value) { + t.Fatalf("expected %q, got %q\n", value, v) + } +} diff --git a/handlers/handler.go b/handlers/handler.go new file mode 100644 index 0000000..c81d929 --- /dev/null +++ b/handlers/handler.go @@ -0,0 +1,32 @@ +package handlers + +import ( + "context" + "net/http" + + "github.com/a-h/templ" + "github.com/gorilla/mux" + "github.com/rramiachraf/dumb/views" + "github.com/sirupsen/logrus" +) + +func New(logger *logrus.Logger) *mux.Router { + r := mux.NewRouter() + + r.Use(mustHeaders) + + r.Handle("/", templ.Handler(views.HomePage())) + r.HandleFunc("/{id}-lyrics", lyrics(logger)).Methods("GET") + r.HandleFunc("/albums/{artist}/{albumName}", album(logger)).Methods("GET") + r.HandleFunc("/images/{filename}.{ext}", imageProxy(logger)).Methods("GET") + r.HandleFunc("/search", search(logger)).Methods("GET") + r.HandleFunc("/{id}/{artist-song}/{verse}/annotations", annotations(logger)).Methods("GET") + r.HandleFunc("/instances.json", instances(logger)).Methods("GET") + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) + r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + views.ErrorPage(404, "page not found").Render(context.Background(), w) + }) + + return r +} diff --git a/handlers/instances.go b/handlers/instances.go index 8df70ec..0eac004 100644 --- a/handlers/instances.go +++ b/handlers/instances.go @@ -20,7 +20,7 @@ func sendError(err error, status int, msg string, l *logrus.Logger, w http.Respo } } -func Instances(l *logrus.Logger) http.HandlerFunc { +func instances(l *logrus.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if instances, err := getCache[[]byte]("instances"); err == nil { w.Header().Set("content-type", ContentTypeJSON) diff --git a/handlers/instances_test.go b/handlers/instances_test.go new file mode 100644 index 0000000..2b551db --- /dev/null +++ b/handlers/instances_test.go @@ -0,0 +1,40 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/sirupsen/logrus" +) + +func TestInstancesList(t *testing.T) { + r, err := http.NewRequest(http.MethodGet, "/instances.json", nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + l := logrus.New() + + m := New(l) + m.ServeHTTP(rr, r) + + c := rr.Result().Header.Get("content-type") + if c != ContentTypeJSON { + t.Fatalf("expected %q, got %q", ContentTypeJSON, c) + } + + defer rr.Result().Body.Close() + + d := json.NewDecoder(rr.Result().Body) + instances := []map[string]any{} + if err := d.Decode(&instances); err != nil { + t.Fatalf("unable to decode json from response, %q\n", err) + } + + if _, exists := instances[0]["clearnet"]; !exists { + t.Fatal("unable to get clearnet value from instances list") + } +} diff --git a/handlers/lyrics.go b/handlers/lyrics.go index d0e1c34..966e1fb 100644 --- a/handlers/lyrics.go +++ b/handlers/lyrics.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" ) -func Lyrics(l *logrus.Logger) http.HandlerFunc { +func lyrics(l *logrus.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] diff --git a/handlers/lyrics_test.go b/handlers/lyrics_test.go new file mode 100644 index 0000000..7c2445a --- /dev/null +++ b/handlers/lyrics_test.go @@ -0,0 +1,45 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/PuerkitoBio/goquery" + "github.com/sirupsen/logrus" +) + +func TestLyrics(t *testing.T) { + url := "/The-silver-seas-catch-yer-own-train-lyrics" + title := "The Silver Seas" + artist := "Catch Yer Own Train" + + r, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + l := logrus.New() + m := New(l) + + m.ServeHTTP(rr, r) + + defer rr.Result().Body.Close() + + doc, err := goquery.NewDocumentFromReader(rr.Result().Body) + if err != nil { + t.Fatal(err) + } + + docTitle := doc.Find("#metadata-info > h2").Text() + docArtist := doc.Find("#metadata-info > h1").Text() + + if docTitle != title { + t.Fatalf("expected %q, got %q\n", title, docTitle) + } + + if docArtist != artist { + t.Fatalf("expected %q, got %q\n", artist, docArtist) + } +} diff --git a/handlers/proxy.go b/handlers/proxy.go index 420b79c..2f8d114 100644 --- a/handlers/proxy.go +++ b/handlers/proxy.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "mime" "net/http" "strings" @@ -23,7 +24,7 @@ func isValidExt(ext string) bool { return false } -func ImageProxy(l *logrus.Logger) http.HandlerFunc { +func imageProxy(l *logrus.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { v := mux.Vars(r) f := v["filename"] @@ -52,7 +53,7 @@ func ImageProxy(l *logrus.Logger) http.HandlerFunc { return } - w.Header().Add("Content-type", fmt.Sprintf("image/%s", ext)) + w.Header().Add("Content-type", mime.TypeByExtension("."+ext)) w.Header().Add("Cache-Control", "max-age=1296000") if _, err = io.Copy(w, res.Body); err != nil { l.Errorln("unable to write image", err) diff --git a/handlers/proxy_test.go b/handlers/proxy_test.go new file mode 100644 index 0000000..5dd976c --- /dev/null +++ b/handlers/proxy_test.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "mime" + "net/http" + "net/http/httptest" + "testing" + + "github.com/sirupsen/logrus" +) + +func TestImageProxy(t *testing.T) { + imgURL := "/images/3852401ae6c6d6a51aafe814d67199f0.1000x1000x1.jpg" + + r, err := http.NewRequest(http.MethodGet, imgURL, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + l := logrus.New() + m := New(l) + + m.ServeHTTP(rr, r) + + cc := rr.Result().Header.Get("cache-control") + maxAge := "max-age=1296000" + ct := rr.Result().Header.Get("content-type") + mimeType := mime.TypeByExtension(".jpg") + + if cc != maxAge { + t.Fatalf("expected %q, got %q\n", maxAge, cc) + } + + if ct != mimeType { + t.Fatalf("expected %q, got %q\n", mimeType, ct) + } +} diff --git a/handlers/search.go b/handlers/search.go index b2a83cd..5bc7d37 100644 --- a/handlers/search.go +++ b/handlers/search.go @@ -12,7 +12,7 @@ import ( "github.com/sirupsen/logrus" ) -func Search(l *logrus.Logger) http.HandlerFunc { +func search(l *logrus.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") url := fmt.Sprintf(`https://genius.com/api/search/multi?q=%s`, url.QueryEscape(query)) diff --git a/handlers/search_test.go b/handlers/search_test.go new file mode 100644 index 0000000..d123181 --- /dev/null +++ b/handlers/search_test.go @@ -0,0 +1,38 @@ +package handlers + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/PuerkitoBio/goquery" + "github.com/sirupsen/logrus" +) + +func TestSearch(t *testing.T) { + url := "/search?q=it+aint+hard+to+tell" + artist := "Nas" + + r, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + l := logrus.New() + m := New(l) + + m.ServeHTTP(rr, r) + + defer rr.Result().Body.Close() + + doc, err := goquery.NewDocumentFromReader(rr.Result().Body) + if err != nil { + t.Fatal(err) + } + + docArtist := doc.Find("#search-item > div > span").First().Text() + if docArtist != artist { + t.Fatalf("expected %q, got %q\n", artist, docArtist) + } +} diff --git a/handlers/utils.go b/handlers/utils.go index 5873190..d9b0c24 100644 --- a/handlers/utils.go +++ b/handlers/utils.go @@ -9,7 +9,7 @@ import ( fhttp "github.com/Danny-Dasilva/fhttp" ) -func MustHeaders(next http.Handler) http.Handler { +func mustHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { csp := "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; object-src 'none'" w.Header().Add("content-security-policy", csp) diff --git a/handlers/utils_test.go b/handlers/utils_test.go new file mode 100644 index 0000000..9db2374 --- /dev/null +++ b/handlers/utils_test.go @@ -0,0 +1,32 @@ +package handlers + +/* +import ( + "encoding/json" + "testing" +) + +func TestSendRequest(t *testing.T) { + res, err := sendRequest("https://tls.peet.ws/api/clean") + if err != nil { + t.Fatal(err) + } + + defer res.Body.Close() + + type fingerprint struct { + JA3 string `json:"ja3"` + } + + decoder := json.NewDecoder(res.Body) + var fg fingerprint + + if err := decoder.Decode(&fg); err != nil { + t.Fatal(err) + } + + if fg.JA3 != JA3 { + t.Fatalf("expected %q, got %q\n", JA3, fg.JA3) + } +} +*/ diff --git a/main.go b/main.go index 67d06e9..bc4298a 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "context" "fmt" "net" "net/http" @@ -9,35 +8,15 @@ import ( "strconv" "time" - "github.com/a-h/templ" - "github.com/gorilla/mux" "github.com/rramiachraf/dumb/handlers" - "github.com/rramiachraf/dumb/views" "github.com/sirupsen/logrus" ) -var logger = logrus.New() - func main() { - r := mux.NewRouter() - - r.Use(handlers.MustHeaders) - - r.Handle("/", templ.Handler(views.HomePage())) - r.HandleFunc("/{id}-lyrics", handlers.Lyrics(logger)).Methods("GET") - r.HandleFunc("/albums/{artist}/{albumName}", handlers.Album(logger)).Methods("GET") - r.HandleFunc("/images/{filename}.{ext}", handlers.ImageProxy(logger)).Methods("GET") - r.HandleFunc("/search", handlers.Search(logger)).Methods("GET") - r.HandleFunc("/{id}/{artist-song}/{verse}/annotations", handlers.Annotations(logger)).Methods("GET") - r.HandleFunc("/instances.json", handlers.Instances(logger)).Methods("GET") - r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) - r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - views.ErrorPage(404, "page not found").Render(context.Background(), w) - }) + var logger = logrus.New() server := &http.Server{ - Handler: r, + Handler: handlers.New(logger), WriteTimeout: 25 * time.Second, ReadTimeout: 25 * time.Second, }