github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/state/teststate/impl.go (about)

     1  /*
     2   * Copyright (c) 2024-present unTill Software Development Group B. V.
     3   * @author Michael Saigachenko
     4   */
     5  package teststate
     6  
     7  import (
     8  	"context"
     9  	"embed"
    10  	"fmt"
    11  	"io"
    12  	"path/filepath"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/stretchr/testify/require"
    17  	"github.com/voedger/voedger/pkg/appdef"
    18  	"github.com/voedger/voedger/pkg/iauthnz"
    19  	"github.com/voedger/voedger/pkg/iratesce"
    20  	"github.com/voedger/voedger/pkg/isecrets"
    21  	"github.com/voedger/voedger/pkg/istorage/mem"
    22  
    23  	istorageimpl "github.com/voedger/voedger/pkg/istorage/provider"
    24  	"github.com/voedger/voedger/pkg/istructs"
    25  	"github.com/voedger/voedger/pkg/istructsmem"
    26  	payloads "github.com/voedger/voedger/pkg/itokens-payloads"
    27  	"github.com/voedger/voedger/pkg/itokensjwt"
    28  	"github.com/voedger/voedger/pkg/parser"
    29  	"github.com/voedger/voedger/pkg/state"
    30  	"github.com/voedger/voedger/pkg/sys/authnz"
    31  	coreutils "github.com/voedger/voedger/pkg/utils"
    32  )
    33  
    34  type testState struct {
    35  	state.IState
    36  
    37  	ctx                  context.Context
    38  	appStructs           istructs.IAppStructs
    39  	appDef               appdef.IAppDef
    40  	cud                  istructs.ICUD
    41  	event                istructs.IPLogEvent
    42  	plogGen              istructs.IIDGenerator
    43  	wsOffsets            map[istructs.WSID]istructs.Offset
    44  	secretReader         isecrets.ISecretReader
    45  	httpHandler          HttpHandlerFunc
    46  	federationCmdHandler state.FederationCommandHandler
    47  	principals           []iauthnz.Principal
    48  	token                string
    49  	queryWsid            istructs.WSID
    50  	queryName            appdef.FullQName
    51  	processorKind        int
    52  	readObjects          []istructs.IObject
    53  }
    54  
    55  func NewTestState(processorKind int, packagePath string, createWorkspaces ...TestWorkspace) ITestState {
    56  	ts := &testState{}
    57  	ts.ctx = context.Background()
    58  	ts.processorKind = processorKind
    59  	ts.secretReader = &secretReader{secrets: make(map[string][]byte)}
    60  	ts.buildAppDef(packagePath, ".", createWorkspaces...)
    61  	ts.buildState(processorKind)
    62  	return ts
    63  }
    64  
    65  type secretReader struct {
    66  	secrets map[string][]byte
    67  }
    68  
    69  func (s *secretReader) ReadSecret(name string) (bb []byte, err error) {
    70  	if bb, ok := s.secrets[name]; ok {
    71  		return bb, nil
    72  	}
    73  	return nil, fmt.Errorf("secret not found: %s", name)
    74  }
    75  
    76  func (ctx *testState) WSID() istructs.WSID {
    77  	if ctx.processorKind == ProcKind_QueryProcessor {
    78  		return ctx.queryWsid
    79  	}
    80  	return ctx.event.Workspace()
    81  }
    82  
    83  func (ctx *testState) Arg() istructs.IObject {
    84  	return ctx.event.ArgumentObject() // TODO: For QP must be different
    85  }
    86  
    87  func (ctx *testState) ResultBuilder() istructs.IObjectBuilder {
    88  	if ctx.event == nil {
    89  		panic("no current event")
    90  	}
    91  	qname := ctx.event.QName()
    92  	command := ctx.appDef.Command(qname)
    93  	if command == nil {
    94  		panic(fmt.Sprintf("%v is not a command", qname))
    95  	}
    96  	return ctx.appStructs.ObjectBuilder(command.Result().QName())
    97  }
    98  
    99  func (ctx *testState) Request(timeout time.Duration, method, url string, body io.Reader, headers map[string]string) (statusCode int, resBody []byte, resHeaders map[string][]string, err error) {
   100  	if ctx.httpHandler == nil {
   101  		panic("http handler not set")
   102  	}
   103  	req := HttpRequest{
   104  		Timeout: timeout,
   105  		Method:  method,
   106  		URL:     url,
   107  		Body:    body,
   108  		Headers: headers,
   109  	}
   110  	resp, err := ctx.httpHandler(req)
   111  	if err != nil {
   112  		return 0, nil, nil, err
   113  	}
   114  	return resp.Status, resp.Body, resp.Headers, nil
   115  }
   116  
   117  func (ctx *testState) PutQuery(wsid istructs.WSID, name appdef.FullQName) {
   118  	ctx.queryWsid = wsid
   119  	ctx.queryName = name
   120  }
   121  
   122  func (ctx *testState) PutRequestSubject(principals []iauthnz.Principal, token string) {
   123  	ctx.principals = principals
   124  	ctx.token = token
   125  }
   126  
   127  func (ctx *testState) PutFederationCmdHandler(emu state.FederationCommandHandler) {
   128  	ctx.federationCmdHandler = emu
   129  }
   130  
   131  func (ctx *testState) emulateFederationCmd(owner, appname string, wsid istructs.WSID, command appdef.QName, body string) (statusCode int, newIDs map[string]int64, result string, err error) {
   132  	if ctx.federationCmdHandler == nil {
   133  		panic("federation command handler not set")
   134  	}
   135  	return ctx.federationCmdHandler(owner, appname, wsid, command, body)
   136  }
   137  
   138  func (ctx *testState) buildState(processorKind int) {
   139  
   140  	appFunc := func() istructs.IAppStructs { return ctx.appStructs }
   141  	eventFunc := func() istructs.IPLogEvent { return ctx.event }
   142  	partitionIDFunc := func() istructs.PartitionID { return TestPartition }
   143  	cudFunc := func() istructs.ICUD { return ctx.cud }
   144  	argFunc := func() istructs.IObject { return ctx.Arg() }
   145  	unloggedArgFunc := func() istructs.IObject { return nil }
   146  	wlogOffsetFunc := func() istructs.Offset { return ctx.event.WLogOffset() }
   147  	wsidFunc := func() istructs.WSID {
   148  		return ctx.WSID()
   149  	}
   150  	resultBuilderFunc := func() istructs.IObjectBuilder {
   151  		return ctx.ResultBuilder()
   152  	}
   153  	principalsFunc := func() []iauthnz.Principal {
   154  		return ctx.principals
   155  	}
   156  	tokenFunc := func() string {
   157  		return ctx.token
   158  	}
   159  	qryResultBuilderFunc := func() istructs.IObjectBuilder {
   160  		localPkgName := ctx.appDef.PackageLocalName(ctx.queryName.PkgPath())
   161  		query := ctx.appDef.Query(appdef.NewQName(localPkgName, ctx.queryName.Entity()))
   162  		if query == nil {
   163  			panic(fmt.Sprintf("query not found: %v", ctx.queryName))
   164  		}
   165  		return ctx.appStructs.ObjectBuilder(query.Result().QName())
   166  	}
   167  	execQueryCallback := func() istructs.ExecQueryCallback {
   168  		return func(o istructs.IObject) error {
   169  			ctx.readObjects = append(ctx.readObjects, o)
   170  			return nil
   171  		}
   172  	}
   173  
   174  	switch processorKind {
   175  	case ProcKind_Actualizer:
   176  		ctx.IState = state.ProvideAsyncActualizerStateFactory()(ctx.ctx, appFunc, partitionIDFunc, wsidFunc, nil, ctx.secretReader, eventFunc, nil, nil,
   177  			IntentsLimit, BundlesLimit, state.WithCustomHttpClient(ctx), state.WithFedearationCommandHandler(ctx.emulateFederationCmd))
   178  	case ProcKind_CommandProcessor:
   179  		ctx.IState = state.ProvideCommandProcessorStateFactory()(ctx.ctx, appFunc, partitionIDFunc, wsidFunc, ctx.secretReader, cudFunc, principalsFunc, tokenFunc,
   180  			IntentsLimit, resultBuilderFunc, argFunc, unloggedArgFunc, wlogOffsetFunc)
   181  	case ProcKind_QueryProcessor:
   182  		ctx.IState = state.ProvideQueryProcessorStateFactory()(ctx.ctx, appFunc, partitionIDFunc, wsidFunc, ctx.secretReader, principalsFunc, tokenFunc, nil, argFunc,
   183  			qryResultBuilderFunc, nil, execQueryCallback, state.QPWithCustomHttpClient(ctx), state.QPWithFedearationCommandHandler(ctx.emulateFederationCmd))
   184  	}
   185  }
   186  
   187  //go:embed testsys/*.sql
   188  var fsTestSys embed.FS
   189  
   190  func (ctx *testState) buildAppDef(packagePath string, packageDir string, createWorkspaces ...TestWorkspace) {
   191  
   192  	absPath, err := filepath.Abs(packageDir)
   193  	if err != nil {
   194  		panic(err)
   195  	}
   196  
   197  	pkgAst, err := parser.ParsePackageDir(packagePath, coreutils.NewPathReader(absPath), "")
   198  	if err != nil {
   199  		panic(err)
   200  	}
   201  	sysPackageAST, err := parser.ParsePackageDir(appdef.SysPackage, fsTestSys, "testsys")
   202  	if err != nil {
   203  		panic(err)
   204  	}
   205  	dummyAppFileAST, err := parser.ParseFile("dummy.sql", fmt.Sprintf(`
   206  		IMPORT SCHEMA '%s' AS %s;
   207  		APPLICATION test(
   208  			USE %s;
   209  		);
   210  	`, packagePath, TestPkgAlias, TestPkgAlias))
   211  	if err != nil {
   212  		panic(err)
   213  	}
   214  	dummyAppPkgAST, err := parser.BuildPackageSchema(packagePath+"_app", []*parser.FileSchemaAST{dummyAppFileAST})
   215  	if err != nil {
   216  		panic(err)
   217  	}
   218  
   219  	packagesAST := []*parser.PackageSchemaAST{pkgAst, dummyAppPkgAST, sysPackageAST}
   220  
   221  	appSchema, err := parser.BuildAppSchema(packagesAST)
   222  	if err != nil {
   223  		panic(err)
   224  	}
   225  
   226  	adb := appdef.New()
   227  	err = parser.BuildAppDefs(appSchema, adb)
   228  	if err != nil {
   229  		panic(err)
   230  	}
   231  
   232  	adf, err := adb.Build()
   233  	if err != nil {
   234  		panic(err)
   235  	}
   236  
   237  	ctx.appDef = adf
   238  
   239  	cfgs := make(istructsmem.AppConfigsType, 1)
   240  	cfg := cfgs.AddConfig(istructs.AppQName_test1_app1, adb)
   241  	cfg.SetNumAppWorkspaces(istructs.DefaultNumAppWorkspaces)
   242  	cfg.Resources.Add(istructsmem.NewCommandFunction(newWorkspaceCmd, istructsmem.NullCommandExec))
   243  	ctx.appDef.Extensions(func(i appdef.IExtension) {
   244  		if i.QName().Pkg() == TestPkgAlias {
   245  			if proj, ok := i.(appdef.IProjector); ok {
   246  				if proj.Sync() {
   247  					cfg.AddSyncProjectors(istructs.Projector{Name: i.QName()})
   248  				} else {
   249  					cfg.AddAsyncProjectors(istructs.Projector{Name: i.QName()})
   250  				}
   251  			} else if cmd, ok := i.(appdef.ICommand); ok {
   252  				cfg.Resources.Add(istructsmem.NewCommandFunction(cmd.QName(), istructsmem.NullCommandExec))
   253  			} else if q, ok := i.(appdef.IQuery); ok {
   254  				cfg.Resources.Add(istructsmem.NewCommandFunction(q.QName(), istructsmem.NullCommandExec))
   255  			}
   256  		}
   257  	})
   258  
   259  	asf := mem.Provide()
   260  	storageProvider := istorageimpl.Provide(asf)
   261  	prov := istructsmem.Provide(
   262  		cfgs,
   263  		iratesce.TestBucketsFactory,
   264  		payloads.ProvideIAppTokensFactory(itokensjwt.TestTokensJWT()),
   265  		storageProvider)
   266  	structs, err := prov.AppStructs(istructs.AppQName_test1_app1)
   267  	if err != nil {
   268  		panic(err)
   269  	}
   270  	ctx.appStructs = structs
   271  	ctx.plogGen = istructsmem.NewIDGenerator()
   272  	ctx.wsOffsets = make(map[istructs.WSID]istructs.Offset)
   273  
   274  	for _, ws := range createWorkspaces {
   275  		rebWs := ctx.appStructs.Events().GetNewRawEventBuilder(istructs.NewRawEventBuilderParams{
   276  			GenericRawEventBuilderParams: istructs.GenericRawEventBuilderParams{
   277  				Workspace:         ws.WSID,
   278  				HandlingPartition: TestPartition,
   279  				QName:             newWorkspaceCmd,
   280  			},
   281  		})
   282  		cud := rebWs.CUDBuilder().Create(authnz.QNameCDocWorkspaceDescriptor)
   283  		cud.PutRecordID(appdef.SystemField_ID, istructs.RecordID(1))
   284  		cud.PutQName("WSKind", appdef.NewQName(TestPkgAlias, ws.WorkspaceDescriptor))
   285  		rawWsEvent, err := rebWs.BuildRawEvent()
   286  		if err != nil {
   287  			panic(err)
   288  		}
   289  		wsEvent, err := ctx.appStructs.Events().PutPlog(rawWsEvent, nil, ctx.plogGen)
   290  		if err != nil {
   291  			panic(err)
   292  		}
   293  		err = ctx.appStructs.Records().Apply(wsEvent)
   294  		if err != nil {
   295  			panic(err)
   296  		}
   297  	}
   298  
   299  }
   300  
   301  func (ctx *testState) nextWSOffs(ws istructs.WSID) istructs.Offset {
   302  	offs, ok := ctx.wsOffsets[ws]
   303  	if !ok {
   304  		offs = istructs.Offset(0)
   305  	}
   306  	offs += 1
   307  	ctx.wsOffsets[ws] = offs
   308  	return offs
   309  }
   310  
   311  func (ctx *testState) PutHttpHandler(handler HttpHandlerFunc) {
   312  	ctx.httpHandler = handler
   313  }
   314  
   315  func (ctx *testState) PutEvent(wsid istructs.WSID, name appdef.FullQName, cb NewEventCallback) (wLogOffs istructs.Offset, newRecordIds []istructs.RecordID) {
   316  	localPkgName := ctx.appDef.PackageLocalName(name.PkgPath())
   317  	wLogOffs = ctx.nextWSOffs(wsid)
   318  	reb := ctx.appStructs.Events().GetNewRawEventBuilder(istructs.NewRawEventBuilderParams{
   319  		GenericRawEventBuilderParams: istructs.GenericRawEventBuilderParams{
   320  			Workspace:         wsid,
   321  			HandlingPartition: TestPartition,
   322  			QName:             appdef.NewQName(localPkgName, name.Entity()),
   323  			WLogOffset:        wLogOffs,
   324  		},
   325  	})
   326  	if cb != nil {
   327  		ctx.cud = reb.CUDBuilder()
   328  		cb(reb.ArgumentObjectBuilder(), ctx.cud)
   329  	}
   330  	rawEvent, err := reb.BuildRawEvent()
   331  	if err != nil {
   332  		panic(err)
   333  	}
   334  	event, err := ctx.appStructs.Events().PutPlog(rawEvent, nil, ctx.plogGen)
   335  	if err != nil {
   336  		panic(err)
   337  	}
   338  
   339  	err = ctx.appStructs.Events().PutWlog(event)
   340  	if err != nil {
   341  		panic(err)
   342  	}
   343  
   344  	newRecordIds = make([]istructs.RecordID, 0)
   345  	err = ctx.appStructs.Records().Apply2(event, func(r istructs.IRecord) {
   346  		newRecordIds = append(newRecordIds, r.ID())
   347  	})
   348  
   349  	if err != nil {
   350  		panic(err)
   351  	}
   352  
   353  	ctx.event = event
   354  	return wLogOffs, newRecordIds
   355  }
   356  
   357  func (ctx *testState) PutView(wsid istructs.WSID, entity appdef.FullQName, callback ViewValueCallback) {
   358  	localPkgName := ctx.appDef.PackageLocalName(entity.PkgPath())
   359  	v := TestViewValue{
   360  		wsid: wsid,
   361  		vr:   ctx.appStructs.ViewRecords(),
   362  		Key:  ctx.appStructs.ViewRecords().KeyBuilder(appdef.NewQName(localPkgName, entity.Entity())),
   363  		Val:  ctx.appStructs.ViewRecords().NewValueBuilder(appdef.NewQName(localPkgName, entity.Entity())),
   364  	}
   365  	callback(v.Key, v.Val)
   366  	err := ctx.appStructs.ViewRecords().Put(wsid, v.Key, v.Val)
   367  	if err != nil {
   368  		panic(err)
   369  	}
   370  }
   371  
   372  func (ctx *testState) PutSecret(name string, secret []byte) {
   373  	ctx.secretReader.(*secretReader).secrets[name] = secret
   374  }
   375  
   376  type intentAssertions struct {
   377  	t   *testing.T
   378  	kb  istructs.IStateKeyBuilder
   379  	vb  istructs.IStateValueBuilder
   380  	ctx *testState
   381  }
   382  
   383  func (ia *intentAssertions) Exists() {
   384  	if ia.vb == nil {
   385  		require.Fail(ia.t, "Expected intent to exist")
   386  	}
   387  }
   388  
   389  func (ia *intentAssertions) Equal(vbc ValueBuilderCallback) {
   390  	if ia.vb == nil {
   391  		panic("intent not found")
   392  	}
   393  
   394  	vb, err := ia.ctx.IState.NewValue(ia.kb)
   395  	if err != nil {
   396  		panic(err)
   397  	}
   398  	vbc(vb)
   399  
   400  	if !ia.vb.Equal(vb) {
   401  		require.Fail(ia.t, "Expected intents to be equal")
   402  	}
   403  }
   404  
   405  func (ctx *testState) RequireIntent(t *testing.T, storage appdef.QName, entity appdef.FullQName, kbc KeyBuilderCallback) IIntentAssertions {
   406  	localPkgName := ctx.appDef.PackageLocalName(entity.PkgPath())
   407  	localEntity := appdef.NewQName(localPkgName, entity.Entity())
   408  	kb, err := ctx.IState.KeyBuilder(storage, localEntity)
   409  	if err != nil {
   410  		panic(err)
   411  	}
   412  	kbc(kb)
   413  	return &intentAssertions{
   414  		t:   t,
   415  		kb:  kb,
   416  		vb:  ctx.IState.FindIntent(kb),
   417  		ctx: ctx,
   418  	}
   419  }