212 lines
6.5 KiB
Go
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)
|
|
}
|
|
}
|