github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/office/office_test.go (about) 1 package office 2 3 import ( 4 "fmt" 5 "io" 6 "net/http" 7 "net/http/httptest" 8 "testing" 9 "time" 10 11 "github.com/cozy/cozy-stack/model/instance" 12 "github.com/cozy/cozy-stack/model/vfs" 13 "github.com/cozy/cozy-stack/pkg/config/config" 14 "github.com/cozy/cozy-stack/pkg/consts" 15 "github.com/cozy/cozy-stack/tests/testutils" 16 "github.com/cozy/cozy-stack/web/errors" 17 "github.com/gavv/httpexpect/v2" 18 "github.com/labstack/echo/v4" 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 ) 22 23 type fakeServer struct { 24 count int 25 } 26 27 func TestOffice(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 key string 33 34 config.UseTestFile(t) 35 ooURL := fakeOOServer() 36 config.GetConfig().Office = map[string]config.Office{ 37 "default": {OnlyOfficeURL: ooURL}, 38 } 39 testutils.NeedCouchdb(t) 40 setup := testutils.NewSetup(t, t.Name()) 41 inst := setup.GetTestInstance() 42 _, token := setup.GetTestClient(consts.Files) 43 44 fileID := createFile(t, inst) 45 46 ts := setup.GetTestServer("/office", Routes) 47 ts.Config.Handler.(*echo.Echo).HTTPErrorHandler = errors.ErrorHandler 48 t.Cleanup(ts.Close) 49 50 t.Run("OnlyOfficeLocal", func(t *testing.T) { 51 e := testutils.CreateTestClient(t, ts.URL) 52 53 obj := e.GET("/office/"+fileID+"/open"). 54 WithHeader("Authorization", "Bearer "+token). 55 Expect().Status(200). 56 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 57 Object() 58 59 data := obj.Value("data").Object() 60 data.ValueEqual("type", consts.OfficeURL) 61 data.ValueEqual("id", fileID) 62 63 attrs := data.Value("attributes").Object() 64 attrs.ValueEqual("document_id", fileID) 65 attrs.ValueEqual("subdomain", "nested") 66 attrs.Value("protocol").String().Contains("http") 67 attrs.ValueEqual("instance", inst.Domain) 68 attrs.Value("public_name").String().NotEmpty() 69 70 oo := attrs.Value("onlyoffice").Object() 71 oo.Value("url").String().NotEmpty() 72 oo.ValueEqual("documentType", "word") 73 74 editor := oo.Value("editor").Object() 75 editor.ValueEqual("mode", "edit") 76 editor.Value("callbackUrl").String().HasSuffix("/office/callback") 77 78 document := oo.Value("document").Object() 79 key = document.Value("key").String().NotEmpty().Raw() 80 }) 81 82 t.Run("SaveOnlyOffice", func(t *testing.T) { 83 e := testutils.CreateTestClient(t, ts.URL) 84 85 // Force save 86 obj := e.POST("/office/callback"). 87 WithHeader("Content-Type", "application/json"). 88 WithBytes([]byte(fmt.Sprintf(`{ 89 "actions": [{"type": 0, "userid": "78e1e841"}], 90 "key": "%s", 91 "status": 6, 92 "url": "%s", 93 "users": ["6d5a81d0"] 94 }`, key, ooURL+"/dl"))). 95 Expect().Status(200). 96 JSON().Object() 97 98 obj.ValueEqual("error", 0.0) 99 100 doc, err := inst.VFS().FileByID(fileID) 101 assert.NoError(t, err) 102 file, err := inst.VFS().OpenFile(doc) 103 assert.NoError(t, err) 104 defer file.Close() 105 buf, err := io.ReadAll(file) 106 assert.NoError(t, err) 107 assert.Equal(t, "version 1", string(buf)) 108 109 // Final save 110 e.POST("/office/callback"). 111 WithHeader("Content-Type", "application/json"). 112 // Change "status": 6 -> "status": 2 113 WithBytes([]byte(fmt.Sprintf(`{ 114 "actions": [{"type": 0, "userid": "78e1e841"}], 115 "key": "%s", 116 "status": 2, 117 "url": "%s", 118 "users": ["6d5a81d0"] 119 }`, key, ooURL+"/dl"))). 120 Expect().Status(200) 121 122 doc, err = inst.VFS().FileByID(fileID) 123 assert.NoError(t, err) 124 file, err = inst.VFS().OpenFile(doc) 125 assert.NoError(t, err) 126 defer file.Close() 127 buf, err = io.ReadAll(file) 128 assert.NoError(t, err) 129 assert.Equal(t, "version 2", string(buf)) 130 }) 131 132 t.Run("Conflict after an upload", func(t *testing.T) { 133 e := testutils.CreateTestClient(t, ts.URL) 134 135 // When a user opens an office document 136 obj := e.GET("/office/"+fileID+"/open"). 137 WithHeader("Authorization", "Bearer "+token). 138 Expect().Status(200). 139 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 140 Object() 141 data := obj.Value("data").Object() 142 attrs := data.Value("attributes").Object() 143 oo := attrs.Value("onlyoffice").Object() 144 document := oo.Value("document").Object() 145 key = document.Value("key").String().NotEmpty().Raw() 146 147 // the key is associated to this file 148 obj = e.POST("/office/keys/"+key). 149 WithHeader("Authorization", "Bearer "+token). 150 Expect().Status(200). 151 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 152 Object() 153 data = obj.Value("data").Object() 154 docID := data.Value("id").String().NotEmpty().Raw() 155 assert.Equal(t, fileID, docID) 156 attrs = data.Value("attributes").Object() 157 name := attrs.Value("name").String().NotEmpty().Raw() 158 assert.Equal(t, "letter.docx", name) 159 160 // When an upload is made that changes the content of this document, 161 // the key will now be associated to a conflict file 162 updateFile(t, inst, fileID) 163 obj = e.POST("/office/keys/"+key). 164 WithHeader("Authorization", "Bearer "+token). 165 Expect().Status(200). 166 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 167 Object() 168 data = obj.Value("data").Object() 169 conflictID := data.Value("id").String().NotEmpty().Raw() 170 assert.NotEqual(t, fileID, conflictID) 171 meta := data.Value("meta").Object() 172 conflictRev := meta.Value("rev").String().NotEmpty().Raw() 173 attrs = data.Value("attributes").Object() 174 conflictName := attrs.Value("name").String().NotEmpty().Raw() 175 assert.Equal(t, "letter (2).docx", conflictName) 176 177 // When another user uses the same key, they obtains the same file 178 obj = e.POST("/office/keys/"+key). 179 WithHeader("Authorization", "Bearer "+token). 180 Expect().Status(200). 181 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 182 Object() 183 data = obj.Value("data").Object() 184 anotherID := data.Value("id").String().NotEmpty().Raw() 185 assert.Equal(t, conflictID, anotherID) 186 meta = data.Value("meta").Object() 187 anotherRev := meta.Value("rev").String().NotEmpty().Raw() 188 assert.Equal(t, conflictRev, anotherRev) 189 attrs = data.Value("attributes").Object() 190 anotherName := attrs.Value("name").String().NotEmpty().Raw() 191 assert.Equal(t, conflictName, anotherName) 192 193 // When another user opens the document, a new key is given 194 obj = e.GET("/office/"+fileID+"/open"). 195 WithHeader("Authorization", "Bearer "+token). 196 Expect().Status(200). 197 JSON(httpexpect.ContentOpts{MediaType: "application/vnd.api+json"}). 198 Object() 199 data = obj.Value("data").Object() 200 attrs = data.Value("attributes").Object() 201 oo = attrs.Value("onlyoffice").Object() 202 document = oo.Value("document").Object() 203 newkey := document.Value("key").String().NotEmpty().Raw() 204 assert.NotEqual(t, key, newkey) 205 206 // When the document is saved with the first key, it's written to the 207 // conflict file 208 e.POST("/office/callback"). 209 WithHeader("Content-Type", "application/json"). 210 WithBytes([]byte(fmt.Sprintf(`{ 211 "actions": [{"type": 0, "userid": "78e1e841"}], 212 "key": "%s", 213 "status": 2, 214 "url": "%s", 215 "users": ["6d5a81d0"] 216 }`, key, ooURL+"/dl"))). 217 Expect().Status(200) 218 conflict, err := inst.VFS().FileByID(conflictID) 219 require.NoError(t, err) 220 assert.Equal(t, "letter (2).docx", conflict.DocName) 221 assert.Equal(t, "onlyoffice-server", conflict.CozyMetadata.UploadedBy.Slug) 222 assert.Equal(t, "onlyoffice-server", conflict.CozyMetadata.UpdatedByApps[0].Slug) 223 assert.NotEqual(t, conflictRev, conflict.Rev()) 224 }) 225 } 226 227 func createFile(t *testing.T, inst *instance.Instance) string { 228 dirID := consts.RootDirID 229 filedoc, err := vfs.NewFileDoc("letter.docx", dirID, -1, nil, 230 "application/msword", "text", time.Now(), false, false, false, nil) 231 require.NoError(t, err) 232 233 f, err := inst.VFS().CreateFile(filedoc, nil) 234 require.NoError(t, err) 235 require.NoError(t, f.Close()) 236 237 return filedoc.ID() 238 } 239 240 func updateFile(t *testing.T, inst *instance.Instance, fileID string) { 241 olddoc, err := inst.VFS().FileByID(fileID) 242 require.NoError(t, err) 243 244 newdoc := olddoc.Clone().(*vfs.FileDoc) 245 newdoc.ByteSize = -1 246 newdoc.MD5Sum = nil 247 newdoc.CozyMetadata.UploadedBy = &vfs.UploadedByEntry{ 248 Slug: "desktop", 249 Version: "0.0.1", 250 } 251 252 f, err := inst.VFS().CreateFile(newdoc, olddoc) 253 require.NoError(t, err) 254 _, err = io.WriteString(f, "updated") 255 require.NoError(t, err) 256 require.NoError(t, f.Close()) 257 } 258 259 func (f *fakeServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 260 f.count++ 261 body := fmt.Sprintf("version %d", f.count) 262 _, _ = w.Write([]byte(body)) 263 } 264 265 func fakeOOServer() string { 266 handler := &fakeServer{} 267 server := httptest.NewServer(handler) 268 return server.URL 269 }