585 lines
20 KiB
Go
585 lines
20 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/github/github-mcp-server/internal/githubv4mock"
|
|
"github.com/github/github-mcp-server/internal/toolsnaps"
|
|
"github.com/github/github-mcp-server/pkg/translations"
|
|
"github.com/google/go-github/v89/github"
|
|
"github.com/google/jsonschema-go/jsonschema"
|
|
"github.com/shurcooL/githubv4"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// recorderTransport routes HTTP requests through an in-process handler, mirroring
|
|
// internal/githubv4mock's own transport. We need it because githubv4mock keys its
|
|
// matchers by query string, so it cannot model a multi-page labels query: every
|
|
// page issues the identical query and differs only by the $cursor variable. This
|
|
// transport lets a single handler answer each page dynamically.
|
|
type recorderTransport struct{ handler http.Handler }
|
|
|
|
func (rt recorderTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
rec := httptest.NewRecorder()
|
|
rt.handler.ServeHTTP(rec, req)
|
|
return rec.Result(), nil
|
|
}
|
|
|
|
// alwaysHasNextPageLabelsClient returns a GraphQL client whose labels query always
|
|
// reports another page, advancing the cursor on each call. It exercises uiGetLabels'
|
|
// page cap: the loop fetches one label per page until it stops at uiGetMaxPages with
|
|
// has_more=true. totalCount is reported as a large server-side count so the test can
|
|
// confirm it stays the full repo count even when results are truncated.
|
|
func alwaysHasNextPageLabelsClient(t *testing.T) *http.Client {
|
|
t.Helper()
|
|
var calls int
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/graphql", func(w http.ResponseWriter, _ *http.Request) {
|
|
calls++
|
|
resp := map[string]any{
|
|
"data": map[string]any{
|
|
"repository": map[string]any{
|
|
"labels": map[string]any{
|
|
"nodes": []any{
|
|
map[string]any{
|
|
"id": fmt.Sprintf("label-%d", calls),
|
|
"name": fmt.Sprintf("label-%d", calls),
|
|
"color": "ededed",
|
|
"description": "",
|
|
},
|
|
},
|
|
"totalCount": 9999,
|
|
"pageInfo": map[string]any{
|
|
"hasNextPage": true,
|
|
"endCursor": fmt.Sprintf("cursor-%d", calls),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(resp)
|
|
})
|
|
return &http.Client{Transport: recorderTransport{handler: mux}}
|
|
}
|
|
|
|
// alwaysNextPageHandler returns a REST handler that always advertises another page
|
|
// via the Link header, regardless of the page requested. It drives a pagination loop
|
|
// purely off the page cap so tests can assert ui_get stops at uiGetMaxPages and sets
|
|
// has_more=true. The same body is returned for every page, so the number of items
|
|
// collected equals the number of pages fetched.
|
|
func alwaysNextPageHandler(t *testing.T, body any) http.HandlerFunc {
|
|
t.Helper()
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
page := 1
|
|
if p := r.URL.Query().Get("page"); p != "" {
|
|
if parsed, err := strconv.Atoi(p); err == nil {
|
|
page = parsed
|
|
}
|
|
}
|
|
w.Header().Set("Link", fmt.Sprintf(`<https://api.github.com/next?page=%d>; rel="next"`, page+1))
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(body)
|
|
}
|
|
}
|
|
|
|
func Test_UIGet(t *testing.T) {
|
|
// Verify tool definition
|
|
serverTool := UIGet(translations.NullTranslationHelper)
|
|
tool := serverTool.Tool
|
|
require.NoError(t, toolsnaps.Test(tool.Name, tool))
|
|
|
|
assert.Equal(t, "ui_get", tool.Name)
|
|
assert.NotEmpty(t, tool.Description)
|
|
assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "method")
|
|
assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "owner")
|
|
assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "repo")
|
|
assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"method", "owner"})
|
|
assert.True(t, tool.Annotations.ReadOnlyHint, "ui_get should be read-only")
|
|
assert.Equal(t, MCPAppsFeatureFlag, serverTool.FeatureFlagEnable, "ui_get should be gated on the MCP Apps feature flag")
|
|
|
|
// ui_get must be app-only so the host hides it from the agent's tool list
|
|
// while keeping it callable by the views (MCP Apps 2026-01-26 spec).
|
|
ui, ok := tool.Meta["ui"].(map[string]any)
|
|
require.True(t, ok, "ui_get should declare _meta.ui")
|
|
assert.Equal(t, []string{"app"}, ui["visibility"], "ui_get should be app-only")
|
|
|
|
// Setup mock data
|
|
mockAssignees := []*github.User{
|
|
{Login: github.Ptr("user1"), AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/1")},
|
|
{Login: github.Ptr("user2"), AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/2")},
|
|
}
|
|
|
|
mockBranches := []*github.Branch{
|
|
{Name: github.Ptr("main"), Protected: github.Ptr(true)},
|
|
{Name: github.Ptr("feature"), Protected: github.Ptr(false)},
|
|
}
|
|
|
|
dueDate := time.Date(2026, 1, 31, 0, 0, 0, 0, time.UTC)
|
|
mockMilestones := []*github.Milestone{
|
|
{Number: github.Ptr(1), Title: github.Ptr("with due date"), DueOn: &github.Timestamp{Time: dueDate}},
|
|
{Number: github.Ptr(2), Title: github.Ptr("no due date")},
|
|
}
|
|
|
|
mockIssueTypes := []*github.IssueType{
|
|
{Name: github.Ptr("Bug")},
|
|
{Name: github.Ptr("Feature")},
|
|
}
|
|
|
|
mockReviewers := []*github.User{
|
|
{Login: github.Ptr("octocat"), AvatarURL: github.Ptr("https://avatars.githubusercontent.com/u/583231")},
|
|
{Login: github.Ptr("dependabot[bot]"), AvatarURL: github.Ptr("https://avatars.githubusercontent.com/in/29110")},
|
|
{Login: github.Ptr("github-actions"), Type: github.Ptr("Bot")},
|
|
}
|
|
|
|
mockReviewerTeams := []*github.Team{
|
|
{Slug: github.Ptr("docs"), Name: github.Ptr("Docs")},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
mockedClient *http.Client
|
|
mockedGQLClient *http.Client
|
|
requestArgs map[string]any
|
|
expectError bool
|
|
expectedErrMsg string
|
|
validateResult func(t *testing.T, responseText string)
|
|
}{
|
|
{
|
|
name: "successful assignees fetch",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
|
"GET /repos/owner/repo/assignees": mockResponse(t, http.StatusOK, mockAssignees),
|
|
}),
|
|
requestArgs: map[string]any{
|
|
"method": "assignees",
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: false,
|
|
validateResult: func(t *testing.T, responseText string) {
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(responseText), &response))
|
|
assert.Contains(t, response, "assignees")
|
|
assert.Contains(t, response, "totalCount")
|
|
assert.Equal(t, false, response["has_more"], "results within the page cap should not be truncated")
|
|
},
|
|
},
|
|
{
|
|
name: "successful branches fetch",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
|
"GET /repos/owner/repo/branches": mockResponse(t, http.StatusOK, mockBranches),
|
|
}),
|
|
requestArgs: map[string]any{
|
|
"method": "branches",
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: false,
|
|
validateResult: func(t *testing.T, responseText string) {
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(responseText), &response))
|
|
assert.Contains(t, response, "branches")
|
|
assert.Contains(t, response, "totalCount")
|
|
assert.Equal(t, false, response["has_more"], "results within the page cap should not be truncated")
|
|
},
|
|
},
|
|
{
|
|
name: "successful milestones fetch",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
|
"GET /repos/owner/repo/milestones": mockResponse(t, http.StatusOK, mockMilestones),
|
|
}),
|
|
requestArgs: map[string]any{
|
|
"method": "milestones",
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: false,
|
|
validateResult: func(t *testing.T, responseText string) {
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(responseText), &response))
|
|
milestones, ok := response["milestones"].([]any)
|
|
require.True(t, ok, "milestones should be a list")
|
|
require.Len(t, milestones, 2)
|
|
first := milestones[0].(map[string]any)
|
|
assert.Equal(t, "2026-01-31", first["due_on"], "milestone with a due date should be formatted")
|
|
second := milestones[1].(map[string]any)
|
|
assert.Equal(t, "", second["due_on"], "milestone without a due date should be empty, not zero time")
|
|
},
|
|
},
|
|
{
|
|
name: "successful issue_types fetch",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
|
"GET /orgs/owner/issue-types": mockResponse(t, http.StatusOK, mockIssueTypes),
|
|
}),
|
|
requestArgs: map[string]any{
|
|
"method": "issue_types",
|
|
"owner": "owner",
|
|
},
|
|
expectError: false,
|
|
validateResult: func(t *testing.T, responseText string) {
|
|
var issueTypes []map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(responseText), &issueTypes))
|
|
require.Len(t, issueTypes, 2)
|
|
assert.Equal(t, "Bug", issueTypes[0]["name"])
|
|
},
|
|
},
|
|
{
|
|
name: "issue_types API error returns response context",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
|
"GET /orgs/owner/issue-types": mockResponse(t, http.StatusForbidden, map[string]string{"message": "Forbidden"}),
|
|
}),
|
|
requestArgs: map[string]any{
|
|
"method": "issue_types",
|
|
"owner": "owner",
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "failed to list issue types",
|
|
},
|
|
{
|
|
name: "successful labels fetch",
|
|
mockedGQLClient: githubv4mock.NewMockedHTTPClient(
|
|
githubv4mock.NewQueryMatcher(
|
|
struct {
|
|
Repository struct {
|
|
Labels struct {
|
|
Nodes []struct {
|
|
ID githubv4.ID
|
|
Name githubv4.String
|
|
Color githubv4.String
|
|
Description githubv4.String
|
|
}
|
|
TotalCount githubv4.Int
|
|
PageInfo struct {
|
|
HasNextPage githubv4.Boolean
|
|
EndCursor githubv4.String
|
|
}
|
|
} `graphql:"labels(first: 100, after: $cursor)"`
|
|
} `graphql:"repository(owner: $owner, name: $repo)"`
|
|
}{},
|
|
map[string]any{
|
|
"owner": githubv4.String("owner"),
|
|
"repo": githubv4.String("repo"),
|
|
"cursor": (*githubv4.String)(nil),
|
|
},
|
|
githubv4mock.DataResponse(map[string]any{
|
|
"repository": map[string]any{
|
|
"labels": map[string]any{
|
|
"nodes": []any{
|
|
map[string]any{
|
|
"id": githubv4.ID("label-1"),
|
|
"name": githubv4.String("bug"),
|
|
"color": githubv4.String("d73a4a"),
|
|
"description": githubv4.String("Something isn't working"),
|
|
},
|
|
},
|
|
"totalCount": githubv4.Int(1),
|
|
"pageInfo": map[string]any{
|
|
"hasNextPage": githubv4.Boolean(false),
|
|
"endCursor": githubv4.String(""),
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
),
|
|
requestArgs: map[string]any{
|
|
"method": "labels",
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: false,
|
|
validateResult: func(t *testing.T, responseText string) {
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(responseText), &response))
|
|
labels, ok := response["labels"].([]any)
|
|
require.True(t, ok, "labels should be a list")
|
|
require.Len(t, labels, 1)
|
|
assert.Equal(t, "bug", labels[0].(map[string]any)["name"])
|
|
assert.Equal(t, float64(1), response["totalCount"])
|
|
assert.Equal(t, false, response["has_more"], "results within the page cap should not be truncated")
|
|
},
|
|
},
|
|
{
|
|
name: "successful issue_fields fetch",
|
|
mockedGQLClient: githubv4mock.NewMockedHTTPClient(
|
|
githubv4mock.NewQueryMatcher(
|
|
issueFieldsRepoQuery{},
|
|
map[string]any{
|
|
"owner": githubv4.String("owner"),
|
|
"name": githubv4.String("repo"),
|
|
},
|
|
githubv4mock.DataResponse(map[string]any{
|
|
"repository": map[string]any{
|
|
"issueFields": map[string]any{
|
|
"nodes": []any{
|
|
map[string]any{
|
|
"__typename": "IssueFieldText",
|
|
"id": "IFT_1",
|
|
"fullDatabaseId": "42",
|
|
"name": "DRI",
|
|
"description": "Directly responsible individual",
|
|
"dataType": "text",
|
|
"visibility": "ORG_ONLY",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
),
|
|
),
|
|
requestArgs: map[string]any{
|
|
"method": "issue_fields",
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: false,
|
|
validateResult: func(t *testing.T, responseText string) {
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(responseText), &response))
|
|
fields, ok := response["fields"].([]any)
|
|
require.True(t, ok, "fields should be a list")
|
|
require.Len(t, fields, 1)
|
|
assert.Equal(t, "DRI", fields[0].(map[string]any)["name"])
|
|
assert.Equal(t, "text", fields[0].(map[string]any)["data_type"])
|
|
assert.Equal(t, float64(1), response["totalCount"])
|
|
},
|
|
},
|
|
{
|
|
name: "successful reviewers fetch",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
|
"GET /repos/owner/repo/collaborators": mockResponse(t, http.StatusOK, mockReviewers),
|
|
"GET /repos/owner/repo/teams": mockResponse(t, http.StatusOK, mockReviewerTeams),
|
|
}),
|
|
requestArgs: map[string]any{
|
|
"method": "reviewers",
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: false,
|
|
validateResult: func(t *testing.T, responseText string) {
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(responseText), &response))
|
|
users, ok := response["users"].([]any)
|
|
require.True(t, ok, "users should be a list")
|
|
require.Len(t, users, 1)
|
|
assert.Equal(t, "octocat", users[0].(map[string]any)["login"])
|
|
teams, ok := response["teams"].([]any)
|
|
require.True(t, ok, "teams should be a list")
|
|
require.Len(t, teams, 1)
|
|
assert.Equal(t, "docs", teams[0].(map[string]any)["slug"])
|
|
assert.Equal(t, "owner", teams[0].(map[string]any)["org"])
|
|
assert.Equal(t, float64(2), response["totalCount"])
|
|
assert.Equal(t, false, response["has_more"], "results within the page cap should not be truncated")
|
|
},
|
|
},
|
|
{
|
|
name: "branches pagination stops at the page cap",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
|
"GET /repos/owner/repo/branches": alwaysNextPageHandler(t, []*github.Branch{{Name: github.Ptr("feature")}}),
|
|
}),
|
|
requestArgs: map[string]any{
|
|
"method": "branches",
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: false,
|
|
validateResult: func(t *testing.T, responseText string) {
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(responseText), &response))
|
|
branches, ok := response["branches"].([]any)
|
|
require.True(t, ok, "branches should be a list")
|
|
assert.Len(t, branches, uiGetMaxPages, "loop should stop at the page cap")
|
|
assert.Equal(t, float64(uiGetMaxPages), response["totalCount"], "totalCount should be the bounded count")
|
|
assert.Equal(t, true, response["has_more"], "truncated results should set has_more")
|
|
},
|
|
},
|
|
{
|
|
name: "labels pagination stops at the page cap",
|
|
mockedGQLClient: alwaysHasNextPageLabelsClient(t),
|
|
requestArgs: map[string]any{
|
|
"method": "labels",
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: false,
|
|
validateResult: func(t *testing.T, responseText string) {
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(responseText), &response))
|
|
labels, ok := response["labels"].([]any)
|
|
require.True(t, ok, "labels should be a list")
|
|
assert.Len(t, labels, uiGetMaxPages, "loop should stop at the page cap")
|
|
assert.Equal(t, true, response["has_more"], "truncated results should set has_more")
|
|
// totalCount stays the server-reported full count, so it can exceed
|
|
// the number of labels returned once results are truncated.
|
|
assert.Equal(t, float64(9999), response["totalCount"])
|
|
},
|
|
},
|
|
{
|
|
name: "reviewers pagination stops at the page cap",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
|
|
"GET /repos/owner/repo/collaborators": alwaysNextPageHandler(t, []*github.User{{Login: github.Ptr("octocat")}}),
|
|
"GET /repos/owner/repo/teams": mockResponse(t, http.StatusOK, mockReviewerTeams),
|
|
}),
|
|
requestArgs: map[string]any{
|
|
"method": "reviewers",
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: false,
|
|
validateResult: func(t *testing.T, responseText string) {
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(responseText), &response))
|
|
users, ok := response["users"].([]any)
|
|
require.True(t, ok, "users should be a list")
|
|
assert.Len(t, users, uiGetMaxPages, "collaborators loop should stop at the page cap")
|
|
assert.Equal(t, true, response["has_more"], "truncating either loop should set has_more")
|
|
},
|
|
},
|
|
{
|
|
name: "missing method parameter",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
|
|
requestArgs: map[string]any{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "missing required parameter: method",
|
|
},
|
|
{
|
|
name: "missing owner parameter",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
|
|
requestArgs: map[string]any{
|
|
"method": "assignees",
|
|
"repo": "repo",
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "missing required parameter: owner",
|
|
},
|
|
{
|
|
name: "missing repo parameter for assignees",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
|
|
requestArgs: map[string]any{
|
|
"method": "assignees",
|
|
"owner": "owner",
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "missing required parameter: repo",
|
|
},
|
|
{
|
|
name: "unknown method",
|
|
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}),
|
|
requestArgs: map[string]any{
|
|
"method": "unknown",
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "unknown method: unknown",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Setup deps with REST and/or GraphQL mocks
|
|
deps := BaseDeps{}
|
|
if tc.mockedClient != nil {
|
|
client, err := github.NewClient(github.WithHTTPClient(tc.mockedClient))
|
|
require.NoError(t, err)
|
|
deps.Client = client
|
|
}
|
|
if tc.mockedGQLClient != nil {
|
|
deps.GQLClient = githubv4.NewClient(tc.mockedGQLClient)
|
|
}
|
|
handler := serverTool.Handler(deps)
|
|
|
|
// Create call request
|
|
request := createMCPRequest(tc.requestArgs)
|
|
|
|
// Call handler
|
|
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
|
|
|
|
// Verify results
|
|
if tc.expectError {
|
|
if err != nil {
|
|
assert.Contains(t, err.Error(), tc.expectedErrMsg)
|
|
return
|
|
}
|
|
require.NotNil(t, result)
|
|
require.True(t, result.IsError)
|
|
errorContent := getErrorResult(t, result)
|
|
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result)
|
|
require.False(t, result.IsError)
|
|
textContent := getTextResult(t, result)
|
|
|
|
if tc.validateResult != nil {
|
|
tc.validateResult(t, textContent.Text)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_marshalUIGetIssueFields_TrimsForUI(t *testing.T) {
|
|
priorityLow := 1
|
|
priorityHigh := 2
|
|
result, _, err := marshalUIGetIssueFields([]IssueField{
|
|
{
|
|
ID: "field-1",
|
|
DatabaseID: 123,
|
|
Name: "Priority",
|
|
Description: "How urgent this is",
|
|
DataType: "single_select",
|
|
Visibility: "public",
|
|
Options: []IssueSingleSelectFieldOption{
|
|
{ID: "option-2", Name: "High", Description: "High priority", Color: "red", Priority: &priorityHigh},
|
|
{ID: "option-1", Name: "Low", Description: "Low priority", Color: "blue", Priority: &priorityLow},
|
|
{ID: "option-3", Name: "No priority", Description: "No priority set", Color: "gray"},
|
|
},
|
|
},
|
|
{
|
|
ID: "field-2",
|
|
Name: "Unsupported",
|
|
DataType: "iteration",
|
|
},
|
|
{
|
|
ID: "field-3",
|
|
Name: "Notes",
|
|
DataType: "text",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
var response map[string]any
|
|
require.NoError(t, json.Unmarshal([]byte(getTextResult(t, result).Text), &response))
|
|
fields := response["fields"].([]any)
|
|
require.Len(t, fields, 2)
|
|
assert.Equal(t, float64(2), response["totalCount"])
|
|
|
|
singleSelectField := fields[0].(map[string]any)
|
|
assert.NotContains(t, singleSelectField, "full_database_id")
|
|
assert.NotContains(t, singleSelectField, "visibility")
|
|
options := singleSelectField["options"].([]any)
|
|
require.Len(t, options, 3)
|
|
assert.Equal(t, "Low", options[0].(map[string]any)["name"])
|
|
assert.Equal(t, "High", options[1].(map[string]any)["name"])
|
|
assert.Equal(t, "No priority", options[2].(map[string]any)["name"])
|
|
assert.NotContains(t, options[0].(map[string]any), "id")
|
|
assert.NotContains(t, options[0].(map[string]any), "priority")
|
|
|
|
textField := fields[1].(map[string]any)
|
|
assert.NotContains(t, textField, "options")
|
|
}
|