blob: 4beace8c4eb9cad698eaf47df691275ccd1f78a2 [file] [log] [blame]
// 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.
}