From 53e17b61011a90e159e5b9e21ff9d88ca613d73a Mon Sep 17 00:00:00 2001 From: "quarterback[bot]" Date: Fri, 21 Aug 2026 22:41:42 +0000 Subject: [PATCH] Add pricing page Authored-by: Quarterback --- internal/http/handlers/pricing_test.go | 53 ++++++++++++++++ internal/http/router.go | 3 + internal/http/router_test.go | 7 ++- web/templates/base.html | 1 + web/templates/pricing.html | 83 ++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 2 deletions(-) create mode 100644 internal/http/handlers/pricing_test.go create mode 100644 web/templates/pricing.html diff --git a/internal/http/handlers/pricing_test.go b/internal/http/handlers/pricing_test.go new file mode 100644 index 0000000..6e44428 --- /dev/null +++ b/internal/http/handlers/pricing_test.go @@ -0,0 +1,53 @@ +package handlers_test + +import ( + "html/template" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/softsrv/rowbot/internal/http/handlers" + "github.com/softsrv/rowbot/web" +) + +func newPricingRealTemplateRenderer(t *testing.T) *handlers.TemplateRenderer { + t.Helper() + base, err := template.ParseFS(web.FS, "templates/base.html") + if err != nil { + t.Fatalf("parse base template: %v", err) + } + if _, err := base.ParseFS(web.FS, "templates/partials/*.html"); err != nil { + t.Fatalf("parse partials: %v", err) + } + return handlers.NewTemplateRenderer(base, web.FS, "templates") +} + +func TestPricingPageRendersPublicPricingContent(t *testing.T) { + renderer := newPricingRealTemplateRenderer(t) + rr := httptest.NewRecorder() + + renderer.Page(rr, http.StatusOK, "pricing.html", nil) + + if rr.Code != http.StatusOK { + t.Fatalf("status = %d, want 200", rr.Code) + } + body := rr.Body.String() + for _, want := range []string{ + "Free", + "Cozy Community", + "Large Community", + "up to 10", + "up to 50", + "up to 250", + "$5", + "$10", + "js-get-started", + `id="get-started-modal"`, + `href="/pricing"`, + } { + if !strings.Contains(body, want) { + t.Fatalf("rendered pricing page missing %q in:\n%s", want, body) + } + } +} diff --git a/internal/http/router.go b/internal/http/router.go index f63d295..d890a38 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -111,6 +111,9 @@ func NewRouter(ctx context.Context, cfg RouterConfig) http.Handler { mux.HandleFunc("GET /privacy", func(w http.ResponseWriter, r *http.Request) { cfg.Renderer.Page(w, http.StatusOK, "privacy.html", nil) }) + mux.HandleFunc("GET /pricing", func(w http.ResponseWriter, r *http.Request) { + cfg.Renderer.Page(w, http.StatusOK, "pricing.html", nil) + }) // {$} anchors this to the exact path "/" — without it, a pattern ending in // "/" is a subtree match in net/http's ServeMux and catches every diff --git a/internal/http/router_test.go b/internal/http/router_test.go index 943ee39..2916697 100644 --- a/internal/http/router_test.go +++ b/internal/http/router_test.go @@ -32,6 +32,9 @@ var routerTestTemplateFS = fstest.MapFS{ "templates/privacy.html": &fstest.MapFile{Data: []byte( `{{define "content"}}privacy{{end}}`, )}, + "templates/pricing.html": &fstest.MapFile{Data: []byte( + `{{define "content"}}pricing{{end}}`, + )}, } func newRouterTestRenderer(t *testing.T) *handlers.TemplateRenderer { @@ -179,14 +182,14 @@ func TestDashboardServerChannelDeleteRouteIsRegistered(t *testing.T) { } } -func TestTermsAndPrivacyPagesArePublic(t *testing.T) { +func TestPublicInformationalPagesArePublic(t *testing.T) { t.Parallel() router := httpapp.NewRouter(context.Background(), httpapp.RouterConfig{ Renderer: newRouterTestRenderer(t), JWTSecret: routerTestJWTSecret, }) - for _, path := range []string{"/terms", "/privacy"} { + for _, path := range []string{"/terms", "/privacy", "/pricing"} { req := httptest.NewRequest(http.MethodGet, path, nil) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) diff --git a/web/templates/base.html b/web/templates/base.html index 74d2fa2..5f5fdf4 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -24,6 +24,7 @@
+ Pricing {{if .User}}