github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/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 ServerAddress: validServerAddress, 152 } 153 assert.Check(t, is.DeepEqual(expected, actual)) 154 } 155 156 func TestNativeStoreGetIdentityToken(t *testing.T) { 157 f := newStore(map[string]types.AuthConfig{ 158 validServerAddress2: { 159 Email: "foo@example2.com", 160 }, 161 }) 162 163 s := &nativeStore{ 164 programFunc: mockCommandFn, 165 fileStore: NewFileStore(f), 166 } 167 actual, err := s.Get(validServerAddress2) 168 assert.NilError(t, err) 169 170 expected := types.AuthConfig{ 171 IdentityToken: "abcd1234", 172 Email: "foo@example2.com", 173 ServerAddress: validServerAddress2, 174 } 175 assert.Check(t, is.DeepEqual(expected, actual)) 176 } 177 178 func TestNativeStoreGetAll(t *testing.T) { 179 f := newStore(map[string]types.AuthConfig{ 180 validServerAddress: { 181 Email: "foo@example.com", 182 }, 183 }) 184 185 s := &nativeStore{ 186 programFunc: mockCommandFn, 187 fileStore: NewFileStore(f), 188 } 189 as, err := s.GetAll() 190 assert.NilError(t, err) 191 assert.Check(t, is.Len(as, 2)) 192 193 if as[validServerAddress].Username != "foo" { 194 t.Fatalf("expected username `foo` for %s, got %s", validServerAddress, as[validServerAddress].Username) 195 } 196 if as[validServerAddress].Password != "bar" { 197 t.Fatalf("expected password `bar` for %s, got %s", validServerAddress, as[validServerAddress].Password) 198 } 199 if as[validServerAddress].IdentityToken != "" { 200 t.Fatalf("expected identity to be empty for %s, got %s", validServerAddress, as[validServerAddress].IdentityToken) 201 } 202 if as[validServerAddress].Email != "foo@example.com" { 203 t.Fatalf("expected email `foo@example.com` for %s, got %s", validServerAddress, as[validServerAddress].Email) 204 } 205 if as[validServerAddress2].Username != "" { 206 t.Fatalf("expected username to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Username) 207 } 208 if as[validServerAddress2].Password != "" { 209 t.Fatalf("expected password to be empty for %s, got %s", validServerAddress2, as[validServerAddress2].Password) 210 } 211 if as[validServerAddress2].IdentityToken != "abcd1234" { 212 t.Fatalf("expected identity token `abcd1324` for %s, got %s", validServerAddress2, as[validServerAddress2].IdentityToken) 213 } 214 if as[validServerAddress2].Email != "" { 215 t.Fatalf("expected no email for %s, got %s", validServerAddress2, as[validServerAddress2].Email) 216 } 217 } 218 219 func TestNativeStoreGetMissingCredentials(t *testing.T) { 220 f := newStore(map[string]types.AuthConfig{ 221 validServerAddress: { 222 Email: "foo@example.com", 223 }, 224 }) 225 226 s := &nativeStore{ 227 programFunc: mockCommandFn, 228 fileStore: NewFileStore(f), 229 } 230 _, err := s.Get(missingCredsAddress) 231 assert.NilError(t, err) 232 } 233 234 func TestNativeStoreGetInvalidAddress(t *testing.T) { 235 f := newStore(map[string]types.AuthConfig{ 236 validServerAddress: { 237 Email: "foo@example.com", 238 }, 239 }) 240 241 s := &nativeStore{ 242 programFunc: mockCommandFn, 243 fileStore: NewFileStore(f), 244 } 245 _, err := s.Get(invalidServerAddress) 246 assert.ErrorContains(t, err, "program failed") 247 } 248 249 func TestNativeStoreErase(t *testing.T) { 250 f := newStore(map[string]types.AuthConfig{ 251 validServerAddress: { 252 Email: "foo@example.com", 253 }, 254 }) 255 256 s := &nativeStore{ 257 programFunc: mockCommandFn, 258 fileStore: NewFileStore(f), 259 } 260 err := s.Erase(validServerAddress) 261 assert.NilError(t, err) 262 assert.Check(t, is.Len(f.GetAuthConfigs(), 0)) 263 } 264 265 func TestNativeStoreEraseInvalidAddress(t *testing.T) { 266 f := newStore(map[string]types.AuthConfig{ 267 validServerAddress: { 268 Email: "foo@example.com", 269 }, 270 }) 271 272 s := &nativeStore{ 273 programFunc: mockCommandFn, 274 fileStore: NewFileStore(f), 275 } 276 err := s.Erase(invalidServerAddress) 277 assert.ErrorContains(t, err, "program failed") 278 }