oras.land/oras-go/v2@v2.5.1-0.20240520045656-aef90e4d04c4/registry/remote/credentials/native_store_test.go (about)

     1  /*
     2  Copyright The ORAS Authors.
     3  Licensed under the Apache License, Version 2.0 (the "License");
     4  you may not use this file except in compliance with the License.
     5  You may obtain a copy of the License at
     6  
     7  http://www.apache.org/licenses/LICENSE-2.0
     8  
     9  Unless required by applicable law or agreed to in writing, software
    10  distributed under the License is distributed on an "AS IS" BASIS,
    11  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  See the License for the specific language governing permissions and
    13  limitations under the License.
    14  */
    15  
    16  package credentials
    17  
    18  import (
    19  	"bytes"
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"strings"
    25  	"testing"
    26  
    27  	"oras.land/oras-go/v2/registry/remote/auth"
    28  	"oras.land/oras-go/v2/registry/remote/credentials/trace"
    29  )
    30  
    31  const (
    32  	basicAuthHost     = "localhost:2333"
    33  	bearerAuthHost    = "localhost:666"
    34  	exeErrorHost      = "localhost:500/exeError"
    35  	jsonErrorHost     = "localhost:500/jsonError"
    36  	noCredentialsHost = "localhost:404"
    37  	traceHost         = "localhost:808"
    38  	testUsername      = "test_username"
    39  	testPassword      = "test_password"
    40  	testRefreshToken  = "test_token"
    41  )
    42  
    43  var (
    44  	errCommandExited       = fmt.Errorf("exited with error")
    45  	errExecute             = fmt.Errorf("Execute failed")
    46  	errCredentialsNotFound = fmt.Errorf(errCredentialsNotFoundMessage)
    47  )
    48  
    49  // testExecuter implements the Executer interface for testing purpose.
    50  // It simulates interactions between the docker client and a remote
    51  // credentials helper.
    52  type testExecuter struct{}
    53  
    54  // Execute mocks the behavior of a credential helper binary. It returns responses
    55  // and errors based on the input.
    56  func (e *testExecuter) Execute(ctx context.Context, input io.Reader, action string) ([]byte, error) {
    57  	in, err := io.ReadAll(input)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  	inS := string(in)
    62  	switch action {
    63  	case "get":
    64  		switch inS {
    65  		case basicAuthHost:
    66  			return []byte(`{"Username": "test_username", "Secret": "test_password"}`), nil
    67  		case bearerAuthHost:
    68  			return []byte(`{"Username": "<token>", "Secret": "test_token"}`), nil
    69  		case exeErrorHost:
    70  			return []byte("Execute failed"), errExecute
    71  		case jsonErrorHost:
    72  			return []byte("json.Unmarshal failed"), nil
    73  		case noCredentialsHost:
    74  			return []byte("credentials not found"), errCredentialsNotFound
    75  		case traceHost:
    76  			traceHook := trace.ContextExecutableTrace(ctx)
    77  			if traceHook != nil {
    78  				if traceHook.ExecuteStart != nil {
    79  					traceHook.ExecuteStart("testExecuter", "get")
    80  				}
    81  				if traceHook.ExecuteDone != nil {
    82  					traceHook.ExecuteDone("testExecuter", "get", nil)
    83  				}
    84  			}
    85  			return []byte(`{"Username": "test_username", "Secret": "test_password"}`), nil
    86  		default:
    87  			return []byte("program failed"), errCommandExited
    88  		}
    89  	case "store":
    90  		var c dockerCredentials
    91  		err := json.NewDecoder(strings.NewReader(inS)).Decode(&c)
    92  		if err != nil {
    93  			return []byte("program failed"), errCommandExited
    94  		}
    95  		switch c.ServerURL {
    96  		case basicAuthHost, bearerAuthHost, exeErrorHost:
    97  			return nil, nil
    98  		case traceHost:
    99  			traceHook := trace.ContextExecutableTrace(ctx)
   100  			if traceHook != nil {
   101  				if traceHook.ExecuteStart != nil {
   102  					traceHook.ExecuteStart("testExecuter", "store")
   103  				}
   104  				if traceHook.ExecuteDone != nil {
   105  					traceHook.ExecuteDone("testExecuter", "store", nil)
   106  				}
   107  			}
   108  			return nil, nil
   109  		default:
   110  			return []byte("program failed"), errCommandExited
   111  		}
   112  	case "erase":
   113  		switch inS {
   114  		case basicAuthHost, bearerAuthHost:
   115  			return nil, nil
   116  		case traceHost:
   117  			traceHook := trace.ContextExecutableTrace(ctx)
   118  			if traceHook != nil {
   119  				if traceHook.ExecuteStart != nil {
   120  					traceHook.ExecuteStart("testExecuter", "erase")
   121  				}
   122  				if traceHook.ExecuteDone != nil {
   123  					traceHook.ExecuteDone("testExecuter", "erase", nil)
   124  				}
   125  			}
   126  			return nil, nil
   127  		default:
   128  			return []byte("program failed"), errCommandExited
   129  		}
   130  	}
   131  	return []byte(fmt.Sprintf("unknown argument %q with %q", action, inS)), errCommandExited
   132  }
   133  
   134  func TestNativeStore_interface(t *testing.T) {
   135  	var ns interface{} = &nativeStore{}
   136  	if _, ok := ns.(Store); !ok {
   137  		t.Error("&NativeStore{} does not conform Store")
   138  	}
   139  }
   140  
   141  func TestNativeStore_basicAuth(t *testing.T) {
   142  	ns := &nativeStore{
   143  		&testExecuter{},
   144  	}
   145  	// Put
   146  	err := ns.Put(context.Background(), basicAuthHost, auth.Credential{Username: testUsername, Password: testPassword})
   147  	if err != nil {
   148  		t.Fatalf("basic auth test ns.Put fails: %v", err)
   149  	}
   150  	// Get
   151  	cred, err := ns.Get(context.Background(), basicAuthHost)
   152  	if err != nil {
   153  		t.Fatalf("basic auth test ns.Get fails: %v", err)
   154  	}
   155  	if cred.Username != testUsername {
   156  		t.Fatal("incorrect username")
   157  	}
   158  	if cred.Password != testPassword {
   159  		t.Fatal("incorrect password")
   160  	}
   161  	// Delete
   162  	err = ns.Delete(context.Background(), basicAuthHost)
   163  	if err != nil {
   164  		t.Fatalf("basic auth test ns.Delete fails: %v", err)
   165  	}
   166  }
   167  
   168  func TestNativeStore_refreshToken(t *testing.T) {
   169  	ns := &nativeStore{
   170  		&testExecuter{},
   171  	}
   172  	// Put
   173  	err := ns.Put(context.Background(), bearerAuthHost, auth.Credential{RefreshToken: testRefreshToken})
   174  	if err != nil {
   175  		t.Fatalf("refresh token test ns.Put fails: %v", err)
   176  	}
   177  	// Get
   178  	cred, err := ns.Get(context.Background(), bearerAuthHost)
   179  	if err != nil {
   180  		t.Fatalf("refresh token test ns.Get fails: %v", err)
   181  	}
   182  	if cred.Username != "" {
   183  		t.Fatalf("expect username to be empty, got %s", cred.Username)
   184  	}
   185  	if cred.RefreshToken != testRefreshToken {
   186  		t.Fatal("incorrect refresh token")
   187  	}
   188  	// Delete
   189  	err = ns.Delete(context.Background(), basicAuthHost)
   190  	if err != nil {
   191  		t.Fatalf("refresh token test ns.Delete fails: %v", err)
   192  	}
   193  }
   194  
   195  func TestNativeStore_errorHandling(t *testing.T) {
   196  	ns := &nativeStore{
   197  		&testExecuter{},
   198  	}
   199  	// Get Error: Execute error
   200  	_, err := ns.Get(context.Background(), exeErrorHost)
   201  	if err != errExecute {
   202  		t.Fatalf("got error: %v, should get exeErr", err)
   203  	}
   204  	// Get Error: json.Unmarshal
   205  	_, err = ns.Get(context.Background(), jsonErrorHost)
   206  	if err == nil {
   207  		t.Fatalf("should get error from json.Unmarshal")
   208  	}
   209  	// Get: Should not return error when credentials are not found
   210  	_, err = ns.Get(context.Background(), noCredentialsHost)
   211  	if err != nil {
   212  		t.Fatalf("should not get error when no credentials are found")
   213  	}
   214  }
   215  
   216  func TestNewDefaultNativeStore(t *testing.T) {
   217  	defaultHelper := getDefaultHelperSuffix()
   218  	wantOK := (defaultHelper != "")
   219  
   220  	if _, ok := NewDefaultNativeStore(); ok != wantOK {
   221  		t.Errorf("NewDefaultNativeStore() = %v, want %v", ok, wantOK)
   222  	}
   223  }
   224  
   225  func TestNativeStore_trace(t *testing.T) {
   226  	ns := &nativeStore{
   227  		&testExecuter{},
   228  	}
   229  	// create trace hooks that write to buffer
   230  	buffer := bytes.Buffer{}
   231  	traceHook := &trace.ExecutableTrace{
   232  		ExecuteStart: func(executableName string, action string) {
   233  			buffer.WriteString(fmt.Sprintf("test trace, start the execution of executable %s with action %s ", executableName, action))
   234  		},
   235  		ExecuteDone: func(executableName string, action string, err error) {
   236  			buffer.WriteString(fmt.Sprintf("test trace, completed the execution of executable %s with action %s and got err %v", executableName, action, err))
   237  		},
   238  	}
   239  	ctx := trace.WithExecutableTrace(context.Background(), traceHook)
   240  	// Test ns.Put trace
   241  	err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
   242  	if err != nil {
   243  		t.Fatalf("trace test ns.Put fails: %v", err)
   244  	}
   245  	bufferContent := buffer.String()
   246  	if bufferContent != "test trace, start the execution of executable testExecuter with action store test trace, completed the execution of executable testExecuter with action store and got err <nil>" {
   247  		t.Fatalf("incorrect buffer content: %s", bufferContent)
   248  	}
   249  	buffer.Reset()
   250  	// Test ns.Get trace
   251  	_, err = ns.Get(ctx, traceHost)
   252  	if err != nil {
   253  		t.Fatalf("trace test ns.Get fails: %v", err)
   254  	}
   255  	bufferContent = buffer.String()
   256  	if bufferContent != "test trace, start the execution of executable testExecuter with action get test trace, completed the execution of executable testExecuter with action get and got err <nil>" {
   257  		t.Fatalf("incorrect buffer content: %s", bufferContent)
   258  	}
   259  	buffer.Reset()
   260  	// Test ns.Delete trace
   261  	err = ns.Delete(ctx, traceHost)
   262  	if err != nil {
   263  		t.Fatalf("trace test ns.Delete fails: %v", err)
   264  	}
   265  	bufferContent = buffer.String()
   266  	if bufferContent != "test trace, start the execution of executable testExecuter with action erase test trace, completed the execution of executable testExecuter with action erase and got err <nil>" {
   267  		t.Fatalf("incorrect buffer content: %s", bufferContent)
   268  	}
   269  }
   270  
   271  // This test ensures that a nil trace will not cause an error.
   272  func TestNativeStore_noTrace(t *testing.T) {
   273  	ns := &nativeStore{
   274  		&testExecuter{},
   275  	}
   276  	// Put
   277  	err := ns.Put(context.Background(), traceHost, auth.Credential{Username: testUsername, Password: testPassword})
   278  	if err != nil {
   279  		t.Fatalf("basic auth test ns.Put fails: %v", err)
   280  	}
   281  	// Get
   282  	cred, err := ns.Get(context.Background(), traceHost)
   283  	if err != nil {
   284  		t.Fatalf("basic auth test ns.Get fails: %v", err)
   285  	}
   286  	if cred.Username != testUsername {
   287  		t.Fatal("incorrect username")
   288  	}
   289  	if cred.Password != testPassword {
   290  		t.Fatal("incorrect password")
   291  	}
   292  	// Delete
   293  	err = ns.Delete(context.Background(), traceHost)
   294  	if err != nil {
   295  		t.Fatalf("basic auth test ns.Delete fails: %v", err)
   296  	}
   297  }
   298  
   299  // This test ensures that an empty trace will not cause an error.
   300  func TestNativeStore_emptyTrace(t *testing.T) {
   301  	ns := &nativeStore{
   302  		&testExecuter{},
   303  	}
   304  	traceHook := &trace.ExecutableTrace{}
   305  	ctx := trace.WithExecutableTrace(context.Background(), traceHook)
   306  	// Put
   307  	err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
   308  	if err != nil {
   309  		t.Fatalf("basic auth test ns.Put fails: %v", err)
   310  	}
   311  	// Get
   312  	cred, err := ns.Get(ctx, traceHost)
   313  	if err != nil {
   314  		t.Fatalf("basic auth test ns.Get fails: %v", err)
   315  	}
   316  	if cred.Username != testUsername {
   317  		t.Fatal("incorrect username")
   318  	}
   319  	if cred.Password != testPassword {
   320  		t.Fatal("incorrect password")
   321  	}
   322  	// Delete
   323  	err = ns.Delete(ctx, traceHost)
   324  	if err != nil {
   325  		t.Fatalf("basic auth test ns.Delete fails: %v", err)
   326  	}
   327  }
   328  
   329  func TestNativeStore_multipleTrace(t *testing.T) {
   330  	ns := &nativeStore{
   331  		&testExecuter{},
   332  	}
   333  	// create trace hooks that write to buffer
   334  	buffer := bytes.Buffer{}
   335  	trace1 := &trace.ExecutableTrace{
   336  		ExecuteStart: func(executableName string, action string) {
   337  			buffer.WriteString(fmt.Sprintf("trace 1 start %s, %s ", executableName, action))
   338  		},
   339  		ExecuteDone: func(executableName string, action string, err error) {
   340  			buffer.WriteString(fmt.Sprintf("trace 1 done %s, %s, %v ", executableName, action, err))
   341  		},
   342  	}
   343  	ctx := context.Background()
   344  	ctx = trace.WithExecutableTrace(ctx, trace1)
   345  	trace2 := &trace.ExecutableTrace{
   346  		ExecuteStart: func(executableName string, action string) {
   347  			buffer.WriteString(fmt.Sprintf("trace 2 start %s, %s ", executableName, action))
   348  		},
   349  		ExecuteDone: func(executableName string, action string, err error) {
   350  			buffer.WriteString(fmt.Sprintf("trace 2 done %s, %s, %v ", executableName, action, err))
   351  		},
   352  	}
   353  	ctx = trace.WithExecutableTrace(ctx, trace2)
   354  	trace3 := &trace.ExecutableTrace{}
   355  	ctx = trace.WithExecutableTrace(ctx, trace3)
   356  	// Test ns.Put trace
   357  	err := ns.Put(ctx, traceHost, auth.Credential{Username: testUsername, Password: testPassword})
   358  	if err != nil {
   359  		t.Fatalf("trace test ns.Put fails: %v", err)
   360  	}
   361  	bufferContent := buffer.String()
   362  	if bufferContent != "trace 2 start testExecuter, store trace 1 start testExecuter, store trace 2 done testExecuter, store, <nil> trace 1 done testExecuter, store, <nil> " {
   363  		t.Fatalf("incorrect buffer content: %s", bufferContent)
   364  	}
   365  	buffer.Reset()
   366  	// Test ns.Get trace
   367  	_, err = ns.Get(ctx, traceHost)
   368  	if err != nil {
   369  		t.Fatalf("trace test ns.Get fails: %v", err)
   370  	}
   371  	bufferContent = buffer.String()
   372  	if bufferContent != "trace 2 start testExecuter, get trace 1 start testExecuter, get trace 2 done testExecuter, get, <nil> trace 1 done testExecuter, get, <nil> " {
   373  		t.Fatalf("incorrect buffer content: %s", bufferContent)
   374  	}
   375  	buffer.Reset()
   376  	// Test ns.Delete trace
   377  	err = ns.Delete(ctx, traceHost)
   378  	if err != nil {
   379  		t.Fatalf("trace test ns.Delete fails: %v", err)
   380  	}
   381  	bufferContent = buffer.String()
   382  	if bufferContent != "trace 2 start testExecuter, erase trace 1 start testExecuter, erase trace 2 done testExecuter, erase, <nil> trace 1 done testExecuter, erase, <nil> " {
   383  		t.Fatalf("incorrect buffer content: %s", bufferContent)
   384  	}
   385  }