github.com/justincormack/cli@v0.0.0-20201215022714-831ebeae9675/cli/command/trust/inspect_pretty_test.go (about)

     1  package trust
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/hex"
     7  	"io"
     8  	"io/ioutil"
     9  	"testing"
    10  
    11  	"github.com/docker/cli/cli/trust"
    12  	"github.com/docker/cli/internal/test"
    13  	notaryfake "github.com/docker/cli/internal/test/notary"
    14  	"github.com/docker/docker/api/types"
    15  	dockerClient "github.com/docker/docker/client"
    16  	"github.com/theupdateframework/notary"
    17  	"github.com/theupdateframework/notary/client"
    18  	"github.com/theupdateframework/notary/tuf/data"
    19  	"github.com/theupdateframework/notary/tuf/utils"
    20  	"gotest.tools/v3/assert"
    21  	is "gotest.tools/v3/assert/cmp"
    22  	"gotest.tools/v3/golden"
    23  )
    24  
    25  // TODO(n4ss): remove common tests with the regular inspect command
    26  
    27  type fakeClient struct {
    28  	dockerClient.Client
    29  }
    30  
    31  func (c *fakeClient) Info(ctx context.Context) (types.Info, error) {
    32  	return types.Info{}, nil
    33  }
    34  
    35  func (c *fakeClient) ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) {
    36  	return types.ImageInspect{}, []byte{}, nil
    37  }
    38  
    39  func (c *fakeClient) ImagePush(ctx context.Context, image string, options types.ImagePushOptions) (io.ReadCloser, error) {
    40  	return &utils.NoopCloser{Reader: bytes.NewBuffer([]byte{})}, nil
    41  }
    42  
    43  func TestTrustInspectPrettyCommandErrors(t *testing.T) {
    44  	testCases := []struct {
    45  		name          string
    46  		args          []string
    47  		expectedError string
    48  	}{
    49  		{
    50  			name:          "not-enough-args",
    51  			expectedError: "requires at least 1 argument",
    52  		},
    53  		{
    54  			name:          "sha-reference",
    55  			args:          []string{"870d292919d01a0af7e7f056271dc78792c05f55f49b9b9012b6d89725bd9abd"},
    56  			expectedError: "invalid repository name",
    57  		},
    58  		{
    59  			name:          "invalid-img-reference",
    60  			args:          []string{"ALPINE"},
    61  			expectedError: "invalid reference format",
    62  		},
    63  	}
    64  	for _, tc := range testCases {
    65  		cmd := newInspectCommand(
    66  			test.NewFakeCli(&fakeClient{}))
    67  		cmd.SetArgs(tc.args)
    68  		cmd.SetOut(ioutil.Discard)
    69  		cmd.Flags().Set("pretty", "true")
    70  		assert.ErrorContains(t, cmd.Execute(), tc.expectedError)
    71  	}
    72  }
    73  
    74  func TestTrustInspectPrettyCommandOfflineErrors(t *testing.T) {
    75  	cli := test.NewFakeCli(&fakeClient{})
    76  	cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository)
    77  	cmd := newInspectCommand(cli)
    78  	cmd.Flags().Set("pretty", "true")
    79  	cmd.SetArgs([]string{"nonexistent-reg-name.io/image"})
    80  	cmd.SetOut(ioutil.Discard)
    81  	assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
    82  
    83  	cli = test.NewFakeCli(&fakeClient{})
    84  	cli.SetNotaryClient(notaryfake.GetOfflineNotaryRepository)
    85  	cmd = newInspectCommand(cli)
    86  	cmd.Flags().Set("pretty", "true")
    87  	cmd.SetArgs([]string{"nonexistent-reg-name.io/image:tag"})
    88  	cmd.SetOut(ioutil.Discard)
    89  	assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access nonexistent-reg-name.io/image")
    90  }
    91  
    92  func TestTrustInspectPrettyCommandUninitializedErrors(t *testing.T) {
    93  	cli := test.NewFakeCli(&fakeClient{})
    94  	cli.SetNotaryClient(notaryfake.GetUninitializedNotaryRepository)
    95  	cmd := newInspectCommand(cli)
    96  	cmd.Flags().Set("pretty", "true")
    97  	cmd.SetArgs([]string{"reg/unsigned-img"})
    98  	cmd.SetOut(ioutil.Discard)
    99  	assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img")
   100  
   101  	cli = test.NewFakeCli(&fakeClient{})
   102  	cli.SetNotaryClient(notaryfake.GetUninitializedNotaryRepository)
   103  	cmd = newInspectCommand(cli)
   104  	cmd.Flags().Set("pretty", "true")
   105  	cmd.SetArgs([]string{"reg/unsigned-img:tag"})
   106  	cmd.SetOut(ioutil.Discard)
   107  	assert.ErrorContains(t, cmd.Execute(), "No signatures or cannot access reg/unsigned-img:tag")
   108  }
   109  
   110  func TestTrustInspectPrettyCommandEmptyNotaryRepoErrors(t *testing.T) {
   111  	cli := test.NewFakeCli(&fakeClient{})
   112  	cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository)
   113  	cmd := newInspectCommand(cli)
   114  	cmd.Flags().Set("pretty", "true")
   115  	cmd.SetArgs([]string{"reg/img:unsigned-tag"})
   116  	cmd.SetOut(ioutil.Discard)
   117  	assert.NilError(t, cmd.Execute())
   118  	assert.Check(t, is.Contains(cli.OutBuffer().String(), "No signatures for reg/img:unsigned-tag"))
   119  	assert.Check(t, is.Contains(cli.OutBuffer().String(), "Administrative keys for reg/img"))
   120  
   121  	cli = test.NewFakeCli(&fakeClient{})
   122  	cli.SetNotaryClient(notaryfake.GetEmptyTargetsNotaryRepository)
   123  	cmd = newInspectCommand(cli)
   124  	cmd.Flags().Set("pretty", "true")
   125  	cmd.SetArgs([]string{"reg/img"})
   126  	cmd.SetOut(ioutil.Discard)
   127  	assert.NilError(t, cmd.Execute())
   128  	assert.Check(t, is.Contains(cli.OutBuffer().String(), "No signatures for reg/img"))
   129  	assert.Check(t, is.Contains(cli.OutBuffer().String(), "Administrative keys for reg/img"))
   130  }
   131  
   132  func TestTrustInspectPrettyCommandFullRepoWithoutSigners(t *testing.T) {
   133  	cli := test.NewFakeCli(&fakeClient{})
   134  	cli.SetNotaryClient(notaryfake.GetLoadedWithNoSignersNotaryRepository)
   135  	cmd := newInspectCommand(cli)
   136  	cmd.Flags().Set("pretty", "true")
   137  	cmd.SetArgs([]string{"signed-repo"})
   138  	assert.NilError(t, cmd.Execute())
   139  
   140  	golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-full-repo-no-signers.golden")
   141  }
   142  
   143  func TestTrustInspectPrettyCommandOneTagWithoutSigners(t *testing.T) {
   144  	cli := test.NewFakeCli(&fakeClient{})
   145  	cli.SetNotaryClient(notaryfake.GetLoadedWithNoSignersNotaryRepository)
   146  	cmd := newInspectCommand(cli)
   147  	cmd.Flags().Set("pretty", "true")
   148  	cmd.SetArgs([]string{"signed-repo:green"})
   149  	assert.NilError(t, cmd.Execute())
   150  
   151  	golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-one-tag-no-signers.golden")
   152  }
   153  
   154  func TestTrustInspectPrettyCommandFullRepoWithSigners(t *testing.T) {
   155  	cli := test.NewFakeCli(&fakeClient{})
   156  	cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository)
   157  	cmd := newInspectCommand(cli)
   158  	cmd.Flags().Set("pretty", "true")
   159  	cmd.SetArgs([]string{"signed-repo"})
   160  	assert.NilError(t, cmd.Execute())
   161  
   162  	golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-full-repo-with-signers.golden")
   163  }
   164  
   165  func TestTrustInspectPrettyCommandUnsignedTagInSignedRepo(t *testing.T) {
   166  	cli := test.NewFakeCli(&fakeClient{})
   167  	cli.SetNotaryClient(notaryfake.GetLoadedNotaryRepository)
   168  	cmd := newInspectCommand(cli)
   169  	cmd.Flags().Set("pretty", "true")
   170  	cmd.SetArgs([]string{"signed-repo:unsigned"})
   171  	assert.NilError(t, cmd.Execute())
   172  
   173  	golden.Assert(t, cli.OutBuffer().String(), "trust-inspect-pretty-unsigned-tag-with-signers.golden")
   174  }
   175  
   176  func TestNotaryRoleToSigner(t *testing.T) {
   177  	assert.Check(t, is.Equal(releasedRoleName, notaryRoleToSigner(data.CanonicalTargetsRole)))
   178  	assert.Check(t, is.Equal(releasedRoleName, notaryRoleToSigner(trust.ReleasesRole)))
   179  	assert.Check(t, is.Equal("signer", notaryRoleToSigner("targets/signer")))
   180  	assert.Check(t, is.Equal("docker/signer", notaryRoleToSigner("targets/docker/signer")))
   181  
   182  	// It's nonsense for other base roles to have signed off on a target, but this function leaves role names intact
   183  	for _, role := range data.BaseRoles {
   184  		if role == data.CanonicalTargetsRole {
   185  			continue
   186  		}
   187  		assert.Check(t, is.Equal(role.String(), notaryRoleToSigner(role)))
   188  	}
   189  	assert.Check(t, is.Equal("notarole", notaryRoleToSigner(data.RoleName("notarole"))))
   190  }
   191  
   192  // check if a role name is "released": either targets/releases or targets TUF roles
   193  func TestIsReleasedTarget(t *testing.T) {
   194  	assert.Check(t, isReleasedTarget(trust.ReleasesRole))
   195  	for _, role := range data.BaseRoles {
   196  		assert.Check(t, is.Equal(role == data.CanonicalTargetsRole, isReleasedTarget(role)))
   197  	}
   198  	assert.Check(t, !isReleasedTarget(data.RoleName("targets/not-releases")))
   199  	assert.Check(t, !isReleasedTarget(data.RoleName("random")))
   200  	assert.Check(t, !isReleasedTarget(data.RoleName("targets/releases/subrole")))
   201  }
   202  
   203  // creates a mock delegation with a given name and no keys
   204  func mockDelegationRoleWithName(name string) data.DelegationRole {
   205  	baseRole := data.NewBaseRole(
   206  		data.RoleName(name),
   207  		notary.MinThreshold,
   208  	)
   209  	return data.DelegationRole{BaseRole: baseRole, Paths: []string{}}
   210  }
   211  
   212  func TestMatchEmptySignatures(t *testing.T) {
   213  	// first try empty targets
   214  	emptyTgts := []client.TargetSignedStruct{}
   215  
   216  	matchedSigRows := matchReleasedSignatures(emptyTgts)
   217  	assert.Check(t, is.Len(matchedSigRows, 0))
   218  }
   219  
   220  func TestMatchUnreleasedSignatures(t *testing.T) {
   221  	// try an "unreleased" target with 3 signatures, 0 rows will appear
   222  	unreleasedTgts := []client.TargetSignedStruct{}
   223  
   224  	tgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
   225  	for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
   226  		unreleasedTgts = append(unreleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: tgt})
   227  	}
   228  
   229  	matchedSigRows := matchReleasedSignatures(unreleasedTgts)
   230  	assert.Check(t, is.Len(matchedSigRows, 0))
   231  }
   232  
   233  func TestMatchOneReleasedSingleSignature(t *testing.T) {
   234  	// now try only 1 "released" target with no additional sigs, 1 row will appear with 0 signers
   235  	oneReleasedTgt := []client.TargetSignedStruct{}
   236  
   237  	// make and append the "released" target to our mock input
   238  	releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
   239  	oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt})
   240  
   241  	// make and append 3 non-released signatures on the "unreleased" target
   242  	unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
   243  	for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
   244  		oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt})
   245  	}
   246  
   247  	matchedSigRows := matchReleasedSignatures(oneReleasedTgt)
   248  	assert.Check(t, is.Len(matchedSigRows, 1))
   249  
   250  	outputRow := matchedSigRows[0]
   251  	// Empty signers because "targets/releases" doesn't show up
   252  	assert.Check(t, is.Len(outputRow.Signers, 0))
   253  	assert.Check(t, is.Equal(releasedTgt.Name, outputRow.SignedTag))
   254  	assert.Check(t, is.Equal(hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest))
   255  }
   256  
   257  func TestMatchOneReleasedMultiSignature(t *testing.T) {
   258  	// now try only 1 "released" target with 3 additional sigs, 1 row will appear with 3 signers
   259  	oneReleasedTgt := []client.TargetSignedStruct{}
   260  
   261  	// make and append the "released" target to our mock input
   262  	releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
   263  	oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: releasedTgt})
   264  
   265  	// make and append 3 non-released signatures on both the "released" and "unreleased" targets
   266  	unreleasedTgt := client.Target{Name: "unreleased", Hashes: data.Hashes{notary.SHA256: []byte("hash")}}
   267  	for _, unreleasedRole := range []string{"targets/a", "targets/b", "targets/c"} {
   268  		oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: unreleasedTgt})
   269  		oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(unreleasedRole), Target: releasedTgt})
   270  	}
   271  
   272  	matchedSigRows := matchReleasedSignatures(oneReleasedTgt)
   273  	assert.Check(t, is.Len(matchedSigRows, 1))
   274  
   275  	outputRow := matchedSigRows[0]
   276  	// We should have three signers
   277  	assert.Check(t, is.DeepEqual(outputRow.Signers, []string{"a", "b", "c"}))
   278  	assert.Check(t, is.Equal(releasedTgt.Name, outputRow.SignedTag))
   279  	assert.Check(t, is.Equal(hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest))
   280  }
   281  
   282  func TestMatchMultiReleasedMultiSignature(t *testing.T) {
   283  	// now try 3 "released" targets with additional sigs to show 3 rows as follows:
   284  	// target-a is signed by targets/releases and targets/a - a will be the signer
   285  	// target-b is signed by targets/releases, targets/a, targets/b - a and b will be the signers
   286  	// target-c is signed by targets/releases, targets/a, targets/b, targets/c - a, b, and c will be the signers
   287  	multiReleasedTgts := []client.TargetSignedStruct{}
   288  	// make target-a, target-b, and target-c
   289  	targetA := client.Target{Name: "target-a", Hashes: data.Hashes{notary.SHA256: []byte("target-a-hash")}}
   290  	targetB := client.Target{Name: "target-b", Hashes: data.Hashes{notary.SHA256: []byte("target-b-hash")}}
   291  	targetC := client.Target{Name: "target-c", Hashes: data.Hashes{notary.SHA256: []byte("target-c-hash")}}
   292  
   293  	// have targets/releases "sign" on all of these targets so they are released
   294  	multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetA})
   295  	multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetB})
   296  	multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/releases"), Target: targetC})
   297  
   298  	// targets/a signs off on all three targets (target-a, target-b, target-c):
   299  	for _, tgt := range []client.Target{targetA, targetB, targetC} {
   300  		multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/a"), Target: tgt})
   301  	}
   302  
   303  	// targets/b signs off on the final two targets (target-b, target-c):
   304  	for _, tgt := range []client.Target{targetB, targetC} {
   305  		multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/b"), Target: tgt})
   306  	}
   307  
   308  	// targets/c only signs off on the last target (target-c):
   309  	multiReleasedTgts = append(multiReleasedTgts, client.TargetSignedStruct{Role: mockDelegationRoleWithName("targets/c"), Target: targetC})
   310  
   311  	matchedSigRows := matchReleasedSignatures(multiReleasedTgts)
   312  	assert.Check(t, is.Len(matchedSigRows, 3))
   313  
   314  	// note that the output is sorted by tag name, so we can reliably index to validate data:
   315  	outputTargetA := matchedSigRows[0]
   316  	assert.Check(t, is.DeepEqual(outputTargetA.Signers, []string{"a"}))
   317  	assert.Check(t, is.Equal(targetA.Name, outputTargetA.SignedTag))
   318  	assert.Check(t, is.Equal(hex.EncodeToString(targetA.Hashes[notary.SHA256]), outputTargetA.Digest))
   319  
   320  	outputTargetB := matchedSigRows[1]
   321  	assert.Check(t, is.DeepEqual(outputTargetB.Signers, []string{"a", "b"}))
   322  	assert.Check(t, is.Equal(targetB.Name, outputTargetB.SignedTag))
   323  	assert.Check(t, is.Equal(hex.EncodeToString(targetB.Hashes[notary.SHA256]), outputTargetB.Digest))
   324  
   325  	outputTargetC := matchedSigRows[2]
   326  	assert.Check(t, is.DeepEqual(outputTargetC.Signers, []string{"a", "b", "c"}))
   327  	assert.Check(t, is.Equal(targetC.Name, outputTargetC.SignedTag))
   328  	assert.Check(t, is.Equal(hex.EncodeToString(targetC.Hashes[notary.SHA256]), outputTargetC.Digest))
   329  }
   330  
   331  func TestMatchReleasedSignatureFromTargets(t *testing.T) {
   332  	// now try only 1 "released" target with no additional sigs, one rows will appear
   333  	oneReleasedTgt := []client.TargetSignedStruct{}
   334  	// make and append the "released" target to our mock input
   335  	releasedTgt := client.Target{Name: "released", Hashes: data.Hashes{notary.SHA256: []byte("released-hash")}}
   336  	oneReleasedTgt = append(oneReleasedTgt, client.TargetSignedStruct{Role: mockDelegationRoleWithName(data.CanonicalTargetsRole.String()), Target: releasedTgt})
   337  	matchedSigRows := matchReleasedSignatures(oneReleasedTgt)
   338  	assert.Check(t, is.Len(matchedSigRows, 1))
   339  	outputRow := matchedSigRows[0]
   340  	// Empty signers because "targets" doesn't show up
   341  	assert.Check(t, is.Len(outputRow.Signers, 0))
   342  	assert.Check(t, is.Equal(releasedTgt.Name, outputRow.SignedTag))
   343  	assert.Check(t, is.Equal(hex.EncodeToString(releasedTgt.Hashes[notary.SHA256]), outputRow.Digest))
   344  }
   345  
   346  func TestGetSignerRolesWithKeyIDs(t *testing.T) {
   347  	roles := []data.Role{
   348  		{
   349  			RootRole: data.RootRole{
   350  				KeyIDs: []string{"key11"},
   351  			},
   352  			Name: "targets/alice",
   353  		},
   354  		{
   355  			RootRole: data.RootRole{
   356  				KeyIDs: []string{"key21", "key22"},
   357  			},
   358  			Name: "targets/releases",
   359  		},
   360  		{
   361  			RootRole: data.RootRole{
   362  				KeyIDs: []string{"key31"},
   363  			},
   364  			Name: data.CanonicalTargetsRole,
   365  		},
   366  		{
   367  			RootRole: data.RootRole{
   368  				KeyIDs: []string{"key41", "key01"},
   369  			},
   370  			Name: data.CanonicalRootRole,
   371  		},
   372  		{
   373  			RootRole: data.RootRole{
   374  				KeyIDs: []string{"key51"},
   375  			},
   376  			Name: data.CanonicalSnapshotRole,
   377  		},
   378  		{
   379  			RootRole: data.RootRole{
   380  				KeyIDs: []string{"key61"},
   381  			},
   382  			Name: data.CanonicalTimestampRole,
   383  		},
   384  		{
   385  			RootRole: data.RootRole{
   386  				KeyIDs: []string{"key71", "key72"},
   387  			},
   388  			Name: "targets/bob",
   389  		},
   390  	}
   391  	expectedSignerRoleToKeyIDs := map[string][]string{
   392  		"alice": {"key11"},
   393  		"bob":   {"key71", "key72"},
   394  	}
   395  
   396  	signerRoleToKeyIDs := getDelegationRoleToKeyMap(roles)
   397  	assert.Check(t, is.DeepEqual(expectedSignerRoleToKeyIDs, signerRoleToKeyIDs))
   398  }
   399  
   400  func TestFormatAdminRole(t *testing.T) {
   401  	aliceRole := data.Role{
   402  		RootRole: data.RootRole{
   403  			KeyIDs: []string{"key11"},
   404  		},
   405  		Name: "targets/alice",
   406  	}
   407  	aliceRoleWithSigs := client.RoleWithSignatures{Role: aliceRole, Signatures: nil}
   408  	assert.Check(t, is.Equal("", formatAdminRole(aliceRoleWithSigs)))
   409  
   410  	releasesRole := data.Role{
   411  		RootRole: data.RootRole{
   412  			KeyIDs: []string{"key11"},
   413  		},
   414  		Name: "targets/releases",
   415  	}
   416  	releasesRoleWithSigs := client.RoleWithSignatures{Role: releasesRole, Signatures: nil}
   417  	assert.Check(t, is.Equal("", formatAdminRole(releasesRoleWithSigs)))
   418  
   419  	timestampRole := data.Role{
   420  		RootRole: data.RootRole{
   421  			KeyIDs: []string{"key11"},
   422  		},
   423  		Name: data.CanonicalTimestampRole,
   424  	}
   425  	timestampRoleWithSigs := client.RoleWithSignatures{Role: timestampRole, Signatures: nil}
   426  	assert.Check(t, is.Equal("", formatAdminRole(timestampRoleWithSigs)))
   427  
   428  	snapshotRole := data.Role{
   429  		RootRole: data.RootRole{
   430  			KeyIDs: []string{"key11"},
   431  		},
   432  		Name: data.CanonicalSnapshotRole,
   433  	}
   434  	snapshotRoleWithSigs := client.RoleWithSignatures{Role: snapshotRole, Signatures: nil}
   435  	assert.Check(t, is.Equal("", formatAdminRole(snapshotRoleWithSigs)))
   436  
   437  	rootRole := data.Role{
   438  		RootRole: data.RootRole{
   439  			KeyIDs: []string{"key11"},
   440  		},
   441  		Name: data.CanonicalRootRole,
   442  	}
   443  	rootRoleWithSigs := client.RoleWithSignatures{Role: rootRole, Signatures: nil}
   444  	assert.Check(t, is.Equal("Root Key:\tkey11\n", formatAdminRole(rootRoleWithSigs)))
   445  
   446  	targetsRole := data.Role{
   447  		RootRole: data.RootRole{
   448  			KeyIDs: []string{"key99", "abc", "key11"},
   449  		},
   450  		Name: data.CanonicalTargetsRole,
   451  	}
   452  	targetsRoleWithSigs := client.RoleWithSignatures{Role: targetsRole, Signatures: nil}
   453  	assert.Check(t, is.Equal("Repository Key:\tabc, key11, key99\n", formatAdminRole(targetsRoleWithSigs)))
   454  }
   455  
   456  func TestPrintSignerInfoSortOrder(t *testing.T) {
   457  	roleToKeyIDs := map[string][]string{
   458  		"signer2-foo":  {"B"},
   459  		"signer10-foo": {"C"},
   460  		"signer1-foo":  {"A"},
   461  	}
   462  
   463  	expected := `SIGNER         KEYS
   464  signer1-foo    A
   465  signer2-foo    B
   466  signer10-foo   C
   467  `
   468  	buf := new(bytes.Buffer)
   469  	assert.NilError(t, printSignerInfo(buf, roleToKeyIDs))
   470  	assert.Check(t, is.Equal(expected, buf.String()))
   471  }