diff --git a/.github/workflows/.github-ci.yml b/.github/workflows/.github-ci.yml index d7705da..2000290 100644 --- a/.github/workflows/.github-ci.yml +++ b/.github/workflows/.github-ci.yml @@ -5,7 +5,6 @@ on: push jobs: build: runs-on: ubuntu-22.04 - steps: - name: Repository meta id: repository @@ -46,5 +45,5 @@ jobs: push: true allow: network.host network: host - platforms: linux/amd64,linux/arm64 + platforms: linux/amd64 tags: ${{ steps.docker.outputs.tags }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index c606e1c..692a233 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,13 @@ -FROM golang:1.17-alpine AS build-env +FROM golang:1.24.1-alpine AS build-env ENV GO111MODULE=on WORKDIR /go/src/github.com/hugomd/ascii-live/ -RUN apk add ca-certificates +# RUN apk add ca-certificates COPY . /go/src/github.com/hugomd/ascii-live/ RUN cd /go/src/github.com/hugomd/ascii-live && \ go mod download && \ - CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main . + CGO_ENABLED=0 GOOS=linux go build -installsuffix cgo -o main . FROM scratch COPY --from=build-env /go/src/github.com/hugomd/ascii-live/main / -COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +# COPY --from=build-env /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt CMD ["/main"] diff --git a/deployment/deployment.yaml b/deployment/deployment.yaml index c1037eb..55e485e 100644 --- a/deployment/deployment.yaml +++ b/deployment/deployment.yaml @@ -44,7 +44,7 @@ metadata: nginx.ingress.kubernetes.io/ssl-redirect: "false" spec: rules: - - host: 192-168-197-2.traefik.me + - host: factory.c-one-infra.de http: paths: - path: /ascii-live @@ -57,4 +57,4 @@ spec: tls: - secretName: ascii-live-tls-prod-cert hosts: - - 192-168-197-2.traefik.me + - factory.c-one-infra.de diff --git a/go.mod b/go.mod index a626244..107bdef 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,15 @@ -module github.com/hugomd/ascii-live +module ascii-live -go 1.17 +go 1.24.1 require ( - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b - github.com/gorilla/mux v1.7.3 + github.com/golang/glog v1.2.4 + github.com/gorilla/mux v1.8.1 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 68db0f5..0659769 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,16 @@ -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +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/golang/glog v1.2.4 h1:CNNw5U8lSiiBk7druxtSHHTsRWcxKoac6kZKm2peBBc= +github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/handlers.go b/handlers/handlers.go new file mode 100644 index 0000000..2368fa8 --- /dev/null +++ b/handlers/handlers.go @@ -0,0 +1,103 @@ +package handlers + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "ascii-live/frames" + + "github.com/golang/glog" + "github.com/gorilla/mux" +) + +var NotFoundMessage = map[string]string{ + "error": "Frames not found. Navigate to /ascii/list for list of frames. Navigate to https://github.com/hugomd/ascii-live to submit new frames.", +} + +var NotCurledMessage = map[string]string{ + "error": "You almost ruined a good surprise. Come on, curl it in terminal.", +} + +var availableFrames []string + +func init() { + for k := range frames.FrameMap { + availableFrames = append(availableFrames, k) + } +} + +func writeJson(w http.ResponseWriter, r *http.Request, res interface{}, status int) { + w.Header().Set("Content-Type", "application/json") + data, err := json.Marshal(res) + if err != nil { + return + } + w.WriteHeader(status) + fmt.Fprint(w, string(data)) +} + +func ListHandler(w http.ResponseWriter, r *http.Request) { + writeJson(w, r, map[string][]string{"frames": availableFrames}, http.StatusOK) +} + +func NotFoundHandler(w http.ResponseWriter, r *http.Request) { + writeJson(w, r, NotFoundMessage, http.StatusNotFound) +} + +func NotCurledHandler(w http.ResponseWriter, r *http.Request) { + writeJson(w, r, NotCurledMessage, http.StatusExpectationFailed) +} + +func Handler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + flusher := w.(http.Flusher) + + vars := mux.Vars(r) + frameSource := vars["frameSource"] + glog.Infof("Frame source %s", frameSource) + + frames, ok := frames.FrameMap[frameSource] + if !ok { + NotFoundHandler(w, r) + return + } + + userAgent := r.Header.Get("User-Agent") + if !strings.Contains(userAgent, "curl") { + NotCurledHandler(w, r) + return + } + + w.Header().Set("Transfer-Encoding", "chunked") + w.WriteHeader(http.StatusOK) + + i := 0 + for { + select { + // Handle client disconnects + case <-ctx.Done(): + glog.Infof("Client stopped listening") + return + default: + if i >= frames.GetLength() { + i = 0 + } + // Artificially wait between reponses. + time.Sleep(frames.GetSleep()) + + // Clear screen + clearScreen := "\033[2J\033[H" + newLine := "\n" + + // Write frames + fmt.Fprint(w, clearScreen+frames.GetFrame(i)+newLine) + i++ + + // Send some data. + flusher.Flush() + } + } +} diff --git a/main.go b/main.go index 457d226..90c71fd 100644 --- a/main.go +++ b/main.go @@ -1,108 +1,15 @@ package main import ( - "encoding/json" "flag" - "fmt" "net/http" - "strings" - "time" - "github.com/hugomd/ascii-live/frames" + "ascii-live/handlers" "github.com/golang/glog" "github.com/gorilla/mux" ) -var NotFoundMessage = map[string]string{ - "error": "Frames not found. Navigate to /list for list of frames. Navigate to https://github.com/hugomd/ascii-live to submit new frames.", -} - -var NotCurledMessage = map[string]string{ - "error": "You almost ruined a good surprise. Come on, curl it in terminal.", -} - -var availableFrames []string - -func init() { - for k := range frames.FrameMap { - availableFrames = append(availableFrames, k) - } -} - -func writeJson(w http.ResponseWriter, r *http.Request, res interface{}, status int) { - w.Header().Set("Content-Type", "application/json") - data, err := json.Marshal(res) - if err != nil { - return - } - w.WriteHeader(status) - fmt.Fprint(w, string(data)) -} - -func listHandler(w http.ResponseWriter, r *http.Request) { - writeJson(w, r, map[string][]string{"frames": availableFrames}, http.StatusOK) -} - -func notFoundHandler(w http.ResponseWriter, r *http.Request) { - writeJson(w, r, NotFoundMessage, http.StatusNotFound) -} - -func notCurledHandler(w http.ResponseWriter, r *http.Request) { - writeJson(w, r, NotCurledMessage, http.StatusExpectationFailed) -} - -func handler(w http.ResponseWriter, r *http.Request) { - cn := w.(http.CloseNotifier) - flusher := w.(http.Flusher) - - vars := mux.Vars(r) - frameSource := vars["frameSource"] - glog.Infof("Frame source %s", frameSource) - - frames, ok := frames.FrameMap[frameSource] - if !ok { - notFoundHandler(w, r) - return - } - - userAgent := r.Header.Get("User-Agent") - if !strings.Contains(userAgent, "curl") { - notCurledHandler(w, r) - return - } - - w.Header().Set("Transfer-Encoding", "chunked") - w.WriteHeader(http.StatusOK) - - i := 0 - for { - select { - // Handle client disconnects - case <-cn.CloseNotify(): - glog.Infof("Client stopped listening") - return - default: - if i >= frames.GetLength() { - i = 0 - } - // Artificially wait between reponses. - time.Sleep(frames.GetSleep()) - - // Clear screen - clearScreen := "\033[2J\033[H" - newLine := "\n" - - // Write frames - fmt.Fprint(w, clearScreen+frames.GetFrame(i)+newLine) - i++ - - // Send some data. - flusher.Flush() - } - } -} - // Server. func main() { flag.Parse() @@ -110,9 +17,9 @@ func main() { flag.Set("logtostderr", "true") r := mux.NewRouter() - r.HandleFunc("/ascii-live/list", listHandler).Methods("GET") - r.HandleFunc("/ascii-live/{frameSource}", handler).Methods("GET") - r.NotFoundHandler = http.HandlerFunc(notFoundHandler) + r.HandleFunc("/ascii-live/list", handlers.ListHandler).Methods("GET") + r.HandleFunc("/ascii-live/{frameSource}", handlers.Handler).Methods("GET") + r.NotFoundHandler = http.HandlerFunc(handlers.NotFoundHandler) srv := &http.Server{ Handler: r, diff --git a/tests/handlers_test.go b/tests/handlers_test.go new file mode 100644 index 0000000..cc9cebf --- /dev/null +++ b/tests/handlers_test.go @@ -0,0 +1,69 @@ +package test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "ascii-live/handlers" + + "github.com/gorilla/mux" +) + +func TestListHandler(t *testing.T) { + r := mux.NewRouter() + r.HandleFunc("/ascii-live/list", handlers.ListHandler) + + req, err := http.NewRequest("GET", "/ascii-live/list", nil) + if err != nil { + t.Fatal(err) + } + + recorder := httptest.NewRecorder() + r.ServeHTTP(recorder, req) + + if status := recorder.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK) + } + + var response map[string][]string + if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil { + t.Errorf("error decoding response: %v", err) + } +} + +func TestNotFoundHandler(t *testing.T) { + r := mux.NewRouter() + r.NotFoundHandler = http.HandlerFunc(handlers.NotFoundHandler) + + req, err := http.NewRequest("GET", "/not-existing", nil) + if err != nil { + t.Fatal(err) + } + + recorder := httptest.NewRecorder() + r.ServeHTTP(recorder, req) // Router verarbeitet die Anfrage + + if status := recorder.Code; status != http.StatusNotFound { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusNotFound) + } +} + +func TestNotCurledHandler(t *testing.T) { + r := mux.NewRouter() + r.HandleFunc("/ascii-live/donut", handlers.NotCurledHandler) + + req, err := http.NewRequest("GET", "/ascii-live/donut", nil) + if err != nil { + t.Fatal(err) + } + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36") + + recorder := httptest.NewRecorder() + r.ServeHTTP(recorder, req) + + if status := recorder.Code; status != http.StatusExpectationFailed { + t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusExpectationFailed) + } +} diff --git a/tests/handlers_testify_test.go b/tests/handlers_testify_test.go new file mode 100644 index 0000000..ad81f72 --- /dev/null +++ b/tests/handlers_testify_test.go @@ -0,0 +1,66 @@ +package test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "ascii-live/handlers" +) + +func TestListHandler2(t *testing.T) { + req, err := http.NewRequest("GET", "/ascii/list", nil) + require.NoError(t, err, "Failed to create request") + + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(handlers.ListHandler) + + handler.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code, "Status code should be OK") + assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"), "Content-Type should be application/json") + + var responseBody map[string][]string + err = json.Unmarshal(recorder.Body.Bytes(), &responseBody) + require.NoError(t, err, "Failed to unmarshal response JSON") + +} + +func TestNotFoundHandler2(t *testing.T) { + req, err := http.NewRequest("GET", "/some/unknown/path", nil) + require.NoError(t, err, "Failed to create request") + + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(handlers.NotFoundHandler) // Test directly + + expectedBodyBytes, err := json.Marshal(handlers.NotFoundMessage) + require.NoError(t, err, "Failed to marshal expected NotFoundMessage") + + handler.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusNotFound, recorder.Code, "Status code should be Not Found") + assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"), "Content-Type should be application/json") + assert.JSONEq(t, string(expectedBodyBytes), recorder.Body.String(), "Response body does not match expected NotFoundMessage") +} + +func TestNotCurledHandler2(t *testing.T) { + req, err := http.NewRequest("GET", "/ascii/someframe", nil) + require.NoError(t, err, "Failed to create request") + // crucially, DO NOT set User-Agent to 'curl' + + recorder := httptest.NewRecorder() + handler := http.HandlerFunc(handlers.NotCurledHandler) // Test directly + + expectedBodyBytes, err := json.Marshal(handlers.NotCurledMessage) + require.NoError(t, err, "Failed to marshal expected NotCurledMessage") + + handler.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusExpectationFailed, recorder.Code, "Status code should be Expectation Failed") + assert.Equal(t, "application/json", recorder.Header().Get("Content-Type"), "Content-Type should be application/json") + assert.JSONEq(t, string(expectedBodyBytes), recorder.Body.String(), "Response body does not match expected NotCurledMessage") +}