diff --git a/test/deps.go b/test/test_internals/deps.go
similarity index 82%
rename from test/deps.go
rename to test/test_internals/deps.go
index 611f1bb935a1ea1209ce5bf335da1e799003b8f7..7691d1ee19041c4697bc08931779f5e971fb5c90 100644
--- a/test/deps.go
+++ b/test/test_internals/deps.go
@@ -1,4 +1,4 @@
-package test
+package test_internals
 
 import (
 	"context"
@@ -17,9 +17,10 @@ type ContainerDeps struct {
 	ctx            context.Context
 	pgContainer    *postgres.PostgresContainer
 	redisContainer testcontainers.Container
-	homeservers    []*SynapseDep
-	machines       []*mmrContainer
 	depNet         *NetworkDep
+
+	Homeservers []*SynapseDep
+	Machines    []*mmrContainer
 }
 
 func MakeTestDeps() (*ContainerDeps, error) {
@@ -54,10 +55,12 @@ func MakeTestDeps() (*ContainerDeps, error) {
 	if err != nil {
 		return nil, err
 	}
-	pgConnStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
+	pgHost, err := pgContainer.ContainerIP(ctx)
 	if err != nil {
 		return nil, err
 	}
+	// we can hardcode the port and most of the connection details because we're behind the docker network here
+	pgConnStr := fmt.Sprintf("host=%s port=5432 user=postgres password=test1234 dbname=mmr sslmode=disable", pgHost)
 
 	// Start a redis container
 	cwd, err := os.Getwd()
@@ -89,32 +92,35 @@ func MakeTestDeps() (*ContainerDeps, error) {
 		Homeservers: []mmrHomeserverTmplArgs{
 			{
 				ServerName:         syn1.ServerName,
-				ClientServerApiUrl: syn1.ClientServerApiUrl,
+				ClientServerApiUrl: syn1.InternalClientServerApiUrl,
 			},
 			{
 				ServerName:         syn2.ServerName,
-				ClientServerApiUrl: syn2.ClientServerApiUrl,
+				ClientServerApiUrl: syn2.InternalClientServerApiUrl,
 			},
 		},
 		RedisAddr:          fmt.Sprintf("%s:%d", redisHost, 6379), // we're behind the network for redis
 		PgConnectionString: pgConnStr,
 	})
+	if err != nil {
+		return nil, err
+	}
 
 	return &ContainerDeps{
 		ctx:            ctx,
 		pgContainer:    pgContainer,
 		redisContainer: redisContainer,
-		homeservers:    []*SynapseDep{syn1, syn2},
-		machines:       mmrs,
+		Homeservers:    []*SynapseDep{syn1, syn2},
+		Machines:       mmrs,
 		depNet:         depNet,
 	}, nil
 }
 
 func (c *ContainerDeps) Teardown() {
-	for _, mmr := range c.machines {
+	for _, mmr := range c.Machines {
 		mmr.Teardown()
 	}
-	for _, hs := range c.homeservers {
+	for _, hs := range c.Homeservers {
 		hs.Teardown()
 	}
 	if err := c.redisContainer.Terminate(c.ctx); err != nil {
diff --git a/test/deps_mmr.go b/test/test_internals/deps_mmr.go
similarity index 70%
rename from test/deps_mmr.go
rename to test/test_internals/deps_mmr.go
index 5e6a76148aa3749b0ce871d81430f4f46db9dba9..7ed34144c0aae04fc62c76a84b1f575187e9d71d 100644
--- a/test/deps_mmr.go
+++ b/test/test_internals/deps_mmr.go
@@ -1,7 +1,8 @@
-package test
+package test_internals
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"log"
 	"os"
@@ -35,6 +36,40 @@ type mmrContainer struct {
 	MachineId int
 }
 
+var mmrCachedImage string
+
+func reuseMmrBuild(ctx context.Context) (string, error) {
+	if mmrCachedImage != "" {
+		return mmrCachedImage, nil
+	}
+	log.Println("[Test Deps] Building MMR image...")
+	buildReq := testcontainers.GenericContainerRequest{
+		ContainerRequest: testcontainers.ContainerRequest{
+			FromDockerfile: testcontainers.FromDockerfile{
+				Dockerfile:    "Dockerfile",
+				Context:       ".",
+				PrintBuildLog: true,
+			},
+		},
+		Started: false,
+	}
+	provider, err := buildReq.ProviderType.GetProvider(testcontainers.WithLogger(testcontainers.Logger))
+	if err != nil {
+		return "", err
+	}
+	c, err := provider.CreateContainer(ctx, buildReq.ContainerRequest)
+	if err != nil {
+		return "", err
+	}
+	if dockerC, ok := c.(*testcontainers.DockerContainer); !ok {
+		return "", errors.New("failed to convert built MMR container to a DockerContainer")
+	} else {
+		mmrCachedImage = dockerC.Image
+	}
+	log.Println("[Test Deps] Cached build as ", mmrCachedImage)
+	return mmrCachedImage, nil
+}
+
 func makeMmrInstances(ctx context.Context, count int, depNet *NetworkDep, tmplArgs mmrTmplArgs) ([]*mmrContainer, error) {
 	// Prepare a config template
 	t, err := template.New("mmr.config.yaml").ParseFiles(path.Join(".", "test", "templates", "mmr.config.yaml"))
@@ -61,6 +96,12 @@ func makeMmrInstances(ctx context.Context, count int, depNet *NetworkDep, tmplAr
 		return nil, err
 	}
 
+	// Cache the MMR image
+	mmrImage, err := reuseMmrBuild(ctx)
+	if err != nil {
+		return nil, err
+	}
+
 	// Start the containers (using the same DB and config)
 	mmrs := make([]*mmrContainer, 0)
 	for i := 0; i < count; i++ {
@@ -68,9 +109,7 @@ func makeMmrInstances(ctx context.Context, count int, depNet *NetworkDep, tmplAr
 		p, _ := nat.NewPort("tcp", "8000")
 		container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
 			ContainerRequest: testcontainers.ContainerRequest{
-				FromDockerfile: testcontainers.FromDockerfile{
-					Dockerfile: "Dockerfile",
-				},
+				Image:        mmrImage,
 				ExposedPorts: []string{"8000/tcp"},
 				Mounts: []testcontainers.ContainerMount{
 					testcontainers.BindMount(f.Name(), "/data/media-repo.yaml"),
@@ -98,6 +137,7 @@ func makeMmrInstances(ctx context.Context, count int, depNet *NetworkDep, tmplAr
 		}
 		//goland:noinspection HttpUrlsUsage
 		csApiUrl := fmt.Sprintf("http://%s:%d", mmrHost, mmrPort.Int())
+		log.Println("@@@@@@@@@@@@@@@@@@@@@@ ", csApiUrl)
 
 		// Create the container object
 		mmrs = append(mmrs, &mmrContainer{
diff --git a/test/deps_network.go b/test/test_internals/deps_network.go
similarity index 98%
rename from test/deps_network.go
rename to test/test_internals/deps_network.go
index b9495cf4c9ba19c2257e5af79741cbf93e84f270..a77179d498be96abc07160a7cbf58761fada6618 100644
--- a/test/deps_network.go
+++ b/test/test_internals/deps_network.go
@@ -1,4 +1,4 @@
-package test
+package test_internals
 
 import (
 	"context"
diff --git a/test/deps_synapse.go b/test/test_internals/deps_synapse.go
similarity index 52%
rename from test/deps_synapse.go
rename to test/test_internals/deps_synapse.go
index 9ef346056ba4f2b03fbed1a93807e9bf8c59efd3..cb317126c0615c6ce6df1f7ecf4974984bdc9fdc 100644
--- a/test/deps_synapse.go
+++ b/test/test_internals/deps_synapse.go
@@ -1,9 +1,15 @@
-package test
+package test_internals
 
 import (
+	"bytes"
 	"context"
+	"encoding/json"
+	"errors"
 	"fmt"
+	"io"
 	"log"
+	"net/http"
+	"net/url"
 	"os"
 	"path"
 	"strings"
@@ -28,8 +34,16 @@ type SynapseDep struct {
 	synContainer  testcontainers.Container
 	tmpConfigPath string
 
-	ClientServerApiUrl string
-	ServerName         string
+	InternalClientServerApiUrl string
+	ExternalClientServerApiUrl string
+	ServerName                 string
+
+	AdminUserId                  string
+	AdminAccessToken             string
+	UnprivilegedAliceUserId      string
+	UnprivilegedAliceAccessToken string
+	UnprivilegedBobUserId        string
+	UnprivilegedBobAccessToken   string
 }
 
 type fixNetwork struct {
@@ -121,7 +135,11 @@ func MakeSynapse(domainName string, depNet *NetworkDep) (*SynapseDep, error) {
 	}
 
 	// Prepare the CS API URL
-	synHost, err := synContainer.ContainerIP(ctx)
+	synHost, err := synContainer.Host(ctx)
+	if err != nil {
+		return nil, err
+	}
+	synIp, err := synContainer.ContainerIP(ctx)
 	if err != nil {
 		return nil, err
 	}
@@ -130,16 +148,105 @@ func MakeSynapse(domainName string, depNet *NetworkDep) (*SynapseDep, error) {
 		return nil, err
 	}
 	//goland:noinspection HttpUrlsUsage
-	csApiUrl := fmt.Sprintf("http://%s:%d", synHost, synPort.Int())
+	intCsApiUrl := fmt.Sprintf("http://%s:%d", synIp, 8008)
+	extCsApiUrl := fmt.Sprintf("http://%s:%d", synHost, synPort.Int())
+
+	// Register the accounts
+	registerUser := func(localpart string, admin bool) (string, string, error) { // userId, accessToken, err
+		adminFlag := "--admin"
+		if !admin {
+			adminFlag = "--no-admin"
+		}
+		cmd := fmt.Sprintf("register_new_matrix_user -c /data/homeserver.yaml -u %s -p test1234 %s", localpart, adminFlag)
+		log.Println("[Synapse Command] " + cmd)
+		i, r, err := synContainer.Exec(ctx, strings.Split(cmd, " "))
+		if err != nil {
+			return "", "", err
+		}
+		b, err := io.ReadAll(r)
+		if err != nil {
+			return "", "", err
+		}
+		if i != 0 {
+			return "", "", errors.New(string(b))
+		}
+
+		// Get user ID and access token from admin API
+		log.Println("[Synapse API] Logging in")
+		endpoint, err := url.JoinPath(extCsApiUrl, "/_matrix/client/v3/login")
+		if err != nil {
+			return "", "", err
+		}
+		b, err = json.Marshal(map[string]interface{}{
+			"type": "m.login.password",
+			"identifier": map[string]interface{}{
+				"type": "m.id.user",
+				"user": localpart,
+			},
+			"password":      "test1234",
+			"refresh_token": false,
+		})
+		if err != nil {
+			return "", "", err
+		}
+		res, err := http.DefaultClient.Post(endpoint, "application/json", bytes.NewBuffer(b))
+		if err != nil {
+			return "", "", err
+		}
+		b, err = io.ReadAll(res.Body)
+		if err != nil {
+			return "", "", err
+		}
+		if res.StatusCode != http.StatusOK {
+			return "", "", errors.New(res.Status + "\n" + string(b))
+		}
+		log.Println("[Synapse API] " + string(b))
+		m := make(map[string]interface{})
+		err = json.Unmarshal(b, &m)
+		if err != nil {
+			return "", "", err
+		}
+
+		var userId interface{}
+		var accessToken interface{}
+		var ok bool
+		if userId, ok = m["user_id"]; !ok {
+			return "", "", errors.New("missing user_id")
+		}
+		if accessToken, ok = m["access_token"]; !ok {
+			return "", "", errors.New("missing access_token")
+		}
+
+		return userId.(string), accessToken.(string), nil
+	}
+	adminUserId, adminAccessToken, err := registerUser("admin", true)
+	if err != nil {
+		return nil, err
+	}
+	aliceUserId, aliceAccessToken, err := registerUser("user_alice", false)
+	if err != nil {
+		return nil, err
+	}
+	bobUserId, bobAccessToken, err := registerUser("user_bob", false)
+	if err != nil {
+		return nil, err
+	}
 
 	// Create the dependency
 	return &SynapseDep{
-		ctx:                ctx,
-		pgContainer:        pgContainer,
-		synContainer:       synContainer,
-		tmpConfigPath:      f.Name(),
-		ClientServerApiUrl: csApiUrl,
-		ServerName:         domainName,
+		ctx:                          ctx,
+		pgContainer:                  pgContainer,
+		synContainer:                 synContainer,
+		tmpConfigPath:                f.Name(),
+		InternalClientServerApiUrl:   intCsApiUrl,
+		ExternalClientServerApiUrl:   extCsApiUrl,
+		ServerName:                   domainName,
+		AdminUserId:                  adminUserId,
+		AdminAccessToken:             adminAccessToken,
+		UnprivilegedAliceUserId:      aliceUserId,
+		UnprivilegedAliceAccessToken: aliceAccessToken,
+		UnprivilegedBobUserId:        bobUserId,
+		UnprivilegedBobAccessToken:   bobAccessToken,
 	}, nil
 }
 
diff --git a/test/testcontainers_ext.go b/test/test_internals/testcontainers_ext.go
similarity index 95%
rename from test/testcontainers_ext.go
rename to test/test_internals/testcontainers_ext.go
index 3fccb7fa225a68c8d4d53f070c8204c68d1d175c..f32ea2f331b9911fb5c381fb363bdc46320b5d49 100644
--- a/test/testcontainers_ext.go
+++ b/test/test_internals/testcontainers_ext.go
@@ -1,4 +1,4 @@
-package test
+package test_internals
 
 import "github.com/testcontainers/testcontainers-go"
 
diff --git a/test/test_internals/util.go b/test/test_internals/util.go
new file mode 100644
index 0000000000000000000000000000000000000000..fe0744b9c1c20c1cf963780613038e7975f39899
--- /dev/null
+++ b/test/test_internals/util.go
@@ -0,0 +1,60 @@
+package test_internals
+
+import (
+	"bytes"
+	"fmt"
+	"image"
+	"image/color"
+	"io"
+	"testing"
+
+	"github.com/disintegration/imaging"
+	"github.com/stretchr/testify/assert"
+)
+
+var evenColor = color.RGBA{R: 255, G: 0, B: 0, A: 255}
+var oddColor = color.RGBA{R: 0, G: 255, B: 0, A: 255}
+var altColor = color.RGBA{R: 0, G: 0, B: 255, A: 255}
+
+func colorFor(x int, y int) color.Color {
+	c := oddColor
+	if (y%2.0) == 0 && (x%2.0) == 0 {
+		c = altColor
+	} else if (y%2.0) == 0 || (x%2.0) == 0 {
+		c = evenColor
+	}
+	return c
+}
+
+func MakeTestImage(width int, height int) (string, io.Reader, error) {
+	img := image.NewNRGBA(image.Rect(0, 0, width, height))
+	for x := 0; x < width; x++ {
+		for y := 0; y < height; y++ {
+			c := colorFor(x, y)
+			img.Set(x, y, c)
+		}
+	}
+
+	b := bytes.NewBuffer(make([]byte, 0))
+	err := imaging.Encode(b, img, imaging.PNG)
+	if err != nil {
+		return "", nil, err
+	}
+
+	return "image/png", b, nil
+}
+
+func AssertIsTestImage(t *testing.T, i io.Reader) {
+	img, _, err := image.Decode(i)
+	assert.NoError(t, err, "Error decoding image")
+	width := img.Bounds().Max.X
+	height := img.Bounds().Max.Y
+	for x := 0; x < width; x++ {
+		for y := 0; y < height; y++ {
+			c := colorFor(x, y)
+			if !assert.Equal(t, c, img.At(x, y), fmt.Sprintf("Wrong colour for pixel %d,%d", x, y)) {
+				return // don't print thousands of errors
+			}
+		}
+	}
+}
diff --git a/test/test_internals/util_client.go b/test/test_internals/util_client.go
new file mode 100644
index 0000000000000000000000000000000000000000..59080654e0676048502ed10ab4c60750bca84216
--- /dev/null
+++ b/test/test_internals/util_client.go
@@ -0,0 +1,66 @@
+package test_internals
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"net/url"
+
+	"github.com/turt2live/matrix-media-repo/database"
+)
+
+type MatrixClient struct {
+	AccessToken     string
+	ClientServerUrl string
+}
+
+func (c *MatrixClient) Upload(filename string, contentType string, body io.Reader) (*MatrixUploadResponse, error) {
+	j, err := c.DoReturnJson("POST", "/_matrix/media/v3/upload", url.Values{"filename": []string{filename}}, contentType, body)
+	if err != nil {
+		return nil, err
+	}
+	val := new(MatrixUploadResponse)
+	err = j.ApplyTo(&val)
+	if err != nil {
+		return nil, err
+	}
+	return val, nil
+}
+
+func (c *MatrixClient) DoReturnJson(method string, endpoint string, qs url.Values, contentType string, body io.Reader) (*database.AnonymousJson, error) {
+	res, err := c.DoRaw(method, endpoint, qs, contentType, body)
+	if err != nil {
+		return nil, err
+	}
+
+	decoder := json.NewDecoder(res.Body)
+	val := new(database.AnonymousJson)
+	err = decoder.Decode(&val)
+	if err != nil {
+		return nil, err
+	}
+
+	return val, nil
+}
+
+func (c *MatrixClient) DoRaw(method string, endpoint string, qs url.Values, contentType string, body io.Reader) (*http.Response, error) {
+	endpoint, err := url.JoinPath(c.ClientServerUrl, endpoint)
+	if err != nil {
+		return nil, err
+	}
+	req, err := http.NewRequest(method, endpoint+"?"+qs.Encode(), body)
+	if err != nil {
+		return nil, err
+	}
+	if contentType != "" {
+		req.Header.Set("Content-Type", contentType)
+	}
+	if c.AccessToken != "" {
+		req.Header.Set("Authorization", "Bearer "+c.AccessToken)
+	}
+
+	log.Println(fmt.Sprintf("[HTTP] [Auth=%s] %s %s", c.AccessToken, req.Method, req.RequestURI))
+	return http.DefaultClient.Do(req)
+}
diff --git a/test/test_internals/util_client_api_types.go b/test/test_internals/util_client_api_types.go
new file mode 100644
index 0000000000000000000000000000000000000000..5b2e79b6687f5ff8565c49e3ecb424350bb3a614
--- /dev/null
+++ b/test/test_internals/util_client_api_types.go
@@ -0,0 +1,5 @@
+package test_internals
+
+type MatrixUploadResponse struct {
+	MxcUri string `json:"content_uri"`
+}
diff --git a/test/upload_suite_test.go b/test/upload_suite_test.go
index 6386b36293192fd135ec91361e5b3e6fa21422a5..45456563529ac0e7844186eb886283cf531098d1 100644
--- a/test/upload_suite_test.go
+++ b/test/upload_suite_test.go
@@ -6,15 +6,17 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/suite"
+	"github.com/turt2live/matrix-media-repo/test/test_internals"
+	"github.com/turt2live/matrix-media-repo/util"
 )
 
 type UploadTestSuite struct {
 	suite.Suite
-	deps *ContainerDeps
+	deps *test_internals.ContainerDeps
 }
 
 func (s *UploadTestSuite) SetupSuite() {
-	deps, err := MakeTestDeps()
+	deps, err := test_internals.MakeTestDeps()
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -30,7 +32,16 @@ func (s *UploadTestSuite) TearDownSuite() {
 func (s *UploadTestSuite) TestUpload() {
 	t := s.T()
 
-	assert.NoError(t, nil)
+	client := &test_internals.MatrixClient{
+		AccessToken:     s.deps.Homeservers[0].UnprivilegedAliceAccessToken,
+		ClientServerUrl: s.deps.Machines[0].HttpUrl,
+	}
+
+	contentType, img, err := test_internals.MakeTestImage(512, 512)
+	res, err := client.Upload("image"+util.ExtensionForContentType(contentType), contentType, img)
+	assert.NoError(t, err)
+	log.Println(res.MxcUri)
+	assert.NotEmpty(t, res.MxcUri)
 }
 
 func TestUploadTestSuite(t *testing.T) {