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  }