github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/sys/it/impl_workspace_test.go (about)

     1  /*
     2   * Copyright (c) 2022-present unTill Pro, Ltd.
     3   */
     4  
     5  package sys_it
     6  
     7  import (
     8  	"fmt"
     9  	"log"
    10  	"strconv"
    11  	"testing"
    12  	"testing/fstest"
    13  
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/voedger/voedger/pkg/appdef"
    17  	"github.com/voedger/voedger/pkg/extensionpoints"
    18  	"github.com/voedger/voedger/pkg/istructs"
    19  	"github.com/voedger/voedger/pkg/sys/authnz"
    20  	"github.com/voedger/voedger/pkg/sys/blobber"
    21  	"github.com/voedger/voedger/pkg/sys/workspace"
    22  	coreutils "github.com/voedger/voedger/pkg/utils"
    23  	it "github.com/voedger/voedger/pkg/vit"
    24  )
    25  
    26  func TestBasicUsage_Workspace(t *testing.T) {
    27  	require := require.New(t)
    28  	vit := it.NewVIT(t, &it.SharedConfig_App1)
    29  	defer vit.TearDown()
    30  
    31  	loginName := vit.NextName()
    32  	wsName := vit.NextName()
    33  	login := vit.SignUp(loginName, "1", istructs.AppQName_test1_app1)
    34  	prn := vit.SignIn(login)
    35  
    36  	t.Run("404 not found on q.sys.QueryChildWorkspaceByName on a non-inited workspace", func(t *testing.T) {
    37  		body := fmt.Sprintf(`
    38  			{
    39  				"args": {
    40  					"WSName": "%s"
    41  				},
    42  				"elements":[
    43  					{
    44  						"fields":["WSName", "WSKind", "WSKindInitializationData", "TemplateName", "TemplateParams", "WSID", "WSError"]
    45  					}
    46  				]
    47  			}`, wsName)
    48  		resp := vit.PostProfile(prn, "q.sys.QueryChildWorkspaceByName", body, coreutils.Expect404())
    49  		resp.Println()
    50  	})
    51  
    52  	t.Run("init user workspace (create a restaurant)", func(t *testing.T) {
    53  		body := fmt.Sprintf(`
    54  		{
    55  			"args": {
    56  				"WSName": "%s",
    57  				"WSKind": "app1pkg.test_ws",
    58  				"WSKindInitializationData": "{\"IntFld\": 10}",
    59  				"TemplateName": "test_template",
    60  				"WSClusterID": 1
    61  			}
    62  		}`, wsName)
    63  		vit.PostProfile(prn, "c.sys.InitChildWorkspace", body)
    64  		ws := vit.WaitForWorkspace(wsName, prn)
    65  
    66  		require.Empty(ws.WSError)
    67  		require.Equal(wsName, ws.Name)
    68  		require.Equal(it.QNameApp1_TestWSKind, ws.Kind)
    69  		require.Equal(`{"IntFld": 10}`, ws.InitDataJSON)
    70  		require.Equal("test_template", ws.TemplateName)
    71  		require.Equal(istructs.ClusterID(1), ws.WSID.ClusterID())
    72  
    73  		t.Run("check the initialized workspace using collection", func(t *testing.T) {
    74  			body = `{"args":{"Schema":"app1pkg.air_table_plan"},"elements":[{"fields":["sys.ID","image","preview"]}]}`
    75  			resp := vit.PostWS(ws, "q.sys.Collection", body)
    76  			require.Len(resp.Sections[0].Elements, 2) // from testTemplate
    77  			appEPs := vit.VVM.AppsExtensionPoints[istructs.AppQName_test1_app1]
    78  			checkDemoAndDemoMinBLOBs(vit, "test_template", appEPs, it.QNameApp1_TestWSKind, resp, ws.WSID, prn.Token)
    79  		})
    80  
    81  		var idOfCDocWSKind int64
    82  
    83  		t.Run("check current cdoc.sys.$wsKind", func(t *testing.T) {
    84  			cdoc, id := vit.GetCDocWSKind(ws)
    85  			idOfCDocWSKind = id
    86  			require.Equal(float64(10), cdoc["IntFld"])
    87  			require.Equal("", cdoc["StrFld"])
    88  			require.Len(cdoc, 2)
    89  		})
    90  
    91  		t.Run("reconfigure the workspace", func(t *testing.T) {
    92  			// CDoc<app1.WSKind> is a singleton
    93  			body = fmt.Sprintf(`
    94  				{
    95  					"cuds": [
    96  						{
    97  							"sys.ID": %d,
    98  							"fields": {
    99  								"sys.QName": "app1pkg.test_ws",
   100  								"IntFld": 42,
   101  								"StrFld": "str"
   102  							}
   103  						}
   104  					]
   105  				}`, idOfCDocWSKind)
   106  			vit.PostWS(ws, "c.sys.CUD", body)
   107  
   108  			// check updated workspace config
   109  			cdoc, _ := vit.GetCDocWSKind(ws)
   110  			require.Len(cdoc, 2)
   111  			require.Equal(float64(42), cdoc["IntFld"])
   112  			require.Equal("str", cdoc["StrFld"])
   113  		})
   114  	})
   115  
   116  	t.Run("create a new workspace with an existing name -> 409 conflict", func(t *testing.T) {
   117  		body := fmt.Sprintf(`{"args": {"WSName": "%s","WSKind": "app1pkg.test_ws","WSKindInitializationData": "{\"WorkStartTime\": \"10\"}","TemplateName": "test","WSClusterID": 1}}`, wsName)
   118  		resp := vit.PostProfile(prn, "c.sys.InitChildWorkspace", body, coreutils.Expect409())
   119  		resp.Println()
   120  	})
   121  
   122  	t.Run("read user workspaces list", func(t *testing.T) {
   123  		body := `{"args":{"Schema":"sys.ChildWorkspace"},"elements":[{"fields":["WSName","WSKind","WSID","WSError"]}]}`
   124  		resp := vit.PostProfile(prn, "q.sys.Collection", body)
   125  		resp.Println()
   126  	})
   127  
   128  	t.Run("400 bad request on create a workspace with kind that is not a QName of a workspace descriptor", func(t *testing.T) {
   129  		wsName := vit.NextName()
   130  		body := fmt.Sprintf(`{"args": {"WSName": "%s","WSKind": "app1pkg.articles","WSKindInitializationData": "{\"WorkStartTime\": \"10\"}","TemplateName": "test","WSClusterID": 1}}`, wsName)
   131  		resp := vit.PostProfile(prn, "c.sys.InitChildWorkspace", body, coreutils.Expect400())
   132  		resp.Println()
   133  	})
   134  }
   135  
   136  func TestWorkspaceAuthorization(t *testing.T) {
   137  	vit := it.NewVIT(t, &it.SharedConfig_App1)
   138  	defer vit.TearDown()
   139  
   140  	ws := vit.WS(istructs.AppQName_test1_app1, "test_ws")
   141  	prn := ws.Owner
   142  
   143  	body := `{"cuds": [{"sys.ID": 1,"fields": {"sys.QName": "app1pkg.test_ws"}}]}`
   144  
   145  	t.Run("403 forbidden", func(t *testing.T) {
   146  		t.Run("workspace is not initialized", func(t *testing.T) {
   147  			// try to exec c.sys.CUD in non-inited ws id 1
   148  			vit.PostApp(istructs.AppQName_test1_app1, 1, "c.sys.CUD", body, coreutils.WithAuthorizeBy(prn.Token), coreutils.Expect403()).Println()
   149  		})
   150  
   151  		t.Run("access denied (wrong wsid)", func(t *testing.T) {
   152  			// create a new login
   153  			login := vit.SignUp(vit.NextName(), "1", istructs.AppQName_test1_app1)
   154  			newPrn := vit.SignIn(login)
   155  
   156  			// try to modify the workspace by the non-owner
   157  			vit.PostApp(istructs.AppQName_test1_app1, ws.WSID, "c.sys.CUD", body, coreutils.WithAuthorizeBy(newPrn.Token), coreutils.Expect403()).Println()
   158  		})
   159  	})
   160  
   161  	t.Run("401 unauthorized", func(t *testing.T) {
   162  		t.Run("token from an another app", func(t *testing.T) {
   163  			login := vit.SignUp(vit.NextName(), "1", istructs.AppQName_test1_app2)
   164  			newPrn := vit.SignIn(login)
   165  			vit.PostApp(istructs.AppQName_test1_app1, ws.WSID, "c.sys.CUD", body, coreutils.WithAuthorizeBy(newPrn.Token), coreutils.Expect401()).Println()
   166  		})
   167  	})
   168  }
   169  
   170  func TestDenyCreateCDocWSKind(t *testing.T) {
   171  	DenyCreateCDocWSKind_Test(t, []appdef.QName{
   172  		authnz.QNameCDoc_WorkspaceKind_UserProfile,
   173  		authnz.QNameCDoc_WorkspaceKind_DeviceProfile,
   174  		authnz.QNameCDoc_WorkspaceKind_AppWorkspace,
   175  	})
   176  }
   177  
   178  func TestDenyCUDCDocOwnerModification(t *testing.T) {
   179  	vit := it.NewVIT(t, &it.SharedConfig_App1)
   180  	defer vit.TearDown()
   181  
   182  	ws := vit.WS(istructs.AppQName_test1_app1, "test_ws")
   183  
   184  	t.Run("CDoc<ChildWorkspace>", func(t *testing.T) {
   185  		// try to modify CDoc<ChildWorkspace>
   186  		_, idOfCDocWSKind := vit.GetCDocChildWorkspace(ws)
   187  		body := fmt.Sprintf(`{"cuds":[{"sys.ID":%d,"fields":{"WSName":"new name"}}]}`, idOfCDocWSKind) // intFld is declared in vit.SharedConfig_Simple
   188  		vit.PostProfile(ws.Owner, "c.sys.CUD", body, coreutils.Expect403()).Println()
   189  	})
   190  
   191  	// note: unable to work with CDoc<Login>
   192  }
   193  
   194  func TestWorkspaceTemplatesValidationErrors(t *testing.T) {
   195  	dummyFile := &fstest.MapFile{}
   196  	cases := []struct {
   197  		desc       string
   198  		blobs      []string
   199  		noDataFile bool
   200  		data       string
   201  		wsInitData string
   202  	}{
   203  		{desc: "no data file", noDataFile: true},
   204  		{desc: "malformed JSON in data file", data: "wrong"},
   205  		{desc: "blob fn format: no ID", blobs: []string{"image.png"}},
   206  		{desc: "blob fn format: no ID", blobs: []string{"_image.png"}},
   207  		{desc: "blob fn format: no blob field", blobs: []string{"42.png"}},
   208  		{desc: "blob fn format: no blob field", blobs: []string{"42_.png"}},
   209  		{desc: "blob fn format: wrong ID", blobs: []string{"sdf_image.png"}},
   210  		{desc: "unknown blob field", blobs: []string{"42_unknown.png"}, data: `[{"sys.ID":42}]`},
   211  		{desc: "orphaned blob", blobs: []string{"43_image.png"}, data: `[{"sys.ID":42}]`},
   212  		{desc: "duplicate blob", blobs: []string{"42_image.png", "42_image.jpg"}, data: `[{"sys.ID":42}]`},
   213  		{desc: "record with no sys.ID field in data file", data: `[{"sys.IsActive":true}]`},
   214  
   215  		// TODO: the following was tested by waiting for the workspace error. Find out how to test it quickly
   216  		// {desc: "invalid wsKindInitializationData", data: `[{"sys.ID":42}]`, wsInitData: `wrong`},
   217  		// {desc: "unknown field in wsKindInitializationData", data: `[{"sys.ID":42}]`, wsInitData: `{"unknown": "10"}`},
   218  		// {desc: "unsupported data type in wsKindInitializationData", data: `[{"sys.ID":42}]`, wsInitData: `{"IntFld": {}}`},
   219  	}
   220  
   221  	epWSTemplates := extensionpoints.NewRootExtensionPoint()
   222  	epTestWSKindTemplates := epWSTemplates.ExtensionPoint(it.QNameApp1_TestWSKind)
   223  	for i, c := range cases {
   224  		str := strconv.Itoa(i)
   225  		fs := fstest.MapFS{}
   226  		if len(c.data) > 0 {
   227  			fs["data.json"] = &fstest.MapFile{Data: []byte(c.data)}
   228  		} else if !c.noDataFile {
   229  			fs["data.json"] = &fstest.MapFile{Data: []byte("[]")}
   230  		}
   231  		for _, df := range c.blobs {
   232  			fs[df] = dummyFile
   233  		}
   234  		epTestWSKindTemplates.AddNamed("test"+str, fs)
   235  	}
   236  
   237  	for i, c := range cases {
   238  		t.Run(c.desc, func(t *testing.T) {
   239  			fs := fstest.MapFS{}
   240  			if len(c.data) > 0 {
   241  				fs["data.json"] = &fstest.MapFile{Data: []byte(c.data)}
   242  			} else if !c.noDataFile {
   243  				fs["data.json"] = &fstest.MapFile{Data: []byte("[]")}
   244  			}
   245  			for _, df := range c.blobs {
   246  				fs[df] = dummyFile
   247  			}
   248  			str := strconv.Itoa(i)
   249  			_, _, err := workspace.ValidateTemplate("test"+str, epTestWSKindTemplates, it.QNameApp1_TestWSKind)
   250  			require.Error(t, err)
   251  			log.Println(err)
   252  		})
   253  	}
   254  
   255  	t.Run("no template for workspace kind", func(t *testing.T) {
   256  		_, _, err := workspace.ValidateTemplate("test", epTestWSKindTemplates, appdef.NewQName("sys", "unknownKind"))
   257  		require.Error(t, err)
   258  		log.Println(err)
   259  	})
   260  }
   261  
   262  func checkDemoAndDemoMinBLOBs(vit *it.VIT, templateName string, ep extensionpoints.IExtensionPoint, wsKind appdef.QName,
   263  	resp *coreutils.FuncResponse, wsid istructs.WSID, token string) {
   264  	require := require.New(vit.T)
   265  	blobs, _, err := workspace.ValidateTemplate(templateName, ep, wsKind)
   266  	require.NoError(err)
   267  	require.Len(blobs, 4)
   268  	blobsMap := map[string]blobber.StoredBLOB{}
   269  	for _, templateBLOB := range blobs {
   270  		blobsMap[string(templateBLOB.Content)] = templateBLOB
   271  	}
   272  	rowIdx := 0
   273  	for _, temp := range blobs {
   274  		switch temp.RecordID {
   275  		// IDs are taken from the actual templates
   276  		case 1:
   277  			rowIdx = 0
   278  		case 2:
   279  			rowIdx = 1
   280  		default:
   281  			vit.T.Fatal(temp.RecordID)
   282  		}
   283  		var fieldIdx int
   284  		if temp.FieldName == "image" {
   285  			fieldIdx = 1
   286  		} else {
   287  			fieldIdx = 2
   288  		}
   289  		blobID := istructs.RecordID(resp.SectionRow(rowIdx)[fieldIdx].(float64))
   290  		uploadedBLOB := vit.GetBLOB(istructs.AppQName_test1_app1, wsid, blobID, token)
   291  		templateBLOB := blobsMap[string(uploadedBLOB.Content)]
   292  		require.Equal(templateBLOB.Name, uploadedBLOB.Name)
   293  		require.Equal(templateBLOB.MimeType, uploadedBLOB.MimeType)
   294  		delete(blobsMap, string(uploadedBLOB.Content))
   295  		rowIdx++
   296  	}
   297  	require.Empty(blobsMap)
   298  }