github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/notes/notes_test.go (about) 1 package notes 2 3 import ( 4 "fmt" 5 "io" 6 "net/http" 7 "os" 8 "strconv" 9 "sync" 10 "testing" 11 "time" 12 13 "github.com/cozy/cozy-stack/model/note" 14 "github.com/cozy/cozy-stack/pkg/config/config" 15 "github.com/cozy/cozy-stack/pkg/consts" 16 "github.com/cozy/cozy-stack/pkg/realtime" 17 "github.com/cozy/cozy-stack/tests/testutils" 18 "github.com/cozy/cozy-stack/web/errors" 19 "github.com/cozy/cozy-stack/web/files" 20 webRealtime "github.com/cozy/cozy-stack/web/realtime" 21 "github.com/gavv/httpexpect/v2" 22 "github.com/labstack/echo/v4" 23 "github.com/stretchr/testify/assert" 24 "github.com/stretchr/testify/require" 25 ) 26 27 func TestNotes(t *testing.T) { 28 if testing.Short() { 29 t.Skip("an instance is required for this test: test skipped due to the use of --short flag") 30 } 31 32 var noteID, otherNoteID string 33 var version int64 34 35 config.UseTestFile(t) 36 testutils.NeedCouchdb(t) 37 setup := testutils.NewSetup(t, t.Name()) 38 inst := setup.GetTestInstance() 39 _, token := setup.GetTestClient(consts.Files) 40 41 ts := setup.GetTestServerMultipleRoutes(map[string]func(*echo.Group){ 42 "/files": files.Routes, 43 "/notes": Routes, 44 "/realtime": webRealtime.Routes, 45 }) 46 ts.Config.Handler.(*echo.Echo).HTTPErrorHandler = errors.ErrorHandler 47 t.Cleanup(ts.Close) 48 49 t.Run("CreateNote", func(t *testing.T) { 50 e := testutils.CreateTestClient(t, ts.URL) 51 52 obj := e.POST("/notes"). 53 WithHeader("Authorization", "Bearer "+token). 54 WithHeader("Content-Type", "application/json"). 55 WithBytes([]byte(`{ 56 "data": { 57 "type": "io.cozy.notes.documents", 58 "attributes": { 59 "title": "A super note", 60 "schema": { 61 "nodes": [ 62 ["doc", { "content": "block+" }], 63 ["paragraph", { "content": "inline*", "group": "block" }], 64 ["blockquote", { "content": "block+", "group": "block" }], 65 ["horizontal_rule", { "group": "block" }], 66 [ 67 "heading", 68 { 69 "content": "inline*", 70 "group": "block", 71 "attrs": { "level": { "default": 1 } } 72 } 73 ], 74 ["code_block", { "content": "text*", "marks": "", "group": "block" }], 75 ["text", { "group": "inline" }], 76 [ 77 "image", 78 { 79 "group": "inline", 80 "inline": true, 81 "attrs": { "alt": {}, "src": {}, "title": {} } 82 } 83 ], 84 ["hard_break", { "group": "inline", "inline": true }], 85 [ 86 "ordered_list", 87 { 88 "content": "list_item+", 89 "group": "block", 90 "attrs": { "order": { "default": 1 } } 91 } 92 ], 93 ["bullet_list", { "content": "list_item+", "group": "block" }], 94 ["list_item", { "content": "paragraph block*" }] 95 ], 96 "marks": [ 97 ["link", { "attrs": { "href": {}, "title": {} }, "inclusive": false }], 98 ["em", {}], 99 ["strong", {}], 100 ["code", {}] 101 ], 102 "topNode": "doc" 103 } 104 } 105 } 106 }`)). 107 Expect().Status(201). 108 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 109 Object() 110 111 assertInitialNote(t, obj) 112 113 noteID = obj.Path("$.data.id").String().NotEmpty().Raw() 114 }) 115 116 t.Run("GetNote", func(t *testing.T) { 117 e := testutils.CreateTestClient(t, ts.URL) 118 119 obj := e.GET("/notes/"+noteID). 120 WithHeader("Authorization", "Bearer "+token). 121 Expect().Status(200). 122 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 123 Object() 124 125 assertInitialNote(t, obj) 126 }) 127 128 t.Run("OpenNote", func(t *testing.T) { 129 e := testutils.CreateTestClient(t, ts.URL) 130 131 obj := e.GET("/notes/"+noteID+"/open"). 132 WithHeader("Authorization", "Bearer "+token). 133 Expect().Status(200). 134 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 135 Object() 136 137 data := obj.Value("data").Object() 138 data.HasValue("type", consts.NotesURL) 139 data.HasValue("id", noteID) 140 141 attrs := data.Value("attributes").Object() 142 attrs.HasValue("note_id", noteID) 143 attrs.HasValue("subdomain", "nested") 144 attrs.HasValue("protocol", "https") 145 attrs.HasValue("instance", inst.Domain) 146 attrs.Value("public_name").String().NotEmpty() 147 }) 148 149 t.Run("ChangeTitleAndSync", func(t *testing.T) { 150 e := testutils.CreateTestClient(t, ts.URL) 151 152 obj := e.PUT("/notes/"+noteID+"/title"). 153 WithHeader("Authorization", "Bearer "+token). 154 WithHeader("Content-Type", "application/vnd.api+json"). 155 WithBytes([]byte(`{ 156 "data": { 157 "type": "io.cozy.notes.documents", 158 "attributes": { 159 "sessionID": "543781490137", 160 "title": "A new title" 161 } 162 } 163 }`)). 164 Expect().Status(200). 165 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 166 Object() 167 168 data := obj.Value("data").Object() 169 data.HasValue("type", "io.cozy.files") 170 data.HasValue("id", noteID) 171 172 attrs := data.Value("attributes").Object() 173 meta := attrs.Value("metadata").Object() 174 175 meta.HasValue("title", "A new title") 176 meta.HasValue("version", 0) 177 meta.Value("schema").Object().NotEmpty() 178 meta.Value("content").Object().NotEmpty() 179 180 // The change was only made in cache, but we have to force persisting the 181 // change to the VFS to check that renaming the file works. 182 e.POST("/notes/"+noteID+"/sync"). 183 WithHeader("Authorization", "Bearer "+token). 184 Expect().Status(204) 185 186 obj = e.GET("/notes/"+noteID). 187 WithHeader("Authorization", "Bearer "+token). 188 Expect().Status(200). 189 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 190 Object() 191 192 data = obj.Value("data").Object() 193 data.HasValue("type", "io.cozy.files") 194 data.HasValue("id", noteID) 195 196 attrs = data.Value("attributes").Object() 197 attrs.HasValue("name", "A new title.cozy-note") 198 199 meta = attrs.Value("metadata").Object() 200 meta.HasValue("title", "A new title") 201 meta.HasValue("version", 0) 202 meta.Value("schema").Object().NotEmpty() 203 meta.Value("content").Object().NotEmpty() 204 }) 205 206 t.Run("ListNotes", func(t *testing.T) { 207 e := testutils.CreateTestClient(t, ts.URL) 208 209 // Change the title 210 e.PUT("/notes/"+noteID+"/title"). 211 WithHeader("Authorization", "Bearer "+token). 212 WithHeader("Content-Type", "application/vnd.api+json"). 213 WithBytes([]byte(`{ 214 "data": { 215 "type": "io.cozy.notes.documents", 216 "attributes": { 217 "sessionID": "543781490137", 218 "title": "A title in cache" 219 } 220 } 221 }`)). 222 Expect().Status(200) 223 224 // The title has been changed in cache, but we don't wait that the file has been renamed 225 obj := e.GET("/notes"). 226 WithHeader("Authorization", "Bearer "+token). 227 Expect().Status(200). 228 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 229 Object() 230 231 data := obj.Value("data").Array() 232 data.Length().IsEqual(1) 233 234 doc := data.Value(0).Object() 235 doc.HasValue("type", "io.cozy.files") 236 doc.HasValue("id", noteID) 237 238 attrs := doc.Value("attributes").Object() 239 attrs.HasValue("name", "A new title.cozy-note") 240 attrs.Value("path").String().HasSuffix("/A new title.cozy-note") 241 attrs.HasValue("mime", "text/vnd.cozy.note+markdown") 242 243 meta := attrs.Value("metadata").Object() 244 meta.HasValue("title", "A title in cache") 245 meta.HasValue("version", 0) 246 meta.Value("schema").Object().NotEmpty() 247 meta.Value("content").Object().NotEmpty() 248 }) 249 250 t.Run("PatchNote", func(t *testing.T) { 251 body := []byte(`{ 252 "data": [{ 253 "type": "io.cozy.notes.steps", 254 "attributes": { 255 "sessionID": "543781490137", 256 "stepType": "replace", 257 "from": 1, 258 "to": 1, 259 "slice": { 260 "content": [{ "type": "text", "text": "H" }] 261 } 262 } 263 }, { 264 "type": "io.cozy.notes.steps", 265 "attributes": { 266 "sessionID": "543781490137", 267 "stepType": "replace", 268 "from": 2, 269 "to": 2, 270 "slice": { 271 "content": [{ "type": "text", "text": "ello" }] 272 } 273 } 274 }] 275 }`) 276 277 t.Run("Success", func(t *testing.T) { 278 e := testutils.CreateTestClient(t, ts.URL) 279 280 obj := e.PATCH("/notes/"+noteID). 281 WithHeader("Authorization", "Bearer "+token). 282 WithHeader("Content-Type", "application/vnd.api+json"). 283 WithHeader("If-Match", "0"). 284 WithBytes(body). 285 Expect().Status(200). 286 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 287 Object() 288 289 data := obj.Value("data").Object() 290 data.HasValue("type", "io.cozy.files") 291 data.HasValue("id", noteID) 292 293 attrs := data.Value("attributes").Object() 294 meta := attrs.Value("metadata").Object() 295 296 version = int64(meta.Value("version").Number().Gt(0).Raw()) 297 meta.Value("schema").Object().NotEmpty() 298 meta.Value("content").Object().NotEmpty() 299 }) 300 301 t.Run("WithInvalidIfMatchHeader", func(t *testing.T) { 302 e := testutils.CreateTestClient(t, ts.URL) 303 304 e.PATCH("/notes/"+noteID). 305 WithHeader("Authorization", "Bearer "+token). 306 WithHeader("Content-Type", "application/vnd.api+json"). 307 WithHeader("If-Match", "0"). 308 WithBytes(body). 309 Expect().Status(409) 310 }) 311 }) 312 313 t.Run("GetSteps", func(t *testing.T) { 314 var lastVersion int 315 316 body := []byte(`{ 317 "data": [{ 318 "type": "io.cozy.notes.steps", 319 "attributes": { 320 "sessionID": "543781490137", 321 "stepType": "replace", 322 "from": 6, 323 "to": 6, 324 "slice": { 325 "content": [{ "type": "text", "text": " " }] 326 } 327 } 328 }, { 329 "type": "io.cozy.notes.steps", 330 "attributes": { 331 "sessionID": "543781490137", 332 "stepType": "replace", 333 "from": 7, 334 "to": 7, 335 "slice": { 336 "content": [{ "type": "text", "text": "world" }] 337 } 338 } 339 }] 340 }`) 341 342 t.Run("Success", func(t *testing.T) { 343 e := testutils.CreateTestClient(t, ts.URL) 344 345 obj := e.PATCH("/notes/"+noteID). 346 WithHeader("Authorization", "Bearer "+token). 347 WithHeader("Content-Type", "application/vnd.api+json"). 348 WithHeader("If-Match", strconv.Itoa(int(version))). 349 WithBytes(body). 350 Expect().Status(200). 351 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 352 Object() 353 354 lastVersion = int(obj.Path("$.data.attributes.metadata.version").Number().Gt(0).Raw()) 355 }) 356 357 t.Run("GetStepsFromCurrentVersion", func(t *testing.T) { 358 e := testutils.CreateTestClient(t, ts.URL) 359 360 obj := e.GET("/notes/"+noteID+"/steps"). 361 WithQuery("Version", int(version)). 362 WithHeader("Authorization", "Bearer "+token). 363 WithHeader("Content-Type", "application/vnd.api+json"). 364 WithHeader("If-Match", strconv.Itoa(int(version))). 365 WithBytes(body). 366 Expect().Status(200). 367 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 368 Object() 369 370 obj.Path("$.meta.count").Number().IsEqual(2) 371 obj.Path("$.data").Array().Length().IsEqual(2) 372 373 first := obj.Path("$.data[0]").Object() 374 first.Value("id").String().NotEmpty() 375 376 attrs := first.Value("attributes").Object() 377 attrs.HasValue("sessionID", "543781490137") 378 attrs.HasValue("stepType", "replace") 379 attrs.HasValue("from", 6) 380 attrs.HasValue("to", 6) 381 attrs.Value("version").Number() 382 383 second := obj.Path("$.data[1]").Object() 384 second.Value("id").String().NotEmpty() 385 386 attrs = second.Value("attributes").Object() 387 attrs.HasValue("sessionID", "543781490137") 388 attrs.HasValue("stepType", "replace") 389 attrs.HasValue("from", 7) 390 attrs.HasValue("to", 7) 391 attrs.HasValue("version", lastVersion) 392 }) 393 394 t.Run("GetStepsFromLastVersion", func(t *testing.T) { 395 e := testutils.CreateTestClient(t, ts.URL) 396 397 obj := e.GET("/notes/"+noteID+"/steps"). 398 WithQuery("Version", lastVersion). 399 WithHeader("Authorization", "Bearer "+token). 400 WithHeader("Content-Type", "application/vnd.api+json"). 401 WithHeader("If-Match", strconv.Itoa(int(version))). 402 WithBytes(body). 403 Expect().Status(200). 404 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 405 Object() 406 407 obj.Path("$.meta.count").Number().IsEqual(0) 408 obj.Path("$.data").Array().IsEmpty() 409 410 version = int64(lastVersion) 411 }) 412 }) 413 414 t.Run("PutSchema", func(t *testing.T) { 415 e := testutils.CreateTestClient(t, ts.URL) 416 417 obj := e.PUT("/notes/"+noteID+"/schema"). 418 WithHeader("Authorization", "Bearer "+token). 419 WithHeader("Content-Type", "application/json"). 420 WithBytes([]byte(`{ 421 "data": { 422 "type": "io.cozy.notes.documents", 423 "attributes": { 424 "schema": { 425 "nodes": [ 426 ["doc", { "content": "block+" }], 427 [ 428 "panel", 429 { 430 "content": "(paragraph | heading | bullet_list | ordered_list)+", 431 "group": "block", 432 "attrs": { "panelType": { "default": "info" } } 433 } 434 ], 435 ["paragraph", { "content": "inline*", "group": "block" }], 436 ["blockquote", { "content": "block+", "group": "block" }], 437 ["horizontal_rule", { "group": "block" }], 438 [ 439 "heading", 440 { 441 "content": "inline*", 442 "group": "block", 443 "attrs": { "level": { "default": 1 } } 444 } 445 ], 446 ["code_block", { "content": "text*", "marks": "", "group": "block" }], 447 ["text", { "group": "inline" }], 448 [ 449 "image", 450 { 451 "group": "inline", 452 "inline": true, 453 "attrs": { "alt": {}, "src": {}, "title": {} } 454 } 455 ], 456 ["hard_break", { "group": "inline", "inline": true }], 457 [ 458 "ordered_list", 459 { 460 "content": "list_item+", 461 "group": "block", 462 "attrs": { "order": { "default": 1 } } 463 } 464 ], 465 ["bullet_list", { "content": "list_item+", "group": "block" }], 466 ["list_item", { "content": "paragraph block*" }] 467 ], 468 "marks": [ 469 ["link", { "attrs": { "href": {}, "title": {} }, "inclusive": false }], 470 ["em", {}], 471 ["strong", {}], 472 ["code", {}] 473 ], 474 "version": 2, 475 "topNode": "doc" 476 } 477 } 478 } 479 }`)). 480 Expect().Status(200). 481 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 482 Object() 483 484 data := obj.Value("data").Object() 485 data.HasValue("type", "io.cozy.files") 486 data.HasValue("id", noteID) 487 488 schema := obj.Path("$.data.attributes.metadata.schema").Object() 489 schema.HasValue("version", 2) 490 schema.Path("$.nodes[1][0]").IsEqual("panel") 491 492 // TODO: add an explanation why we need this sleep period 493 time.Sleep(1 * time.Second) 494 495 e.GET("/notes/"+noteID+"/steps"). 496 WithQuery("Version", version). 497 WithHeader("Authorization", "Bearer "+token). 498 Expect().Status(412) 499 500 version = int64(obj.Path("$.data.attributes.metadata.version").Number().Raw()) 501 }) 502 503 t.Run("PutTelepointer", func(t *testing.T) { 504 e := testutils.CreateTestClient(t, ts.URL) 505 506 wg := sync.WaitGroup{} 507 wg.Add(1) 508 go func() { 509 sub := realtime.GetHub().Subscriber(inst) 510 sub.Subscribe(consts.NotesEvents) 511 512 // Suscribtion ok, unlock the first wait 513 wg.Done() 514 515 e := <-sub.Channel 516 assert.Equal(t, "UPDATED", e.Verb) 517 assert.Equal(t, noteID, e.Doc.ID()) 518 doc, ok := e.Doc.(note.Event) 519 assert.True(t, ok) 520 assert.Equal(t, consts.NotesTelepointers, doc["doctype"]) 521 assert.Equal(t, "543781490137", doc["sessionID"]) 522 assert.Equal(t, "textSelection", doc["type"]) 523 assert.EqualValues(t, 7, doc["anchor"]) 524 assert.EqualValues(t, 12, doc["head"]) 525 526 // Event received and validated, unlock the second wait. 527 wg.Done() 528 }() 529 530 // Wait that the goroutine has subscribed to the realtime 531 wg.Wait() 532 533 wg.Add(1) 534 e.PUT("/notes/"+noteID+"/telepointer"). 535 WithHeader("Authorization", "Bearer "+token). 536 WithHeader("Content-Type", "application/json"). 537 WithBytes([]byte(`{ 538 "data": { 539 "type": "io.cozy.notes.telepointers", 540 "attributes": { 541 "sessionID": "543781490137", 542 "anchor": 7, 543 "head": 12, 544 "type": "textSelection" 545 } 546 } 547 }`)). 548 Expect().Status(204) 549 550 // Wait that the goroutine has received the telepointer update 551 wg.Wait() 552 }) 553 554 t.Run("NoteMarkdown", func(t *testing.T) { 555 // Force the changes to the VFS 556 err := note.Update(inst, noteID) 557 assert.NoError(t, err) 558 doc, err := inst.VFS().FileByID(noteID) 559 assert.NoError(t, err) 560 file, err := inst.VFS().OpenFile(doc) 561 assert.NoError(t, err) 562 defer file.Close() 563 buf, err := io.ReadAll(file) 564 assert.NoError(t, err) 565 assert.Equal(t, "Hello world", string(buf)) 566 }) 567 568 t.Run("NoteRealtime", func(t *testing.T) { 569 e := testutils.CreateTestClient(t, ts.URL) 570 571 ws := e.GET("/realtime/"). 572 WithWebsocketUpgrade(). 573 Expect().Status(http.StatusSwitchingProtocols). 574 Websocket() 575 defer ws.Disconnect() 576 577 ws.WriteText(fmt.Sprintf(`{"method": "AUTH", "payload": "%s"}`, token)) 578 579 ws.WriteText(`{"method": "SUBSCRIBE", "payload": { "type": "io.cozy.notes.events", "id": "` + noteID + `" }}`) 580 581 // To check that the realtime has made the subscription, we send a fake 582 // message and wait for its response. 583 ws.WriteText(`{"method": "PING"}`). 584 Expect().TextMessage(). 585 JSON() 586 587 pointer := note.Event{ 588 "sessionID": "543781490137", 589 "anchor": 7, 590 "head": 12, 591 "type": "textSelection", 592 } 593 pointer.SetID(noteID) 594 err := note.PutTelepointer(inst, pointer) 595 assert.NoError(t, err) 596 597 obj := ws.Expect().TextMessage(). 598 JSON().Object() 599 600 obj.HasValue("event", "UPDATED") 601 payload := obj.Value("payload").Object() 602 payload.HasValue("id", noteID) 603 payload.HasValue("type", "io.cozy.notes.events") 604 605 doc := payload.Value("doc").Object() 606 doc.HasValue("doctype", "io.cozy.notes.telepointers") 607 doc.HasValue("sessionID", "543781490137") 608 doc.HasValue("anchor", 7) 609 doc.HasValue("head", 12) 610 doc.HasValue("type", "textSelection") 611 612 file, err := inst.VFS().FileByID(noteID) 613 require.NoError(t, err) 614 file, err = note.UpdateTitle(inst, file, "A very new title", "543781490137") 615 require.NoError(t, err) 616 617 obj = ws.Expect().TextMessage(). 618 JSON().Object() 619 620 obj.HasValue("event", "UPDATED") 621 payload = obj.Value("payload").Object() 622 payload.HasValue("id", noteID) 623 payload.HasValue("type", "io.cozy.notes.events") 624 625 doc = payload.Value("doc").Object() 626 doc.HasValue("doctype", "io.cozy.notes.documents") 627 doc.HasValue("title", "A very new title") 628 doc.HasValue("sessionID", "543781490137") 629 630 slice := map[string]interface{}{ 631 "content": []interface{}{ 632 map[string]interface{}{"type": "text", "text": "X"}, 633 }, 634 } 635 steps := []note.Step{ 636 {"sessionID": "543781490137", "stepType": "replace", "from": 2, "to": 2, "slice": slice}, 637 {"sessionID": "543781490137", "stepType": "replace", "from": 3, "to": 3, "slice": slice}, 638 } 639 file, err = note.ApplySteps(inst, file, fmt.Sprintf("%d", version), steps) 640 require.NoError(t, err) 641 642 obj = ws.Expect().TextMessage(). 643 JSON().Object() 644 645 obj.HasValue("event", "UPDATED") 646 payload = obj.Value("payload").Object() 647 payload.HasValue("id", noteID) 648 payload.HasValue("type", "io.cozy.notes.events") 649 650 doc4 := payload.Value("doc").Object() 651 652 obj = ws.Expect().TextMessage(). 653 JSON().Object() 654 655 obj.HasValue("event", "UPDATED") 656 payload = obj.Value("payload").Object() 657 payload.HasValue("id", noteID) 658 payload.HasValue("type", "io.cozy.notes.events") 659 doc5 := payload.Value("doc").Object() 660 661 // // In some cases, the steps can be received in the bad order because of the 662 // // concurrency between the goroutines in the realtime hub. 663 if doc4.Value("version").Number().Raw() > doc5.Value("version").Number().Raw() { 664 doc4, doc5 = doc5, doc4 665 } 666 667 doc4.HasValue("doctype", "io.cozy.notes.steps") 668 doc4.HasValue("sessionID", "543781490137") 669 doc4.HasValue("stepType", "replace") 670 doc4.HasValue("from", 2) 671 doc4.HasValue("to", 2) 672 vers4 := int(doc4.Value("version").Number().Gt(0).Raw()) 673 674 doc5.HasValue("doctype", "io.cozy.notes.steps") 675 doc5.HasValue("sessionID", "543781490137") 676 doc5.HasValue("stepType", "replace") 677 doc5.HasValue("from", 3) 678 doc5.HasValue("to", 3) 679 vers5 := int(doc5.Value("version").Number(). 680 NotEqual(0). 681 NotEqual(vers4). 682 Raw()) 683 684 assert.EqualValues(t, file.Metadata["version"], vers5) 685 }) 686 687 t.Run("CreateNote with a content", func(t *testing.T) { 688 e := testutils.CreateTestClient(t, ts.URL) 689 690 obj := e.POST("/notes"). 691 WithHeader("Authorization", "Bearer "+token). 692 WithHeader("Content-Type", "application/json"). 693 WithBytes([]byte(`{ 694 "data": { 695 "type": "io.cozy.notes.documents", 696 "attributes": { 697 "title": "A note with some content", 698 "schema": { 699 "nodes": [ 700 ["doc", { "content": "block+" }], 701 ["paragraph", { "content": "inline*", "group": "block" }], 702 ["text", { "group": "inline" }], 703 ["bullet_list", { "content": "list_item+", "group": "block" }], 704 ["list_item", { "content": "paragraph block*" }] 705 ], 706 "marks": [ 707 ["em", {}], 708 ["strong", {}] 709 ], 710 "topNode": "doc" 711 }, 712 "content": { 713 "content": [ 714 { 715 "content": [{ "text": "Hello world", "type": "text" }], 716 "type": "paragraph" 717 } 718 ], 719 "type": "doc" 720 } 721 } 722 } 723 }`)). 724 Expect().Status(201). 725 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 726 Object() 727 728 data := obj.Value("data").Object() 729 730 data.HasValue("type", "io.cozy.files") 731 otherNoteID = data.Value("id").String().NotEmpty().Raw() 732 733 attrs := data.Value("attributes").Object() 734 attrs.HasValue("type", "file") 735 attrs.HasValue("name", "A note with some content.cozy-note") 736 attrs.HasValue("mime", "text/vnd.cozy.note+markdown") 737 738 meta := attrs.Value("metadata").Object() 739 meta.HasValue("title", "A note with some content") 740 meta.HasValue("version", 0) 741 meta.Value("schema").Object().NotEmpty() 742 743 expected := map[string]interface{}{ 744 "content": []interface{}{ 745 map[string]interface{}{ 746 "content": []interface{}{ 747 map[string]interface{}{"text": "Hello world", "type": "text"}, 748 }, 749 "type": "paragraph", 750 }, 751 }, 752 "type": "doc", 753 } 754 meta.Value("content").Object().IsEqual(expected) 755 }) 756 757 t.Run("UploadImage", func(t *testing.T) { 758 e := testutils.CreateTestClient(t, ts.URL) 759 760 rawFile, err := os.ReadFile("../../tests/fixtures/wet-cozy_20160910__M4Dz.jpg") 761 require.NoError(t, err) 762 763 for i := 0; i < 3; i++ { 764 obj := e.POST("/notes/"+noteID+"/images"). 765 WithQuery("Name", "wet.jpg"). 766 WithHeader("Authorization", "Bearer "+token). 767 WithHeader("Content-Type", "image/jpeg"). 768 WithBytes(rawFile). 769 Expect().Status(201). 770 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 771 Object() 772 773 data := obj.Value("data").Object() 774 data.HasValue("type", consts.NotesImages) 775 data.Value("id").String().NotEmpty() 776 data.Value("meta").Object().NotEmpty() 777 778 attrs := data.Value("attributes").Object() 779 if i == 0 { 780 attrs.HasValue("name", "wet.jpg") 781 } else { 782 attrs.HasValue("name", fmt.Sprintf("wet (%d).jpg", i+1)) 783 } 784 785 attrs.Value("cozyMetadata").Object().NotEmpty() 786 attrs.HasValue("mime", "image/jpeg") 787 attrs.HasValue("width", 440) 788 attrs.HasValue("height", 294) 789 790 data.Path("$.links.self").String().NotEmpty() 791 } 792 }) 793 794 t.Run("CopyImage", func(t *testing.T) { 795 e := testutils.CreateTestClient(t, ts.URL) 796 797 rawFile, err := os.ReadFile("../../tests/fixtures/wet-cozy_20160910__M4Dz.jpg") 798 require.NoError(t, err) 799 800 obj := e.POST("/notes/"+noteID+"/images"). 801 WithQuery("Name", "tobecopied.jpg"). 802 WithHeader("Authorization", "Bearer "+token). 803 WithHeader("Content-Type", "image/jpeg"). 804 WithBytes(rawFile). 805 Expect().Status(201). 806 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 807 Object() 808 809 data := obj.Value("data").Object() 810 imgID := data.Value("id").String().NotEmpty().Raw() 811 812 obj = e.POST("/notes/"+imgID+"/copy"). 813 WithQuery("To", otherNoteID). 814 WithHeader("Authorization", "Bearer "+token). 815 Expect().Status(201). 816 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 817 Object() 818 819 data = obj.Value("data").Object() 820 data.HasValue("type", consts.NotesImages) 821 data.Value("id").String().NotEmpty().NotEqual(imgID) 822 data.Value("meta").Object().NotEmpty() 823 824 attrs := data.Value("attributes").Object() 825 attrs.HasValue("name", "tobecopied.jpg") 826 827 attrs.Value("cozyMetadata").Object().NotEmpty() 828 attrs.HasValue("mime", "image/jpeg") 829 attrs.HasValue("width", 440) 830 attrs.HasValue("height", 294) 831 832 data.Path("$.links.self").String().NotEmpty() 833 }) 834 835 t.Run("GetImage", func(t *testing.T) { 836 e := testutils.CreateTestClient(t, ts.URL) 837 838 rawFile, err := os.ReadFile("../../tests/fixtures/wet-cozy_20160910__M4Dz.jpg") 839 require.NoError(t, err) 840 841 obj := e.POST("/notes/"+noteID+"/images"). 842 WithQuery("Name", "wet.jpg"). 843 WithHeader("Authorization", "Bearer "+token). 844 WithHeader("Content-Type", "image/jpeg"). 845 WithBytes(rawFile). 846 Expect().Status(201). 847 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 848 Object() 849 850 data := obj.Value("data").Object() 851 data.HasValue("type", consts.NotesImages) 852 data.Value("id").String().NotEmpty() 853 data.Value("meta").Object().NotEmpty() 854 855 link := data.Path("$.links.self").String().NotEmpty().Raw() 856 857 e.GET(link). 858 Expect().Status(200). 859 Body().IsEqual(string(rawFile)) 860 861 obj = e.GET("/files/"+noteID). 862 WithHeader("Authorization", "Bearer "+token). 863 Expect().Status(200). 864 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 865 Object() 866 867 image := obj.Value("included").Array(). 868 Find(func(_ int, value *httpexpect.Value) bool { 869 value.Object().NotHasValue("type", consts.FilesVersions) 870 return true 871 }). 872 Object() 873 874 image.HasValue("type", consts.NotesImages) 875 image.Value("id").String().NotEmpty() 876 image.Value("meta").Object().NotEmpty() 877 878 attrs := image.Value("attributes").Object() 879 attrs.Value("name").String().NotEmpty() 880 attrs.Value("cozyMetadata").Object().NotEmpty() 881 attrs.HasValue("mime", "image/jpeg") 882 883 data.Path("$.links.self").String().NotEmpty() 884 }) 885 886 t.Run("ImportNotes", func(t *testing.T) { 887 e := testutils.CreateTestClient(t, ts.URL) 888 889 obj := e.POST("/files/io.cozy.files.root-dir"). 890 WithQuery("Type", "file"). 891 WithQuery("Name", "An imported note.cozy-note"). 892 WithHeader("Authorization", "Bearer "+token). 893 WithHeader("Content-Type", "text/plain"). 894 WithBytes([]byte(` 895 # Title 896 897 Text with **bold** and [underlined]{.underlined}. 898 `)). 899 Expect().Status(201). 900 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 901 Object() 902 903 data := obj.Value("data").Object() 904 data.HasValue("type", "io.cozy.files") 905 fileID := data.Value("id").String().NotEmpty().Raw() 906 907 attrs := data.Value("attributes").Object() 908 attrs.HasValue("type", "file") 909 attrs.HasValue("name", "An imported note.cozy-note") 910 attrs.HasValue("mime", "text/vnd.cozy.note+markdown") 911 912 meta := attrs.Value("metadata").Object() 913 meta.HasValue("title", "An imported note") 914 meta.Value("schema").Object().NotEmpty() 915 meta.Value("content").Object().NotEmpty() 916 917 obj = e.GET("/notes/"+fileID+"/open"). 918 WithHeader("Authorization", "Bearer "+token). 919 Expect().Status(200). 920 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 921 Object() 922 923 data = obj.Value("data").Object() 924 data.HasValue("id", fileID) 925 data.Path("$.attributes.instance").IsEqual(inst.Domain) 926 }) 927 928 t.Run("CopyNoteWithAnImage", func(t *testing.T) { 929 e := testutils.CreateTestClient(t, ts.URL) 930 931 toImport, err := os.ReadFile("../../tests/fixtures/note-with-an-image.cozy-note") 932 require.NoError(t, err) 933 934 obj := e.POST("/files/io.cozy.files.root-dir"). 935 WithQuery("Type", "file"). 936 WithQuery("Name", "Note with an image.cozy-note"). 937 WithHeader("Authorization", "Bearer "+token). 938 WithHeader("Content-Type", "text/plain"). 939 WithBytes(toImport). 940 Expect().Status(201). 941 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 942 Object() 943 944 data := obj.Value("data").Object() 945 data.HasValue("type", "io.cozy.files") 946 srcID := data.Value("id").String().NotEmpty().Raw() 947 948 obj = e.GET("/files/"+srcID). 949 WithHeader("Authorization", "Bearer "+token). 950 Expect().Status(200). 951 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 952 Object() 953 954 image := obj.Value("included").Array(). 955 Find(func(_ int, value *httpexpect.Value) bool { 956 value.Object().NotHasValue("type", consts.FilesVersions) 957 return true 958 }). 959 Object() 960 961 image.HasValue("type", consts.NotesImages) 962 image.Value("id").String().NotEmpty() 963 image.Value("meta").Object().NotEmpty() 964 965 attrs := image.Value("attributes").Object() 966 attrs.Value("name").String().NotEmpty() 967 attrs.Value("cozyMetadata").Object().NotEmpty() 968 attrs.HasValue("mime", "image/png") 969 970 link := data.Path("$.links.self").String().NotEmpty().Raw() 971 972 e.GET(link). 973 WithHeader("Authorization", "Bearer "+token). 974 Expect().Status(200) 975 976 obj = e.POST("/files/"+srcID+"/copy"). 977 WithHeader("Authorization", "Bearer "+token). 978 Expect().Status(201). 979 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 980 Object() 981 982 data = obj.Value("data").Object() 983 data.HasValue("type", "io.cozy.files") 984 dstID := data.Value("id").String().NotEmpty().Raw() 985 986 obj = e.GET("/files/"+dstID). 987 WithHeader("Authorization", "Bearer "+token). 988 Expect().Status(200). 989 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 990 Object() 991 992 image = obj.Value("included").Array(). 993 Find(func(_ int, value *httpexpect.Value) bool { 994 value.Object().NotHasValue("type", consts.FilesVersions) 995 return true 996 }). 997 Object() 998 999 image.HasValue("type", consts.NotesImages) 1000 image.Value("id").String().NotEmpty() 1001 image.Value("meta").Object().NotEmpty() 1002 1003 attrs = image.Value("attributes").Object() 1004 attrs.Value("name").String().NotEmpty() 1005 attrs.Value("cozyMetadata").Object().NotEmpty() 1006 attrs.HasValue("mime", "image/png") 1007 1008 link = data.Path("$.links.self").String().NotEmpty().Raw() 1009 1010 e.GET(link). 1011 WithHeader("Authorization", "Bearer "+token). 1012 Expect().Status(200) 1013 }) 1014 } 1015 1016 func assertInitialNote(t *testing.T, obj *httpexpect.Object) { 1017 data := obj.Value("data").Object() 1018 1019 data.HasValue("type", "io.cozy.files") 1020 data.Value("id").String().NotEmpty() 1021 1022 attrs := data.Value("attributes").Object() 1023 attrs.HasValue("type", "file") 1024 attrs.HasValue("name", "A super note.cozy-note") 1025 attrs.HasValue("mime", "text/vnd.cozy.note+markdown") 1026 1027 fcm := attrs.Value("cozyMetadata").Object() 1028 fcm.Value("createdAt").String().AsDateTime(time.RFC3339) 1029 fcm.Value("createdOn").String().NotEmpty() 1030 1031 meta := attrs.Value("metadata").Object() 1032 meta.HasValue("title", "A super note") 1033 meta.HasValue("version", 0) 1034 meta.Value("schema").Object().NotEmpty() 1035 meta.Value("content").Object().NotEmpty() 1036 }