package oss import ( "bytes" "context" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/tinode/chat/server/media" "github.com/tinode/chat/server/store" "github.com/tinode/chat/server/store/types" ) func TestMinIOUploadDownloadDelete(t *testing.T) { const body = "hello minio" var stored []byte var deleted bool server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, "/xgjs/public/") { t.Errorf("unexpected request path: %s", r.URL.Path) http.Error(w, "wrong path", http.StatusNotFound) return } switch r.Method { case http.MethodPut: stored, _ = io.ReadAll(r.Body) if strings.Contains(r.Header.Get("Content-Encoding"), "aws-chunked") { separator := bytes.Index(stored, []byte("\r\n")) if separator < 0 || len(stored) < separator+2+len(body) { t.Errorf("invalid signed chunked request") http.Error(w, "invalid body", http.StatusBadRequest) return } stored = stored[separator+2 : separator+2+len(body)] } w.Header().Set("ETag", `"test-etag"`) case http.MethodGet: w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) w.Header().Set("ETag", `"test-etag"`) if r.Header.Get("Range") == "bytes=6-10" { w.WriteHeader(http.StatusPartialContent) _, _ = w.Write(stored[6:]) return } _, _ = w.Write(stored) case http.MethodDelete: deleted = true w.WriteHeader(http.StatusNoContent) default: t.Errorf("unexpected request method: %s", r.Method) http.Error(w, "wrong method", http.StatusMethodNotAllowed) } })) defer server.Close() previous := store.ThirdPartyConfig store.ThirdPartyConfig = fakeThirdPartyConfig{ "oss_enabled": "0", "minio_endpoint": server.URL, "minio_bucket": "xgjs", "minio_region": "us-east-1", "minio_access_key_id": "test-key", "minio_secret_access_key": "test-secret", } defer func() { store.ThirdPartyConfig = previous }() handler := &ossHandler{} if err := handler.Init(`{"provider":"minio","use_sys_config":true}`); err != nil { t.Fatal(err) } if handler.bucket != nil || handler.minio == nil { t.Fatal("MinIO provider should initialize without OSS credentials") } file := &media.PublicFileDef{ID: "file_test", Purpose: "avatar", MimeType: "text/plain", CreatedAt: time.Now()} location, size, err := handler.UploadPublic(file, bytes.NewReader([]byte(body))) if err != nil { t.Fatal(err) } if size != int64(len(body)) || string(stored) != body || !strings.HasPrefix(location, "minio://xgjs/public/avatar/") { t.Fatalf("upload: location=%q size=%d data=%q", location, size, stored) } fd := &types.FileDef{Location: location, Size: size} _, reader, err := handler.downloadMinIO(fd) if err != nil { t.Fatal(err) } defer reader.Close() buf := make([]byte, len(body)) if _, err := io.ReadFull(reader, buf); err != nil || string(buf) != body { t.Fatalf("download: %q, %v", buf, err) } if _, err := reader.Seek(6, io.SeekStart); err != nil { t.Fatal(err) } if got, err := io.ReadAll(reader); err != nil || string(got) != "minio" { t.Fatalf("range download: %q, %v", got, err) } if err := handler.Delete([]string{location}); err != nil || !deleted { t.Fatalf("delete: deleted=%v err=%v", deleted, err) } } func TestMinIOLocationRejectsDifferentBucket(t *testing.T) { handler := &ossHandler{minio: &minioStorage{bucket: "xgjs"}} if _, _, err := handler.minioObject("minio://other/public/file"); err != types.ErrMalformed { t.Fatalf("wrong bucket should be rejected, got %v", err) } } func TestMinIOPublicEndpointUsesHTTPSPathStyle(t *testing.T) { storage, err := newMinIOStorage(minioConfig{ Endpoint: "https://minio.118im.com", Bucket: "xgjs", Region: "us-east-1", AccessKeyID: "test-key", SecretAccessKey: "test-secret", }) if err != nil { t.Fatal(err) } url, err := storage.client.PresignedGetObject(context.Background(), storage.bucket, "images/test.jpg", time.Minute, nil) if err != nil { t.Fatal(err) } if url.Scheme != "https" || url.Host != "minio.118im.com" || url.Path != "/xgjs/images/test.jpg" { t.Fatalf("unexpected MinIO URL: %s", url) } }