From 01f431b4c534d1fca842616cbf667b328b5414db Mon Sep 17 00:00:00 2001 From: Artem Dvoretskii Date: Wed, 31 Jan 2024 21:45:29 +0300 Subject: [PATCH] add more tests and guthub tests workflow --- .github/workflows/e2e-tests.yml | 60 +++ .mockery.yml | 11 + Makefile | 5 + docker/compose/docker-compose.infra.yml | 2 +- e2e/e2e_helpers_test.go | 114 ++++- e2e/e2e_test.go | 9 +- e2e/streamer.yaml | 3 + e2e/userapi.yaml | 2 +- e2e/userapi_test.go | 31 +- e2e/videoapi.yaml | 2 +- e2e/videoapi_test.go | 50 +- go.mod | 1 + go.sum | 1 + internal/api/store/database_test.go | 24 + internal/api/store/migrate_test.go | 99 ++++ internal/api/store/migrations/test.sql | 1 + internal/api/user/auth/auth.go | 3 +- internal/api/user/service.go | 6 +- internal/api/user/store/store.go | 23 +- internal/api/user/store/store_test.go | 79 +++ internal/api/user/validator_test.go | 53 ++ internal/api/video/client/client.go | 4 - internal/api/video/mock_store_test.go | 451 ++++++++++++++++++ internal/api/video/model/status.go | 5 - internal/api/video/model/status_test.go | 83 ++++ internal/api/video/service.go | 2 - internal/api/video/serviceapi.go | 18 - internal/api/video/serviceapi_test.go | 41 ++ internal/api/video/store/store.go | 18 +- internal/api/video/store/store_test.go | 55 +++ internal/api/video/userapi.go | 13 +- internal/api/video/userapi_test.go | 286 +++++++++++ internal/app/app.go | 4 + internal/event/event.go | 1 - internal/event/notificator/notificator.go | 2 - .../event/notificator/notificator_test.go | 68 +++ internal/media/store/s3/s3.go | 7 + internal/media/store/store.go | 5 + internal/media/streamer/service.go | 8 +- internal/media/uploader/service_test.go | 65 +++ internal/mp4/dumper.go | 7 +- pkg/config/config.go | 1 + pkg/config/config_test.go | 301 ++++++++++++ 43 files changed, 1941 insertions(+), 83 deletions(-) create mode 100644 .github/workflows/e2e-tests.yml create mode 100644 .mockery.yml create mode 100644 internal/api/store/database_test.go create mode 100644 internal/api/store/migrate_test.go create mode 100644 internal/api/store/migrations/test.sql create mode 100644 internal/api/user/store/store_test.go create mode 100644 internal/api/user/validator_test.go create mode 100644 internal/api/video/mock_store_test.go create mode 100644 internal/api/video/model/status_test.go create mode 100644 internal/api/video/serviceapi_test.go create mode 100644 internal/api/video/store/store_test.go create mode 100644 internal/api/video/userapi_test.go create mode 100644 internal/event/notificator/notificator_test.go create mode 100644 internal/media/store/store.go create mode 100644 internal/media/uploader/service_test.go create mode 100644 pkg/config/config_test.go diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml new file mode 100644 index 0000000..59d9bb5 --- /dev/null +++ b/.github/workflows/e2e-tests.yml @@ -0,0 +1,60 @@ +name: e2e tests + +on: + pull_request: + push: + branches: + - main + +jobs: + + e2etests: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:16.1 + env: + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 5s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7 + ports: + - 6379:6379 + + minio: + image: minio/minio:edge-cicd + options: --health-cmd "curl -s http://localhost:9000/minio/health/live" + env: + MINIO_ROOT_USER: admin + MINIO_ROOT_PASSWORD: password + ports: + - 9000:9000 + + steps: + - uses: actions/checkout@v4 + + - name: setup db + env: + PGPASSWORD: postgres + run: | + psql -h localhost -p 5432 -U postgres -f ./docker/compose/initdb/0001_init.sql + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: install deps + run: go mod download + + - name: run tests + run: | + go test -v -count=1 -cover -coverpkg=./... -coverprofile=profile.cov --tags e2e ./... + go tool cover -func profile.cov diff --git a/.mockery.yml b/.mockery.yml new file mode 100644 index 0000000..7e2490a --- /dev/null +++ b/.mockery.yml @@ -0,0 +1,11 @@ +quiet: False +disable-version-string: True +with-expecter: True +mockname: "Mock{{.InterfaceName}}" +filename: "mock_{{.InterfaceName|lower}}_test.go" +outpkg: "{{.PackageName}}" +dir: "{{.InterfaceDir}}" +packages: + github.com/adwski/vidi/internal/api/video: + interfaces: + Store: {} diff --git a/Makefile b/Makefile index 1719643..1dfcd6a 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ +.PHONY: mock +mock: + find . -type f -name "mock_*" -exec rm -rf {} + + mockery + .PHONY: docker-dev docker-dev: cd docker/compose ;\ diff --git a/docker/compose/docker-compose.infra.yml b/docker/compose/docker-compose.infra.yml index 0c9b775..e51dc9c 100644 --- a/docker/compose/docker-compose.infra.yml +++ b/docker/compose/docker-compose.infra.yml @@ -36,7 +36,7 @@ services: - "vidi-db:/var/lib/postgresql/data" - "./initdb:/docker-entrypoint-initdb.d:ro" ports: - - "5400:5432" + - "5432:5432" networks: - vidi diff --git a/e2e/e2e_helpers_test.go b/e2e/e2e_helpers_test.go index 422eb11..685f5df 100644 --- a/e2e/e2e_helpers_test.go +++ b/e2e/e2e_helpers_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" + "github.com/davecgh/go-spew/spew" + "github.com/Eyevinn/dash-mpd/mpd" common "github.com/adwski/vidi/internal/api/model" "github.com/adwski/vidi/internal/api/user/model" @@ -35,6 +37,16 @@ func userRegister(t *testing.T, user *model.UserRequest) *http.Cookie { return getCookieWithToken(t, resp.Cookies()) } +func userRegisterFail(t *testing.T, user any, code int) { + t.Helper() + + resp, body := makeCommonRequest(t, endpointUserRegister, user) + require.True(t, resp.IsError()) + require.NotEmpty(t, body.Error) + require.Empty(t, body.Message) + require.Equal(t, code, resp.StatusCode()) +} + func userLogin(t *testing.T, user *model.UserRequest) *http.Cookie { t.Helper() @@ -45,13 +57,14 @@ func userLogin(t *testing.T, user *model.UserRequest) *http.Cookie { return getCookieWithToken(t, resp.Cookies()) } -func userLoginFail(t *testing.T, user *model.UserRequest) { +func userLoginFail(t *testing.T, user any, code int) { t.Helper() resp, body := makeCommonRequest(t, endpointUserLogin, user) require.Truef(t, resp.IsError(), "user should not exist") require.Empty(t, body.Message) require.NotEmpty(t, body.Error) + require.Equal(t, code, resp.StatusCode()) } func makeCommonRequest(t *testing.T, url string, reqBody interface{}) (*resty.Response, *common.Response) { @@ -105,6 +118,22 @@ func videoWatch(t *testing.T, userCookie *http.Cookie, v *video.Response) *video return &watchBody } +func videoWatchFail(t *testing.T, userCookie *http.Cookie, v *video.Response, code int) { + t.Helper() + + var ( + errBody common.Response + ) + resp, err := resty.New().R().SetHeader("Accept", "application/json"). + SetError(&errBody). + SetCookie(userCookie). + Post(endpointVideo + v.ID + "/watch") + require.NoError(t, err) + require.True(t, resp.IsError()) + require.Equal(t, code, resp.StatusCode()) + require.NotEmpty(t, errBody.Error) +} + func videoUpload(t *testing.T, url string) { t.Helper() @@ -119,6 +148,34 @@ func videoUpload(t *testing.T, url string) { require.Equal(t, http.StatusNoContent, resp.StatusCode()) } +func videoUploadFail(t *testing.T, url string) { + t.Helper() + + f, errF := os.Open("../testFiles/test_seq_h264_high.mp4") + require.NoError(t, errF) + + resp, err := resty.New().R(). + SetHeader("Content-Type", "video/mp4"). + SetBody(f).Post(url) + require.NoError(t, err) + require.True(t, resp.IsError()) + require.Equal(t, http.StatusBadRequest, resp.StatusCode()) +} + +func videoUploadFailGet(t *testing.T, url string) { + t.Helper() + + f, errF := os.Open("../testFiles/test_seq_h264_high.mp4") + require.NoError(t, errF) + + resp, err := resty.New().R(). + SetHeader("Content-Type", "video/mp4"). + SetBody(f).Get(url) + require.NoError(t, err) + require.True(t, resp.IsError()) + require.Equal(t, http.StatusBadRequest, resp.StatusCode()) +} + func videoDelete(t *testing.T, userCookie *http.Cookie, id string) { t.Helper() @@ -157,6 +214,23 @@ func videoGet(t *testing.T, userCookie *http.Cookie, id string) *video.Response return &videoBody } +func videoGetFail(t *testing.T, userCookie *http.Cookie, id string, code int) { + t.Helper() + + var ( + videoBody video.Response + errBody common.Response + ) + resp, err := resty.New().R().SetHeader("Accept", "application/json"). + SetError(&errBody). + SetCookie(userCookie). + SetResult(&videoBody).Get(endpointVideo + id) + require.NoError(t, err) + require.True(t, resp.IsError()) + require.Equal(t, code, resp.StatusCode()) + // require.NotEmpty(t, errBody.Error) +} + func videoGetAll(t *testing.T, userCookie *http.Cookie) []*video.Response { t.Helper() @@ -228,6 +302,26 @@ func watchVideo(t *testing.T, url string) { checkStaticMPD(t, vMpd) downloadSegments(t, url) + + downloadSegmentsFail(t, url) +} + +func downloadSegmentsFail(t *testing.T, url string) { + t.Helper() + + prefixURL := strings.TrimSuffix(url, "/manifest.mpd") + + prefixURLNoSess := prefixURL[:strings.LastIndexByte(prefixURL, '/')] + + r := resty.New() + + downloadSegmentFail(t, r, prefixURL+"/not-existent.mp4", http.StatusNotFound) + downloadSegmentFail(t, r, prefixURLNoSess+"/qweqweqwe/vide1_1.m4s", http.StatusNotFound) + downloadSegmentFail(t, r, prefixURLNoSess, http.StatusBadRequest) + downloadSegmentFail(t, r, prefixURLNoSess+"/qw/", http.StatusBadRequest) + downloadSegmentFail(t, r, prefixURL+"/vide1_init.wrong", http.StatusBadRequest) + downloadSegmentFailPost(t, r, prefixURL+"/vide1_init.mp4", http.StatusBadRequest) + spew.Dump(prefixURLNoSess) } func downloadSegments(t *testing.T, url string) { @@ -263,6 +357,24 @@ func downloadSegment(t *testing.T, r *resty.Client, url, contentType string) { assert.Equal(t, contentType, resp.Header().Get("Content-Type")) } +func downloadSegmentFail(t *testing.T, r *resty.Client, url string, code int) { + t.Helper() + + resp, err := r.R().Get(url) + require.NoError(t, err) + require.True(t, resp.IsError()) + require.Equal(t, code, resp.StatusCode()) +} + +func downloadSegmentFailPost(t *testing.T, r *resty.Client, url string, code int) { + t.Helper() + + resp, err := r.R().Post(url) + require.NoError(t, err) + require.True(t, resp.IsError()) + require.Equal(t, code, resp.StatusCode()) +} + func checkStaticMPD(t *testing.T, vMpd *mpd.MPD) { t.Helper() diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index a70766b..1f84a32 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -5,15 +5,16 @@ package e2e import ( "context" - "github.com/adwski/vidi/internal/app/processor" - "github.com/adwski/vidi/internal/app/streamer" - "github.com/adwski/vidi/internal/app/uploader" - "github.com/adwski/vidi/internal/app/video" "os" "sync" "testing" "time" + "github.com/adwski/vidi/internal/app/processor" + "github.com/adwski/vidi/internal/app/streamer" + "github.com/adwski/vidi/internal/app/uploader" + "github.com/adwski/vidi/internal/app/video" + "github.com/adwski/vidi/internal/app/user" ) diff --git a/e2e/streamer.yaml b/e2e/streamer.yaml index 1104984..8fe3005 100644 --- a/e2e/streamer.yaml +++ b/e2e/streamer.yaml @@ -12,3 +12,6 @@ s3: access_key: admin secret_key: password bucket: vidi +cors: + enable: true + allow_origin: "*" diff --git a/e2e/userapi.yaml b/e2e/userapi.yaml index 219f9f0..1461cb7 100644 --- a/e2e/userapi.yaml +++ b/e2e/userapi.yaml @@ -5,4 +5,4 @@ server: api: prefix: /api/user database: - dsn: postgres://userapi:userapi@localhost:5400/userapi?sslmode=disable + dsn: postgres://userapi:userapi@localhost:5432/userapi?sslmode=disable diff --git a/e2e/userapi_test.go b/e2e/userapi_test.go index 900c8f1..e5b5a34 100644 --- a/e2e/userapi_test.go +++ b/e2e/userapi_test.go @@ -4,6 +4,7 @@ package e2e import ( + "net/http" "testing" "github.com/adwski/vidi/internal/api/user/model" @@ -16,7 +17,7 @@ func TestUserRegistration(t *testing.T) { userLoginFail(t, &model.UserRequest{ Username: "testuser", Password: "testpass", - }) + }, http.StatusUnauthorized) //------------------------------------------------------------------------------- // Register user @@ -26,6 +27,19 @@ func TestUserRegistration(t *testing.T) { Password: "testpass", }) t.Logf("user is registered, token: %v", cookie.Value) + + //------------------------------------------------------------------------------- + // Register existing user + //------------------------------------------------------------------------------- + userRegisterFail(t, &model.UserRequest{ + Username: "testuser", + Password: "testpass", + }, http.StatusConflict) + + //------------------------------------------------------------------------------- + // Register with invalid data + //------------------------------------------------------------------------------- + userRegisterFail(t, "", http.StatusBadRequest) } func TestUserLogin(t *testing.T) { @@ -44,5 +58,18 @@ func TestUserLogin(t *testing.T) { userLoginFail(t, &model.UserRequest{ Username: "testuser2", Password: "testpass2", - }) + }, http.StatusUnauthorized) + + //------------------------------------------------------------------------------- + // Login with wrong password + //------------------------------------------------------------------------------- + userLoginFail(t, &model.UserRequest{ + Username: "testuser", + Password: "testpass2", + }, http.StatusUnauthorized) + + //------------------------------------------------------------------------------- + // Login with invalid params + //------------------------------------------------------------------------------- + userLoginFail(t, "", http.StatusBadRequest) } diff --git a/e2e/videoapi.yaml b/e2e/videoapi.yaml index 24b230a..f4d051b 100644 --- a/e2e/videoapi.yaml +++ b/e2e/videoapi.yaml @@ -5,7 +5,7 @@ server: api: prefix: /api/video database: - dsn: postgres://videoapi:videoapi@localhost:5400/videoapi?sslmode=disable + dsn: postgres://videoapi:videoapi@localhost:5432/videoapi?sslmode=disable redis: dsn: redis://localhost:6379/0 media: diff --git a/e2e/videoapi_test.go b/e2e/videoapi_test.go index 4afe3ed..3de43be 100644 --- a/e2e/videoapi_test.go +++ b/e2e/videoapi_test.go @@ -4,6 +4,8 @@ package e2e import ( + "net/http" + "strings" "testing" "time" @@ -12,12 +14,14 @@ import ( "github.com/stretchr/testify/require" ) -func TestCreateAndDeleteVideo(t *testing.T) { +func TestCreateFail(t *testing.T) { //------------------------------------------------------------------------------- // Create video with no cookie //------------------------------------------------------------------------------- videoCreateFail(t) +} +func TestCreateAndDeleteVideo(t *testing.T) { //------------------------------------------------------------------------------- // Login with existent user //------------------------------------------------------------------------------- @@ -120,3 +124,47 @@ func TestWatchVideo(t *testing.T) { watchVideo(t, watchResponse.WatchURL) } + +func TestFails(t *testing.T) { + //------------------------------------------------------------------------------- + // Login with existent user + //------------------------------------------------------------------------------- + cookie := userLogin(t, &user.UserRequest{ + Username: "testuser", + Password: "testpass", + }) + t.Logf("user logged in, token: %v", cookie.Value) + + //------------------------------------------------------------------------------- + // Create video + //------------------------------------------------------------------------------- + videoResponse := videoCreate(t, cookie) + t.Logf("video created, id: %s, upload url: %v", videoResponse.ID, videoResponse.UploadURL) + + //------------------------------------------------------------------------------- + // Get video + //------------------------------------------------------------------------------- + videoGetFail(t, cookie, "not-existent", http.StatusNotFound) + + //------------------------------------------------------------------------------- + // Get video no auth + //------------------------------------------------------------------------------- + videoGetFail(t, &http.Cookie{Name: "x", Value: "y"}, videoResponse.ID, http.StatusUnauthorized) + + //------------------------------------------------------------------------------- + // Get watch URL no-ready video + //------------------------------------------------------------------------------- + videoWatchFail(t, cookie, videoResponse, http.StatusMethodNotAllowed) + + //------------------------------------------------------------------------------- + // Upload video, invalid request + //------------------------------------------------------------------------------- + videoUploadFailGet(t, videoResponse.UploadURL) + + //------------------------------------------------------------------------------- + // Upload video, invalid requests + //------------------------------------------------------------------------------- + url := videoResponse.UploadURL[:strings.LastIndex(videoResponse.UploadURL, "/")] + videoUploadFail(t, url) + videoUploadFail(t, url+"/qweqwe/") +} diff --git a/go.mod b/go.mod index 6175506..95c1329 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect diff --git a/go.sum b/go.sum index 384da1a..1b9d023 100644 --- a/go.sum +++ b/go.sum @@ -180,6 +180,7 @@ github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/internal/api/store/database_test.go b/internal/api/store/database_test.go new file mode 100644 index 0000000..1757ef5 --- /dev/null +++ b/internal/api/store/database_test.go @@ -0,0 +1,24 @@ +//go:build e2e +// +build e2e + +package store + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestDatabase_UpDown(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + db, err := New(context.Background(), &Config{ + Logger: logger, + DSN: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable", + }) + require.NoError(t, err) + db.Close() +} diff --git a/internal/api/store/migrate_test.go b/internal/api/store/migrate_test.go new file mode 100644 index 0000000..dd7ce78 --- /dev/null +++ b/internal/api/store/migrate_test.go @@ -0,0 +1,99 @@ +//go:build e2e +// +build e2e + +package store + +import ( + "embed" + "testing" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestDatabase_migrateNilMigrationDir(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + db := &Database{ + log: logger, + } + err = db.migrate(&Config{ + Logger: logger, + MigrationsDir: nil, + DSN: "postgres://postgres:postgres@localhost:5432/postgres", + Migrate: true, + }) + require.Error(t, err) + t.Log(err) +} + +func TestDatabase_migrateDisabled(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + db := &Database{ + log: logger, + } + err = db.migrate(&Config{ + Logger: logger, + MigrationsDir: nil, + DSN: "postgres://postgres:postgres@localhost:5432/postgres", + Migrate: false, + }) + require.Nil(t, err) +} + +func TestDatabase_migrateInvalidMigrationDir(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + db := &Database{ + log: logger, + } + err = db.migrate(&Config{ + Logger: logger, + MigrationsDir: &embed.FS{}, + DSN: "postgres://postgres:postgres@localhost:5432/postgres", + Migrate: true, + }) + require.Error(t, err) + t.Log(err) +} + +//go:embed migrations/*.sql +var migrations embed.FS + +func TestDatabase_migrateInvalidDSN(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + db := &Database{ + log: logger, + } + err = db.migrate(&Config{ + Logger: logger, + MigrationsDir: &migrations, + DSN: "qwe://postgres:postgres@localhost:5432/postgres", + Migrate: true, + }) + require.Error(t, err) + t.Log(err) +} + +func TestDatabase_migrateEmptyMigrations(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + db := &Database{ + log: logger, + } + err = db.migrate(&Config{ + Logger: logger, + MigrationsDir: &migrations, + DSN: "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable", + Migrate: true, + }) + require.Error(t, err) + t.Log(err) +} diff --git a/internal/api/store/migrations/test.sql b/internal/api/store/migrations/test.sql new file mode 100644 index 0000000..da84fa5 --- /dev/null +++ b/internal/api/store/migrations/test.sql @@ -0,0 +1 @@ +/* nothing */ diff --git a/internal/api/user/auth/auth.go b/internal/api/user/auth/auth.go index c2f9443..365c10b 100644 --- a/internal/api/user/auth/auth.go +++ b/internal/api/user/auth/auth.go @@ -6,9 +6,8 @@ import ( "net/http" "time" - "github.com/adwski/vidi/internal/generators" - "github.com/adwski/vidi/internal/api/user/model" + "github.com/adwski/vidi/internal/generators" "github.com/golang-jwt/jwt/v5" echojwt "github.com/labstack/echo-jwt/v4" "github.com/labstack/echo/v4" diff --git a/internal/api/user/service.go b/internal/api/user/service.go index dd65db2..b0120e8 100644 --- a/internal/api/user/service.go +++ b/internal/api/user/service.go @@ -135,12 +135,12 @@ func (svc *Service) login(c echo.Context) error { switch { case errors.Is(err, model.ErrNotFound): - return c.JSON(http.StatusNotFound, &common.Response{ - Error: err.Error(), + return c.JSON(http.StatusUnauthorized, &common.Response{ + Error: "incorrect credentials", }) case errors.Is(err, model.ErrIncorrectCredentials): return c.JSON(http.StatusUnauthorized, &common.Response{ - Error: err.Error(), + Error: "incorrect credentials", }) default: svc.logger.Error("internal error", zap.Error(err)) diff --git a/internal/api/user/store/store.go b/internal/api/user/store/store.go index 47b00b4..e99821e 100644 --- a/internal/api/user/store/store.go +++ b/internal/api/user/store/store.go @@ -61,11 +61,11 @@ func (s *Store) Get(ctx context.Context, u *model.User) error { if err := s.Pool().QueryRow(ctx, query, u.Name).Scan(&u.ID, &hash); err != nil { return handleDBErr(err) } - return s.compare(hash, u.Password) + return comparePwd(hash, u.Password) } func (s *Store) Create(ctx context.Context, u *model.User) error { - hash, err := s.hashPwd(u.Password) + hash, err := hashPwd(u.Password) if err != nil { return err } @@ -103,7 +103,7 @@ func handleDBErr(err error) error { return fmt.Errorf("postgress error: %w", pgErr) } -func (s *Store) hashPwd(pwd string) (string, error) { +func hashPwd(pwd string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(pwd), bcryptCost) if err != nil { return "", fmt.Errorf("cannot hash password: %w", err) @@ -111,13 +111,14 @@ func (s *Store) hashPwd(pwd string) (string, error) { return string(b), nil } -// compare does 'special' bcrypt-comparison of hashes since we cannot compare them directly. -func (s *Store) compare(hash, pwd string) error { - if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pwd)); err != nil { - if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { - return model.ErrIncorrectCredentials - } - return fmt.Errorf("cannot compare user hash: %w", err) +// comparePwd does 'special' bcrypt-comparison of hashes since we cannot compare them directly. +func comparePwd(hash, pwd string) error { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pwd)) + if err == nil { + return nil + } + if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) { + return model.ErrIncorrectCredentials } - return nil + return fmt.Errorf("cannot compare user hash: %w", err) } diff --git a/internal/api/user/store/store_test.go b/internal/api/user/store/store_test.go new file mode 100644 index 0000000..2d1a18f --- /dev/null +++ b/internal/api/user/store/store_test.go @@ -0,0 +1,79 @@ +package store + +import ( + "errors" + "testing" + + "github.com/adwski/vidi/internal/api/user/model" + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_handleDBErrNotFound(t *testing.T) { + assert.Equal(t, model.ErrNotFound, handleDBErr(pgx.ErrNoRows)) +} + +func Test_handleDBErrUnknown(t *testing.T) { + assert.Contains(t, handleDBErr(errors.New("err")).Error(), "unknown database error") +} + +func Test_handleDBErrUnknownPG(t *testing.T) { + err := &pgconn.PgError{ + Code: "somecode", + } + assert.Contains(t, handleDBErr(err).Error(), "postgress error") +} + +func Test_handleDBErrAlreadyExists(t *testing.T) { + err := &pgconn.PgError{ + Code: pgerrcode.UniqueViolation, + ConstraintName: constrainUsername, + } + assert.Equal(t, model.ErrAlreadyExists, handleDBErr(err)) +} + +func Test_handleDBErrUIDAlreadyExists(t *testing.T) { + err := &pgconn.PgError{ + Code: pgerrcode.UniqueViolation, + ConstraintName: constrainUID, + } + assert.Equal(t, model.ErrUIDAlreadyExists, handleDBErr(err)) +} + +func TestHashPwdPasswordTooLong(t *testing.T) { + _, err := hashPwd("qweqweqweqqweqweqweqqweqweqweqqweqweqweqqweqweqweqqweqweqweqqweqweqweq123123") + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot hash password") +} + +func TestHashPwd(t *testing.T) { + hash, err := hashPwd("qweqwe") + require.NoError(t, err) + require.NotEmpty(t, hash) +} + +func TestComparePwdErrHash(t *testing.T) { + err := comparePwd("qwr", "qwe") + assert.Contains(t, err.Error(), "cannot compare user hash") +} + +func TestComparePwdIncorrectCreds(t *testing.T) { + hash, err := hashPwd("qweqwe") + require.NoError(t, err) + require.NotEmpty(t, hash) + + err = comparePwd(hash, "qweqwe1") + assert.Equal(t, model.ErrIncorrectCredentials, err) +} + +func TestComparePwd(t *testing.T) { + hash, err := hashPwd("qweqwe") + require.NoError(t, err) + require.NotEmpty(t, hash) + + err = comparePwd(hash, "qweqwe") + require.NoError(t, err) +} diff --git a/internal/api/user/validator_test.go b/internal/api/user/validator_test.go new file mode 100644 index 0000000..541fd75 --- /dev/null +++ b/internal/api/user/validator_test.go @@ -0,0 +1,53 @@ +package user + +import ( + "testing" + + "github.com/adwski/vidi/internal/api/user/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestRequestValidator_Validate(t *testing.T) { + tests := []struct { + name string + args any + err string + }{ + { + name: "no error", + args: &model.UserRequest{ + Username: "testUser", + Password: "testPass", + }, + }, + { + name: "request validation error", + args: &model.UserRequest{ + Username: "test", + Password: "test", + }, + err: "missing required params", + }, + { + name: "unknown error", + args: nil, + err: "unknown error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + rv := NewRequestValidator(logger) + + err = rv.Validate(tt.args) + if tt.err != "" { + assert.Contains(t, err.Error(), tt.err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/internal/api/video/client/client.go b/internal/api/video/client/client.go index 8c265f9..f27df5e 100644 --- a/internal/api/video/client/client.go +++ b/internal/api/video/client/client.go @@ -70,10 +70,6 @@ func (c *Client) UpdateVideoStatus(videoID, param string) error { return c.makeUpdateRequest(videoID, paramNameStatus, param) } -func (c *Client) UpdateVideoLocation(videoID, param string) error { - return c.makeUpdateRequest(videoID, paramNameLocation, param) -} - func (c *Client) makeUpdateRequest(videoID, param, value string) error { response, req := c.constructUpdateRequest() resp, err := req.Put(fmt.Sprintf("%s/service/%s/%s/%s", c.endpoint, videoID, param, value)) diff --git a/internal/api/video/mock_store_test.go b/internal/api/video/mock_store_test.go new file mode 100644 index 0000000..e8a622b --- /dev/null +++ b/internal/api/video/mock_store_test.go @@ -0,0 +1,451 @@ +// Code generated by mockery. DO NOT EDIT. + +package video + +import ( + context "context" + + model "github.com/adwski/vidi/internal/api/video/model" + mock "github.com/stretchr/testify/mock" +) + +// MockStore is an autogenerated mock type for the Store type +type MockStore struct { + mock.Mock +} + +type MockStore_Expecter struct { + mock *mock.Mock +} + +func (_m *MockStore) EXPECT() *MockStore_Expecter { + return &MockStore_Expecter{mock: &_m.Mock} +} + +// Create provides a mock function with given fields: ctx, vi +func (_m *MockStore) Create(ctx context.Context, vi *model.Video) error { + ret := _m.Called(ctx, vi) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *model.Video) error); ok { + r0 = rf(ctx, vi) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockStore_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type MockStore_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - vi *model.Video +func (_e *MockStore_Expecter) Create(ctx interface{}, vi interface{}) *MockStore_Create_Call { + return &MockStore_Create_Call{Call: _e.mock.On("Create", ctx, vi)} +} + +func (_c *MockStore_Create_Call) Run(run func(ctx context.Context, vi *model.Video)) *MockStore_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*model.Video)) + }) + return _c +} + +func (_c *MockStore_Create_Call) Return(_a0 error) *MockStore_Create_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockStore_Create_Call) RunAndReturn(run func(context.Context, *model.Video) error) *MockStore_Create_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function with given fields: ctx, id, userID +func (_m *MockStore) Delete(ctx context.Context, id string, userID string) error { + ret := _m.Called(ctx, id, userID) + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, id, userID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockStore_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type MockStore_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - userID string +func (_e *MockStore_Expecter) Delete(ctx interface{}, id interface{}, userID interface{}) *MockStore_Delete_Call { + return &MockStore_Delete_Call{Call: _e.mock.On("Delete", ctx, id, userID)} +} + +func (_c *MockStore_Delete_Call) Run(run func(ctx context.Context, id string, userID string)) *MockStore_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockStore_Delete_Call) Return(_a0 error) *MockStore_Delete_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockStore_Delete_Call) RunAndReturn(run func(context.Context, string, string) error) *MockStore_Delete_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function with given fields: ctx, id, userID +func (_m *MockStore) Get(ctx context.Context, id string, userID string) (*model.Video, error) { + ret := _m.Called(ctx, id, userID) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 *model.Video + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*model.Video, error)); ok { + return rf(ctx, id, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *model.Video); ok { + r0 = rf(ctx, id, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.Video) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, id, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockStore_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type MockStore_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - userID string +func (_e *MockStore_Expecter) Get(ctx interface{}, id interface{}, userID interface{}) *MockStore_Get_Call { + return &MockStore_Get_Call{Call: _e.mock.On("Get", ctx, id, userID)} +} + +func (_c *MockStore_Get_Call) Run(run func(ctx context.Context, id string, userID string)) *MockStore_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *MockStore_Get_Call) Return(_a0 *model.Video, _a1 error) *MockStore_Get_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockStore_Get_Call) RunAndReturn(run func(context.Context, string, string) (*model.Video, error)) *MockStore_Get_Call { + _c.Call.Return(run) + return _c +} + +// GetAll provides a mock function with given fields: ctx, userID +func (_m *MockStore) GetAll(ctx context.Context, userID string) ([]*model.Video, error) { + ret := _m.Called(ctx, userID) + + if len(ret) == 0 { + panic("no return value specified for GetAll") + } + + var r0 []*model.Video + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]*model.Video, error)); ok { + return rf(ctx, userID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []*model.Video); ok { + r0 = rf(ctx, userID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Video) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, userID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockStore_GetAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAll' +type MockStore_GetAll_Call struct { + *mock.Call +} + +// GetAll is a helper method to define mock.On call +// - ctx context.Context +// - userID string +func (_e *MockStore_Expecter) GetAll(ctx interface{}, userID interface{}) *MockStore_GetAll_Call { + return &MockStore_GetAll_Call{Call: _e.mock.On("GetAll", ctx, userID)} +} + +func (_c *MockStore_GetAll_Call) Run(run func(ctx context.Context, userID string)) *MockStore_GetAll_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *MockStore_GetAll_Call) Return(_a0 []*model.Video, _a1 error) *MockStore_GetAll_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockStore_GetAll_Call) RunAndReturn(run func(context.Context, string) ([]*model.Video, error)) *MockStore_GetAll_Call { + _c.Call.Return(run) + return _c +} + +// GetListByStatus provides a mock function with given fields: ctx, status +func (_m *MockStore) GetListByStatus(ctx context.Context, status model.Status) ([]*model.Video, error) { + ret := _m.Called(ctx, status) + + if len(ret) == 0 { + panic("no return value specified for GetListByStatus") + } + + var r0 []*model.Video + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, model.Status) ([]*model.Video, error)); ok { + return rf(ctx, status) + } + if rf, ok := ret.Get(0).(func(context.Context, model.Status) []*model.Video); ok { + r0 = rf(ctx, status) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.Video) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, model.Status) error); ok { + r1 = rf(ctx, status) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockStore_GetListByStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetListByStatus' +type MockStore_GetListByStatus_Call struct { + *mock.Call +} + +// GetListByStatus is a helper method to define mock.On call +// - ctx context.Context +// - status model.Status +func (_e *MockStore_Expecter) GetListByStatus(ctx interface{}, status interface{}) *MockStore_GetListByStatus_Call { + return &MockStore_GetListByStatus_Call{Call: _e.mock.On("GetListByStatus", ctx, status)} +} + +func (_c *MockStore_GetListByStatus_Call) Run(run func(ctx context.Context, status model.Status)) *MockStore_GetListByStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(model.Status)) + }) + return _c +} + +func (_c *MockStore_GetListByStatus_Call) Return(_a0 []*model.Video, _a1 error) *MockStore_GetListByStatus_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockStore_GetListByStatus_Call) RunAndReturn(run func(context.Context, model.Status) ([]*model.Video, error)) *MockStore_GetListByStatus_Call { + _c.Call.Return(run) + return _c +} + +// Update provides a mock function with given fields: ctx, vi +func (_m *MockStore) Update(ctx context.Context, vi *model.Video) error { + ret := _m.Called(ctx, vi) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *model.Video) error); ok { + r0 = rf(ctx, vi) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockStore_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type MockStore_Update_Call struct { + *mock.Call +} + +// Update is a helper method to define mock.On call +// - ctx context.Context +// - vi *model.Video +func (_e *MockStore_Expecter) Update(ctx interface{}, vi interface{}) *MockStore_Update_Call { + return &MockStore_Update_Call{Call: _e.mock.On("Update", ctx, vi)} +} + +func (_c *MockStore_Update_Call) Run(run func(ctx context.Context, vi *model.Video)) *MockStore_Update_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*model.Video)) + }) + return _c +} + +func (_c *MockStore_Update_Call) Return(_a0 error) *MockStore_Update_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockStore_Update_Call) RunAndReturn(run func(context.Context, *model.Video) error) *MockStore_Update_Call { + _c.Call.Return(run) + return _c +} + +// UpdateLocation provides a mock function with given fields: ctx, vi +func (_m *MockStore) UpdateLocation(ctx context.Context, vi *model.Video) error { + ret := _m.Called(ctx, vi) + + if len(ret) == 0 { + panic("no return value specified for UpdateLocation") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *model.Video) error); ok { + r0 = rf(ctx, vi) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockStore_UpdateLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateLocation' +type MockStore_UpdateLocation_Call struct { + *mock.Call +} + +// UpdateLocation is a helper method to define mock.On call +// - ctx context.Context +// - vi *model.Video +func (_e *MockStore_Expecter) UpdateLocation(ctx interface{}, vi interface{}) *MockStore_UpdateLocation_Call { + return &MockStore_UpdateLocation_Call{Call: _e.mock.On("UpdateLocation", ctx, vi)} +} + +func (_c *MockStore_UpdateLocation_Call) Run(run func(ctx context.Context, vi *model.Video)) *MockStore_UpdateLocation_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*model.Video)) + }) + return _c +} + +func (_c *MockStore_UpdateLocation_Call) Return(_a0 error) *MockStore_UpdateLocation_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockStore_UpdateLocation_Call) RunAndReturn(run func(context.Context, *model.Video) error) *MockStore_UpdateLocation_Call { + _c.Call.Return(run) + return _c +} + +// UpdateStatus provides a mock function with given fields: ctx, vi +func (_m *MockStore) UpdateStatus(ctx context.Context, vi *model.Video) error { + ret := _m.Called(ctx, vi) + + if len(ret) == 0 { + panic("no return value specified for UpdateStatus") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *model.Video) error); ok { + r0 = rf(ctx, vi) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockStore_UpdateStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateStatus' +type MockStore_UpdateStatus_Call struct { + *mock.Call +} + +// UpdateStatus is a helper method to define mock.On call +// - ctx context.Context +// - vi *model.Video +func (_e *MockStore_Expecter) UpdateStatus(ctx interface{}, vi interface{}) *MockStore_UpdateStatus_Call { + return &MockStore_UpdateStatus_Call{Call: _e.mock.On("UpdateStatus", ctx, vi)} +} + +func (_c *MockStore_UpdateStatus_Call) Run(run func(ctx context.Context, vi *model.Video)) *MockStore_UpdateStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*model.Video)) + }) + return _c +} + +func (_c *MockStore_UpdateStatus_Call) Return(_a0 error) *MockStore_UpdateStatus_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockStore_UpdateStatus_Call) RunAndReturn(run func(context.Context, *model.Video) error) *MockStore_UpdateStatus_Call { + _c.Call.Return(run) + return _c +} + +// NewMockStore creates a new instance of MockStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockStore(t interface { + mock.TestingT + Cleanup(func()) +}) *MockStore { + mock := &MockStore{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/api/video/model/status.go b/internal/api/video/model/status.go index 851fd1c..8b3d18a 100644 --- a/internal/api/video/model/status.go +++ b/internal/api/video/model/status.go @@ -55,11 +55,6 @@ func (s *Status) UnmarshalJSON(b []byte) (err error) { switch value := v.(type) { case string: *s, err = GetStatusFromName(value) - case int: - *s = Status(value) - if s.String() == "" { - err = fmt.Errorf("unknown status num: %v", value) - } case float64: *s = Status(int(value)) if s.String() == "" { diff --git a/internal/api/video/model/status_test.go b/internal/api/video/model/status_test.go new file mode 100644 index 0000000..0254608 --- /dev/null +++ b/internal/api/video/model/status_test.go @@ -0,0 +1,83 @@ +package model + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStatus_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + arg []byte + err string + status Status + }{ + { + name: "unmarshall string", + arg: []byte(`"ready"`), + status: StatusReady, + }, + { + name: "unmarshall num", + arg: []byte(`0`), + status: StatusCreated, + }, + { + name: "unmarshall invalid num", + arg: []byte(`-1233`), + err: "unknown status num", + }, + { + name: "unmarshall invalid type", + arg: []byte(`{"a":"b"}`), + err: "invalid type", + }, + { + name: "unmarshall invalid json", + arg: []byte(`qweqwe1231`), + err: "invalid character", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var s Status + + err := s.UnmarshalJSON(tt.arg) + if tt.err == "" { + assert.NoError(t, err) + assert.Equal(t, tt.status, s) + } else { + assert.Contains(t, err.Error(), tt.err) + } + }) + } +} + +func TestGetStatusFromName(t *testing.T) { + tests := []struct { + name string + arg string + want Status + err error + }{ + { + name: "valid status", + arg: "uploaded", + want: StatusUploaded, + }, + { + name: "invalid status", + arg: "qweqweqwasd", + err: errors.New("incorrect status name"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s, err := GetStatusFromName(tt.arg) + assert.Equal(t, tt.want, s) + assert.Equal(t, tt.err, err) + }) + } +} diff --git a/internal/api/video/service.go b/internal/api/video/service.go index 1fb3444..eba3dac 100644 --- a/internal/api/video/service.go +++ b/internal/api/video/service.go @@ -29,7 +29,6 @@ type Store interface { GetListByStatus(ctx context.Context, status model.Status) ([]*model.Video, error) Update(ctx context.Context, vi *model.Video) error - UpdateLocation(ctx context.Context, vi *model.Video) error UpdateStatus(ctx context.Context, vi *model.Video) error } @@ -101,7 +100,6 @@ func NewService(cfg *ServiceConfig) (*Service, error) { // Service zone serviceAPI := api.Group("/service") serviceAPI.Use(authenticator.MiddlewareService()) - serviceAPI.PUT("/:id/location/:location", svc.updateVideoLocation) serviceAPI.PUT("/:id/status/:status", svc.updateVideoStatus) serviceAPI.PUT("/:id", svc.updateVideo) serviceAPI.POST("/search", svc.listVideos) diff --git a/internal/api/video/serviceapi.go b/internal/api/video/serviceapi.go index 19dd376..fef69d3 100644 --- a/internal/api/video/serviceapi.go +++ b/internal/api/video/serviceapi.go @@ -35,24 +35,6 @@ func (svc *Service) getServiceSession(c echo.Context) error { }) } -func (svc *Service) updateVideoLocation(c echo.Context) error { - if err := svc.getServiceSession(c); err != nil { - return err - } - id := c.Param("id") - location := c.Param("location") - if len(location) == 0 { - return c.JSON(http.StatusBadRequest, &common.Response{ - Error: "location cannot be empty", - }) - } - err := svc.s.UpdateLocation(c.Request().Context(), &model.Video{ - ID: id, - Location: location, - }) - return svc.commonResponse(c, err) -} - func (svc *Service) updateVideoStatus(c echo.Context) error { if err := svc.getServiceSession(c); err != nil { return err diff --git a/internal/api/video/serviceapi_test.go b/internal/api/video/serviceapi_test.go new file mode 100644 index 0000000..e151455 --- /dev/null +++ b/internal/api/video/serviceapi_test.go @@ -0,0 +1,41 @@ +package video + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/adwski/vidi/internal/api/user/auth" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestService_getServiceSessionNoAuth(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + svc, err := NewService(&ServiceConfig{ + Logger: logger, + APIPrefix: "/api/video", + WatchURLPrefix: "/watch", + UploadURLPrefix: "/upload", + AuthConfig: auth.Config{ + Secret: "qweqweqwe", + Domain: "domain.com", + HTTPS: false, + Expiration: time.Hour, + }, + }) + require.NoError(t, err) + + req := httptest.NewRequest(http.MethodPost, "/", nil) + rec := httptest.NewRecorder() + c := echo.New().NewContext(req, rec) + err = svc.getServiceSession(c) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnauthorized, c.Response().Status) +} diff --git a/internal/api/video/store/store.go b/internal/api/video/store/store.go index 9ee360f..3eceb23 100644 --- a/internal/api/video/store/store.go +++ b/internal/api/video/store/store.go @@ -114,12 +114,6 @@ func (s *Store) GetListByStatus(ctx context.Context, status model.Status) ([]*mo return videos, nil } -func (s *Store) UpdateLocation(ctx context.Context, vi *model.Video) error { - query := `update videos set location = $2 where id = $1` - tag, err := s.Pool().Exec(ctx, query, vi.ID, vi.Location) - return handleTagOneRowAndErr(&tag, err) -} - func (s *Store) UpdateStatus(ctx context.Context, vi *model.Video) error { query := `update videos set status = $2 where id = $1` tag, err := s.Pool().Exec(ctx, query, vi.ID, int(vi.Status)) @@ -133,13 +127,13 @@ func (s *Store) Update(ctx context.Context, vi *model.Video) error { } func handleTagOneRowAndErr(tag *pgconn.CommandTag, err error) error { - if err == nil { - if tag.RowsAffected() != 1 { - return fmt.Errorf("affected rows: %d, expected: 1", tag.RowsAffected()) - } - return nil + if err != nil { + return handleDBErr(err) + } + if tag.RowsAffected() != 1 { + return fmt.Errorf("affected rows: %d, expected: 1", tag.RowsAffected()) } - return handleDBErr(err) + return nil } func handleDBErr(err error) error { diff --git a/internal/api/video/store/store_test.go b/internal/api/video/store/store_test.go new file mode 100644 index 0000000..7dec22b --- /dev/null +++ b/internal/api/video/store/store_test.go @@ -0,0 +1,55 @@ +package store + +import ( + "errors" + "testing" + + "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" + "github.com/stretchr/testify/require" + + "github.com/adwski/vidi/internal/api/video/model" + "github.com/jackc/pgx/v5" + "github.com/stretchr/testify/assert" +) + +func Test_handleDBErrNotFound(t *testing.T) { + assert.Equal(t, model.ErrNotFound, handleDBErr(pgx.ErrNoRows)) +} + +func Test_handleDBErrUnknown(t *testing.T) { + assert.Contains(t, handleDBErr(errors.New("err")).Error(), "unknown database error") +} + +func Test_handleDBErrUnknownPG(t *testing.T) { + err := &pgconn.PgError{ + Code: "somecode", + } + assert.Contains(t, handleDBErr(err).Error(), "postgress error") +} + +func Test_handleDBErrAlreadyExists(t *testing.T) { + err := &pgconn.PgError{ + Code: pgerrcode.UniqueViolation, + ConstraintName: constrainUID, + } + assert.Equal(t, model.ErrAlreadyExists, handleDBErr(err)) +} + +func Test_handleTagOneRowAndErr(t *testing.T) { + tag := pgconn.NewCommandTag("qwe1") + err := handleTagOneRowAndErr(&tag, nil) + require.NoError(t, err) +} + +func Test_handleTagOneRowAndErrWrongAffected(t *testing.T) { + tag := pgconn.NewCommandTag("qwe11") + err := handleTagOneRowAndErr(&tag, nil) + assert.Contains(t, err.Error(), "affected rows: ") +} + +func Test_handleTagOneRowAndErrCallHandleDBErr(t *testing.T) { + tag := pgconn.NewCommandTag("qwe1") + err := handleTagOneRowAndErr(&tag, errors.New("custom err")) + assert.Contains(t, err.Error(), "custom err") +} diff --git a/internal/api/video/userapi.go b/internal/api/video/userapi.go index 5e206dc..d6e166a 100644 --- a/internal/api/video/userapi.go +++ b/internal/api/video/userapi.go @@ -5,13 +5,12 @@ import ( "errors" "net/http" - "github.com/adwski/vidi/internal/session" - sessionStore "github.com/adwski/vidi/internal/session/store" - common "github.com/adwski/vidi/internal/api/model" "github.com/adwski/vidi/internal/api/user/auth" user "github.com/adwski/vidi/internal/api/user/model" "github.com/adwski/vidi/internal/api/video/model" + "github.com/adwski/vidi/internal/session" + sessionStore "github.com/adwski/vidi/internal/session/store" "github.com/labstack/echo/v4" "go.uber.org/zap" ) @@ -69,14 +68,14 @@ func (svc *Service) watchVideo(c echo.Context) error { } if video.IsErrored() { - return c.JSON(http.StatusOK, &common.Response{ + return c.JSON(http.StatusMethodNotAllowed, &common.Response{ Error: "video cannot be watched", }) } if !video.IsReady() { - return c.JSON(http.StatusOK, &common.Response{ - Message: "video is not ready", + return c.JSON(http.StatusMethodNotAllowed, &common.Response{ + Error: "video is not ready", }) } @@ -110,7 +109,7 @@ func (svc *Service) createVideo(c echo.Context) error { newID, errID := svc.idGen.Get() if errID != nil { svc.logger.Error("cannot generate new video id", zap.Error(errID)) - return c.JSON(http.StatusNotFound, &common.Response{ + return c.JSON(http.StatusInternalServerError, &common.Response{ Error: common.InternalError, }) } diff --git a/internal/api/video/userapi_test.go b/internal/api/video/userapi_test.go new file mode 100644 index 0000000..ead54e8 --- /dev/null +++ b/internal/api/video/userapi_test.go @@ -0,0 +1,286 @@ +//nolint:dupl //similar test cases +package video + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/adwski/vidi/internal/api/user/auth" + "github.com/adwski/vidi/internal/api/video/model" + "github.com/adwski/vidi/internal/generators" + "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestService_getVideoNoSession(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + a, err := auth.NewAuth(&auth.Config{ + Secret: "qweqeqwe", + }) + require.NoError(t, err) + svc := Service{ + logger: logger, + auth: a, + } + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + ctx := echo.New().NewContext(r, w) + + err = svc.getVideo(ctx) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnauthorized, ctx.Response().Status) +} + +func TestService_getVideosNoSession(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + a, err := auth.NewAuth(&auth.Config{ + Secret: "qweqeqwe", + }) + require.NoError(t, err) + svc := Service{ + logger: logger, + auth: a, + s: NewMockStore(t), + } + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + ctx := echo.New().NewContext(r, w) + + err = svc.getVideos(ctx) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnauthorized, ctx.Response().Status) +} + +func TestService_getVideosError(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + a, err := auth.NewAuth(&auth.Config{ + Secret: "qweqeqwe", + }) + require.NoError(t, err) + + s := NewMockStore(t) + svc := Service{ + logger: logger, + auth: a, + s: s, + } + + s.EXPECT().GetAll(mock.Anything, "qweqweqwe").Return(nil, errors.New("err")) + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + ctx := echo.New().NewContext(r, w) + ctx.Set("vUser", &jwt.Token{Claims: &auth.Claims{ + UserID: "qweqweqwe", + }}) + + err = svc.getVideos(ctx) + require.NoError(t, err) + + assert.Equal(t, http.StatusInternalServerError, ctx.Response().Status) +} + +func TestService_watchVideoNoSession(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + a, err := auth.NewAuth(&auth.Config{ + Secret: "qweqeqwe", + }) + require.NoError(t, err) + + svc := Service{ + logger: logger, + auth: a, + } + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + ctx := echo.New().NewContext(r, w) + + err = svc.watchVideo(ctx) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnauthorized, ctx.Response().Status) +} + +func TestService_watchVideoDBError(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + a, err := auth.NewAuth(&auth.Config{ + Secret: "qweqeqwe", + }) + require.NoError(t, err) + + s := NewMockStore(t) + svc := Service{ + logger: logger, + auth: a, + s: s, + } + + s.EXPECT().Get(mock.Anything, mock.Anything, "qweqweqwe").Return(nil, errors.New("err")) + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + ctx := echo.New().NewContext(r, w) + ctx.Set("vUser", &jwt.Token{Claims: &auth.Claims{ + UserID: "qweqweqwe", + }}) + + err = svc.watchVideo(ctx) + require.NoError(t, err) + + assert.Equal(t, http.StatusInternalServerError, ctx.Response().Status) +} + +func TestService_watchVideoError(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + a, err := auth.NewAuth(&auth.Config{ + Secret: "qweqeqwe", + }) + require.NoError(t, err) + + s := NewMockStore(t) + svc := Service{ + logger: logger, + auth: a, + s: s, + } + + s.EXPECT().Get(mock.Anything, mock.Anything, "qweqweqwe").Return(&model.Video{ + Status: model.StatusError, + }, nil) + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + ctx := echo.New().NewContext(r, w) + ctx.Set("vUser", &jwt.Token{Claims: &auth.Claims{ + UserID: "qweqweqwe", + }}) + + err = svc.watchVideo(ctx) + require.NoError(t, err) + + assert.Equal(t, http.StatusMethodNotAllowed, ctx.Response().Status) +} + +func TestService_deleteVideoNoSession(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + a, err := auth.NewAuth(&auth.Config{ + Secret: "qweqeqwe", + }) + require.NoError(t, err) + + svc := Service{ + logger: logger, + auth: a, + } + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + ctx := echo.New().NewContext(r, w) + + err = svc.deleteVideo(ctx) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnauthorized, ctx.Response().Status) +} + +func TestService_deleteVideoDBError(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + a, err := auth.NewAuth(&auth.Config{ + Secret: "qweqeqwe", + }) + require.NoError(t, err) + + s := NewMockStore(t) + svc := Service{ + logger: logger, + auth: a, + s: s, + } + + s.EXPECT().Delete(mock.Anything, mock.Anything, "qweqweqwe").Return(errors.New("err")) + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + ctx := echo.New().NewContext(r, w) + ctx.Set("vUser", &jwt.Token{Claims: &auth.Claims{ + UserID: "qweqweqwe", + }}) + + err = svc.deleteVideo(ctx) + require.NoError(t, err) + + assert.Equal(t, http.StatusInternalServerError, ctx.Response().Status) +} + +func TestService_createVideoNoSession(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + a, err := auth.NewAuth(&auth.Config{ + Secret: "qweqeqwe", + }) + require.NoError(t, err) + + svc := Service{ + logger: logger, + auth: a, + } + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + ctx := echo.New().NewContext(r, w) + + err = svc.createVideo(ctx) + require.NoError(t, err) + + assert.Equal(t, http.StatusUnauthorized, ctx.Response().Status) +} + +func TestService_createVideoDBError(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + a, err := auth.NewAuth(&auth.Config{ + Secret: "qweqeqwe", + }) + require.NoError(t, err) + + s := NewMockStore(t) + svc := Service{ + logger: logger, + auth: a, + s: s, + idGen: generators.NewID(), + } + + s.EXPECT().Create(mock.Anything, mock.Anything).Return(errors.New("err")) + + r := httptest.NewRequest(http.MethodGet, "/", nil) + w := httptest.NewRecorder() + ctx := echo.New().NewContext(r, w) + ctx.Set("vUser", &jwt.Token{Claims: &auth.Claims{ + UserID: "qweqweqwe", + }}) + + err = svc.createVideo(ctx) + require.NoError(t, err) + + assert.Equal(t, http.StatusInternalServerError, ctx.Response().Status) +} diff --git a/internal/app/app.go b/internal/app/app.go index 44718c4..c34dd68 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -137,6 +137,10 @@ func (app *App) configure(configName string) int { return 0 } +func (app *App) SetLogger(logger *zap.Logger) { + app.logger = logger +} + func (app *App) readConfig(name string) error { app.viper.SetConfigName(name) app.viper.SetConfigType("yaml") diff --git a/internal/event/event.go b/internal/event/event.go index f6ca60a..58edda5 100644 --- a/internal/event/event.go +++ b/internal/event/event.go @@ -4,7 +4,6 @@ import "github.com/adwski/vidi/internal/api/video/model" const ( KindUpdateStatus = iota + 1 - KindUpdateLocation KindUpdateStatusAndLocation ) diff --git a/internal/event/notificator/notificator.go b/internal/event/notificator/notificator.go index bcf1c0e..b26fe0b 100644 --- a/internal/event/notificator/notificator.go +++ b/internal/event/notificator/notificator.go @@ -71,8 +71,6 @@ func (n *Notificator) processEvent(ev *event.Event) { switch ev.Kind { case event.KindUpdateStatus: err = n.c.UpdateVideoStatus(ev.Video.ID, ev.Video.Status.String()) - case event.KindUpdateLocation: - err = n.c.UpdateVideoLocation(ev.Video.ID, ev.Video.Location) case event.KindUpdateStatusAndLocation: err = n.c.UpdateVideo(ev.Video.ID, ev.Video.Status.String(), ev.Video.Location) default: diff --git a/internal/event/notificator/notificator_test.go b/internal/event/notificator/notificator_test.go new file mode 100644 index 0000000..1d9b1a5 --- /dev/null +++ b/internal/event/notificator/notificator_test.go @@ -0,0 +1,68 @@ +package notificator + +import ( + "bytes" + "context" + "io" + "sync" + "testing" + + "github.com/adwski/vidi/internal/api/video/model" + "github.com/stretchr/testify/assert" + "go.uber.org/zap/zapcore" + + "github.com/adwski/vidi/internal/event" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +func TestNotificator_RunAndStop(t *testing.T) { + logger, err := zap.NewDevelopment() + require.NoError(t, err) + + n := Notificator{ + logger: logger, + evCh: make(chan *event.Event, 10), + } + + n.evCh <- &event.Event{Kind: 1000} + n.evCh <- &event.Event{Kind: 1000} + n.evCh <- &event.Event{Kind: 1000} + + wg := &sync.WaitGroup{} + ctx, cancel := context.WithCancel(context.Background()) + + wg.Add(1) + go n.Run(ctx, wg, make(chan error)) + + cancel() + wg.Wait() +} + +func TestNotificator_processUnknown(t *testing.T) { + logBuf := bytes.NewBuffer([]byte{}) + logger := newLogger(logBuf) + + n := Notificator{ + logger: logger, + evCh: make(chan *event.Event), + } + + n.processEvent(&event.Event{ + Video: model.Video{}, + Kind: 1000, + }) + assert.Contains(t, logBuf.String(), "unknown event kind") +} + +func newLogger(w io.Writer) *zap.Logger { + encoderCfg := zapcore.EncoderConfig{ + MessageKey: "msg", + LevelKey: "level", + NameKey: "logger", + EncodeLevel: zapcore.LowercaseLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + } + return zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(encoderCfg), zapcore.AddSync(w), zapcore.DebugLevel)) +} diff --git a/internal/media/store/s3/s3.go b/internal/media/store/s3/s3.go index 2b676ac..b00e89d 100644 --- a/internal/media/store/s3/s3.go +++ b/internal/media/store/s3/s3.go @@ -4,6 +4,9 @@ import ( "context" "fmt" "io" + "net/http" + + "github.com/adwski/vidi/internal/media/store" "github.com/minio/minio-go/v7" "go.uber.org/zap" @@ -35,6 +38,10 @@ func (s *Store) Get(ctx context.Context, name string) (io.ReadCloser, int64, err } stat, errS := obj.Stat() if errS != nil { + er := minio.ToErrorResponse(errS) + if er.StatusCode == http.StatusNotFound { + return nil, 0, store.ErrNotFount + } return nil, 0, fmt.Errorf("cannot get object stats: %w", errS) } return obj, stat.Size, nil diff --git a/internal/media/store/store.go b/internal/media/store/store.go new file mode 100644 index 0000000..f0d20a7 --- /dev/null +++ b/internal/media/store/store.go @@ -0,0 +1,5 @@ +package store + +import "errors" + +var ErrNotFount = errors.New("not found") diff --git a/internal/media/streamer/service.go b/internal/media/streamer/service.go index 9e92161..af5190f 100644 --- a/internal/media/streamer/service.go +++ b/internal/media/streamer/service.go @@ -6,6 +6,8 @@ import ( "fmt" "strings" + "github.com/adwski/vidi/internal/media/store" + "github.com/adwski/vidi/internal/media/store/s3" "github.com/adwski/vidi/internal/session" sessionStore "github.com/adwski/vidi/internal/session/store" @@ -108,7 +110,7 @@ func (svc *Service) handleWatch(ctx *fasthttp.RequestCtx) { return } svc.logger.Debug("cannot get session", zap.Error(errSess)) - ctx.Error(internalError, fasthttp.StatusNotFound) + ctx.Error(internalError, fasthttp.StatusInternalServerError) return } @@ -119,6 +121,10 @@ func (svc *Service) handleWatch(ctx *fasthttp.RequestCtx) { // Get segment reader rc, size, errS3 := svc.mediaS.Get(ctx, svc.getSegmentName(sess, path)) if errS3 != nil { + if errors.Is(errS3, store.ErrNotFount) { + ctx.Error(notFoundError, fasthttp.StatusNotFound) + return + } svc.logger.Error("error while retrieving segment", zap.Error(errS3)) ctx.Error(internalError, fasthttp.StatusInternalServerError) return diff --git a/internal/media/uploader/service_test.go b/internal/media/uploader/service_test.go new file mode 100644 index 0000000..154e588 --- /dev/null +++ b/internal/media/uploader/service_test.go @@ -0,0 +1,65 @@ +package uploader + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" +) + +func Test_checkHeader(t *testing.T) { + type args struct { + cType string + cLen int + } + tests := []struct { + name string + args args + contentLength int + err string + }{ + { + name: "valid headers", + args: args{ + cType: "video/mp4", + cLen: 100, + }, + contentLength: 100, + }, + { + name: "wrong content-length", + args: args{ + cType: "video/mp4", + cLen: 0, + }, + err: "wrong or missing content length", + }, + { + name: "wrong content-type", + args: args{ + cType: "qweqwe/asdas", + cLen: 100, + }, + err: "wrong content type", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := &fasthttp.RequestCtx{ + Request: fasthttp.Request{Header: fasthttp.RequestHeader{}}, + } + ctx.Request.Header.SetContentLength(tt.args.cLen) + ctx.Request.Header.SetContentType(tt.args.cType) + + ln, err := checkHeader(ctx) + if tt.err != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.err) + } else { + require.NoError(t, err) + assert.Equal(t, tt.contentLength, ln) + } + }) + } +} diff --git a/internal/mp4/dumper.go b/internal/mp4/dumper.go index 38bcd41..a580e0c 100644 --- a/internal/mp4/dumper.go +++ b/internal/mp4/dumper.go @@ -17,14 +17,13 @@ const ( // Dump prints out codec info and segmentation patter for mp4 file. func Dump(path string, segDuration time.Duration) { + dump(os.Stdout, path, segDuration) +} +func dump(w io.Writer, path string, segDuration time.Duration) { segmentDuration := segDuration if segDuration < defaultSegmentDuration { segmentDuration = defaultSegmentDuration } - - dump(os.Stdout, path, segmentDuration) -} -func dump(w io.Writer, path string, segmentDuration time.Duration) { mF, err := mp4ff.ReadMP4File(path) if err != nil { fmt.Printf("cannot open mp4 file: %v\n", err) diff --git a/pkg/config/config.go b/pkg/config/config.go index 97cc95c..7d95b79 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -27,6 +27,7 @@ func (vec *ViperEC) GetDuration(key string) time.Duration { d, err := cast.ToDurationE(vec.Get(key)) if err != nil { vec.errs[key] = err + return 0 } if d == 0 { vec.errs[key] = errors.New("cannot be zero") diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..9c802b3 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,301 @@ +//nolint:dupl // similar test flows +package config + +import ( + "bytes" + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupViper(t *testing.T, r io.Reader) *ViperEC { + t.Helper() + + vec := NewViperEC() + vec.SetConfigType("yaml") + err := vec.ReadConfig(r) + require.NoError(t, err) + + return vec +} + +func TestViperEC_GetDuration(t *testing.T) { + type args struct { + config io.Reader + key string + } + tests := []struct { + name string + args args + want time.Duration + err string + }{ + { + name: "get duration", + args: args{ + key: "duration", + config: bytes.NewReader([]byte("duration: 5s")), + }, + want: 5 * time.Second, + err: "", + }, + { + name: "get duration error", + args: args{ + key: "duration", + config: bytes.NewReader([]byte("duration: sss")), + }, + err: "invalid", + }, + { + name: "get duration zero", + args: args{ + key: "duration", + config: bytes.NewReader([]byte("duration: 0")), + }, + err: "cannot be zero", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vec := setupViper(t, tt.args.config) + dur := vec.GetDuration(tt.args.key) + assert.Equal(t, tt.want, dur) + if tt.err == "" { + assert.Empty(t, vec.Errors()) + } else { + assert.True(t, vec.HasErrors()) + assert.Contains(t, vec.Errors()[tt.args.key].Error(), tt.err) + } + }) + } +} + +func TestViperEC_GetBool(t *testing.T) { + type args struct { + config io.Reader + key string + } + tests := []struct { + name string + args args + want bool + err string + }{ + { + name: "get bool", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: true")), + }, + want: true, + err: "", + }, + { + name: "get duration error", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: sss")), + }, + err: "invalid", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vec := setupViper(t, tt.args.config) + val := vec.GetBool(tt.args.key) + assert.Equal(t, tt.want, val) + if tt.err == "" { + assert.Empty(t, vec.Errors()) + } else { + assert.True(t, vec.HasErrors()) + assert.Contains(t, vec.Errors()[tt.args.key].Error(), tt.err) + } + }) + } +} + +func TestViperEC_GetBoolNoError(t *testing.T) { + type args struct { + config io.Reader + key string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "get bool", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: true")), + }, + want: true, + }, + { + name: "get duration error", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: sss")), + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vec := setupViper(t, tt.args.config) + val := vec.GetBool(tt.args.key) + assert.Equal(t, tt.want, val) + }) + } +} + +func TestViperEC_GetURL(t *testing.T) { + type args struct { + config io.Reader + key string + } + tests := []struct { + name string + args args + want string + err string + }{ + { + name: "get url", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: http://wer.asd")), + }, + want: "http://wer.asd", + err: "", + }, + { + name: "get url error", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: :wer.asd")), + }, + err: "is not valid url", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vec := setupViper(t, tt.args.config) + dur := vec.GetURL(tt.args.key) + assert.Equal(t, tt.want, dur) + if tt.err == "" { + assert.Empty(t, vec.Errors()) + } else { + assert.True(t, vec.HasErrors()) + assert.Contains(t, vec.Errors()[tt.args.key].Error(), tt.err) + } + }) + } +} + +func TestViperEC_GetURIPrefix(t *testing.T) { + type args struct { + config io.Reader + key string + } + tests := []struct { + name string + args args + want string + err string + }{ + { + name: "get uri prefix", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: /api")), + }, + want: "/api", + }, + { + name: "get uri suffix error", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: /api/")), + }, + err: "must not end with", + }, + { + name: "get uri empty", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: ")), + }, + err: "cannot be empty", + }, + { + name: "get uri prefix error", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: api/")), + }, + err: "must start with", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vec := setupViper(t, tt.args.config) + dur := vec.GetURIPrefix(tt.args.key) + assert.Equal(t, tt.want, dur) + if tt.err == "" { + assert.Empty(t, vec.Errors()) + } else { + assert.True(t, vec.HasErrors()) + assert.Contains(t, vec.Errors()[tt.args.key].Error(), tt.err) + } + }) + } +} + +func TestViperEC_GetString(t *testing.T) { + type args struct { + config io.Reader + key string + } + tests := []struct { + name string + args args + want string + err string + }{ + { + name: "get string", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: qwe")), + }, + want: "qwe", + }, + { + name: "get string empty", + args: args{ + key: "key", + config: bytes.NewReader([]byte("key: ")), + }, + err: "cannot be empty", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vec := setupViper(t, tt.args.config) + dur := vec.GetString(tt.args.key) + assert.Equal(t, tt.want, dur) + if tt.err == "" { + assert.Empty(t, vec.Errors()) + } else { + assert.True(t, vec.HasErrors()) + assert.Contains(t, vec.Errors()[tt.args.key].Error(), tt.err) + } + }) + } +}