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 }