github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/vit/utils.go (about) 1 /* 2 * Copyright (c) 2022-present unTill Pro, Ltd. 3 */ 4 5 package vit 6 7 import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "math" 12 "mime" 13 "testing" 14 "time" 15 16 "github.com/stretchr/testify/require" 17 "github.com/voedger/voedger/pkg/goutils/logger" 18 "github.com/voedger/voedger/pkg/in10n" 19 "github.com/voedger/voedger/pkg/istorage" 20 "github.com/voedger/voedger/pkg/istorage/mem" 21 "github.com/voedger/voedger/pkg/utils/federation" 22 "github.com/voedger/voedger/pkg/vvm" 23 24 "github.com/voedger/voedger/pkg/appdef" 25 "github.com/voedger/voedger/pkg/istructs" 26 "github.com/voedger/voedger/pkg/registry" 27 "github.com/voedger/voedger/pkg/sys/authnz" 28 coreutils "github.com/voedger/voedger/pkg/utils" 29 ) 30 31 func (vit *VIT) GetBLOB(appQName istructs.AppQName, wsid istructs.WSID, blobID istructs.RecordID, token string) *BLOB { 32 vit.T.Helper() 33 resp, err := vit.IFederation.ReadBLOB(appQName, wsid, blobID, coreutils.WithAuthorizeBy(token)) 34 require.NoError(vit.T, err) 35 contentDisposition := resp.HTTPResp.Header.Get(coreutils.ContentDisposition) 36 _, params, err := mime.ParseMediaType(contentDisposition) 37 require.NoError(vit.T, err) 38 return &BLOB{ 39 Content: []byte(resp.Body), 40 Name: params["filename"], 41 MimeType: resp.HTTPResp.Header.Get(coreutils.ContentType), 42 } 43 } 44 45 func (vit *VIT) signUp(login Login, wsKindInitData string, opts ...coreutils.ReqOptFunc) { 46 vit.T.Helper() 47 body := fmt.Sprintf(`{"args":{"Login":"%s","AppName":"%s","SubjectKind":%d,"WSKindInitializationData":%q,"ProfileCluster":%d},"unloggedArgs":{"Password":"%s"}}`, 48 login.Name, login.AppQName.String(), login.subjectKind, wsKindInitData, login.clusterID, login.Pwd) 49 vit.PostApp(istructs.AppQName_sys_registry, login.PseudoProfileWSID, "c.registry.CreateLogin", body, opts...) 50 } 51 52 func WithClusterID(clusterID istructs.ClusterID) signUpOptFunc { 53 return func(opts *signUpOpts) { 54 opts.profileClusterID = clusterID 55 } 56 } 57 58 func WithReqOpt(reqOpt coreutils.ReqOptFunc) signUpOptFunc { 59 return func(opts *signUpOpts) { 60 opts.reqOpts = append(opts.reqOpts, reqOpt) 61 } 62 } 63 64 func (vit *VIT) SignUp(loginName, pwd string, appQName istructs.AppQName, opts ...signUpOptFunc) Login { 65 vit.T.Helper() 66 signUpOpts := getSignUpOpts(opts) 67 login := NewLogin(loginName, pwd, appQName, istructs.SubjectKind_User, signUpOpts.profileClusterID) 68 vit.signUp(login, `{"DisplayName":"User Name"}`, signUpOpts.reqOpts...) 69 return login 70 } 71 72 func getSignUpOpts(opts []signUpOptFunc) *signUpOpts { 73 res := &signUpOpts{ 74 profileClusterID: istructs.MainClusterID, 75 } 76 for _, opt := range opts { 77 opt(res) 78 } 79 return res 80 } 81 82 func (vit *VIT) SignUpDevice(loginName, pwd string, appQName istructs.AppQName, opts ...signUpOptFunc) Login { 83 vit.T.Helper() 84 signUpOpts := getSignUpOpts(opts) 85 login := NewLogin(loginName, pwd, appQName, istructs.SubjectKind_Device, signUpOpts.profileClusterID) 86 vit.signUp(login, "{}", signUpOpts.reqOpts...) 87 return login 88 } 89 90 func (vit *VIT) GetCDocLoginID(login Login) int64 { 91 vit.T.Helper() 92 as, err := vit.IAppStructsProvider.AppStructs(istructs.AppQName_sys_registry) 93 require.NoError(vit.T, err) // notest 94 appWSID := coreutils.GetAppWSID(login.PseudoProfileWSID, as.NumAppWorkspaces()) 95 body := fmt.Sprintf(`{"args":{"query":"select CDocLoginID from registry.LoginIdx where AppWSID = %d and AppIDLoginHash = '%s/%s'"}, "elements":[{"fields":["Result"]}]}`, 96 appWSID, login.AppQName, registry.GetLoginHash(login.Name)) 97 sys := vit.GetSystemPrincipal(istructs.AppQName_sys_registry) 98 resp := vit.PostApp(istructs.AppQName_sys_registry, login.PseudoProfileWSID, "q.sys.SqlQuery", body, coreutils.WithAuthorizeBy(sys.Token)) 99 m := map[string]interface{}{} 100 require.NoError(vit.T, json.Unmarshal([]byte(resp.SectionRow()[0].(string)), &m)) 101 return int64(m["CDocLoginID"].(float64)) 102 } 103 104 func (vit *VIT) GetCDocWSKind(ws *AppWorkspace) (cdoc map[string]interface{}, id int64) { 105 vit.T.Helper() 106 return vit.getCDoc(ws.Owner.AppQName, ws.Kind, ws.WSID) 107 } 108 109 func (vit *VIT) getCDoc(appQName istructs.AppQName, qName appdef.QName, wsid istructs.WSID) (cdoc map[string]interface{}, id int64) { 110 vit.T.Helper() 111 body := bytes.NewBufferString(fmt.Sprintf(`{"args":{"Schema":"%s"},"elements":[{"fields":["sys.ID"`, qName)) 112 fields := []string{} 113 as, err := vit.IAppStructsProvider.AppStructs(appQName) 114 require.NoError(vit.T, err) 115 if doc := as.AppDef().CDoc(qName); doc != nil { 116 for _, field := range doc.Fields() { 117 if field.IsSys() { 118 continue 119 } 120 body.WriteString(fmt.Sprintf(`,"%s"`, field.Name())) 121 fields = append(fields, field.Name()) 122 } 123 } 124 body.WriteString("]}]}") 125 sys := vit.GetSystemPrincipal(appQName) 126 resp := vit.PostApp(appQName, wsid, "q.sys.Collection", body.String(), coreutils.WithAuthorizeBy(sys.Token)) 127 if len(resp.Sections) == 0 { 128 vit.T.Fatalf("no CDoc<%s> at workspace id %d", qName.String(), wsid) 129 } 130 id = int64(resp.SectionRow()[0].(float64)) 131 cdoc = map[string]interface{}{} 132 for i, fieldName := range fields { 133 cdoc[fieldName] = resp.SectionRow()[i+1] 134 } 135 return 136 } 137 138 func (vit *VIT) GetCDocChildWorkspace(ws *AppWorkspace) (cdoc map[string]interface{}, id int64) { 139 vit.T.Helper() 140 return vit.getCDoc(ws.Owner.AppQName, authnz.QNameCDocChildWorkspace, ws.Owner.ProfileWSID) 141 } 142 143 func (vit *VIT) waitForWorkspace(wsName string, owner *Principal, respGetter func(owner *Principal, body string) *coreutils.FuncResponse) (ws *AppWorkspace) { 144 const ( 145 // respect linter 146 tmplNameIdx = 3 147 tmplParamsIdx = 4 148 wsidIdx = 5 149 wsErrIdx = 6 150 ) 151 deadline := time.Now().Add(getWorkspaceInitAwaitTimeout()) 152 logger.Verbose("workspace", wsName, "awaiting started") 153 for time.Now().Before(deadline) { 154 body := fmt.Sprintf(` 155 { 156 "args": { 157 "WSName": "%s" 158 }, 159 "elements":[ 160 { 161 "fields":["WSName", "WSKind", "WSKindInitializationData", "TemplateName", "TemplateParams", "WSID", "WSError"] 162 } 163 ] 164 }`, wsName) 165 166 resp := respGetter(owner, body) 167 wsid := istructs.WSID(resp.SectionRow()[wsidIdx].(float64)) 168 wsError := resp.SectionRow()[wsErrIdx].(string) 169 if wsid == 0 && len(wsError) == 0 { 170 time.Sleep(workspaceQueryDelay) 171 continue 172 } 173 wsKind, err := appdef.ParseQName(resp.SectionRow()[1].(string)) 174 require.NoError(vit.T, err) 175 if len(wsError) > 0 { 176 vit.T.Fatal(wsError) 177 } 178 return &AppWorkspace{ 179 WorkspaceDescriptor: WorkspaceDescriptor{ 180 WSParams: WSParams{ 181 Name: resp.SectionRow()[0].(string), 182 Kind: wsKind, 183 InitDataJSON: resp.SectionRow()[2].(string), 184 TemplateName: resp.SectionRow()[tmplNameIdx].(string), 185 TemplateParams: resp.SectionRow()[tmplParamsIdx].(string), 186 ClusterID: istructs.MainClusterID, 187 ownerLoginName: owner.Name, 188 }, 189 WSID: wsid, 190 WSError: wsError, 191 }, 192 Owner: owner, 193 } 194 } 195 vit.T.Fatalf("workspace %s is not initialized in an acceptable time", wsName) 196 return ws 197 } 198 199 func (vit *VIT) WaitForWorkspace(wsName string, owner *Principal) (ws *AppWorkspace) { 200 return vit.waitForWorkspace(wsName, owner, func(owner *Principal, body string) *coreutils.FuncResponse { 201 return vit.PostProfile(owner, "q.sys.QueryChildWorkspaceByName", body) 202 }) 203 } 204 205 func (vit *VIT) WaitForChildWorkspace(parentWS *AppWorkspace, wsName string) (ws *AppWorkspace) { 206 return vit.waitForWorkspace(wsName, parentWS.Owner, func(owner *Principal, body string) *coreutils.FuncResponse { 207 return vit.PostWS(parentWS, "q.sys.QueryChildWorkspaceByName", body) 208 }) 209 } 210 211 func DoNotFailOnTimeout() signInOptFunc { 212 return func(opts *signInOpts) { 213 opts.failOnTimeout = false 214 } 215 } 216 217 func (vit *VIT) SignIn(login Login, optFuncs ...signInOptFunc) (prn *Principal) { 218 vit.T.Helper() 219 opts := &signInOpts{ 220 failOnTimeout: true, 221 } 222 for _, opt := range optFuncs { 223 opt(opts) 224 } 225 deadline := time.Now().Add(getWorkspaceInitAwaitTimeout()) 226 for time.Now().Before(deadline) { 227 body := fmt.Sprintf(` 228 { 229 "args": { 230 "Login": "%s", 231 "Password": "%s", 232 "AppName": "%s" 233 }, 234 "elements":[ 235 { 236 "fields":["PrincipalToken", "WSID", "WSError"] 237 } 238 ] 239 }`, login.Name, login.Pwd, login.AppQName.String()) 240 resp := vit.PostApp(istructs.AppQName_sys_registry, login.PseudoProfileWSID, "q.registry.IssuePrincipalToken", body) 241 profileWSID := istructs.WSID(resp.SectionRow()[1].(float64)) 242 wsError := resp.SectionRow()[2].(string) 243 token := resp.SectionRow()[0].(string) 244 if profileWSID == 0 && len(wsError) == 0 { 245 time.Sleep(workspaceQueryDelay) 246 continue 247 } 248 require.Empty(vit.T, wsError) 249 require.NotEmpty(vit.T, token) 250 return &Principal{ 251 Login: login, 252 Token: token, 253 ProfileWSID: profileWSID, 254 } 255 } 256 if opts.failOnTimeout { 257 vit.T.Fatal("user profile is not initialized in an acceptable time") 258 } 259 return nil 260 } 261 262 // owner could be *vit.Principal or *vit.AppWorkspace 263 func (vit *VIT) InitChildWorkspace(wsd WSParams, ownerIntf interface{}, opts ...coreutils.ReqOptFunc) { 264 vit.T.Helper() 265 body := fmt.Sprintf(`{ 266 "args": { 267 "WSName": "%s", 268 "WSKind": "%s", 269 "WSKindInitializationData": %q, 270 "TemplateName": "%s", 271 "TemplateParams": %q, 272 "WSClusterID": %d 273 } 274 }`, wsd.Name, wsd.Kind.String(), wsd.InitDataJSON, wsd.TemplateName, wsd.TemplateParams, wsd.ClusterID) 275 276 switch owner := ownerIntf.(type) { 277 case *Principal: 278 vit.PostProfile(owner, "c.sys.InitChildWorkspace", body, opts...) 279 case *AppWorkspace: 280 vit.PostWS(owner, "c.sys.InitChildWorkspace", body, opts...) 281 default: 282 panic("ownerIntf could be vit.*Principal or vit.*AppWorkspace only") 283 } 284 } 285 286 func SimpleWSParams(wsName string) WSParams { 287 return WSParams{ 288 Name: wsName, 289 Kind: QNameApp1_TestWSKind, 290 ClusterID: istructs.MainClusterID, 291 InitDataJSON: `{"IntFld": 42}`, // 292 } 293 } 294 295 func (vit *VIT) CreateWorkspace(wsp WSParams, owner *Principal, opts ...coreutils.ReqOptFunc) *AppWorkspace { 296 vit.InitChildWorkspace(wsp, owner, opts...) 297 ws := vit.WaitForWorkspace(wsp.Name, owner) 298 require.Empty(vit.T, ws.WSError) 299 return ws 300 } 301 302 func (vit *VIT) SubscribeForN10n(ws *AppWorkspace, projectionQName appdef.QName) federation.OffsetsChan { 303 vit.T.Helper() 304 return vit.SubscribeForN10nProjectionKey(in10n.ProjectionKey{ 305 App: ws.AppQName(), 306 Projection: projectionQName, 307 WS: ws.WSID, 308 }) 309 } 310 311 // will be unsubscribed automatically on vit.TearDown() 312 func (vit *VIT) SubscribeForN10nProjectionKey(pk in10n.ProjectionKey) federation.OffsetsChan { 313 vit.T.Helper() 314 offsetsChan, unsubscribe := vit.SubscribeForN10nUnsubscribe(pk) 315 vit.lock.Lock() // need to lock because the vit instance is used in different goroutines in e.g. Test_Race_RestaurantIntenseUsage() 316 vit.cleanups = append(vit.cleanups, func(vit *VIT) { 317 unsubscribe() 318 }) 319 vit.lock.Unlock() 320 return offsetsChan 321 } 322 323 func (vit *VIT) SubscribeForN10nUnsubscribe(pk in10n.ProjectionKey) (offsetsChan federation.OffsetsChan, unsubscribe func()) { 324 vit.T.Helper() 325 offsetsChan, unsubscribe, err := vit.IFederation.N10NSubscribe(pk) 326 require.NoError(vit.T, err) 327 return offsetsChan, unsubscribe 328 } 329 330 func (vit *VIT) MetricsRequest(client coreutils.IHTTPClient, opts ...coreutils.ReqOptFunc) (resp string) { 331 vit.T.Helper() 332 url := fmt.Sprintf("http://127.0.0.1:%d/metrics", vit.VoedgerVM.MetricsServicePort()) 333 res, err := client.Req(url, "", opts...) 334 require.NoError(vit.T, err) 335 return res.Body 336 } 337 338 func (vit *VIT) GetAny(entity string, ws *AppWorkspace) istructs.RecordID { 339 vit.T.Helper() 340 body := fmt.Sprintf(`{"args":{"Query":"select DocID from sys.CollectionView where PartKey = 1 and DocQName = '%s'"},"elements":[{"fields":["Result"]}]}`, entity) 341 resp := vit.PostWS(ws, "q.sys.SqlQuery", body) 342 if len(resp.Sections) == 0 { 343 vit.T.Fatalf("no %s at workspace id %d", entity, ws.WSID) 344 } 345 data := map[string]interface{}{} 346 require.NoError(vit.T, json.Unmarshal([]byte(resp.SectionRow()[0].(string)), &data)) 347 return istructs.RecordID(data["DocID"].(float64)) 348 } 349 350 func NewLogin(name, pwd string, appQName istructs.AppQName, subjectKind istructs.SubjectKindType, clusterID istructs.ClusterID) Login { 351 pseudoWSID := coreutils.GetPseudoWSID(istructs.NullWSID, name, istructs.MainClusterID) 352 return Login{name, pwd, pseudoWSID, appQName, subjectKind, clusterID, map[appdef.QName]func(verifiedValues map[string]string) map[string]interface{}{}} 353 } 354 355 func TestDeadline() time.Time { 356 deadline := time.Now().Add(5 * time.Second) 357 if coreutils.IsDebug() { 358 deadline = deadline.Add(time.Hour) 359 } 360 return deadline 361 } 362 363 func getWorkspaceInitAwaitTimeout() time.Duration { 364 if coreutils.IsDebug() { 365 // so long for Test_Race_RestaurantIntenseUsage with -race 366 return math.MaxInt 367 } 368 return defaultWorkspaceAwaitTimeout 369 } 370 371 func DummyWS(wsKind appdef.QName, wsid istructs.WSID, ownerPrn *Principal) *AppWorkspace { 372 return &AppWorkspace{ 373 WorkspaceDescriptor: WorkspaceDescriptor{ 374 WSParams: WSParams{ 375 Kind: wsKind, 376 ClusterID: istructs.MainClusterID, 377 }, 378 WSID: wsid, 379 }, 380 Owner: ownerPrn, 381 } 382 } 383 384 // calls testBeforeRestart() then stops then VIT, then launches new VIT on the same config but with storage from previous VIT 385 // then calls testAfterRestart() with the new VIT 386 // cfg must be owned 387 func TestRestartPreservingStorage(t *testing.T, cfg *VITConfig, testBeforeRestart, testAfterRestart func(t *testing.T, vit *VIT)) { 388 memStorage := mem.Provide() 389 cfg.opts = append(cfg.opts, WithVVMConfig(func(cfg *vvm.VVMConfig) { 390 cfg.StorageFactory = func() (provider istorage.IAppStorageFactory, err error) { 391 return memStorage, nil 392 } 393 cfg.KeyspaceNameSuffix = t.Name() 394 })) 395 func() { 396 vit := NewVIT(t, cfg) 397 defer vit.TearDown() 398 testBeforeRestart(t, vit) 399 }() 400 vit := NewVIT(t, cfg) 401 defer vit.TearDown() 402 testAfterRestart(t, vit) 403 }