| // Copyright 2025 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package mcp |
| |
| import ( |
| "log" |
| "slices" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| ) |
| |
| type testItem struct { |
| Name string |
| Value string |
| } |
| |
| type testListParams struct { |
| Cursor string |
| } |
| |
| func (p *testListParams) cursorPtr() *string { |
| return &p.Cursor |
| } |
| |
| type testListResult struct { |
| Items []*testItem |
| NextCursor string |
| } |
| |
| func (r *testListResult) nextCursorPtr() *string { |
| return &r.NextCursor |
| } |
| |
| var allTestItems = []*testItem{ |
| {"alpha", "val-A"}, |
| {"bravo", "val-B"}, |
| {"charlie", "val-C"}, |
| {"delta", "val-D"}, |
| {"echo", "val-E"}, |
| {"foxtrot", "val-F"}, |
| {"golf", "val-G"}, |
| {"hotel", "val-H"}, |
| {"india", "val-I"}, |
| {"juliet", "val-J"}, |
| {"kilo", "val-K"}, |
| } |
| |
| // getCursor encodes a string input into a URL-safe base64 cursor, |
| // fatally logging any encoding errors. |
| func getCursor(input string) string { |
| cursor, err := encodeCursor(input) |
| if err != nil { |
| log.Fatalf("encodeCursor(%s) error = %v", input, err) |
| } |
| return cursor |
| } |
| |
| func TestServerPaginateBasic(t *testing.T) { |
| testCases := []struct { |
| name string |
| initialItems []*testItem |
| inputCursor string |
| inputPageSize int |
| wantFeatures []*testItem |
| wantNextCursor string |
| wantErr bool |
| }{ |
| { |
| name: "FirstPage_DefaultSize_Full", |
| initialItems: allTestItems, |
| inputCursor: "", |
| inputPageSize: 5, |
| wantFeatures: allTestItems[0:5], |
| wantNextCursor: getCursor("echo"), // Based on last item of first page |
| wantErr: false, |
| }, |
| { |
| name: "SecondPage_DefaultSize_Full", |
| initialItems: allTestItems, |
| inputCursor: getCursor("echo"), |
| inputPageSize: 5, |
| wantFeatures: allTestItems[5:10], |
| wantNextCursor: getCursor("juliet"), // Based on last item of second page |
| wantErr: false, |
| }, |
| { |
| name: "SecondPage_DefaultSize_Full_OutOfOrder", |
| initialItems: append(allTestItems[5:], allTestItems[0:5]...), |
| inputCursor: getCursor("echo"), |
| inputPageSize: 5, |
| wantFeatures: allTestItems[5:10], |
| wantNextCursor: getCursor("juliet"), // Based on last item of second page |
| wantErr: false, |
| }, |
| { |
| name: "SecondPage_DefaultSize_Full_Duplicates", |
| initialItems: append(allTestItems, allTestItems[0:5]...), |
| inputCursor: getCursor("echo"), |
| inputPageSize: 5, |
| wantFeatures: allTestItems[5:10], |
| wantNextCursor: getCursor("juliet"), // Based on last item of second page |
| wantErr: false, |
| }, |
| { |
| name: "LastPage_Remaining", |
| initialItems: allTestItems, |
| inputCursor: getCursor("juliet"), |
| inputPageSize: 5, |
| wantFeatures: allTestItems[10:11], // Only 1 item left |
| wantNextCursor: "", // No more pages |
| wantErr: false, |
| }, |
| { |
| name: "PageSize_1", |
| initialItems: allTestItems, |
| inputCursor: "", |
| inputPageSize: 1, |
| wantFeatures: allTestItems[0:1], |
| wantNextCursor: getCursor("alpha"), |
| wantErr: false, |
| }, |
| { |
| name: "PageSize_All", |
| initialItems: allTestItems, |
| inputCursor: "", |
| inputPageSize: len(allTestItems), // Page size equals total |
| wantFeatures: allTestItems, |
| wantNextCursor: "", // No more pages |
| wantErr: false, |
| }, |
| { |
| name: "PageSize_LargerThanAll", |
| initialItems: allTestItems, |
| inputCursor: "", |
| inputPageSize: len(allTestItems) + 5, // Page size larger than total |
| wantFeatures: allTestItems, |
| wantNextCursor: "", |
| wantErr: false, |
| }, |
| { |
| name: "EmptySet", |
| initialItems: nil, |
| inputCursor: "", |
| inputPageSize: 5, |
| wantFeatures: nil, |
| wantNextCursor: "", |
| wantErr: false, |
| }, |
| { |
| name: "InvalidCursor", |
| initialItems: allTestItems, |
| inputCursor: "not-a-valid-gob-base64-cursor", |
| inputPageSize: 5, |
| wantFeatures: nil, // Should be nil for error cases |
| wantNextCursor: "", |
| wantErr: true, |
| }, |
| { |
| name: "AboveNonExistentID", |
| initialItems: allTestItems, |
| inputCursor: getCursor("dne"), // A UID that doesn't exist |
| inputPageSize: 5, |
| wantFeatures: allTestItems[4:9], // Should return elements above UID. |
| wantNextCursor: getCursor("india"), |
| wantErr: false, |
| }, |
| } |
| |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| fs := newFeatureSet(func(t *testItem) string { return t.Name }) |
| fs.add(tc.initialItems...) |
| params := &testListParams{Cursor: tc.inputCursor} |
| gotResult, err := paginateList(fs, tc.inputPageSize, params, &testListResult{}, func(res *testListResult, items []*testItem) { |
| res.Items = items |
| }) |
| if (err != nil) != tc.wantErr { |
| t.Errorf("paginateList(%s) error, got %v, wantErr %v", tc.name, err, tc.wantErr) |
| } |
| if tc.wantErr { |
| return |
| } |
| if diff := cmp.Diff(tc.wantFeatures, gotResult.Items); diff != "" { |
| t.Errorf("paginateList(%s) mismatch (-want +got):\n%s", tc.name, diff) |
| } |
| if tc.wantNextCursor != gotResult.NextCursor { |
| t.Errorf("paginateList(%s) nextCursor, got %v, want %v", tc.name, gotResult.NextCursor, tc.wantNextCursor) |
| } |
| }) |
| } |
| } |
| |
| func TestServerPaginateVariousPageSizes(t *testing.T) { |
| fs := newFeatureSet(func(t *testItem) string { return t.Name }) |
| fs.add(allTestItems...) |
| // Try all possible page sizes, ensuring we get the correct list of items. |
| for pageSize := 1; pageSize < len(allTestItems)+1; pageSize++ { |
| var gotItems []*testItem |
| var nextCursor string |
| wantChunks := slices.Collect(slices.Chunk(allTestItems, pageSize)) |
| index := 0 |
| // Iterate through all pages, comparing sub-slices to the paginated list. |
| for { |
| params := &testListParams{Cursor: nextCursor} |
| gotResult, err := paginateList(fs, pageSize, params, &testListResult{}, func(res *testListResult, items []*testItem) { |
| res.Items = items |
| }) |
| if err != nil { |
| t.Fatalf("paginateList() unexpected error for pageSize %d, cursor %q: %v", pageSize, nextCursor, err) |
| } |
| if diff := cmp.Diff(wantChunks[index], gotResult.Items); diff != "" { |
| t.Errorf("paginateList mismatch (-want +got):\n%s", diff) |
| } |
| gotItems = append(gotItems, gotResult.Items...) |
| nextCursor = gotResult.NextCursor |
| if nextCursor == "" { |
| break |
| } |
| index++ |
| } |
| |
| if len(gotItems) != len(allTestItems) { |
| t.Fatalf("paginateList() returned %d items, want %d", len(allTestItems), len(gotItems)) |
| } |
| } |
| } |