github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/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 }