github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/processors/command/impl_test.go (about) 1 /* 2 * Copyright (c) 2020-present unTill Pro, Ltd. 3 */ 4 5 package commandprocessor 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "log" 13 "net/http" 14 "strings" 15 "testing" 16 "time" 17 18 "github.com/stretchr/testify/require" 19 20 "github.com/voedger/voedger/pkg/appdef" 21 "github.com/voedger/voedger/pkg/appparts" 22 "github.com/voedger/voedger/pkg/iauthnzimpl" 23 "github.com/voedger/voedger/pkg/in10n" 24 "github.com/voedger/voedger/pkg/in10nmem" 25 "github.com/voedger/voedger/pkg/iratesce" 26 "github.com/voedger/voedger/pkg/isecretsimpl" 27 "github.com/voedger/voedger/pkg/istorage/mem" 28 istorageimpl "github.com/voedger/voedger/pkg/istorage/provider" 29 "github.com/voedger/voedger/pkg/istructs" 30 "github.com/voedger/voedger/pkg/istructsmem" 31 payloads "github.com/voedger/voedger/pkg/itokens-payloads" 32 "github.com/voedger/voedger/pkg/itokensjwt" 33 imetrics "github.com/voedger/voedger/pkg/metrics" 34 "github.com/voedger/voedger/pkg/pipeline" 35 "github.com/voedger/voedger/pkg/processors" 36 "github.com/voedger/voedger/pkg/projectors" 37 coreutils "github.com/voedger/voedger/pkg/utils" 38 wsdescutil "github.com/voedger/voedger/pkg/utils/testwsdesc" 39 "github.com/voedger/voedger/pkg/vvm/engines" 40 ibus "github.com/voedger/voedger/staging/src/github.com/untillpro/airs-ibus" 41 "github.com/voedger/voedger/staging/src/github.com/untillpro/ibusmem" 42 ) 43 44 var ( 45 testCRecord = appdef.NewQName("test", "TestCRecord") 46 testCDoc = appdef.NewQName("test", "TestCDoc") 47 testWDoc = appdef.NewQName("test", "TestWDoc") 48 ) 49 50 func TestBasicUsage(t *testing.T) { 51 require := require.New(t) 52 check := make(chan interface{}, 1) 53 cudsCheck := make(chan interface{}) 54 55 testCmdQName := appdef.NewQName(appdef.SysPackage, "Test") 56 // схема параметров тестовой команды 57 testCmdQNameParams := appdef.NewQName(appdef.SysPackage, "TestParams") 58 // схема unlogged-параметров тестовой команды 59 testCmdQNameParamsUnlogged := appdef.NewQName(appdef.SysPackage, "TestParamsUnlogged") 60 prepareAppDef := func(appDef appdef.IAppDefBuilder, cfg *istructsmem.AppConfigType) { 61 pars := appDef.AddObject(testCmdQNameParams) 62 pars.AddField("Text", appdef.DataKind_string, true) 63 64 unloggedPars := appDef.AddObject(testCmdQNameParamsUnlogged) 65 unloggedPars.AddField("Password", appdef.DataKind_string, true) 66 67 appDef.AddCDoc(testCDoc).AddContainer("TestCRecord", testCRecord, 0, 1) 68 appDef.AddCRecord(testCRecord) 69 appDef.AddCommand(testCmdQName).SetUnloggedParam(testCmdQNameParamsUnlogged).SetParam(testCmdQNameParams) 70 71 // сама тестовая команда 72 testExec := func(args istructs.ExecCommandArgs) (err error) { 73 cuds := args.Workpiece.(*cmdWorkpiece).parsedCUDs 74 if len(cuds) > 0 { 75 require.Len(cuds, 1) 76 require.Equal(float64(1), cuds[0].fields[appdef.SystemField_ID]) 77 require.Equal(testCDoc.String(), cuds[0].fields[appdef.SystemField_QName]) 78 close(cudsCheck) 79 } 80 require.Equal(istructs.WSID(1), args.PrepareArgs.WSID) 81 require.NotNil(args.State) 82 83 // просто проверим, что мы получили то, что передал клиент 84 text := args.ArgumentObject.AsString("Text") 85 if text == "fire error" { 86 return errors.New(text) 87 } else { 88 require.Equal("hello", text) 89 } 90 require.Equal("pass", args.ArgumentUnloggedObject.AsString("Password")) 91 92 check <- 1 // сигнал: проверки случились 93 return 94 } 95 testCmd := istructsmem.NewCommandFunction(testCmdQName, testExec) 96 cfg.Resources.Add(testCmd) 97 } 98 99 app := setUp(t, prepareAppDef) 100 defer tearDown(app) 101 102 channelID, err := app.n10nBroker.NewChannel("test", 24*time.Hour) 103 require.NoError(err) 104 projectionKey := in10n.ProjectionKey{ 105 App: istructs.AppQName_untill_airs_bp, 106 Projection: projectors.PLogUpdatesQName, 107 WS: 1, 108 } 109 go app.n10nBroker.WatchChannel(app.ctx, channelID, func(projection in10n.ProjectionKey, _ istructs.Offset) { 110 require.Equal(projectionKey, projection) 111 check <- 1 112 }) 113 app.n10nBroker.Subscribe(channelID, projectionKey) 114 defer app.n10nBroker.Unsubscribe(channelID, projectionKey) 115 116 t.Run("basic usage", func(t *testing.T) { 117 // command processor работает через ibus.SendResponse -> нам нужен sender -> тестируем через ibus.SendRequest2() 118 request := ibus.Request{ 119 Body: []byte(`{"args":{"Text":"hello"},"unloggedArgs":{"Password":"pass"},"cuds":[{"fields":{"sys.ID":1,"sys.QName":"test.TestCDoc"}}]}`), 120 AppQName: istructs.AppQName_untill_airs_bp.String(), 121 WSID: 1, 122 Resource: "c.sys.Test", 123 // need to authorize, otherwise execute will be forbidden 124 Header: app.sysAuthHeader, 125 } 126 resp, sections, secErr, err := app.bus.SendRequest2(app.ctx, request, coreutils.GetTestBusTimeout()) 127 require.NoError(err) 128 require.Nil(secErr, secErr) 129 require.Nil(sections) 130 log.Println(string(resp.Data)) 131 require.Equal(http.StatusOK, resp.StatusCode) 132 require.Equal(coreutils.ApplicationJSON, resp.ContentType) 133 // убедимся, что команда действительно отработала и нотификации отправились 134 <-check 135 <-check 136 137 // убедимся, что CUD'ы проверились 138 <-cudsCheck 139 }) 140 141 t.Run("500 internal server error command exec error", func(t *testing.T) { 142 request := ibus.Request{ 143 Body: []byte(`{"args":{"Text":"fire error"},"unloggedArgs":{"Password":"pass"}}`), 144 AppQName: istructs.AppQName_untill_airs_bp.String(), 145 WSID: 1, 146 Resource: "c.sys.Test", 147 Header: app.sysAuthHeader, 148 } 149 resp, sections, secErr, err := app.bus.SendRequest2(app.ctx, request, coreutils.GetTestBusTimeout()) 150 require.NoError(err) 151 require.Nil(secErr) 152 require.Nil(sections) 153 require.Equal(http.StatusInternalServerError, resp.StatusCode) 154 require.Equal(coreutils.ApplicationJSON, resp.ContentType) 155 require.Equal(`{"sys.Error":{"HTTPStatus":500,"Message":"fire error"}}`, string(resp.Data)) 156 require.Contains(string(resp.Data), "fire error") 157 log.Println(string(resp.Data)) 158 }) 159 } 160 161 func sendCUD(t *testing.T, wsid istructs.WSID, app testApp, expectedCode ...int) map[string]interface{} { 162 require := require.New(t) 163 req := ibus.Request{ 164 WSID: int64(wsid), 165 AppQName: istructs.AppQName_untill_airs_bp.String(), 166 Resource: "c.sys.CUD", 167 Body: []byte(`{"cuds":[ 168 {"fields":{"sys.ID":1,"sys.QName":"test.TestCDoc"}}, 169 {"fields":{"sys.ID":2,"sys.QName":"test.TestWDoc"}}, 170 {"fields":{"sys.ID":3,"sys.QName":"test.TestCRecord","sys.ParentID":1,"sys.Container":"TestCRecord"}} 171 ]}`), 172 Header: app.sysAuthHeader, 173 } 174 resp, sections, secErr, err := app.bus.SendRequest2(app.ctx, req, coreutils.GetTestBusTimeout()) 175 require.NoError(err) 176 require.Nil(secErr) 177 require.Nil(sections) 178 if len(expectedCode) == 0 { 179 require.Equal(http.StatusOK, resp.StatusCode) 180 } else { 181 require.Equal(expectedCode[0], resp.StatusCode) 182 } 183 respData := map[string]interface{}{} 184 require.NoError(json.Unmarshal(resp.Data, &respData)) 185 return respData 186 } 187 188 func TestRecoveryOnSyncProjectorError(t *testing.T) { 189 require := require.New(t) 190 191 cudQName := appdef.NewQName(appdef.SysPackage, "CUD") 192 testErr := errors.New("test error") 193 counter := 0 194 app := setUp(t, func(appDef appdef.IAppDefBuilder, cfg *istructsmem.AppConfigType) { 195 appDef.AddCRecord(testCRecord) 196 appDef.AddCDoc(testCDoc).AddContainer("TestCRecord", testCRecord, 0, 1) 197 appDef.AddWDoc(testWDoc) 198 appDef.AddCommand(cudQName) 199 200 failingProjQName := appdef.NewQName(appdef.SysPackage, "Failer") 201 cfg.AddSyncProjectors( 202 istructs.Projector{ 203 Name: failingProjQName, 204 Func: func(istructs.IPLogEvent, istructs.IState, istructs.IIntents) error { 205 counter++ 206 if counter == 3 { // 1st event is insert WorkspaceDescriptor stub 207 return testErr 208 } 209 return nil 210 }, 211 }) 212 appDef.AddProjector(failingProjQName).SetSync(true).Events().Add(cudQName, appdef.ProjectorEventKind_Execute) 213 cfg.Resources.Add(istructsmem.NewCommandFunction(cudQName, istructsmem.NullCommandExec)) 214 }) 215 defer tearDown(app) 216 217 // ok to c.sys.CUD 218 respData := sendCUD(t, 1, app) 219 require.Equal(2, int(respData["CurrentWLogOffset"].(float64))) // 1st is WorkspaceDescriptor stub insert 220 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID), istructs.RecordID(respData["NewIDs"].(map[string]interface{})["1"].(float64))) 221 require.Equal(istructs.NewRecordID(istructs.FirstBaseRecordID), istructs.RecordID(respData["NewIDs"].(map[string]interface{})["2"].(float64))) 222 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID)+1, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["3"].(float64))) 223 224 // 2nd c.sys.CUD -> sync projector failure, expect 500 internal server error 225 respData = sendCUD(t, 1, app, http.StatusInternalServerError) 226 require.Equal(testErr.Error(), respData["sys.Error"].(map[string]interface{})["Message"].(string)) 227 228 // PLog and record is applied but WLog is not written here because sync projector is failed 229 // partition is scheduled to be recovered 230 231 // 3rd c.sys.CUD - > recovery procedure must re-apply 2nd event (PLog, records and WLog), then 3rd event is processed ok (sync projectors are ok) 232 respData = sendCUD(t, 1, app) 233 require.Equal(4, int(respData["CurrentWLogOffset"].(float64))) 234 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID)+4, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["1"].(float64))) 235 require.Equal(istructs.NewRecordID(istructs.FirstBaseRecordID)+2, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["2"].(float64))) 236 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID)+5, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["3"].(float64))) 237 } 238 239 func TestRecovery(t *testing.T) { 240 require := require.New(t) 241 242 cudQName := appdef.NewQName(appdef.SysPackage, "CUD") 243 app := setUp(t, func(appDef appdef.IAppDefBuilder, cfg *istructsmem.AppConfigType) { 244 appDef.AddCRecord(testCRecord) 245 appDef.AddCDoc(testCDoc).AddContainer("TestCRecord", testCRecord, 0, 1) 246 appDef.AddWDoc(testWDoc) 247 appDef.AddCommand(cudQName) 248 cfg.Resources.Add(istructsmem.NewCommandFunction(cudQName, istructsmem.NullCommandExec)) 249 }) 250 defer tearDown(app) 251 252 cmdCUD := istructsmem.NewCommandFunction(cudQName, istructsmem.NullCommandExec) 253 app.cfg.Resources.Add(cmdCUD) 254 255 respData := sendCUD(t, 1, app) 256 require.Equal(2, int(respData["CurrentWLogOffset"].(float64))) 257 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID), istructs.RecordID(respData["NewIDs"].(map[string]interface{})["1"].(float64))) 258 require.Equal(istructs.NewRecordID(istructs.FirstBaseRecordID), istructs.RecordID(respData["NewIDs"].(map[string]interface{})["2"].(float64))) 259 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID)+1, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["3"].(float64))) 260 261 restartCmdProc(&app) 262 respData = sendCUD(t, 1, app) 263 require.Equal(3, int(respData["CurrentWLogOffset"].(float64))) 264 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID)+2, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["1"].(float64))) 265 require.Equal(istructs.NewRecordID(istructs.FirstBaseRecordID)+1, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["2"].(float64))) 266 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID)+3, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["3"].(float64))) 267 268 restartCmdProc(&app) 269 respData = sendCUD(t, 2, app) 270 require.Equal(2, int(respData["CurrentWLogOffset"].(float64))) 271 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID), istructs.RecordID(respData["NewIDs"].(map[string]interface{})["1"].(float64))) 272 require.Equal(istructs.NewRecordID(istructs.FirstBaseRecordID), istructs.RecordID(respData["NewIDs"].(map[string]interface{})["2"].(float64))) 273 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID)+1, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["3"].(float64))) 274 275 restartCmdProc(&app) 276 respData = sendCUD(t, 1, app) 277 require.Equal(4, int(respData["CurrentWLogOffset"].(float64))) 278 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID)+4, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["1"].(float64))) 279 require.Equal(istructs.NewRecordID(istructs.FirstBaseRecordID)+2, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["2"].(float64))) 280 require.Equal(istructs.NewCDocCRecordID(istructs.FirstBaseRecordID)+5, istructs.RecordID(respData["NewIDs"].(map[string]interface{})["3"].(float64))) 281 282 app.cancel() 283 <-app.done 284 } 285 286 func restartCmdProc(app *testApp) { 287 app.cancel() 288 <-app.done 289 app.ctx, app.cancel = context.WithCancel(context.Background()) 290 app.done = make(chan struct{}) 291 go func() { 292 app.cmdProcService.Run(app.ctx) 293 close(app.done) 294 }() 295 } 296 297 func TestCUDUpdate(t *testing.T) { 298 require := require.New(t) 299 300 testQName := appdef.NewQName("test", "test") 301 302 cudQName := appdef.NewQName(appdef.SysPackage, "CUD") 303 app := setUp(t, func(appDef appdef.IAppDefBuilder, cfg *istructsmem.AppConfigType) { 304 appDef.AddCDoc(testQName).AddField("IntFld", appdef.DataKind_int32, false) 305 appDef.AddCommand(cudQName) 306 cfg.Resources.Add(istructsmem.NewCommandFunction(cudQName, istructsmem.NullCommandExec)) 307 }) 308 defer tearDown(app) 309 310 cmdCUD := istructsmem.NewCommandFunction(cudQName, istructsmem.NullCommandExec) 311 app.cfg.Resources.Add(cmdCUD) 312 313 // insert 314 req := ibus.Request{ 315 WSID: 1, 316 AppQName: istructs.AppQName_untill_airs_bp.String(), 317 Resource: "c.sys.CUD", 318 Body: []byte(`{"cuds":[{"fields":{"sys.ID":1,"sys.QName":"test.test"}}]}`), 319 Header: app.sysAuthHeader, 320 } 321 resp, sections, secErr, err := app.bus.SendRequest2(app.ctx, req, coreutils.GetTestBusTimeout()) 322 require.NoError(err) 323 require.Nil(secErr, secErr) 324 require.Nil(sections) 325 require.Equal(http.StatusOK, resp.StatusCode) 326 require.Equal(coreutils.ApplicationJSON, resp.ContentType) 327 m := map[string]interface{}{} 328 require.NoError(json.Unmarshal(resp.Data, &m)) 329 330 t.Run("update", func(t *testing.T) { 331 id := int64(m["NewIDs"].(map[string]interface{})["1"].(float64)) 332 req.Body = []byte(fmt.Sprintf(`{"cuds":[{"sys.ID":%d,"fields":{"sys.QName":"test.test", "IntFld": 42}}]}`, id)) 333 resp, sections, secErr, err = app.bus.SendRequest2(app.ctx, req, coreutils.GetTestBusTimeout()) 334 require.NoError(err) 335 require.Nil(secErr) 336 require.Nil(sections) 337 require.Equal(http.StatusOK, resp.StatusCode) 338 require.Equal(coreutils.ApplicationJSON, resp.ContentType) 339 }) 340 341 t.Run("404 not found on update not existing", func(t *testing.T) { 342 req.Body = []byte(fmt.Sprintf(`{"cuds":[{"sys.ID":%d,"fields":{"sys.QName":"test.test", "IntFld": 42}}]}`, istructs.NonExistingRecordID)) 343 resp, sections, secErr, err = app.bus.SendRequest2(app.ctx, req, coreutils.GetTestBusTimeout()) 344 require.NoError(err) 345 require.Nil(secErr) 346 require.Nil(sections) 347 require.Equal(http.StatusNotFound, resp.StatusCode) 348 require.Equal(coreutils.ApplicationJSON, resp.ContentType) 349 }) 350 } 351 352 func Test400BadRequestOnCUDErrors(t *testing.T) { 353 require := require.New(t) 354 355 testQName := appdef.NewQName("test", "test") 356 357 cudQName := appdef.NewQName(appdef.SysPackage, "CUD") 358 app := setUp(t, func(appDef appdef.IAppDefBuilder, cfg *istructsmem.AppConfigType) { 359 appDef.AddCDoc(testQName) 360 appDef.AddCommand(cudQName) 361 cfg.Resources.Add(istructsmem.NewCommandFunction(cudQName, istructsmem.NullCommandExec)) 362 }) 363 defer tearDown(app) 364 365 cmdCUD := istructsmem.NewCommandFunction(cudQName, istructsmem.NullCommandExec) 366 app.cfg.Resources.Add(cmdCUD) 367 368 cases := []struct { 369 desc string 370 bodyAdd string 371 expectedMessageLike string 372 }{ 373 {"not an object", `"cuds":42`, `'cuds' must be an array of objects`}, 374 {`element is not an object`, `"cuds":[42]`, `cuds[0]: not an object`}, 375 {`missing fields`, `"cuds":[{}]`, `cuds[0]: "fields" missing`}, 376 {`fields is not an object`, `"cuds":[{"fields":42}]`, `cuds[0]: field 'fields' must be an object`}, 377 {`fields: sys.ID missing`, `"cuds":[{"fields":{"sys.QName":"test.Test"}}]`, `cuds[0]: "sys.ID" missing`}, 378 {`fields: sys.ID is not a number (create)`, `"cuds":[{"sys.ID":"wrong","fields":{"sys.QName":"test.Test"}}]`, `cuds[0]: field 'sys.ID' must be an int64`}, 379 {`fields: sys.ID is not a number (update)`, `"cuds":[{"fields":{"sys.ID":"wrong","sys.QName":"test.Test"}}]`, `cuds[0]: field 'sys.ID' must be an int64`}, 380 {`fields: wrong qName`, `"cuds":[{"fields":{"sys.ID":1,"sys.QName":"wrong"}},{"fields":{"sys.ID":1,"sys.QName":"test.Test"}}]`, `convert error: string «wrong» to QName`}, 381 } 382 383 for _, c := range cases { 384 t.Run(c.desc, func(t *testing.T) { 385 req := ibus.Request{ 386 WSID: 1, 387 AppQName: istructs.AppQName_untill_airs_bp.String(), 388 Resource: "c.sys.CUD", 389 Body: []byte("{" + c.bodyAdd + "}"), 390 Header: app.sysAuthHeader, 391 } 392 resp, sections, secErr, err := app.bus.SendRequest2(app.ctx, req, coreutils.GetTestBusTimeout()) 393 require.NoError(err) 394 require.Nil(secErr) 395 require.Nil(sections) 396 require.Equal(http.StatusBadRequest, resp.StatusCode, c.desc) 397 require.Equal(coreutils.ApplicationJSON, resp.ContentType, c.desc) 398 require.Contains(string(resp.Data), jsonEscape(c.expectedMessageLike), c.desc) 399 require.Contains(string(resp.Data), `"HTTPStatus":400`, c.desc) 400 }) 401 } 402 } 403 404 func TestErrors(t *testing.T) { 405 require := require.New(t) 406 407 testCmdQNameParams := appdef.NewQName(appdef.SysPackage, "TestParams") 408 testCmdQNameParamsUnlogged := appdef.NewQName(appdef.SysPackage, "TestParamsUnlogged") 409 410 testCmdQName := appdef.NewQName(appdef.SysPackage, "Test") 411 app := setUp(t, func(appDef appdef.IAppDefBuilder, cfg *istructsmem.AppConfigType) { 412 appDef.AddObject(testCmdQNameParams). 413 AddField("Text", appdef.DataKind_string, true) 414 415 appDef.AddObject(testCmdQNameParamsUnlogged). 416 AddField("Password", appdef.DataKind_string, true) 417 418 appDef.AddCommand(testCmdQName).SetUnloggedParam(testCmdQNameParamsUnlogged).SetParam(testCmdQNameParams) 419 cfg.Resources.Add(istructsmem.NewCommandFunction(testCmdQName, istructsmem.NullCommandExec)) 420 }) 421 defer tearDown(app) 422 423 baseReq := ibus.Request{ 424 WSID: 1, 425 AppQName: istructs.AppQName_untill_airs_bp.String(), 426 Resource: "c.sys.Test", 427 Body: []byte(`{"args":{"Text":"hello"},"unloggedArgs":{"Password":"123"}}`), 428 Header: app.sysAuthHeader, 429 } 430 431 cases := []struct { 432 desc string 433 ibus.Request 434 expectedMessageLike string 435 expectedStatusCode int 436 }{ 437 {"unknown app", ibus.Request{AppQName: "untill/unknown"}, "application untill/unknown not found", http.StatusServiceUnavailable}, 438 {"bad request body", ibus.Request{Body: []byte("{wrong")}, "failed to unmarshal request body: invalid character 'w' looking for beginning of object key string", http.StatusBadRequest}, 439 {"unknown func", ibus.Request{Resource: "c.sys.Unknown"}, "unknown function", http.StatusBadRequest}, 440 {"args: field of wrong type provided", ibus.Request{Body: []byte(`{"args":{"Text":42}}`)}, "wrong field type", http.StatusBadRequest}, 441 {"args: not an object", ibus.Request{Body: []byte(`{"args":42}`)}, `"args" field must be an object`, http.StatusBadRequest}, 442 {"args: missing at all with a required field", ibus.Request{Body: []byte(`{}`)}, "", http.StatusBadRequest}, 443 {"unloggedArgs: not an object", ibus.Request{Body: []byte(`{"unloggedArgs":42,"args":{"Text":"txt"}}`)}, `"unloggedArgs" field must be an object`, http.StatusBadRequest}, 444 {"unloggedArgs: field of wrong type provided", ibus.Request{Body: []byte(`{"unloggedArgs":{"Password":42},"args":{"Text":"txt"}}`)}, "wrong field type", http.StatusBadRequest}, 445 {"unloggedArgs: missing required field of unlogged args, no unlogged args at all", ibus.Request{Body: []byte(`{"args":{"Text":"txt"}}`)}, "", http.StatusBadRequest}, 446 {"cuds: not an object", ibus.Request{Body: []byte(`{"args":{"Text":"hello"},"unloggedArgs":{"Password":"123"},"cuds":42}`)}, `field 'cuds' must be an array of objects`, http.StatusBadRequest}, 447 } 448 449 for _, c := range cases { 450 t.Run(c.desc, func(t *testing.T) { 451 req := baseReq 452 req.Body = make([]byte, len(baseReq.Body)) 453 copy(req.Body, baseReq.Body) 454 if len(c.AppQName) > 0 { 455 req.AppQName = c.AppQName 456 } 457 if len(c.Body) > 0 { 458 req.Body = make([]byte, len(c.Body)) 459 copy(req.Body, c.Body) 460 } 461 if len(c.Resource) > 0 { 462 req.Resource = c.Resource 463 } 464 resp, sections, secErr, err := app.bus.SendRequest2(app.ctx, req, coreutils.GetTestBusTimeout()) 465 require.NoError(err, c.desc) 466 require.Nil(secErr) 467 require.Nil(sections) 468 require.Equal(c.expectedStatusCode, resp.StatusCode, c.desc) 469 require.Equal(coreutils.ApplicationJSON, resp.ContentType, c.desc) 470 require.Contains(string(resp.Data), jsonEscape(c.expectedMessageLike), c.desc) 471 require.Contains(string(resp.Data), fmt.Sprintf(`"HTTPStatus":%d`, c.expectedStatusCode), c.desc) 472 }) 473 } 474 } 475 476 func TestAuthnz(t *testing.T) { 477 require := require.New(t) 478 479 qNameTestDeniedCDoc := appdef.NewQName(appdef.SysPackage, "TestDeniedCDoc") // the same in core/iauthnzimpl 480 481 qNameAllowedCmd := appdef.NewQName(appdef.SysPackage, "TestAllowedCmd") 482 qNameDeniedCmd := appdef.NewQName(appdef.SysPackage, "TestDeniedCmd") // the same in core/iauthnzimpl 483 app := setUp(t, func(appDef appdef.IAppDefBuilder, cfg *istructsmem.AppConfigType) { 484 appDef.AddCDoc(qNameTestDeniedCDoc) 485 appDef.AddCommand(qNameAllowedCmd) 486 appDef.AddCommand(qNameDeniedCmd) 487 cfg.Resources.Add(istructsmem.NewCommandFunction(qNameAllowedCmd, istructsmem.NullCommandExec)) 488 cfg.Resources.Add(istructsmem.NewCommandFunction(qNameDeniedCmd, istructsmem.NullCommandExec)) 489 }) 490 defer tearDown(app) 491 492 pp := payloads.PrincipalPayload{ 493 Login: "testlogin", 494 SubjectKind: istructs.SubjectKind_User, 495 ProfileWSID: 1, 496 } 497 token, err := app.appTokens.IssueToken(10*time.Second, &pp) 498 require.NoError(err) 499 500 type testCase struct { 501 desc string 502 req ibus.Request 503 expectedStatusCode int 504 } 505 cases := []testCase{ 506 { 507 desc: "403 on cmd EXECUTE forbidden", req: ibus.Request{ 508 Body: []byte(`{}`), 509 AppQName: istructs.AppQName_untill_airs_bp.String(), 510 WSID: 1, 511 Resource: "c.sys.TestDeniedCmd", 512 Header: getAuthHeader(token), 513 }, 514 expectedStatusCode: http.StatusForbidden, 515 }, 516 { 517 desc: "403 on INSERT CUD forbidden", req: ibus.Request{ 518 Body: []byte(`{"cuds":[{"fields":{"sys.ID":1,"sys.QName":"sys.TestDeniedCDoc"}}]}`), 519 AppQName: istructs.AppQName_untill_airs_bp.String(), 520 WSID: 1, 521 Resource: "c.sys.TestAllowedCmd", 522 Header: getAuthHeader(token), 523 }, 524 expectedStatusCode: http.StatusForbidden, 525 }, 526 { 527 desc: "403 if no token for a func that requires authentication", req: ibus.Request{ 528 Body: []byte(`{}`), 529 AppQName: istructs.AppQName_untill_airs_bp.String(), 530 WSID: 1, 531 Resource: "c.sys.TestAllowedCmd", 532 }, 533 expectedStatusCode: http.StatusForbidden, 534 }, 535 } 536 537 for _, c := range cases { 538 t.Run(c.desc, func(t *testing.T) { 539 resp, sections, secErr, err := app.bus.SendRequest2(app.ctx, c.req, coreutils.GetTestBusTimeout()) 540 require.NoError(err) 541 require.Nil(secErr, secErr) 542 require.Nil(sections) 543 log.Println(string(resp.Data)) 544 require.Equal(c.expectedStatusCode, resp.StatusCode) 545 }) 546 } 547 } 548 549 func getAuthHeader(token string) map[string][]string { 550 return map[string][]string{ 551 coreutils.Authorization: { 552 "Bearer " + token, 553 }, 554 } 555 } 556 557 func TestBasicUsage_FuncWithRawArg(t *testing.T) { 558 require := require.New(t) 559 testCmdQName := appdef.NewQName(appdef.SysPackage, "Test") 560 ch := make(chan interface{}) 561 app := setUp(t, func(appDef appdef.IAppDefBuilder, cfg *istructsmem.AppConfigType) { 562 appDef.AddCommand(testCmdQName).SetParam(istructs.QNameRaw) 563 cfg.Resources.Add(istructsmem.NewCommandFunction(testCmdQName, func(args istructs.ExecCommandArgs) (err error) { 564 require.EqualValues("custom content", args.ArgumentObject.AsString(processors.Field_RawObject_Body)) 565 close(ch) 566 return 567 })) 568 }) 569 defer tearDown(app) 570 571 request := ibus.Request{ 572 Body: []byte(`custom content`), 573 AppQName: istructs.AppQName_untill_airs_bp.String(), 574 WSID: 1, 575 Resource: "c.sys.Test", 576 Header: app.sysAuthHeader, 577 } 578 resp, sections, secErr, err := app.bus.SendRequest2(app.ctx, request, coreutils.GetTestBusTimeout()) 579 require.NoError(err) 580 require.Nil(secErr) 581 require.Nil(sections) 582 require.Equal(http.StatusOK, resp.StatusCode) 583 require.Equal(coreutils.ApplicationJSON, resp.ContentType) 584 <-ch 585 } 586 587 func TestRateLimit(t *testing.T) { 588 require := require.New(t) 589 590 qName := appdef.NewQName(appdef.SysPackage, "MyCmd") 591 parsQName := appdef.NewQName(appdef.SysPackage, "Params") 592 593 app := setUp(t, 594 func(appDef appdef.IAppDefBuilder, cfg *istructsmem.AppConfigType) { 595 appDef.AddObject(parsQName) 596 appDef.AddCommand(qName).SetParam(parsQName) 597 cfg.Resources.Add(istructsmem.NewCommandFunction(qName, istructsmem.NullCommandExec)) 598 599 cfg.FunctionRateLimits.AddWorkspaceLimit(qName, istructs.RateLimit{ 600 Period: time.Minute, 601 MaxAllowedPerDuration: 2, 602 }) 603 }) 604 defer tearDown(app) 605 606 request := ibus.Request{ 607 Body: []byte(`{"args":{}}`), 608 AppQName: istructs.AppQName_untill_airs_bp.String(), 609 WSID: 1, 610 Resource: "c.sys.MyCmd", 611 Header: app.sysAuthHeader, 612 } 613 614 // first 2 calls are ok 615 for i := 0; i < 2; i++ { 616 resp, sections, secErr, err := app.bus.SendRequest2(app.ctx, request, coreutils.GetTestBusTimeout()) 617 require.NoError(err) 618 require.Nil(secErr) 619 require.Nil(sections) 620 require.Equal(http.StatusOK, resp.StatusCode) 621 } 622 623 // 3rd exceeds rate limits 624 resp, sections, secErr, err := app.bus.SendRequest2(app.ctx, request, coreutils.GetTestBusTimeout()) 625 require.NoError(err) 626 require.Nil(secErr) 627 require.Nil(sections) 628 require.Equal(http.StatusTooManyRequests, resp.StatusCode) 629 } 630 631 type testApp struct { 632 ctx context.Context 633 cfg *istructsmem.AppConfigType 634 bus ibus.IBus 635 cancel context.CancelFunc 636 done chan struct{} 637 cmdProcService pipeline.IService 638 serviceChannel CommandChannel 639 n10nBroker in10n.IN10nBroker 640 n10nBrokerCleanup func() 641 642 appTokens istructs.IAppTokens 643 sysAuthHeader map[string][]string 644 } 645 646 func tearDown(app testApp) { 647 // завершим command processor IService 648 app.n10nBrokerCleanup() 649 app.cancel() 650 <-app.done 651 } 652 653 // simulate real app behavior 654 func replyBadRequest(sender ibus.ISender, message string) { 655 res := coreutils.NewHTTPErrorf(http.StatusBadRequest, message) 656 sender.SendResponse(ibus.Response{ 657 ContentType: coreutils.ApplicationJSON, 658 StatusCode: http.StatusBadRequest, 659 Data: []byte(res.ToJSON()), 660 }) 661 } 662 663 // test app deployment constants 664 var ( 665 testAppName = istructs.AppQName_untill_airs_bp 666 testAppEngines = [appparts.ProcessorKind_Count]int{10, 10, 10} 667 testAppPartID istructs.PartitionID = 1 668 testAppPartCount istructs.NumAppPartitions = 1 669 ) 670 671 func setUp(t *testing.T, prepare func(appDef appdef.IAppDefBuilder, cfg *istructsmem.AppConfigType)) testApp { 672 require := require.New(t) 673 // command processor - это IService, работающий через CommandChannel(iprocbus.ServiceChannel). Подготовим этот channel 674 serviceChannel := make(CommandChannel) 675 done := make(chan struct{}) 676 677 ctx, cancel := context.WithCancel(context.Background()) 678 679 cfgs := istructsmem.AppConfigsType{} 680 asf := mem.Provide() 681 appStorageProvider := istorageimpl.Provide(asf) 682 683 // build application 684 adb := appdef.New() 685 adb.AddObject(istructs.QNameRaw).AddField(processors.Field_RawObject_Body, appdef.DataKind_string, true, appdef.MaxLen(appdef.MaxFieldLength)) 686 wsdescutil.AddWorkspaceDescriptorStubDef(adb) 687 qNameTestWSKind := appdef.NewQName(appdef.SysPackage, "TestWSKind") 688 adb.AddCDoc(qNameTestWSKind).SetSingleton() 689 cfg := cfgs.AddConfig(istructs.AppQName_untill_airs_bp, adb) 690 cfg.SetNumAppWorkspaces(istructs.DefaultNumAppWorkspaces) 691 if prepare != nil { 692 prepare(adb, cfg) 693 } 694 695 appDef, err := adb.Build() 696 require.NoError(err) 697 698 appStructsProvider := istructsmem.Provide(cfgs, iratesce.TestBucketsFactory, 699 payloads.ProvideIAppTokensFactory(itokensjwt.TestTokensJWT()), appStorageProvider) 700 701 secretReader := isecretsimpl.ProvideSecretReader() 702 n10nBroker, n10nBrokerCleanup := in10nmem.ProvideEx2(in10n.Quotas{ 703 Channels: 1000, 704 ChannelsPerSubject: 10, 705 Subscriptions: 1000, 706 SubscriptionsPerSubject: 10, 707 }, time.Now) 708 709 // prepare the AppParts to borrow AppStructs 710 appParts, appPartsClean, err := appparts.NewWithActualizerWithExtEnginesFactories(appStructsProvider, 711 projectors.NewSyncActualizerFactoryFactory(projectors.ProvideSyncActualizerFactory(), secretReader, n10nBroker), 712 engines.ProvideExtEngineFactories( 713 engines.ExtEngineFactoriesConfig{ 714 AppConfigs: cfgs, 715 WASMCompile: false, 716 })) 717 require.NoError(err) 718 defer appPartsClean() 719 720 appParts.DeployApp(testAppName, appDef, testAppPartCount, testAppEngines) 721 appParts.DeployAppPartitions(testAppName, []istructs.PartitionID{testAppPartID}) 722 723 // command processor работает через ibus.SendResponse -> нам нужна реализация ibus 724 bus := ibusmem.Provide(func(ctx context.Context, sender ibus.ISender, request ibus.Request) { 725 // сымитируем работу реального приложения при приеме запроса-команды 726 cmdQName, err := appdef.ParseQName(request.Resource[2:]) 727 require.NoError(err) 728 appQName, err := istructs.ParseAppQName(request.AppQName) 729 require.NoError(err) 730 tp := appDef.Type(cmdQName) 731 if tp.Kind() == appdef.TypeKind_null { 732 replyBadRequest(sender, "unknown function") 733 return 734 } 735 token := "" 736 if authHeaders, ok := request.Header[coreutils.Authorization]; ok { 737 token = strings.TrimPrefix(authHeaders[0], "Bearer ") 738 } 739 icm := NewCommandMessage(ctx, request.Body, appQName, istructs.WSID(request.WSID), sender, testAppPartID, cmdQName, token, "") 740 serviceChannel <- icm 741 }) 742 743 tokens := itokensjwt.ProvideITokens(itokensjwt.SecretKeyExample, time.Now) 744 appTokens := payloads.ProvideIAppTokensFactory(tokens).New(testAppName) 745 systemToken, err := payloads.GetSystemPrincipalTokenApp(appTokens) 746 require.NoError(err) 747 cmdProcessorFactory := ProvideServiceFactory(appParts, time.Now, n10nBroker, imetrics.Provide(), "vvm", iauthnzimpl.NewDefaultAuthenticator(iauthnzimpl.TestSubjectRolesGetter, iauthnzimpl.TestIsDeviceAllowedFuncs), iauthnzimpl.NewDefaultAuthorizer(), secretReader) 748 cmdProcService := cmdProcessorFactory(serviceChannel, testAppPartID) 749 750 go func() { 751 cmdProcService.Run(ctx) 752 close(done) 753 }() 754 755 as, err := appStructsProvider.AppStructs(istructs.AppQName_untill_airs_bp) 756 require.NoError(err) 757 err = wsdescutil.CreateCDocWorkspaceDescriptorStub(as, testAppPartID, 1, qNameTestWSKind, 1, 1) 758 require.NoError(err) 759 err = wsdescutil.CreateCDocWorkspaceDescriptorStub(as, testAppPartID, 2, qNameTestWSKind, 2, 1) 760 require.NoError(err) 761 762 return testApp{ 763 cfg: cfg, 764 bus: bus, 765 cancel: cancel, 766 ctx: ctx, 767 done: done, 768 cmdProcService: cmdProcService, 769 serviceChannel: serviceChannel, 770 n10nBroker: n10nBroker, 771 n10nBrokerCleanup: n10nBrokerCleanup, 772 appTokens: appTokens, 773 sysAuthHeader: getAuthHeader(systemToken), 774 } 775 } 776 777 func jsonEscape(i string) string { 778 b, err := json.Marshal(i) 779 if err != nil { 780 panic(err) 781 } 782 s := string(b) 783 return s[1 : len(s)-1] 784 }