github.com/google/go-github/v65@v65.0.0/github/repos_rules_test.go (about) 1 // Copyright 2023 The go-github AUTHORS. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 package github 7 8 import ( 9 "context" 10 "fmt" 11 "net/http" 12 "testing" 13 14 "github.com/google/go-cmp/cmp" 15 ) 16 17 func TestRepositoryRule_UnmarshalJSON(t *testing.T) { 18 tests := map[string]struct { 19 data string 20 want *RepositoryRule 21 wantErr bool 22 }{ 23 "Invalid JSON": { 24 data: `{`, 25 want: &RepositoryRule{ 26 Type: "", 27 Parameters: nil, 28 }, 29 wantErr: true, 30 }, 31 "With Metadata": { 32 data: `{ 33 "type": "creation", 34 "ruleset_source_type": "Repository", 35 "ruleset_source": "google", 36 "ruleset_id": 1984 37 }`, 38 want: &RepositoryRule{ 39 RulesetSource: "google", 40 RulesetSourceType: "Repository", 41 RulesetID: 1984, 42 Type: "creation", 43 }, 44 }, 45 "Valid creation": { 46 data: `{"type":"creation"}`, 47 want: NewCreationRule(), 48 }, 49 "Valid deletion": { 50 data: `{"type":"deletion"}`, 51 want: &RepositoryRule{ 52 Type: "deletion", 53 Parameters: nil, 54 }, 55 }, 56 "Valid required_linear_history": { 57 data: `{"type":"required_linear_history"}`, 58 want: &RepositoryRule{ 59 Type: "required_linear_history", 60 Parameters: nil, 61 }, 62 }, 63 "Valid required_signatures": { 64 data: `{"type":"required_signatures"}`, 65 want: &RepositoryRule{ 66 Type: "required_signatures", 67 Parameters: nil, 68 }, 69 }, 70 "Valid merge_queue": { 71 data: `{"type":"merge_queue"}`, 72 want: &RepositoryRule{ 73 Type: "merge_queue", 74 Parameters: nil, 75 }, 76 }, 77 "Valid merge_queue with params": { 78 data: `{ 79 "type":"merge_queue", 80 "parameters":{ 81 "check_response_timeout_minutes": 35, 82 "grouping_strategy": "HEADGREEN", 83 "max_entries_to_build": 8, 84 "max_entries_to_merge": 4, 85 "merge_method": "SQUASH", 86 "min_entries_to_merge": 2, 87 "min_entries_to_merge_wait_minutes": 13 88 } 89 }`, 90 want: NewMergeQueueRule(&MergeQueueRuleParameters{ 91 CheckResponseTimeoutMinutes: 35, 92 GroupingStrategy: "HEADGREEN", 93 MaxEntriesToBuild: 8, 94 MaxEntriesToMerge: 4, 95 MergeMethod: "SQUASH", 96 MinEntriesToMerge: 2, 97 MinEntriesToMergeWaitMinutes: 13, 98 }), 99 }, 100 "Invalid merge_queue with params": { 101 data: `{ 102 "type":"merge_queue", 103 "parameters":{ 104 "check_response_timeout_minutes": "35", 105 "grouping_strategy": "HEADGREEN", 106 "max_entries_to_build": "8", 107 "max_entries_to_merge": "4", 108 "merge_method": "SQUASH", 109 "min_entries_to_merge": "2", 110 "min_entries_to_merge_wait_minutes": "13" 111 } 112 }`, 113 want: &RepositoryRule{ 114 Type: "merge_queue", 115 Parameters: nil, 116 }, 117 wantErr: true, 118 }, 119 "Valid non_fast_forward": { 120 data: `{"type":"non_fast_forward"}`, 121 want: &RepositoryRule{ 122 Type: "non_fast_forward", 123 Parameters: nil, 124 }, 125 }, 126 "Valid update params": { 127 data: `{"type":"update","parameters":{"update_allows_fetch_and_merge":true}}`, 128 want: NewUpdateRule(&UpdateAllowsFetchAndMergeRuleParameters{UpdateAllowsFetchAndMerge: true}), 129 }, 130 "Invalid update params": { 131 data: `{"type":"update","parameters":{"update_allows_fetch_and_merge":"true"}}`, 132 want: &RepositoryRule{ 133 Type: "update", 134 Parameters: nil, 135 }, 136 wantErr: true, 137 }, 138 "Valid required_deployments params": { 139 data: `{"type":"required_deployments","parameters":{"required_deployment_environments":["test"]}}`, 140 want: NewRequiredDeploymentsRule(&RequiredDeploymentEnvironmentsRuleParameters{ 141 RequiredDeploymentEnvironments: []string{"test"}, 142 }), 143 }, 144 "Invalid required_deployments params": { 145 data: `{"type":"required_deployments","parameters":{"required_deployment_environments":true}}`, 146 want: &RepositoryRule{ 147 Type: "required_deployments", 148 Parameters: nil, 149 }, 150 wantErr: true, 151 }, 152 "Valid commit_message_pattern params": { 153 data: `{"type":"commit_message_pattern","parameters":{"operator":"starts_with","pattern":"github"}}`, 154 want: NewCommitMessagePatternRule(&RulePatternParameters{ 155 Operator: "starts_with", 156 Pattern: "github", 157 }), 158 }, 159 "Invalid commit_message_pattern params": { 160 data: `{"type":"commit_message_pattern","parameters":{"operator":"starts_with","pattern":1}}`, 161 want: &RepositoryRule{ 162 Type: "commit_message_pattern", 163 Parameters: nil, 164 }, 165 wantErr: true, 166 }, 167 "Valid commit_author_email_pattern params": { 168 data: `{"type":"commit_author_email_pattern","parameters":{"operator":"starts_with","pattern":"github"}}`, 169 want: NewCommitAuthorEmailPatternRule(&RulePatternParameters{ 170 Operator: "starts_with", 171 Pattern: "github", 172 }), 173 }, 174 "Invalid commit_author_email_pattern params": { 175 data: `{"type":"commit_author_email_pattern","parameters":{"operator":"starts_with","pattern":1}}`, 176 want: &RepositoryRule{ 177 Type: "commit_author_email_pattern", 178 Parameters: nil, 179 }, 180 wantErr: true, 181 }, 182 "Valid committer_email_pattern params": { 183 data: `{"type":"committer_email_pattern","parameters":{"operator":"starts_with","pattern":"github"}}`, 184 want: NewCommitterEmailPatternRule(&RulePatternParameters{ 185 Operator: "starts_with", 186 Pattern: "github", 187 }), 188 }, 189 "Invalid committer_email_pattern params": { 190 data: `{"type":"committer_email_pattern","parameters":{"operator":"starts_with","pattern":1}}`, 191 want: &RepositoryRule{ 192 Type: "committer_email_pattern", 193 Parameters: nil, 194 }, 195 wantErr: true, 196 }, 197 "Valid branch_name_pattern params": { 198 data: `{"type":"branch_name_pattern","parameters":{"operator":"starts_with","pattern":"github"}}`, 199 want: NewBranchNamePatternRule(&RulePatternParameters{ 200 Operator: "starts_with", 201 Pattern: "github", 202 }), 203 }, 204 "Invalid branch_name_pattern params": { 205 data: `{"type":"branch_name_pattern","parameters":{"operator":"starts_with","pattern":1}}`, 206 want: &RepositoryRule{ 207 Type: "branch_name_pattern", 208 Parameters: nil, 209 }, 210 wantErr: true, 211 }, 212 "Valid tag_name_pattern params": { 213 data: `{"type":"tag_name_pattern","parameters":{"operator":"starts_with","pattern":"github"}}`, 214 want: NewTagNamePatternRule(&RulePatternParameters{ 215 Operator: "starts_with", 216 Pattern: "github", 217 }), 218 }, 219 "Invalid tag_name_pattern params": { 220 data: `{"type":"tag_name_pattern","parameters":{"operator":"starts_with","pattern":1}}`, 221 want: &RepositoryRule{ 222 Type: "tag_name_pattern", 223 Parameters: nil, 224 }, 225 wantErr: true, 226 }, 227 "Valid file_path_restriction params": { 228 data: `{"type":"file_path_restriction","parameters":{"restricted_file_paths":["/a/file"]}}`, 229 want: NewFilePathRestrictionRule(&RuleFileParameters{ 230 RestrictedFilePaths: &[]string{"/a/file"}, 231 }), 232 }, 233 "Invalid file_path_restriction params": { 234 data: `{"type":"file_path_restriction","parameters":{"restricted_file_paths":true}}`, 235 want: &RepositoryRule{ 236 Type: "file_path_restriction", 237 Parameters: nil, 238 }, 239 wantErr: true, 240 }, 241 "Valid pull_request params": { 242 data: `{ 243 "type":"pull_request", 244 "parameters":{ 245 "dismiss_stale_reviews_on_push": true, 246 "require_code_owner_review": true, 247 "require_last_push_approval": true, 248 "required_approving_review_count": 1, 249 "required_review_thread_resolution":true 250 } 251 }`, 252 want: NewPullRequestRule(&PullRequestRuleParameters{ 253 DismissStaleReviewsOnPush: true, 254 RequireCodeOwnerReview: true, 255 RequireLastPushApproval: true, 256 RequiredApprovingReviewCount: 1, 257 RequiredReviewThreadResolution: true, 258 }), 259 }, 260 "Invalid pull_request params": { 261 data: `{"type":"pull_request","parameters": {"dismiss_stale_reviews_on_push":"true"}}`, 262 want: &RepositoryRule{ 263 Type: "pull_request", 264 Parameters: nil, 265 }, 266 wantErr: true, 267 }, 268 "Valid required_status_checks params": { 269 data: `{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"test","integration_id":1}],"strict_required_status_checks_policy":true,"do_not_enforce_on_create":true}}`, 270 want: NewRequiredStatusChecksRule(&RequiredStatusChecksRuleParameters{ 271 DoNotEnforceOnCreate: true, 272 RequiredStatusChecks: []RuleRequiredStatusChecks{ 273 { 274 Context: "test", 275 IntegrationID: Int64(1), 276 }, 277 }, 278 StrictRequiredStatusChecksPolicy: true, 279 }), 280 }, 281 "Invalid required_status_checks params": { 282 data: `{"type":"required_status_checks", 283 "parameters": { 284 "required_status_checks": [ 285 { 286 "context": 1 287 } 288 ] 289 }}`, 290 want: &RepositoryRule{ 291 Type: "required_status_checks", 292 Parameters: nil, 293 }, 294 wantErr: true, 295 }, 296 "Required workflows params": { 297 data: `{"type":"workflows","parameters":{"workflows":[{"path": ".github/workflows/test.yml", "repository_id": 1}]}}`, 298 want: NewRequiredWorkflowsRule(&RequiredWorkflowsRuleParameters{ 299 RequiredWorkflows: []*RuleRequiredWorkflow{ 300 { 301 Path: ".github/workflows/test.yml", 302 RepositoryID: Int64(1), 303 }, 304 }, 305 }), 306 }, 307 "Invalid type": { 308 data: `{"type":"unknown"}`, 309 want: &RepositoryRule{ 310 Type: "", 311 Parameters: nil, 312 }, 313 wantErr: true, 314 }, 315 } 316 317 for name, tc := range tests { 318 rule := &RepositoryRule{} 319 320 t.Run(name, func(t *testing.T) { 321 err := rule.UnmarshalJSON([]byte(tc.data)) 322 if err == nil && tc.wantErr { 323 t.Errorf("RepositoryRule.UnmarshalJSON returned nil instead of an error") 324 } 325 if err != nil && !tc.wantErr { 326 t.Errorf("RepositoryRule.UnmarshalJSON returned an unexpected error: %+v", err) 327 } 328 if !cmp.Equal(tc.want, rule) { 329 t.Errorf("RepositoryRule.UnmarshalJSON expected rule %+v, got %+v", tc.want, rule) 330 } 331 }) 332 } 333 } 334 335 func TestRepositoriesService_GetRulesForBranch(t *testing.T) { 336 client, mux, _, teardown := setup() 337 defer teardown() 338 339 mux.HandleFunc("/repos/o/repo/rules/branches/branch", func(w http.ResponseWriter, r *http.Request) { 340 testMethod(t, r, "GET") 341 fmt.Fprint(w, `[ 342 { 343 "ruleset_id": 42069, 344 "ruleset_source_type": "Repository", 345 "ruleset_source": "google", 346 "type": "creation" 347 }, 348 { 349 "ruleset_id": 42069, 350 "ruleset_source_type": "Organization", 351 "ruleset_source": "google", 352 "type": "update", 353 "parameters": { 354 "update_allows_fetch_and_merge": true 355 } 356 } 357 ]`) 358 }) 359 360 ctx := context.Background() 361 rules, _, err := client.Repositories.GetRulesForBranch(ctx, "o", "repo", "branch") 362 if err != nil { 363 t.Errorf("Repositories.GetRulesForBranch returned error: %v", err) 364 } 365 366 creationRule := NewCreationRule() 367 creationRule.RulesetID = 42069 368 creationRule.RulesetSource = "google" 369 creationRule.RulesetSourceType = "Repository" 370 updateRule := NewUpdateRule(&UpdateAllowsFetchAndMergeRuleParameters{ 371 UpdateAllowsFetchAndMerge: true, 372 }) 373 updateRule.RulesetID = 42069 374 updateRule.RulesetSource = "google" 375 updateRule.RulesetSourceType = "Organization" 376 377 want := []*RepositoryRule{ 378 creationRule, 379 updateRule, 380 } 381 if !cmp.Equal(rules, want) { 382 t.Errorf("Repositories.GetRulesForBranch returned %+v, want %+v", rules, want) 383 } 384 385 const methodName = "GetRulesForBranch" 386 387 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 388 got, resp, err := client.Repositories.GetRulesForBranch(ctx, "o", "repo", "branch") 389 if got != nil { 390 t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) 391 } 392 return resp, err 393 }) 394 } 395 396 func TestRepositoriesService_GetRulesForBranchEmptyUpdateRule(t *testing.T) { 397 client, mux, _, teardown := setup() 398 defer teardown() 399 400 mux.HandleFunc("/repos/o/repo/rules/branches/branch", func(w http.ResponseWriter, r *http.Request) { 401 testMethod(t, r, "GET") 402 fmt.Fprint(w, `[ 403 { 404 "type": "update" 405 } 406 ]`) 407 }) 408 409 ctx := context.Background() 410 rules, _, err := client.Repositories.GetRulesForBranch(ctx, "o", "repo", "branch") 411 if err != nil { 412 t.Errorf("Repositories.GetRulesForBranch returned error: %v", err) 413 } 414 415 updateRule := NewUpdateRule(nil) 416 417 want := []*RepositoryRule{ 418 updateRule, 419 } 420 if !cmp.Equal(rules, want) { 421 t.Errorf("Repositories.GetRulesForBranch returned %+v, want %+v", Stringify(rules), Stringify(want)) 422 } 423 424 const methodName = "GetRulesForBranch" 425 426 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 427 got, resp, err := client.Repositories.GetRulesForBranch(ctx, "o", "repo", "branch") 428 if got != nil { 429 t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) 430 } 431 return resp, err 432 }) 433 } 434 435 func TestRepositoriesService_GetAllRulesets(t *testing.T) { 436 client, mux, _, teardown := setup() 437 defer teardown() 438 439 mux.HandleFunc("/repos/o/repo/rulesets", func(w http.ResponseWriter, r *http.Request) { 440 testMethod(t, r, "GET") 441 fmt.Fprint(w, `[ 442 { 443 "id": 42, 444 "name": "ruleset", 445 "source_type": "Repository", 446 "source": "o/repo", 447 "enforcement": "enabled" 448 }, 449 { 450 "id": 314, 451 "name": "Another ruleset", 452 "source_type": "Repository", 453 "source": "o/repo", 454 "enforcement": "enabled" 455 } 456 ]`) 457 }) 458 459 ctx := context.Background() 460 ruleSet, _, err := client.Repositories.GetAllRulesets(ctx, "o", "repo", false) 461 if err != nil { 462 t.Errorf("Repositories.GetAllRulesets returned error: %v", err) 463 } 464 465 want := []*Ruleset{ 466 { 467 ID: Int64(42), 468 Name: "ruleset", 469 SourceType: String("Repository"), 470 Source: "o/repo", 471 Enforcement: "enabled", 472 }, 473 { 474 ID: Int64(314), 475 Name: "Another ruleset", 476 SourceType: String("Repository"), 477 Source: "o/repo", 478 Enforcement: "enabled", 479 }, 480 } 481 if !cmp.Equal(ruleSet, want) { 482 t.Errorf("Repositories.GetAllRulesets returned %+v, want %+v", ruleSet, want) 483 } 484 485 const methodName = "GetAllRulesets" 486 487 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 488 got, resp, err := client.Repositories.GetAllRulesets(ctx, "o", "repo", false) 489 if got != nil { 490 t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) 491 } 492 return resp, err 493 }) 494 } 495 496 func TestRepositoriesService_CreateRuleset(t *testing.T) { 497 client, mux, _, teardown := setup() 498 defer teardown() 499 500 mux.HandleFunc("/repos/o/repo/rulesets", func(w http.ResponseWriter, r *http.Request) { 501 testMethod(t, r, "POST") 502 fmt.Fprint(w, `{ 503 "id": 42, 504 "name": "ruleset", 505 "source_type": "Repository", 506 "source": "o/repo", 507 "enforcement": "enabled" 508 }`) 509 }) 510 511 ctx := context.Background() 512 ruleSet, _, err := client.Repositories.CreateRuleset(ctx, "o", "repo", &Ruleset{ 513 Name: "ruleset", 514 Enforcement: "enabled", 515 }) 516 if err != nil { 517 t.Errorf("Repositories.CreateRuleset returned error: %v", err) 518 } 519 520 want := &Ruleset{ 521 ID: Int64(42), 522 Name: "ruleset", 523 SourceType: String("Repository"), 524 Source: "o/repo", 525 Enforcement: "enabled", 526 } 527 if !cmp.Equal(ruleSet, want) { 528 t.Errorf("Repositories.CreateRuleset returned %+v, want %+v", ruleSet, want) 529 } 530 531 const methodName = "CreateRuleset" 532 533 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 534 got, resp, err := client.Repositories.CreateRuleset(ctx, "o", "repo", &Ruleset{}) 535 if got != nil { 536 t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) 537 } 538 return resp, err 539 }) 540 } 541 542 func TestRepositoriesService_GetRuleset(t *testing.T) { 543 client, mux, _, teardown := setup() 544 defer teardown() 545 546 mux.HandleFunc("/repos/o/repo/rulesets/42", func(w http.ResponseWriter, r *http.Request) { 547 testMethod(t, r, "GET") 548 fmt.Fprint(w, `{ 549 "id": 42, 550 "name": "ruleset", 551 "source_type": "Organization", 552 "source": "o", 553 "enforcement": "enabled" 554 }`) 555 }) 556 557 ctx := context.Background() 558 ruleSet, _, err := client.Repositories.GetRuleset(ctx, "o", "repo", 42, true) 559 if err != nil { 560 t.Errorf("Repositories.GetRuleset returned error: %v", err) 561 } 562 563 want := &Ruleset{ 564 ID: Int64(42), 565 Name: "ruleset", 566 SourceType: String("Organization"), 567 Source: "o", 568 Enforcement: "enabled", 569 } 570 if !cmp.Equal(ruleSet, want) { 571 t.Errorf("Repositories.GetRuleset returned %+v, want %+v", ruleSet, want) 572 } 573 574 const methodName = "GetRuleset" 575 576 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 577 got, resp, err := client.Repositories.GetRuleset(ctx, "o", "repo", 42, true) 578 if got != nil { 579 t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) 580 } 581 return resp, err 582 }) 583 } 584 585 func TestRepositoriesService_UpdateRulesetNoBypassActor(t *testing.T) { 586 client, mux, _, teardown := setup() 587 defer teardown() 588 589 rs := &Ruleset{ 590 Name: "ruleset", 591 Source: "o/repo", 592 Enforcement: "enabled", 593 } 594 595 mux.HandleFunc("/repos/o/repo/rulesets/42", func(w http.ResponseWriter, r *http.Request) { 596 testMethod(t, r, "PUT") 597 fmt.Fprint(w, `{ 598 "id": 42, 599 "name": "ruleset", 600 "source_type": "Repository", 601 "source": "o/repo", 602 "enforcement": "enabled" 603 }`) 604 }) 605 606 ctx := context.Background() 607 608 ruleSet, _, err := client.Repositories.UpdateRulesetNoBypassActor(ctx, "o", "repo", 42, rs) 609 610 if err != nil { 611 t.Errorf("Repositories.UpdateRulesetNoBypassActor returned error: %v \n", err) 612 } 613 614 want := &Ruleset{ 615 ID: Int64(42), 616 Name: "ruleset", 617 SourceType: String("Repository"), 618 Source: "o/repo", 619 Enforcement: "enabled", 620 } 621 622 if !cmp.Equal(ruleSet, want) { 623 t.Errorf("Repositories.UpdateRulesetNoBypassActor returned %+v, want %+v", ruleSet, want) 624 } 625 626 const methodName = "UpdateRulesetNoBypassActor" 627 628 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 629 got, resp, err := client.Repositories.UpdateRulesetNoBypassActor(ctx, "o", "repo", 42, nil) 630 if got != nil { 631 t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) 632 } 633 return resp, err 634 }) 635 } 636 637 func TestRepositoriesService_UpdateRuleset(t *testing.T) { 638 client, mux, _, teardown := setup() 639 defer teardown() 640 641 mux.HandleFunc("/repos/o/repo/rulesets/42", func(w http.ResponseWriter, r *http.Request) { 642 testMethod(t, r, "PUT") 643 fmt.Fprint(w, `{ 644 "id": 42, 645 "name": "ruleset", 646 "source_type": "Repository", 647 "source": "o/repo", 648 "enforcement": "enabled" 649 }`) 650 }) 651 652 ctx := context.Background() 653 ruleSet, _, err := client.Repositories.UpdateRuleset(ctx, "o", "repo", 42, &Ruleset{ 654 Name: "ruleset", 655 Enforcement: "enabled", 656 }) 657 if err != nil { 658 t.Errorf("Repositories.UpdateRuleset returned error: %v", err) 659 } 660 661 want := &Ruleset{ 662 ID: Int64(42), 663 Name: "ruleset", 664 SourceType: String("Repository"), 665 Source: "o/repo", 666 Enforcement: "enabled", 667 } 668 669 if !cmp.Equal(ruleSet, want) { 670 t.Errorf("Repositories.UpdateRuleset returned %+v, want %+v", ruleSet, want) 671 } 672 673 const methodName = "UpdateRuleset" 674 675 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 676 got, resp, err := client.Repositories.UpdateRuleset(ctx, "o", "repo", 42, nil) 677 if got != nil { 678 t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) 679 } 680 return resp, err 681 }) 682 } 683 684 func TestRepositoriesService_DeleteRuleset(t *testing.T) { 685 client, mux, _, teardown := setup() 686 defer teardown() 687 688 mux.HandleFunc("/repos/o/repo/rulesets/42", func(w http.ResponseWriter, r *http.Request) { 689 testMethod(t, r, "DELETE") 690 }) 691 692 ctx := context.Background() 693 _, err := client.Repositories.DeleteRuleset(ctx, "o", "repo", 42) 694 if err != nil { 695 t.Errorf("Repositories.DeleteRuleset returned error: %v", err) 696 } 697 698 const methodName = "DeleteRuleset" 699 700 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 701 return client.Repositories.DeleteRuleset(ctx, "o", "repo", 42) 702 }) 703 }