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  }