test: add unit tests

This commit is contained in:
rramiachraf 2024-03-06 20:53:29 +01:00
parent c8747c0182
commit afc0347a1b
19 changed files with 358 additions and 31 deletions

18
.github/workflows/test.yml vendored Normal file
View File

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

View File

@ -8,5 +8,7 @@ build:gentempl esbuild
templ generate templ generate
cat ./style/*.css | ./esbuild --loader=css --minify > ./static/style.css cat ./style/*.css | ./esbuild --loader=css --minify > ./static/style.css
go build -ldflags="-X 'github.com/rramiachraf/dumb/data.Version=$(VERSION)' -s -w" go build -ldflags="-X 'github.com/rramiachraf/dumb/data.Version=$(VERSION)' -s -w"
test:
go test ./... -v
fmt: fmt:
templ fmt . templ fmt .

View File

@ -12,7 +12,7 @@ import (
"github.com/sirupsen/logrus" "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) { return func(w http.ResponseWriter, r *http.Request) {
artist := mux.Vars(r)["artist"] artist := mux.Vars(r)["artist"]
albumName := mux.Vars(r)["albumName"] albumName := mux.Vars(r)["albumName"]

39
handlers/album_test.go Normal file
View File

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

View File

@ -16,7 +16,7 @@ import (
"github.com/sirupsen/logrus" "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) { return func(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"] id := mux.Vars(r)["id"]
if a, err := getCache[data.Annotation]("annotation:" + id); err == nil { if a, err := getCache[data.Annotation]("annotation:" + id); err == nil {

View File

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

25
handlers/cache_test.go Normal file
View File

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

32
handlers/handler.go Normal file
View File

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

View File

@ -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) { return func(w http.ResponseWriter, r *http.Request) {
if instances, err := getCache[[]byte]("instances"); err == nil { if instances, err := getCache[[]byte]("instances"); err == nil {
w.Header().Set("content-type", ContentTypeJSON) w.Header().Set("content-type", ContentTypeJSON)

View File

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

View File

@ -12,7 +12,7 @@ import (
"github.com/sirupsen/logrus" "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) { return func(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"] id := mux.Vars(r)["id"]

45
handlers/lyrics_test.go Normal file
View File

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

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"mime"
"net/http" "net/http"
"strings" "strings"
@ -23,7 +24,7 @@ func isValidExt(ext string) bool {
return false return false
} }
func ImageProxy(l *logrus.Logger) http.HandlerFunc { func imageProxy(l *logrus.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
v := mux.Vars(r) v := mux.Vars(r)
f := v["filename"] f := v["filename"]
@ -52,7 +53,7 @@ func ImageProxy(l *logrus.Logger) http.HandlerFunc {
return 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") w.Header().Add("Cache-Control", "max-age=1296000")
if _, err = io.Copy(w, res.Body); err != nil { if _, err = io.Copy(w, res.Body); err != nil {
l.Errorln("unable to write image", err) l.Errorln("unable to write image", err)

38
handlers/proxy_test.go Normal file
View File

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

View File

@ -12,7 +12,7 @@ import (
"github.com/sirupsen/logrus" "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) { return func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q") query := r.URL.Query().Get("q")
url := fmt.Sprintf(`https://genius.com/api/search/multi?q=%s`, url.QueryEscape(query)) url := fmt.Sprintf(`https://genius.com/api/search/multi?q=%s`, url.QueryEscape(query))

38
handlers/search_test.go Normal file
View File

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

View File

@ -9,7 +9,7 @@ import (
fhttp "github.com/Danny-Dasilva/fhttp" 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) { 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'" csp := "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; object-src 'none'"
w.Header().Add("content-security-policy", csp) w.Header().Add("content-security-policy", csp)

32
handlers/utils_test.go Normal file
View File

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

25
main.go
View File

@ -1,7 +1,6 @@
package main package main
import ( import (
"context"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@ -9,35 +8,15 @@ import (
"strconv" "strconv"
"time" "time"
"github.com/a-h/templ"
"github.com/gorilla/mux"
"github.com/rramiachraf/dumb/handlers" "github.com/rramiachraf/dumb/handlers"
"github.com/rramiachraf/dumb/views"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
func main() {
var logger = logrus.New() 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)
})
server := &http.Server{ server := &http.Server{
Handler: r, Handler: handlers.New(logger),
WriteTimeout: 25 * time.Second, WriteTimeout: 25 * time.Second,
ReadTimeout: 25 * time.Second, ReadTimeout: 25 * time.Second,
} }