From ef0fa50ba18b5c34833569f3fc001d3dffdf2503 Mon Sep 17 00:00:00 2001 From: yaziine Date: Wed, 21 Sep 2022 22:50:49 +0400 Subject: [PATCH] feat: add devices validation --- .github/workflows/ci.yaml | 4 +- .github/workflows/lint.yaml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/reviewdog.yaml | 4 +- .golangci.yml | 2 +- Makefile | 2 +- go.mod | 2 +- pkg/cmd/chat/imports/validator/index.go | 25 +++++ pkg/cmd/chat/imports/validator/items.go | 98 +++++++++++++++++-- .../validator/testdata/invalid-devices.json | 86 ++++++++++++++++ .../validator/testdata/valid-data.json | 32 +++++- .../chat/imports/validator/validator_test.go | 22 ++++- pkg/cmd/config/config.go | 2 +- pkg/config/config.go | 20 ++-- 14 files changed, 267 insertions(+), 36 deletions(-) create mode 100644 pkg/cmd/chat/imports/validator/testdata/invalid-devices.json diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e94e8a5d..eb5bc273 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -17,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: "1.18" + go-version: "1.19" - uses: actions/checkout@v2 @@ -27,4 +27,4 @@ jobs: STREAM_SECRET: ${{ secrets.STREAM_SECRET }} run: | go test -coverprofile cover.out -v -race ./... - go tool cover -func=cover.out \ No newline at end of file + go tool cover -func=cover.out diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index ecb95fb6..ccc66ecc 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -22,7 +22,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: '1.18' + go-version: '1.19' - name: Tidy run: go mod tidy -v && git diff --no-patch --exit-code || { git status; echo 'Unchecked diff, did you forget go mod tidy again?' ; false ; }; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2880c79a..e487a117 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: '1.18' + go-version: '1.19' - run: | git tag "${{ env.VERSION }}" diff --git a/.github/workflows/reviewdog.yaml b/.github/workflows/reviewdog.yaml index abc4faad..d7eabcb2 100644 --- a/.github/workflows/reviewdog.yaml +++ b/.github/workflows/reviewdog.yaml @@ -20,7 +20,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: "1.18" + go-version: "1.19" - name: Install golangci-lint run: @@ -30,4 +30,4 @@ jobs: env: REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: - $(go env GOPATH)/bin/golangci-lint run --out-format line-number | reviewdog -f=golangci-lint -name=golangci-lint -reporter=github-pr-review \ No newline at end of file + $(go env GOPATH)/bin/golangci-lint run --out-format line-number | reviewdog -f=golangci-lint -name=golangci-lint -reporter=github-pr-review diff --git a/.golangci.yml b/.golangci.yml index 1c0ebbc9..257373b8 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,5 @@ run: - go: '1.18' + go: '1.19' deadline: 210s timeout: 10m skip-dirs: diff --git a/Makefile b/Makefile index d2c634d2..22b0c6a4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ NAME = stream-cli -GOLANGCI_VERSION = 1.45.0 +GOLANGCI_VERSION = 1.49.0 GOLANGCI = .bin/golangci/$(GOLANGCI_VERSION)/golangci-lint $(GOLANGCI): @curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(dir $(GOLANGCI)) v$(GOLANGCI_VERSION) diff --git a/go.mod b/go.mod index 0c70bbc3..6ff33f3f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/GetStream/stream-cli -go 1.18 +go 1.19 require ( github.com/AlecAivazis/survey/v2 v2.3.4 diff --git a/pkg/cmd/chat/imports/validator/index.go b/pkg/cmd/chat/imports/validator/index.go index 2b7621bd..93e2e966 100644 --- a/pkg/cmd/chat/imports/validator/index.go +++ b/pkg/cmd/chat/imports/validator/index.go @@ -67,6 +67,9 @@ type index struct { // reactions reactionPKs map[Bits256]struct{} + + // devices + devicePKs map[Bits256]struct{} } func newIndex(roles map[string]*streamchat.Role, channelTypes map[string]*streamchat.ChannelType) *index { @@ -84,6 +87,7 @@ func newIndex(roles map[string]*streamchat.Role, channelTypes map[string]*stream messagePKsWithReaction: make(map[Bits256]struct{}), messagePKsReplies: make(map[Bits256]struct{}), reactionPKs: make(map[Bits256]struct{}), + devicePKs: make(map[Bits256]struct{}), } } @@ -94,6 +98,7 @@ func (i *index) stats() map[string]int { "members": len(i.memberPKs), "messages": len(i.messagePKs), "reactions": len(i.reactionPKs), + "devices": len(i.devicePKs), } } @@ -302,3 +307,23 @@ func (i *index) addReaction(messageID, reactionType, userID string) error { return nil } + +func getDevicePK(deviceID string) Bits256 { + return hashValues(deviceID) +} + +func (i *index) deviceExist(deviceID string) bool { + pk := getDevicePK(deviceID) + _, ok := i.devicePKs[pk] + return ok +} + +func (i *index) addDevice(deviceID string) error { + if i.deviceExist(deviceID) { + return fmt.Errorf("duplicate device id:%s", deviceID) + } + + pk := getDevicePK(deviceID) + i.devicePKs[pk] = struct{}{} + return nil +} diff --git a/pkg/cmd/chat/imports/validator/items.go b/pkg/cmd/chat/imports/validator/items.go index 18236229..bda33be7 100644 --- a/pkg/cmd/chat/imports/validator/items.go +++ b/pkg/cmd/chat/imports/validator/items.go @@ -9,6 +9,8 @@ import ( "strings" "time" "unicode/utf8" + + streamchat "github.com/GetStream/stream-chat-go/v5" ) var ( @@ -35,6 +37,8 @@ func newItem(rawItem *rawItem) (Item, error) { return newMessageItem(rawItem.Item) case "reaction": return newReactionItem(rawItem.Item) + case "device": + return newDeviceItem(rawItem.Item) default: return nil, fmt.Errorf("invalid item type %q", rawItem.Type) } @@ -88,14 +92,20 @@ func newUserItem(itemBody json.RawMessage) (*userItem, error) { } type userItem struct { - ID string `json:"id"` - Role string `json:"role"` - Invisible bool `json:"invisible"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt time.Time `json:"deleted_at"` - Teams []string `json:"teams"` - Custom extraFields + ID string `json:"id"` + Role string `json:"role"` + Invisible bool `json:"invisible"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt time.Time `json:"deleted_at"` + Teams []string `json:"teams"` + PushNotifications pushNotification `json:"push_notifications"` + Custom extraFields +} + +type pushNotification struct { + Disabled bool `json:"disabled"` + DisabledUntil *time.Time `json:"disabled_until"` } func (u *userItem) validateFields() error { @@ -130,6 +140,78 @@ func (u *userItem) validateReferences(idx *index) error { return nil } +type deviceItem struct { + ID string `json:"id"` + UserID string `json:"user_id"` + CreatedAt time.Time `json:"created_at"` + Disabled bool `json:"disabled"` + DisabledReason string `json:"disabled_reason"` + PushProviderType string `json:"push_provider_type"` + PushProviderName string `json:"push_provider_name"` +} + +func newDeviceItem(itemBody json.RawMessage) (*deviceItem, error) { + var device deviceItem + if err := unmarshalItem(itemBody, &device); err != nil { + return nil, err + } + return &device, nil +} + +var pushProviders = []string{ + streamchat.PushProviderFirebase, + streamchat.PushProviderHuawei, + streamchat.PushProviderAPNS, + streamchat.PushProviderXiaomi, +} + +func (d *deviceItem) validateFields() error { + if d.ID == "" { + return errors.New("device.id required") + } + if len(d.ID) > 255 { + return errors.New("device.id max length exceeded (255)") + } + + if d.UserID == "" { + return errors.New("device.user_id required") + } + if len(d.UserID) > 255 { + return errors.New("device.user_id max length exceeded (255)") + } + + if d.CreatedAt.IsZero() { + return errors.New("device.created_at required") + } + + if d.PushProviderType == "" { + return errors.New("device.push_provider_type required") + } + var found bool + for _, p := range pushProviders { + if d.PushProviderType == p { + found = true + break + } + } + if !found { + return fmt.Errorf("device.push_provider_type invalid, available options are: %s", strings.Join(pushProviders, ",")) + } + + return nil +} + +func (d *deviceItem) index(i *index) error { + return i.addDevice(d.ID) +} + +func (d *deviceItem) validateReferences(i *index) error { + if d.UserID != "" && !i.userExist(d.UserID) { + return fmt.Errorf("device.user_id %q doesn't exist", d.UserID) + } + return nil +} + var channelReservedFields = []string{ "last_message_at", "cid", "created_by_pk", "members", "config", "app_pk", "pk", } diff --git a/pkg/cmd/chat/imports/validator/testdata/invalid-devices.json b/pkg/cmd/chat/imports/validator/testdata/invalid-devices.json new file mode 100644 index 00000000..febe5f32 --- /dev/null +++ b/pkg/cmd/chat/imports/validator/testdata/invalid-devices.json @@ -0,0 +1,86 @@ +[ + { + "type": "user", + "item": { + "id": "userA", + "role": "user" + } + }, + { + "type": "device", + "item": { + "id": "id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long-id-too-long", + "user_id": "userA", + "created_at": "2022-01-01T01:01:01Z", + "disabled": false, + "push_provider_type": "firebase" + } + }, + { + "type": "device", + "item": { + "id": "123", + "user_id": "userB", + "created_at": "2022-01-01T01:01:01Z", + "disabled": false, + "push_provider_type": "firebase" + } + }, + { + "type": "device", + "item": { + "user_id": "userA", + "created_at": "2022-01-01T01:01:01Z", + "disabled": false, + "push_provider_type": "firebase" + } + }, + { + "type": "device", + "item": { + "id": "no user ID", + "created_at": "2022-01-01T01:01:01Z", + "disabled": false, + "push_provider_type": "firebase" + } + }, + { + "type": "device", + "item": { + "id": "123", + "user_id": "userA", + "created_at": "2022-01-01T01:01:01Z", + "disabled": false, + "push_provider_type": "unknown_provider" + } + }, + { + "type": "device", + "item": { + "id": "123", + "user_id": "userA", + "disabled": false, + "push_provider_type": "unknown_provider" + } + }, + { + "type": "device", + "item": { + "id": "duplicate", + "user_id": "userA", + "created_at": "2022-01-01T01:01:01Z", + "disabled": false, + "push_provider_type": "apn" + } + }, + { + "type": "device", + "item": { + "id": "duplicate", + "user_id": "userA", + "created_at": "2022-01-01T01:01:01Z", + "disabled": false, + "push_provider_type": "xiaomi" + } + } +] diff --git a/pkg/cmd/chat/imports/validator/testdata/valid-data.json b/pkg/cmd/chat/imports/validator/testdata/valid-data.json index 7d48554a..0e4ff169 100644 --- a/pkg/cmd/chat/imports/validator/testdata/valid-data.json +++ b/pkg/cmd/chat/imports/validator/testdata/valid-data.json @@ -7,7 +7,11 @@ "teams": [ "teamA" ], - "foo": "bar" + "foo": "bar", + "push_notifications": { + "disabled": true, + "disabled_until": "2022-12-31T23:59:59Z" + } } }, { @@ -17,7 +21,10 @@ "role": "user", "teams": [ "teamB" - ] + ], + "push_notifications": { + "disabled": false + } } }, { @@ -160,5 +167,26 @@ "user_id": "user3", "created_at": "2022-01-01T01:01:01Z" } + }, + { + "type": "device", + "item": { + "id": "123", + "user_id": "user1", + "created_at": "2022-01-01T01:01:01Z", + "disabled": true, + "disabled_reason": "provider creds are not valid", + "push_provider_type": "xiaomi" + } + }, + { + "type": "device", + "item": { + "id": "456", + "user_id": "user2", + "created_at": "2022-01-01T01:01:01Z", + "disabled": false, + "push_provider_type": "firebase" + } } ] diff --git a/pkg/cmd/chat/imports/validator/validator_test.go b/pkg/cmd/chat/imports/validator/validator_test.go index 7f5dc833..6fcfde1b 100644 --- a/pkg/cmd/chat/imports/validator/validator_test.go +++ b/pkg/cmd/chat/imports/validator/validator_test.go @@ -17,11 +17,11 @@ func TestValidator_Validate(t *testing.T) { want *Results }{ {name: "Valid data", filename: "valid-data.json", want: &Results{ - Stats: map[string]int{"channels": 3, "members": 4, "messages": 3, "reactions": 3, "users": 4}, + Stats: map[string]int{"channels": 3, "devices": 2, "members": 4, "messages": 3, "reactions": 3, "users": 4}, Errors: nil, }}, {name: "Invalid users", filename: "invalid-users.json", want: &Results{ - Stats: map[string]int{"channels": 0, "members": 0, "messages": 0, "reactions": 0, "users": 1}, + Stats: map[string]int{"channels": 0, "devices": 0, "members": 0, "messages": 0, "reactions": 0, "users": 1}, Errors: []error{ errors.New(`validation error: user.id required`), errors.New(`validation error: user.id max length exceeded (255)`), @@ -32,7 +32,7 @@ func TestValidator_Validate(t *testing.T) { }, }}, {name: "Invalid channels", filename: "invalid-channels.json", want: &Results{ - Stats: map[string]int{"channels": 2, "members": 0, "messages": 0, "reactions": 0, "users": 1}, + Stats: map[string]int{"channels": 2, "devices": 0, "members": 0, "messages": 0, "reactions": 0, "users": 1}, Errors: []error{ errors.New(`validation error: either channel.id or channel.member_ids required`), errors.New(`validation error: either channel.id or channel.member_ids required`), @@ -49,7 +49,7 @@ func TestValidator_Validate(t *testing.T) { }, }}, {name: "Invalid members", filename: "invalid-members.json", want: &Results{ - Stats: map[string]int{"channels": 4, "members": 5, "messages": 0, "reactions": 0, "users": 3}, + Stats: map[string]int{"channels": 4, "devices": 0, "members": 5, "messages": 0, "reactions": 0, "users": 3}, Errors: []error{ errors.New(`validation error: member.user_id required`), errors.New(`validation error: member.channel_type required`), @@ -63,7 +63,7 @@ func TestValidator_Validate(t *testing.T) { }, }}, {name: "Invalid messages", filename: "invalid-messages.json", want: &Results{ - Stats: map[string]int{"channels": 2, "members": 2, "messages": 0, "reactions": 0, "users": 1}, + Stats: map[string]int{"channels": 2, "devices": 0, "members": 2, "messages": 0, "reactions": 0, "users": 1}, Errors: []error{ errors.New(`validation error: message.id max length exceeded (255)`), errors.New(`validation error: message.channel_type required`), @@ -77,6 +77,18 @@ func TestValidator_Validate(t *testing.T) { errors.New(`reference error: user "" doesn't exist (message_id messageA)`), }, }}, + {name: "Invalid devices", filename: "invalid-devices.json", want: &Results{ + Stats: map[string]int{"channels": 0, "devices": 2, "members": 0, "messages": 0, "reactions": 0, "users": 1}, + Errors: []error{ + errors.New(`validation error: device.id max length exceeded (255)`), + errors.New(`validation error: device.id required`), + errors.New(`validation error: device.user_id required`), + errors.New(`validation error: device.push_provider_type invalid, available options are: firebase,huawei,apn,xiaomi`), + errors.New(`validation error: device.created_at required`), + errors.New(`duplicate device id:duplicate`), + errors.New(`reference error: device.user_id "userB" doesn't exist`), + }, + }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/cmd/config/config.go b/pkg/cmd/config/config.go index 2f61d17b..2d37d641 100644 --- a/pkg/cmd/config/config.go +++ b/pkg/cmd/config/config.go @@ -186,7 +186,7 @@ func questions() []*survey.Question { _, err := url.ParseRequestURI(u) if err != nil { - return errors.New("invalid url format. make sure it matches ://") + return errors.New("invalid url format make sure it matches ://") } return nil }, diff --git a/pkg/config/config.go b/pkg/config/config.go index ef12c7da..823276f9 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -46,15 +46,6 @@ func (c *Config) Get(name string) (*App, error) { return nil, fmt.Errorf("application %q doesn't exist", name) } -func (c *Config) GetCredentials(cmd *cobra.Command) (string, string, error) { - a, err := c.GetDefaultAppOrExplicit(cmd) - if err != nil { - return "", "", err - } - - return a.AccessKey, a.AccessSecretKey, nil -} - func (c *Config) GetDefaultAppOrExplicit(cmd *cobra.Command) (*App, error) { appName := c.Default explicit, err := cmd.Flags().GetString("app") @@ -74,12 +65,19 @@ func (c *Config) GetDefaultAppOrExplicit(cmd *cobra.Command) (*App, error) { } func (c *Config) GetClient(cmd *cobra.Command) (*stream.Client, error) { - key, secret, err := c.GetCredentials(cmd) + a, err := c.GetDefaultAppOrExplicit(cmd) if err != nil { return nil, err } - return stream.NewClient(key, secret) + client, err := stream.NewClient(a.AccessKey, a.AccessSecretKey) + if err != nil { + return nil, err + } + if a.ChatURL != DefaultChatEdgeURL { + client.BaseURL = a.ChatURL + } + return client, nil } func (c *Config) Add(newApp App) error {