diff --git a/README.md b/README.md index 5e50878..fa5e83a 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,12 @@ cd backend/ sqlc generate ``` +Mocks require [mockery](https://github.com/vektra/mockery) to be installed, and can be regenerated with: + +``` +go generate +``` + ### Migrations Database migrations require [golang-migrate](https://github.com/golang-migrate/migrate). diff --git a/backend/generated/mocks/S3Client.go b/backend/generated/mocks/S3Client.go new file mode 100644 index 0000000..7bcf177 --- /dev/null +++ b/backend/generated/mocks/S3Client.go @@ -0,0 +1,166 @@ +// Code generated by mockery v2.9.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + s3 "github.com/aws/aws-sdk-go-v2/service/s3" +) + +// S3Client is an autogenerated mock type for the S3Client type +type S3Client struct { + mock.Mock +} + +// AbortMultipartUpload provides a mock function with given fields: _a0, _a1, _a2 +func (_m *S3Client) AbortMultipartUpload(_a0 context.Context, _a1 *s3.AbortMultipartUploadInput, _a2 ...func(*s3.Options)) (*s3.AbortMultipartUploadOutput, error) { + _va := make([]interface{}, len(_a2)) + for _i := range _a2 { + _va[_i] = _a2[_i] + } + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *s3.AbortMultipartUploadOutput + if rf, ok := ret.Get(0).(func(context.Context, *s3.AbortMultipartUploadInput, ...func(*s3.Options)) *s3.AbortMultipartUploadOutput); ok { + r0 = rf(_a0, _a1, _a2...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*s3.AbortMultipartUploadOutput) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *s3.AbortMultipartUploadInput, ...func(*s3.Options)) error); ok { + r1 = rf(_a0, _a1, _a2...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CompleteMultipartUpload provides a mock function with given fields: _a0, _a1, _a2 +func (_m *S3Client) CompleteMultipartUpload(_a0 context.Context, _a1 *s3.CompleteMultipartUploadInput, _a2 ...func(*s3.Options)) (*s3.CompleteMultipartUploadOutput, error) { + _va := make([]interface{}, len(_a2)) + for _i := range _a2 { + _va[_i] = _a2[_i] + } + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *s3.CompleteMultipartUploadOutput + if rf, ok := ret.Get(0).(func(context.Context, *s3.CompleteMultipartUploadInput, ...func(*s3.Options)) *s3.CompleteMultipartUploadOutput); ok { + r0 = rf(_a0, _a1, _a2...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*s3.CompleteMultipartUploadOutput) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *s3.CompleteMultipartUploadInput, ...func(*s3.Options)) error); ok { + r1 = rf(_a0, _a1, _a2...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// CreateMultipartUpload provides a mock function with given fields: _a0, _a1, _a2 +func (_m *S3Client) CreateMultipartUpload(_a0 context.Context, _a1 *s3.CreateMultipartUploadInput, _a2 ...func(*s3.Options)) (*s3.CreateMultipartUploadOutput, error) { + _va := make([]interface{}, len(_a2)) + for _i := range _a2 { + _va[_i] = _a2[_i] + } + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *s3.CreateMultipartUploadOutput + if rf, ok := ret.Get(0).(func(context.Context, *s3.CreateMultipartUploadInput, ...func(*s3.Options)) *s3.CreateMultipartUploadOutput); ok { + r0 = rf(_a0, _a1, _a2...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*s3.CreateMultipartUploadOutput) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *s3.CreateMultipartUploadInput, ...func(*s3.Options)) error); ok { + r1 = rf(_a0, _a1, _a2...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetObject provides a mock function with given fields: _a0, _a1, _a2 +func (_m *S3Client) GetObject(_a0 context.Context, _a1 *s3.GetObjectInput, _a2 ...func(*s3.Options)) (*s3.GetObjectOutput, error) { + _va := make([]interface{}, len(_a2)) + for _i := range _a2 { + _va[_i] = _a2[_i] + } + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *s3.GetObjectOutput + if rf, ok := ret.Get(0).(func(context.Context, *s3.GetObjectInput, ...func(*s3.Options)) *s3.GetObjectOutput); ok { + r0 = rf(_a0, _a1, _a2...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*s3.GetObjectOutput) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *s3.GetObjectInput, ...func(*s3.Options)) error); ok { + r1 = rf(_a0, _a1, _a2...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UploadPart provides a mock function with given fields: _a0, _a1, _a2 +func (_m *S3Client) UploadPart(_a0 context.Context, _a1 *s3.UploadPartInput, _a2 ...func(*s3.Options)) (*s3.UploadPartOutput, error) { + _va := make([]interface{}, len(_a2)) + for _i := range _a2 { + _va[_i] = _a2[_i] + } + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *s3.UploadPartOutput + if rf, ok := ret.Get(0).(func(context.Context, *s3.UploadPartInput, ...func(*s3.Options)) *s3.UploadPartOutput); ok { + r0 = rf(_a0, _a1, _a2...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*s3.UploadPartOutput) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *s3.UploadPartInput, ...func(*s3.Options)) error); ok { + r1 = rf(_a0, _a1, _a2...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/backend/generated/mocks/S3PresignClient.go b/backend/generated/mocks/S3PresignClient.go new file mode 100644 index 0000000..2a35f49 --- /dev/null +++ b/backend/generated/mocks/S3PresignClient.go @@ -0,0 +1,48 @@ +// Code generated by mockery v2.9.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + s3 "github.com/aws/aws-sdk-go-v2/service/s3" + + v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" +) + +// S3PresignClient is an autogenerated mock type for the S3PresignClient type +type S3PresignClient struct { + mock.Mock +} + +// PresignGetObject provides a mock function with given fields: _a0, _a1, _a2 +func (_m *S3PresignClient) PresignGetObject(_a0 context.Context, _a1 *s3.GetObjectInput, _a2 ...func(*s3.PresignOptions)) (*v4.PresignedHTTPRequest, error) { + _va := make([]interface{}, len(_a2)) + for _i := range _a2 { + _va[_i] = _a2[_i] + } + var _ca []interface{} + _ca = append(_ca, _a0, _a1) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 *v4.PresignedHTTPRequest + if rf, ok := ret.Get(0).(func(context.Context, *s3.GetObjectInput, ...func(*s3.PresignOptions)) *v4.PresignedHTTPRequest); ok { + r0 = rf(_a0, _a1, _a2...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v4.PresignedHTTPRequest) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *s3.GetObjectInput, ...func(*s3.PresignOptions)) error); ok { + r1 = rf(_a0, _a1, _a2...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/backend/generated/mocks/Store.go b/backend/generated/mocks/Store.go new file mode 100644 index 0000000..3ab992d --- /dev/null +++ b/backend/generated/mocks/Store.go @@ -0,0 +1,165 @@ +// Code generated by mockery v2.9.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + store "git.netflux.io/rob/clipper/generated/store" + + uuid "github.com/google/uuid" +) + +// Store is an autogenerated mock type for the Store type +type Store struct { + mock.Mock +} + +// CreateMediaSet provides a mock function with given fields: _a0, _a1 +func (_m *Store) CreateMediaSet(_a0 context.Context, _a1 store.CreateMediaSetParams) (store.MediaSet, error) { + ret := _m.Called(_a0, _a1) + + var r0 store.MediaSet + if rf, ok := ret.Get(0).(func(context.Context, store.CreateMediaSetParams) store.MediaSet); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(store.MediaSet) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, store.CreateMediaSetParams) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetMediaSet provides a mock function with given fields: _a0, _a1 +func (_m *Store) GetMediaSet(_a0 context.Context, _a1 uuid.UUID) (store.MediaSet, error) { + ret := _m.Called(_a0, _a1) + + var r0 store.MediaSet + if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) store.MediaSet); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(store.MediaSet) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetMediaSetByYoutubeID provides a mock function with given fields: _a0, _a1 +func (_m *Store) GetMediaSetByYoutubeID(_a0 context.Context, _a1 string) (store.MediaSet, error) { + ret := _m.Called(_a0, _a1) + + var r0 store.MediaSet + if rf, ok := ret.Get(0).(func(context.Context, string) store.MediaSet); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(store.MediaSet) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetEncodedAudioUploaded provides a mock function with given fields: _a0, _a1 +func (_m *Store) SetEncodedAudioUploaded(_a0 context.Context, _a1 store.SetEncodedAudioUploadedParams) (store.MediaSet, error) { + ret := _m.Called(_a0, _a1) + + var r0 store.MediaSet + if rf, ok := ret.Get(0).(func(context.Context, store.SetEncodedAudioUploadedParams) store.MediaSet); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(store.MediaSet) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, store.SetEncodedAudioUploadedParams) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetRawAudioUploaded provides a mock function with given fields: _a0, _a1 +func (_m *Store) SetRawAudioUploaded(_a0 context.Context, _a1 store.SetRawAudioUploadedParams) (store.MediaSet, error) { + ret := _m.Called(_a0, _a1) + + var r0 store.MediaSet + if rf, ok := ret.Get(0).(func(context.Context, store.SetRawAudioUploadedParams) store.MediaSet); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(store.MediaSet) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, store.SetRawAudioUploadedParams) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetVideoThumbnailUploaded provides a mock function with given fields: _a0, _a1 +func (_m *Store) SetVideoThumbnailUploaded(_a0 context.Context, _a1 store.SetVideoThumbnailUploadedParams) (store.MediaSet, error) { + ret := _m.Called(_a0, _a1) + + var r0 store.MediaSet + if rf, ok := ret.Get(0).(func(context.Context, store.SetVideoThumbnailUploadedParams) store.MediaSet); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(store.MediaSet) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, store.SetVideoThumbnailUploadedParams) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SetVideoUploaded provides a mock function with given fields: _a0, _a1 +func (_m *Store) SetVideoUploaded(_a0 context.Context, _a1 store.SetVideoUploadedParams) (store.MediaSet, error) { + ret := _m.Called(_a0, _a1) + + var r0 store.MediaSet + if rf, ok := ret.Get(0).(func(context.Context, store.SetVideoUploadedParams) store.MediaSet); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(store.MediaSet) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, store.SetVideoUploadedParams) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/backend/generated/mocks/YoutubeClient.go b/backend/generated/mocks/YoutubeClient.go new file mode 100644 index 0000000..bb93805 --- /dev/null +++ b/backend/generated/mocks/YoutubeClient.go @@ -0,0 +1,70 @@ +// Code generated by mockery v2.9.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + io "io" + + mock "github.com/stretchr/testify/mock" + + youtube "github.com/kkdai/youtube/v2" +) + +// YoutubeClient is an autogenerated mock type for the YoutubeClient type +type YoutubeClient struct { + mock.Mock +} + +// GetStreamContext provides a mock function with given fields: _a0, _a1, _a2 +func (_m *YoutubeClient) GetStreamContext(_a0 context.Context, _a1 *youtube.Video, _a2 *youtube.Format) (io.ReadCloser, int64, error) { + ret := _m.Called(_a0, _a1, _a2) + + var r0 io.ReadCloser + if rf, ok := ret.Get(0).(func(context.Context, *youtube.Video, *youtube.Format) io.ReadCloser); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(io.ReadCloser) + } + } + + var r1 int64 + if rf, ok := ret.Get(1).(func(context.Context, *youtube.Video, *youtube.Format) int64); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Get(1).(int64) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *youtube.Video, *youtube.Format) error); ok { + r2 = rf(_a0, _a1, _a2) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// GetVideoContext provides a mock function with given fields: _a0, _a1 +func (_m *YoutubeClient) GetVideoContext(_a0 context.Context, _a1 string) (*youtube.Video, error) { + ret := _m.Called(_a0, _a1) + + var r0 *youtube.Video + if rf, ok := ret.Get(0).(func(context.Context, string) *youtube.Video); ok { + r0 = rf(_a0, _a1) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*youtube.Video) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/backend/go.mod b/backend/go.mod index 651c167..b30bab8 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -48,6 +48,7 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rs/cors v1.8.0 // indirect + github.com/stretchr/objx v0.2.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.7.0 // indirect golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 // indirect diff --git a/backend/go.sum b/backend/go.sum index a8aba53..037cdf3 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -584,6 +584,7 @@ github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/backend/media/service_test.go b/backend/media/service_test.go index 1296c8a..d6f6d53 100644 --- a/backend/media/service_test.go +++ b/backend/media/service_test.go @@ -1,13 +1,125 @@ package media_test import ( + "context" + "fmt" "io" + "os" "testing" + "git.netflux.io/rob/clipper/config" + "git.netflux.io/rob/clipper/generated/mocks" + "git.netflux.io/rob/clipper/generated/store" "git.netflux.io/rob/clipper/media" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.uber.org/zap" ) +func TestGetAudioSegment(t *testing.T) { + testCases := []struct { + name string + fixturePath string + fixtureLen int64 + startFrame, endFrame int64 + channels int32 + numBins int + wantPeaks []int16 + wantErr string + }{ + { + name: "entire fixture, stereo, 1 bin", + fixturePath: "testdata/tone-44100-stereo-int16.raw", + fixtureLen: 176400, + startFrame: 0, + endFrame: 44100, + channels: 2, + numBins: 1, + wantPeaks: []int16{32747, 32747}, + }, + { + name: "entire fixture, stereo, 4 bins", + fixturePath: "testdata/tone-44100-stereo-int16.raw", + fixtureLen: 176400, + startFrame: 0, + endFrame: 44100, + channels: 2, + numBins: 4, + wantPeaks: []int16{8173, 8177, 16366, 16370, 24557, 24555, 32747, 32747}, + }, + { + name: "entire fixture, stereo, 16 bins", + fixturePath: "testdata/tone-44100-stereo-int16.raw", + fixtureLen: 176400, + startFrame: 0, + endFrame: 44100, + channels: 2, + numBins: 16, + wantPeaks: []int16{2029, 2029, 4075, 4076, 6124, 6125, 8173, 8177, 10222, 10221, 12267, 12265, 14314, 14313, 16366, 16370, 18413, 18411, 20453, 20454, 22505, 22508, 24557, 24555, 26604, 26605, 28644, 28643, 30698, 30694, 32747, 32747}, + }, + { + name: "entire fixture, mono, 1 bin", + fixturePath: "testdata/tone-44100-mono-int16.raw", + fixtureLen: 88200, + startFrame: 0, + endFrame: 44100, + channels: 1, + numBins: 1, + wantPeaks: []int16{32748}, + }, + { + name: "entire fixture, mono, 32 bins", + fixturePath: "testdata/tone-44100-mono-int16.raw", + fixtureLen: 88200, + startFrame: 0, + endFrame: 44100, + channels: 1, + numBins: 32, + wantPeaks: []int16{1026, 2030, 3071, 4075, 5122, 6126, 7167, 8172, 9213, 10217, 11259, 12264, 13311, 14315, 15360, 16364, 17405, 18412, 19450, 20453, 21497, 22504, 23549, 24554, 25599, 26607, 27641, 28642, 29688, 30738, 31746, 32748}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + expectedBytes := (tc.endFrame - tc.startFrame) * int64(tc.channels) * media.SizeOfInt16 + + audioFile, err := os.Open(tc.fixturePath) + require.NoError(t, err) + defer audioFile.Close() + audioData := io.NopCloser(io.LimitReader(audioFile, int64(expectedBytes))) + + mediaSetID := uuid.New() + mediaSet := store.MediaSet{ID: mediaSetID, AudioChannels: tc.channels} + + // store is passed the mediaSetID and returns a mediaSet + store := &mocks.Store{} + store.On("GetMediaSet", mock.Anything, mediaSetID).Return(mediaSet, nil) + defer store.AssertExpectations(t) + + // S3 is passed the expected byte range, and returns an io.Reader + s3Client := &mocks.S3Client{} + s3Client.On("GetObject", mock.Anything, mock.MatchedBy(func(input *s3.GetObjectInput) bool { + return *input.Range == fmt.Sprintf("bytes=0-%d", expectedBytes) + })).Return(&s3.GetObjectOutput{Body: audioData, ContentLength: tc.fixtureLen}, nil) + defer s3Client.AssertExpectations(t) + s3API := media.S3API{S3Client: s3Client, S3PresignClient: &mocks.S3PresignClient{}} + + service := media.NewMediaSetService(store, nil, s3API, config.Config{}, zap.NewNop()) + peaks, err := service.GetAudioSegment(context.Background(), mediaSetID, tc.startFrame, tc.endFrame, tc.numBins) + + if tc.wantErr == "" { + assert.NoError(t, err) + assert.Equal(t, tc.wantPeaks, peaks) + } else { + assert.EqualError(t, err, tc.wantErr) + } + }) + } +} + type testReader struct { count int data [][]byte diff --git a/backend/media/testdata/tone-44100-mono-int16.raw b/backend/media/testdata/tone-44100-mono-int16.raw new file mode 100644 index 0000000..723e43f Binary files /dev/null and b/backend/media/testdata/tone-44100-mono-int16.raw differ diff --git a/backend/media/testdata/tone-44100-stereo-int16.raw b/backend/media/testdata/tone-44100-stereo-int16.raw new file mode 100644 index 0000000..ff07d35 Binary files /dev/null and b/backend/media/testdata/tone-44100-stereo-int16.raw differ diff --git a/backend/media/types.go b/backend/media/types.go index 7a24da6..9839eca 100644 --- a/backend/media/types.go +++ b/backend/media/types.go @@ -1,5 +1,9 @@ package media +//go:generate mockery --recursive --name S3* --output ../generated/mocks +//go:generate mockery --recursive --name Store --output ../generated/mocks +//go:generate mockery --recursive --name YoutubeClient --output ../generated/mocks + import ( "context" "io"