package onlyoffice
import (
"bytes"
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestMinioObjectKey(t *testing.T) {
cases := []struct {
fileID string
folderID string
want string
}{
{"3785", "652", "00/00/01/files/folder_652/file_3785/v1/content.pdf"},
{"1", "2", "00/00/01/files/folder_2/file_1/v1/content.pdf"},
{"3666", "4000", "00/00/01/files/folder_4000/file_3666/v1/content.pdf"},
}
for _, tc := range cases {
if got := minioObjectKey(tc.fileID, tc.folderID); got != tc.want {
t.Errorf("minioObjectKey(%q, %q) = %q, want %q", tc.fileID, tc.folderID, got, tc.want)
}
}
}
func TestMinioObjectKeyFromURL(t *testing.T) {
cases := []struct {
name string
url string
bucket string
want string
ok bool
}{
{
name: "path style drops bucket segment",
url: "https://s3.us-east-1.amazonaws.com/office/00/00/01/files/folder_4000/file_3785/v1/content.pdf?AWSAccessKeyId=minio",
bucket: "office",
want: "00/00/01/files/folder_4000/file_3785/v1/content.pdf",
ok: true,
},
{
name: "doubled bucket segment (portal serviceurl includes bucket)",
url: "https://s3.us-east-1.amazonaws.com/office/office/00/00/01/files/folder_4000/file_3785/v1/content.pdf?AWSAccessKeyId=minio",
bucket: "office",
want: "00/00/01/files/folder_4000/file_3785/v1/content.pdf",
ok: true,
},
{
name: "virtual host style keeps path",
url: "https://office.s3.us-east-1.amazonaws.com/00/00/01/files/folder_4000/file_3785/v1/content.pdf",
bucket: "office",
want: "00/00/01/files/folder_4000/file_3785/v1/content.pdf",
ok: true,
},
{
name: "foreign first segment kept",
url: "https://example.com/other/file_1/v1/content.pdf",
bucket: "office",
want: "other/file_1/v1/content.pdf",
ok: true,
},
{name: "empty path", url: "https://example.com", bucket: "office", ok: false},
{name: "traversal", url: "https://example.com/office/../etc/passwd", bucket: "office", ok: false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, ok := minioObjectKeyFromURL(tc.url, tc.bucket)
if ok != tc.ok || got != tc.want {
t.Errorf("minioObjectKeyFromURL(%q, %q) = (%q, %v), want (%q, %v)", tc.url, tc.bucket, got, ok, tc.want, tc.ok)
}
})
}
}
func TestIsStaleS3Redirect(t *testing.T) {
cases := []struct {
name string
url string
body []byte
want bool
}{
{
name: "aws redirect with minio access key",
url: "https://s3.us-east-1.amazonaws.com/office/x/file_1?AWSAccessKeyId=minio&Expires=1",
want: true,
},
{
name: "aws redirect with minio x-amz-credential",
url: "https://office.s3.us-east-1.amazonaws.com/00/00/01/files/folder_1/file_1?X-Amz-Credential=minio%2F20260914",
want: true,
},
{
name: "invalid access key xml body",
url: "https://portal.internal/download/1",
body: []byte(`InvalidAccessKeyIdminio`),
want: true,
},
{
name: "regular pdf from portal",
url: "https://portal.internal/download/1",
body: []byte("%PDF-1.7 data"),
want: false,
},
{
name: "aws redirect with foreign key",
url: "https://s3.us-east-1.amazonaws.com/office/x?AWSAccessKeyId=other",
want: false,
},
{
name: "amazonaws in path but foreign host",
url: "https://example.com/amazonaws.com/file?AWSAccessKeyId=minio",
want: false,
},
{
name: "empty",
want: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := isStaleS3Redirect(tc.url, tc.body); got != tc.want {
t.Errorf("isStaleS3Redirect(%q, %q) = %v, want %v", tc.url, tc.body, got, tc.want)
}
})
}
}
const staleS3Body = `` +
`InvalidAccessKeyId` +
`The AWS Access Key Id you provided does not exist in our records.` +
`minio`
func TestDownloadFileMinioFallback(t *testing.T) {
const payload = "PDFDATA-3785"
portal := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/2.0/files/file/3785.json":
w.Header().Set("Content-Type", "application/json")
io.WriteString(w, `{"response":{"id":3785,"title":"04.pdf","folderId":655,"viewUrl":"/download/3785"}}`)
case "/download/3785":
http.Redirect(w, r, "/office/00/00/01/files/folder_4000/file_3785/v1/content.pdf?AWSAccessKeyId=minio", http.StatusTemporaryRedirect)
case "/office/00/00/01/files/folder_4000/file_3785/v1/content.pdf":
w.WriteHeader(http.StatusForbidden)
io.WriteString(w, staleS3Body)
default:
http.NotFound(w, r)
}
}))
defer portal.Close()
var minioPath, minioAuth string
minio := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
minioPath, minioAuth = r.URL.Path, r.Header.Get("Authorization")
io.WriteString(w, payload)
}))
defer minio.Close()
t.Setenv("MINIO_ENDPOINT", minio.URL)
t.Setenv("MINIO_BUCKET", "office")
t.Setenv("MINIO_ACCESS_KEY", "testkey")
t.Setenv("MINIO_SECRET_KEY", "testsecret")
c := &Client{
client: portal.Client(),
credentials: &Credentials{Url: portal.URL},
token: &Token{Value: "Bearer test", Expires: Time(time.Now().Add(time.Hour))},
}
var buf bytes.Buffer
n, err := c.DownloadFile(context.Background(), "3785", &buf)
if err != nil {
t.Fatalf("DownloadFile: %v", err)
}
if n != int64(len(payload)) || buf.String() != payload {
t.Fatalf("got %d bytes %q, want %d bytes %q", n, buf.String(), len(payload), payload)
}
if want := "/office/00/00/01/files/folder_4000/file_3785/v1/content.pdf"; minioPath != want {
t.Errorf("minio path = %q, want %q", minioPath, want)
}
if !strings.HasPrefix(minioAuth, "AWS4-HMAC-SHA256") {
t.Errorf("minio request not SigV4-signed; Authorization=%q", minioAuth)
}
}
func TestDownloadFileMinioFallbackWithoutCreds(t *testing.T) {
portal := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/2.0/files/file/3785.json":
io.WriteString(w, `{"response":{"id":3785,"folderId":652,"viewUrl":"/download/3785"}}`)
default:
w.WriteHeader(http.StatusForbidden)
io.WriteString(w, staleS3Body)
}
}))
defer portal.Close()
t.Setenv("MINIO_ACCESS_KEY", "")
t.Setenv("MINIO_SECRET_KEY", "")
c := &Client{
client: portal.Client(),
credentials: &Credentials{Url: portal.URL},
token: &Token{Value: "Bearer test", Expires: Time(time.Now().Add(time.Hour))},
}
_, err := c.DownloadFile(context.Background(), "3785", io.Discard)
if err == nil {
t.Fatal("expected error without minio credentials")
}
if !strings.Contains(err.Error(), "MINIO_ACCESS_KEY/MINIO_SECRET_KEY not set") {
t.Fatalf("unexpected error: %v", err)
}
}