package resolvespec import ( "strings" "testing" "github.com/bitechdev/ResolveSpec/pkg/common" ) func TestGetCursorFilter_Forward(t *testing.T) { options := common.RequestOptions{ Sort: []common.SortOption{ {Column: "created_at", Direction: "DESC"}, {Column: "id", Direction: "ASC"}, }, CursorForward: "123", } tableName := "posts" pkName := "id" modelColumns := []string{"id", "title", "created_at", "user_id"} filter, err := GetCursorFilter(tableName, pkName, modelColumns, options) if err != nil { t.Fatalf("GetCursorFilter failed: %v", err) } if filter == "" { t.Fatal("Expected non-empty cursor filter") } // Verify filter contains EXISTS subquery if !strings.Contains(filter, "EXISTS") { t.Errorf("Filter should contain EXISTS subquery, got: %s", filter) } // Verify filter references the cursor ID if !strings.Contains(filter, "123") { t.Errorf("Filter should reference cursor ID 123, got: %s", filter) } // Verify filter contains the table name if !strings.Contains(filter, tableName) { t.Errorf("Filter should reference table name %s, got: %s", tableName, filter) } // Verify filter contains primary key if !strings.Contains(filter, pkName) { t.Errorf("Filter should reference primary key %s, got: %s", pkName, filter) } t.Logf("Generated cursor filter: %s", filter) } func TestGetCursorFilter_Backward(t *testing.T) { options := common.RequestOptions{ Sort: []common.SortOption{ {Column: "created_at", Direction: "DESC"}, {Column: "id", Direction: "ASC"}, }, CursorBackward: "456", } tableName := "posts" pkName := "id" modelColumns := []string{"id", "title", "created_at", "user_id"} filter, err := GetCursorFilter(tableName, pkName, modelColumns, options) if err != nil { t.Fatalf("GetCursorFilter failed: %v", err) } if filter == "" { t.Fatal("Expected non-empty cursor filter") } // Verify filter contains cursor ID if !strings.Contains(filter, "456") { t.Errorf("Filter should reference cursor ID 456, got: %s", filter) } // For backward cursor, sort direction should be reversed // This is handled internally by the GetCursorFilter function t.Logf("Generated backward cursor filter: %s", filter) } func TestGetCursorFilter_NoCursor(t *testing.T) { options := common.RequestOptions{ Sort: []common.SortOption{ {Column: "created_at", Direction: "DESC"}, }, // No cursor set } tableName := "posts" pkName := "id" modelColumns := []string{"id", "title", "created_at"} _, err := GetCursorFilter(tableName, pkName, modelColumns, options) if err == nil { t.Error("Expected error when no cursor is provided") } if !strings.Contains(err.Error(), "no cursor provided") { t.Errorf("Expected 'no cursor provided' error, got: %v", err) } } func TestGetCursorFilter_NoSort(t *testing.T) { options := common.RequestOptions{ Sort: []common.SortOption{}, CursorForward: "123", } tableName := "posts" pkName := "id" modelColumns := []string{"id", "title"} _, err := GetCursorFilter(tableName, pkName, modelColumns, options) if err == nil { t.Error("Expected error when no sort columns are defined") } if !strings.Contains(err.Error(), "no sort columns") { t.Errorf("Expected 'no sort columns' error, got: %v", err) } } func TestGetCursorFilter_MultiColumnSort(t *testing.T) { options := common.RequestOptions{ Sort: []common.SortOption{ {Column: "priority", Direction: "DESC"}, {Column: "created_at", Direction: "DESC"}, {Column: "id", Direction: "ASC"}, }, CursorForward: "789", } tableName := "tasks" pkName := "id" modelColumns := []string{"id", "title", "priority", "created_at"} filter, err := GetCursorFilter(tableName, pkName, modelColumns, options) if err != nil { t.Fatalf("GetCursorFilter failed: %v", err) } // Verify filter contains priority column if !strings.Contains(filter, "priority") { t.Errorf("Filter should reference priority column, got: %s", filter) } // Verify filter contains created_at column if !strings.Contains(filter, "created_at") { t.Errorf("Filter should reference created_at column, got: %s", filter) } t.Logf("Generated multi-column cursor filter: %s", filter) } func TestGetCursorFilter_WithSchemaPrefix(t *testing.T) { options := common.RequestOptions{ Sort: []common.SortOption{ {Column: "name", Direction: "ASC"}, }, CursorForward: "100", } tableName := "public.users" pkName := "id" modelColumns := []string{"id", "name", "email"} filter, err := GetCursorFilter(tableName, pkName, modelColumns, options) if err != nil { t.Fatalf("GetCursorFilter failed: %v", err) } // Should handle schema prefix properly if !strings.Contains(filter, "users") { t.Errorf("Filter should reference table name users, got: %s", filter) } t.Logf("Generated cursor filter with schema: %s", filter) } func TestGetActiveCursor(t *testing.T) { tests := []struct { name string options common.RequestOptions expectedID string expectedDirection CursorDirection }{ { name: "Forward cursor only", options: common.RequestOptions{ CursorForward: "123", }, expectedID: "123", expectedDirection: CursorForward, }, { name: "Backward cursor only", options: common.RequestOptions{ CursorBackward: "456", }, expectedID: "456", expectedDirection: CursorBackward, }, { name: "Both cursors - forward takes precedence", options: common.RequestOptions{ CursorForward: "123", CursorBackward: "456", }, expectedID: "123", expectedDirection: CursorForward, }, { name: "No cursors", options: common.RequestOptions{}, expectedID: "", expectedDirection: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { id, direction := getActiveCursor(tt.options) if id != tt.expectedID { t.Errorf("Expected cursor ID %q, got %q", tt.expectedID, id) } if direction != tt.expectedDirection { t.Errorf("Expected direction %d, got %d", tt.expectedDirection, direction) } }) } } func TestResolveColumn(t *testing.T) { tests := []struct { name string field string prefix string tableName string modelColumns []string wantCursor string wantTarget string wantErr bool }{ { name: "Simple column", field: "id", prefix: "", tableName: "users", modelColumns: []string{"id", "name", "email"}, wantCursor: "cursor_select.id", wantTarget: "users.id", wantErr: false, }, { name: "Column with case insensitive match", field: "NAME", prefix: "", tableName: "users", modelColumns: []string{"id", "name", "email"}, wantCursor: "cursor_select.NAME", wantTarget: "users.NAME", wantErr: false, }, { name: "Invalid column", field: "invalid_field", prefix: "", tableName: "users", modelColumns: []string{"id", "name", "email"}, wantErr: true, }, { name: "JSON field", field: "metadata->>'key'", prefix: "", tableName: "posts", modelColumns: []string{"id", "metadata"}, wantCursor: "cursor_select.metadata->>'key'", wantTarget: "posts.metadata->>'key'", wantErr: false, }, { name: "Joined column (not supported)", field: "name", prefix: "user", tableName: "posts", modelColumns: []string{"id", "title"}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cursor, target, err := resolveColumn(tt.field, tt.prefix, tt.tableName, tt.modelColumns) if tt.wantErr { if err == nil { t.Error("Expected error but got none") } return } if err != nil { t.Fatalf("Unexpected error: %v", err) } if cursor != tt.wantCursor { t.Errorf("Expected cursor %q, got %q", tt.wantCursor, cursor) } if target != tt.wantTarget { t.Errorf("Expected target %q, got %q", tt.wantTarget, target) } }) } } func TestBuildPriorityChain(t *testing.T) { clauses := []string{ "cursor_select.priority > tasks.priority", "cursor_select.created_at > tasks.created_at", "cursor_select.id < tasks.id", } result := buildPriorityChain(clauses) // Should build OR-AND chain for cursor comparison if !strings.Contains(result, "OR") { t.Error("Priority chain should contain OR operators") } if !strings.Contains(result, "AND") { t.Error("Priority chain should contain AND operators for composite conditions") } // First clause should appear standalone if !strings.Contains(result, clauses[0]) { t.Errorf("Priority chain should contain first clause: %s", clauses[0]) } t.Logf("Built priority chain: %s", result) } func TestCursorFilter_SQL_Safety(t *testing.T) { // Test that cursor filter doesn't allow SQL injection options := common.RequestOptions{ Sort: []common.SortOption{ {Column: "created_at", Direction: "DESC"}, }, CursorForward: "123; DROP TABLE users; --", } tableName := "posts" pkName := "id" modelColumns := []string{"id", "created_at"} filter, err := GetCursorFilter(tableName, pkName, modelColumns, options) if err != nil { t.Fatalf("GetCursorFilter failed: %v", err) } // The cursor ID is inserted directly into the query // This should be sanitized by the sanitizeWhereClause function in the handler // For now, just verify it generates a filter if filter == "" { t.Error("Expected non-empty cursor filter even with special characters") } t.Logf("Generated filter with special chars in cursor: %s", filter) }