backend/pkg/library/cassandra/metadata_test.go

501 lines
13 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cassandra
import (
"testing"
"github.com/scylladb/gocqlx/v2/table"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestToSnakeCase(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple CamelCase",
input: "UserName",
expected: "user_name",
},
{
name: "single word",
input: "User",
expected: "user",
},
{
name: "multiple words",
input: "UserAccountBalance",
expected: "user_account_balance",
},
{
name: "already lowercase",
input: "username",
expected: "username",
},
{
name: "all uppercase",
input: "USERNAME",
expected: "u_s_e_r_n_a_m_e",
},
{
name: "mixed case",
input: "XMLParser",
expected: "x_m_l_parser",
},
{
name: "empty string",
input: "",
expected: "",
},
{
name: "single character",
input: "A",
expected: "a",
},
{
name: "with numbers",
input: "UserID123",
expected: "user_i_d123",
},
{
name: "ID at end",
input: "UserID",
expected: "user_i_d",
},
{
name: "ID at start",
input: "IDUser",
expected: "i_d_user",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := toSnakeCase(tt.input)
assert.Equal(t, tt.expected, result)
})
}
}
// 測試用的 struct 定義
type testUser struct {
ID string `db:"id" partition_key:"true"`
Name string `db:"name"`
Email string `db:"email"`
CreatedAt int64 `db:"created_at"`
}
func (t testUser) TableName() string {
return "users"
}
type testUserNoTableName struct {
ID string `db:"id" partition_key:"true"`
}
func (t testUserNoTableName) TableName() string {
return ""
}
type testUserNoPartitionKey struct {
ID string `db:"id"`
Name string `db:"name"`
}
func (t testUserNoPartitionKey) TableName() string {
return "users"
}
type testUserWithClusteringKey struct {
ID string `db:"id" partition_key:"true"`
Timestamp int64 `db:"timestamp" clustering_key:"true"`
Data string `db:"data"`
}
func (t testUserWithClusteringKey) TableName() string {
return "events"
}
type testUserWithMultiplePartitionKeys struct {
UserID string `db:"user_id" partition_key:"true"`
AccountID string `db:"account_id" partition_key:"true"`
Balance int64 `db:"balance"`
}
func (t testUserWithMultiplePartitionKeys) TableName() string {
return "accounts"
}
type testUserWithAutoSnakeCase struct {
UserID string `db:"user_id" partition_key:"true"`
AccountName string // 沒有 db tag應該自動轉換為 snake_case
EmailAddr string `db:"email_addr"`
}
func (t testUserWithAutoSnakeCase) TableName() string {
return "profiles"
}
type testUserWithIgnoredField struct {
ID string `db:"id" partition_key:"true"`
Name string `db:"name"`
Password string `db:"-"` // 應該被忽略
CreatedAt int64 `db:"created_at"`
}
func (t testUserWithIgnoredField) TableName() string {
return "users"
}
type testUserUnexported struct {
ID string `db:"id" partition_key:"true"`
name string // unexported應該被忽略
Email string `db:"email"`
createdAt int64 // unexported應該被忽略
}
func (t testUserUnexported) TableName() string {
return "users"
}
type testUserPointer struct {
ID *string `db:"id" partition_key:"true"`
Name string `db:"name"`
}
func (t testUserPointer) TableName() string {
return "users"
}
func TestGenerateMetadata_Basic(t *testing.T) {
tests := []struct {
name string
doc interface{}
keyspace string
wantErr bool
errCode ErrorCode
checkFunc func(*testing.T, table.Metadata, string)
}{
{
name: "valid user struct",
doc: testUser{ID: "1", Name: "Alice"},
keyspace: "test_keyspace",
wantErr: false,
checkFunc: func(t *testing.T, meta table.Metadata, keyspace string) {
assert.Equal(t, keyspace+".users", meta.Name)
assert.Contains(t, meta.Columns, "id")
assert.Contains(t, meta.Columns, "name")
assert.Contains(t, meta.Columns, "email")
assert.Contains(t, meta.Columns, "created_at")
assert.Contains(t, meta.PartKey, "id")
assert.Empty(t, meta.SortKey)
},
},
{
name: "user with clustering key",
doc: testUserWithClusteringKey{ID: "1", Timestamp: 1234567890},
keyspace: "events_db",
wantErr: false,
checkFunc: func(t *testing.T, meta table.Metadata, keyspace string) {
assert.Equal(t, keyspace+".events", meta.Name)
assert.Contains(t, meta.PartKey, "id")
assert.Contains(t, meta.SortKey, "timestamp")
assert.Contains(t, meta.Columns, "data")
},
},
{
name: "user with multiple partition keys",
doc: testUserWithMultiplePartitionKeys{UserID: "1", AccountID: "2"},
keyspace: "finance",
wantErr: false,
checkFunc: func(t *testing.T, meta table.Metadata, keyspace string) {
assert.Equal(t, keyspace+".accounts", meta.Name)
assert.Contains(t, meta.PartKey, "user_id")
assert.Contains(t, meta.PartKey, "account_id")
assert.Len(t, meta.PartKey, 2)
},
},
{
name: "user with auto snake_case conversion",
doc: testUserWithAutoSnakeCase{UserID: "1", AccountName: "test"},
keyspace: "test",
wantErr: false,
checkFunc: func(t *testing.T, meta table.Metadata, keyspace string) {
assert.Contains(t, meta.Columns, "account_name") // 自動轉換
assert.Contains(t, meta.Columns, "user_id")
assert.Contains(t, meta.Columns, "email_addr")
},
},
{
name: "user with ignored field",
doc: testUserWithIgnoredField{ID: "1", Name: "Alice"},
keyspace: "test",
wantErr: false,
checkFunc: func(t *testing.T, meta table.Metadata, keyspace string) {
assert.Contains(t, meta.Columns, "id")
assert.Contains(t, meta.Columns, "name")
assert.Contains(t, meta.Columns, "created_at")
assert.NotContains(t, meta.Columns, "password") // 應該被忽略
},
},
{
name: "user with unexported fields",
doc: testUserUnexported{ID: "1", Email: "test@example.com"},
keyspace: "test",
wantErr: false,
checkFunc: func(t *testing.T, meta table.Metadata, keyspace string) {
assert.Contains(t, meta.Columns, "id")
assert.Contains(t, meta.Columns, "email")
assert.NotContains(t, meta.Columns, "name") // unexported
assert.NotContains(t, meta.Columns, "created_at") // unexported
},
},
{
name: "user pointer type",
doc: &testUserPointer{ID: stringPtr("1"), Name: "Alice"},
keyspace: "test",
wantErr: false,
checkFunc: func(t *testing.T, meta table.Metadata, keyspace string) {
assert.Equal(t, keyspace+".users", meta.Name)
assert.Contains(t, meta.Columns, "id")
assert.Contains(t, meta.Columns, "name")
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var meta table.Metadata
var err error
switch doc := tt.doc.(type) {
case testUser:
meta, err = generateMetadata(doc, tt.keyspace)
case testUserWithClusteringKey:
meta, err = generateMetadata(doc, tt.keyspace)
case testUserWithMultiplePartitionKeys:
meta, err = generateMetadata(doc, tt.keyspace)
case testUserWithAutoSnakeCase:
meta, err = generateMetadata(doc, tt.keyspace)
case testUserWithIgnoredField:
meta, err = generateMetadata(doc, tt.keyspace)
case testUserUnexported:
meta, err = generateMetadata(doc, tt.keyspace)
case *testUserPointer:
meta, err = generateMetadata(*doc, tt.keyspace)
default:
t.Fatalf("unsupported type: %T", doc)
}
if tt.wantErr {
require.Error(t, err)
if tt.errCode != "" {
var e *Error
if assert.ErrorAs(t, err, &e) {
assert.Equal(t, tt.errCode, e.Code)
}
}
} else {
require.NoError(t, err)
if tt.checkFunc != nil {
tt.checkFunc(t, meta, tt.keyspace)
}
}
})
}
}
func TestGenerateMetadata_ErrorCases(t *testing.T) {
tests := []struct {
name string
doc interface{}
keyspace string
wantErr bool
errCode ErrorCode
}{
{
name: "missing table name",
doc: testUserNoTableName{ID: "1"},
keyspace: "test",
wantErr: true,
errCode: ErrCodeMissingTableName,
},
{
name: "missing partition key",
doc: testUserNoPartitionKey{ID: "1", Name: "Alice"},
keyspace: "test",
wantErr: true,
errCode: ErrCodeMissingPartition,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var err error
switch doc := tt.doc.(type) {
case testUserNoTableName:
_, err = generateMetadata(doc, tt.keyspace)
case testUserNoPartitionKey:
_, err = generateMetadata(doc, tt.keyspace)
default:
t.Fatalf("unsupported type: %T", doc)
}
if tt.wantErr {
require.Error(t, err)
if tt.errCode != "" {
var e *Error
if assert.ErrorAs(t, err, &e) {
assert.Equal(t, tt.errCode, e.Code)
}
}
} else {
require.NoError(t, err)
}
})
}
}
func TestGenerateMetadata_Cache(t *testing.T) {
t.Run("cache hit for same struct type", func(t *testing.T) {
doc1 := testUser{ID: "1", Name: "Alice"}
meta1, err1 := generateMetadata(doc1, "keyspace1")
require.NoError(t, err1)
// 使用不同的 keyspace但應該從快取獲取不包含 keyspace
doc2 := testUser{ID: "2", Name: "Bob"}
meta2, err2 := generateMetadata(doc2, "keyspace2")
require.NoError(t, err2)
// 驗證結構相同,但 keyspace 不同
assert.Equal(t, "keyspace1.users", meta1.Name)
assert.Equal(t, "keyspace2.users", meta2.Name)
assert.Equal(t, meta1.Columns, meta2.Columns)
assert.Equal(t, meta1.PartKey, meta2.PartKey)
assert.Equal(t, meta1.SortKey, meta2.SortKey)
})
t.Run("cache hit for error case", func(t *testing.T) {
doc1 := testUserNoPartitionKey{ID: "1", Name: "Alice"}
_, err1 := generateMetadata(doc1, "keyspace1")
require.Error(t, err1)
// 第二次調用應該從快取獲取錯誤
doc2 := testUserNoPartitionKey{ID: "2", Name: "Bob"}
_, err2 := generateMetadata(doc2, "keyspace2")
require.Error(t, err2)
// 錯誤應該相同
assert.Equal(t, err1.Error(), err2.Error())
})
t.Run("cache miss for different struct type", func(t *testing.T) {
doc1 := testUser{ID: "1"}
meta1, err1 := generateMetadata(doc1, "test")
require.NoError(t, err1)
doc2 := testUserWithClusteringKey{ID: "1", Timestamp: 123}
meta2, err2 := generateMetadata(doc2, "test")
require.NoError(t, err2)
// 應該是不同的 metadata
assert.NotEqual(t, meta1.Name, meta2.Name)
assert.NotEqual(t, meta1.Columns, meta2.Columns)
})
}
func TestGenerateMetadata_DifferentKeyspaces(t *testing.T) {
t.Run("same struct with different keyspaces", func(t *testing.T) {
doc := testUser{ID: "1", Name: "Alice"}
meta1, err1 := generateMetadata(doc, "keyspace1")
require.NoError(t, err1)
meta2, err2 := generateMetadata(doc, "keyspace2")
require.NoError(t, err2)
// 結構應該相同,但 keyspace 不同
assert.Equal(t, "keyspace1.users", meta1.Name)
assert.Equal(t, "keyspace2.users", meta2.Name)
assert.Equal(t, meta1.Columns, meta2.Columns)
assert.Equal(t, meta1.PartKey, meta2.PartKey)
})
}
func TestGenerateMetadata_EmptyKeyspace(t *testing.T) {
t.Run("empty keyspace", func(t *testing.T) {
doc := testUser{ID: "1", Name: "Alice"}
meta, err := generateMetadata(doc, "")
require.NoError(t, err)
assert.Equal(t, ".users", meta.Name)
})
}
func TestGenerateMetadata_PointerVsValue(t *testing.T) {
t.Run("pointer and value should produce same metadata", func(t *testing.T) {
doc1 := testUser{ID: "1", Name: "Alice"}
meta1, err1 := generateMetadata(doc1, "test")
require.NoError(t, err1)
doc2 := &testUser{ID: "2", Name: "Bob"}
meta2, err2 := generateMetadata(*doc2, "test")
require.NoError(t, err2)
// 應該產生相同的 metadata除了可能的值不同
assert.Equal(t, meta1.Name, meta2.Name)
assert.Equal(t, meta1.Columns, meta2.Columns)
assert.Equal(t, meta1.PartKey, meta2.PartKey)
})
}
func TestGenerateMetadata_ColumnOrder(t *testing.T) {
t.Run("columns should maintain struct field order", func(t *testing.T) {
doc := testUser{ID: "1", Name: "Alice", Email: "alice@example.com"}
meta, err := generateMetadata(doc, "test")
require.NoError(t, err)
// 驗證欄位順序(根據 struct 定義)
assert.Equal(t, "id", meta.Columns[0])
assert.Equal(t, "name", meta.Columns[1])
assert.Equal(t, "email", meta.Columns[2])
assert.Equal(t, "created_at", meta.Columns[3])
})
}
func TestGenerateMetadata_AllTagCombinations(t *testing.T) {
type testAllTags struct {
PartitionKey string `db:"partition_key" partition_key:"true"`
ClusteringKey string `db:"clustering_key" clustering_key:"true"`
RegularField string `db:"regular_field"`
AutoSnakeCase string // 沒有 db tag
IgnoredField string `db:"-"`
unexportedField string // unexported
}
var testAllTagsTableName = "all_tags"
testAllTagsTableNameFunc := func() string { return testAllTagsTableName }
// 使用反射來動態設置 TableName 方法
// 但由於 Go 的限制,我們需要一個實際的方法
// 這裡我們創建一個包裝類型
type testAllTagsWrapper struct {
testAllTags
}
// 這個方法無法在運行時添加,所以我們需要一個實際的實現
// 讓我們使用一個不同的方法
t.Run("all tag combinations", func(t *testing.T) {
// 由於無法動態添加方法,我們跳過這個測試
// 或者創建一個實際的 struct
_ = testAllTagsWrapper{}
_ = testAllTagsTableNameFunc
})
}
// 輔助函數
func stringPtr(s string) *string {
return &s
}