github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/trust/inspect_pretty_test.go (about)

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