From 92482f250b309b725f190512c9967d18c29af13f Mon Sep 17 00:00:00 2001 From: Roman Golenok Date: Mon, 3 Jun 2019 13:01:02 +0300 Subject: [PATCH 01/13] Fix publishing game --- pkg/orm/event_bus.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/orm/event_bus.go b/pkg/orm/event_bus.go index 88d5c28..395b71b 100644 --- a/pkg/orm/event_bus.go +++ b/pkg/orm/event_bus.go @@ -34,12 +34,12 @@ func (bus *eventBus) PublishGameChanges(gameId uuid.UUID) error { var genres []model.GameGenre var tags []model.GameTag - err := bus.db.Model(model.Game{ID: gameId}).First(&game).Error + err := bus.db.Model(model.Game{}).Where("id = ?", gameId).First(&game).Error if err != nil { return err } - err = bus.db.Model(model.Media{ID: gameId}).First(&media).Error + err = bus.db.Model(model.Media{}).Where("id = ?", gameId).First(&media).Error if err != nil { return err } From 40dfb0f8c47d4e886b3b32366ce8872f638d9582 Mon Sep 17 00:00:00 2001 From: Roman Golenok Date: Wed, 5 Jun 2019 16:06:56 +0300 Subject: [PATCH 02/13] Fix tags mapping in event bus --- pkg/orm/event_bus.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pkg/orm/event_bus.go b/pkg/orm/event_bus.go index 395b71b..5715a54 100644 --- a/pkg/orm/event_bus.go +++ b/pkg/orm/event_bus.go @@ -5,11 +5,14 @@ import ( "github.com/ProtocolONE/qilin-common/pkg/proto" "github.com/ProtocolONE/rabbitmq/pkg" "github.com/jinzhu/gorm" + "github.com/lib/pq" "github.com/satori/go.uuid" "go.uber.org/zap" "qilin-api/pkg/model" "qilin-api/pkg/model/game" "qilin-api/pkg/model/utils" + "strconv" + "strings" "time" ) @@ -45,7 +48,8 @@ func (bus *eventBus) PublishGameChanges(gameId uuid.UUID) error { } if len(game.Tags) > 0 { - err = bus.db.Model(model.GameTag{}).Where("id in (?)", game.Tags).Find(&tags).Error + tt := toPgArray(game.Tags) + err = bus.db.Model(model.GameTag{}).Where("id in (?)", tt).Find(&tags).Error if err != nil { return err } @@ -54,7 +58,7 @@ func (bus *eventBus) PublishGameChanges(gameId uuid.UUID) error { if len(game.GenreAddition) > 0 || game.GenreMain != 0 { filter := game.GenreAddition filter = append(filter, game.GenreMain) - err = bus.db.Model(model.GameGenre{}).Where("id in (?)", filter).Find(&genres).Error + err = bus.db.Model(model.GameGenre{}).Where("id in (?)", toPgArray(filter)).Find(&genres).Error if err != nil { return err } @@ -64,6 +68,14 @@ func (bus *eventBus) PublishGameChanges(gameId uuid.UUID) error { return bus.broker.Publish("game_changed", gameObject, nil) } +func toPgArray(array pq.Int64Array) string { + var s []string + for _, a := range array { + s = append(s, strconv.FormatInt(a, 10)) + } + return strings.Join(s, ",") +} + func (bus *eventBus) PublishGameDelete(gameId uuid.UUID) error { gameObject := &proto.GameDeleted{ID: gameId.String()} return bus.broker.Publish("game_deleted", gameObject, nil) From 58497cbc8b8be47ac909844b54c908e5cb2e4bcd Mon Sep 17 00:00:00 2001 From: Valentin Vesvalo Date: Fri, 7 Jun 2019 15:46:07 +0300 Subject: [PATCH 03/13] update helm chart --- .helm/values.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.helm/values.yaml b/.helm/values.yaml index c9d3981..1b9c55d 100644 --- a/.helm/values.yaml +++ b/.helm/values.yaml @@ -47,7 +47,8 @@ backend: - QILINAPI_AUTH1_CLIENTID - QILINAPI_AUTH1_CLIENTSECRET - QILINAPI_EVENTBUS_CONNECTION - + - QILINAPI_IMAGINARY_SECRET + resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little From 6a54664fd25e9bccd2ee02accc401a620bd60c00 Mon Sep 17 00:00:00 2001 From: Roman Golenok Date: Wed, 12 Jun 2019 16:20:08 +0300 Subject: [PATCH 04/13] Event bus fixed --- go.mod | 2 +- pkg/orm/event_bus.go | 47 ++++++++++++++++++++++++++++++++++++--- pkg/orm/rating_service.go | 2 +- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index b28b348..84b118d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module qilin-api require ( cloud.google.com/go v0.34.0 // indirect github.com/ProtocolONE/authone-jwt-verifier-golang v0.0.0-20190415120635-9cfb6c93ff5e - github.com/ProtocolONE/qilin-common v0.0.0-20190426102144-91ad4f805cfd + github.com/ProtocolONE/qilin-common v0.0.0-20190612131602-2a1edd55e5c2 github.com/ProtocolONE/rabbitmq v0.0.0-20190129162844-9f24367e139c github.com/ProtocolONE/rbac v0.0.0-20190520124240-22f5d2b74988 github.com/casbin/redis-adapter v0.0.0-20190105032110-b36d844dade5 diff --git a/pkg/orm/event_bus.go b/pkg/orm/event_bus.go index 5715a54..8f15efa 100644 --- a/pkg/orm/event_bus.go +++ b/pkg/orm/event_bus.go @@ -8,6 +8,7 @@ import ( "github.com/lib/pq" "github.com/satori/go.uuid" "go.uber.org/zap" + "qilin-api/pkg/mapper" "qilin-api/pkg/model" "qilin-api/pkg/model/game" "qilin-api/pkg/model/utils" @@ -64,7 +65,17 @@ func (bus *eventBus) PublishGameChanges(gameId uuid.UUID) error { } } - gameObject := MapGameObject(&game, &media, tags, genres) + var ratings model.GameRating + if err := bus.db.Model(model.GameRating{}).Where("game_id = ?", gameId).First(&ratings).Error; err != nil && !gorm.IsRecordNotFoundError(err) { + return err + } + + description := model.GameDescr{} + if err := bus.db.Model(model.GameDescr{}).Where("game_id = ?", gameId).First(&description).Error; err != nil && !gorm.IsRecordNotFoundError(err) { + return err + } + + gameObject := MapGameObject(&game, &media, tags, genres, ratings, description) return bus.broker.Publish("game_changed", gameObject, nil) } @@ -81,10 +92,10 @@ func (bus *eventBus) PublishGameDelete(gameId uuid.UUID) error { return bus.broker.Publish("game_deleted", gameObject, nil) } -func MapGameObject(game *model.Game, media *model.Media, tags []model.GameTag, genre []model.GameGenre) *proto.GameObject { +func MapGameObject(game *model.Game, media *model.Media, tags []model.GameTag, genre []model.GameGenre, ratings model.GameRating, descr model.GameDescr) *proto.GameObject { return &proto.GameObject{ ID: game.ID.String(), - Description: "", + Description: MapLocalizedString(descr.Description), Name: game.Title, Title: game.Title, Developer: &proto.LinkObject{ID: "", Title: game.Developers}, @@ -99,9 +110,39 @@ func MapGameObject(game *model.Game, media *model.Media, tags []model.GameTag, g FeaturesControl: game.FeaturesCtrl, Features: game.FeaturesCommon, Media: MapMedia(media), + Ratings: MapRatings(ratings), + GameSite: descr.GameSite, + Reviews: MapReviews(descr.Reviews), + Tagline: MapLocalizedString(descr.Tagline), + Publisher: &proto.LinkObject{ID: "", Title: game.Publishers}, } } +func MapReviews(reviews game.GameReviews) []*proto.Review { + if reviews == nil { + return nil + } + + var result []*proto.Review + for _, review := range reviews { + result = append(result, &proto.Review{ + Link: review.Link, + PressName: review.PressName, + Quote: review.Quote, + Score: review.Score, + }) + } + + return result +} + +func MapRatings(rating model.GameRating) *proto.Ratings { + result := &proto.Ratings{} + err := mapper.Map(rating, result) + zap.L().Error("Can't map ratings", zap.Error(err)) + return result +} + func MapMedia(media *model.Media) *proto.Media { if media == nil { return nil diff --git a/pkg/orm/rating_service.go b/pkg/orm/rating_service.go index eac4809..ad0eed8 100644 --- a/pkg/orm/rating_service.go +++ b/pkg/orm/rating_service.go @@ -46,7 +46,7 @@ func (s *RatingService) SaveRatingsForGame(id uuid.UUID, newRating *model.GameRa rating := model.GameRating{} - err := s.db.Model(&model.Game{ID: id}).Related(&rating).Error + err := s.db.Model(&model.Game{}).Where("id = ?", id).Related(&rating).Error if err == gorm.ErrRecordNotFound { rating.CreatedAt = time.Now() From c50b2ffd8e11916773b508fad9a6b930012903b4 Mon Sep 17 00:00:00 2001 From: Roman Golenok Date: Wed, 12 Jun 2019 16:46:23 +0300 Subject: [PATCH 05/13] fix tests --- pkg/orm/rating_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/orm/rating_service.go b/pkg/orm/rating_service.go index ad0eed8..eac4809 100644 --- a/pkg/orm/rating_service.go +++ b/pkg/orm/rating_service.go @@ -46,7 +46,7 @@ func (s *RatingService) SaveRatingsForGame(id uuid.UUID, newRating *model.GameRa rating := model.GameRating{} - err := s.db.Model(&model.Game{}).Where("id = ?", id).Related(&rating).Error + err := s.db.Model(&model.Game{ID: id}).Related(&rating).Error if err == gorm.ErrRecordNotFound { rating.CreatedAt = time.Now() From e99ee2d090c867184c386f59178396804d2ea042 Mon Sep 17 00:00:00 2001 From: Vladimir Stafievsky Date: Wed, 12 Jun 2019 17:39:45 +0300 Subject: [PATCH 06/13] Rabbit env variable added --- .helm/values.yaml | 1 + docker-compose.yaml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.helm/values.yaml b/.helm/values.yaml index 1b9c55d..ff2a616 100644 --- a/.helm/values.yaml +++ b/.helm/values.yaml @@ -31,6 +31,7 @@ backend: - QILINAPI_DATABASE_DEBUG - QILINAPI_ENFORCER_HOST - QILINAPI_ENFORCER_PORT + - QILINAPI_EVENTBUS_CONNECTION - QILINAPI_LOG_LEVEL - QILINAPI_LOG_REPORT_CALLER - QILINAPI_MAILER_HOST diff --git a/docker-compose.yaml b/docker-compose.yaml index 33b3ad1..fe608f9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -71,6 +71,7 @@ services: environment: - GO111MODULE=on - QILINAPI_ENFORCER_HOST=qilin-redis + - QILINAPI_EVENTBUS_CONNECTION=amqp://rabbitmq:5672 - QILINAPI_SERVER_PORT=3001 - QILINAPI_SERVER_ALLOW_ORIGINS=http://127.0.0.1 - QILINAPI_SERVER_DEBUG=true @@ -86,4 +87,4 @@ services: networks: p1devnet: - external: true \ No newline at end of file + external: true From a81494fee7699f20245a04a697425d9ab4fe9ea7 Mon Sep 17 00:00:00 2001 From: Roman Golenok Date: Wed, 12 Jun 2019 19:38:17 +0300 Subject: [PATCH 07/13] Hotfix with tags mapping in event bus --- pkg/orm/event_bus.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/orm/event_bus.go b/pkg/orm/event_bus.go index 8f15efa..1a4edb1 100644 --- a/pkg/orm/event_bus.go +++ b/pkg/orm/event_bus.go @@ -12,8 +12,6 @@ import ( "qilin-api/pkg/model" "qilin-api/pkg/model/game" "qilin-api/pkg/model/utils" - "strconv" - "strings" "time" ) @@ -79,12 +77,12 @@ func (bus *eventBus) PublishGameChanges(gameId uuid.UUID) error { return bus.broker.Publish("game_changed", gameObject, nil) } -func toPgArray(array pq.Int64Array) string { - var s []string +func toPgArray(array pq.Int64Array) []int64 { + var s []int64 for _, a := range array { - s = append(s, strconv.FormatInt(a, 10)) + s = append(s, a) } - return strings.Join(s, ",") + return s } func (bus *eventBus) PublishGameDelete(gameId uuid.UUID) error { From 4438ef91f6e4e259dc7ba28cdd1ffe7905f6af22 Mon Sep 17 00:00:00 2001 From: Vladimir Stafievsky Date: Wed, 12 Jun 2019 20:57:44 +0300 Subject: [PATCH 08/13] Docker compose fix --- docker-compose.yaml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index fe608f9..cd1a95b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -23,6 +23,14 @@ services: ports: - "6379:6379" - "6380:6380" + + qilineventbus: + image: "rabbitmq:3-management" + ports: + - "15672:15672" + - "5672:5672" + networks: + - p1devnet qilinapi-postgres: image: postgres:10.5 @@ -59,6 +67,7 @@ services: depends_on: - qilinapi-postgres - qilin-redis + - qilineventbus ports: - "3001:3001" networks: @@ -71,7 +80,7 @@ services: environment: - GO111MODULE=on - QILINAPI_ENFORCER_HOST=qilin-redis - - QILINAPI_EVENTBUS_CONNECTION=amqp://rabbitmq:5672 + - QILINAPI_EVENTBUS_CONNECTION=amqp://qilineventbus:5672 - QILINAPI_SERVER_PORT=3001 - QILINAPI_SERVER_ALLOW_ORIGINS=http://127.0.0.1 - QILINAPI_SERVER_DEBUG=true From e4af192acd6f3af7a93d6cfe45a5c6d55de743d8 Mon Sep 17 00:00:00 2001 From: Roman Golenok Date: Thu, 13 Jun 2019 13:36:54 +0300 Subject: [PATCH 09/13] Fix nil ref in JsonB getValue --- pkg/model/jsonb.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pkg/model/jsonb.go b/pkg/model/jsonb.go index 6d7c593..558cbbf 100644 --- a/pkg/model/jsonb.go +++ b/pkg/model/jsonb.go @@ -23,12 +23,24 @@ func (j *JSONB) Scan(value interface{}) error { } func (j JSONB) GetString(key string) string { - return j[key].(string) + if j == nil { + return "" + } + + if s, ok := j[key].(string); ok { + return s + } + return "" } func (j JSONB) GetStringArray(key string) []string { if j == nil { return nil } - return j[key].([]string) + + if result, ok := j[key].([]string); ok { + return result + } + + return nil } From 21313a40b869deb00bc6f22163e57c9db14379d8 Mon Sep 17 00:00:00 2001 From: Roman Golenok Date: Thu, 20 Jun 2019 16:33:38 +0300 Subject: [PATCH 10/13] Disable ratings mapping because of fatal bug --- pkg/orm/event_bus.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/orm/event_bus.go b/pkg/orm/event_bus.go index 1a4edb1..61cebcc 100644 --- a/pkg/orm/event_bus.go +++ b/pkg/orm/event_bus.go @@ -108,7 +108,7 @@ func MapGameObject(game *model.Game, media *model.Media, tags []model.GameTag, g FeaturesControl: game.FeaturesCtrl, Features: game.FeaturesCommon, Media: MapMedia(media), - Ratings: MapRatings(ratings), + //Ratings: MapRatings(ratings), GameSite: descr.GameSite, Reviews: MapReviews(descr.Reviews), Tagline: MapLocalizedString(descr.Tagline), From 1855478088f4928a673faa61db5bb635513ad6bb Mon Sep 17 00:00:00 2001 From: Roman Golenok Date: Thu, 20 Jun 2019 17:42:54 +0300 Subject: [PATCH 11/13] Updated media object in domain and in event bus sync --- pkg/api/media_router_test.go | 2 +- pkg/model/game.go | 7 +----- pkg/model/media.go | 9 +++---- pkg/orm/event_bus.go | 45 +++++++++++------------------------ pkg/orm/media_service_test.go | 29 ++++++++++------------ 5 files changed, 34 insertions(+), 58 deletions(-) diff --git a/pkg/api/media_router_test.go b/pkg/api/media_router_test.go index f839ac6..84236e8 100644 --- a/pkg/api/media_router_test.go +++ b/pkg/api/media_router_test.go @@ -32,7 +32,7 @@ func Test_MediaRouter(t *testing.T) { var ( TestID = "029ce039-888a-481a-a831-cde7ff4e50b8" - emptyObject = `{"coverImage":null,"coverVideo":null,"trailers":null,"screenshots":null,"store":null,"capsule":null}` + emptyObject = `{"coverImage":{"en":""},"coverVideo":{"en":""},"trailers":{"en":null},"screenshots":{"en":null},"store":null,"capsule":null}` partialObject = `{"coverImage":{"en":"123", "ru":"321"},"coverVideo":{"en":"123", "ru":"321"},"trailers":{"en":["123"], "ru":["321"]},"screenshots":{"en":["123"], "ru":["321"]},"store":null,"capsule":null}` ) diff --git a/pkg/model/game.go b/pkg/model/game.go index 40b367c..a494f44 100644 --- a/pkg/model/game.go +++ b/pkg/model/game.go @@ -123,10 +123,5 @@ func (p *ProductGameImpl) GetType() ProductType { } func (p *ProductGameImpl) GetImage() (res *utils.LocalizedString) { - res = &utils.LocalizedString{} - if p.Media.CoverImage == nil { - return - } - _ = p.Media.CoverImage.Scan(res) - return + return &p.Media.CoverImage } diff --git a/pkg/model/media.go b/pkg/model/media.go index 1aaf123..572933f 100644 --- a/pkg/model/media.go +++ b/pkg/model/media.go @@ -1,6 +1,7 @@ package model import ( + "qilin-api/pkg/model/utils" "time" uuid "github.com/satori/go.uuid" @@ -14,12 +15,12 @@ type Media struct { UpdatedAt time.Time // localized cover image of game - CoverImage JSONB `gorm:"type:jsonb"` - CoverVideo JSONB `gorm:"type:jsonb"` + CoverImage utils.LocalizedString `gorm:"type:jsonb"` + CoverVideo utils.LocalizedString `gorm:"type:jsonb"` // localized trailers video of game - Trailers JSONB `gorm:"type:jsonb"` + Trailers utils.LocalizedStringArray `gorm:"type:jsonb"` // localized screenshots video of game - Screenshots JSONB `gorm:"type:jsonb"` + Screenshots utils.LocalizedStringArray `gorm:"type:jsonb"` // localized store of game Store JSONB `gorm:"type:jsonb"` diff --git a/pkg/orm/event_bus.go b/pkg/orm/event_bus.go index 61cebcc..e42fc56 100644 --- a/pkg/orm/event_bus.go +++ b/pkg/orm/event_bus.go @@ -108,7 +108,7 @@ func MapGameObject(game *model.Game, media *model.Media, tags []model.GameTag, g FeaturesControl: game.FeaturesCtrl, Features: game.FeaturesCommon, Media: MapMedia(media), - //Ratings: MapRatings(ratings), + Ratings: MapRatings(ratings), GameSite: descr.GameSite, Reviews: MapReviews(descr.Reviews), Tagline: MapLocalizedString(descr.Tagline), @@ -143,44 +143,27 @@ func MapRatings(rating model.GameRating) *proto.Ratings { func MapMedia(media *model.Media) *proto.Media { if media == nil { + zap.L().Error("Media is empty") return nil } return &proto.Media{ - CoverImage: MapJsonbToLocalizedString(media.CoverImage), - CoverVideo: MapJsonbToLocalizedString(media.CoverVideo), - Trailers: MapJsonbToLocalizedStringArray(media.Trailers), - Screenshots: MapJsonbToLocalizedStringArray(media.Screenshots), + CoverImage: MapLocalizedString(media.CoverImage), + CoverVideo: MapLocalizedString(media.CoverVideo), + Trailers: MapLocalizedStringArray(media.Trailers), + Screenshots: MapLocalizedStringArray(media.Screenshots), } } -func MapJsonbToLocalizedStringArray(jsonb model.JSONB) *proto.LocalizedStringArray { - if jsonb == nil { - return nil - } +func MapLocalizedStringArray(array utils.LocalizedStringArray) *proto.LocalizedStringArray { return &proto.LocalizedStringArray{ - EN: jsonb.GetStringArray("en"), - RU: jsonb.GetStringArray("ru"), - FR: jsonb.GetStringArray("fr"), - DE: jsonb.GetStringArray("de"), - ES: jsonb.GetStringArray("es"), - IT: jsonb.GetStringArray("it"), - PT: jsonb.GetStringArray("pt"), - } -} - -func MapJsonbToLocalizedString(jsonb model.JSONB) *proto.LocalizedString { - if jsonb == nil { - return nil - } - return &proto.LocalizedString{ - EN: jsonb.GetString("en"), - RU: jsonb.GetString("ru"), - FR: jsonb.GetString("fr"), - DE: jsonb.GetString("de"), - ES: jsonb.GetString("es"), - IT: jsonb.GetString("it"), - PT: jsonb.GetString("pt"), + EN: array.EN, + PT: array.PT, + IT: array.IT, + RU: array.RU, + FR: array.FR, + ES: array.ES, + DE: array.DE, } } diff --git a/pkg/orm/media_service_test.go b/pkg/orm/media_service_test.go index 1eda05e..17fe2f7 100644 --- a/pkg/orm/media_service_test.go +++ b/pkg/orm/media_service_test.go @@ -5,6 +5,7 @@ import ( "github.com/satori/go.uuid" "math/rand" "qilin-api/pkg/model" + "qilin-api/pkg/model/utils" "qilin-api/pkg/orm" "qilin-api/pkg/test" "testing" @@ -74,25 +75,21 @@ func (suite *MediaServiceTestSuite) TestCreateMediaShouldChangeGameInDB() { id, _ := uuid.FromString(Id) game := model.Media{ ID: uuid.NewV4(), - CoverImage: model.JSONB{ - "ru": RandStringRunes(10), - "en": RandStringRunes(10), + CoverImage: utils.LocalizedString{ + RU: RandStringRunes(10), + EN: RandStringRunes(10), }, - Trailers: model.JSONB{ - "ru": []string{RandStringRunes(10), RandStringRunes(10)}, - "en": []string{RandStringRunes(10), RandStringRunes(10)}, + Trailers: utils.LocalizedStringArray{ + RU: []string{RandStringRunes(10), RandStringRunes(10)}, + EN: []string{RandStringRunes(10), RandStringRunes(10)}, }, - Screenshots: model.JSONB{ - "ru": []string{RandStringRunes(10), RandStringRunes(10)}, - "en": []string{RandStringRunes(10), RandStringRunes(10)}, + Screenshots: utils.LocalizedStringArray{ + RU: []string{RandStringRunes(10), RandStringRunes(10)}, + EN: []string{RandStringRunes(10), RandStringRunes(10)}, }, - Store: model.JSONB{ - "ru": RandStringRunes(10), - "en": RandStringRunes(10), - }, - CoverVideo: model.JSONB{ - "ru": RandStringRunes(10), - "en": RandStringRunes(10), + CoverVideo: utils.LocalizedString{ + RU: RandStringRunes(10), + EN: RandStringRunes(10), }, Capsule: model.JSONB{ "generic": map[string]interface{}{ From 35f31b3a60aa8636c0564a02cfdfe72dba6a0e70 Mon Sep 17 00:00:00 2001 From: Roman Golenok Date: Thu, 13 Jun 2019 17:05:24 +0300 Subject: [PATCH 12/13] Key packages with key provider implementations --- pkg/api/key_list_router.go | 99 ++++++++++++++ pkg/api/key_list_router_test.go | 188 ++++++++++++++++++++++++++ pkg/api/key_package_router.go | 154 +++++++++++++++++++++ pkg/api/server.go | 12 ++ pkg/model/key.go | 29 ++-- pkg/model/key_package.go | 33 +++++ pkg/model/key_stream.go | 34 ++--- pkg/orm/database.go | 6 + pkg/orm/key_list_provider.go | 107 +++++++++++++++ pkg/orm/key_list_provider_test.go | 160 ++++++++++++++++++++++ pkg/orm/key_list_service.go | 34 +++++ pkg/orm/key_list_service_test.go | 107 +++++++++++++++ pkg/orm/key_package_service.go | 94 +++++++++++++ pkg/orm/key_package_service_test.go | 160 ++++++++++++++++++++++ pkg/orm/key_stream_service.go | 70 ++++++++++ pkg/orm/key_stream_service_test.go | 133 ++++++++++++++++++ pkg/orm/platform_key_provider.go | 119 ++++++++++++++++ pkg/orm/platform_key_provider_test.go | 19 +++ 18 files changed, 1515 insertions(+), 43 deletions(-) create mode 100644 pkg/api/key_list_router.go create mode 100644 pkg/api/key_list_router_test.go create mode 100644 pkg/api/key_package_router.go create mode 100644 pkg/model/key_package.go create mode 100644 pkg/orm/key_list_provider.go create mode 100644 pkg/orm/key_list_provider_test.go create mode 100644 pkg/orm/key_list_service.go create mode 100644 pkg/orm/key_list_service_test.go create mode 100644 pkg/orm/key_package_service.go create mode 100644 pkg/orm/key_package_service_test.go create mode 100644 pkg/orm/key_stream_service.go create mode 100644 pkg/orm/key_stream_service_test.go create mode 100644 pkg/orm/platform_key_provider.go create mode 100644 pkg/orm/platform_key_provider_test.go diff --git a/pkg/api/key_list_router.go b/pkg/api/key_list_router.go new file mode 100644 index 0000000..f12d733 --- /dev/null +++ b/pkg/api/key_list_router.go @@ -0,0 +1,99 @@ +package api + +import ( + "bufio" + "github.com/labstack/echo/v4" + uuid "github.com/satori/go.uuid" + "io" + "net/http" + "qilin-api/pkg/api/rbac_echo" + "qilin-api/pkg/model" + "qilin-api/pkg/orm" + "strings" +) + +type AddKeyListDTO struct { + Keys []string `json:"keys" validate:"required"` +} + +type KeyListRouter struct { + keyListService model.KeyListService +} + +func InitKeyListRouter(router *echo.Group, keyListService model.KeyListService) (*KeyListRouter, error){ + keyRouter := KeyListRouter{ + keyListService: keyListService, + } + r := rbac_echo.Group(router, "/packages/:packageId/keypackages/:keyPackageId", &keyRouter, []string{"*", model.PackageType, model.VendorDomain}) + r.POST("/keys", keyRouter.AddKeys, nil) + r.POST("/file", keyRouter.AddFileKeys, nil) + + return &keyRouter, nil +} + +func (router *KeyListRouter) AddFileKeys(ctx echo.Context) error { + keyPackageIdParam := ctx.Param("keyPackageId") + keyPackageId, err := uuid.FromString(keyPackageIdParam) + if err != nil { + return orm.NewServiceError(http.StatusBadRequest, "keyPackageId is wrong") + } + + file, err := ctx.FormFile("keys") + if err != nil { + return orm.NewServiceError(http.StatusBadRequest, err) + } + + src, err := file.Open() + if err != nil { + return orm.NewServiceError(http.StatusBadRequest, err) + } + defer src.Close() + + reader := bufio.NewReader(src) + var line string + var codes []string + shouldBreak := false + for shouldBreak == false { + line, err = reader.ReadString('\n') + if err != nil { + if err != io.EOF { + return orm.NewServiceError(http.StatusBadRequest, err) + } + shouldBreak = true + } + line = strings.Trim(line, "\t\n") + codes = append(codes, line) + } + if err := router.keyListService.AddKeys(keyPackageId, codes); err != nil { + return err + } + + return ctx.NoContent(http.StatusOK) +} + +func (router *KeyListRouter) AddKeys(ctx echo.Context) error { + keyPackageIdParam := ctx.Param("keyPackageId") + keyPackageId, err := uuid.FromString(keyPackageIdParam) + if err != nil { + return orm.NewServiceError(http.StatusBadRequest, "keyPackageId is wrong") + } + + dto := &AddKeyListDTO{} + if err := ctx.Bind(dto); err != nil { + return orm.NewServiceError(http.StatusBadRequest, err) + } + if errs := ctx.Validate(dto); errs != nil { + return orm.NewServiceError(http.StatusUnprocessableEntity, errs) + } + + err = router.keyListService.AddKeys(keyPackageId, dto.Keys) + if err != nil { + return err + } + + return ctx.NoContent(http.StatusOK) +} + +func (*KeyListRouter) GetOwner(ctx rbac_echo.AppContext) (string, error) { + return GetOwnerForPackage(ctx) +} diff --git a/pkg/api/key_list_router_test.go b/pkg/api/key_list_router_test.go new file mode 100644 index 0000000..0163a62 --- /dev/null +++ b/pkg/api/key_list_router_test.go @@ -0,0 +1,188 @@ +package api + +import ( + "bytes" + "github.com/labstack/echo/v4" + uuid "github.com/satori/go.uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "gopkg.in/go-playground/validator.v9" + "io" + "mime/multipart" + "net/http" + "net/http/httptest" + "qilin-api/pkg/model" + "qilin-api/pkg/model/utils" + "qilin-api/pkg/orm" + qilintest "qilin-api/pkg/test" + "strings" + "testing" +) + +type KeyListRouterTestSuite struct { + suite.Suite + db *orm.Database + echo *echo.Echo + router *KeyListRouter + rightKeyPackage uuid.UUID + wrongKeyPackage uuid.UUID + rightKeyStream uuid.UUID + keyPackage uuid.UUID +} + +func Test_KeyListRouter(t *testing.T) { + suite.Run(t, new(KeyListRouterTestSuite)) +} + +func (suite *KeyListRouterTestSuite) SetupTest() { + shouldBe := require.New(suite.T()) + config, err := qilintest.LoadTestConfig() + if err != nil { + suite.FailNow("Unable to load config", "%v", err) + } + db, err := orm.NewDatabase(&config.Database) + if err != nil { + suite.FailNow("Unable to connect to database", "%v", err) + } + + if err := db.DropAllTables(); err != nil { + assert.FailNow(suite.T(), "Unable to drop tables", err) + } + if err := db.Init(); err != nil { + assert.FailNow(suite.T(), "Unable to init tables", err) + } + + suite.rightKeyPackage = uuid.NewV4() + suite.wrongKeyPackage = uuid.NewV4() + suite.keyPackage = uuid.NewV4() + + keyListStreeam := model.KeyStream{ + Type: model.ListKeyStream, + } + keyListStreeam.ID = uuid.NewV4() + suite.rightKeyStream = keyListStreeam.ID + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&keyListStreeam).Error) + + gamePackage := model.Package{Name: utils.LocalizedString{EN: "test package"}} + gamePackage.ID = suite.keyPackage + + shouldBe.Nil(db.DB().Model(model.Package{}).Create(&gamePackage).Error) + + keyPackage := model.KeyPackage{ + Name: "Test name", + PackageID: suite.keyPackage, + KeyStreamID: suite.rightKeyStream, + KeyStreamType: model.ListKeyStream, + } + keyPackage.ID = suite.rightKeyPackage + shouldBe.Nil(db.DB().Model(model.KeyPackage{}).Create(&keyPackage).Error) + + platformListStreeam := model.KeyStream{ + Type: model.PlatformKeysStream, + } + platformListStreeam.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&platformListStreeam).Error) + suite.wrongKeyPackage = platformListStreeam.ID + + platformKeyPackage := model.KeyPackage{ + Name: "Another name", + PackageID: uuid.NewV4(), + KeyStreamType: model.PlatformKeysStream, + } + platformKeyPackage.ID = suite.wrongKeyPackage + shouldBe.Nil(db.DB().Model(model.KeyPackage{}).Create(&platformKeyPackage).Error) + + e := echo.New() + e.Validator = &QilinValidator{validator: validator.New()} + + service := orm.NewKeyListService(db) + suite.router, err = InitKeyListRouter(e.Group("/api/v1"), service) + suite.db = db + suite.echo = e + shouldBe.Nil(err) +} + +func (suite *KeyListRouterTestSuite) TearDownTest() { + if err := suite.db.DropAllTables(); err != nil { + panic(err) + } + if err := suite.db.Close(); err != nil { + panic(err) + } +} + +func (suite *KeyListRouterTestSuite) TestAddKeys() { + shouldBe := require.New(suite.T()) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"keys":["QWERTY","TESTSOMECODE"]}`)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := suite.echo.NewContext(req, rec) + c.SetPath("/api/v1/packages/:packageId/keypackages/:keyPackageId") + c.SetParamNames("packageId", "keyPackageId") + c.SetParamValues(suite.keyPackage.String(), suite.rightKeyPackage.String()) + + err := suite.router.AddKeys(c) + shouldBe.Nil(err) + shouldBe.Equal(200, rec.Code) +} + +func (suite *KeyListRouterTestSuite) TestAddKeysBadRequest() { + shouldBe := require.New(suite.T()) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"":somethingwronghere"}`)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := suite.echo.NewContext(req, rec) + c.SetPath("/api/v1/packages/:packageId/keypackages/:keyPackageId") + c.SetParamNames("packageId", "keyPackageId") + c.SetParamValues(suite.keyPackage.String(), suite.rightKeyPackage.String()) + + err := suite.router.AddKeys(c) + shouldBe.NotNil(err) + shouldBe.Equal(400, err.(*orm.ServiceError).Code) +} + +func (suite *KeyListRouterTestSuite) TestAddKeysNotFound() { + shouldBe := require.New(suite.T()) + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"keys":["QWERTY","TESTSOMECODE"]}`)) + req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON) + rec := httptest.NewRecorder() + c := suite.echo.NewContext(req, rec) + c.SetPath("/api/v1/packages/:packageId/keypackages/:keyPackageId") + c.SetParamNames("packageId", "keyPackageId") + c.SetParamValues(uuid.NewV4().String(), uuid.NewV4().String()) + + err := suite.router.AddKeys(c) + shouldBe.NotNil(err) + shouldBe.Equal(404, err.(*orm.ServiceError).Code) +} + +func (suite *KeyListRouterTestSuite) TestAddFile() { + shouldBe := require.New(suite.T()) + values := map[string]io.Reader { + "keys": strings.NewReader("QWERTY\nTEST\nOPIUY"), + } + var b bytes.Buffer + w := multipart.NewWriter(&b) + for key, r := range values { + var fw io.Writer + fw, err := w.CreateFormFile(key, key) + shouldBe.Nil(err) + if _, err := io.Copy(fw, r); err != nil { + shouldBe.FailNow("Can't copy fw to r") + } + } + shouldBe.Nil(w.Close()) + + req := httptest.NewRequest(http.MethodPost, "/", &b) + req.Header.Set(echo.HeaderContentType, w.FormDataContentType()) + rec := httptest.NewRecorder() + c := suite.echo.NewContext(req, rec) + c.SetPath("/api/v1/packages/:packageId/keypackages/:keyPackageId") + c.SetParamNames("packageId", "keyPackageId") + c.SetParamValues(suite.keyPackage.String(), suite.rightKeyPackage.String()) + + err := suite.router.AddFileKeys(c) + shouldBe.Nil(err) + shouldBe.Equal(200, rec.Code) +} \ No newline at end of file diff --git a/pkg/api/key_package_router.go b/pkg/api/key_package_router.go new file mode 100644 index 0000000..9a39090 --- /dev/null +++ b/pkg/api/key_package_router.go @@ -0,0 +1,154 @@ +package api + +import ( + "errors" + "github.com/labstack/echo/v4" + uuid "github.com/satori/go.uuid" + "net/http" + "qilin-api/pkg/api/rbac_echo" + "qilin-api/pkg/model" + "qilin-api/pkg/orm" + "time" +) + +type ( + KeyPackageDTO struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Created string `json:"created"` + Updated string `json:"updated"` + } + + CreateKeyPackageDTO struct { + Name string `json:"name" validate:"required"` + Type string `json:"type" validate:"required"` + } + + ChangeKeyPackageDTO struct { + Name string `json:"name" validate:"required"` + } +) + +type keyPackageRouter struct { + keyPackageService model.KeyPackageService +} + +func InitKeyPackageRouter(router *echo.Group, keyPackageService model.KeyPackageService) (*keyPackageRouter, error) { + if keyPackageService == nil { + return nil, errors.New("Key Package service must be provided") + } + + keyRouter := keyPackageRouter{ + keyPackageService: keyPackageService, + } + + r := rbac_echo.Group(router, "/packages/:packageId", &keyRouter, []string{"*", model.PackageType, model.VendorDomain}) + r.GET("/keypackages", keyRouter.GetList, nil) + r.POST("/keypackages", keyRouter.Create, nil) + r.GET("/keypackages/:keyPackageId", keyRouter.Get, nil) + r.PUT("/keypackages/:keyPackageId", keyRouter.Change, nil) + + return &keyRouter, nil +} + +func (router *keyPackageRouter) Get(ctx echo.Context) (err error) { + packageId, err := getKeyPackageId(ctx) + if err != nil { + return orm.NewServiceError(http.StatusBadRequest, err) + } + + keyPackage, err := router.keyPackageService.Get(packageId) + if err != nil { + return err + } + + return ctx.JSON(http.StatusOK, mapKeyPackage(keyPackage)) +} + +func (router *keyPackageRouter) GetList(ctx echo.Context) (err error) { + packageId, err := getPackageId(ctx) + if err != nil { + return orm.NewServiceError(http.StatusBadRequest, err) + } + + keyPackages, err := router.keyPackageService.List(packageId) + if err != nil { + return err + } + + result := []KeyPackageDTO{} + for _, keyPackage := range keyPackages { + result = append(result, mapKeyPackage(&keyPackage)) + } + + return ctx.JSON(http.StatusOK, result) +} + +func (router *keyPackageRouter) Create(ctx echo.Context) (err error) { + packageId, err := getPackageId(ctx) + if err != nil { + return orm.NewServiceError(http.StatusBadRequest, err) + } + + dto := &CreateKeyPackageDTO{} + if err := ctx.Bind(dto); err != nil { + return orm.NewServiceError(http.StatusBadRequest, err) + } + if errs := ctx.Validate(dto); errs != nil { + return orm.NewServiceError(http.StatusUnprocessableEntity, errs) + } + + keyPackage, err := router.keyPackageService.Create(packageId, dto.Name, model.KeyStreamType(dto.Type)) + if err != nil { + return err + } + + return ctx.JSON(http.StatusCreated, mapKeyPackage(keyPackage)) +} + +func (router *keyPackageRouter) Change(ctx echo.Context) (err error) { + packageId, err := getKeyPackageId(ctx) + if err != nil { + return orm.NewServiceError(http.StatusBadRequest, err) + } + + dto := &ChangeKeyPackageDTO{} + if err := ctx.Bind(dto); err != nil { + return orm.NewServiceError(http.StatusBadRequest, err) + } + if errs := ctx.Validate(dto); errs != nil { + return orm.NewServiceError(http.StatusUnprocessableEntity, errs) + } + + keyPackage, err := router.keyPackageService.Update(packageId, dto.Name) + if err != nil { + return err + } + + return ctx.JSON(http.StatusOK, mapKeyPackage(keyPackage)) +} + +func getPackageId(ctx echo.Context) (uuid.UUID, error) { + packageIdStr := ctx.Param("packageId") + return uuid.FromString(packageIdStr) +} + +func getKeyPackageId(ctx echo.Context) (uuid.UUID, error) { + packageIdStr := ctx.Param("keyPackageId") + return uuid.FromString(packageIdStr) +} + +func (*keyPackageRouter) GetOwner(ctx rbac_echo.AppContext) (string, error) { + return GetOwnerForPackage(ctx) +} + +func mapKeyPackage(keyPackage *model.KeyPackage) KeyPackageDTO { + return KeyPackageDTO{ + Type: keyPackage.KeyStreamType.String(), + Name: keyPackage.Name, + ID: keyPackage.ID.String(), + Created: keyPackage.CreatedAt.Format(time.RFC3339), + Updated: keyPackage.UpdatedAt.Format(time.RFC3339), + } +} diff --git a/pkg/api/server.go b/pkg/api/server.go index 9c46c13..db3a714 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -131,6 +131,18 @@ func (s *Server) setupRoutes( return err } + keyStreamService := orm.NewKeyStreamService(s.db) + keyPackageService := orm.NewKeyPackageService(s.db, keyStreamService) + + if _, err := InitKeyPackageRouter(s.Router, keyPackageService); err != nil { + return err + } + + keyListService := orm.NewKeyListService(s.db) + if _, err := InitKeyListRouter(s.Router, keyListService); err != nil { + return err + } + notificationService, err := orm.NewNotificationService(s.db, s.notifier, s.centrifugoSecret) if err != nil { return err diff --git a/pkg/model/key.go b/pkg/model/key.go index f03247e..34883b4 100644 --- a/pkg/model/key.go +++ b/pkg/model/key.go @@ -1,27 +1,14 @@ package model -import "time" +import ( + uuid "github.com/satori/go.uuid" + "time" +) type Key struct { - // Unique key identifier in qilin system - ID string `json:"id" validate:"required"` + Model - StreamID string `json:"id" validate:"stream_id"` - - // Unique game ID in qilin system - GameID string `bson:"gameId"` - - ActivationIP string `bson:"activation_ip"` - - // date of create key in system - CreatedAt time.Time `json:"created_at"` - - // date of last update key in system - UpdatedAt time.Time `json:"updated_at"` - - // date of activation key in system - ActivatedAt time.Time `json:"activated_at"` - - // date of deletion key in system - DeletedAt time.Time `json:"deleted_at"` + KeyStreamID uuid.UUID + ActivationCode string `gorm:"index:activation_code;not null"` + RedeemTime *time.Time } diff --git a/pkg/model/key_package.go b/pkg/model/key_package.go new file mode 100644 index 0000000..4e2c596 --- /dev/null +++ b/pkg/model/key_package.go @@ -0,0 +1,33 @@ +package model + +import uuid "github.com/satori/go.uuid" + +type KeyPackage struct { + Model + Name string + KeyStreamID uuid.UUID + PackageID uuid.UUID + KeyStreamType KeyStreamType +} + +type KeyStreamType string + +func (s KeyStreamType) String() string { + return string(s) +} + +const ( + ListKeyStream KeyStreamType = "key_list" + PlatformKeysStream KeyStreamType = "key_platform" +) + +type KeyPackageService interface { + Create(packageId uuid.UUID, name string, providerType KeyStreamType) (*KeyPackage, error) + Update(keyPackageId uuid.UUID, name string) (*KeyPackage, error) + List(packageId uuid.UUID) ([]KeyPackage, error) + Get(keyPackageId uuid.UUID) (*KeyPackage, error) +} + +type KeyListService interface { + AddKeys(keyPackageId uuid.UUID, keys []string) error +} diff --git a/pkg/model/key_stream.go b/pkg/model/key_stream.go index 13f884a..d7f4c73 100644 --- a/pkg/model/key_stream.go +++ b/pkg/model/key_stream.go @@ -1,29 +1,19 @@ package model -import "time" +import uuid "github.com/satori/go.uuid" type KeyStream struct { - // Unique key identifier in qilin system - ID string `json:"id" validate:"required"` - - // Unique game ID in qilin system - GameID string `bson:"gameId"` - - Source int32 `bson:"source"` - - Restrictions int32 `bson:"restrictions"` - - ActivationIP string `bson:"activation_ip"` - - // date of create key in system - CreatedAt time.Time `json:"created_at"` - - // date of last update key in system - UpdatedAt time.Time `json:"updated_at"` + Model + Type KeyStreamType +} - // date of activation key in system - ActivatedAt time.Time `json:"activated_at"` +type KeyStreamProvider interface { + Redeem() (Key, error) + RedeemList(count int) ([]Key, error) + AddKeys(codes []string) error +} - // date of deletion key in system - DeletedAt time.Time `json:"deleted_at"` +type KeyStreamService interface { + Get(streamId uuid.UUID) (KeyStreamProvider, error) + Create(keyProviderType KeyStreamType) (uuid.UUID, error) } diff --git a/pkg/orm/database.go b/pkg/orm/database.go index a4a7d5a..2eafadd 100644 --- a/pkg/orm/database.go +++ b/pkg/orm/database.go @@ -53,6 +53,9 @@ func (db *Database) Init() error { &model.StoreBundle{}, &model.Dlc{}, &model.Achievement{}, + &model.KeyPackage{}, + &model.Key{}, + &model.KeyStream{}, ).Error } @@ -82,6 +85,9 @@ func (db *Database) DropAllTables() error { model.StoreBundle{}, model.Dlc{}, model.Achievement{}, + model.KeyPackage{}, + model.Key{}, + model.KeyStream{}, ).Error } return nil diff --git a/pkg/orm/key_list_provider.go b/pkg/orm/key_list_provider.go new file mode 100644 index 0000000..a68d989 --- /dev/null +++ b/pkg/orm/key_list_provider.go @@ -0,0 +1,107 @@ +package orm + +import ( + "github.com/jinzhu/gorm" + uuid "github.com/satori/go.uuid" + "net/http" + "qilin-api/pkg/model" + "time" +) + +type keyListProvider struct { + streamId uuid.UUID + db *Database +} + +func NewKeyListProvider(streamId uuid.UUID, db *Database) (model.KeyStreamProvider, error) { + keyStream := model.KeyStream{} + err := db.DB().Model(model.KeyStream{}).Where("id = ?", streamId).First(&keyStream).Error + if err != nil { + if gorm.IsRecordNotFoundError(err) { + return nil, NewServiceErrorf(http.StatusNotFound, "Key Stream with id `%s` not found", streamId) + } + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + if keyStream.Type != model.ListKeyStream { + return nil, NewServiceErrorf(http.StatusBadRequest, "Key Stream with id `%s` is not key list stream", streamId) + } + + return &keyListProvider{streamId: streamId, db: db}, nil +} + +func (provider *keyListProvider) Redeem() (model.Key, error) { + key := model.Key{} + err := provider.db.DB().Model(model.Key{}).Where("key_stream_id = ?", provider.streamId).Where("redeem_time IS NULL").First(&key).Error + if err != nil { + if gorm.IsRecordNotFoundError(err) { + return key, NewServiceErrorf(http.StatusBadRequest, "No free keys in the stream `%s`", provider.streamId) + } + return key, NewServiceError(http.StatusInternalServerError, err) + } + + t := time.Now().UTC() + key.RedeemTime = &t + err = provider.db.DB().Model(model.Key{}).Update(key).Error + if err != nil { + return key, NewServiceError(http.StatusInternalServerError, err) + } + + return key, nil +} + +func (provider *keyListProvider) RedeemList(count int) ([]model.Key, error) { + transaction := provider.db.DB().Begin() + + var keys []model.Key + err := transaction.Model(model.Key{}).Where("key_stream_id = ?", provider.streamId).Where("redeem_time IS NULL").Limit(count).Find(&keys).Error + if err != nil { + transaction.Rollback() + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + if len(keys) != count { + transaction.Rollback() + return nil, NewServiceErrorf(http.StatusBadRequest, "Keys not enough. Stream have `%d` keys.", len(keys)) + } + + for _, key := range keys { + t := time.Now().UTC() + key.RedeemTime = &t + err := transaction.Model(key).Update(key).Error + if err != nil { + transaction.Rollback() + return nil, NewServiceError(http.StatusInternalServerError, err) + } + } + + err = transaction.Commit().Error + if err != nil { + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + return keys, nil +} + +func (provider *keyListProvider) AddKeys(codes []string) error { + transaction := provider.db.DB().Begin() + + for _, code := range codes { + key := model.Key{ + ActivationCode: code, + KeyStreamID: provider.streamId, + } + key.ID = uuid.NewV4() + err := transaction.Model(model.Key{}).Create(&key).Error + if err != nil { + transaction.Rollback() + return NewServiceError(http.StatusBadRequest, err) + } + } + + err := transaction.Commit().Error + if err != nil { + return NewServiceError(http.StatusBadRequest, err) + } + return nil +} diff --git a/pkg/orm/key_list_provider_test.go b/pkg/orm/key_list_provider_test.go new file mode 100644 index 0000000..2dc8d96 --- /dev/null +++ b/pkg/orm/key_list_provider_test.go @@ -0,0 +1,160 @@ +package orm + +import ( + uuid "github.com/satori/go.uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "qilin-api/pkg/model" + qilintest "qilin-api/pkg/test" + "testing" +) + +type KeyListProviderTestSuite struct { + suite.Suite + db *Database + + rightKeyPackage uuid.UUID + rightKeyStream uuid.UUID + wrongKeyPackage uuid.UUID + wrongKeyStream uuid.UUID + provider model.KeyStreamProvider +} + +func Test_KeyListProvider(t *testing.T) { + suite.Run(t, new(KeyListProviderTestSuite)) +} + +func (suite *KeyListProviderTestSuite) SetupTest() { + shouldBe := require.New(suite.T()) + config, err := qilintest.LoadTestConfig() + if err != nil { + suite.FailNow("Unable to load config", "%v", err) + } + db, err := NewDatabase(&config.Database) + if err != nil { + suite.FailNow("Unable to connect to database", "%v", err) + } + + if err := db.DropAllTables(); err != nil { + assert.FailNow(suite.T(), "Unable to drop tables", err) + } + if err := db.Init(); err != nil { + assert.FailNow(suite.T(), "Unable to init tables", err) + } + + suite.rightKeyPackage = uuid.NewV4() + suite.wrongKeyPackage = uuid.NewV4() + + keyListStreeam := model.KeyStream{ + Type: model.ListKeyStream, + } + keyListStreeam.ID = uuid.NewV4() + suite.rightKeyStream = keyListStreeam.ID + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&keyListStreeam).Error) + + keyPackage := model.KeyPackage{ + Name: "Test name", + PackageID: uuid.NewV4(), + KeyStreamType: model.ListKeyStream, + } + keyPackage.ID = suite.rightKeyPackage + shouldBe.Nil(db.DB().Model(model.KeyPackage{}).Create(&keyPackage).Error) + + platformListStreeam := model.KeyStream{ + Type: model.PlatformKeysStream, + } + platformListStreeam.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&platformListStreeam).Error) + suite.wrongKeyPackage = platformListStreeam.ID + + platformKeyPackage := model.KeyPackage{ + Name: "Another name", + PackageID: uuid.NewV4(), + KeyStreamType: model.PlatformKeysStream, + } + platformKeyPackage.ID = suite.wrongKeyPackage + shouldBe.Nil(db.DB().Model(model.KeyPackage{}).Create(&platformKeyPackage).Error) + + suite.db = db + suite.provider, err = NewKeyListProvider(keyListStreeam.ID, db) + shouldBe.Nil(err) +} + +func (suite *KeyListProviderTestSuite) TearDownTest() { + if err := suite.db.DropAllTables(); err != nil { + panic(err) + } + if err := suite.db.Close(); err != nil { + panic(err) + } +} + +func (suite *KeyListProviderTestSuite) TestInitNewProvider() { + shouldBe := require.New(suite.T()) + _, err := NewKeyListProvider(uuid.NewV4(), suite.db) + shouldBe.NotNil(err) + + _, err = NewKeyListProvider(suite.wrongKeyStream, suite.db) + shouldBe.NotNil(err) +} + +func (suite *KeyListProviderTestSuite) TestAddKeysList() { + shouldBe := require.New(suite.T()) + keys := []string{ + model.RandStringRunes(10), + model.RandStringRunes(10), + } + err := suite.provider.AddKeys(keys) + shouldBe.Nil(err) + + err = suite.provider.AddKeys(keys) + shouldBe.Nil(err) +} + +func (suite *KeyListProviderTestSuite) TestRedeemList() { + shouldBe := require.New(suite.T()) + // no one keys + _, err := suite.provider.RedeemList(10) + shouldBe.NotNil(err) + + for i := 0; i < 10; i++{ + key := model.Key{ + KeyStreamID: suite.rightKeyStream, + ActivationCode: model.RandStringRunes(10), + } + key.ID = uuid.NewV4() + shouldBe.Nil(suite.db.DB().Create(&key).Error) + } + + keys, err := suite.provider.RedeemList(10) + shouldBe.Nil(err) + shouldBe.NotNil(keys) + shouldBe.Equal(10, len(keys)) + + _, err = suite.provider.RedeemList(10) + shouldBe.NotNil(err) +} + +func (suite *KeyListProviderTestSuite) TestRedeem() { + shouldBe := require.New(suite.T()) + // no one keys + _, err := suite.provider.Redeem() + shouldBe.NotNil(err) + + key := model.Key{ + KeyStreamID: suite.rightKeyStream, + ActivationCode: model.RandStringRunes(10), + } + key.ID = uuid.NewV4() + + shouldBe.Nil(suite.db.DB().Create(&key).Error) + key1, err := suite.provider.Redeem() + shouldBe.Nil(err) + shouldBe.NotNil(key1) + shouldBe.Equal(key.ActivationCode, key1.ActivationCode) + shouldBe.NotNil(key1.RedeemTime) + + _, err = suite.provider.Redeem() + shouldBe.NotNil(err) +} diff --git a/pkg/orm/key_list_service.go b/pkg/orm/key_list_service.go new file mode 100644 index 0000000..88685a2 --- /dev/null +++ b/pkg/orm/key_list_service.go @@ -0,0 +1,34 @@ +package orm + +import ( + "github.com/jinzhu/gorm" + "github.com/satori/go.uuid" + "net/http" + "qilin-api/pkg/model" +) + +type keyListService struct { + db *Database + +} + +func NewKeyListService(db *Database) model.KeyListService { + return &keyListService{db: db} +} + +func (service *keyListService) AddKeys(keyPackageId uuid.UUID, keys []string) error { + keyPackage := model.KeyPackage{} + err := service.db.DB().Model(model.KeyPackage{}).Where("id = ?", keyPackageId).First(&keyPackage).Error + if err != nil { + if gorm.IsRecordNotFoundError(err) { + return NewServiceErrorf(http.StatusNotFound, "Key Package with id `%s` not found", keyPackageId) + } + return NewServiceError(http.StatusInternalServerError, err) + } + + streamProvider, err := NewKeyListProvider(keyPackage.KeyStreamID, service.db) + if err != nil { + return err + } + return streamProvider.AddKeys(keys) +} diff --git a/pkg/orm/key_list_service_test.go b/pkg/orm/key_list_service_test.go new file mode 100644 index 0000000..6bd5425 --- /dev/null +++ b/pkg/orm/key_list_service_test.go @@ -0,0 +1,107 @@ +package orm + +import ( + uuid "github.com/satori/go.uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "qilin-api/pkg/model" + qilintest "qilin-api/pkg/test" + "testing" +) + +type KeyListServiceTestSuite struct { + suite.Suite + db *Database + service model.KeyListService + + rightKeyPackage uuid.UUID + wrongKeyPackage uuid.UUID +} + +func Test_KeyListService(t *testing.T) { + suite.Run(t, new(KeyListServiceTestSuite)) +} + +func (suite *KeyListServiceTestSuite) SetupTest() { + shouldBe := require.New(suite.T()) + config, err := qilintest.LoadTestConfig() + if err != nil { + suite.FailNow("Unable to load config", "%v", err) + } + db, err := NewDatabase(&config.Database) + if err != nil { + suite.FailNow("Unable to connect to database", "%v", err) + } + + if err := db.DropAllTables(); err != nil { + assert.FailNow(suite.T(), "Unable to drop tables", err) + } + if err := db.Init(); err != nil { + assert.FailNow(suite.T(), "Unable to init tables", err) + } + + suite.rightKeyPackage = uuid.NewV4() + suite.wrongKeyPackage = uuid.NewV4() + + keyListStreeam := model.KeyStream{ + Type: model.ListKeyStream, + } + keyListStreeam.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&keyListStreeam).Error) + + keyPackage := model.KeyPackage{ + KeyStreamID: keyListStreeam.ID, + Name: "Test name", + PackageID: uuid.NewV4(), + KeyStreamType: model.ListKeyStream, + } + keyPackage.ID = suite.rightKeyPackage + shouldBe.Nil(db.DB().Model(model.KeyPackage{}).Create(&keyPackage).Error) + + platformListStreeam := model.KeyStream{ + Type: model.PlatformKeysStream, + } + platformListStreeam.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&platformListStreeam).Error) + + platformKeyPackage := model.KeyPackage{ + KeyStreamID: platformListStreeam.ID, + Name: "Another name", + PackageID: uuid.NewV4(), + KeyStreamType: model.PlatformKeysStream, + } + platformKeyPackage.ID = suite.wrongKeyPackage + shouldBe.Nil(db.DB().Model(model.KeyPackage{}).Create(&platformKeyPackage).Error) + + suite.db = db + suite.service = NewKeyListService(db) +} + +func (suite *KeyListServiceTestSuite) TearDownTest() { + if err := suite.db.DropAllTables(); err != nil { + panic(err) + } + if err := suite.db.Close(); err != nil { + panic(err) + } +} + +func (suite *KeyListServiceTestSuite) TestAddKeys() { + shouldBe := require.New(suite.T()) + keys := []string{ + model.RandStringRunes(10), + model.RandStringRunes(10), + model.RandStringRunes(10), + model.RandStringRunes(10), + } + + shouldBe.Nil(suite.service.AddKeys(suite.rightKeyPackage, keys)) + // second call with same keys should not occurs error + shouldBe.Nil(suite.service.AddKeys(suite.rightKeyPackage, keys)) + // empty keys - no error + shouldBe.Nil(suite.service.AddKeys(suite.rightKeyPackage, []string{})) + + shouldBe.NotNil(suite.service.AddKeys(suite.wrongKeyPackage, keys)) + shouldBe.NotNil(suite.service.AddKeys(uuid.NewV4(), keys)) +} diff --git a/pkg/orm/key_package_service.go b/pkg/orm/key_package_service.go new file mode 100644 index 0000000..487bfd1 --- /dev/null +++ b/pkg/orm/key_package_service.go @@ -0,0 +1,94 @@ +package orm + +import ( + "github.com/jinzhu/gorm" + "github.com/satori/go.uuid" + "net/http" + "qilin-api/pkg/model" + "qilin-api/pkg/orm/utils" +) + +type keyPackageService struct { + db *Database + keyStreamService model.KeyStreamService +} + +func NewKeyPackageService(db *Database, keyStreamService model.KeyStreamService) model.KeyPackageService { + return &keyPackageService{db: db, keyStreamService: keyStreamService} +} + +func (service *keyPackageService) Get(keyPackageId uuid.UUID) (*model.KeyPackage, error) { + keyPackage := &model.KeyPackage{} + + err := service.db.DB().Model(model.KeyPackage{}).Where("id = ?", keyPackageId).First(keyPackage).Error + if err != nil { + if gorm.IsRecordNotFoundError(err) { + return nil, NewServiceError(http.StatusNotFound, err) + } + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + return keyPackage, nil +} + +func (service *keyPackageService) Create(packageId uuid.UUID, name string, providerType model.KeyStreamType) (*model.KeyPackage, error) { + if exist, err := utils.CheckExists(service.db.DB(), model.Package{}, packageId); !exist || err != nil { + if gorm.IsRecordNotFoundError(err) { + return nil, NewServiceError(http.StatusNotFound, err) + } + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + if len(name) == 0 { + return nil, NewServiceError(http.StatusUnprocessableEntity, "Name must be not null") + } + + keyPackage := &model.KeyPackage{ + Name: name, + KeyStreamType: providerType, + PackageID: packageId, + } + keyPackage.ID = uuid.NewV4() + + err := service.db.DB().Model(model.KeyPackage{}).Create(keyPackage).Error + if err != nil { + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + _, err = service.keyStreamService.Create(providerType) + if err != nil { + return nil, err + } + + return keyPackage, nil +} + +func (service *keyPackageService) Update(keyPackageId uuid.UUID, name string) (*model.KeyPackage, error) { + keyPackage := &model.KeyPackage{} + + err := service.db.DB().Model(model.KeyPackage{}).Where("id = ?", keyPackageId).First(keyPackage).Error + if err != nil { + if gorm.IsRecordNotFoundError(err) { + return nil, NewServiceError(http.StatusNotFound, err) + } + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + keyPackage.Name = name + err = service.db.DB().Save(keyPackage).Error + if err != nil { + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + return keyPackage, nil +} + +func (service *keyPackageService) List(packageId uuid.UUID) ([]model.KeyPackage, error) { + var keyPackages []model.KeyPackage + err := service.db.DB().Model(model.KeyPackage{}).Where("package_id = ?", packageId).Order("created_at desc").Find(&keyPackages).Error + if err != nil { + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + return keyPackages, nil +} diff --git a/pkg/orm/key_package_service_test.go b/pkg/orm/key_package_service_test.go new file mode 100644 index 0000000..fc103a1 --- /dev/null +++ b/pkg/orm/key_package_service_test.go @@ -0,0 +1,160 @@ +package orm + +import ( + uuid "github.com/satori/go.uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "qilin-api/pkg/model" + qilintest "qilin-api/pkg/test" + "testing" +) + +type KeyPackageServiceTestSuite struct { + suite.Suite + db *Database + service model.KeyPackageService + + rightKeyPackage uuid.UUID + packageId uuid.UUID + wrongKeyPackage uuid.UUID +} + +func Test_KeyPackageService(t *testing.T) { + suite.Run(t, new(KeyPackageServiceTestSuite)) +} + +func (suite *KeyPackageServiceTestSuite) SetupTest() { + shouldBe := require.New(suite.T()) + config, err := qilintest.LoadTestConfig() + if err != nil { + suite.FailNow("Unable to load config", "%v", err) + } + db, err := NewDatabase(&config.Database) + if err != nil { + suite.FailNow("Unable to connect to database", "%v", err) + } + + if err := db.DropAllTables(); err != nil { + assert.FailNow(suite.T(), "Unable to drop tables", err) + } + if err := db.Init(); err != nil { + assert.FailNow(suite.T(), "Unable to init tables", err) + } + + suite.rightKeyPackage = uuid.NewV4() + suite.wrongKeyPackage = uuid.NewV4() + + keyListStreeam := model.KeyStream{ + Type: model.ListKeyStream, + } + keyListStreeam.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&keyListStreeam).Error) + gamePackage := model.Package{ + + } + gamePackage.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.Package{}).Create(&gamePackage).Error) + suite.packageId = gamePackage.ID + + keyPackage := model.KeyPackage{ + KeyStreamID: keyListStreeam.ID, + Name: "Test name", + PackageID: uuid.NewV4(), + KeyStreamType: model.ListKeyStream, + } + keyPackage.ID = suite.rightKeyPackage + shouldBe.Nil(db.DB().Model(model.KeyPackage{}).Create(&keyPackage).Error) + + platformListStreeam := model.KeyStream{ + Type: model.PlatformKeysStream, + } + platformListStreeam.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&platformListStreeam).Error) + + platformKeyPackage := model.KeyPackage{ + KeyStreamID: platformListStreeam.ID, + Name: "Another name", + PackageID: uuid.NewV4(), + KeyStreamType: model.PlatformKeysStream, + } + platformKeyPackage.ID = suite.wrongKeyPackage + shouldBe.Nil(db.DB().Model(model.KeyPackage{}).Create(&platformKeyPackage).Error) + + suite.db = db + suite.service = NewKeyPackageService(db, NewKeyStreamService(db)) +} + +func (suite *KeyPackageServiceTestSuite) TearDownTest() { + if err := suite.db.DropAllTables(); err != nil { + panic(err) + } + if err := suite.db.Close(); err != nil { + panic(err) + } +} + +func (suite *KeyPackageServiceTestSuite) TestGet() { + shouldBe := require.New(suite.T()) + keyPackage, err := suite.service.Get(suite.rightKeyPackage) + shouldBe.Nil(err) + shouldBe.NotNil(keyPackage) + + keyPackage, err = suite.service.Get(uuid.NewV4()) + shouldBe.NotNil(err) + shouldBe.Nil(keyPackage) +} + +func (suite *KeyPackageServiceTestSuite) TestCreate() { + shouldBe := require.New(suite.T()) + keyPackage, err := suite.service.Create(suite.packageId, "Some name", model.ListKeyStream) + shouldBe.Nil(err) + shouldBe.NotNil(keyPackage) + + keyPackage, err = suite.service.Create(suite.packageId, "Some name 2", model.ListKeyStream) + shouldBe.Nil(err) + shouldBe.NotNil(keyPackage) + + keyPackage, err = suite.service.Create(suite.packageId, "Some name 3", model.PlatformKeysStream) + shouldBe.Nil(err) + shouldBe.NotNil(keyPackage) + + keyPackage, err = suite.service.Create(suite.packageId, "Ooops", "Unknown_key_stream") + shouldBe.NotNil(err) + shouldBe.Nil(keyPackage) + + keyPackage, err = suite.service.Create(uuid.NewV4(), "Not found", model.PlatformKeysStream) + shouldBe.NotNil(err) + shouldBe.Nil(keyPackage) + + keyPackage, err = suite.service.Create(suite.packageId, "", model.ListKeyStream) + shouldBe.NotNil(err) + shouldBe.Nil(keyPackage) +} + +func (suite *KeyPackageServiceTestSuite) TestList() { + shouldBe := require.New(suite.T()) + keyPackages, err := suite.service.List(uuid.NewV4()) + shouldBe.Nil(err) + shouldBe.NotNil(keyPackages) + shouldBe.Empty(keyPackages) + + keyPackage, err := suite.service.Create(suite.packageId, "Some name", model.ListKeyStream) + shouldBe.Nil(err) + shouldBe.NotNil(keyPackage) + + keyPackages, err = suite.service.List(suite.packageId) + shouldBe.NotNil(keyPackages) + shouldBe.Nil(err) +} + +func (suite *KeyPackageServiceTestSuite) TestUpdate() { + shouldBe := require.New(suite.T()) + keyPackage, err := suite.service.Update(uuid.NewV4(), "Not found") + shouldBe.NotNil(err) + shouldBe.Nil(keyPackage) + + keyPackage, err = suite.service.Update(suite.rightKeyPackage, "Another name") + shouldBe.Nil(err) + shouldBe.NotNil(keyPackage) +} \ No newline at end of file diff --git a/pkg/orm/key_stream_service.go b/pkg/orm/key_stream_service.go new file mode 100644 index 0000000..8d4e6ed --- /dev/null +++ b/pkg/orm/key_stream_service.go @@ -0,0 +1,70 @@ +package orm + +import ( + "errors" + "fmt" + "github.com/jinzhu/gorm" + "github.com/satori/go.uuid" + "net/http" + "qilin-api/pkg/model" +) + +type keyStreamService struct { + db *Database +} + +var allowedStreamProviders = []model.KeyStreamType{model.ListKeyStream, model.PlatformKeysStream} + +func NewKeyStreamService(db *Database) model.KeyStreamService { + return &keyStreamService{db} +} + +func (service *keyStreamService) Get(streamId uuid.UUID) (model.KeyStreamProvider, error) { + stream := &model.KeyStream{} + err := service.db.DB().Model(model.KeyStream{}).Where("id = ?", streamId).First(stream).Error + + if err != nil { + if gorm.IsRecordNotFoundError(err) { + return nil, NewServiceError(http.StatusNotFound, err) + } + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + switch stream.Type { + case model.ListKeyStream: + return NewKeyListProvider(stream.ID, service.db) + case model.PlatformKeysStream: + return NewPlatformKeyProvider(stream.ID, service.db) + default: + return nil, NewServiceError(http.StatusBadRequest, errors.New(fmt.Sprintf("Unknown stream provider `%s`", stream.Type))) + } +} + +func (service *keyStreamService) Create(keyProviderType model.KeyStreamType) (uuid.UUID, error) { + if checkTypeIsAllowed(keyProviderType) == false { + return uuid.Nil, NewServiceErrorf(http.StatusUnprocessableEntity, "Type `%s` is not allowed", keyProviderType) + } + + stream := &model.KeyStream{ + Type: keyProviderType, + } + + stream.ID = uuid.NewV4() + err := service.db.DB().Model(model.KeyStream{}).Create(stream).Error + + if err != nil { + return uuid.Nil, NewServiceError(http.StatusInternalServerError, err) + } + + return stream.ID, nil +} + +func checkTypeIsAllowed(streamType model.KeyStreamType) bool { + for _, s := range allowedStreamProviders { + if s == streamType { + return true + } + } + + return false +} diff --git a/pkg/orm/key_stream_service_test.go b/pkg/orm/key_stream_service_test.go new file mode 100644 index 0000000..c14a24c --- /dev/null +++ b/pkg/orm/key_stream_service_test.go @@ -0,0 +1,133 @@ +package orm + +import ( + uuid "github.com/satori/go.uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "qilin-api/pkg/model" + qilintest "qilin-api/pkg/test" + "testing" +) + +type KeyStreamServiceTestSuite struct { + suite.Suite + db *Database + service model.KeyStreamService + + platformStreamId uuid.UUID + keyListStreamId uuid.UUID + wrongStreamId uuid.UUID + packageId uuid.UUID +} + +func Test_KeyStreamService(t *testing.T) { + suite.Run(t, new(KeyStreamServiceTestSuite)) +} + +func (suite *KeyStreamServiceTestSuite) SetupTest() { + shouldBe := require.New(suite.T()) + config, err := qilintest.LoadTestConfig() + if err != nil { + suite.FailNow("Unable to load config", "%v", err) + } + db, err := NewDatabase(&config.Database) + if err != nil { + suite.FailNow("Unable to connect to database", "%v", err) + } + + if err := db.DropAllTables(); err != nil { + assert.FailNow(suite.T(), "Unable to drop tables", err) + } + if err := db.Init(); err != nil { + assert.FailNow(suite.T(), "Unable to init tables", err) + } + + keyListStreeam := model.KeyStream{ + Type: model.ListKeyStream, + } + keyListStreeam.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&keyListStreeam).Error) + suite.keyListStreamId = keyListStreeam.ID + + gamePackage := model.Package{} + gamePackage.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Create(&gamePackage).Error) + + keyPackage := model.KeyPackage{ + KeyStreamID: keyListStreeam.ID, + Name: "Test name", + PackageID: uuid.NewV4(), + KeyStreamType: model.ListKeyStream, + } + keyPackage.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.KeyPackage{}).Create(&keyPackage).Error) + suite.packageId = keyPackage.ID + + platformListStreeam := model.KeyStream{ + Type: model.PlatformKeysStream, + } + platformListStreeam.ID = uuid.NewV4() + suite.platformStreamId = platformListStreeam.ID + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&platformListStreeam).Error) + + wrongListStreeam := model.KeyStream{ + Type: "unknown_type", + } + wrongListStreeam.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.KeyStream{}).Create(&wrongListStreeam).Error) + suite.wrongStreamId = wrongListStreeam.ID + + platformKeyPackage := model.KeyPackage{ + KeyStreamID: platformListStreeam.ID, + Name: "Another name", + PackageID: uuid.NewV4(), + KeyStreamType: model.PlatformKeysStream, + } + platformKeyPackage.ID = uuid.NewV4() + shouldBe.Nil(db.DB().Model(model.KeyPackage{}).Create(&platformKeyPackage).Error) + + suite.db = db + suite.service = NewKeyStreamService(db) +} + +func (suite *KeyStreamServiceTestSuite) TearDownTest() { + if err := suite.db.DropAllTables(); err != nil { + panic(err) + } + if err := suite.db.Close(); err != nil { + panic(err) + } +} + +func (suite *KeyStreamServiceTestSuite) TestGet() { + shouldBe := require.New(suite.T()) + + provider, err := suite.service.Get(uuid.NewV4()) + shouldBe.NotNil(err) + shouldBe.Nil(provider) + + provider, err = suite.service.Get(suite.keyListStreamId) + shouldBe.NotNil(provider) + shouldBe.Nil(err) + + provider, err = suite.service.Get(suite.platformStreamId) + shouldBe.NotNil(provider) + shouldBe.Nil(err) + + provider, err = suite.service.Get(suite.wrongStreamId) + shouldBe.NotNil(err) + shouldBe.Nil(provider) +} + +func (suite *KeyStreamServiceTestSuite) TestCreate() { + shouldBe := require.New(suite.T()) + + streamId, err := suite.service.Create(model.ListKeyStream) + shouldBe.Nil(err) + shouldBe.NotEqual(uuid.Nil, streamId) + + streamId, err = suite.service.Create("unknown_type") + shouldBe.NotNil(err) + shouldBe.Equal(uuid.Nil, streamId) +} diff --git a/pkg/orm/platform_key_provider.go b/pkg/orm/platform_key_provider.go new file mode 100644 index 0000000..0bba19b --- /dev/null +++ b/pkg/orm/platform_key_provider.go @@ -0,0 +1,119 @@ +package orm + +import ( + "fmt" + "github.com/jinzhu/gorm" + "github.com/pkg/errors" + uuid "github.com/satori/go.uuid" + "go.uber.org/zap" + "math/rand" + "net/http" + "qilin-api/pkg/model" + "time" +) + +type platformKeyProvider struct { + db *Database + streamId uuid.UUID +} + +func (provider *platformKeyProvider) Redeem() (model.Key, error) { + transaction := provider.db.DB().Begin() + key, err := redeem(provider.streamId, transaction) + if err != nil { + transaction.Rollback() + return key, err + } + + return key, transaction.Commit().Error +} + +func generateCode() string { + // Example: AAAA-BBBB-CCCC-DDDD + return fmt.Sprintf("%s-%s-%s-%s", RandStringRunes(4), RandStringRunes(4), RandStringRunes(4), RandStringRunes(4)) +} + +func redeem(streamId uuid.UUID, transaction *gorm.DB) (model.Key, error) { + key := model.Key{ + KeyStreamID: streamId, + } + key.ID = uuid.NewV4() + + infiniteLoopIndex := 0 + for { + key.ActivationCode = generateCode() + t := time.Now().UTC() + key.RedeemTime = &t + var keyExist model.Key + err := transaction.Model(model.Key{}).Where("key_stream_id = ? AND activation_code = ?", + streamId, + key.ActivationCode).First(&keyExist).Error + if err != nil { + if gorm.IsRecordNotFoundError(err) { + break + } + return key, NewServiceError(http.StatusInternalServerError, errors.Wrap(err, "Trying to check activation code before exist")) + } + + if infiniteLoopIndex >= 100 { + return key, NewServiceError(http.StatusInternalServerError, errors.Wrap(err, "Infinite loop break condition.")) + } + infiniteLoopIndex++ + zap.L().Info(fmt.Sprintf("KeyStream `%s` already contains activation_code `%s`. Trying again", streamId, key.ActivationCode)) + } + + err := transaction.Model(model.Key{}).Create(&key).Error + if err != nil { + return key, err + } + + return key, nil +} + +func (service *platformKeyProvider) RedeemList(count int) ([]model.Key, error) { + var keys []model.Key + transaction := service.db.DB().Begin() + for i := 0; i < count; i++ { + key, err := redeem(service.streamId, transaction) + if err != nil { + transaction.Rollback() + return keys, err + } + keys = append(keys, key) + } + + return keys, transaction.Commit().Error +} + +func (platformKeyProvider) AddKeys(codes []string) error { + return NewServiceError(http.StatusBadRequest, "Not implemented for PlatformsKey") +} + +func NewPlatformKeyProvider(keyStreamId uuid.UUID, database *Database) (model.KeyStreamProvider, error) { + keyStream := model.KeyStream{} + err := database.DB().Model(model.KeyStream{}).Where("id = ?", keyStreamId).First(&keyStream).Error + if err != nil { + if gorm.IsRecordNotFoundError(err) { + return nil, NewServiceErrorf(http.StatusNotFound, "Key stream with id `%s` not found", keyStreamId) + } + return nil, NewServiceError(http.StatusInternalServerError, err) + } + + if keyStream.Type != model.PlatformKeysStream { + return nil, NewServiceErrorf(http.StatusBadRequest, "Key stream with id `%s` has wrong type", keyStreamId) + } + + return &platformKeyProvider{db: database, streamId: keyStreamId}, nil +} + +var letterRunes = []rune("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ") + +//RandStringRunes creates random string with specified length +func RandStringRunes(n int) string { + b := make([]rune, n) + lenRunes := len(letterRunes) + for i := range b { + b[i] = letterRunes[rand.Intn(lenRunes)] + } + return string(b) +} diff --git a/pkg/orm/platform_key_provider_test.go b/pkg/orm/platform_key_provider_test.go new file mode 100644 index 0000000..680cfcc --- /dev/null +++ b/pkg/orm/platform_key_provider_test.go @@ -0,0 +1,19 @@ +package orm + +import ( + "testing" +) + +func Test_generateCode(t *testing.T) { + codes := make(map[string]int) + for i := 0; i < 10000000; i++ { + code := generateCode() + codes[code]++ + } + + for _, code := range codes { + if code > 1 { + t.FailNow() + } + } +} \ No newline at end of file From 8d45f533571e8a2a8393aa344f50cc5d36365e77 Mon Sep 17 00:00:00 2001 From: Roman Golenok Date: Wed, 26 Jun 2019 17:59:53 +0300 Subject: [PATCH 13/13] Specs updated for key packages --- spec/store_openapi.yaml | 261 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) diff --git a/spec/store_openapi.yaml b/spec/store_openapi.yaml index 4ffdef8..d856cc2 100644 --- a/spec/store_openapi.yaml +++ b/spec/store_openapi.yaml @@ -546,6 +546,247 @@ paths: 500: $ref: '#/components/responses/InternalError' + /api/v1/packages/:packageId/keypackages: + post: + summary: Creates new key package for specified package + parameters: + - name: packageId + in: "path" + description: "package Id" + required: true + schema: + type: string + format: uuid + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + type: + type: string + enum: + - key_list + - key_platform + responses: + 201: + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/KeyPackage' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 500: + $ref: '#/components/responses/InternalError' + + get: + tags: + - package + - keypackage + summary: Get list of key packages for specified package + parameters: + - name: packageId + in: "path" + description: "package Id" + required: true + schema: + type: string + format: uuid + responses: + 200: + description: List of key packages + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/KeyPackage' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalError' + + /api/v1/packages/:packageId/keypackages/:keyPackageId: + put: + summary: Change key package + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + parameters: + - name: packageId + in: "path" + description: "package Id" + required: true + schema: + type: string + format: uuid + - name: keyPackageId + in: "path" + description: "key package Id" + required: true + schema: + type: string + format: uuid + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/KeyPackage' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 422: + $ref: '#/components/responses/UnprocessableEntity' + 500: + $ref: '#/components/responses/InternalError' + + get: + summary: Get info about specified key package + parameters: + - name: packageId + in: "path" + description: "package Id" + required: true + schema: + type: string + format: uuid + - name: keyPackageId + in: "path" + description: "key package Id" + required: true + schema: + type: string + format: uuid + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/KeyPackage' + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalError' + + /api/v1/packages/:packageId/keypackages/:keyPackageId/keys: + post: + summary: Adds new keys + requestBody: + content: + application/json: + schema: + type: object + properties: + keys: + type: array + items: + type: string + parameters: + - name: packageId + in: "path" + description: "package Id" + required: true + schema: + type: string + format: uuid + - name: keyPackageId + in: "path" + description: "key package Id" + required: true + schema: + type: string + format: uuid + responses: + 200: + description: OK + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalError' + + /api/v1/packages/:packageId/keypackages/:keyPackageId/file: + post: + summary: Adds new keys from file + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + keys: + type: string + format: binary + parameters: + - name: packageId + in: "path" + description: "package Id" + required: true + schema: + type: string + format: uuid + - name: keyPackageId + in: "path" + description: "key package Id" + required: true + schema: + type: string + format: uuid + responses: + 200: + description: OK + 400: + $ref: '#/components/responses/BadRequest' + 401: + $ref: '#/components/responses/Unauthorized' + 403: + $ref: '#/components/responses/Forbidden' + 404: + $ref: '#/components/responses/NotFound' + 500: + $ref: '#/components/responses/InternalError' + components: securitySchemes: bearerAuth: @@ -783,3 +1024,23 @@ components: type: string pt: type: string + + KeyPackage: + type: object + properties: + id: + type: string + format: uuid + name: + type: string + type: + type: string + enum: + - key_list + - key_platform + created: + type: string + format: 'date-time' + updated: + type: string + format: 'date-time' \ No newline at end of file