github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/dsref/spec/resolve.go (about)

     1  package spec
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"net/url"
     8  	"testing"
     9  	"time"
    10  
    11  	"github.com/google/go-cmp/cmp"
    12  	"github.com/multiformats/go-multiaddr"
    13  	"github.com/qri-io/dataset"
    14  	"github.com/qri-io/qfs"
    15  	testkeys "github.com/qri-io/qri/auth/key/test"
    16  	"github.com/qri-io/qri/dsref"
    17  	"github.com/qri-io/qri/event"
    18  	"github.com/qri-io/qri/logbook"
    19  	"github.com/qri-io/qri/logbook/oplog"
    20  	"github.com/qri-io/qri/profile"
    21  )
    22  
    23  // PutRefFunc adds a reference to a system that retains references
    24  // PutRefFunc is required to run the ResolverSpec test, when called the Resolver
    25  // should retain the reference for later retrieval by the spec test. PutRefFunc
    26  // also passes the author & oplog that back the reference
    27  type PutRefFunc func(ref dsref.Ref, author *profile.Profile, log *oplog.Log) error
    28  
    29  // AssertResolverSpec confirms the expected behaviour of a dsref.Resolver
    30  // Interface implementation. In addition to this test passing, implementations
    31  // MUST be nil-callable. Please add a nil-callable test for each implementation
    32  func AssertResolverSpec(t *testing.T, r dsref.Resolver, putFunc PutRefFunc) {
    33  	var (
    34  		ctx              = context.Background()
    35  		username, dsname = "resolve_spec_test_peer", "stored_ref_dataset"
    36  		headPath         = "/ipfs/QmeXaMpLe"
    37  		journal          = ForeignLogbook(t, username)
    38  	)
    39  
    40  	profileID := journal.Owner().ID.Encode()
    41  	initID, log, err := GenerateExampleOplog(ctx, journal, dsname, headPath)
    42  	if err != nil {
    43  		t.Fatal(err)
    44  	}
    45  
    46  	expectRef := dsref.Ref{
    47  		InitID:    initID,
    48  		ProfileID: profileID,
    49  		Username:  username,
    50  		Name:      dsname,
    51  		Path:      headPath,
    52  	}
    53  
    54  	t.Run("dsrefResolverSpec", func(t *testing.T) {
    55  		if err := putFunc(expectRef, journal.Owner(), log); err != nil {
    56  			t.Fatalf("put ref failed: %s", err)
    57  		}
    58  
    59  		_, err := r.ResolveRef(ctx, &dsref.Ref{Username: "username", Name: "does_not_exist"})
    60  		if err == nil {
    61  			t.Errorf("expected error resolving nonexistent reference, got none")
    62  		} else if !errors.Is(err, dsref.ErrRefNotFound) {
    63  			t.Errorf("expected standard error resolving nonexistent ref: %q, got: %q", dsref.ErrRefNotFound, err)
    64  		}
    65  
    66  		resolveMe := dsref.Ref{
    67  			Username: username,
    68  			Name:     dsname,
    69  		}
    70  
    71  		addr, err := r.ResolveRef(ctx, &resolveMe)
    72  		if err != nil {
    73  			t.Error(err)
    74  		}
    75  
    76  		if addr != "" {
    77  			if _, err := multiaddr.NewMultiaddr(addr); err != nil {
    78  				if _, urlParseErr := url.Parse(addr); urlParseErr == nil {
    79  					t.Logf("warning: non-empty source must be a valid multiaddr, but returned a url: %s\nURLS will not be permitted in the future", addr)
    80  				} else {
    81  					t.Errorf("non-empty source must be a valid multiaddr.\nmultiaddr parse error: %s", err)
    82  				}
    83  			}
    84  		}
    85  
    86  		if diff := cmp.Diff(expectRef, resolveMe); diff != "" {
    87  			t.Errorf("result mismatch. (-want +got):\n%s", diff)
    88  		}
    89  
    90  		resolveMe = dsref.Ref{
    91  			Username: username,
    92  			Name:     dsname,
    93  			Path:     "/ill_provide_the_path_thank_you_very_much",
    94  		}
    95  
    96  		expectRef = dsref.Ref{
    97  			Username:  username,
    98  			Name:      dsname,
    99  			ProfileID: profileID,
   100  			Path:      "/ill_provide_the_path_thank_you_very_much",
   101  			InitID:    expectRef.InitID,
   102  		}
   103  
   104  		addr, err = r.ResolveRef(ctx, &resolveMe)
   105  		if err != nil {
   106  			t.Error(err)
   107  		}
   108  
   109  		if addr != "" {
   110  			if _, err := multiaddr.NewMultiaddr(addr); err != nil {
   111  				if _, urlParseErr := url.Parse(addr); urlParseErr == nil {
   112  					t.Logf("warning: non-empty source must be a valid multiaddr, but returned a url: %s\nURLS will not be permitted in the future", addr)
   113  				} else {
   114  					t.Errorf("non-empty source must be a valid multiaddr.\nmultiaddr parse error: %s", err)
   115  				}
   116  			}
   117  		}
   118  
   119  		if diff := cmp.Diff(expectRef, resolveMe); diff != "" {
   120  			t.Errorf("provided path result mismatch. (-want +got):\n%s", diff)
   121  		}
   122  
   123  		// resolveMe = dsref.Ref{
   124  		// 	Username: username,
   125  		// 	Name:     dsname,
   126  		// 	InitID:   initID,
   127  		// }
   128  
   129  		// expectRef = dsref.Ref{
   130  		// 	Username:  username,
   131  		// 	Name:      dsname,
   132  		// 	ProfileID: profileID,
   133  		// 	Path:      headPath,
   134  		// 	InitID:    initID,
   135  		// }
   136  
   137  		// _, err = r.ResolveRef(ctx, &resolveMe)
   138  		// if err != nil {
   139  		// 	t.Error(err)
   140  		// }
   141  		// if resolveMe.InitID != expectRef.InitID {
   142  		// 	t.Errorf("providing an InitID result mismatch. want: %q\ngot:  %q", expectRef.InitID, resolveMe.InitID)
   143  		// }
   144  		// if diff := cmp.Diff(expectRef, resolveMe); diff != "" {
   145  		// 	t.Errorf("provided InitID result mismatch. (-want +got):\n%s", diff)
   146  		// }
   147  
   148  		// providing just an initID MUST populate the alias (human side) of a
   149  		// reference.
   150  		resolveMe = dsref.Ref{
   151  			InitID: initID,
   152  
   153  			// erroneous fields need to be be overwritten
   154  			Username: "no not good",
   155  			Name:     "incorrect",
   156  			Path:     "nope_not_right",
   157  		}
   158  
   159  		expectRef = dsref.Ref{
   160  			Username:  username,
   161  			Name:      dsname,
   162  			ProfileID: profileID,
   163  			Path:      headPath,
   164  			InitID:    initID,
   165  		}
   166  
   167  		_, err = r.ResolveRef(ctx, &resolveMe)
   168  		if err != nil {
   169  			t.Error(err)
   170  		}
   171  		if resolveMe.InitID != expectRef.InitID {
   172  			t.Errorf("providing InitID-only result mismatch. want: %q\ngot:  %q", expectRef.InitID, resolveMe.InitID)
   173  		}
   174  		if diff := cmp.Diff(expectRef, resolveMe); diff != "" {
   175  			t.Errorf("provided InitID-only result mismatch. (-want +got):\n%s", diff)
   176  		}
   177  
   178  		// providing a missing initID MUST return ErrRefNotFound or a wrap thereof
   179  		resolveMe = dsref.Ref{
   180  			InitID: "nope_not_here",
   181  		}
   182  
   183  		if _, err = r.ResolveRef(ctx, &resolveMe); !errors.Is(err, dsref.ErrRefNotFound) {
   184  			t.Errorf("resolving a missing initID must return ErrRefNotFound or a wrap thereof.\ngot: %s", err)
   185  		}
   186  
   187  		// TODO(b5): need to add a test that confirms ResolveRef CANNOT return
   188  		// paths outside of logbook HEAD. Subsystems that store references to
   189  		// mutable paths (eg: FSI links) cannot be set as reference resolution
   190  	})
   191  }
   192  
   193  // ErrResolversInconsistent indicates two resolvers honored a
   194  // resolution request, but gave differing responses
   195  var ErrResolversInconsistent = fmt.Errorf("inconsistent resolvers")
   196  
   197  // InconsistentResolvers confirms two resolvers have different responses for
   198  // the same reference
   199  // this function will not fail the test on error, only write warnings via t.Log
   200  func InconsistentResolvers(t *testing.T, ref dsref.Ref, a, b dsref.Resolver) error {
   201  	err := ConsistentResolvers(t, ref, a, b)
   202  	if err == nil {
   203  		return fmt.Errorf("resolvers are consistent, expected inconsitency")
   204  	}
   205  	if errors.Is(err, ErrResolversInconsistent) {
   206  		return nil
   207  	}
   208  
   209  	return err
   210  }
   211  
   212  // ConsistentResolvers checks that a set of resolvers return equivalent values
   213  // for a given reference
   214  // this function will not fail the test on error, only write warnings via t.Log
   215  func ConsistentResolvers(t *testing.T, ref dsref.Ref, resolvers ...dsref.Resolver) error {
   216  	var (
   217  		ctx      = context.Background()
   218  		err      error
   219  		resolved *dsref.Ref
   220  	)
   221  
   222  	for i, r := range resolvers {
   223  		got := ref.Copy()
   224  		if _, resolveErr := r.ResolveRef(ctx, &got); resolveErr != nil {
   225  			// only legal error return value is dsref.ErrRefNotFound
   226  			if resolveErr != dsref.ErrRefNotFound {
   227  				return fmt.Errorf("unexpected error checking consistency with resolver %d (%v): %w", i, r, resolveErr)
   228  			}
   229  
   230  			if err == nil && resolved == nil {
   231  				err = resolveErr
   232  				continue
   233  			} else if resolved != nil {
   234  				return fmt.Errorf("%w: index %d (%v) doesn't have reference that was found elsewhere", ErrResolversInconsistent, i, r)
   235  			}
   236  			// err and resolveErr are both ErrNotFound
   237  			continue
   238  		}
   239  
   240  		if resolved == nil {
   241  			resolved = &got
   242  			continue
   243  		} else if resolved.Equals(got) {
   244  			continue
   245  		}
   246  
   247  		return fmt.Errorf("%w: index %d (%v): %s != %s", ErrResolversInconsistent, i, r, resolved, got)
   248  	}
   249  
   250  	return nil
   251  }
   252  
   253  // ForeignLogbook creates a logbook to use as an external source of oplog data
   254  func ForeignLogbook(t *testing.T, username string) *logbook.Book {
   255  	t.Helper()
   256  
   257  	ms := qfs.NewMemFS()
   258  	pk := testkeys.GetKeyData(9).PrivKey
   259  	pro, err := profile.NewSparsePKProfile(username, pk)
   260  	if err != nil {
   261  		t.Fatal(err)
   262  	}
   263  	journal, err := logbook.NewJournal(*pro, event.NilBus, ms, "/mem/logbook.qfb")
   264  	if err != nil {
   265  		t.Fatal(err)
   266  	}
   267  
   268  	return journal
   269  }
   270  
   271  // GenerateExampleOplog makes an example dataset history on a given journal,
   272  // returning the initID and a signed log
   273  func GenerateExampleOplog(ctx context.Context, journal *logbook.Book, dsname, headPath string) (string, *oplog.Log, error) {
   274  	author := journal.Owner()
   275  	initID, err := journal.WriteDatasetInit(ctx, author, dsname)
   276  	if err != nil {
   277  		return "", nil, err
   278  	}
   279  
   280  	err = journal.WriteVersionSave(ctx, author, &dataset.Dataset{
   281  		ID:       initID,
   282  		Peername: author.Peername,
   283  		Name:     dsname,
   284  		Commit: &dataset.Commit{
   285  			Timestamp: time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC),
   286  			Title:     "initial commit",
   287  		},
   288  		Path:         headPath,
   289  		PreviousPath: "",
   290  	}, nil)
   291  	if err != nil {
   292  		return "", nil, err
   293  	}
   294  
   295  	lg, err := journal.UserDatasetBranchesLog(ctx, initID)
   296  	if err != nil {
   297  		return "", nil, err
   298  	}
   299  	if err := lg.Sign(author.PrivKey); err != nil {
   300  		return "", nil, err
   301  	}
   302  
   303  	return initID, lg, err
   304  }