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{}