Select Git revision
buffered_write_syncer_test.go
item_test.go 21.28 KiB
package items
import (
"context"
"fmt"
"testing"
"time"
"git.perx.ru/perxis/perxis-go/pkg/schema"
"git.perx.ru/perxis/perxis-go/pkg/schema/field"
"github.com/redpanda-data/benthos/v4/public/service"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestItem_Set(t *testing.T) {
item := &Item{}
_ = item.Set("id", "id")
assert.Equal(t, "id", item.ID)
now := time.Now()
_ = item.Set("created_at", now)
assert.Equal(t, now, item.CreatedAt)
_ = item.Set("a.b.c", 101)
assert.Equal(t, map[string]any{"a": map[string]any{"b": map[string]any{"c": 101}}}, item.Data)
}
func TestItem_DeleteItemData(t *testing.T) {
tests := []struct {
name string
item *Item
field string
want map[string]any
wantErr assert.ErrorAssertionFunc
}{
{
name: "Simple",
item: &Item{Data: map[string]any{"a": "b", "c": "d"}},
field: "a",
want: map[string]any{"c": "d"},
wantErr: assert.NoError,
},
{
name: "Item data is nil",
item: &Item{Data: nil},
field: "a",
want: nil,
wantErr: assert.NoError,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
err := tc.item.Delete(tc.field)
assert.NoError(t, err)
if !tc.wantErr(t, err) {
return
}
assert.Equal(t, tc.want, tc.item.Data)
})
}
}
func TestGetField(t *testing.T) {
sch := schema.New(
"a", field.String(),
"obj", field.Object(
"a", field.Number(field.NumberFormatFloat),
"b", field.String(),
),
"arr", field.Array(field.Object("a", field.Time())),
)
sch.ClearState()
tests := []struct {
name string
field string
want *field.Field
wantErr assert.ErrorAssertionFunc
}{
{"Simple", "a", field.String(), assert.NoError},
{"Incorrect field", "b", nil, assert.Error},
{
"Object",
"obj",
field.Object("a", field.Number(field.NumberFormatFloat), "b", field.String()),
assert.NoError,
},
{"Object path", "obj.a", field.Number(field.NumberFormatFloat), assert.NoError},
{"Array", "arr", field.Array(field.Object("a", field.Time())), assert.NoError},
{"Array path", "arr.a", field.Time(), assert.NoError},
{"Array item", "arr.", field.Object("a", field.Time()), assert.NoError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetField(tt.field, sch)
if !tt.wantErr(t, err, fmt.Sprintf("GetField(%v, sch)", tt.field)) {
return
}
assert.Equalf(t, tt.want, got, "GetField(%v, sch)", tt.field)
})
}
}
func TestItem_Proto(t *testing.T) {
w := time.Now().UTC()
tests := []struct {
name string
item *Item
}{
{
name: "All fields are filled",
item: &Item{
ID: "id-1",
SpaceID: "space-1",
EnvID: "env-1",
CollectionID: "coll-1",
State: StateDraft,
CreatedRevAt: w.Add(time.Hour),
CreatedBy: "user-1",
CreatedAt: w,
UpdatedAt: w.Add(time.Hour),
UpdatedBy: "user-2",
Data: map[string]any{"a": "b", "c": "d", "x": nil},
LocaleID: "ru",
Translations: map[string]map[string]interface{}{
"ru": {"a": "B"},
"en": nil,
},
TranslationsIDs: []string{"ru", "en"},
RevisionID: "rev-1",
RevisionDescription: "desc-1",
Permissions: PermissionsAllowAny,
SearchScore: 100.0,
Deleted: false,
Hidden: true,
Template: false,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.item, ItemFromProto(ItemToProto(tc.item)))
})
}
}
func TestItem_Encode_Decode(t *testing.T) {
w := time.Now().UTC()
tests := []struct {
name string
item *Item
}{
{
name: "Data",
item: &Item{
ID: "id-1",
SpaceID: "space-1",
EnvID: "env-1",
CollectionID: "coll-1",
State: StateDraft,
CreatedRevAt: w.Add(time.Hour),
CreatedBy: "user-1",
CreatedAt: w,
UpdatedAt: w.Add(time.Hour),
UpdatedBy: "user-2",
Data: map[string]any{
"a": "text-a",
"b": 124.1,
"c": map[string]interface{}{"x": "y"},
"d": []interface{}{"k", "l", "m"},
},
RevisionID: "rev-1",
RevisionDescription: "desc-1",
Permissions: PermissionsAllowAny,
SearchScore: 100.0,
Deleted: false,
Hidden: true,
Template: false,
},
},
{
name: "Data and Translations",
item: &Item{
ID: "id-1",
SpaceID: "space-1",
EnvID: "env-1",
CollectionID: "coll-1",
State: StateDraft,
CreatedRevAt: w.Add(time.Hour),
CreatedBy: "user-1",
CreatedAt: w,
UpdatedAt: w.Add(time.Hour),
UpdatedBy: "user-2",
Data: map[string]any{
"a": "text-a",
"b": 124.1,
"c": map[string]interface{}{"x": "y"},
"d": []interface{}{"k", "l", "m"},
},
LocaleID: "ru",
Translations: map[string]map[string]interface{}{
"ru": {"a": "ru-a"},
"en": {"a": "en-a"},
},
TranslationsIDs: []string{"ru", "en"},
RevisionID: "rev-1",
RevisionDescription: "desc-1",
Permissions: PermissionsAllowAny,
SearchScore: 100.0,
Deleted: false,
Hidden: false,
Template: false,
},
},
{
name: "Nil Translation",
item: &Item{
ID: "id-1",
SpaceID: "space-1",
EnvID: "env-1",
CollectionID: "coll-1",
State: StateDraft,
CreatedRevAt: w.Add(time.Hour),
CreatedBy: "user-1",
CreatedAt: w,
UpdatedAt: w.Add(time.Hour),
UpdatedBy: "user-2",
Data: map[string]any{
"a": "text-a",
"b": 124.1,
"c": map[string]interface{}{"x": "y"},
"d": []interface{}{"k", "l", "m"},
},
LocaleID: "ru",
Translations: map[string]map[string]interface{}{
"ru": {"a": "ru-a"},
"en": nil,
},
TranslationsIDs: []string{"ru", "en"},
RevisionID: "rev-1",
RevisionDescription: "desc-1",
Permissions: PermissionsAllowAny,
SearchScore: 100.0,
Deleted: false,
Hidden: false,
Template: false,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
sch := schema.New(
"a", field.String(),
"b", field.Number(field.NumberFormatFloat),
"c", field.Object("x", field.String()),
"d", field.Array(field.String()),
)
enc, err := tc.item.Encode(ctx, sch)
require.NoError(t, err)
dec, err := enc.Decode(ctx, sch)
require.NoError(t, err)
assert.Equal(t, tc.item, dec)
})
}
}
func Test_mergeItemData(t *testing.T) {
tests := []struct {
name string
schema *schema.Schema
origData map[string]any
updData map[string]any
want map[string]interface{}
wantErr bool
}{
{
name: "merge with non-nil original data",
schema: schema.New(
"field1", field.String(),
"field2", field.String(),
"field3", field.String(),
),
origData: map[string]interface{}{
"field1": "value1",
"field2": "value2",
},
updData: map[string]interface{}{
"field2": "new_value2",
"field3": "value3",
},
want: map[string]interface{}{
"field1": "value1",
"field2": "new_value2",
"field3": "value3",
},
wantErr: false,
},
{
name: "merge with nil original data",
schema: schema.New(
"field1", field.String(),
),
origData: nil,
updData: map[string]interface{}{
"field1": "value1",
},
want: map[string]interface{}{
"field1": "value1",
},
wantErr: false,
},
{
name: "merge with empty original data",
schema: schema.New(
"field1", field.String(),
),
origData: map[string]interface{}{},
updData: map[string]interface{}{
"field1": "value1",
},
want: map[string]interface{}{
"field1": "value1",
},
wantErr: false,
},
{
name: "merge with schema fields",
schema: schema.New(
"field1", field.String(),
"field2", field.String(),
"field3", field.String(),
),
origData: map[string]interface{}{
"field1": "value1",
"field2": "value2",
},
updData: map[string]interface{}{
"field2": "new_value2",
"field3": "value3",
},
want: map[string]interface{}{
"field1": "value1",
"field2": "new_value2",
"field3": "value3",
},
wantErr: false,
},
{
name: "merge with extra fields not in schema",
schema: schema.New(
"field1", field.String(),
"field2", field.String(),
),
origData: map[string]interface{}{
"field1": "value1",
"extra_field": "extra_value",
},
updData: map[string]interface{}{
"field2": "value2",
"another_extra": "another_value",
},
want: map[string]interface{}{
"field1": "value1",
"field2": "value2",
},
wantErr: false,
},
{
name: "merge with different field types",
schema: schema.New(
"string_field", field.String(),
"number_field", field.Number(field.NumberFormatInt),
"bool_field", field.String(),
),
origData: map[string]interface{}{
"string_field": "old_value",
"number_field": 42,
},
updData: map[string]interface{}{
"string_field": "new_value",
"bool_field": "true",
},
want: map[string]interface{}{
"string_field": "new_value",
"number_field": 42,
"bool_field": "true",
},
wantErr: false,
},
{
name: "merge with nested schema",
schema: schema.New(
"user", field.Object(
"name", field.String(),
"age", field.Number(field.NumberFormatInt),
"active", field.Bool(),
),
"metadata", field.Object(
"created_at", field.String(),
"updated_at", field.String(),
),
),
origData: map[string]interface{}{
"user": map[string]interface{}{
"name": "John",
"age": 30,
"active": true,
},
"metadata": map[string]interface{}{
"created_at": "2024-01-01",
},
},
updData: map[string]interface{}{
"user": map[string]interface{}{
"name": "John Doe",
},
"metadata": map[string]interface{}{
"updated_at": "2024-03-20",
},
},
want: map[string]interface{}{
"user": map[string]interface{}{
"name": "John Doe",
"age": 30,
"active": true,
},
"metadata": map[string]interface{}{
"created_at": "2024-01-01",
"updated_at": "2024-03-20",
},
},
wantErr: false,
},
{
name: "merge with array fields",
schema: schema.New(
"tags", field.Array(field.String()),
"numbers", field.Array(field.Number(field.NumberFormatInt)),
"mixed", field.Array(field.String()),
),
origData: map[string]interface{}{
"tags": []interface{}{"tag1", "tag2"},
"numbers": []interface{}{1, 2, 3},
},
updData: map[string]interface{}{
"tags": []interface{}{"tag3", "tag4"},
"mixed": []interface{}{"value1", "value2"},
},
want: map[string]interface{}{
"tags": []interface{}{"tag3", "tag4"},
"numbers": []interface{}{1, 2, 3},
"mixed": []interface{}{"value1", "value2"},
},
wantErr: false,
},
{
name: "merge with required fields",
schema: schema.New(
"required_field", field.String(),
"optional_field", field.String(),
),
origData: map[string]interface{}{
"required_field": "original",
"optional_field": "optional",
},
updData: map[string]interface{}{
"required_field": "updated",
},
want: map[string]interface{}{
"required_field": "updated",
"optional_field": "optional",
},
wantErr: false,
},
{
name: "merge with validation rules",
schema: schema.New(
"email", field.String(),
"age", field.Number(field.NumberFormatInt),
),
origData: map[string]interface{}{
"email": "test@example.com",
"age": 25,
},
updData: map[string]interface{}{
"email": "new@example.com",
"age": 30,
},
want: map[string]interface{}{
"email": "new@example.com",
"age": 30,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := MergeItemData(context.Background(), tt.schema, tt.origData, tt.updData)
if (err != nil) != tt.wantErr {
t.Errorf("mergeItemData() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, tt.want, got)
})
}
}
func TestToMap(t *testing.T) {
sch := schema.New(
"str", field.String(),
"num", field.Number(field.NumberFormatFloat),
"embedded", field.Object(
"now", field.Time(),
),
"timestamp", field.Timestamp(),
)
tests := []struct {
name string
input *Item
sch *schema.Schema
want map[string]any
assertErr assert.ErrorAssertionFunc
}{
{
name: "nil item",
input: nil,
sch: nil,
want: nil,
assertErr: assert.Error,
},
{
name: "nil schema",
input: &Item{},
sch: nil,
want: nil,
assertErr: assert.Error,
},
{
name: "simple",
input: &Item{
ID: "item_id",
SpaceID: "space_id",
EnvID: "env_id",
CollectionID: "coll_id",
State: StatePublished,
CreatedRevAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
CreatedBy: "created_by",
CreatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
UpdatedBy: "updated_by",
Data: map[string]any{
"str": "string",
"num": 2.7,
"embedded": map[string]any{
"now": time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
},
"timestamp": 1723420800,
},
LocaleID: "locale_id",
Translations: map[string]map[string]any{
"ru": {
"str": "строка",
"num": 3.14,
},
},
TranslationsIDs: []string{"ru"},
RevisionID: "rev_id",
RevisionDescription: "rev_desc",
Permissions: &Permissions{
Edit: true,
Archive: false,
Publish: true,
SoftDelete: false,
HardDelete: false,
},
SearchScore: 123.0,
Deleted: false,
Hidden: true,
Template: true,
},
sch: sch,
want: map[string]any{
"id": "item_id",
"spaceId": "space_id",
"envId": "env_id",
"collectionId": "coll_id",
"state": StatePublished,
"createdRevAt": "2024-08-12T00:00:00Z",
"createdBy": "created_by",
"createdAt": "2024-08-12T00:00:00Z",
"updatedAt": "2024-08-12T00:00:00Z",
"updatedBy": "updated_by",
"data": map[string]any{
"str": "string",
"num": 2.7,
"embedded": map[string]any{
"now": "2024-08-12T00:00:00Z",
},
"timestamp": int64(1723420800),
},
"localeId": "locale_id",
"translations": map[string]map[string]any{
"ru": {
"str": "строка",
"num": 3.14,
},
},
"translationsIds": []string{"ru"},
"revId": "rev_id",
"revDescription": "rev_desc",
"hidden": true,
"template": true,
},
assertErr: assert.NoError,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := ToMap(tc.input, tc.sch)
tc.assertErr(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestFromMap(t *testing.T) {
sch := schema.New(
"str", field.String(),
"num", field.Number(field.NumberFormatFloat),
"embedded", field.Object(
"now", field.Time(),
),
"timestamp", field.Timestamp(),
)
tests := []struct {
name string
input map[string]any
want *Item
sch *schema.Schema
assertErr assert.ErrorAssertionFunc
}{
{
name: "nil item",
input: nil,
sch: nil,
want: nil,
assertErr: assert.Error,
},
{
name: "empty item",
input: map[string]any{},
sch: nil,
want: nil,
assertErr: assert.Error,
},
{
name: "nil schema",
input: map[string]any{},
sch: nil,
assertErr: assert.Error,
},
{
name: "simple",
input: map[string]any{
"id": "item_id",
"spaceId": "space_id",
"envId": "env_id",
"collectionId": "coll_id",
"state": StatePublished,
"createdRevAt": "2024-08-12T00:00:00Z",
"createdBy": "created_by",
"createdAt": "2024-08-12T00:00:00Z",
"updatedAt": "2024-08-12T00:00:00Z",
"updatedBy": "updated_by",
"data": map[string]any{
"str": "string",
"num": 2.7,
"embedded": map[string]any{
"now": "2024-08-12T00:00:00Z",
},
"timestamp": 1723420800,
},
"localeId": "locale_id",
"translations": map[string]map[string]any{
"ru": {
"str": "строка",
"num": 3.14,
},
},
"translationsIds": []string{"ru"},
"revId": "rev_id",
"revDescription": "rev_desc",
"deleted": false,
"hidden": true,
"template": true,
},
sch: sch,
want: &Item{
ID: "item_id",
SpaceID: "space_id",
EnvID: "env_id",
CollectionID: "coll_id",
State: StatePublished,
CreatedRevAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
CreatedBy: "created_by",
CreatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
UpdatedBy: "updated_by",
Data: map[string]any{
"str": "string",
"num": 2.7,
"embedded": map[string]any{
"now": time.Date(2024, time.August, 12, 00, 0, 0, 0, time.UTC),
},
"timestamp": int64(1723420800),
},
LocaleID: "locale_id",
Translations: map[string]map[string]any{
"ru": {
"str": "строка",
"num": 3.14,
},
},
TranslationsIDs: []string{"ru"},
RevisionID: "rev_id",
RevisionDescription: "rev_desc",
Permissions: nil,
SearchScore: 0.0,
Deleted: false,
Hidden: true,
Template: true,
},
assertErr: assert.NoError,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got, err := FromMap(tc.input, sch)
tc.assertErr(t, err)
assert.Equal(t, tc.want, got)
})
}
}
func TestFromMap_WithAsStructuredMut(t *testing.T) {
// Схема для данных
sch := schema.New(
"number", field.Number(field.NumberFormatInt),
"obj", field.Object(
"arr", field.Array(field.String()),
"bool", field.Bool(),
"date", field.Time(),
"stamp", field.Timestamp(),
),
"text", field.String(),
)
// Ожидаемые результаты
expectedDate, _ := time.Parse(time.RFC3339, "2025-05-31T21:00:00Z")
tests := []struct {
name string
data interface{}
}{
{
name: "JSON",
data: []byte(`{
"collectionId":"test_collection_1",
"createdAt":"2025-06-18T09:52:11Z",
"createdBy":"c361pni1l3r7ve137ieg",
"createdRevAt":"2025-06-18T09:52:11Z",
"data":{
"number":1,
"obj":{
"arr":["1","2","3"],
"bool":true,
"date":"2025-05-31T21:00:00Z",
"stamp":1000
},
"text":"massive"
},
"envId":"d0s98dajvknu95q7ia40",
"id":"d198oiqjvknp270h3tf0",
"revId":"d198oiqjvknp270h3tfg",
"spaceId":"d0s98dajvknu95q7ia3g",
"state":1,
"updatedAt":"2025-06-18T09:52:11Z",
"updatedBy":"c361pni1l3r7ve137ieg"
}`),
},
{
name: "Map",
data: map[string]any{
"collectionId": "test_collection_1",
"createdAt": "2025-06-18T09:52:11Z",
"createdBy": "c361pni1l3r7ve137ieg",
"createdRevAt": "2025-06-18T09:52:11Z",
"data": map[string]any{
"number": 1,
"obj": map[string]any{
"arr": []any{"1", "2", "3"},
"bool": true,
"date": "2025-05-31T21:00:00Z",
"stamp": 1000,
},
"text": "massive",
},
"envId": "d0s98dajvknu95q7ia40",
"id": "d198oiqjvknp270h3tf0",
"revId": "d198oiqjvknp270h3tfg",
"spaceId": "d0s98dajvknu95q7ia3g",
"state": 1,
"updatedAt": "2025-06-18T09:52:11Z",
"updatedBy": "c361pni1l3r7ve137ieg",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
var v any
var err error
// Обрабатываем разные типы входных данных
if bytes, ok := tc.data.([]byte); ok {
msg := service.NewMessage(bytes)
v, err = msg.AsStructuredMut()
require.NoError(t, err, "AsStructuredMut не должен возвращать ошибку для %s", tc.name)
} else {
msg := service.NewMessage(nil)
msg.SetStructuredMut(tc.data)
v, err = msg.AsStructuredMut()
require.NoError(t, err, "AsStructuredMut не должен возвращать ошибку для %s", tc.name)
}
data, _ := v.(map[string]any)
item, err := FromMap(data, sch)
require.NoError(t, err, "FromMap должен корректно обработать данные из %s", tc.name)
// Проверяем, что число корректно преобразовано (может быть int64 после AsStructuredMut)
number := item.Data["number"]
if num, ok := number.(int64); ok {
assert.Equal(t, int64(1), num)
} else {
assert.Equal(t, 1, number)
}
assert.Equal(t, "massive", item.Data["text"])
// Проверяем объект
obj, ok := item.Data["obj"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, true, obj["bool"])
assert.Equal(t, []interface{}{"1", "2", "3"}, obj["arr"])
// Проверяем дату
assert.Equal(t, expectedDate, obj["date"])
})
}
}