github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/teams/loader_chain_test.go (about)

     1  package teams
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"reflect"
    11  	"strings"
    12  	"testing"
    13  
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/keybase/client/go/libkb"
    17  	"github.com/keybase/client/go/protocol/keybase1"
    18  	"github.com/keybase/client/go/sig3"
    19  	"github.com/keybase/client/go/teams/hidden"
    20  	storage "github.com/keybase/client/go/teams/storage"
    21  
    22  	jsonw "github.com/keybase/go-jsonw"
    23  )
    24  
    25  type TestCase struct {
    26  	FileName string
    27  	Log      []string `json:"log"`
    28  	Teams    map[string] /*team label*/ struct {
    29  		ID           keybase1.TeamID   `json:"id"`
    30  		Links        []json.RawMessage `json:"links"`
    31  		Hidden       []sig3.ExportJSON `json:"hidden"`
    32  		TeamKeyBoxes []struct {
    33  			ChainType keybase1.SeqType      `json:"chain_type"`
    34  			Seqno     keybase1.Seqno        `json:"seqno"` // the team seqno at which the box was added
    35  			TeamBox   TeamBox               `json:"box"`
    36  			Prev      *prevKeySealedEncoded `json:"prev"`
    37  		} `json:"team_key_boxes"`
    38  		RatchetBlindingKeySet *hidden.RatchetBlindingKeySet `json:"ratchet_blinding_keys"`
    39  	} `json:"teams"`
    40  	Users map[string] /*user label*/ struct {
    41  		UID               keybase1.UID   `json:"uid"`
    42  		EldestSeqno       keybase1.Seqno `json:"eldest_seqno"`
    43  		LinkMap           linkMapT       `json:"link_map"`
    44  		PerUserKeySecrets map[keybase1.Seqno]string/*hex of PerUserKeySeed*/ `json:"puk_secrets"`
    45  	} `json:"users"`
    46  	KeyOwners        map[keybase1.KID] /*kid*/ string/*username*/ `json:"key_owners"`
    47  	KeyPubKeyV2NaCls map[keybase1.KID]json.RawMessage `json:"key_pubkeyv2nacls"`
    48  	TeamMerkle       map[string] /*TeamID AND TeamID-seqno:Seqno*/ struct {
    49  		Seqno      keybase1.Seqno             `json:"seqno"`
    50  		LinkID     keybase1.LinkID            `json:"link_id"`
    51  		HiddenResp libkb.MerkleHiddenResponse `json:"hidden_response"`
    52  	} `json:"team_merkle"`
    53  	MerkleTriples map[string] /*LeafID-HashMeta*/ libkb.MerkleTriple `json:"merkle_triples"`
    54  
    55  	// A session is a series of Load operations sharing a cache.
    56  	Sessions []struct {
    57  		Loads []TestCaseLoad `json:"loads"`
    58  	} `json:"sessions"`
    59  
    60  	Skip bool `json:"skip"`
    61  	Todo bool `json:"todo"`
    62  }
    63  
    64  type TestCaseLoad struct {
    65  	// Client behavior
    66  	NeedAdmin         bool `json:"need_admin"`
    67  	NeedKeyGeneration int  `json:"need_keygen"`
    68  
    69  	// Server behavior
    70  	Stub          []keybase1.Seqno              `json:"stub"`           // Stub out these links.
    71  	Omit          []keybase1.Seqno              `json:"omit"`           // Do not return these links.
    72  	Upto          keybase1.Seqno                `json:"upto"`           // Load up to this seqno inclusive.
    73  	SubteamReader bool                          `json:"subteam_reader"` // Whether to say the response is for the purpose of loading a subteam
    74  	OmitPrevs     keybase1.PerTeamKeyGeneration `json:"omit_prevs"`     // Do not return prevs that contain the secret for <= this number
    75  	ForceLastBox  bool                          `json:"force_last_box"` // Send the last known box no matter what
    76  	OmitBox       bool                          `json:"omit_box"`       // Send no box
    77  	HiddenUpto    keybase1.Seqno                `json:"hidden_upto"`    // Load up to this seqno inclusive (for hidden chains)
    78  
    79  	// Expected result
    80  	Error            bool   `json:"error"`
    81  	ErrorSubstr      string `json:"error_substr"`
    82  	ErrorType        string `json:"error_type"`
    83  	ErrorTypeFull    string `json:"error_type_full"`
    84  	ErrorAfterGetKey bool   `json:"error_after_get_key"`
    85  	NStubbed         *int   `json:"n_stubbed"`
    86  	ThenGetKey       int    `json:"then_get_key"`
    87  }
    88  
    89  func getTeamchainJSONDir(t *testing.T) string {
    90  	cmd := exec.Command("go", "list", "-json", "-m", "github.com/keybase/keybase-test-vectors")
    91  	output, err := cmd.Output()
    92  	require.NoError(t, err)
    93  	list := struct {
    94  		Dir string `json:"Dir"`
    95  	}{}
    96  	require.NoError(t, json.Unmarshal(output, &list))
    97  	return filepath.Join(list.Dir, "teamchains")
    98  }
    99  
   100  func TestUnits(t *testing.T) {
   101  	t.Logf("running units")
   102  	jsonDir := getTeamchainJSONDir(t)
   103  	files, err := os.ReadDir(jsonDir)
   104  	require.NoError(t, err)
   105  	selectUnit := os.Getenv("KEYBASE_TEAM_TEST_SELECT")
   106  	var runLog []string
   107  	var skipLog []string
   108  	for _, f := range files {
   109  		if !f.IsDir() && strings.HasSuffix(f.Name(), ".json") {
   110  			if len(selectUnit) > 0 && f.Name() != selectUnit && f.Name() != selectUnit+".json" {
   111  				continue
   112  			}
   113  			_, didRun := runUnitFile(t, filepath.Join(jsonDir, f.Name()))
   114  			if didRun {
   115  				runLog = append(runLog, f.Name())
   116  			} else {
   117  				skipLog = append(skipLog, f.Name())
   118  			}
   119  		}
   120  	}
   121  	require.NotZero(t, runLog, "found no test units")
   122  	t.Logf("ran %v units", len(runLog))
   123  	for _, name := range runLog {
   124  		t.Logf("  ✓ %v", name)
   125  	}
   126  	if len(skipLog) > 0 {
   127  		s := ""
   128  		if len(skipLog) != 1 {
   129  			s = "s"
   130  		}
   131  		t.Logf("skipped %d unit%s", len(skipLog), s)
   132  		for _, name := range skipLog {
   133  			t.Logf("  ⏭️ %s", name)
   134  		}
   135  	}
   136  	if len(selectUnit) > 0 {
   137  		t.Fatalf("test passed but only ran selected unit: %v", runLog)
   138  	}
   139  }
   140  
   141  func runUnitFile(t *testing.T, jsonPath string) (*Team, bool) {
   142  	fileName := filepath.Base(jsonPath)
   143  	t.Logf("reading test json file: %v", fileName)
   144  	data, err := os.ReadFile(jsonPath)
   145  	require.NoError(t, err)
   146  	var unit TestCase
   147  	err = json.Unmarshal(data, &unit)
   148  	if err != nil {
   149  		handleTestCaseLoadFailure(t, data, err)
   150  		return nil, true
   151  	}
   152  	unit.FileName = fileName
   153  	return runUnit(t, unit)
   154  }
   155  
   156  type loadFailure struct {
   157  	Failure struct {
   158  		Error         bool   `json:"error"`
   159  		ErrorTypeFull string `json:"error_type_full"`
   160  		ErrorSubstr   string `json:"error_substr"`
   161  	} `json:"load_failure"`
   162  }
   163  
   164  func handleTestCaseLoadFailure(t *testing.T, data []byte, loadErr error) {
   165  	var unit loadFailure
   166  	err := json.Unmarshal(data, &unit)
   167  	require.NoError(t, err, "reading unit file json (after failure)")
   168  	require.True(t, unit.Failure.Error, "unexpected failure in test load: %v", loadErr)
   169  	require.Equal(t, unit.Failure.ErrorTypeFull, reflect.TypeOf(loadErr).String())
   170  	require.Contains(t, loadErr.Error(), unit.Failure.ErrorSubstr)
   171  }
   172  
   173  func runUnitFromFilename(t *testing.T, filename string) (*Team, bool) {
   174  	jsonDir := getTeamchainJSONDir(t)
   175  	return runUnitFile(t, filepath.Join(jsonDir, filename))
   176  }
   177  
   178  func runUnit(t *testing.T, unit TestCase) (lastLoadRet *Team, didRun bool) {
   179  	t.Logf("starting unit: %v", unit.FileName)
   180  	defer t.Logf("exit unit: %v", unit.FileName)
   181  
   182  	if unit.Skip {
   183  		t.Logf("Marked 'skip' so skipping")
   184  		return nil, false
   185  	}
   186  
   187  	// Print the link payloads
   188  	for teamLabel, team := range unit.Teams {
   189  		for i, link := range team.Links {
   190  			var outer struct {
   191  				PayloadJSON string `json:"payload_json"`
   192  			}
   193  			err := json.Unmarshal(link, &outer)
   194  			require.NoError(t, err)
   195  			var inner interface{}
   196  
   197  			err = jsonw.EnsureMaxDepthBytesDefault([]byte(outer.PayloadJSON))
   198  			if err != nil {
   199  				t.Logf("team link '%v' #'%v': JSON exceeds max depth permissable: %v", teamLabel, i+1, err)
   200  			}
   201  			require.NoError(t, err)
   202  			err = json.Unmarshal([]byte(outer.PayloadJSON), &inner)
   203  			if err != nil {
   204  				t.Logf("team link '%v' #'%v': corrupted: %v", teamLabel, i+1, err)
   205  			} else {
   206  				bs, err := json.MarshalIndent(inner, "", "  ")
   207  				require.NoError(t, err)
   208  				t.Logf("team link '%v' #'%v': %v", teamLabel, i+1, string(bs))
   209  			}
   210  		}
   211  		for i, bundle := range team.Hidden {
   212  			t.Logf("team hidden'%v' #'%v': %+v", teamLabel, i+1, bundle)
   213  		}
   214  	}
   215  
   216  	require.NotNil(t, unit.Sessions, "unit has no sessions")
   217  	for iSession, session := range unit.Sessions {
   218  		require.NotNil(t, session.Loads, "unit has no loads in session %v", iSession)
   219  
   220  		tc := SetupTest(t, "team", 1)
   221  		defer tc.Cleanup()
   222  
   223  		// The auditor won't work in this case, since we have fake links that won't match the
   224  		// local database. In particular, the head merkle seqno might be off the right end
   225  		// of the merkle sequence in the DB.
   226  		tc.G.Env.Test.TeamSkipAudit = true
   227  
   228  		// Install a loader with a mock interface to the outside world.
   229  		t.Logf("install mock loader")
   230  		mock := NewMockLoaderContext(t, tc.G, unit)
   231  		merkleStorage := storage.NewMerkle()
   232  		storage := storage.NewStorage(tc.G)
   233  		loader := NewTeamLoader(tc.G, mock, storage, merkleStorage)
   234  		tc.G.SetTeamLoader(loader)
   235  
   236  		for iLoad, loadSpec := range session.Loads {
   237  			t.Logf("load the team session:%v load:%v", iSession, iLoad)
   238  			mock.state = MockLoaderContextState{
   239  				loadSpec: loadSpec,
   240  			}
   241  			loadArg := keybase1.LoadTeamArg{
   242  				NeedAdmin:                 loadSpec.NeedAdmin,
   243  				ForceRepoll:               iLoad > 0,
   244  				Name:                      mock.defaultTeamName.String(),
   245  				SkipNeedHiddenRotateCheck: true,
   246  			}
   247  			if loadSpec.NeedKeyGeneration > 0 {
   248  				loadArg.Refreshers = keybase1.TeamRefreshers{
   249  					NeedKeyGeneration: keybase1.PerTeamKeyGeneration(loadSpec.NeedKeyGeneration),
   250  				}
   251  			}
   252  			team, err := Load(context.TODO(), tc.G, loadArg)
   253  			if err != nil {
   254  				t.Logf("got error: [%T] %v", err, err)
   255  			}
   256  			if !loadSpec.Error {
   257  				require.NoError(t, err, "unit: %v", unit.FileName)
   258  				for _, teamDesc := range unit.Teams {
   259  					if loadSpec.Upto == 0 {
   260  						require.Len(t, team.chain().inner.LinkIDs, len(teamDesc.Links))
   261  					}
   262  					if loadSpec.NStubbed != nil {
   263  						require.Len(t, team.chain().inner.StubbedLinks, *loadSpec.NStubbed, "number of stubbed links in load result")
   264  					}
   265  					if loadSpec.ThenGetKey != 0 {
   266  						gen := keybase1.PerTeamKeyGeneration(loadSpec.ThenGetKey)
   267  						_, err := team.ApplicationKeyAtGeneration(context.Background(), keybase1.TeamApplication_KBFS, gen)
   268  						if !loadSpec.ErrorAfterGetKey {
   269  							require.NoError(t, err, "getting application key at gen: %v", gen)
   270  						} else {
   271  							require.Errorf(t, err, "unexpected get key success in %v", unit.FileName)
   272  							errstr := err.Error()
   273  							if len(loadSpec.ErrorSubstr) > 0 {
   274  								require.Contains(t, errstr, loadSpec.ErrorSubstr)
   275  							}
   276  							if len(loadSpec.ErrorType) > 0 {
   277  								require.Equal(t, loadSpec.ErrorType, reflect.TypeOf(err).Name(), "unexpected error type [%T]", err)
   278  							}
   279  							if len(loadSpec.ErrorTypeFull) > 0 {
   280  								require.Equal(t, loadSpec.ErrorTypeFull, reflect.TypeOf(err).String(), "unexpected error type [%T]", err)
   281  							}
   282  						}
   283  					} else {
   284  						require.False(t, loadSpec.ErrorAfterGetKey, "test does not make sense: ErrorAfterGetKey but no ThenGetKey")
   285  					}
   286  				}
   287  			} else {
   288  				require.Errorf(t, err, "unexpected team load success in %v", unit.FileName)
   289  				errstr := err.Error()
   290  				if len(loadSpec.ErrorSubstr) > 0 {
   291  					require.Contains(t, errstr, loadSpec.ErrorSubstr)
   292  				}
   293  				if len(loadSpec.ErrorType) > 0 {
   294  					require.Equal(t, loadSpec.ErrorType, reflect.TypeOf(err).Name(), "unexpected error type [%T]", err)
   295  				}
   296  				if len(loadSpec.ErrorTypeFull) > 0 {
   297  					require.Equal(t, loadSpec.ErrorTypeFull, reflect.TypeOf(err).String(), "unexpected error type [%T]", err)
   298  				}
   299  			}
   300  
   301  			lastLoadRet = team
   302  		}
   303  	}
   304  
   305  	require.False(t, unit.Todo, "test marked as TODO")
   306  	return lastLoadRet, true
   307  }