Files
mesh-catalog/modules/brother/cmd/brother/objects_test.go
T

212 lines
6.5 KiB
Go

package main
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"sync"
"testing"
"time"
)
// memObjects is the bucket in memory, for the tests of what the device process keeps.
type memObjects struct {
mu sync.Mutex
kept map[string][]byte
puts int
// corrupt, when set, makes a GET of a link answer bytes that are not the kept ones.
corrupt bool
// links records each link signed: "<method> <key> <seconds>".
links []string
}
func newMemObjects() *memObjects { return &memObjects{kept: map[string][]byte{}} }
func (m *memObjects) Bucket() string { return "brother-test" }
func (m *memObjects) Has(_ context.Context, key string) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
_, ok := m.kept[key]
return ok, nil
}
func (m *memObjects) Put(_ context.Context, key, _ string, data []byte) error {
m.mu.Lock()
defer m.mu.Unlock()
m.kept[key] = append([]byte(nil), data...)
m.puts++
return nil
}
func (m *memObjects) Get(_ context.Context, key string) ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
b, ok := m.kept[key]
if !ok {
return nil, fmt.Errorf("%s is not kept", key)
}
return b, nil
}
func (m *memObjects) List(_ context.Context, prefix string) ([]string, error) {
var out []string
for _, k := range m.keys() {
if strings.HasPrefix(k, prefix) {
out = append(out, k)
}
}
sort.Strings(out)
return out, nil
}
func (m *memObjects) Delete(_ context.Context, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.kept, key)
return nil
}
// Link is a link the bucket in memory serves itself (RoundTrip): the key in the path and the life in the query, as a
// presigned link says them (X-Amz-Expires).
func (m *memObjects) Link(method, key string, expires time.Duration) (string, error) {
m.mu.Lock()
m.links = append(m.links, method+" "+key+" "+strconv.Itoa(int(expires/time.Second)))
m.mu.Unlock()
return "http://" + m.Bucket() + ".objects.test/" + key + "?X-Amz-Expires=" + strconv.Itoa(int(expires/time.Second)), nil
}
// RoundTrip answers a GET of a link made by Link with the object kept under its key.
func (m *memObjects) RoundTrip(r *http.Request) (*http.Response, error) {
m.mu.Lock()
b, ok := m.kept[strings.TrimPrefix(r.URL.Path, "/")]
corrupt := m.corrupt
m.mu.Unlock()
if corrupt && ok {
b = append(append([]byte(nil), b...), 0)
}
status := 200
if !ok {
status = 404
}
return &http.Response{StatusCode: status, Body: io.NopCloser(bytes.NewReader(b)), ContentLength: int64(len(b)),
Header: http.Header{}, Request: r}, nil
}
func (m *memObjects) keys() []string {
m.mu.Lock()
defer m.mu.Unlock()
var k []string
for key := range m.kept {
k = append(k, key)
}
return k
}
// 1453/AC8: the S3 client puts, heads and gets path-style under a Signature Version 4 header, and Store keeps
// a key once.
func TestTheBucketIsSpokenToPathStyleSignedAndKeptOnce(t *testing.T) {
var mu sync.Mutex
objects := map[string][]byte{}
var puts int
var auth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
auth = r.Header.Get("Authorization")
if !strings.HasPrefix(r.URL.Path, "/scans/") {
http.Error(w, "path-style: /<bucket>/<key>", http.StatusBadRequest)
return
}
key := strings.TrimPrefix(r.URL.Path, "/scans/")
switch r.Method {
case http.MethodPut:
b, _ := io.ReadAll(r.Body)
objects[key] = b
puts++
case http.MethodHead, http.MethodGet:
b, ok := objects[key]
if !ok {
http.NotFound(w, r)
return
}
if r.Method == http.MethodGet {
w.Write(b)
}
}
}))
defer srv.Close()
host := strings.TrimPrefix(srv.URL, "http://")
at, port, _ := strings.Cut(host, ":")
s3 := NewS3(StoreFacts{Scheme: "http", At: at, Port: port, Bucket: "scans", Region: "us-east-1", Key: "AKID", Secret: "sek"})
ctx := context.Background()
for i := 0; i < 2; i++ {
if err := Store(ctx, s3, "abc.pdf", "application/pdf", []byte("%PDF-x")); err != nil {
t.Fatal(err)
}
}
if puts != 1 {
t.Errorf("%d puts, want the one: a key is kept once", puts)
}
got, err := s3.Get(ctx, "abc.pdf")
if err != nil || string(got) != "%PDF-x" {
t.Errorf("Get = %q, %v", got, err)
}
if !strings.HasPrefix(auth, "AWS4-HMAC-SHA256 Credential=AKID/") || !strings.Contains(auth, "SignedHeaders=") || strings.Contains(auth, "sek") {
t.Errorf("Authorization = %q", auth)
}
if _, err := s3.Get(ctx, "none.pdf"); err == nil {
t.Errorf("a key not kept was got")
}
}
// 1453: the store's facts are read from store.json, and one that names no bucket or key is refused by name.
func TestTheStoresFactsAreRead(t *testing.T) {
p := filepath.Join(t.TempDir(), "store.json")
os.WriteFile(p, []byte(`{"at":"minio.example","port":"9000","bucket":"brother","key":"k","secret":"s"}`), 0o600)
f, err := ReadStoreFacts(p)
if err != nil || f.Scheme != "http" || f.Region != "us-east-1" || f.Bucket != "brother" {
t.Errorf("facts = %+v, %v", f, err)
}
os.WriteFile(p, []byte(`{"at":"x"}`), 0o600)
if _, err := ReadStoreFacts(p); err == nil || !strings.Contains(err.Error(), "bucket") {
t.Errorf("error = %v", err)
}
if _, err := ReadStoreFacts(""); err == nil || !strings.Contains(err.Error(), "BROTHER_STORE") {
t.Errorf("error = %v", err)
}
}
// 1538: the bucket lists keys by prefix (ListObjectsV2, signed, path-style) and deletes one.
func TestTheBucketListsByPrefixAndDeletes(t *testing.T) {
var gotQuery, gotDelete string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
gotQuery = r.URL.RawQuery
w.Write([]byte(`<?xml version="1.0"?><ListBucketResult><Contents><Key>pending/a/manifest.json</Key></Contents><Contents><Key>pending/b/manifest.json</Key></Contents></ListBucketResult>`))
case http.MethodDelete:
gotDelete = r.URL.Path
w.WriteHeader(http.StatusNoContent)
}
}))
defer srv.Close()
at, port, _ := strings.Cut(strings.TrimPrefix(srv.URL, "http://"), ":")
s3 := NewS3(StoreFacts{Scheme: "http", At: at, Port: port, Bucket: "scans", Region: "us-east-1", Key: "AKID", Secret: "sek"})
keys, err := s3.List(context.Background(), "pending/")
if err != nil || len(keys) != 2 || keys[1] != "pending/b/manifest.json" || !strings.Contains(gotQuery, "prefix=pending%2F") || !strings.Contains(gotQuery, "list-type=2") {
t.Errorf("List = %v, %v (query %q)", keys, err, gotQuery)
}
if err := s3.Delete(context.Background(), "pending/a/001.jpg"); err != nil || gotDelete != "/scans/pending/a/001.jpg" {
t.Errorf("Delete: %v, %q", err, gotDelete)
}
}