github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/data/data_test.go (about)

     1  package data
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/cozy/cozy-stack/model/instance"
    10  	"github.com/cozy/cozy-stack/pkg/config/config"
    11  	"github.com/cozy/cozy-stack/pkg/couchdb"
    12  	"github.com/cozy/cozy-stack/tests/testutils"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  	"golang.org/x/sync/errgroup"
    16  )
    17  
    18  type M map[string]interface{}
    19  type S []interface{}
    20  
    21  func TestData(t *testing.T) {
    22  	if testing.Short() {
    23  		t.Skip("an instance is required for this test: test skipped due to the use of --short flag")
    24  	}
    25  
    26  	const Type = "io.cozy.events"
    27  	const ID = "4521C325F6478E45"
    28  
    29  	config.UseTestFile(t)
    30  	testutils.NeedCouchdb(t)
    31  	setup := testutils.NewSetup(t, t.Name())
    32  	testInstance := setup.GetTestInstance()
    33  	scope := "io.cozy.doctypes io.cozy.files io.cozy.events " +
    34  		"io.cozy.anothertype io.cozy.nottype"
    35  
    36  	_, token := setup.GetTestClient(scope)
    37  	ts := setup.GetTestServer("/data", Routes)
    38  	t.Cleanup(ts.Close)
    39  
    40  	_ = couchdb.ResetDB(testInstance, Type)
    41  	_ = couchdb.CreateNamedDoc(testInstance, &couchdb.JSONDoc{
    42  		Type: Type,
    43  		M: map[string]interface{}{
    44  			"_id":  ID,
    45  			"test": "testvalue",
    46  		},
    47  	})
    48  
    49  	t.Run("SuccessGet", func(t *testing.T) {
    50  		e := testutils.CreateTestClient(t, ts.URL)
    51  
    52  		e.GET("/data/"+Type+"/"+ID).
    53  			WithHeader("Authorization", "Bearer "+token).
    54  			Expect().Status(200).
    55  			JSON().Object().
    56  			ValueEqual("test", "testvalue")
    57  	})
    58  
    59  	t.Run("GetForMissingDoc", func(t *testing.T) {
    60  		e := testutils.CreateTestClient(t, ts.URL)
    61  
    62  		e.GET("/data/no.such.doctype/id").
    63  			Expect().Status(401)
    64  
    65  		e.GET("/data/" + Type + "/no.such.id").
    66  			Expect().Status(401)
    67  
    68  		e.GET("/data/"+Type+"/no.such.id").
    69  			WithHeader("Authorization", "Bearer "+token).
    70  			Expect().Status(404)
    71  	})
    72  
    73  	t.Run("GetWithSlash", func(t *testing.T) {
    74  		e := testutils.CreateTestClient(t, ts.URL)
    75  
    76  		err := couchdb.CreateNamedDoc(testInstance, &couchdb.JSONDoc{
    77  			Type: Type, M: map[string]interface{}{
    78  				"_id":  "with/slash",
    79  				"test": "valueslash",
    80  			}})
    81  		require.NoError(t, err)
    82  
    83  		e.GET("/data/"+Type+"/with%2Fslash").
    84  			WithHeader("Authorization", "Bearer "+token).
    85  			Expect().Status(200).
    86  			JSON().Object().
    87  			ValueEqual("test", "valueslash")
    88  	})
    89  
    90  	t.Run("WrongDoctype", func(t *testing.T) {
    91  		e := testutils.CreateTestClient(t, ts.URL)
    92  
    93  		_ = couchdb.DeleteDB(testInstance, "io.cozy.nottype")
    94  
    95  		e.GET("/data/io.cozy.nottype/"+ID).
    96  			WithHeader("Authorization", "Bearer "+token).
    97  			Expect().Status(404).
    98  			JSON().Object().
    99  			ValueEqual("error", "not_found").
   100  			ValueEqual("reason", "wrong_doctype")
   101  	})
   102  
   103  	t.Run("UnderscoreName", func(t *testing.T) {
   104  		e := testutils.CreateTestClient(t, ts.URL)
   105  
   106  		e.GET("/data/"+Type+"/_foo").
   107  			WithHeader("Authorization", "Bearer "+token).
   108  			Expect().Status(400)
   109  	})
   110  
   111  	t.Run("VFSDoctype", func(t *testing.T) {
   112  		e := testutils.CreateTestClient(t, ts.URL)
   113  
   114  		e.POST("/data/io.cozy.files/").
   115  			WithHeader("Authorization", "Bearer "+token).
   116  			WithHeader("Content-Type", "application/json").
   117  			WithBytes([]byte(`{ "wrong-vfs": "structure" }`)).
   118  			Expect().Status(403).
   119  			JSON().Object().
   120  			Value("error").String().Contains("reserved")
   121  	})
   122  
   123  	t.Run("WrongID", func(t *testing.T) {
   124  		e := testutils.CreateTestClient(t, ts.URL)
   125  
   126  		e.GET("/data/"+Type+"/NOTID").
   127  			WithHeader("Authorization", "Bearer "+token).
   128  			Expect().Status(404).
   129  			JSON().Object().
   130  			ValueEqual("error", "not_found").
   131  			ValueEqual("reason", "missing")
   132  	})
   133  
   134  	t.Run("SuccessCreateKnownDoctype", func(t *testing.T) {
   135  		e := testutils.CreateTestClient(t, ts.URL)
   136  
   137  		obj := e.POST("/data/"+Type+"/").
   138  			WithHeader("Authorization", "Bearer "+token).
   139  			WithHeader("Content-Type", "application/json").
   140  			WithBytes([]byte(`{ "somefield": "avalue" }`)).
   141  			Expect().Status(201).
   142  			JSON().Object()
   143  
   144  		obj.ValueEqual("ok", true)
   145  		obj.Value("id").String().NotEmpty()
   146  		obj.ValueEqual("type", Type)
   147  		obj.Value("rev").String().NotEmpty()
   148  
   149  		var data couchdb.JSONDoc
   150  
   151  		obj.Value("data").Decode(&data)
   152  		obj.ValueEqual("id", data.ID())
   153  		obj.ValueEqual("type", data.Type)
   154  		obj.ValueEqual("rev", data.Rev())
   155  		assert.Equal(t, "avalue", data.Get("somefield"))
   156  	})
   157  
   158  	t.Run("SuccessCreateUnknownDoctype", func(t *testing.T) {
   159  		e := testutils.CreateTestClient(t, ts.URL)
   160  
   161  		type2 := "io.cozy.anothertype"
   162  		obj := e.POST("/data/"+type2+"/").
   163  			WithHeader("Authorization", "Bearer "+token).
   164  			WithHeader("Content-Type", "application/json").
   165  			WithBytes([]byte(`{ "somefield": "avalue" }`)).
   166  			Expect().Status(201).
   167  			JSON().Object()
   168  
   169  		obj.ValueEqual("ok", true)
   170  		obj.Value("id").String().NotEmpty()
   171  		obj.ValueEqual("type", type2)
   172  		obj.Value("rev").String().NotEmpty()
   173  
   174  		var data couchdb.JSONDoc
   175  
   176  		obj.Value("data").Decode(&data)
   177  		obj.ValueEqual("id", data.ID())
   178  		obj.ValueEqual("type", data.Type)
   179  		obj.ValueEqual("rev", data.Rev())
   180  		assert.Equal(t, "avalue", data.Get("somefield"))
   181  	})
   182  
   183  	t.Run("WrongCreateWithID", func(t *testing.T) {
   184  		e := testutils.CreateTestClient(t, ts.URL)
   185  
   186  		e.POST("/data/"+Type+"/").
   187  			WithHeader("Authorization", "Bearer "+token).
   188  			WithHeader("Content-Type", "application/json").
   189  			WithBytes([]byte(`{ 
   190          "_id": "this-should-not-be-an-id",
   191          "somefield": "avalue"
   192        }`)).
   193  			Expect().Status(400)
   194  	})
   195  
   196  	t.Run("SuccessUpdate", func(t *testing.T) {
   197  		e := testutils.CreateTestClient(t, ts.URL)
   198  
   199  		// Get revision
   200  		doc := getDocForTest(Type, testInstance)
   201  
   202  		// update it
   203  		obj := e.PUT("/data/"+doc.DocType()+"/"+doc.ID()).
   204  			WithHeader("Authorization", "Bearer "+token).
   205  			WithHeader("Content-Type", "application/json").
   206  			WithJSON(map[string]interface{}{
   207  				"_id":       doc.ID(),
   208  				"_rev":      doc.Rev(),
   209  				"test":      doc.Get("test"),
   210  				"somefield": "anewvalue",
   211  			}).
   212  			Expect().Status(200).
   213  			JSON().Object()
   214  
   215  		obj.NotContainsKey("error")
   216  		obj.ValueEqual("id", doc.ID())
   217  		obj.ValueEqual("ok", true)
   218  		obj.Value("rev").String().NotEmpty()
   219  		obj.ValueNotEqual("rev", doc.Rev())
   220  
   221  		var data couchdb.JSONDoc
   222  
   223  		obj.Value("data").Decode(&data)
   224  		obj.ValueEqual("id", data.ID())
   225  		obj.ValueEqual("type", data.Type)
   226  		obj.ValueEqual("rev", data.Rev())
   227  		assert.Equal(t, "anewvalue", data.Get("somefield"))
   228  	})
   229  
   230  	t.Run("WrongIDInDocUpdate", func(t *testing.T) {
   231  		e := testutils.CreateTestClient(t, ts.URL)
   232  
   233  		// Get revision
   234  		doc := getDocForTest(Type, testInstance)
   235  
   236  		// update it
   237  		e.PUT("/data/"+doc.DocType()+"/"+doc.ID()).
   238  			WithHeader("Authorization", "Bearer "+token).
   239  			WithHeader("Content-Type", "application/json").
   240  			WithJSON(map[string]interface{}{
   241  				"_id":       "this is not the id in the URL",
   242  				"_rev":      doc.Rev(),
   243  				"test":      doc.M["test"],
   244  				"somefield": "anewvalue",
   245  			}).
   246  			Expect().Status(400)
   247  	})
   248  
   249  	t.Run("CreateDocWithAFixedID", func(t *testing.T) {
   250  		e := testutils.CreateTestClient(t, ts.URL)
   251  
   252  		// update it
   253  		obj := e.PUT("/data/"+Type+"/specific-id").
   254  			WithHeader("Authorization", "Bearer "+token).
   255  			WithHeader("Content-Type", "application/json").
   256  			WithJSON(map[string]interface{}{
   257  				"test":      "value",
   258  				"somefield": "anewvalue",
   259  			}).
   260  			Expect().Status(200).
   261  			JSON().Object()
   262  
   263  		obj.NotContainsKey("error")
   264  		obj.ValueEqual("id", "specific-id")
   265  		obj.ValueEqual("ok", true)
   266  		obj.Value("rev").String().NotEmpty()
   267  
   268  		var data couchdb.JSONDoc
   269  		obj.Value("data").Decode(&data)
   270  		obj.ValueEqual("id", data.ID())
   271  		obj.ValueEqual("type", data.Type)
   272  		obj.ValueEqual("rev", data.Rev())
   273  		assert.Equal(t, "anewvalue", data.Get("somefield"))
   274  	})
   275  
   276  	t.Run("NoRevInDocUpdate", func(t *testing.T) {
   277  		e := testutils.CreateTestClient(t, ts.URL)
   278  
   279  		// Get revision
   280  		doc := getDocForTest(Type, testInstance)
   281  
   282  		// update it
   283  		e.PUT("/data/"+doc.DocType()+"/"+doc.ID()).
   284  			WithHeader("Authorization", "Bearer "+token).
   285  			WithHeader("Content-Type", "application/json").
   286  			WithJSON(map[string]interface{}{
   287  				"_id":       doc.ID(),
   288  				"test":      doc.M["test"],
   289  				"somefield": "anewvalue",
   290  			}).
   291  			Expect().Status(400)
   292  	})
   293  
   294  	t.Run("PreviousRevInDocUpdate", func(t *testing.T) {
   295  		e := testutils.CreateTestClient(t, ts.URL)
   296  
   297  		// Get revision
   298  		doc := getDocForTest(Type, testInstance)
   299  		firstRev := doc.Rev()
   300  
   301  		// correcly update it
   302  		e.PUT("/data/"+doc.DocType()+"/"+doc.ID()).
   303  			WithHeader("Authorization", "Bearer "+token).
   304  			WithHeader("Content-Type", "application/json").
   305  			WithJSON(map[string]interface{}{
   306  				"_id":       doc.ID(),
   307  				"_rev":      doc.Rev(),
   308  				"somefield": "anewvalue",
   309  			}).
   310  			Expect().Status(200)
   311  
   312  		// update it
   313  		e.PUT("/data/"+doc.DocType()+"/"+doc.ID()).
   314  			WithHeader("Authorization", "Bearer "+token).
   315  			WithHeader("Content-Type", "application/json").
   316  			WithJSON(map[string]interface{}{
   317  				"_id":       doc.ID(),
   318  				"_rev":      firstRev,
   319  				"somefield": "anewvalue2",
   320  			}).
   321  			Expect().Status(409)
   322  	})
   323  
   324  	t.Run("SuccessDeleteIfMatch", func(t *testing.T) {
   325  		e := testutils.CreateTestClient(t, ts.URL)
   326  
   327  		// Get revision
   328  		doc := getDocForTest(Type, testInstance)
   329  		rev := doc.Rev()
   330  
   331  		// Do deletion
   332  		obj := e.DELETE("/data/"+doc.DocType()+"/"+doc.ID()).
   333  			WithHeader("Authorization", "Bearer "+token).
   334  			WithHeader("If-Match", rev).
   335  			Expect().Status(200).
   336  			JSON().Object()
   337  
   338  		obj.ValueEqual("id", doc.ID())
   339  		obj.ValueEqual("ok", true)
   340  		obj.ValueEqual("deleted", true)
   341  		obj.ValueNotEqual("rev", doc.Rev())
   342  	})
   343  
   344  	t.Run("FailDeleteIfNotMatch", func(t *testing.T) {
   345  		e := testutils.CreateTestClient(t, ts.URL)
   346  
   347  		// Get revision
   348  		doc := getDocForTest(Type, testInstance)
   349  
   350  		// Do deletion
   351  		e.DELETE("/data/"+doc.DocType()+"/"+doc.ID()).
   352  			WithHeader("Authorization", "Bearer "+token).
   353  			WithHeader("If-Match", "1-238238232322121"). // invalid rev
   354  			Expect().Status(409)
   355  	})
   356  
   357  	t.Run("FailDeleteIfHeaderAndRevMismatch", func(t *testing.T) {
   358  		e := testutils.CreateTestClient(t, ts.URL)
   359  
   360  		// Get revision
   361  		doc := getDocForTest(Type, testInstance)
   362  
   363  		// Do deletion
   364  		e.DELETE("/data/"+doc.DocType()+"/"+doc.ID()).
   365  			WithQuery("rev", "1-238238232322121").
   366  			WithHeader("Authorization", "Bearer "+token).
   367  			WithHeader("If-Match", "1-23823823231"). // not the same rev
   368  			Expect().Status(400)
   369  	})
   370  
   371  	t.Run("FailDeleteIfNoRev", func(t *testing.T) {
   372  		e := testutils.CreateTestClient(t, ts.URL)
   373  
   374  		// Get revision
   375  		doc := getDocForTest(Type, testInstance)
   376  
   377  		// Do deletion
   378  		e.DELETE("/data/"+doc.DocType()+"/"+doc.ID()).
   379  			WithHeader("Authorization", "Bearer "+token).
   380  			Expect().Status(400)
   381  	})
   382  
   383  	t.Run("DefineIndex", func(t *testing.T) {
   384  		e := testutils.CreateTestClient(t, ts.URL)
   385  
   386  		obj := e.POST("/data/"+Type+"/_index").
   387  			WithHeader("Authorization", "Bearer "+token).
   388  			WithHeader("Content-Type", "application/json").
   389  			WithBytes([]byte(`{ "index": { "fields": ["foo"] } }`)).
   390  			Expect().Status(200).
   391  			JSON().Object()
   392  
   393  		obj.NotContainsKey("error")
   394  		obj.NotContainsKey("reason")
   395  		obj.ValueEqual("result", "created")
   396  		obj.Value("name").String().NotEmpty()
   397  		obj.Value("id").String().NotEmpty()
   398  	})
   399  
   400  	t.Run("ReDefineIndex", func(t *testing.T) {
   401  		e := testutils.CreateTestClient(t, ts.URL)
   402  
   403  		obj := e.POST("/data/"+Type+"/_index").
   404  			WithHeader("Authorization", "Bearer "+token).
   405  			WithHeader("Content-Type", "application/json").
   406  			WithBytes([]byte(`{ "index": { "fields": ["foo"] } }`)).
   407  			Expect().Status(200).
   408  			JSON().Object()
   409  
   410  		obj.NotContainsKey("error")
   411  		obj.NotContainsKey("reason")
   412  		obj.ValueEqual("result", "exists")
   413  		obj.Value("name").String().NotEmpty()
   414  		obj.Value("id").String().NotEmpty()
   415  	})
   416  
   417  	t.Run("DefineIndexUnexistingDoctype", func(t *testing.T) {
   418  		e := testutils.CreateTestClient(t, ts.URL)
   419  
   420  		_ = couchdb.DeleteDB(testInstance, "io.cozy.nottype")
   421  
   422  		obj := e.POST("/data/io.cozy.nottype/_index").
   423  			WithHeader("Authorization", "Bearer "+token).
   424  			WithHeader("Content-Type", "application/json").
   425  			WithBytes([]byte(`{ "index": { "fields": ["foo"] } }`)).
   426  			Expect().Status(200).
   427  			JSON().Object()
   428  
   429  		obj.NotContainsKey("error")
   430  		obj.NotContainsKey("reason")
   431  		obj.ValueEqual("result", "created")
   432  		obj.Value("name").String().NotEmpty()
   433  		obj.Value("id").String().NotEmpty()
   434  	})
   435  
   436  	t.Run("FindDocuments", func(t *testing.T) {
   437  		e := testutils.CreateTestClient(t, ts.URL)
   438  
   439  		_ = couchdb.ResetDB(testInstance, Type)
   440  
   441  		// Insert some docs
   442  		_ = getDocForTest(Type, testInstance)
   443  		_ = getDocForTest(Type, testInstance)
   444  		_ = getDocForTest(Type, testInstance)
   445  
   446  		// Create the index
   447  		e.POST("/data/"+Type+"/_index").
   448  			WithHeader("Authorization", "Bearer "+token).
   449  			WithHeader("Content-Type", "application/json").
   450  			WithBytes([]byte(`{ "index": { "fields": ["test"] } }`)).
   451  			Expect().Status(200).
   452  			JSON().Object().
   453  			NotContainsKey("error")
   454  
   455  		// Select with the index
   456  		obj := e.POST("/data/"+Type+"/_find").
   457  			WithHeader("Authorization", "Bearer "+token).
   458  			WithHeader("Content-Type", "application/json").
   459  			WithBytes([]byte(`{ "selector": { "test": "value" } }`)).
   460  			Expect().Status(200).
   461  			JSON().Object()
   462  
   463  		docs := obj.Value("docs").Array()
   464  		docs.Length().Equal(3)
   465  		obj.NotContainsKey("execution_stats")
   466  	})
   467  
   468  	t.Run("FindDocumentsWithStats", func(t *testing.T) {
   469  		e := testutils.CreateTestClient(t, ts.URL)
   470  
   471  		_ = couchdb.ResetDB(testInstance, Type)
   472  		_ = getDocForTest(Type, testInstance)
   473  
   474  		// Create the index
   475  		e.POST("/data/"+Type+"/_index").
   476  			WithHeader("Authorization", "Bearer "+token).
   477  			WithHeader("Content-Type", "application/json").
   478  			WithBytes([]byte(`{ "index": { "fields": ["test"] } }`)).
   479  			Expect().Status(200).
   480  			JSON().Object().
   481  			NotContainsKey("error")
   482  
   483  		// Select with the index
   484  		e.POST("/data/"+Type+"/_find").
   485  			WithHeader("Authorization", "Bearer "+token).
   486  			WithHeader("Content-Type", "application/json").
   487  			WithBytes([]byte(`{ "selector": { "test": "value" }, "execution_stats": true }`)).
   488  			Expect().Status(200).
   489  			JSON().Object().
   490  			Value("execution_stats").Object().NotEmpty()
   491  	})
   492  
   493  	t.Run("FindDocumentsPaginated", func(t *testing.T) {
   494  		e := testutils.CreateTestClient(t, ts.URL)
   495  
   496  		_ = couchdb.ResetDB(testInstance, Type)
   497  
   498  		// Push 150 docs
   499  		for i := 1; i <= 150; i++ {
   500  			_ = getDocForTest(Type, testInstance)
   501  		}
   502  
   503  		// Create an index
   504  		e.POST("/data/"+Type+"/_index").
   505  			WithHeader("Authorization", "Bearer "+token).
   506  			WithHeader("Content-Type", "application/json").
   507  			WithBytes([]byte(`{ "index": { "fields": ["test"] } }`)).
   508  			Expect().Status(200).
   509  			JSON().Object().
   510  			NotContainsKey("error")
   511  
   512  		// Select with the index
   513  		obj := e.POST("/data/"+Type+"/_find").
   514  			WithHeader("Authorization", "Bearer "+token).
   515  			WithHeader("Content-Type", "application/json").
   516  			WithBytes([]byte(`{ "selector": { "test": "value" } }`)).
   517  			Expect().Status(200).
   518  			JSON().Object()
   519  
   520  		docs := obj.Value("docs").Array()
   521  		docs.Length().Equal(100)
   522  		obj.ValueEqual("next", true)
   523  
   524  		// A new select with the index and a limit
   525  		obj = e.POST("/data/"+Type+"/_find").
   526  			WithHeader("Authorization", "Bearer "+token).
   527  			WithHeader("Content-Type", "application/json").
   528  			WithBytes([]byte(`{ "selector": { "test": "value" }, "limit": 10 }`)).
   529  			Expect().Status(200).
   530  			JSON().Object()
   531  
   532  		docs = obj.Value("docs").Array()
   533  		docs.Length().Equal(10)
   534  		obj.ValueEqual("next", true)
   535  	})
   536  
   537  	t.Run("FindDocumentsPaginatedBookmark", func(t *testing.T) {
   538  		e := testutils.CreateTestClient(t, ts.URL)
   539  
   540  		_ = couchdb.ResetDB(testInstance, Type)
   541  
   542  		// Insert 200 docs
   543  		for i := 1; i <= 200; i++ {
   544  			_ = getDocForTest(Type, testInstance)
   545  		}
   546  
   547  		// Create an index
   548  		e.POST("/data/"+Type+"/_index").
   549  			WithHeader("Authorization", "Bearer "+token).
   550  			WithHeader("Content-Type", "application/json").
   551  			WithBytes([]byte(`{ "index": { "fields": ["test"] } }`)).
   552  			Expect().Status(200).
   553  			JSON().Object().
   554  			NotContainsKey("error")
   555  
   556  		// Select with the index
   557  		obj := e.POST("/data/"+Type+"/_find").
   558  			WithHeader("Authorization", "Bearer "+token).
   559  			WithHeader("Content-Type", "application/json").
   560  			WithBytes([]byte(`{ "selector": { "test": "value" } }`)).
   561  			Expect().Status(200).
   562  			JSON().Object()
   563  
   564  		docs := obj.Value("docs").Array()
   565  		docs.Length().Equal(100)
   566  		obj.ValueEqual("limit", 100)
   567  		obj.ValueEqual("next", true)
   568  		bm := obj.Value("bookmark").String().NotEmpty().Raw()
   569  
   570  		// New select with the index and a bookmark
   571  		obj = e.POST("/data/"+Type+"/_find").
   572  			WithHeader("Authorization", "Bearer "+token).
   573  			WithHeader("Content-Type", "application/json").
   574  			WithBytes([]byte(`{ "selector": { "test": "value"}, "bookmark": "` + bm + `" }`)).
   575  			Expect().Status(200).
   576  			JSON().Object()
   577  
   578  		docs = obj.Value("docs").Array()
   579  		docs.Length().Equal(100)
   580  		obj.ValueEqual("limit", 100)
   581  		obj.ValueEqual("next", true)
   582  		bm = obj.Value("bookmark").String().NotEmpty().Raw()
   583  
   584  		// Select 3 with the index and the same bookmark
   585  		obj = e.POST("/data/"+Type+"/_find").
   586  			WithHeader("Authorization", "Bearer "+token).
   587  			WithHeader("Content-Type", "application/json").
   588  			WithBytes([]byte(`{ "selector": { "test": "value"}, "bookmark": "` + bm + `" }`)).
   589  			Expect().Status(200).
   590  			JSON().Object()
   591  
   592  		docs = obj.Value("docs").Array()
   593  		docs.Length().Equal(0)
   594  		obj.ValueEqual("next", false)
   595  
   596  		// Select 4 with the index a value matching nothing
   597  		obj = e.POST("/data/"+Type+"/_find").
   598  			WithHeader("Authorization", "Bearer "+token).
   599  			WithHeader("Content-Type", "application/json").
   600  			WithBytes([]byte(`{ "selector": { "test": "novalue" }}`)).
   601  			Expect().Status(200).
   602  			JSON().Object()
   603  
   604  		docs = obj.Value("docs").Array()
   605  		docs.Length().Equal(0)
   606  		obj.Value("bookmark").String().Empty()
   607  	})
   608  
   609  	t.Run("FindDocumentsWithoutIndex", func(t *testing.T) {
   610  		e := testutils.CreateTestClient(t, ts.URL)
   611  
   612  		obj := e.POST("/data/"+Type+"/_find").
   613  			WithHeader("Authorization", "Bearer "+token).
   614  			WithHeader("Content-Type", "application/json").
   615  			WithBytes([]byte(`{ "selector": { "no-index-for-this-field": "value" } }`)).
   616  			Expect().Status(400).
   617  			JSON().Object()
   618  
   619  		obj.Value("error").String().Contains("no_inde")
   620  		obj.Value("reason").String().Contains("no matching index")
   621  	})
   622  
   623  	t.Run("GetChanges", func(t *testing.T) {
   624  		e := testutils.CreateTestClient(t, ts.URL)
   625  
   626  		assert.NoError(t, couchdb.ResetDB(testInstance, Type))
   627  
   628  		seqno := e.GET("/data/"+Type+"/_changes").
   629  			WithQuery("style", "all_docs").
   630  			WithHeader("Authorization", "Bearer "+token).
   631  			Expect().Status(200).
   632  			JSON().Object().
   633  			Value("last_seq").String().NotEmpty().Raw()
   634  
   635  		// creates 3 docs
   636  		_ = getDocForTest(Type, testInstance)
   637  		_ = getDocForTest(Type, testInstance)
   638  		_ = getDocForTest(Type, testInstance)
   639  
   640  		e.GET("/data/"+Type+"/_changes").
   641  			WithQuery("limit", 2).
   642  			WithQuery("since", seqno).
   643  			WithHeader("Authorization", "Bearer "+token).
   644  			Expect().Status(200).
   645  			JSON().Object().
   646  			Value("results").Array().Length().Equal(2)
   647  
   648  		e.GET("/data/"+Type+"/_changes").
   649  			WithQuery("since", seqno).
   650  			WithHeader("Authorization", "Bearer "+token).
   651  			Expect().Status(200).
   652  			JSON().Object().
   653  			Value("results").Array().Length().Equal(3)
   654  	})
   655  
   656  	t.Run("PostChanges", func(t *testing.T) {
   657  		e := testutils.CreateTestClient(t, ts.URL)
   658  
   659  		assert.NoError(t, couchdb.ResetDB(testInstance, Type))
   660  
   661  		// creates 3 docs
   662  		doc1 := getDocForTest(Type, testInstance)
   663  		_ = getDocForTest(Type, testInstance)
   664  		doc2 := getDocForTest(Type, testInstance)
   665  
   666  		e.POST("/data/"+Type+"/_changes").
   667  			WithQuery("include_docs", true).
   668  			WithQuery("filter", "_doc_ids").
   669  			WithHeader("Authorization", "Bearer "+token).
   670  			WithBytes([]byte(fmt.Sprintf(`{"doc_ids": ["%s", "%s"]}`, doc1.ID(), doc2.ID()))).
   671  			Expect().Status(200).
   672  			JSON().Object().
   673  			Value("results").Array().Length().Equal(2)
   674  	})
   675  
   676  	t.Run("WrongFeedChanges", func(t *testing.T) {
   677  		e := testutils.CreateTestClient(t, ts.URL)
   678  
   679  		e.POST("/data/"+Type+"/_changes").
   680  			WithQuery("feed", "continuous").
   681  			WithHeader("Authorization", "Bearer "+token).
   682  			Expect().Status(400)
   683  	})
   684  
   685  	t.Run("WrongStyleChanges", func(t *testing.T) {
   686  		e := testutils.CreateTestClient(t, ts.URL)
   687  
   688  		e.POST("/data/"+Type+"/_changes").
   689  			WithQuery("style", "not_a_valid_style").
   690  			WithHeader("Authorization", "Bearer "+token).
   691  			Expect().Status(400)
   692  	})
   693  
   694  	t.Run("LimitIsNoNumber", func(t *testing.T) {
   695  		e := testutils.CreateTestClient(t, ts.URL)
   696  
   697  		e.POST("/data/"+Type+"/_changes").
   698  			WithQuery("limit", "not_a_number").
   699  			WithHeader("Authorization", "Bearer "+token).
   700  			Expect().Status(400)
   701  	})
   702  
   703  	t.Run("UnsupportedOption", func(t *testing.T) {
   704  		e := testutils.CreateTestClient(t, ts.URL)
   705  
   706  		e.POST("/data/"+Type+"/_changes").
   707  			WithQuery("inlude_docs", true). // typo (inlude instead of include)
   708  			WithHeader("Authorization", "Bearer "+token).
   709  			Expect().Status(400)
   710  	})
   711  
   712  	t.Run("GetAllDocs", func(t *testing.T) {
   713  		e := testutils.CreateTestClient(t, ts.URL)
   714  
   715  		obj := e.GET("/data/"+Type+"/_all_docs").
   716  			WithQuery("include_docs", true).
   717  			WithHeader("Authorization", "Bearer "+token).
   718  			Expect().Status(200).
   719  			JSON().Object()
   720  
   721  		obj.ValueEqual("total_rows", 3)
   722  		obj.ValueEqual("offset", 0)
   723  		rows := obj.Value("rows").Array()
   724  		rows.Length().Equal(3)
   725  
   726  		first := rows.First().Object()
   727  		first.Value("id").String().NotEmpty()
   728  		doc := first.Value("doc").Object()
   729  		doc.ValueEqual("test", "value")
   730  		doc.Path("$.foo.bar").Equal("one")
   731  		doc.Path("$.foo.baz").Equal("two")
   732  		doc.Path("$.foo.qux").Equal("quux")
   733  	})
   734  
   735  	t.Run("GetAllDocsWithFields", func(t *testing.T) {
   736  		e := testutils.CreateTestClient(t, ts.URL)
   737  
   738  		obj := e.GET("/data/"+Type+"/_all_docs").
   739  			WithQuery("include_docs", true).
   740  			WithQuery("Fields", "test,nosuchfield,foo.qux").
   741  			WithHeader("Authorization", "Bearer "+token).
   742  			Expect().Status(200).
   743  			JSON().Object()
   744  
   745  		obj.ValueEqual("total_rows", 3)
   746  		obj.ValueEqual("offset", 0)
   747  		rows := obj.Value("rows").Array()
   748  		rows.Length().Equal(3)
   749  
   750  		first := rows.First().Object()
   751  		first.Value("id").String().NotEmpty()
   752  		doc := first.Value("doc").Object()
   753  		doc.ValueEqual("test", "value")
   754  		foo := doc.Value("foo").Object()
   755  		foo.NotContainsKey("bar")
   756  		foo.NotContainsKey("baz")
   757  		foo.ValueEqual("qux", "quux")
   758  	})
   759  
   760  	t.Run("NormalDocs", func(t *testing.T) {
   761  		e := testutils.CreateTestClient(t, ts.URL)
   762  
   763  		view := &couchdb.View{
   764  			Name:    "foobar",
   765  			Doctype: Type,
   766  			Map: `
   767  	 function(doc) {
   768  	   emit(doc.foobar, doc);
   769  	 }`,
   770  		}
   771  		g, _ := errgroup.WithContext(context.Background())
   772  		couchdb.DefineViews(g, testInstance, []*couchdb.View{view})
   773  		require.NoError(t, g.Wait())
   774  
   775  		err := couchdb.CreateNamedDoc(testInstance, &couchdb.JSONDoc{
   776  			Type: Type,
   777  			M: map[string]interface{}{
   778  				"_id":  "four",
   779  				"test": "fourthvalue",
   780  			},
   781  		})
   782  		require.NoError(t, err)
   783  
   784  		obj := e.GET("/data/"+Type+"/_normal_docs").
   785  			WithQuery("limit", 2).
   786  			WithHeader("Authorization", "Bearer "+token).
   787  			Expect().Status(200).
   788  			JSON().Object()
   789  
   790  		obj.ValueEqual("total_rows", 4)
   791  		obj.Value("bookmark").String().NotEmpty()
   792  		bookmark := obj.Value("bookmark").String().NotEmpty().Raw()
   793  		obj.NotContainsKey("execution_stats")
   794  
   795  		rows := obj.Value("rows").Array()
   796  		rows.Length().Equal(2)
   797  
   798  		elem := rows.Element(1).Object()
   799  		elem.Value("_id").String().NotEmpty()
   800  		elem.ValueEqual("test", "value")
   801  
   802  		// skip pagination
   803  		obj = e.GET("/data/"+Type+"/_normal_docs").
   804  			WithQuery("limit", 2).
   805  			WithQuery("skip", 2).
   806  			WithHeader("Authorization", "Bearer "+token).
   807  			Expect().Status(200).
   808  			JSON().Object()
   809  
   810  		obj.ValueEqual("total_rows", 4)
   811  		obj.Value("bookmark").String().NotEmpty()
   812  		rows = obj.Value("rows").Array()
   813  		rows.Length().Equal(2)
   814  
   815  		elem = rows.Element(1).Object()
   816  		elem.Value("_id").String().NotEmpty()
   817  		elem.ValueEqual("test", "fourthvalue")
   818  
   819  		// bookmark pagination
   820  		obj = e.GET("/data/"+Type+"/_normal_docs").
   821  			WithQuery("bookmark", bookmark).
   822  			WithHeader("Authorization", "Bearer "+token).
   823  			Expect().Status(200).
   824  			JSON().Object()
   825  
   826  		obj.ValueEqual("total_rows", 4)
   827  		rows = obj.Value("rows").Array()
   828  		rows.Length().Equal(2)
   829  
   830  		elem = rows.Element(1).Object()
   831  		elem.Value("_id").String().NotEmpty()
   832  		elem.ValueEqual("test", "fourthvalue")
   833  
   834  		// _normal_docs with no results
   835  		emptyType := "io.cozy.anothertype"
   836  		_ = couchdb.ResetDB(testInstance, emptyType)
   837  
   838  		obj = e.GET("/data/"+emptyType+"/_normal_docs").
   839  			WithHeader("Authorization", "Bearer "+token).
   840  			Expect().Status(200).
   841  			JSON().Object()
   842  
   843  		obj.ValueEqual("total_rows", 0)
   844  		obj.Value("bookmark").String().Empty()
   845  
   846  		// execution stats
   847  		obj = e.GET("/data/"+emptyType+"/_normal_docs").
   848  			WithQuery("execution_stats", true).
   849  			WithHeader("Authorization", "Bearer "+token).
   850  			Expect().Status(200).
   851  			JSON().Object()
   852  
   853  		obj.Value("execution_stats").Object().NotEmpty()
   854  	})
   855  
   856  	t.Run("GetDesignDocs", func(t *testing.T) {
   857  		e := testutils.CreateTestClient(t, ts.URL)
   858  
   859  		def := M{"index": M{"fields": S{"foo"}}}
   860  		_, err := couchdb.DefineIndexRaw(testInstance, Type, &def)
   861  		require.NoError(t, err)
   862  
   863  		obj := e.GET("/data/"+Type+"/_design_docs").
   864  			WithHeader("Authorization", "Bearer "+token).
   865  			Expect().Status(200).
   866  			JSON().Object()
   867  
   868  		rows := obj.Value("rows").Array()
   869  		rows.Length().Gt(0)
   870  
   871  		elem := rows.First().Object()
   872  		elem.Value("id").String().NotEmpty()
   873  		elem.Path("$.value.rev").String().NotEmpty()
   874  	})
   875  
   876  	t.Run("GetDesignDoc", func(t *testing.T) {
   877  		e := testutils.CreateTestClient(t, ts.URL)
   878  
   879  		ddoc := "myindex"
   880  		def := M{"index": M{"fields": S{"foo"}}, "ddoc": ddoc}
   881  		_, err := couchdb.DefineIndexRaw(testInstance, Type, &def)
   882  		assert.NoError(t, err)
   883  
   884  		obj := e.GET("/data/"+Type+"/_design/"+ddoc).
   885  			WithHeader("Authorization", "Bearer "+token).
   886  			Expect().Status(200).
   887  			JSON().Object()
   888  
   889  		obj.ValueEqual("_id", "_design/"+ddoc)
   890  		obj.Value("_rev").String().NotEmpty()
   891  	})
   892  
   893  	t.Run("DeleteDesignDoc", func(t *testing.T) {
   894  		e := testutils.CreateTestClient(t, ts.URL)
   895  
   896  		def := M{"index": M{"fields": S{"foo"}}}
   897  		_, err := couchdb.DefineIndexRaw(testInstance, Type, &def)
   898  		require.NoError(t, err)
   899  
   900  		// Get the number of design document
   901  		obj := e.GET("/data/"+Type+"/_design_docs").
   902  			WithHeader("Authorization", "Bearer "+token).
   903  			Expect().Status(200).
   904  			JSON().Object()
   905  
   906  		rows := obj.Value("rows").Array()
   907  		nbDD := rows.Length().Gt(0).Raw()
   908  
   909  		elem := rows.Element(0).Object()
   910  		id := elem.Value("id").String().NotEmpty().Raw()
   911  
   912  		ddoc := strings.Split(id, "/")[1]
   913  		rev := elem.Path("$.value.rev").String().NotEmpty().Raw()
   914  
   915  		// Delete the first design document
   916  		e.DELETE("/data/"+Type+"/_design/"+ddoc).
   917  			WithQuery("rev", rev).
   918  			WithHeader("Authorization", "Bearer "+token).
   919  			Expect().Status(200)
   920  
   921  		// Check that the number of design document have decreased
   922  		obj = e.GET("/data/"+Type+"/_design_docs").
   923  			WithHeader("Authorization", "Bearer "+token).
   924  			Expect().Status(200).
   925  			JSON().Object()
   926  
   927  		rows = obj.Value("rows").Array()
   928  		rows.Length().Lt(nbDD)
   929  	})
   930  
   931  	t.Run("CannotDeleteStackDesignDoc", func(t *testing.T) {
   932  		e := testutils.CreateTestClient(t, ts.URL)
   933  
   934  		// Fetch the dir-by-path and by-parent-type-name indexes rev
   935  		obj := e.GET("/data/io.cozy.files/_design_docs").
   936  			WithHeader("Authorization", "Bearer "+token).
   937  			Expect().Status(200).
   938  			JSON().Object()
   939  
   940  		var indexRev, viewRev string
   941  
   942  		for _, row := range obj.Value("rows").Array().Iter() {
   943  			info := row.Object()
   944  			if info.Value("id").String().Raw() == "_design/dir-by-path" {
   945  				indexRev = info.Path("$.value.rev").String().NotEmpty().Raw()
   946  			}
   947  			if info.Value("id").String().Raw() == "_design/by-parent-type-name" {
   948  				viewRev = info.Path("$.value.rev").String().NotEmpty().Raw()
   949  			}
   950  		}
   951  
   952  		// Try to delete the dir-by-path index
   953  		e.DELETE("/data/io.cozy.files/_design/dir-by-path").
   954  			WithQuery("rev", indexRev).
   955  			WithHeader("Authorization", "Bearer "+token).
   956  			Expect().Status(403)
   957  
   958  		// Try to delete the dir-by-path index
   959  		e.DELETE("/data/io.cozy.files/_design/by-parent-type-name").
   960  			WithQuery("rev", viewRev).
   961  			WithHeader("Authorization", "Bearer "+token).
   962  			Expect().Status(403)
   963  	})
   964  
   965  	t.Run("CopyDesignDoc", func(t *testing.T) {
   966  		e := testutils.CreateTestClient(t, ts.URL)
   967  
   968  		srcDdoc := "indextocopy"
   969  		targetID := "_design/indexcopied"
   970  		def := M{"index": M{"fields": S{"foo"}}, "ddoc": srcDdoc}
   971  		_, err := couchdb.DefineIndexRaw(testInstance, Type, &def)
   972  		assert.NoError(t, err)
   973  
   974  		rev := e.GET("/data/"+Type+"/_design/"+srcDdoc).
   975  			WithHeader("Authorization", "Bearer "+token).
   976  			Expect().Status(200).
   977  			JSON().Object().
   978  			Value("_rev").String().NotEmpty().Raw()
   979  
   980  		e.POST("/data/"+Type+"/_design/"+srcDdoc+"/copy").
   981  			WithQuery("rev", rev).
   982  			WithHeader("Authorization", "Bearer "+token).
   983  			WithHeader("Destination", targetID).
   984  			Expect().Status(201).
   985  			JSON().Object().
   986  			ValueEqual("id", targetID).
   987  			ValueEqual("rev", rev)
   988  	})
   989  
   990  	t.Run("DeleteDatabase", func(t *testing.T) {
   991  		e := testutils.CreateTestClient(t, ts.URL)
   992  
   993  		e.DELETE("/data/"+Type+"/").
   994  			WithHeader("Authorization", "Bearer "+token).
   995  			Expect().Status(200).
   996  			JSON().Object().
   997  			ValueEqual("deleted", true)
   998  	})
   999  
  1000  	t.Run("DeleteDatabaseNoPermission", func(t *testing.T) {
  1001  		e := testutils.CreateTestClient(t, ts.URL)
  1002  
  1003  		doctype := "io.cozy.forbidden"
  1004  		e.DELETE("/data/"+doctype+"/").
  1005  			WithHeader("Authorization", "Bearer "+token).
  1006  			Expect().Status(403)
  1007  	})
  1008  }
  1009  
  1010  func getDocForTest(t string, instance *instance.Instance) *couchdb.JSONDoc {
  1011  	doc := couchdb.JSONDoc{
  1012  		Type: t,
  1013  		M: map[string]interface{}{
  1014  			"test":   "value",
  1015  			"foo":    map[string]interface{}{"bar": "one", "baz": "two", "qux": "quux"},
  1016  			"courge": 1,
  1017  		},
  1018  	}
  1019  	_ = couchdb.CreateDoc(instance, &doc)
  1020  	return &doc
  1021  }