@@ -1162,6 +1162,281 @@ func Test_AddProjectItem(t *testing.T) {
11621162}
11631163}
11641164
1165+ func Test_UpdateProjectItem (t * testing.T ) {
1166+ mockClient := gh .NewClient (nil )
1167+ tool ,_ := UpdateProjectItem (stubGetClientFn (mockClient ),translations .NullTranslationHelper )
1168+ require .NoError (t ,toolsnaps .Test (tool .Name ,tool ))
1169+
1170+ assert .Equal (t ,"update_project_item" ,tool .Name )
1171+ assert .NotEmpty (t ,tool .Description )
1172+ assert .Contains (t ,tool .InputSchema .Properties ,"owner_type" )
1173+ assert .Contains (t ,tool .InputSchema .Properties ,"owner" )
1174+ assert .Contains (t ,tool .InputSchema .Properties ,"project_number" )
1175+ assert .Contains (t ,tool .InputSchema .Properties ,"item_id" )
1176+ assert .Contains (t ,tool .InputSchema .Properties ,"new_field" )
1177+ assert .ElementsMatch (t ,tool .InputSchema .Required , []string {"owner_type" ,"owner" ,"project_number" ,"item_id" ,"new_field" })
1178+
1179+ orgUpdatedItem := map [string ]any {
1180+ "id" :801 ,
1181+ "content_type" :"Issue" ,
1182+ }
1183+ userUpdatedItem := map [string ]any {
1184+ "id" :802 ,
1185+ "content_type" :"PullRequest" ,
1186+ }
1187+
1188+ tests := []struct {
1189+ name string
1190+ mockedClient * http.Client
1191+ requestArgs map [string ]any
1192+ expectError bool
1193+ expectedErrMsg string
1194+ expectedID int
1195+ }{
1196+ {
1197+ name :"success organization update" ,
1198+ mockedClient :mock .NewMockedHTTPClient (
1199+ mock .WithRequestMatchHandler (
1200+ mock.EndpointPattern {Pattern :"/orgs/{org}/projectsV2/{project}/items/{item_id}" ,Method :http .MethodPatch },
1201+ http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) {
1202+ body ,err := io .ReadAll (r .Body )
1203+ assert .NoError (t ,err )
1204+ var payload struct {
1205+ Fields []struct {
1206+ ID int `json:"id"`
1207+ Value interface {}`json:"value"`
1208+ }`json:"fields"`
1209+ }
1210+ assert .NoError (t ,json .Unmarshal (body ,& payload ))
1211+ require .Len (t ,payload .Fields ,1 )
1212+ assert .Equal (t ,101 ,payload .Fields [0 ].ID )
1213+ assert .Equal (t ,"Done" ,payload .Fields [0 ].Value )
1214+ w .WriteHeader (http .StatusOK )
1215+ _ ,_ = w .Write (mock .MustMarshal (orgUpdatedItem ))
1216+ }),
1217+ ),
1218+ ),
1219+ requestArgs :map [string ]any {
1220+ "owner" :"octo-org" ,
1221+ "owner_type" :"org" ,
1222+ "project_number" :float64 (1001 ),
1223+ "item_id" :float64 (5555 ),
1224+ "new_field" :map [string ]any {
1225+ "id" :float64 (101 ),
1226+ "value" :"Done" ,
1227+ },
1228+ },
1229+ expectedID :801 ,
1230+ },
1231+ {
1232+ name :"success user update" ,
1233+ mockedClient :mock .NewMockedHTTPClient (
1234+ mock .WithRequestMatchHandler (
1235+ mock.EndpointPattern {Pattern :"/users/{user}/projectsV2/{project}/items/{item_id}" ,Method :http .MethodPatch },
1236+ http .HandlerFunc (func (w http.ResponseWriter ,r * http.Request ) {
1237+ body ,err := io .ReadAll (r .Body )
1238+ assert .NoError (t ,err )
1239+ var payload struct {
1240+ Fields []struct {
1241+ ID int `json:"id"`
1242+ Value interface {}`json:"value"`
1243+ }`json:"fields"`
1244+ }
1245+ assert .NoError (t ,json .Unmarshal (body ,& payload ))
1246+ require .Len (t ,payload .Fields ,1 )
1247+ assert .Equal (t ,202 ,payload .Fields [0 ].ID )
1248+ assert .Equal (t ,42.0 ,payload .Fields [0 ].Value )// number value example
1249+ w .WriteHeader (http .StatusCreated )
1250+ _ ,_ = w .Write (mock .MustMarshal (userUpdatedItem ))
1251+ }),
1252+ ),
1253+ ),
1254+ requestArgs :map [string ]any {
1255+ "owner" :"octocat" ,
1256+ "owner_type" :"user" ,
1257+ "project_number" :float64 (2002 ),
1258+ "item_id" :float64 (6666 ),
1259+ "new_field" :map [string ]any {
1260+ "id" :float64 (202 ),
1261+ "value" :float64 (42 ),
1262+ },
1263+ },
1264+ expectedID :802 ,
1265+ },
1266+ {
1267+ name :"api error" ,
1268+ mockedClient :mock .NewMockedHTTPClient (
1269+ mock .WithRequestMatchHandler (
1270+ mock.EndpointPattern {Pattern :"/orgs/{org}/projectsV2/{project}/items/{item_id}" ,Method :http .MethodPatch },
1271+ mockResponse (t ,http .StatusInternalServerError ,map [string ]string {"message" :"boom" }),
1272+ ),
1273+ ),
1274+ requestArgs :map [string ]any {
1275+ "owner" :"octo-org" ,
1276+ "owner_type" :"org" ,
1277+ "project_number" :float64 (3003 ),
1278+ "item_id" :float64 (7777 ),
1279+ "new_field" :map [string ]any {
1280+ "id" :float64 (303 ),
1281+ "value" :"In Progress" ,
1282+ },
1283+ },
1284+ expectError :true ,
1285+ expectedErrMsg :"failed to add a project item" ,// implementation uses this message for update failures
1286+ },
1287+ {
1288+ name :"missing owner" ,
1289+ mockedClient :mock .NewMockedHTTPClient (),
1290+ requestArgs :map [string ]any {
1291+ "owner_type" :"org" ,
1292+ "project_number" :float64 (1 ),
1293+ "item_id" :float64 (2 ),
1294+ "new_field" :map [string ]any {
1295+ "id" :float64 (1 ),
1296+ "value" :"X" ,
1297+ },
1298+ },
1299+ expectError :true ,
1300+ },
1301+ {
1302+ name :"missing owner_type" ,
1303+ mockedClient :mock .NewMockedHTTPClient (),
1304+ requestArgs :map [string ]any {
1305+ "owner" :"octo-org" ,
1306+ "project_number" :float64 (1 ),
1307+ "item_id" :float64 (2 ),
1308+ "new_field" :map [string ]any {
1309+ "id" :float64 (1 ),
1310+ "value" :"X" ,
1311+ },
1312+ },
1313+ expectError :true ,
1314+ },
1315+ {
1316+ name :"missing project_number" ,
1317+ mockedClient :mock .NewMockedHTTPClient (),
1318+ requestArgs :map [string ]any {
1319+ "owner" :"octo-org" ,
1320+ "owner_type" :"org" ,
1321+ "item_id" :float64 (2 ),
1322+ "new_field" :map [string ]any {
1323+ "id" :float64 (1 ),
1324+ "value" :"X" ,
1325+ },
1326+ },
1327+ expectError :true ,
1328+ },
1329+ {
1330+ name :"missing item_id" ,
1331+ mockedClient :mock .NewMockedHTTPClient (),
1332+ requestArgs :map [string ]any {
1333+ "owner" :"octo-org" ,
1334+ "owner_type" :"org" ,
1335+ "project_number" :float64 (1 ),
1336+ "new_field" :map [string ]any {
1337+ "id" :float64 (1 ),
1338+ "value" :"X" ,
1339+ },
1340+ },
1341+ expectError :true ,
1342+ },
1343+ {
1344+ name :"missing new_field" ,
1345+ mockedClient :mock .NewMockedHTTPClient (),
1346+ requestArgs :map [string ]any {
1347+ "owner" :"octo-org" ,
1348+ "owner_type" :"org" ,
1349+ "project_number" :float64 (1 ),
1350+ "item_id" :float64 (2 ),
1351+ },
1352+ expectError :true ,
1353+ },
1354+ {
1355+ name :"new_field not object" ,
1356+ mockedClient :mock .NewMockedHTTPClient (),
1357+ requestArgs :map [string ]any {
1358+ "owner" :"octo-org" ,
1359+ "owner_type" :"org" ,
1360+ "project_number" :float64 (1 ),
1361+ "item_id" :float64 (2 ),
1362+ "new_field" :"not-an-object" ,
1363+ },
1364+ expectError :true ,
1365+ },
1366+ {
1367+ name :"new_field missing id" ,
1368+ mockedClient :mock .NewMockedHTTPClient (),
1369+ requestArgs :map [string ]any {
1370+ "owner" :"octo-org" ,
1371+ "owner_type" :"org" ,
1372+ "project_number" :float64 (1 ),
1373+ "item_id" :float64 (2 ),
1374+ "new_field" :map [string ]any {},
1375+ },
1376+ expectError :true ,
1377+ },
1378+ {
1379+ name :"new_field missing value" ,
1380+ mockedClient :mock .NewMockedHTTPClient (),
1381+ requestArgs :map [string ]any {
1382+ "owner" :"octo-org" ,
1383+ "owner_type" :"org" ,
1384+ "project_number" :float64 (1 ),
1385+ "item_id" :float64 (2 ),
1386+ "new_field" :map [string ]any {
1387+ "id" :float64 (9 ),
1388+ },
1389+ },
1390+ expectError :true ,
1391+ },
1392+ }
1393+
1394+ for _ ,tc := range tests {
1395+ t .Run (tc .name ,func (t * testing.T ) {
1396+ client := gh .NewClient (tc .mockedClient )
1397+ _ ,handler := UpdateProjectItem (stubGetClientFn (client ),translations .NullTranslationHelper )
1398+ request := createMCPRequest (tc .requestArgs )
1399+ result ,err := handler (context .Background (),request )
1400+
1401+ require .NoError (t ,err )
1402+ if tc .expectError {
1403+ require .True (t ,result .IsError )
1404+ text := getTextResult (t ,result ).Text
1405+ if tc .expectedErrMsg != "" {
1406+ assert .Contains (t ,text ,tc .expectedErrMsg )
1407+ }
1408+ switch tc .name {
1409+ case "missing owner" :
1410+ assert .Contains (t ,text ,"missing required parameter: owner" )
1411+ case "missing owner_type" :
1412+ assert .Contains (t ,text ,"missing required parameter: owner_type" )
1413+ case "missing project_number" :
1414+ assert .Contains (t ,text ,"missing required parameter: project_number" )
1415+ case "missing item_id" :
1416+ assert .Contains (t ,text ,"missing required parameter: item_id" )
1417+ case "missing new_field" :
1418+ assert .Contains (t ,text ,"missing required parameter: new_field" )
1419+ case "new_field not object" :
1420+ assert .Contains (t ,text ,"new_field must be an object" )
1421+ case "new_field missing id" :
1422+ assert .Contains (t ,text ,"new_field.id is required" )
1423+ case "new_field missing value" :
1424+ assert .Contains (t ,text ,"new_field.value is required" )
1425+ }
1426+ return
1427+ }
1428+
1429+ require .False (t ,result .IsError )
1430+ textContent := getTextResult (t ,result )
1431+ var item map [string ]any
1432+ require .NoError (t ,json .Unmarshal ([]byte (textContent .Text ),& item ))
1433+ if tc .expectedID != 0 {
1434+ assert .Equal (t ,float64 (tc .expectedID ),item ["id" ])
1435+ }
1436+ })
1437+ }
1438+ }
1439+
11651440func Test_DeleteProjectItem (t * testing.T ) {
11661441mockClient := gh .NewClient (nil )
11671442tool ,_ := DeleteProjectItem (stubGetClientFn (mockClient ),translations .NullTranslationHelper )