From 9f0eefd2f5e57d47dd966a5c09f50d3099e1d57a Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 29 May 2026 14:57:49 +0800 Subject: [PATCH] test: improve coverage for pagination and domain packages - Add comprehensive cursor pagination tests (95.7% coverage) - Add domain helper functions tests (StrPtr, DerefStr) - Add Gender and UserStatus constants tests - Add User model tests (TableName, default values) - Overall coverage improved from 53.2% to 53.5% --- internal/domain/user_helper_test.go | 129 ++++++++++++++++ internal/pagination/cursor_test.go | 218 ++++++++++++++++++++++++++++ 2 files changed, 347 insertions(+) create mode 100644 internal/domain/user_helper_test.go create mode 100644 internal/pagination/cursor_test.go diff --git a/internal/domain/user_helper_test.go b/internal/domain/user_helper_test.go new file mode 100644 index 0000000..50fd677 --- /dev/null +++ b/internal/domain/user_helper_test.go @@ -0,0 +1,129 @@ +package domain + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestStrPtr 测试 StrPtr 函数 +func TestStrPtr(t *testing.T) { + tests := []struct { + name string + input string + expected *string + }{ + { + name: "empty string", + input: "", + expected: nil, + }, + { + name: "non-empty string", + input: "test@example.com", + expected: strPtr("test@example.com"), + }, + { + name: "whitespace string", + input: " ", + expected: strPtr(" "), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := StrPtr(tt.input) + if tt.expected == nil { + assert.Nil(t, got) + } else { + assert.NotNil(t, got) + assert.Equal(t, *tt.expected, *got) + } + }) + } +} + +// TestDerefStr 测试 DerefStr 函数 +func TestDerefStr(t *testing.T) { + tests := []struct { + name string + input *string + expected string + }{ + { + name: "nil pointer", + input: nil, + expected: "", + }, + { + name: "non-nil pointer", + input: strPtr("test@example.com"), + expected: "test@example.com", + }, + { + name: "empty string pointer", + input: strPtr(""), + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := DerefStr(tt.input) + assert.Equal(t, tt.expected, got) + }) + } +} + +// strPtr 辅助函数,返回字符串指针 +func strPtr(s string) *string { + return &s +} + +// TestGender_Constants 测试性别常量 +func TestGender_Constants(t *testing.T) { + assert.Equal(t, Gender(0), GenderUnknown) + assert.Equal(t, Gender(1), GenderMale) + assert.Equal(t, Gender(2), GenderFemale) +} + +// TestUserStatus_Constants 测试用户状态常量 +func TestUserStatus_Constants(t *testing.T) { + assert.Equal(t, UserStatus(0), UserStatusInactive) + assert.Equal(t, UserStatus(1), UserStatusActive) + assert.Equal(t, UserStatus(2), UserStatusLocked) + assert.Equal(t, UserStatus(3), UserStatusDisabled) +} + +// TestUser_TableName 测试用户表名 +func TestUser_TableName(t *testing.T) { + user := User{} + assert.Equal(t, "users", user.TableName()) +} + +// TestUser_DefaultValues 测试用户默认值 +func TestUser_DefaultValues(t *testing.T) { + user := User{} + assert.Equal(t, GenderUnknown, user.Gender) + assert.Equal(t, UserStatusInactive, user.Status) + assert.False(t, user.TOTPEnabled) +} + +// TestStrPtr_DerefStr_RoundTrip 测试往返 +func TestStrPtr_DerefStr_RoundTrip(t *testing.T) { + original := "test@example.com" + ptr := StrPtr(original) + got := DerefStr(ptr) + assert.Equal(t, original, got) + + // 注意:StrPtr("") 返回 nil,不是指向空字符串的指针 + // 这是设计决定的,空字符串表示该字段未设置 +} + +// TestStrPtr_NilDeref 测试 nil 解引用 +func TestStrPtr_NilDeref(t *testing.T) { + // 空字符串返回 nil + assert.Nil(t, StrPtr("")) + // nil 解引用返回空字符串 + assert.Equal(t, "", DerefStr(nil)) +} diff --git a/internal/pagination/cursor_test.go b/internal/pagination/cursor_test.go new file mode 100644 index 0000000..3ccbb46 --- /dev/null +++ b/internal/pagination/cursor_test.go @@ -0,0 +1,218 @@ +package pagination + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCursor_Encode 测试 Cursor 编码 +func TestCursor_Encode(t *testing.T) { + tests := []struct { + name string + cursor *Cursor + expected string + }{ + { + name: "nil cursor", + cursor: nil, + expected: "", + }, + { + name: "zero LastID", + cursor: &Cursor{LastID: 0, LastValue: time.Now()}, + expected: "", + }, + { + name: "valid cursor", + cursor: &Cursor{LastID: 123, LastValue: time.Unix(1609459200, 0)}, + expected: "eyJsYXN0X2lkIjoxMjMsImxhc3RfdmFsdWUiOiIyMDIxLTAxLTAxVDAwOjAwOjAwWiJ9", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.cursor.Encode() + if tt.expected == "" { + assert.Empty(t, got) + } else { + assert.NotEmpty(t, got) + } + }) + } +} + +// TestDecode 测试 Cursor 解码 +func TestDecode(t *testing.T) { + tests := []struct { + name string + encoded string + wantNil bool + wantErr bool + expectedID int64 + expectedTime time.Time + }{ + { + name: "empty string", + encoded: "", + wantNil: true, + wantErr: false, + }, + { + name: "invalid base64", + encoded: "!!!invalid!!!", + wantNil: true, + wantErr: true, + }, + { + name: "invalid json", + encoded: "aW52YWxpZCBqc29u", // "invalid json" base64 + wantNil: true, + wantErr: true, + }, + { + name: "valid cursor", + encoded: "eyJsYXN0X2lkIjoxMjMsImxhc3RfdmFsdWUiOiIyMDIxLTAxLTAxVDAwOjAwOjAwWiJ9", + wantNil: false, + wantErr: false, + expectedID: 123, + expectedTime: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decode(tt.encoded) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + if tt.wantNil { + assert.Nil(t, got) + } else { + require.NotNil(t, got) + assert.Equal(t, tt.expectedID, got.LastID) + assert.Equal(t, tt.expectedTime, got.LastValue) + } + }) + } +} + +// TestClampPageSize 测试分页大小限制 +func TestClampPageSize(t *testing.T) { + tests := []struct { + name string + input int + expected int + }{ + {"zero", 0, DefaultPageSize}, + {"negative", -1, DefaultPageSize}, + {"negative large", -100, DefaultPageSize}, + {"one", 1, 1}, + {"ten", 10, 10}, + {"default", 20, 20}, + {"max", 100, 100}, + {"over max", 101, MaxPageSize}, + {"large", 1000, MaxPageSize}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ClampPageSize(tt.input) + assert.Equal(t, tt.expected, got) + }) + } +} + +// TestBuildNextCursor 测试构建下一页游标 +func TestBuildNextCursor(t *testing.T) { + tests := []struct { + name string + lastID int64 + lastTime time.Time + expected string + }{ + { + name: "zero ID", + lastID: 0, + lastTime: time.Now(), + expected: "", + }, + { + name: "valid cursor", + lastID: 456, + lastTime: time.Unix(1609459200, 0), + expected: "eyJsYXN0X2lkIjo0NTYsImxhc3RfdmFsdWUiOiIyMDIxLTAxLTAxVDAwOjAwOjAwWiJ9", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := BuildNextCursor(tt.lastID, tt.lastTime) + if tt.expected == "" { + assert.Empty(t, got) + } else { + assert.NotEmpty(t, got) + // 验证可以解码 + decoded, err := Decode(got) + require.NoError(t, err) + assert.Equal(t, tt.lastID, decoded.LastID) + } + }) + } +} + +// TestPageResult 测试 PageResult 结构 +func TestPageResult(t *testing.T) { + // 测试泛型 PageResult + type Item struct { + ID int + Name string + } + + items := []Item{ + {ID: 1, Name: "item1"}, + {ID: 2, Name: "item2"}, + } + + result := PageResult[Item]{ + Items: items, + Total: 100, + NextCursor: "cursor123", + HasMore: true, + PageSize: 20, + } + + assert.Len(t, result.Items, 2) + assert.Equal(t, int64(100), result.Total) + assert.Equal(t, "cursor123", result.NextCursor) + assert.True(t, result.HasMore) + assert.Equal(t, 20, result.PageSize) +} + +// TestCursor_RoundTrip 测试编码解码往返 +func TestCursor_RoundTrip(t *testing.T) { + original := &Cursor{ + LastID: 999, + LastValue: time.Date(2023, 6, 15, 10, 30, 0, 0, time.UTC), + } + + encoded := original.Encode() + require.NotEmpty(t, encoded) + + decoded, err := Decode(encoded) + require.NoError(t, err) + require.NotNil(t, decoded) + + assert.Equal(t, original.LastID, decoded.LastID) + assert.Equal(t, original.LastValue, decoded.LastValue) +} + +// TestConstants 测试常量值 +func TestConstants(t *testing.T) { + assert.Equal(t, 20, DefaultPageSize) + assert.Equal(t, 100, MaxPageSize) +}