feat(11.1-04): add the Database section and the core Services pages
- docs/database: models, migrations, queries and pagination, relations, casts and validation, attachments and transactions (lagoon.Transaction, lagoon.AfterCommit, nested savepoints, lagoon.OnDatabase) - docs/services: configuration, events, routing with auth groups, rate limiting, authentication, the OAuth server, mail and localization - runnable Examples for lagoon, attach, compass, surf, wire, bouncer, wristband, postcard, phrasebook and festival; lagoon TestDocs* regions run on the package's Postgres harness through DocsDB - 15 new required pages
This commit is contained in:
208
modules/surf/example_test.go
Normal file
208
modules/surf/example_test.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package surf_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/bouncer"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/pact"
|
||||
"git.golem15.com/golem15/summercms/modules/party"
|
||||
"git.golem15.com/golem15/summercms/modules/surf"
|
||||
"git.golem15.com/golem15/summercms/modules/wire"
|
||||
)
|
||||
|
||||
// secret signs the example's tokens. A real application reads its JWT
|
||||
// secret from configuration.
|
||||
const secret = "example-secret-that-is-long-enough"
|
||||
|
||||
// users loads the principal behind a token subject.
|
||||
type users struct{}
|
||||
|
||||
func (users) FindByID(ctx context.Context, id uint) (*bouncer.Principal, error) {
|
||||
return &bouncer.Principal{ID: id}, nil
|
||||
}
|
||||
|
||||
// BlogPlugin is the acme.blog plugin; only its HTTP surface is shown here.
|
||||
type BlogPlugin struct {
|
||||
trusted []netip.Prefix
|
||||
}
|
||||
|
||||
func (p *BlogPlugin) ID() string { return "acme.blog" }
|
||||
func (p *BlogPlugin) Requires() []string { return nil }
|
||||
|
||||
// Register reads http.trusted_proxies once, for the plugin's bucket keys.
|
||||
func (p *BlogPlugin) Register(app *backpack.App) error {
|
||||
p.trusted = surf.TrustedProxies(app.Config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *BlogPlugin) Boot(app *backpack.App) error { return nil }
|
||||
|
||||
// Middlewares registers the plugin's named middleware: here, a JWT guard
|
||||
// that answers 401 when the request has no valid token.
|
||||
func (p *BlogPlugin) Middlewares() map[string]pact.Middleware {
|
||||
guards := bouncer.NewRegistry()
|
||||
guard := bouncer.NewJWTGuard(secret, users{}, bouncer.NewMemoryBlacklist())
|
||||
if err := guards.Register(p.ID(), "acme.auth", guard); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
auth, err := guards.Middleware("acme.auth")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return map[string]pact.Middleware{"acme.auth": auth}
|
||||
}
|
||||
|
||||
// Buckets declares a named rate limit, used as throttle:blog.comments.
|
||||
func (p *BlogPlugin) Buckets() map[string]surf.Bucket {
|
||||
return map[string]surf.Bucket{
|
||||
"blog.comments": {
|
||||
Max: 1,
|
||||
Decay: time.Minute,
|
||||
Key: func(r *http.Request) string { return "comments|" + surf.ClientIP(r, p.trusted) },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Routes is the Go form of the plugin's routes.php.
|
||||
func (p *BlogPlugin) Routes(r pact.Router) error {
|
||||
r.Group("/api/blog", surf.Use("throttle:60,1"), func(g pact.Router) {
|
||||
g.Get("/posts/{id}", showPost)
|
||||
g.Where("id", `[0-9]+`)
|
||||
g.Get("/posts/{status}/list", listPosts)
|
||||
g.WhereIn("status", "draft", "published")
|
||||
|
||||
// An auth group: every route inside needs a signed-in user.
|
||||
g.Group("", surf.Use("acme.auth"), func(auth pact.Router) {
|
||||
auth.Get("/me", showMe)
|
||||
auth.Post("/posts/{id}/comments", addComment, "throttle:blog.comments", "body.limit:65536")
|
||||
})
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func showPost(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := surf.IntParam(r, "id")
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
wire.WriteJSON(w, http.StatusOK, map[string]any{"id": id})
|
||||
}
|
||||
|
||||
func listPosts(w http.ResponseWriter, r *http.Request) {
|
||||
wire.WriteJSON(w, http.StatusOK, map[string]any{"status": r.PathValue("status"), "data": wire.Slice[string](nil)})
|
||||
}
|
||||
|
||||
func showMe(w http.ResponseWriter, r *http.Request) {
|
||||
user, _ := bouncer.User(r.Context())
|
||||
wire.WriteJSON(w, http.StatusOK, map[string]any{"id": user.ID})
|
||||
}
|
||||
|
||||
func addComment(w http.ResponseWriter, r *http.Request) {
|
||||
wire.WriteJSON(w, http.StatusCreated, map[string]any{"created": true})
|
||||
}
|
||||
|
||||
func ExampleAssemble() {
|
||||
// The application passes its config; http.body_limits is required there.
|
||||
app := backpack.New(nil)
|
||||
plugin := &BlogPlugin{}
|
||||
if err := plugin.Register(app); err != nil { // the runtime calls Register
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
h, err := surf.Assemble(app, []party.Plugin{plugin})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
token, _, _ := bouncer.Mint(secret, "42", "http://127.0.0.1:8080/api/login", time.Hour)
|
||||
|
||||
do := func(method, path string, auth bool) {
|
||||
req := httptest.NewRequest(method, path, strings.NewReader("{}"))
|
||||
if auth {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, req)
|
||||
fmt.Println(method, path, rec.Code, strings.TrimSpace(rec.Body.String()))
|
||||
}
|
||||
do("GET", "/api/blog/posts/7", false)
|
||||
do("GET", "/api/blog/posts/seven", false)
|
||||
do("GET", "/api/blog/posts/draft/list", false)
|
||||
do("GET", "/api/blog/posts/deleted/list", false)
|
||||
do("GET", "/api/blog/me", false)
|
||||
do("GET", "/api/blog/me", true)
|
||||
do("POST", "/api/blog/posts/7/comments", true)
|
||||
do("POST", "/api/blog/posts/7/comments", true)
|
||||
// Output:
|
||||
// GET /api/blog/posts/7 200 {"id":7}
|
||||
// GET /api/blog/posts/seven 404 404 page not found
|
||||
// GET /api/blog/posts/draft/list 200 {"data":[],"status":"draft"}
|
||||
// GET /api/blog/posts/deleted/list 404 404 page not found
|
||||
// GET /api/blog/me 401 {"error":true,"message":"Token not provided"}
|
||||
// GET /api/blog/me 200 {"id":42}
|
||||
// POST /api/blog/posts/7/comments 201 {"created":true}
|
||||
// POST /api/blog/posts/7/comments 429 {"message":"Too Many Attempts."}
|
||||
}
|
||||
|
||||
func ExampleBuildRouter() {
|
||||
r, err := surf.BuildRouter(backpack.New(nil), []party.Plugin{&BlogPlugin{}})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
for _, rt := range r.Routes() {
|
||||
fmt.Println(rt.Method, rt.Pattern, rt.PluginID, rt.Middleware)
|
||||
}
|
||||
// Output:
|
||||
// GET /api/blog/posts/{id} acme.blog [throttle:60,1]
|
||||
// GET /api/blog/posts/{status}/list acme.blog [throttle:60,1]
|
||||
// GET /api/blog/me acme.blog [throttle:60,1 acme.auth]
|
||||
// POST /api/blog/posts/{id}/comments acme.blog [throttle:60,1 acme.auth throttle:blog.comments body.limit:65536]
|
||||
}
|
||||
|
||||
func ExampleClientIP() {
|
||||
cfg, err := compass.Open(compass.Options{Dir: "config", Env: "development", Environ: []string{}})
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
_ = cfg.Set("http.trusted_proxies", []string{"10.0.0.0/8"})
|
||||
trusted := surf.TrustedProxies(cfg)
|
||||
|
||||
// Through the load balancer at 10.0.0.5: the forwarded client is used.
|
||||
viaProxy := httptest.NewRequest("GET", "/api/blog/posts", nil)
|
||||
viaProxy.RemoteAddr = "10.0.0.5:4711"
|
||||
viaProxy.Header.Set("X-Forwarded-For", "198.51.100.23, 10.0.0.9")
|
||||
fmt.Println(surf.ClientIP(viaProxy, trusted))
|
||||
|
||||
// Straight from the internet: a forged header is ignored.
|
||||
direct := httptest.NewRequest("GET", "/api/blog/posts", nil)
|
||||
direct.RemoteAddr = "203.0.113.7:5000"
|
||||
direct.Header.Set("X-Forwarded-For", "127.0.0.1")
|
||||
fmt.Println(surf.ClientIP(direct, trusted))
|
||||
// Output:
|
||||
// 198.51.100.23
|
||||
// 203.0.113.7
|
||||
}
|
||||
|
||||
// TestDocsDeclarations checks the plugin declarations the Routing and Rate
|
||||
// limiting pages show.
|
||||
func TestDocsDeclarations(t *testing.T) {
|
||||
p := &BlogPlugin{}
|
||||
if _, ok := p.Middlewares()["acme.auth"]; !ok {
|
||||
t.Fatal("Middlewares() has no acme.auth")
|
||||
}
|
||||
if b := p.Buckets()["blog.comments"]; b.Max != 1 || b.Decay != time.Minute {
|
||||
t.Fatalf("Buckets() = %+v", b)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user