diff --git a/client/client.go b/client/client.go index 74d4b0fd..1cdc0166 100644 --- a/client/client.go +++ b/client/client.go @@ -79,6 +79,27 @@ type CSAPI struct { txnID int64 } +// CreateMedia creates an MXC URI for asynchronous media uploads. +func (c *CSAPI) CreateMedia(t TestLike) string { + t.Helper() + res := c.MustDo(t, "POST", []string{"_matrix", "media", "v1", "create"}) + body := ParseJSON(t, res) + return GetJSONFieldStr(t, body, "content_uri") +} + +// UploadMediaAsync uploads the provided content to the given server and media ID. Fails the test on error. +func (c *CSAPI) UploadMediaAsync(t TestLike, serverName, mediaID string, fileBody []byte, fileName string, contentType string) { + t.Helper() + query := url.Values{} + if fileName != "" { + query.Set("filename", fileName) + } + c.MustDo( + t, "PUT", []string{"_matrix", "media", "v3", "upload", serverName, mediaID}, + WithRawBody(fileBody), WithContentType(contentType), WithQueries(query), + ) +} + // UploadContent uploads the provided content with an optional file name. Fails the test on error. Returns the MXC URI. func (c *CSAPI) UploadContent(t TestLike, fileBody []byte, fileName string, contentType string) string { t.Helper() diff --git a/tests/csapi/media_async_uploads_test.go b/tests/csapi/media_async_uploads_test.go new file mode 100644 index 00000000..901caa45 --- /dev/null +++ b/tests/csapi/media_async_uploads_test.go @@ -0,0 +1,73 @@ +package csapi_tests + +import ( + "bytes" + "net/http" + "strings" + "testing" + + "github.com/matrix-org/complement" + "github.com/matrix-org/complement/client" + "github.com/matrix-org/complement/helpers" + "github.com/matrix-org/complement/internal/data" + "github.com/matrix-org/complement/match" + "github.com/matrix-org/complement/must" + "github.com/matrix-org/complement/runtime" +) + +func TestAsyncUpload(t *testing.T) { + runtime.SkipIf(t, runtime.Dendrite) // Dendrite doesn't support async uploads + + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + + var mxcURI, mediaID string + t.Run("Create media", func(t *testing.T) { + mxcURI = alice.CreateMedia(t) + parts := strings.Split(mxcURI, "/") + mediaID = parts[len(parts)-1] + }) + + origin, mediaID := client.SplitMxc(mxcURI) + + t.Run("Not yet uploaded", func(t *testing.T) { + // Check that the media is not yet uploaded + res := alice.Do(t, "GET", []string{"_matrix", "media", "v3", "download", origin, mediaID}) + must.MatchResponse(t, res, match.HTTPResponse{ + StatusCode: http.StatusGatewayTimeout, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_NOT_YET_UPLOADED"), + match.JSONKeyEqual("error", "Media has not been uploaded yet"), + }, + }) + }) + + wantContentType := "image/png" + + t.Run("Upload media", func(t *testing.T) { + alice.UploadMediaAsync(t, origin, mediaID, data.MatrixPng, "test.png", wantContentType) + }) + + t.Run("Cannot upload to a media ID that has already been uploaded to", func(t *testing.T) { + res := alice.Do(t, "PUT", []string{"_matrix", "media", "v3", "upload", origin, mediaID}) + must.MatchResponse(t, res, match.HTTPResponse{ + StatusCode: http.StatusConflict, + JSON: []match.JSON{ + match.JSONKeyEqual("errcode", "M_CANNOT_OVERWRITE_MEDIA"), + match.JSONKeyEqual("error", "Media ID already has content"), + }, + }) + }) + + t.Run("Download media", func(t *testing.T) { + content, contentType := alice.DownloadContent(t, mxcURI) + if !bytes.Equal(data.MatrixPng, content) { + t.Fatalf("uploaded and downloaded content doesn't match: want %v\ngot\n%v", data.MatrixPng, content) + } + if contentType != wantContentType { + t.Fatalf("expected contentType to be %s, got %s", wantContentType, contentType) + } + }) +}