| // Copyright 2023 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package cipd |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "testing" |
| |
| "github.com/google/go-cmp/cmp" |
| "go.chromium.org/luci/auth" |
| api "go.chromium.org/luci/cipd/api/cipd/v1" |
| "go.chromium.org/luci/cipd/client/cipd" |
| "go.chromium.org/luci/cipd/common" |
| "go.chromium.org/luci/grpc/prpc" |
| "go.chromium.org/luci/hardcoded/chromeinfra" |
| "go.fuchsia.dev/infra/cmd/recipe_wrapper/bcid" |
| ) |
| |
| func TestDeferredSteps(t *testing.T) { |
| tests := []struct { |
| name string |
| input string |
| want []*DeferredStep |
| wantMismatch bool |
| }{ |
| { |
| name: "One step should parse", |
| input: ` |
| { |
| "_timestamp": "1234", |
| "name": "My First Step", |
| "command": ["curl", "-v", "google.com"], |
| "environment": { |
| "USER": "cflewis", |
| "CURL_SSL_BACKEND": "OpenSSL" |
| } |
| } |
| `, |
| want: []*DeferredStep{ |
| { |
| Name: "My First Step", |
| Command: []string{"curl", "-v", "google.com"}, |
| Environment: map[string]string{"USER": "cflewis", "CURL_SSL_BACKEND": "OpenSSL"}, |
| }, |
| }, |
| }, |
| { |
| // Only one step in a file is valid. |
| name: "Multiple steps shouldn't parse", |
| input: ` |
| { |
| "steps": [ |
| { |
| "_timestamp": "1234", |
| "name": "My First Step", |
| "command": ["curl", "-v", "google.com"], |
| "environment": { |
| "USER": "cflewis", |
| "CURL_SSL_BACKEND": "OpenSSL" |
| } |
| }, |
| { |
| "_timestamp": "5678", |
| "name": "My Second Step", |
| "command": ["curl", "example.com"] |
| } |
| ] |
| } |
| `, |
| wantMismatch: true, |
| }, |
| { |
| name: "Mismatched JSON should return an error", |
| input: ` |
| { |
| "steps": [ |
| { |
| "_timestamp": "1234", |
| "name": "My First Step", |
| "command": ["curl", "-v", "google.com"], |
| "environment": { |
| "USER": "cflewis", |
| "CURL_SSL_BACKEND": "OpenSSL" |
| } |
| } |
| ] |
| } |
| `, |
| want: []*DeferredStep{ |
| { |
| Name: "My Wrong Step", |
| Command: []string{"curl", "-v", "google.com"}, |
| Environment: map[string]string{"USER": "cflewis", "CURL_SSL_BACKEND": "OpenSSL"}, |
| }, |
| }, |
| wantMismatch: true, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| steps, err := ParseDeferredSteps(strings.NewReader(test.input)) |
| if err != nil { |
| t.Fatalf("got %v, want nil error", err) |
| } |
| |
| if cmp.Equal(steps, test.want) && test.wantMismatch { |
| t.Errorf("got %+v, want mismatch with %+v", steps, test.want) |
| } |
| if !cmp.Equal(steps, test.want) && !test.wantMismatch { |
| var stepStrs []string |
| for _, step := range steps { |
| stepStrs = append(stepStrs, fmt.Sprintf("%+v", step)) |
| } |
| |
| t.Errorf("got %+v, want %+v", strings.Join(stepStrs, ","), test.want) |
| } |
| }) |
| } |
| } |
| |
| func TestRegisteredPkgs(t *testing.T) { |
| wd, err := os.Getwd() |
| if err != nil { |
| t.Fatalf("can't get working directory: %v", err) |
| } |
| tests := []struct { |
| name string |
| steps []*DeferredStep |
| want []string |
| }{ |
| { |
| name: "Multiple registers should return multiple paths", |
| steps: []*DeferredStep{ |
| { |
| Command: []string{"cipd", "--help"}, |
| }, |
| { |
| Command: []string{"cipd", "pkg-register", "foo"}, |
| }, |
| { |
| Command: []string{"cipd", "pkg-deploy", "foo"}, |
| }, |
| { |
| Command: []string{"cipd", "pkg-register", "./pkg/bar.pkg"}, |
| }, |
| // ~ does not work here. Go does not expand it, rather the shell does. |
| // See https://stackoverflow.com/q/17609732 |
| { |
| Command: []string{"cipd", "pkg-register", "/home/cflewis/.local/bin/baz"}, |
| }, |
| }, |
| want: []string{ |
| filepath.Join(wd, "foo"), |
| filepath.Join(wd, "pkg/bar.pkg"), |
| "/home/cflewis/.local/bin/baz", |
| }, |
| }, |
| { |
| name: "No register should return an empty slice", |
| steps: []*DeferredStep{ |
| { |
| Command: []string{"cipd", "--help"}, |
| }, |
| { |
| Command: []string{"cipd", "pkg-deploy", "foo"}, |
| }, |
| }, |
| }, |
| { |
| name: "No steps should return an empty slice", |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| if diff := cmp.Diff(RegisteredPkgs(test.steps), test.want); diff != "" { |
| t.Errorf("got diff: %v", diff) |
| } |
| }) |
| } |
| } |
| |
| func TestAttestPkgs(t *testing.T) { |
| _, pathErr := exec.LookPath("attestation_tool") |
| if testing.Short() || pathErr != nil { |
| t.Skip("no attestation_tool or test ran with -short, skipping") |
| } |
| tmpfile, err := os.CreateTemp("", "cipd_test") |
| if err != nil { |
| t.Fatalf("can't create tempfile: %v", err) |
| } |
| |
| stmtBundles, errs := AttestPkgs([]*DeferredStep{ |
| { |
| Command: []string{"cipd", "pkg-register", tmpfile.Name()}, |
| }, |
| }, bcid.TestKeyID) |
| if errs != nil { |
| t.Errorf("got errors: %+v, want nil error", errs) |
| } |
| |
| for _, sb := range stmtBundles { |
| // A real invocation of the attestation_tool is non-deterministic because it appears |
| // to rely on the clock time as an input. |
| // Instead of relying on the data itself, just check that the data was formatted as |
| // expected. This validates that attestation_tool did work. |
| if sb.Statement.GetSubject()[0].GetName() != tmpfile.Name() || |
| sb.Bundle.ContentType != "application/vnd.in-toto+json" || |
| sb.Bundle.Signatures[0].KeyID != "gcpkms://"+bcid.TestKeyID || sb.Bundle.Signatures[0].Sig == "" { |
| t.Errorf("InToto struct did not follow expected format\nContentType = %q\nKey ID = %q\nSig = %q", |
| sb.Bundle.ContentType, sb.Bundle.Signatures[0].KeyID, sb.Bundle.Signatures[0].Sig) |
| } |
| } |
| } |
| |
| type testCIPDOutputter struct{} |
| |
| func (*testCIPDOutputter) CombinedOutput(ctx context.Context, args ...string) ([]byte, error) { |
| cmd := exec.CommandContext(ctx, "cipd", args...) |
| cmd.Env = append(cmd.Environ(), "CIPD_SERVICE_URL="+SERVICE_DEV_URL) |
| return cmd.CombinedOutput() |
| } |
| |
| const pkgName = "experimental/fuchsia/recipe_wrapper/cipd_test" |
| |
| type CIPDResult struct { |
| Package string `json:"package"` |
| InstanceID string `json:"instance_id"` |
| } |
| type CIPDResults struct { |
| Result *CIPDResult `json:"result"` |
| } |
| |
| func containsMetadata(s []*api.InstanceMetadata, md cipd.Metadata) bool { |
| for _, im := range s { |
| if im.GetKey() == md.Key && string(im.GetValue()) == string(md.Value) { |
| return true |
| } |
| } |
| |
| return false |
| } |
| |
| func newRepoClient(ctx context.Context, t *testing.T) api.RepositoryClient { |
| c, err := auth.NewAuthenticator(ctx, auth.OptionalLogin, chromeinfra.DefaultAuthOptions()).Client() |
| if err != nil { |
| t.Fatalf("couldn't initialize authenticator: %v", err) |
| } |
| prpcC := &prpc.Client{ |
| C: c, |
| Host: "chrome-infra-packages-dev.appspot.com", |
| } |
| |
| return api.NewRepositoryClient(prpcC) |
| } |
| |
| func TestAddMetadata(t *testing.T) { |
| _, err := exec.LookPath("cipd") |
| if testing.Short() || err != nil { |
| // Testing without cipd is so trivial there's no point in doing anything. |
| // Even if the code is only using the client library, it does assume that |
| // the required auth flow for cipd has already been completed (e.g. `cipd auth-login`). |
| // If `cipd` isn't here, then the auth flow can't have happened. |
| t.Skip("`cipd` not on path") |
| } |
| ctx := context.Background() |
| |
| // An executed command is used instead of using the luci-go cipd client |
| // because the package creation code included in the `cipd` executable |
| // is non-trivial. It's more reliable to reuse the executable than attempt |
| // to recreate the logic here. |
| tmpdir, err := os.MkdirTemp("", "cipd_test") |
| if err != nil { |
| t.Fatalf("unable to create tempdir: %v", err) |
| } |
| f, err := os.CreateTemp(tmpdir, "") |
| if err != nil { |
| t.Fatalf("unable to create temp file: %v", err) |
| } |
| f.WriteString("Hello, cipd_test!") |
| f.Close() |
| defer os.RemoveAll(tmpdir) |
| |
| // Make a temporary file to output results to. |
| // This is necessary so the created version can be inspected. |
| // The version ID is a requirement for adding metadata. |
| outF, err := os.CreateTemp("", "cipd_test_output") |
| if err != nil { |
| t.Fatalf("unable to create JSON output file: %v", err) |
| } |
| defer os.Remove(outF.Name()) |
| |
| cmd := exec.Command( |
| "cipd", |
| "create", |
| "-in", tmpdir, |
| "-name", pkgName, |
| "-json-output", outF.Name(), |
| ) |
| cmd.Env = append(cmd.Environ(), "CIPD_SERVICE_URL="+SERVICE_DEV_URL) |
| out, err := cmd.CombinedOutput() |
| if err != nil { |
| t.Fatalf("unable to create test package: %s/%v", out, err) |
| } |
| outF.Close() |
| |
| // Retrieve the instance ID to add metadata to. |
| resultData, err := os.ReadFile(outF.Name()) |
| if err != nil { |
| t.Fatalf("couldn't read CIPD create JSON results file: %v", err) |
| } |
| var results CIPDResults |
| err = json.Unmarshal(resultData, &results) |
| if err != nil { |
| t.Fatalf("couldn't get JSON results out of CIPD create results file: %v", err) |
| } |
| |
| want := []cipd.Metadata{ |
| { |
| Key: "foo", |
| Value: []byte("bar"), |
| ContentType: "text/plain", |
| }, |
| { |
| Key: "alice", |
| Value: []byte("bob"), |
| ContentType: "text/plain", |
| }, |
| } |
| |
| err = addMetadataWithOutputter( |
| ctx, &testCIPDOutputter{}, SERVICE_DEV_URL, &common.Pin{PackageName: pkgName, InstanceID: results.Result.InstanceID}, want) |
| if err != nil { |
| t.Fatalf("couldn't add metadata: %v", err) |
| } |
| |
| repoC := newRepoClient(ctx, t) |
| resp, err := repoC.ListMetadata(ctx, &api.ListMetadataRequest{ |
| Package: pkgName, |
| Instance: common.InstanceIDToObjectRef(results.Result.InstanceID), |
| }) |
| if err != nil { |
| t.Fatalf("couldn't get metadata back: %v", err) |
| } |
| |
| for _, md := range want { |
| if !containsMetadata(resp.GetMetadata(), md) { |
| t.Errorf("returned metadata does not contain %v", md) |
| } |
| } |
| |
| // The package can't be deleted here for cleanup as that requires admin privileges |
| // on the package registry. It's OK for the test package to just pile up multiple |
| // versions. |
| } |