Mirgate project to go

This commit is contained in:
2025-10-02 13:23:09 +02:00
parent d0c2a48238
commit 0e54a6019e
40 changed files with 325 additions and 272 deletions

170
internal/server/server.go Normal file
View File

@@ -0,0 +1,170 @@
package server
import (
"errors"
"fmt"
"html"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"sync"
"time"
)
const (
address = ":5000"
htmlContentType = "text/html; charset=utf-8"
notFoundTemplate = `
<div class="tui-window">
<fieldset class="tui-fieldset">
<legend>Error</legend>
<p>Route %s not found.</p>
</fieldset>
</div>
`
)
// Server hosts the Luxtools HTTP handlers.
type Server struct {
mux *http.ServeMux
indexPage []byte
staticRoot string
assetsRoot string
counterMu sync.Mutex
clickCount int
}
// New constructs a configured Server ready to serve requests.
func New() (*Server, error) {
projectRoot, err := goProjectRoot()
if err != nil {
return nil, err
}
indexPath := filepath.Join(projectRoot, "web", "templates", "index.html")
staticDir := filepath.Join(projectRoot, "web", "static")
assetsDir := filepath.Join(projectRoot, "assets")
indexPage, err := os.ReadFile(indexPath)
if err != nil {
return nil, fmt.Errorf("failed to read index template: %w", err)
}
if _, err := os.Stat(staticDir); err != nil {
return nil, fmt.Errorf("static directory check failed: %w", err)
}
if _, err := os.Stat(assetsDir); err != nil {
return nil, fmt.Errorf("assets directory check failed: %w", err)
}
s := &Server{
mux: http.NewServeMux(),
indexPage: indexPage,
staticRoot: staticDir,
assetsRoot: assetsDir,
}
s.registerRoutes()
return s, nil
}
// Router exposes the configured http.Handler for the server.
func (s *Server) Router() http.Handler {
return s.mux
}
// Address returns the default listen address for the server.
func (s *Server) Address() string {
return address
}
func (s *Server) registerRoutes() {
s.mux.Handle("/static/", http.StripPrefix("/static/", allowGetHead(http.FileServer(http.Dir(s.staticRoot)))))
s.mux.Handle("/assets/", http.StripPrefix("/assets/", allowGetHead(http.FileServer(http.Dir(s.assetsRoot)))))
s.mux.HandleFunc("/", s.handleRequest)
}
func (s *Server) handleRequest(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
s.sendHTML(w, http.StatusOK, s.indexPage)
case "/counter":
s.handleCounter(w, r)
case "/time":
s.sendHTMLString(w, http.StatusOK, renderTime(time.Now()))
default:
s.sendHTMLString(w, http.StatusNotFound, fmt.Sprintf(notFoundTemplate, html.EscapeString(r.URL.Path)))
}
}
func (s *Server) handleCounter(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost || r.Method == http.MethodPut {
s.counterMu.Lock()
s.clickCount++
s.counterMu.Unlock()
}
s.counterMu.Lock()
count := s.clickCount
s.counterMu.Unlock()
s.sendHTMLString(w, http.StatusOK, renderCounter(count))
}
func (s *Server) sendHTML(w http.ResponseWriter, status int, body []byte) {
w.Header().Set("Content-Type", htmlContentType)
w.WriteHeader(status)
if len(body) > 0 {
if _, err := w.Write(body); err != nil {
log.Printf("write response failed: %v", err)
}
}
}
func (s *Server) sendHTMLString(w http.ResponseWriter, status int, body string) {
s.sendHTML(w, status, []byte(body))
}
func renderCounter(count int) string {
return fmt.Sprintf(`
<div id="counter" class="tui-panel tui-panel-inline">
<p><strong>Clicks:</strong> %d</p>
<button class="tui-button" hx-post="/counter" hx-target="#counter" hx-swap="outerHTML">
Increment
</button>
</div>
`, count)
}
func renderTime(now time.Time) string {
return fmt.Sprintf(`
<div id="server-time" class="tui-panel tui-panel-inline">
<p><strong>Server time:</strong> %s</p>
</div>
`, now.Format("2006-01-02 15:04:05"))
}
func allowGetHead(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet, http.MethodHead:
next.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
func goProjectRoot() (string, error) {
_, currentFile, _, ok := runtime.Caller(0)
if !ok {
return "", errors.New("unable to determine caller information")
}
dir := filepath.Dir(currentFile)
projectRoot := filepath.Clean(filepath.Join(dir, "..", ".."))
return projectRoot, nil
}

View File

@@ -0,0 +1,68 @@
package server
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestRenderCounter(t *testing.T) {
html := renderCounter(7)
if !strings.Contains(html, "Clicks:") {
t.Fatalf("expected counter markup to include label")
}
if !strings.Contains(html, "> 7<") {
t.Fatalf("expected counter value in markup, got %q", html)
}
}
func TestRenderTime(t *testing.T) {
now := time.Date(2024, time.August, 1, 12, 34, 56, 0, time.UTC)
html := renderTime(now)
expected := "2024-08-01 12:34:56"
if !strings.Contains(html, expected) {
t.Fatalf("expected formatted timestamp %s in markup", expected)
}
}
func TestCounterEndpoint(t *testing.T) {
srv, err := New()
if err != nil {
t.Fatalf("failed to create server: %v", err)
}
req := httptest.NewRequest(http.MethodPost, "/counter", nil)
resp := httptest.NewRecorder()
srv.Router().ServeHTTP(resp, req)
if resp.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", resp.Code)
}
body := resp.Body.String()
if !strings.Contains(body, "> 1<") {
t.Fatalf("expected counter to increment to 1, body: %q", body)
}
}
func TestNotFound(t *testing.T) {
srv, err := New()
if err != nil {
t.Fatalf("failed to create server: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/unknown", nil)
resp := httptest.NewRecorder()
srv.Router().ServeHTTP(resp, req)
if resp.Code != http.StatusNotFound {
t.Fatalf("expected 404, got %d", resp.Code)
}
if !strings.Contains(resp.Body.String(), "Route /unknown not found.") {
t.Fatalf("expected not found message, got %q", resp.Body.String())
}
}