github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/config/credentials/native_store_test.go (about) 1 package credentials 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "strings" 8 "testing" 9 10 "github.com/docker/cli/cli/config/types" 11 "github.com/docker/docker-credential-helpers/client" 12 "github.com/docker/docker-credential-helpers/credentials" 13 "github.com/pkg/errors" 14 "gotest.tools/v3/assert" 15 is "gotest.tools/v3/assert/cmp" 16 ) 17 18 const ( 19 validServerAddress = "https://index.docker.io/v1" 20 validServerAddress2 = "https://example.com:5002" 21 invalidServerAddress = "https://foobar.example.com" 22 missingCredsAddress = "https://missing.docker.io/v1" 23 ) 24 25 var errCommandExited = errors.Errorf("exited 1") 26 27 // mockCommand simulates interactions between the docker client and a remote 28 // credentials helper. 29 // Unit tests inject this mocked command into the remote to control execution. 30 type mockCommand struct { 31 arg string 32 input io.Reader 33 } 34 35 // Output returns responses from the remote credentials helper. 36 // It mocks those responses based in the input in the mock. 37 func (m *mockCommand) Output() ([]byte, error) { 38 in, err := io.ReadAll(m.input) 39 if err != nil { 40 return nil, err 41 } 42 inS := string(in) 43 44 switch m.arg { 45 case "erase": 46 switch inS { 47 case validServerAddress: 48 return nil, nil 49 default: 50 return []byte("program failed"), errCommandExited 51 } 52 case "get": 53 switch inS { 54 case validServerAddress: 55 return []byte(`{"Username": "foo", "Secret": "bar"}`), nil 56 case validServerAddress2: 57 return []byte(`{"Username": "<token>", "Secret": "abcd1234"}`), nil 58 case missingCredsAddress: 59 return []byte(credentials.NewErrCredentialsNotFound().Error()), errCommandExited 60 case invalidServerAddress: 61 return []byte("program failed"), errCommandExited 62 } 63 case "store": 64 var c credentials.Credentials 65 err := json.NewDecoder(strings.NewReader(inS)).Decode(&c) 66 if err != nil { 67 return []byte("program failed"), errCommandExited 68 } 69 switch c.ServerURL { 70 case validServerAddress: 71 return nil, nil 72 default: 73 return []byte("program failed"), errCommandExited 74 } 75 case "list": 76 return []byte(fmt.Sprintf(`{"%s": "%s", "%s": "%s"}`, validServerAddress, "foo", validServerAddress2, "<token>")), nil 77 } 78 79 return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errCommandExited 80 } 81 82 // Input sets the input to send to a remote credentials helper. 83 func (m *mockCommand) Input(in io.Reader) { 84 m.input = in 85 } 86 87 func mockCommandFn(args ...string) client.Program { 88 return &mockCommand{ 89 arg: args[0], 90 } 91 } 92 93 func TestNativeStoreAddCredentials(t *testing.T) { 94 f := newStore(make(map[string]types.AuthConfig)) 95 s := &nativeStore{ 96 programFunc: mockCommandFn, 97 fileStore: NewFileStore(f), 98 } 99 auth := types.AuthConfig{ 100 Username: "foo", 101 Password: "bar", 102 Email: "foo@example.com", 103 ServerAddress: validServerAddress, 104 } 105 err := s.Store(auth) 106 assert.NilError(t, err) 107 assert.Check(t, is.Len(f.GetAuthConfigs(), 1)) 108 109 actual, ok := f.GetAuthConfigs()[validServerAddress] 110 assert.Check(t, ok) 111 expected := types.AuthConfig{ 112 Email: auth.Email, 113 ServerAddress: auth.ServerAddress, 114 } 115 assert.Check(t, is.DeepEqual(expected, actual)) 116 } 117 118 func TestNativeStoreAddInvalidCredentials(t *testing.T) { 119 f := newStore(make(map[string]types.AuthConfig)) 120 s := &nativeStore{ 121 programFunc: mockCommandFn, 122 fileStore: NewFileStore(f), 123 } 124 err := s.Store(types.AuthConfig{ 125 Username: "foo", 126 Password: "bar", 127 Email: "foo@example.com", 128 ServerAddress: invalidServerAddress, 129 }) 130 assert.ErrorContains(t, err, "program failed") 131 assert.Check(t, is.Len(f.GetAuthConfigs(), 0)) 132 } 133 134 func TestNativeStoreGet(t *testing.T) { 135 f := newStore(map[string]types.AuthConfig{ 136 validServerAddress: { 137 Email: "foo@example.com", 138 }, 139 }) 140 s := &nativeStore{ 141 programFunc: mockCommandFn, 142 fileStore: NewFileStore(f), 143 } 144 actual, err := s.Get(validServerAddress) 145 assert.NilError(t, err) 146 147 expected := types.AuthConfig{ 148 Username: "foo", 149 Password: "bar", 150 Email: "foo@example.com", 151 } 152 assert.Check(t, is.DeepEqual(expected, actual)) 153 } 154 155 func TestNativeStoreGetIdentityToken(t *testing.T) { 156 f := newStore(map[string]types.AuthConfig{ 157 validServerAddress2: { 158 Email: "foo@example2.com", 159 }, 160 }) 161 162 s := &nativeStore{ 163 programFunc: mockCommandFn, 164 fileStore: NewFileStore(f), 165 } 166 actual, err := s.Get(validServerAddress2) 167 assert.NilError(t, err) 168 169 expected := types.AuthConfig{ 170 IdentityToken: "abcd1234", 171 Email: "foo@example2.com", 172 } 173 assert.Check(t, is.DeepEqual(expected, actual)) 174 } 175 176 func TestNativeStoreGetAll(t *testing.T) { 177 f := newStore(map[string]types.AuthConfig{ 178 validServerAddress: { 179 Email: "foo@example.com", 180 }, 181 }) 182 183 s := &nativeStore{ 184 programFunc: mockCommandFn, 185 fileStore: NewFileStore(f), 186 } 187 as, err := s.GetAll() 188 assert.NilError(t, err) 189 assert.Check(t, is.Len(as, 2)) 190 191 if as[validServerAddress].Username != "foo" { 192 t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username) 193 } 194 if as[validServerAddress].Password != "bar" { 195 t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password) 196 } 197 if as[validServerAddress].IdentityToken != "" { 198 t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken) 199 } 200 if as[validServerAddress].Email != "foo@example.com" { 201 t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email) 202 } 203 if as[validServerAddress2].Username != "" { 204 t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username) 205 } 206 if as[validServerAddress2].Password != "" { 207 t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password) 208 } 209 if as[validServerAddress2].IdentityToken != "abcd1234" { 210 t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken) 211 } 212 if as[validServerAddress2].Email != "" { 213 t.Fatalf("expected no email for %s, got %s", validServerAddress2, as[validServerAddress2].Email) 214 } 215 } 216 217 func TestNativeStoreGetMissingCredentials(t *testing.T) { 218 f := newStore(map[string]types.AuthConfig{ 219 validServerAddress: { 220 Email: "foo@example.com", 221 }, 222 }) 223 224 s := &nativeStore{ 225 programFunc: mockCommandFn, 226 fileStore: NewFileStore(f), 227 } 228 _, err := s.Get(missingCredsAddress) 229 assert.NilError(t, err) 230 } 231 232 func TestNativeStoreGetInvalidAddress(t *testing.T) { 233 f := newStore(map[string]types.AuthConfig{ 234 validServerAddress: { 235 Email: "foo@example.com", 236 }, 237 }) 238 239 s := &nativeStore{ 240 programFunc: mockCommandFn, 241 fileStore: NewFileStore(f), 242 } 243 _, err := s.Get(invalidServerAddress) 244 assert.ErrorContains(t, err, "program failed") 245 } 246 247 func TestNativeStoreErase(t *testing.T) { 248 f := newStore(map[string]types.AuthConfig{ 249 validServerAddress: { 250 Email: "foo@example.com", 251 }, 252 }) 253 254 s := &nativeStore{ 255 programFunc: mockCommandFn, 256 fileStore: NewFileStore(f), 257 } 258 err := s.Erase(validServerAddress) 259 assert.NilError(t, err) 260 assert.Check(t, is.Len(f.GetAuthConfigs(), 0)) 261 } 262 263 func TestNativeStoreEraseInvalidAddress(t *testing.T) { 264 f := newStore(map[string]types.AuthConfig{ 265 validServerAddress: { 266 Email: "foo@example.com", 267 }, 268 }) 269 270 s := &nativeStore{ 271 programFunc: mockCommandFn, 272 fileStore: NewFileStore(f), 273 } 274 err := s.Erase(invalidServerAddress) 275 assert.ErrorContains(t, err, "program failed") 276 }