feat: status page id validation and cache key

This commit is contained in:
2026-08-24 14:02:46 +00:00
parent 1c15961309
commit 9c0bbd13dd
2 changed files with 64 additions and 1 deletions
+22
View File
@@ -2,6 +2,8 @@ package services
import (
"context"
"errors"
"regexp"
"time"
"gitea.hostxtra.co.uk/mrhid6/vantage/server/internal/db"
@@ -10,6 +12,26 @@ import (
"go.mongodb.org/mongo-driver/v2/mongo/options"
)
// ErrInvalidPageID is returned for any page id that would not be safe or
// pleasant in a URL handed to a customer.
var ErrInvalidPageID = errors.New("page id must be 3-40 characters of a-z, 0-9 and -, starting and ending alphanumeric")
// The slug is operator-chosen rather than random because it is printed on
// support pages and typed by people. First and last characters are
// alphanumeric so a page id never reads as a flag or a trailing separator.
var pageIDRe = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{1,38}[a-z0-9]$`)
func ValidatePageID(id string) error {
if !pageIDRe.MatchString(id) {
return ErrInvalidPageID
}
return nil
}
func statusCacheKey(instanceID, pageID string) string {
return "vantage:status:" + instanceID + ":" + pageID
}
func spCtx() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 5*time.Second)
}
+42 -1
View File
@@ -1,6 +1,9 @@
package services
import "testing"
import (
"strings"
"testing"
)
// A new instance-scoped collection that is not in ScopedCollections leaves its
// rows behind when the instance is deleted. This is the cheapest possible
@@ -20,3 +23,41 @@ func TestStatusCollectionsAreScoped(t *testing.T) {
}
}
}
func TestValidatePageID(t *testing.T) {
valid := []string{"api", "prod-eu", "status2", "a1b", strings.Repeat("a", 40)}
for _, s := range valid {
if err := ValidatePageID(s); err != nil {
t.Errorf("ValidatePageID(%q) = %v, want nil", s, err)
}
}
invalid := []string{
"", // empty
"ab", // too short
strings.Repeat("a", 41), // too long
"-api", // leading hyphen
"api-", // trailing hyphen
"API", // uppercase
"my page", // space
"api_v2", // underscore
"api/v2", // path separator
"..", // dots
}
for _, s := range invalid {
if err := ValidatePageID(s); err == nil {
t.Errorf("ValidatePageID(%q) = nil, want error", s)
}
}
}
func TestStatusCacheKeyIsScopedByInstance(t *testing.T) {
a := statusCacheKey("inst-a", "api")
b := statusCacheKey("inst-b", "api")
if a == b {
t.Fatalf("two instances share a cache key: %q", a)
}
if a != "vantage:status:inst-a:api" {
t.Fatalf("statusCacheKey = %q, want vantage:status:inst-a:api", a)
}
}