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 }