- 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
170 lines
6.8 KiB
Go
170 lines
6.8 KiB
Go
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"
|
|
)
|
|
|
|
// TestDecodeInline (D-11, D-18) checks the body of POST
|
|
// /materials/{id}/files/inline before any database work.
|
|
func TestDecodeInline(t *testing.T) {
|
|
pdf := []byte("%PDF-1.4\n%notatki\n")
|
|
enc := base64.StdEncoding.EncodeToString(pdf)
|
|
|
|
t.Run("valid", func(t *testing.T) {
|
|
name, mime, data, errs := api.DecodeInline([]byte(`{"name":" notatki.pdf ","mime":"Application/PDF","content_base64":"` + enc + `"}`))
|
|
if errs != nil {
|
|
t.Fatalf("errors %v", errs)
|
|
}
|
|
if name != "notatki.pdf" || mime != "application/pdf" || string(data) != string(pdf) {
|
|
t.Errorf("got %q %q %q", name, mime, data)
|
|
}
|
|
})
|
|
t.Run("mime optional", func(t *testing.T) {
|
|
_, mime, data, errs := api.DecodeInline([]byte(`{"name":"a.pdf","content_base64":"` + enc + `"}`))
|
|
if errs != nil || mime != "" || len(data) != len(pdf) {
|
|
t.Errorf("got mime %q, %d bytes, errors %v", mime, len(data), errs)
|
|
}
|
|
})
|
|
|
|
big := strings.Repeat("A", base64.StdEncoding.EncodedLen(api.MaterialInlineMaxBytes)+4)
|
|
cases := []struct {
|
|
name, body, field, contains string
|
|
}{
|
|
{"missing name", `{"content_base64":"` + enc + `"}`, "name", ""},
|
|
{"blank name", `{"name":" ","content_base64":"` + enc + `"}`, "name", ""},
|
|
{"empty content", `{"name":"a.pdf","content_base64":""}`, "content_base64", ""},
|
|
{"missing content", `{"name":"a.pdf"}`, "content_base64", ""},
|
|
{"invalid base64", `{"name":"a.pdf","content_base64":"%%%not base64"}`, "content_base64", "base64"},
|
|
{"url-safe alphabet", `{"name":"a.pdf","content_base64":"-_-_"}`, "content_base64", "base64"},
|
|
{"unknown key", `{"name":"a.pdf","content_base64":"` + enc + `","path":"/etc/passwd"}`, "json", ""},
|
|
{"not an object", `[1,2]`, "json", ""},
|
|
{"bad mime", `{"name":"a.pdf","mime":"pdf","content_base64":"` + enc + `"}`, "mime", ""},
|
|
{"too large", `{"name":"a.pdf","content_base64":"` + big + `"}`, "content_base64", "bm_manage_create_material_upload"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
_, _, data, errs := api.DecodeInline([]byte(tc.body))
|
|
if data != nil {
|
|
t.Errorf("data returned with an invalid body")
|
|
}
|
|
msgs := errs[tc.field]
|
|
if len(msgs) == 0 {
|
|
t.Fatalf("errors %v have no %s entry", errs, tc.field)
|
|
}
|
|
if tc.contains != "" && !strings.Contains(msgs[0], tc.contains) {
|
|
t.Errorf("%s message %q does not name %q", tc.field, msgs[0], tc.contains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestAttachMaterialInlineNoDatabase: an invalid body is a 422 before the
|
|
// database is looked up; a valid one without a database is a 503.
|
|
func TestAttachMaterialInlineNoDatabase(t *testing.T) {
|
|
user := &bouncer.Principal{ID: 7}
|
|
enc := base64.StdEncoding.EncodeToString([]byte("%PDF-1.4\n"))
|
|
valid := bodyRequest{noDB().AttachMaterialInline, http.MethodPost, "/", `{"name":"a.pdf","content_base64":"` + enc + `"}`}
|
|
if code, body := serveBody(t, valid, user); code != http.StatusServiceUnavailable {
|
|
t.Errorf("valid body without a database = %d %v, want 503", code, body)
|
|
}
|
|
dbLooked := false
|
|
h := &api.Handlers{DB: func() *gorm.DB { dbLooked = true; return nil }}
|
|
invalid := bodyRequest{h.AttachMaterialInline, http.MethodPost, "/", `{"name":"","content_base64":""}`}
|
|
code, body := serveBody(t, invalid, user)
|
|
if code != http.StatusUnprocessableEntity {
|
|
t.Errorf("invalid body = %d %v, want 422", code, body)
|
|
}
|
|
if errs, _ := body["errors"].(map[string]any); errs["name"] == nil || errs["content_base64"] == nil {
|
|
t.Errorf("errors %v lack name and content_base64", body["errors"])
|
|
}
|
|
if dbLooked {
|
|
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)
|
|
}
|
|
}
|