github.com/google/go-github/v71@v71.0.0/github/actions_artifacts_test.go (about) 1 // Copyright 2020 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 "errors" 11 "fmt" 12 "net/http" 13 "net/url" 14 "strings" 15 "testing" 16 17 "github.com/google/go-cmp/cmp" 18 ) 19 20 func TestActionsService_ListArtifacts(t *testing.T) { 21 t.Parallel() 22 client, mux, _ := setup(t) 23 24 mux.HandleFunc("/repos/o/r/actions/artifacts", func(w http.ResponseWriter, r *http.Request) { 25 testMethod(t, r, "GET") 26 testFormValues(t, r, values{"page": "2", "name": "TheArtifact"}) 27 fmt.Fprint(w, 28 `{ 29 "total_count":1, 30 "artifacts":[{"id":1}] 31 }`, 32 ) 33 }) 34 35 opts := &ListArtifactsOptions{ 36 Name: Ptr("TheArtifact"), 37 ListOptions: ListOptions{Page: 2}, 38 } 39 ctx := context.Background() 40 artifacts, _, err := client.Actions.ListArtifacts(ctx, "o", "r", opts) 41 if err != nil { 42 t.Errorf("Actions.ListArtifacts returned error: %v", err) 43 } 44 45 want := &ArtifactList{TotalCount: Ptr(int64(1)), Artifacts: []*Artifact{{ID: Ptr(int64(1))}}} 46 if !cmp.Equal(artifacts, want) { 47 t.Errorf("Actions.ListArtifacts returned %+v, want %+v", artifacts, want) 48 } 49 50 const methodName = "ListArtifacts" 51 testBadOptions(t, methodName, func() (err error) { 52 _, _, err = client.Actions.ListArtifacts(ctx, "\n", "\n", opts) 53 return err 54 }) 55 56 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 57 got, resp, err := client.Actions.ListArtifacts(ctx, "o", "r", opts) 58 if got != nil { 59 t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) 60 } 61 return resp, err 62 }) 63 } 64 65 func TestActionsService_ListArtifacts_invalidOwner(t *testing.T) { 66 t.Parallel() 67 client, _, _ := setup(t) 68 69 ctx := context.Background() 70 _, _, err := client.Actions.ListArtifacts(ctx, "%", "r", nil) 71 testURLParseError(t, err) 72 } 73 74 func TestActionsService_ListArtifacts_invalidRepo(t *testing.T) { 75 t.Parallel() 76 client, _, _ := setup(t) 77 78 ctx := context.Background() 79 _, _, err := client.Actions.ListArtifacts(ctx, "o", "%", nil) 80 testURLParseError(t, err) 81 } 82 83 func TestActionsService_ListArtifacts_notFound(t *testing.T) { 84 t.Parallel() 85 client, mux, _ := setup(t) 86 87 mux.HandleFunc("/repos/o/r/actions/artifacts", func(w http.ResponseWriter, r *http.Request) { 88 testMethod(t, r, "GET") 89 w.WriteHeader(http.StatusNotFound) 90 }) 91 92 ctx := context.Background() 93 artifacts, resp, err := client.Actions.ListArtifacts(ctx, "o", "r", nil) 94 if err == nil { 95 t.Errorf("Expected HTTP 404 response") 96 } 97 if got, want := resp.Response.StatusCode, http.StatusNotFound; got != want { 98 t.Errorf("Actions.ListArtifacts return status %d, want %d", got, want) 99 } 100 if artifacts != nil { 101 t.Errorf("Actions.ListArtifacts return %+v, want nil", artifacts) 102 } 103 } 104 105 func TestActionsService_ListWorkflowRunArtifacts(t *testing.T) { 106 t.Parallel() 107 client, mux, _ := setup(t) 108 109 mux.HandleFunc("/repos/o/r/actions/runs/1/artifacts", func(w http.ResponseWriter, r *http.Request) { 110 testMethod(t, r, "GET") 111 testFormValues(t, r, values{"page": "2"}) 112 fmt.Fprint(w, 113 `{ 114 "total_count":1, 115 "artifacts":[{"id":1}] 116 }`, 117 ) 118 }) 119 120 opts := &ListOptions{Page: 2} 121 ctx := context.Background() 122 artifacts, _, err := client.Actions.ListWorkflowRunArtifacts(ctx, "o", "r", 1, opts) 123 if err != nil { 124 t.Errorf("Actions.ListWorkflowRunArtifacts returned error: %v", err) 125 } 126 127 want := &ArtifactList{TotalCount: Ptr(int64(1)), Artifacts: []*Artifact{{ID: Ptr(int64(1))}}} 128 if !cmp.Equal(artifacts, want) { 129 t.Errorf("Actions.ListWorkflowRunArtifacts returned %+v, want %+v", artifacts, want) 130 } 131 132 const methodName = "ListWorkflowRunArtifacts" 133 testBadOptions(t, methodName, func() (err error) { 134 _, _, err = client.Actions.ListWorkflowRunArtifacts(ctx, "\n", "\n", -1, opts) 135 return err 136 }) 137 138 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 139 got, resp, err := client.Actions.ListWorkflowRunArtifacts(ctx, "o", "r", 1, opts) 140 if got != nil { 141 t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) 142 } 143 return resp, err 144 }) 145 } 146 147 func TestActionsService_ListWorkflowRunArtifacts_invalidOwner(t *testing.T) { 148 t.Parallel() 149 client, _, _ := setup(t) 150 151 ctx := context.Background() 152 _, _, err := client.Actions.ListWorkflowRunArtifacts(ctx, "%", "r", 1, nil) 153 testURLParseError(t, err) 154 } 155 156 func TestActionsService_ListWorkflowRunArtifacts_invalidRepo(t *testing.T) { 157 t.Parallel() 158 client, _, _ := setup(t) 159 160 ctx := context.Background() 161 _, _, err := client.Actions.ListWorkflowRunArtifacts(ctx, "o", "%", 1, nil) 162 testURLParseError(t, err) 163 } 164 165 func TestActionsService_ListWorkflowRunArtifacts_notFound(t *testing.T) { 166 t.Parallel() 167 client, mux, _ := setup(t) 168 169 mux.HandleFunc("/repos/o/r/actions/runs/1/artifacts", func(w http.ResponseWriter, r *http.Request) { 170 testMethod(t, r, "GET") 171 w.WriteHeader(http.StatusNotFound) 172 }) 173 174 ctx := context.Background() 175 artifacts, resp, err := client.Actions.ListWorkflowRunArtifacts(ctx, "o", "r", 1, nil) 176 if err == nil { 177 t.Errorf("Expected HTTP 404 response") 178 } 179 if got, want := resp.Response.StatusCode, http.StatusNotFound; got != want { 180 t.Errorf("Actions.ListWorkflowRunArtifacts return status %d, want %d", got, want) 181 } 182 if artifacts != nil { 183 t.Errorf("Actions.ListWorkflowRunArtifacts return %+v, want nil", artifacts) 184 } 185 } 186 187 func TestActionsService_GetArtifact(t *testing.T) { 188 t.Parallel() 189 client, mux, _ := setup(t) 190 191 mux.HandleFunc("/repos/o/r/actions/artifacts/1", func(w http.ResponseWriter, r *http.Request) { 192 testMethod(t, r, "GET") 193 fmt.Fprint(w, `{ 194 "id":1, 195 "node_id":"xyz", 196 "name":"a", 197 "size_in_bytes":5, 198 "archive_download_url":"u" 199 }`) 200 }) 201 202 ctx := context.Background() 203 artifact, _, err := client.Actions.GetArtifact(ctx, "o", "r", 1) 204 if err != nil { 205 t.Errorf("Actions.GetArtifact returned error: %v", err) 206 } 207 208 want := &Artifact{ 209 ID: Ptr(int64(1)), 210 NodeID: Ptr("xyz"), 211 Name: Ptr("a"), 212 SizeInBytes: Ptr(int64(5)), 213 ArchiveDownloadURL: Ptr("u"), 214 } 215 if !cmp.Equal(artifact, want) { 216 t.Errorf("Actions.GetArtifact returned %+v, want %+v", artifact, want) 217 } 218 219 const methodName = "GetArtifact" 220 testBadOptions(t, methodName, func() (err error) { 221 _, _, err = client.Actions.GetArtifact(ctx, "\n", "\n", -1) 222 return err 223 }) 224 225 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 226 got, resp, err := client.Actions.GetArtifact(ctx, "o", "r", 1) 227 if got != nil { 228 t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) 229 } 230 return resp, err 231 }) 232 } 233 234 func TestActionsService_GetArtifact_invalidOwner(t *testing.T) { 235 t.Parallel() 236 client, _, _ := setup(t) 237 238 ctx := context.Background() 239 _, _, err := client.Actions.GetArtifact(ctx, "%", "r", 1) 240 testURLParseError(t, err) 241 } 242 243 func TestActionsService_GetArtifact_invalidRepo(t *testing.T) { 244 t.Parallel() 245 client, _, _ := setup(t) 246 247 ctx := context.Background() 248 _, _, err := client.Actions.GetArtifact(ctx, "o", "%", 1) 249 testURLParseError(t, err) 250 } 251 252 func TestActionsService_GetArtifact_notFound(t *testing.T) { 253 t.Parallel() 254 client, mux, _ := setup(t) 255 256 mux.HandleFunc("/repos/o/r/actions/artifacts/1", func(w http.ResponseWriter, r *http.Request) { 257 testMethod(t, r, "GET") 258 w.WriteHeader(http.StatusNotFound) 259 }) 260 261 ctx := context.Background() 262 artifact, resp, err := client.Actions.GetArtifact(ctx, "o", "r", 1) 263 if err == nil { 264 t.Errorf("Expected HTTP 404 response") 265 } 266 if got, want := resp.Response.StatusCode, http.StatusNotFound; got != want { 267 t.Errorf("Actions.GetArtifact return status %d, want %d", got, want) 268 } 269 if artifact != nil { 270 t.Errorf("Actions.GetArtifact return %+v, want nil", artifact) 271 } 272 } 273 274 func TestActionsService_DownloadArtifact(t *testing.T) { 275 t.Parallel() 276 277 tcs := []struct { 278 name string 279 respectRateLimits bool 280 }{ 281 { 282 name: "withoutRateLimits", 283 respectRateLimits: false, 284 }, 285 { 286 name: "withRateLimits", 287 respectRateLimits: true, 288 }, 289 } 290 291 for _, tc := range tcs { 292 t.Run(tc.name, func(t *testing.T) { 293 t.Parallel() 294 client, mux, _ := setup(t) 295 client.RateLimitRedirectionalEndpoints = tc.respectRateLimits 296 297 mux.HandleFunc("/repos/o/r/actions/artifacts/1/zip", func(w http.ResponseWriter, r *http.Request) { 298 testMethod(t, r, "GET") 299 http.Redirect(w, r, "https://github.com/artifact", http.StatusFound) 300 }) 301 302 ctx := context.Background() 303 url, resp, err := client.Actions.DownloadArtifact(ctx, "o", "r", 1, 1) 304 if err != nil { 305 t.Errorf("Actions.DownloadArtifact returned error: %v", err) 306 } 307 if resp.StatusCode != http.StatusFound { 308 t.Errorf("Actions.DownloadArtifact returned status: %d, want %d", resp.StatusCode, http.StatusFound) 309 } 310 311 want := "https://github.com/artifact" 312 if url.String() != want { 313 t.Errorf("Actions.DownloadArtifact returned %+v, want %+v", url.String(), want) 314 } 315 316 const methodName = "DownloadArtifact" 317 testBadOptions(t, methodName, func() (err error) { 318 _, _, err = client.Actions.DownloadArtifact(ctx, "\n", "\n", -1, 1) 319 return err 320 }) 321 322 // Add custom round tripper 323 client.client.Transport = roundTripperFunc(func(r *http.Request) (*http.Response, error) { 324 return nil, errors.New("failed to download artifact") 325 }) 326 // propagate custom round tripper to client without CheckRedirect 327 client.initialize() 328 testBadOptions(t, methodName, func() (err error) { 329 _, _, err = client.Actions.DownloadArtifact(ctx, "o", "r", 1, 1) 330 return err 331 }) 332 }) 333 } 334 } 335 336 func TestActionsService_DownloadArtifact_invalidOwner(t *testing.T) { 337 t.Parallel() 338 tcs := []struct { 339 name string 340 respectRateLimits bool 341 }{ 342 { 343 name: "withoutRateLimits", 344 respectRateLimits: false, 345 }, 346 { 347 name: "withRateLimits", 348 respectRateLimits: true, 349 }, 350 } 351 352 for _, tc := range tcs { 353 t.Run(tc.name, func(t *testing.T) { 354 t.Parallel() 355 client, _, _ := setup(t) 356 client.RateLimitRedirectionalEndpoints = tc.respectRateLimits 357 358 ctx := context.Background() 359 _, _, err := client.Actions.DownloadArtifact(ctx, "%", "r", 1, 1) 360 testURLParseError(t, err) 361 }) 362 } 363 } 364 365 func TestActionsService_DownloadArtifact_invalidRepo(t *testing.T) { 366 t.Parallel() 367 tcs := []struct { 368 name string 369 respectRateLimits bool 370 }{ 371 { 372 name: "withoutRateLimits", 373 respectRateLimits: false, 374 }, 375 { 376 name: "withRateLimits", 377 respectRateLimits: true, 378 }, 379 } 380 381 for _, tc := range tcs { 382 t.Run(tc.name, func(t *testing.T) { 383 t.Parallel() 384 client, _, _ := setup(t) 385 client.RateLimitRedirectionalEndpoints = tc.respectRateLimits 386 387 ctx := context.Background() 388 _, _, err := client.Actions.DownloadArtifact(ctx, "o", "%", 1, 1) 389 testURLParseError(t, err) 390 }) 391 } 392 } 393 394 func TestActionsService_DownloadArtifact_StatusMovedPermanently_dontFollowRedirects(t *testing.T) { 395 t.Parallel() 396 tcs := []struct { 397 name string 398 respectRateLimits bool 399 }{ 400 { 401 name: "withoutRateLimits", 402 respectRateLimits: false, 403 }, 404 { 405 name: "withRateLimits", 406 respectRateLimits: true, 407 }, 408 } 409 410 for _, tc := range tcs { 411 t.Run(tc.name, func(t *testing.T) { 412 t.Parallel() 413 client, mux, _ := setup(t) 414 client.RateLimitRedirectionalEndpoints = tc.respectRateLimits 415 416 mux.HandleFunc("/repos/o/r/actions/artifacts/1/zip", func(w http.ResponseWriter, r *http.Request) { 417 testMethod(t, r, "GET") 418 http.Redirect(w, r, "https://github.com/artifact", http.StatusMovedPermanently) 419 }) 420 421 ctx := context.Background() 422 _, resp, _ := client.Actions.DownloadArtifact(ctx, "o", "r", 1, 0) 423 if resp.StatusCode != http.StatusMovedPermanently { 424 t.Errorf("Actions.DownloadArtifact return status %d, want %d", resp.StatusCode, http.StatusMovedPermanently) 425 } 426 }) 427 } 428 } 429 430 func TestActionsService_DownloadArtifact_StatusMovedPermanently_followRedirects(t *testing.T) { 431 t.Parallel() 432 tcs := []struct { 433 name string 434 respectRateLimits bool 435 }{ 436 { 437 name: "withoutRateLimits", 438 respectRateLimits: false, 439 }, 440 { 441 name: "withRateLimits", 442 respectRateLimits: true, 443 }, 444 } 445 446 for _, tc := range tcs { 447 t.Run(tc.name, func(t *testing.T) { 448 t.Parallel() 449 client, mux, serverURL := setup(t) 450 client.RateLimitRedirectionalEndpoints = tc.respectRateLimits 451 452 mux.HandleFunc("/repos/o/r/actions/artifacts/1/zip", func(w http.ResponseWriter, r *http.Request) { 453 testMethod(t, r, "GET") 454 redirectURL, _ := url.Parse(serverURL + baseURLPath + "/redirect") 455 http.Redirect(w, r, redirectURL.String(), http.StatusMovedPermanently) 456 }) 457 mux.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) { 458 testMethod(t, r, "GET") 459 http.Redirect(w, r, "http://github.com/artifact", http.StatusFound) 460 }) 461 462 ctx := context.Background() 463 url, resp, err := client.Actions.DownloadArtifact(ctx, "o", "r", 1, 1) 464 if err != nil { 465 t.Errorf("Actions.DownloadArtifact return error: %v", err) 466 } 467 if resp.StatusCode != http.StatusFound { 468 t.Errorf("Actions.DownloadArtifact return status %d, want %d", resp.StatusCode, http.StatusFound) 469 } 470 want := "http://github.com/artifact" 471 if url.String() != want { 472 t.Errorf("Actions.DownloadArtifact returned %+v, want %+v", url.String(), want) 473 } 474 }) 475 } 476 } 477 478 func TestActionsService_DownloadArtifact_unexpectedCode(t *testing.T) { 479 t.Parallel() 480 tcs := []struct { 481 name string 482 respectRateLimits bool 483 }{ 484 { 485 name: "withoutRateLimits", 486 respectRateLimits: false, 487 }, 488 { 489 name: "withRateLimits", 490 respectRateLimits: true, 491 }, 492 } 493 494 for _, tc := range tcs { 495 t.Run(tc.name, func(t *testing.T) { 496 t.Parallel() 497 client, mux, serverURL := setup(t) 498 client.RateLimitRedirectionalEndpoints = tc.respectRateLimits 499 500 mux.HandleFunc("/repos/o/r/actions/artifacts/1/zip", func(w http.ResponseWriter, r *http.Request) { 501 testMethod(t, r, "GET") 502 redirectURL, _ := url.Parse(serverURL + baseURLPath + "/redirect") 503 http.Redirect(w, r, redirectURL.String(), http.StatusMovedPermanently) 504 }) 505 mux.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) { 506 testMethod(t, r, "GET") 507 w.WriteHeader(http.StatusNoContent) 508 }) 509 510 ctx := context.Background() 511 url, resp, err := client.Actions.DownloadArtifact(ctx, "o", "r", 1, 1) 512 if err == nil { 513 t.Fatalf("Actions.DownloadArtifact should return error on unexpected code") 514 } 515 if !strings.Contains(err.Error(), "unexpected status code") { 516 t.Error("Actions.DownloadArtifact should return unexpected status code") 517 } 518 if got, want := resp.Response.StatusCode, http.StatusNoContent; got != want { 519 t.Errorf("Actions.DownloadArtifact return status %d, want %d", got, want) 520 } 521 if url != nil { 522 t.Errorf("Actions.DownloadArtifact return %+v, want nil", url) 523 } 524 }) 525 } 526 } 527 528 func TestActionsService_DeleteArtifact(t *testing.T) { 529 t.Parallel() 530 client, mux, _ := setup(t) 531 532 mux.HandleFunc("/repos/o/r/actions/artifacts/1", func(w http.ResponseWriter, r *http.Request) { 533 testMethod(t, r, "DELETE") 534 }) 535 536 ctx := context.Background() 537 _, err := client.Actions.DeleteArtifact(ctx, "o", "r", 1) 538 if err != nil { 539 t.Errorf("Actions.DeleteArtifact return error: %v", err) 540 } 541 542 const methodName = "DeleteArtifact" 543 testBadOptions(t, methodName, func() (err error) { 544 _, err = client.Actions.DeleteArtifact(ctx, "\n", "\n", -1) 545 return err 546 }) 547 548 testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { 549 return client.Actions.DeleteArtifact(ctx, "o", "r", 1) 550 }) 551 } 552 553 func TestActionsService_DeleteArtifact_invalidOwner(t *testing.T) { 554 t.Parallel() 555 client, _, _ := setup(t) 556 557 ctx := context.Background() 558 _, err := client.Actions.DeleteArtifact(ctx, "%", "r", 1) 559 testURLParseError(t, err) 560 } 561 562 func TestActionsService_DeleteArtifact_invalidRepo(t *testing.T) { 563 t.Parallel() 564 client, _, _ := setup(t) 565 566 ctx := context.Background() 567 _, err := client.Actions.DeleteArtifact(ctx, "o", "%", 1) 568 testURLParseError(t, err) 569 } 570 571 func TestActionsService_DeleteArtifact_notFound(t *testing.T) { 572 t.Parallel() 573 client, mux, _ := setup(t) 574 575 mux.HandleFunc("/repos/o/r/actions/artifacts/1", func(w http.ResponseWriter, r *http.Request) { 576 testMethod(t, r, "DELETE") 577 w.WriteHeader(http.StatusNotFound) 578 }) 579 580 ctx := context.Background() 581 resp, err := client.Actions.DeleteArtifact(ctx, "o", "r", 1) 582 if err == nil { 583 t.Errorf("Expected HTTP 404 response") 584 } 585 if got, want := resp.Response.StatusCode, http.StatusNotFound; got != want { 586 t.Errorf("Actions.DeleteArtifact return status %d, want %d", got, want) 587 } 588 } 589 590 func TestArtifact_Marshal(t *testing.T) { 591 t.Parallel() 592 testJSONMarshal(t, &Artifact{}, "{}") 593 594 u := &Artifact{ 595 ID: Ptr(int64(1)), 596 NodeID: Ptr("nid"), 597 Name: Ptr("n"), 598 SizeInBytes: Ptr(int64(1)), 599 URL: Ptr("u"), 600 ArchiveDownloadURL: Ptr("a"), 601 Expired: Ptr(false), 602 CreatedAt: &Timestamp{referenceTime}, 603 UpdatedAt: &Timestamp{referenceTime}, 604 ExpiresAt: &Timestamp{referenceTime}, 605 WorkflowRun: &ArtifactWorkflowRun{ 606 ID: Ptr(int64(1)), 607 RepositoryID: Ptr(int64(1)), 608 HeadRepositoryID: Ptr(int64(1)), 609 HeadBranch: Ptr("b"), 610 HeadSHA: Ptr("s"), 611 }, 612 } 613 614 want := `{ 615 "id": 1, 616 "node_id": "nid", 617 "name": "n", 618 "size_in_bytes": 1, 619 "url": "u", 620 "archive_download_url": "a", 621 "expired": false, 622 "created_at": ` + referenceTimeStr + `, 623 "updated_at": ` + referenceTimeStr + `, 624 "expires_at": ` + referenceTimeStr + `, 625 "workflow_run": { 626 "id": 1, 627 "repository_id": 1, 628 "head_repository_id": 1, 629 "head_branch": "b", 630 "head_sha": "s" 631 } 632 }` 633 634 testJSONMarshal(t, u, want) 635 } 636 637 func TestArtifactList_Marshal(t *testing.T) { 638 t.Parallel() 639 testJSONMarshal(t, &ArtifactList{}, "{}") 640 641 u := &ArtifactList{ 642 TotalCount: Ptr(int64(1)), 643 Artifacts: []*Artifact{ 644 { 645 ID: Ptr(int64(1)), 646 NodeID: Ptr("nid"), 647 Name: Ptr("n"), 648 SizeInBytes: Ptr(int64(1)), 649 URL: Ptr("u"), 650 ArchiveDownloadURL: Ptr("a"), 651 Expired: Ptr(false), 652 CreatedAt: &Timestamp{referenceTime}, 653 UpdatedAt: &Timestamp{referenceTime}, 654 ExpiresAt: &Timestamp{referenceTime}, 655 WorkflowRun: &ArtifactWorkflowRun{ 656 ID: Ptr(int64(1)), 657 RepositoryID: Ptr(int64(1)), 658 HeadRepositoryID: Ptr(int64(1)), 659 HeadBranch: Ptr("b"), 660 HeadSHA: Ptr("s"), 661 }, 662 }, 663 }, 664 } 665 666 want := `{ 667 "total_count": 1, 668 "artifacts": [{ 669 "id": 1, 670 "node_id": "nid", 671 "name": "n", 672 "size_in_bytes": 1, 673 "url": "u", 674 "archive_download_url": "a", 675 "expired": false, 676 "created_at": ` + referenceTimeStr + `, 677 "updated_at": ` + referenceTimeStr + `, 678 "expires_at": ` + referenceTimeStr + `, 679 "workflow_run": { 680 "id": 1, 681 "repository_id": 1, 682 "head_repository_id": 1, 683 "head_branch": "b", 684 "head_sha": "s" 685 } 686 }] 687 }` 688 689 testJSONMarshal(t, u, want) 690 }