github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/data/references_test.go (about) 1 package data 2 3 import ( 4 "net/url" 5 "testing" 6 "time" 7 8 "github.com/cozy/cozy-stack/model/instance" 9 "github.com/cozy/cozy-stack/model/vfs" 10 "github.com/cozy/cozy-stack/pkg/config/config" 11 "github.com/cozy/cozy-stack/pkg/consts" 12 "github.com/cozy/cozy-stack/pkg/couchdb" 13 "github.com/cozy/cozy-stack/tests/testutils" 14 "github.com/gavv/httpexpect/v2" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 ) 18 19 func TestReferences(t *testing.T) { 20 if testing.Short() { 21 t.Skip("an instance is required for this test: test skipped due to the use of --short flag") 22 } 23 24 const Type = "io.cozy.events" 25 const ID = "4521C325F6478E45" 26 27 config.UseTestFile(t) 28 testutils.NeedCouchdb(t) 29 setup := testutils.NewSetup(t, t.Name()) 30 testInstance := setup.GetTestInstance() 31 scope := "io.cozy.doctypes io.cozy.files io.cozy.events " + 32 "io.cozy.anothertype io.cozy.nottype" 33 34 _, token := setup.GetTestClient(scope) 35 ts := setup.GetTestServer("/data", Routes) 36 t.Cleanup(ts.Close) 37 38 _ = couchdb.ResetDB(testInstance, Type) 39 _ = couchdb.CreateNamedDoc(testInstance, &couchdb.JSONDoc{ 40 Type: Type, 41 M: map[string]interface{}{ 42 "_id": ID, 43 "test": "testvalue", 44 }, 45 }) 46 47 t.Run("ListReferencesHandler", func(t *testing.T) { 48 e := testutils.CreateTestClient(t, ts.URL) 49 50 // Make doc 51 doc := getDocForTest(Type, testInstance) 52 53 // Make Files 54 makeReferencedTestFile(t, testInstance, doc, "testtoref2.txt") 55 makeReferencedTestFile(t, testInstance, doc, "testtoref3.txt") 56 makeReferencedTestFile(t, testInstance, doc, "testtoref4.txt") 57 makeReferencedTestFile(t, testInstance, doc, "testtoref5.txt") 58 59 // Simple query 60 obj := e.GET("/data/"+doc.DocType()+"/"+doc.ID()+"/relationships/references"). 61 WithHeader("Authorization", "Bearer "+token). 62 Expect().Status(200). 63 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 64 Object() 65 66 obj.Path("$.meta.count").Equal(4) 67 obj.Value("data").Array().Length().Equal(4) 68 obj.Value("links").Object().NotContainsKey("next") 69 70 // Use the page limit 71 obj = e.GET("/data/"+doc.DocType()+"/"+doc.ID()+"/relationships/references"). 72 WithQuery("page[limit]", 3). 73 WithHeader("Authorization", "Bearer "+token). 74 Expect().Status(200). 75 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 76 Object() 77 78 obj.Path("$.meta.count").Equal(4) 79 obj.Value("data").Array().Length().Equal(3) 80 rawNext := obj.Value("links").Object().Value("next").String().NotEmpty().Raw() 81 82 nextURL, err := url.Parse(rawNext) 83 require.NoError(t, err) 84 85 // Use the bookmark 86 obj = e.GET(nextURL.Path). 87 WithQueryString(nextURL.RawQuery). 88 WithHeader("Authorization", "Bearer "+token). 89 Expect().Status(200). 90 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 91 Object() 92 93 obj.Value("data").Array().Length().Equal(1) 94 obj.Value("links").Object().NotContainsKey("next") 95 96 // Include the files 97 obj = e.GET("/data/"+doc.DocType()+"/"+doc.ID()+"/relationships/references"). 98 WithQuery("include", "files"). 99 WithHeader("Authorization", "Bearer "+token). 100 Expect().Status(200). 101 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 102 Object() 103 104 obj.Value("included").Array().Length().Equal(4) 105 obj.Path("$.included[0].id").String().NotEmpty() 106 }) 107 108 t.Run("AddReferencesHandler", func(t *testing.T) { 109 e := testutils.CreateTestClient(t, ts.URL) 110 111 // Make doc 112 doc := getDocForTest(Type, testInstance) 113 114 // Make File 115 name := "testtoref.txt" 116 dirID := consts.RootDirID 117 mime, class := vfs.ExtractMimeAndClassFromFilename(name) 118 filedoc, err := vfs.NewFileDoc(name, dirID, -1, nil, mime, class, time.Now(), false, false, false, nil) 119 require.NoError(t, err) 120 121 f, err := testInstance.VFS().CreateFile(filedoc, nil) 122 require.NoError(t, err) 123 require.NoError(t, f.Close()) 124 125 // update it 126 e.POST("/data/"+doc.DocType()+"/"+doc.ID()+"/relationships/references"). 127 WithHeader("Authorization", "Bearer "+token). 128 WithHeader("Content-Type", "application/vnd.api+json"). 129 WithBytes([]byte(`{ 130 "data": { 131 "id": "` + filedoc.ID() + `", 132 "type": "` + filedoc.DocType() + `" 133 } 134 }`)). 135 Expect().Status(204) 136 137 fdoc, err := testInstance.VFS().FileByID(filedoc.ID()) 138 assert.NoError(t, err) 139 assert.Len(t, fdoc.ReferencedBy, 1) 140 }) 141 142 t.Run("RemoveReferencesHandler", func(t *testing.T) { 143 e := testutils.CreateTestClient(t, ts.URL) 144 145 // Make doc 146 doc := getDocForTest(Type, testInstance) 147 148 // Make Files 149 f6 := makeReferencedTestFile(t, testInstance, doc, "testtoref6.txt") 150 f7 := makeReferencedTestFile(t, testInstance, doc, "testtoref7.txt") 151 f8 := makeReferencedTestFile(t, testInstance, doc, "testtoref8.txt") 152 f9 := makeReferencedTestFile(t, testInstance, doc, "testtoref9.txt") 153 154 // update it 155 e.DELETE("/data/"+doc.DocType()+"/"+doc.ID()+"/relationships/references"). 156 WithHeader("Authorization", "Bearer "+token). 157 WithHeader("Content-Type", "application/vnd.api+json"). 158 WithBytes([]byte(`{ 159 "data": [ 160 {"id": "` + f8 + `", "type": "` + consts.Files + `"}, 161 {"id": "` + f6 + `", "type": "` + consts.Files + `"} 162 ] 163 }`)). 164 Expect().Status(204) 165 166 fdoc6, err := testInstance.VFS().FileByID(f6) 167 assert.NoError(t, err) 168 assert.Len(t, fdoc6.ReferencedBy, 0) 169 fdoc8, err := testInstance.VFS().FileByID(f8) 170 assert.NoError(t, err) 171 assert.Len(t, fdoc8.ReferencedBy, 0) 172 173 fdoc7, err := testInstance.VFS().FileByID(f7) 174 assert.NoError(t, err) 175 assert.Len(t, fdoc7.ReferencedBy, 1) 176 fdoc9, err := testInstance.VFS().FileByID(f9) 177 assert.NoError(t, err) 178 assert.Len(t, fdoc9.ReferencedBy, 1) 179 }) 180 181 t.Run("ReferencesWithSlash", func(t *testing.T) { 182 e := testutils.CreateTestClient(t, ts.URL) 183 184 // Make File 185 name := "test-ref-with-slash.txt" 186 dirID := consts.RootDirID 187 mime, class := vfs.ExtractMimeAndClassFromFilename(name) 188 filedoc, err := vfs.NewFileDoc(name, dirID, -1, nil, mime, class, time.Now(), false, false, false, nil) 189 require.NoError(t, err) 190 191 f, err := testInstance.VFS().CreateFile(filedoc, nil) 192 require.NoError(t, err) 193 require.NoError(t, f.Close()) 194 195 // Add a reference to io.cozy.apps/foobar 196 e.POST("/data/"+Type+"/io.cozy.apps%2ffoobar/relationships/references"). 197 WithHeader("Authorization", "Bearer "+token). 198 WithHeader("Content-Type", "application/vnd.api+json"). 199 WithBytes([]byte(`{ 200 "data": [ 201 {"id": "` + filedoc.ID() + `", "type": "` + filedoc.DocType() + `"} 202 ] 203 }`)). 204 Expect().Status(204) 205 206 fdoc, err := testInstance.VFS().FileByID(filedoc.ID()) 207 assert.NoError(t, err) 208 assert.Len(t, fdoc.ReferencedBy, 1) 209 assert.Equal(t, "io.cozy.apps/foobar", fdoc.ReferencedBy[0].ID) 210 211 // Check that we can find the reference with / 212 obj := e.GET("/data/"+Type+"/io.cozy.apps%2ffoobar/relationships/references"). 213 WithHeader("Authorization", "Bearer "+token). 214 Expect().Status(200). 215 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 216 Object() 217 218 obj.Path("$.meta.count").Equal(1) 219 obj.Value("data").Array().Length().Equal(1) 220 obj.Path("$.data[0].id").Equal(fdoc.ID()) 221 222 // Try again, but this time encode / as %2F instead of %2f 223 obj = e.GET("/data/"+Type+"/io.cozy.apps%2Ffoobar/relationships/references"). 224 WithHeader("Authorization", "Bearer "+token). 225 Expect().Status(200). 226 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 227 Object() 228 229 obj.Path("$.meta.count").Equal(1) 230 obj.Value("data").Array().Length().Equal(1) 231 obj.Path("$.data[0].id").Equal(fdoc.ID()) 232 233 // Add dummy references on io.cozy.apps%2ffoobaz and io.cozy.apps%2Ffooqux 234 foobazRef := couchdb.DocReference{ 235 ID: "io.cozy.apps%2ffoobaz", 236 Type: Type, 237 } 238 fooquxRef := couchdb.DocReference{ 239 ID: "io.cozy.apps%2Ffooqux", 240 Type: Type, 241 } 242 fdoc.ReferencedBy = append(fdoc.ReferencedBy, foobazRef, fooquxRef) 243 err = couchdb.UpdateDoc(testInstance, fdoc) 244 assert.NoError(t, err) 245 246 // Check that we can find the reference with %2f 247 obj = e.GET("/data/"+Type+"/io.cozy.apps%2ffoobar/relationships/references"). 248 WithHeader("Authorization", "Bearer "+token). 249 Expect().Status(200). 250 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 251 Object() 252 253 obj.Path("$.meta.count").Equal(1) 254 obj.Value("data").Array().Length().Equal(1) 255 obj.Path("$.data[0].id").Equal(fdoc.ID()) 256 257 // Check that we can find the reference with %2F 258 obj = e.GET("/data/"+Type+"/io.cozy.apps%2Ffoobar/relationships/references"). 259 WithHeader("Authorization", "Bearer "+token). 260 Expect().Status(200). 261 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 262 Object() 263 264 obj.Path("$.meta.count").Equal(1) 265 obj.Value("data").Array().Length().Equal(1) 266 obj.Path("$.data[0].id").Equal(fdoc.ID()) 267 268 // Remove the reference with a / 269 e.DELETE("/data/"+Type+"/io.cozy.apps%2Ffoobar/relationships/references"). 270 WithHeader("Authorization", "Bearer "+token). 271 WithHeader("Content-Type", "application/vnd.api+json"). 272 WithBytes([]byte(`{ 273 "data": [ 274 {"id": "` + fdoc.ID() + `", "type": "` + consts.Files + `"} 275 ] 276 }`)). 277 Expect().Status(204) 278 279 // Remove the reference with a %2f 280 e.DELETE("/data/"+Type+"/io.cozy.apps%2ffoobaz/relationships/references"). 281 WithHeader("Authorization", "Bearer "+token). 282 WithHeader("Content-Type", "application/vnd.api+json"). 283 WithBytes([]byte(`{ 284 "data": [ 285 {"id": "` + fdoc.ID() + `", "type": "` + consts.Files + `"} 286 ] 287 }`)). 288 Expect().Status(204) 289 290 // Remove the reference with a %2F 291 e.DELETE("/data/"+Type+"/io.cozy.apps%2Ffooqux/relationships/references"). 292 WithHeader("Authorization", "Bearer "+token). 293 WithHeader("Content-Type", "application/vnd.api+json"). 294 WithBytes([]byte(`{ 295 "data": [ 296 {"id": "` + fdoc.ID() + `", "type": "` + consts.Files + `"} 297 ] 298 }`)). 299 Expect().Status(204) 300 301 // Check that all the references have been removed 302 fdoc2, err := testInstance.VFS().FileByID(fdoc.ID()) 303 assert.NoError(t, err) 304 assert.Len(t, fdoc2.ReferencedBy, 0) 305 }) 306 } 307 308 func makeReferencedTestFile(t *testing.T, instance *instance.Instance, doc couchdb.Doc, name string) string { 309 dirID := consts.RootDirID 310 mime, class := vfs.ExtractMimeAndClassFromFilename(name) 311 filedoc, err := vfs.NewFileDoc(name, dirID, -1, nil, mime, class, time.Now(), false, false, false, nil) 312 if !assert.NoError(t, err) { 313 return "" 314 } 315 316 filedoc.ReferencedBy = []couchdb.DocReference{ 317 { 318 ID: doc.ID(), 319 Type: doc.DocType(), 320 }, 321 } 322 323 f, err := instance.VFS().CreateFile(filedoc, nil) 324 if !assert.NoError(t, err) { 325 return "" 326 } 327 if err = f.Close(); !assert.NoError(t, err) { 328 return "" 329 } 330 return filedoc.ID() 331 }