package artifact import ( "bytes" "context" "fmt" "os" "testing" "time" ) // TestE2E_ArtifactRoundTrip exercises the real results service. It only runs // inside a CI pipeline (GITHUB_ACTIONS=true) and is skipped locally so that // `go test ./...` stays hermetic. Its purpose is to detect drift between the // results service the runner talks to and the API this client expects. func TestE2E_ArtifactRoundTrip(t *testing.T) { if os.Getenv("GITHUB_ACTIONS") != "true" { t.Skip("e2e test only runs in CI") } resultsURL := os.Getenv("ACTIONS_RESULTS_URL") runtimeToken := os.Getenv("ACTIONS_RUNTIME_TOKEN") if resultsURL == "" || runtimeToken == "" { t.Skip("ACTIONS_RESULTS_URL and ACTIONS_RUNTIME_TOKEN are required") } ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() run, job, err := JobInfo() if err != nil { t.Fatalf("JobInfo: %v", err) } client := NewClient(resultsURL, runtimeToken) name := fmt.Sprintf("e2e-%d-%d", os.Getpid(), time.Now().UnixNano()) expire := time.Now().Add(1 * time.Hour) create, err := client.CreateArtifact(ctx, CreateArtifactRequest{ RunID: run, JobRunID: job, Name: name, Version: 4, ExpiresAt: &expire, }) if err != nil { t.Fatalf("CreateArtifact: %v", err) } if !create.Ok { t.Fatalf("CreateArtifact not ok: %+v", create) } t.Cleanup(func() { if _, err := client.DeleteArtifact(context.Background(), DeleteArtifactRequest{RunID: run, JobRunID: job, Name: name}); err != nil { t.Logf("cleanup DeleteArtifact: %v", err) } }) content := []byte("artifact e2e " + name) pushed, err := PushBlob(ctx, nil, bytes.NewReader(content), create.SignedUploadUrl, 0, 0) if err != nil { t.Fatalf("PushBlob: %v", err) } finalize, err := client.FinalizeArtifact(ctx, FinalizeArtifactRequest{ RunID: run, JobRunID: job, Name: name, Size: pushed.Size, Hash: pushed.SHA256Sum, }) if err != nil { t.Fatalf("FinalizeArtifact: %v", err) } if !finalize.Ok { t.Fatalf("FinalizeArtifact not ok: %+v", finalize) } signed := getSignedArtifactURL(t, ctx, client, run, job, name) var buf bytes.Buffer pulled, err := PullBlob(ctx, nil, &buf, signed.SignedUrl) if err != nil { t.Fatalf("PullBlob: %v", err) } if !bytes.Equal(buf.Bytes(), content) { t.Errorf("round trip mismatch: got %q want %q", buf.String(), content) } if pulled.SHA256Sum != pushed.SHA256Sum { t.Errorf("hash mismatch: got %q want %q", pulled.SHA256Sum, pushed.SHA256Sum) } } // getSignedArtifactURL retries briefly because a freshly finalized artifact may // not be signed yet. func getSignedArtifactURL(t *testing.T, ctx context.Context, client *Client, run, job, name string) *GetSignedArtifactURLResponse { t.Helper() var lastErr error for attempt := 0; attempt < 5; attempt++ { signed, err := client.GetSignedArtifactURL(ctx, GetSignedArtifactURLRequest{RunID: run, JobRunID: job, Name: name}) if err == nil && signed.SignedUrl != "" { return signed } lastErr = err select { case <-ctx.Done(): t.Fatalf("GetSignedArtifactURL: %v", ctx.Err()) case <-time.After(time.Duration(attempt+1) * time.Second): } } t.Fatalf("GetSignedArtifactURL: %v", lastErr) return nil }