202 lines
4.6 KiB
Go
202 lines
4.6 KiB
Go
package usecase
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"context"
|
||
|
|
"errors"
|
||
|
|
"io"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"cmroubao/backend-api/internal/domain"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestAssetServiceUploadPersistsNormalizedMetadata(t *testing.T) {
|
||
|
|
repository := &fakeAssetRepository{}
|
||
|
|
store := &fakeReferenceImageStore{
|
||
|
|
result: NormalizedReferenceImage{
|
||
|
|
StorageKey: "aa/file.jpg",
|
||
|
|
InputMediaType: "image/png",
|
||
|
|
InputSHA256: strings.Repeat("1", 64),
|
||
|
|
MediaType: "image/jpeg",
|
||
|
|
SizeBytes: 123,
|
||
|
|
SHA256: strings.Repeat("2", 64),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
service := mustAssetService(t, repository, store)
|
||
|
|
result, err := service.UploadTaskReference(
|
||
|
|
context.Background(),
|
||
|
|
UploadTaskReferenceCommand{
|
||
|
|
CreatorSubject: " local-admin ",
|
||
|
|
IdempotencyKey: " upload-1 ",
|
||
|
|
DeclaredMediaType: "image/png",
|
||
|
|
Content: bytes.NewReader([]byte("fixture")),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("UploadTaskReference() error = %v", err)
|
||
|
|
}
|
||
|
|
if result.Replayed ||
|
||
|
|
result.Asset.CreatorSubject != "local-admin" ||
|
||
|
|
result.Asset.Purpose != domain.AssetPurposeTaskReference ||
|
||
|
|
result.Asset.StorageKey != store.result.StorageKey ||
|
||
|
|
len(repository.hash) != 64 {
|
||
|
|
t.Fatalf("result/repository = %+v/%+v", result, repository)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssetServiceDeletesNewFileWhenRepositoryReplaysOrFails(
|
||
|
|
t *testing.T,
|
||
|
|
) {
|
||
|
|
for _, test := range []struct {
|
||
|
|
name string
|
||
|
|
replay bool
|
||
|
|
createErr error
|
||
|
|
wantErr bool
|
||
|
|
}{
|
||
|
|
{name: "replay", replay: true},
|
||
|
|
{name: "failure", createErr: ErrRepositoryUnavailable, wantErr: true},
|
||
|
|
} {
|
||
|
|
t.Run(test.name, func(t *testing.T) {
|
||
|
|
repository := &fakeAssetRepository{
|
||
|
|
replay: test.replay,
|
||
|
|
createErr: test.createErr,
|
||
|
|
}
|
||
|
|
store := &fakeReferenceImageStore{
|
||
|
|
result: NormalizedReferenceImage{
|
||
|
|
StorageKey: "aa/candidate.jpg",
|
||
|
|
InputMediaType: "image/jpeg",
|
||
|
|
InputSHA256: strings.Repeat("1", 64),
|
||
|
|
MediaType: "image/jpeg",
|
||
|
|
SizeBytes: 123,
|
||
|
|
SHA256: strings.Repeat("2", 64),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
service := mustAssetService(t, repository, store)
|
||
|
|
_, err := service.UploadTaskReference(
|
||
|
|
context.Background(),
|
||
|
|
UploadTaskReferenceCommand{
|
||
|
|
CreatorSubject: "local-admin",
|
||
|
|
IdempotencyKey: "upload-1",
|
||
|
|
DeclaredMediaType: "image/jpeg",
|
||
|
|
Content: bytes.NewReader([]byte("fixture")),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
if (err != nil) != test.wantErr {
|
||
|
|
t.Fatalf("UploadTaskReference() error = %v", err)
|
||
|
|
}
|
||
|
|
if store.deleted != "aa/candidate.jpg" {
|
||
|
|
t.Fatalf("deleted key = %q", store.deleted)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAssetServiceMapsImageValidationError(t *testing.T) {
|
||
|
|
service := mustAssetService(
|
||
|
|
t,
|
||
|
|
&fakeAssetRepository{},
|
||
|
|
&fakeReferenceImageStore{
|
||
|
|
err: &ImageStoreError{
|
||
|
|
Kind: ImageStoreErrorInvalid,
|
||
|
|
Cause: errors.New("private decoder detail"),
|
||
|
|
},
|
||
|
|
},
|
||
|
|
)
|
||
|
|
_, err := service.UploadTaskReference(
|
||
|
|
context.Background(),
|
||
|
|
UploadTaskReferenceCommand{
|
||
|
|
CreatorSubject: "local-admin",
|
||
|
|
IdempotencyKey: "upload-1",
|
||
|
|
DeclaredMediaType: "image/jpeg",
|
||
|
|
Content: bytes.NewReader([]byte("bad")),
|
||
|
|
},
|
||
|
|
)
|
||
|
|
assertUsecaseError(t, err, ErrorKindInvalid, "ASSET_IMAGE_INVALID")
|
||
|
|
if strings.Contains(err.Error(), "private") {
|
||
|
|
t.Fatal("public error leaked decoder detail")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
type fakeAssetRepository struct {
|
||
|
|
asset domain.Asset
|
||
|
|
replay bool
|
||
|
|
createErr error
|
||
|
|
hash string
|
||
|
|
}
|
||
|
|
|
||
|
|
func (repository *fakeAssetRepository) CreateAssetIdempotent(
|
||
|
|
_ context.Context,
|
||
|
|
asset domain.Asset,
|
||
|
|
_ string,
|
||
|
|
hash string,
|
||
|
|
) (domain.Asset, bool, error) {
|
||
|
|
repository.asset = asset
|
||
|
|
repository.hash = hash
|
||
|
|
if repository.createErr != nil {
|
||
|
|
return asset, false, repository.createErr
|
||
|
|
}
|
||
|
|
if repository.replay {
|
||
|
|
return asset, false, nil
|
||
|
|
}
|
||
|
|
return asset, true, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (repository *fakeAssetRepository) GetAsset(
|
||
|
|
context.Context,
|
||
|
|
string,
|
||
|
|
string,
|
||
|
|
) (domain.Asset, error) {
|
||
|
|
return repository.asset, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
type fakeReferenceImageStore struct {
|
||
|
|
result NormalizedReferenceImage
|
||
|
|
err error
|
||
|
|
deleted string
|
||
|
|
}
|
||
|
|
|
||
|
|
func (store *fakeReferenceImageStore) Put(
|
||
|
|
context.Context,
|
||
|
|
string,
|
||
|
|
string,
|
||
|
|
io.Reader,
|
||
|
|
) (NormalizedReferenceImage, error) {
|
||
|
|
return store.result, store.err
|
||
|
|
}
|
||
|
|
|
||
|
|
func (store *fakeReferenceImageStore) Open(
|
||
|
|
context.Context,
|
||
|
|
string,
|
||
|
|
) (io.ReadCloser, error) {
|
||
|
|
return io.NopCloser(bytes.NewReader(nil)), nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (store *fakeReferenceImageStore) Delete(
|
||
|
|
_ context.Context,
|
||
|
|
key string,
|
||
|
|
) error {
|
||
|
|
store.deleted = key
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func mustAssetService(
|
||
|
|
t *testing.T,
|
||
|
|
repository AssetRepository,
|
||
|
|
store ReferenceImageStore,
|
||
|
|
) *AssetService {
|
||
|
|
t.Helper()
|
||
|
|
service, err := NewAssetService(
|
||
|
|
repository,
|
||
|
|
store,
|
||
|
|
fakeClock{},
|
||
|
|
&sequenceIDs{},
|
||
|
|
)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("NewAssetService() error = %v", err)
|
||
|
|
}
|
||
|
|
return service
|
||
|
|
}
|
||
|
|
|
||
|
|
var _ Clock = fakeClock{}
|