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  }