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

@@ -3,6 +3,8 @@ package bm_test
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -57,6 +59,7 @@ var pinnedTools = []string{
"bm_manage_create_class_session",
"bm_manage_create_course",
"bm_manage_create_deadline",
"bm_manage_create_material_upload",
"bm_manage_create_meeting",
"bm_manage_create_reading",
"bm_manage_delete_class_session",
@@ -374,12 +377,13 @@ func testManageTools(t *testing.T, tools []pact.MCPTool) {
// manageToolCount is the number of management tools: 28 record and
// material tools plus the material file tools.
const manageToolCount = 30
const manageToolCount = 31
// fileBodyKeys are the body keys of each material file tool, exactly.
var fileBodyKeys = map[string]map[string]bool{
"manage_attach_material_file": {"name": false, "mime": true, "content_base64": false},
"manage_detach_material_file": {},
"manage_attach_material_file": {"name": false, "mime": true, "content_base64": false},
"manage_detach_material_file": {},
"manage_create_material_upload": {},
}
// testFileTool (D-10, D-11, D-17) checks one material file tool: its body
@@ -414,6 +418,13 @@ func testFileTool(t *testing.T, tool pact.MCPTool, fields map[string]bool) {
if !idPath {
t.Errorf("%s: no id path field", tool.Name)
}
if tool.Name == "manage_create_material_upload" {
for _, s := range []string{"10 minutes", "curl", "50 MB", "once"} {
if !strings.Contains(tool.Description, s) {
t.Errorf("%s: description does not mention %q", tool.Name, s)
}
}
}
if tool.Name == "manage_detach_material_file" {
if tool.Method != http.MethodDelete || (tool.Destructive != nil && !*tool.Destructive) {
t.Errorf("%s is not destructive (method %s)", tool.Name, tool.Method)
@@ -628,6 +639,23 @@ func TestManageBucketKey(t *testing.T) {
if got := manage.Key(httptest.NewRequest(http.MethodGet, "/", nil)); !strings.HasPrefix(got, "jz.bm.manage.ip|") {
t.Errorf("bare key = %q, want the IP", got)
}
ip, ok := buckets["jz.bm.upload-ip"]
if !ok || ip.Max != 10 {
t.Errorf("jz.bm.upload-ip declared %v with Max %d, want 10", ok, ip.Max)
} else if got := ip.Key(httptest.NewRequest(http.MethodPost, "/", nil)); !strings.HasPrefix(got, "jz.bm.upload.ip|") {
t.Errorf("upload IP key = %q", got)
}
ticket, ok := buckets["jz.bm.upload-ticket"]
if !ok || ticket.Max != 3 {
t.Errorf("jz.bm.upload-ticket declared %v with Max %d, want 3", ok, ticket.Max)
} else {
r := httptest.NewRequest(http.MethodPost, "/", nil)
r.SetPathValue("ticket", "secret-ticket.mac")
sum := sha256.Sum256([]byte("secret-ticket.mac"))
if got, want := ticket.Key(r), "jz.bm.upload.ticket|"+hex.EncodeToString(sum[:])[:32]; got != want {
t.Errorf("upload ticket key = %q, want %q", got, want)
}
}
token, ok := buckets["jz.bm.token"]
if !ok {
t.Fatal("jz.bm.token is not declared")