Browse Source

♻️ Refacotred media endpoint due official micropub tests

develop
Maxim Lebedev 6 months ago
parent
commit
0e3778d0c7
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
  1. 15
      internal/domain/config.go
  2. 25
      internal/domain/media.go
  3. BIN
      internal/domain/testdata/sunset.jpg
  4. 24
      internal/media/delivery/http/media_http.go
  5. 122
      internal/media/delivery/http/media_http_test.go
  6. BIN
      internal/media/delivery/http/testdata/micropub-rocks.png
  7. BIN
      internal/media/delivery/http/testdata/sunset.jpg
  8. BIN
      internal/media/delivery/http/testdata/w3c-socialwg.gif
  9. 8
      internal/media/repository.go
  10. 15
      internal/media/repository/memory/memory_media.go
  11. 42
      internal/media/repository/memory/memory_media_test.go
  12. 6
      internal/media/usecase.go
  13. 11
      internal/media/usecase/media_ucase.go
  14. 32
      internal/media/usecase/media_ucase_test.go

15
internal/domain/config.go

@ -0,0 +1,15 @@
package domain
import "testing"
type Config struct {
BaseURL string
}
func TestConfig(tb testing.TB) *Config {
tb.Helper()
return &Config{
BaseURL: "https://example.com/",
}
}

25
internal/domain/media.go

@ -0,0 +1,25 @@
package domain
import (
_ "embed"
"testing"
)
type Media struct {
Name string
ContentType string
Content []byte
}
//go:embed testdata/sunset.jpg
var testMediaContent []byte
func TestMedia(tb testing.TB) *Media {
tb.Helper()
return &Media{
Name: "sunset.jpg",
ContentType: "image/jpeg",
Content: testMediaContent,
}
}

BIN
internal/domain/testdata/sunset.jpg vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

24
internal/media/delivery/http/media_http.go

@ -3,10 +3,11 @@ package http
import (
"bytes"
"encoding/json"
"mime"
"path"
"path/filepath"
"github.com/fasthttp/router"
"github.com/lestrrat-go/jwx/jwa"
http "github.com/valyala/fasthttp"
"golang.org/x/xerrors"
@ -18,12 +19,14 @@ import (
// RequestHandler represents a handler with business logic for HTTP requests.
type RequestHandler struct {
config *domain.Config
useCase media.UseCase
}
// New creates a new HTTP delivery handler.
func New(useCase media.UseCase) *RequestHandler {
func New(config *domain.Config, useCase media.UseCase) *RequestHandler {
return &RequestHandler{
config: config,
useCase: useCase,
}
}
@ -86,7 +89,11 @@ func (h *RequestHandler) Update(ctx *http.RequestCtx) {
return
}
fileName, err := h.useCase.Upload(ctx, ff.Filename, buf.Bytes())
fileName, err := h.useCase.Upload(ctx, &domain.Media{
Name: ff.Filename,
ContentType: mime.TypeByExtension(filepath.Ext(ff.Filename)),
Content: buf.Bytes(),
})
if err != nil {
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(&domain.Error{
@ -99,16 +106,16 @@ func (h *RequestHandler) Update(ctx *http.RequestCtx) {
}
ctx.SetStatusCode(http.StatusCreated)
ctx.Response.Header.Set(http.HeaderLocation, path.Join("/", "media", fileName))
ctx.Response.Header.Set(http.HeaderLocation, h.config.BaseURL+path.Join("media", fileName))
encoder.Encode(struct{}{})
}
func (h *RequestHandler) Read(ctx *http.RequestCtx) {
ctx.SetContentType(common.MIMEApplicationJSON)
encoder := json.NewEncoder(ctx)
fileName, ok := ctx.UserValue("fileName").(string)
if !ok {
ctx.SetContentType(common.MIMEApplicationJSON)
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(&domain.Error{
Code: "invalid_request",
@ -119,8 +126,9 @@ func (h *RequestHandler) Read(ctx *http.RequestCtx) {
return
}
contents, err := h.useCase.Download(ctx, fileName)
result, err := h.useCase.Download(ctx, fileName)
if err != nil {
ctx.SetContentType(common.MIMEApplicationJSON)
ctx.SetStatusCode(http.StatusBadRequest)
encoder.Encode(&domain.Error{
Code: "invalid_request",
@ -132,6 +140,6 @@ func (h *RequestHandler) Read(ctx *http.RequestCtx) {
}
ctx.SetStatusCode(http.StatusOK)
ctx.SetContentType(common.MIMEApplicationOctetStream)
ctx.SetBody(contents)
ctx.SetContentType(result.ContentType)
ctx.SetBody(result.Content)
}

122
internal/media/delivery/http/media_http_test.go

@ -2,10 +2,9 @@ package http_test
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"embed"
"mime/multipart"
"path/filepath"
"sync"
"testing"
@ -14,35 +13,82 @@ import (
"github.com/stretchr/testify/require"
http "github.com/valyala/fasthttp"
"source.toby3d.me/website/micropub/internal/domain"
delivery "source.toby3d.me/website/micropub/internal/media/delivery/http"
repository "source.toby3d.me/website/micropub/internal/media/repository/memory"
"source.toby3d.me/website/micropub/internal/media/usecase"
"source.toby3d.me/website/micropub/internal/testing/httptest"
)
const testFileName string = "sunset.jpg"
type TestCase struct {
name string
fileName string
expContentType string
}
//go:embed testdata/*
var testData embed.FS
func TestUpload(t *testing.T) {
t.Parallel()
_, contents := testFile(t)
buf := bytes.NewBuffer(nil)
w := multipart.NewWriter(buf)
cfg := domain.TestConfig(t)
r := router.New()
delivery.New(cfg, usecase.NewMediaUseCase(repository.NewMemoryMediaRepository(new(sync.Map)))).Register(r)
ff, err := w.CreateFormFile("file", testFileName)
require.NoError(t, err)
client, _, cleanup := httptest.New(t, r.Handler)
t.Cleanup(cleanup)
_, err = ff.Write(contents)
require.NoError(t, err)
for _, testCase := range []TestCase{
{
name: "jpg",
fileName: "sunset.jpg",
expContentType: "image/jpeg",
}, {
name: "png",
fileName: "micropub-rocks.png",
expContentType: "image/png",
}, {
name: "gif",
fileName: "w3c-socialwg.gif",
expContentType: "image/gif",
},
} {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
upResp := upload(t, client, testCase.fileName)
defer http.ReleaseResponse(upResp)
assert.Equal(t, upResp.StatusCode(), http.StatusCreated, "returned HTTP 201")
assert.NotNil(t, upResp.Header.Peek(http.HeaderLocation), "returned a Location header")
downResp := download(t, client, upResp.Header.Peek(http.HeaderLocation))
assert.Equal(t, http.StatusOK, downResp.StatusCode(), "the URL exists")
assert.Equal(t, testCase.expContentType, string(downResp.Header.ContentType()),
"has the expected content type")
})
}
}
require.NoError(t, w.Close())
func upload(tb testing.TB, client *http.Client, fileName string) *http.Response {
tb.Helper()
r := router.New()
r.POST("/media", delivery.New(usecase.NewMediaUseCase(repository.NewMemoryMediaRepository(new(sync.Map)))).
Update)
contents, err := testData.ReadFile(filepath.Join("testdata", fileName))
require.NoError(tb, err)
client, _, cleanup := httptest.New(t, r.Handler)
t.Cleanup(cleanup)
// NOTE(toby3d): upload
buf := bytes.NewBuffer(nil)
w := multipart.NewWriter(buf)
ff, err := w.CreateFormFile("file", fileName)
require.NoError(tb, err)
_, err = ff.Write(contents)
require.NoError(tb, err)
require.NoError(tb, w.Close())
req := http.AcquireRequest()
defer http.ReleaseRequest(req)
@ -52,43 +98,21 @@ func TestUpload(t *testing.T) {
req.SetBody(buf.Bytes())
resp := http.AcquireResponse()
defer http.ReleaseResponse(resp)
require.NoError(t, client.Do(req, resp))
assert.Equal(t, resp.StatusCode(), http.StatusCreated)
require.NotNil(t, resp.Header.Peek(http.HeaderLocation))
}
func TestDownload(t *testing.T) {
t.Parallel()
fileName, contents := testFile(t)
repo := repository.NewMemoryMediaRepository(new(sync.Map))
require.NoError(t, repo.Create(context.Background(), fileName, contents))
r := router.New()
r.GET("/media/{fileName:*}", delivery.New(usecase.NewMediaUseCase(repo)).Read)
require.NoError(tb, client.Do(req, resp))
client, _, cleanup := httptest.New(t, r.Handler)
t.Cleanup(cleanup)
status, body, err := client.Get(nil, "https://example.com/media/"+fileName)
assert.NoError(t, err)
assert.Equal(t, status, http.StatusOK)
assert.Equal(t, contents, body)
return resp
}
func testFile(tb testing.TB) (string, []byte) {
func download(tb testing.TB, client *http.Client, location []byte) *http.Response {
tb.Helper()
fileName := make([]byte, usecase.DefaultNameLength)
_, err := rand.Read(fileName)
require.NoError(tb, err)
req := http.AcquireRequest()
defer http.ReleaseRequest(req)
req.Header.SetMethod(http.MethodGet)
req.SetRequestURIBytes(location)
contents := make([]byte, 128)
_, err = rand.Read(contents)
require.NoError(tb, err)
resp := http.AcquireResponse()
require.NoError(tb, client.Do(req, resp))
return base64.RawURLEncoding.EncodeToString(fileName) + ".jpg", contents
return resp
}

BIN
internal/media/delivery/http/testdata/micropub-rocks.png vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
internal/media/delivery/http/testdata/sunset.jpg vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
internal/media/delivery/http/testdata/w3c-socialwg.gif vendored

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

8
internal/media/repository.go

@ -3,17 +3,19 @@ package media
import (
"context"
"errors"
"source.toby3d.me/website/micropub/internal/domain"
)
type Repository interface {
// Create save media contents by provided name.
Create(ctx context.Context, name string, contents []byte) error
Create(ctx context.Context, name string, media *domain.Media) error
// Delete remove early saved media file contents by name.
Delete(ctx context.Context, name string) error
// Get returns media file contents by name.
Get(ctx context.Context, name string) ([]byte, error)
Get(ctx context.Context, name string) (*domain.Media, error)
}
var ErrNotFound = errors.New("media not found")
var ErrNotExist = errors.New("media not exist")

15
internal/media/repository/memory/memory_media.go

@ -5,6 +5,7 @@ import (
"path/filepath"
"sync"
"source.toby3d.me/website/micropub/internal/domain"
"source.toby3d.me/website/micropub/internal/media"
)
@ -20,24 +21,24 @@ func NewMemoryMediaRepository(store *sync.Map) media.Repository {
}
}
func (repo *memoryMediaRepository) Create(ctx context.Context, name string, contents []byte) error {
repo.store.Store(filepath.Join(DefaultPathPrefix, name), contents)
func (repo *memoryMediaRepository) Create(ctx context.Context, name string, media *domain.Media) error {
repo.store.Store(filepath.Join(DefaultPathPrefix, name), media)
return nil
}
func (repo *memoryMediaRepository) Get(ctx context.Context, name string) ([]byte, error) {
func (repo *memoryMediaRepository) Get(ctx context.Context, name string) (*domain.Media, error) {
src, ok := repo.store.Load(filepath.Join(DefaultPathPrefix, name))
if !ok {
return nil, media.ErrNotFound
return nil, media.ErrNotExist
}
contents, ok := src.([]byte)
result, ok := src.(*domain.Media)
if !ok {
return nil, media.ErrNotFound
return nil, media.ErrNotExist
}
return contents, nil
return result, nil
}
func (repo *memoryMediaRepository) Delete(ctx context.Context, name string) error {

42
internal/media/repository/memory/memory_media_test.go

@ -2,7 +2,6 @@ package memory_test
import (
"context"
"crypto/rand"
"path"
"sync"
"testing"
@ -10,57 +9,48 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"source.toby3d.me/website/micropub/internal/domain"
repository "source.toby3d.me/website/micropub/internal/media/repository/memory"
)
func TestCreate(t *testing.T) {
t.Parallel()
fileName, contents := testFile(t)
store := new(sync.Map)
media := domain.TestMedia(t)
store := new(sync.Map)
require.NoError(t, repository.NewMemoryMediaRepository(store).
Create(context.Background(), fileName, contents))
Create(context.Background(), "sample.ext", media))
result, ok := store.Load(path.Join(repository.DefaultPathPrefix, fileName))
result, ok := store.Load(path.Join(repository.DefaultPathPrefix, "sample.ext"))
assert.True(t, ok)
assert.Equal(t, result, contents)
assert.Equal(t, media, result)
}
func TestGet(t *testing.T) {
t.Parallel()
fileName, contents := testFile(t)
store := new(sync.Map)
media := domain.TestMedia(t)
store.Store(path.Join(repository.DefaultPathPrefix, fileName), contents)
store := new(sync.Map)
store.Store(path.Join(repository.DefaultPathPrefix, "sample.ext"), media)
result, err := repository.NewMemoryMediaRepository(store).Get(context.Background(), fileName)
result, err := repository.NewMemoryMediaRepository(store).Get(context.Background(), "sample.ext")
assert.NoError(t, err)
assert.Equal(t, result, contents)
assert.Equal(t, media, result)
}
func TestDelete(t *testing.T) {
t.Parallel()
fileName, contents := testFile(t)
store := new(sync.Map)
media := domain.TestMedia(t)
store.Store(path.Join(repository.DefaultPathPrefix, fileName), contents)
store := new(sync.Map)
store.Store(path.Join(repository.DefaultPathPrefix, "sample.ext"), media)
require.NoError(t, repository.NewMemoryMediaRepository(store).Delete(context.Background(), fileName))
require.NoError(t, repository.NewMemoryMediaRepository(store).Delete(context.Background(), "sample.ext"))
result, ok := store.Load(path.Join(repository.DefaultPathPrefix, fileName))
result, ok := store.Load(path.Join(repository.DefaultPathPrefix, "sample.ext"))
assert.False(t, ok)
assert.Nil(t, result)
}
func testFile(tb testing.TB) (string, []byte) {
tb.Helper()
contents := make([]byte, 128)
_, err := rand.Read(contents)
require.NoError(tb, err)
return "sunset.jpg", contents
}

6
internal/media/usecase.go

@ -2,13 +2,15 @@ package media
import (
"context"
"source.toby3d.me/website/micropub/internal/domain"
)
type UseCase interface {
// Upload save uploaded media file in temporary storage with random
// generated name.
Upload(ctx context.Context, name string, contents []byte) (string, error)
Upload(ctx context.Context, media *domain.Media) (string, error)
// Download returns early uploaded media file by random generated name.
Download(ctx context.Context, name string) ([]byte, error)
Download(ctx context.Context, name string) (*domain.Media, error)
}

11
internal/media/usecase/media_ucase.go

@ -7,6 +7,7 @@ import (
"fmt"
"path/filepath"
"source.toby3d.me/website/micropub/internal/domain"
"source.toby3d.me/website/micropub/internal/media"
)
@ -14,7 +15,7 @@ type mediaUseCase struct {
repo media.Repository
}
const DefaultNameLength = 64
const DefaultNameLength = 32
func NewMediaUseCase(repo media.Repository) media.UseCase {
return &mediaUseCase{
@ -22,21 +23,21 @@ func NewMediaUseCase(repo media.Repository) media.UseCase {
}
}
func (useCase *mediaUseCase) Upload(ctx context.Context, name string, src []byte) (string, error) {
func (useCase *mediaUseCase) Upload(ctx context.Context, media *domain.Media) (string, error) {
newName := make([]byte, DefaultNameLength)
if _, err := rand.Read(newName); err != nil {
return "", fmt.Errorf("cannot generate random string: %w", err)
}
fileName := base64.RawURLEncoding.EncodeToString(newName) + filepath.Ext(name)
if err := useCase.repo.Create(ctx, fileName, src); err != nil {
fileName := base64.RawURLEncoding.EncodeToString(newName) + filepath.Ext(media.Name)
if err := useCase.repo.Create(ctx, fileName, media); err != nil {
return "", fmt.Errorf("cannot create media: %w", err)
}
return fileName, nil
}
func (useCase *mediaUseCase) Download(ctx context.Context, name string) ([]byte, error) {
func (useCase *mediaUseCase) Download(ctx context.Context, name string) (*domain.Media, error) {
result, err := useCase.repo.Get(ctx, name)
if err != nil {
return nil, fmt.Errorf("cannot find media: %w", err)

32
internal/media/usecase/media_ucase_test.go

@ -4,12 +4,14 @@ import (
"context"
"crypto/rand"
"encoding/base64"
"path/filepath"
"sync"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"source.toby3d.me/website/micropub/internal/domain"
repository "source.toby3d.me/website/micropub/internal/media/repository/memory"
"source.toby3d.me/website/micropub/internal/media/usecase"
)
@ -17,10 +19,10 @@ import (
func TestUpload(t *testing.T) {
t.Parallel()
_, contents := testFile(t)
media := domain.TestMedia(t)
result, err := usecase.NewMediaUseCase(repository.NewMemoryMediaRepository(new(sync.Map))).
Upload(context.Background(), "sunset.jpg", contents)
Upload(context.Background(), media)
assert.NoError(t, err)
assert.NotEmpty(t, result)
}
@ -28,26 +30,18 @@ func TestUpload(t *testing.T) {
func TestDownload(t *testing.T) {
t.Parallel()
fileName, contents := testFile(t)
repo := repository.NewMemoryMediaRepository(new(sync.Map))
require.NoError(t, repo.Create(context.Background(), fileName, contents))
result, err := usecase.NewMediaUseCase(repo).Download(context.Background(), fileName)
assert.NoError(t, err)
assert.Equal(t, result, contents)
}
func testFile(tb testing.TB) (string, []byte) {
tb.Helper()
media := domain.TestMedia(t)
fileName := make([]byte, usecase.DefaultNameLength)
_, err := rand.Read(fileName)
require.NoError(tb, err)
require.NoError(t, err)
newName := base64.RawURLEncoding.EncodeToString(fileName) + filepath.Ext(media.Name)
contents := make([]byte, 128)
_, err = rand.Read(contents)
require.NoError(tb, err)
repo := repository.NewMemoryMediaRepository(new(sync.Map))
require.NoError(t, repo.Create(context.Background(), newName, media))
return base64.RawURLEncoding.EncodeToString(fileName) + ".jpg", contents
result, err := usecase.NewMediaUseCase(repo).Download(context.Background(), newName)
assert.NoError(t, err)
assert.Equal(t, result, media)
}

Loading…
Cancel
Save