test: expect signed one-time material uploads

- TestUploadTicket: HMAC under the jz.bm.upload.v1: label, expiry,
  malformed and foreign tickets, single use through the cache
- TestSignedMaterialUploadRefusesBadTickets, TestCreateMaterialUploadNeedsCaller
- TestRoutes, TestBMMCP and TestManageBucketKey expect the upload-url
  route, the uploads route with its two buckets and the upload tool
- compile-only stubs
This commit is contained in:
2026-10-28 15:07:30 +01:00
parent 93066c63ea
commit ce6ac0a78e
7 changed files with 364 additions and 4 deletions

View File

@@ -1,12 +1,16 @@
package api_test
import (
"context"
"encoding/base64"
"net/http"
"strings"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/cooler"
"git.golem15.com/jakub/sm-bm-plugin/classes"
"git.golem15.com/jakub/sm-bm-plugin/controllers/api"
"gorm.io/gorm"
)
@@ -88,3 +92,78 @@ func TestAttachMaterialInlineNoDatabase(t *testing.T) {
t.Error("the database was looked up for an invalid body")
}
}
// TestSignedMaterialUploadRefusesBadTickets (D-12): a malformed, forged,
// never issued, expired, wrong-kind or used ticket gets the same 403 before
// the database is looked up.
func TestSignedMaterialUploadRefusesBadTickets(t *testing.T) {
key := []byte("app-key")
cache, err := cooler.NewMemoryCache("test:", 100)
if err != nil {
t.Fatal(err)
}
dbLooked := false
h := &api.Handlers{
DB: func() *gorm.DB { dbLooked = true; return nil },
UploadKey: func() []byte { return key },
Cache: func() cooler.Cache { return cache },
}
now := time.Now()
forged, _, err := classes.IssueUpload(context.Background(), cache, []byte("other-key"), "material", 1, 7, now)
if err != nil {
t.Fatal(err)
}
unissued, err := classes.SignUpload(key, classes.UploadTicket{Kind: "material", Target: 1, User: 7, Expires: now.Add(time.Minute), Nonce: strings.Repeat("0f", 16)})
if err != nil {
t.Fatal(err)
}
expired, _, err := classes.IssueUpload(context.Background(), cache, key, "material", 1, 7, now.Add(-classes.UploadTTL-time.Second))
if err != nil {
t.Fatal(err)
}
deck, _, err := classes.IssueUpload(context.Background(), cache, key, "deck", 1, 7, now)
if err != nil {
t.Fatal(err)
}
refused := map[string]string{
"empty": "",
"garbage": "not-a-ticket",
"two parts": "abc.def",
"forged": forged,
"unissued": unissued,
"expired": expired,
"wrong kind": deck,
}
for name, ticket := range refused {
code, body := serve(t, h.SignedMaterialUpload, http.MethodPost, "/", nil, map[string]string{"ticket": ticket})
if code != http.StatusForbidden || body["error"] != true || body["message"] != "Invalid or expired upload URL." {
t.Errorf("%s: %d %v, want 403 Invalid or expired upload URL.", name, code, body)
}
}
if dbLooked {
t.Error("the database was looked up for a refused ticket")
}
valid, _, err := classes.IssueUpload(context.Background(), cache, key, "material", 1, 7, now)
if err != nil {
t.Fatal(err)
}
if code, body := serve(t, h.SignedMaterialUpload, http.MethodPost, "/", nil, map[string]string{"ticket": valid}); code != http.StatusServiceUnavailable {
t.Errorf("valid ticket without a database = %d %v, want 503", code, body)
}
if code, body := serve(t, h.SignedMaterialUpload, http.MethodPost, "/", nil, map[string]string{"ticket": valid}); code != http.StatusForbidden {
t.Errorf("used ticket = %d %v, want 403", code, body)
}
}
// TestCreateMaterialUploadNeedsCaller: no signed-in user is a 401; a caller
// without a database is a 503.
func TestCreateMaterialUploadNeedsCaller(t *testing.T) {
h := noDB()
if code, body := serve(t, h.CreateMaterialUpload, http.MethodPost, "/", nil, map[string]string{"id": "1"}); code != http.StatusUnauthorized || body["error"] != true {
t.Errorf("without a caller = %d %v, want 401", code, body)
}
if code, body := serve(t, h.CreateMaterialUpload, http.MethodPost, "/", &bouncer.Principal{ID: 7}, map[string]string{"id": "1"}); code != http.StatusServiceUnavailable {
t.Errorf("without a database = %d %v, want 503", code, body)
}
}