feat(backend): implement task creation and admin web
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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{}
|
||||
Reference in New Issue
Block a user