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  }