Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,69 @@
package service
import (
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
microstore "go-micro.dev/v4/store"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/collaboration/pkg/config"
"github.com/qsfera/server/services/collaboration/pkg/helpers"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
AppURLs *helpers.AppURLs
GatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient]
Store microstore.Store
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the Logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Config provides a function to set the Config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// AppURLs provides a function to set the AppURLs option.
func AppURLs(val *helpers.AppURLs) Option {
return func(o *Options) {
o.AppURLs = val
}
}
// GatewaySelector provides a function to set the GatewaySelector option.
func GatewaySelector(val pool.Selectable[gatewayv1beta1.GatewayAPIClient]) Option {
return func(o *Options) {
o.GatewaySelector = val
}
}
// Store proivdes a function to set the store
func Store(val microstore.Store) Option {
return func(o *Options) {
o.Store = val
}
}
@@ -0,0 +1,295 @@
package service
import (
"context"
"errors"
"net/url"
"path"
"strconv"
"strings"
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
"github.com/opencloud-eu/reva/v2/pkg/storagespace"
"github.com/opencloud-eu/reva/v2/pkg/utils"
microstore "go-micro.dev/v4/store"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/collaboration/pkg/config"
"github.com/qsfera/server/services/collaboration/pkg/helpers"
"github.com/qsfera/server/services/collaboration/pkg/middleware"
"github.com/qsfera/server/services/collaboration/pkg/wopisrc"
)
// NewHandler creates a new grpc service implementing the OpenInApp interface
func NewHandler(opts ...Option) (*Service, func(), error) {
teardown := func() {
/* this is required as a argument for the return value to satisfy the interface */
/* in case you are wondering about the necessity of this comment, sonarcloud is asking for it */
}
options := newOptions(opts...)
gatewaySelector := options.GatewaySelector
var err error
if gatewaySelector == nil {
gatewaySelector, err = pool.GatewaySelector(options.Config.CS3Api.Gateway.Name)
if err != nil {
return nil, teardown, err
}
}
if options.AppURLs == nil {
return nil, teardown, errors.New("AppURLs option is required")
}
return &Service{
id: options.Config.GRPC.Namespace + "." + options.Config.Service.Name,
appURLs: options.AppURLs,
logger: options.Logger,
config: options.Config,
gatewaySelector: gatewaySelector,
store: options.Store,
}, teardown, nil
}
// Service implements the OpenInApp interface
type Service struct {
id string
appURLs *helpers.AppURLs
logger log.Logger
config *config.Config
gatewaySelector pool.Selectable[gatewayv1beta1.GatewayAPIClient]
store microstore.Store
}
// OpenInApp will implement the OpenInApp interface of the app provider
func (s *Service) OpenInApp(
ctx context.Context,
req *appproviderv1beta1.OpenInAppRequest,
) (*appproviderv1beta1.OpenInAppResponse, error) {
// get the current user
var user *userv1beta1.User = nil
meReq := &gatewayv1beta1.WhoAmIRequest{
Token: req.GetAccessToken(),
}
gwc, err := s.gatewaySelector.Next()
if err != nil {
s.logger.Error().Err(err).Msg("OpenInApp: could not select a gateway client")
return nil, err
}
meResp, err := gwc.WhoAmI(ctx, meReq)
if err == nil {
if meResp.GetStatus().GetCode() == rpcv1beta1.Code_CODE_OK {
user = meResp.GetUser()
}
}
// required for the response, it will be used also for logs
providerFileRef := providerv1beta1.Reference{
ResourceId: req.GetResourceInfo().GetId(),
Path: ".",
}
logger := s.logger.With().
Str("FileReference", providerFileRef.String()).
Str("ViewMode", req.GetViewMode().String()).
Str("Requester", user.GetId().String()).
Logger()
// get the file extension to use the right wopi app url
fileExt := path.Ext(req.GetResourceInfo().GetPath())
// get the appURL we need to use
appURL := s.getAppUrl(fileExt, req.GetViewMode())
if appURL == "" {
logger.Error().Msg("OpenInApp: neither edit nor view app URL found")
return nil, errors.New("neither edit nor view app URL found")
}
// append the parameters we need
appURL, err = s.addQueryToURL(appURL, req)
if err != nil {
logger.Error().Err(err).Msg("OpenInApp: error parsing appUrl")
return &appproviderv1beta1.OpenInAppResponse{
Status: &rpcv1beta1.Status{
Code: rpcv1beta1.Code_CODE_INVALID_ARGUMENT,
Message: "OpenInApp: error parsing appUrl",
},
}, nil
}
// create the wopiContext and generate the token
wopiContext := middleware.WopiContext{
AccessToken: req.GetAccessToken(), // it will be encrypted
ViewOnlyToken: utils.ReadPlainFromOpaque(req.GetOpaque(), "viewOnlyToken"),
FileReference: &providerFileRef,
ViewMode: req.GetViewMode(),
}
if templateID := utils.ReadPlainFromOpaque(req.GetOpaque(), "template"); templateID != "" {
// we can ignore the error here, as we are sure that the templateID is not empty
templateRes, _ := storagespace.ParseID(templateID)
// we need to have at least both opaqueID and spaceID set
if templateRes.GetOpaqueId() == "" || templateRes.GetSpaceId() == "" {
logger.Error().Err(err).Msg("OpenInApp: error parsing templateID")
return &appproviderv1beta1.OpenInAppResponse{
Status: &rpcv1beta1.Status{
Code: rpcv1beta1.Code_CODE_INVALID_ARGUMENT,
Message: "OpenInApp: error parsing templateID",
},
}, nil
}
wopiContext.TemplateReference = &providerv1beta1.Reference{
ResourceId: &templateRes,
Path: ".",
}
}
accessToken, accessExpiration, err := middleware.GenerateWopiToken(wopiContext, s.config, s.store)
if err != nil {
logger.Error().Err(err).Msg("OpenInApp: error generating the token")
return &appproviderv1beta1.OpenInAppResponse{
Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_INTERNAL},
}, err
}
logger.Debug().Msg("OpenInApp: success")
return &appproviderv1beta1.OpenInAppResponse{
Status: &rpcv1beta1.Status{Code: rpcv1beta1.Code_CODE_OK},
AppUrl: &appproviderv1beta1.OpenInAppURL{
AppUrl: appURL,
Method: "POST",
FormParameters: map[string]string{
// these parameters will be passed to the web server by the app provider application
"access_token": accessToken,
// milliseconds since Jan 1, 1970 UTC as required in https://docs.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/concepts#access_token_ttl
//"access_token_ttl": strconv.FormatInt(claims.ExpiresAt.UnixMilli(), 10),
"access_token_ttl": strconv.FormatInt(accessExpiration, 10),
},
},
}, nil
}
// getAppUrl will get the appURL that should be used based on the extension
// and the provided view mode.
// "view" urls will be chosen first, then if the view mode is "read/write",
// "edit" urls will be prioritized. Note that "view" url might be returned for
// "read/write" view mode if no "edit" url is found.
func (s *Service) getAppUrl(fileExt string, viewMode appproviderv1beta1.ViewMode) string {
// prioritize view action if possible
appURL := s.appURLs.GetAppURLFor("view", fileExt)
if strings.ToLower(s.config.App.Product) == "collabora" {
// collabora provides only one action per extension. usual options
// are "view" (checked above), "edit" or "view_comment" (this last one
// is exclusive of collabora)
if appURL == "" {
if editURL := s.appURLs.GetAppURLFor("edit", fileExt); editURL != "" {
return editURL
}
if commentURL := s.appURLs.GetAppURLFor("view_comment", fileExt); commentURL != "" {
return commentURL
}
}
} else {
// If not collabora, there might be an edit action for the extension.
// If read/write mode has been requested, prioritize edit action.
if viewMode == appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE {
if editAppURL := s.appURLs.GetAppURLFor("edit", fileExt); editAppURL != "" {
appURL = editAppURL
}
}
}
return appURL
}
// addQueryToURL will add specific query parameters to the baseURL. These
// parameters are:
// * "WOPISrc" pointing to the requested resource in the OpenInAppRequest
// * "dchat" to disable the chat, based on configuration
// * "lang" (WOPI app dependent) with the language in the request. "lang"
// for collabora, "ui" for onlyoffice and "UI_LLCC" for the rest
func (s *Service) addQueryToURL(baseURL string, req *appproviderv1beta1.OpenInAppRequest) (string, error) {
u, err := url.Parse(baseURL)
if err != nil {
return "", err
}
// build a urlsafe and stable file reference that can be used for proxy routing,
// so that all sessions on one file end on the same office server
fileRef := helpers.HashResourceId(req.GetResourceInfo().GetId())
wopiSrcURL, err := wopisrc.GenerateWopiSrc(fileRef, s.config)
if err != nil {
return "", err
}
q := u.Query()
q.Add("WOPISrc", wopiSrcURL.String())
if s.config.Wopi.DisableChat {
q.Add("dchat", "1")
}
lang := utils.ReadPlainFromOpaque(req.GetOpaque(), "lang")
// @TODO: this is a temporary solution until we figure out how to send these from oc web
switch lang {
case "bg":
lang = "bg-BG"
case "cs":
lang = "cs-CZ"
case "de":
lang = "de-DE"
case "en":
lang = "en-GB"
case "es":
lang = "es-ES"
case "fr":
lang = "fr-FR"
case "gl":
lang = "gl-ES"
case "it":
lang = "it-IT"
case "nl":
lang = "nl-NL"
case "ko":
lang = "ko-KR"
case "sq":
lang = "sq-AL"
case "sv":
lang = "sv-SE"
case "tr":
lang = "tr-TR"
case "zh":
lang = "zh-CN"
}
if lang != "" {
switch strings.ToLower(s.config.App.Product) {
case "collabora":
q.Add("lang", lang)
case "onlyoffice":
q.Add("ui", lang)
default:
q.Add("UI_LLCC", lang)
}
}
if strings.ToLower(s.config.App.Product) == "collabora" {
q.Add("closebutton", "false")
}
qs := q.Encode()
u.RawQuery = qs
return u.String(), nil
}
@@ -0,0 +1,13 @@
package service_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestService(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Service Suite")
}
@@ -0,0 +1,383 @@
package service_test
import (
"context"
"strconv"
"time"
appproviderv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1"
authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1"
gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/golang-jwt/jwt/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
"github.com/opencloud-eu/reva/v2/pkg/utils"
cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks"
"github.com/stretchr/testify/mock"
"github.com/qsfera/server/pkg/log"
"github.com/qsfera/server/services/collaboration/mocks"
"github.com/qsfera/server/services/collaboration/pkg/config"
"github.com/qsfera/server/services/collaboration/pkg/helpers"
service "github.com/qsfera/server/services/collaboration/pkg/service/grpc/v0"
)
// Based on https://github.com/cs3org/reva/blob/b99ad4865401144a981d4cfd1ae28b5a018ea51d/pkg/token/manager/jwt/jwt.go#L82
func MintToken(u *userv1beta1.User, secret string, nowTime time.Time) string {
scopes := make(map[string]*authpb.Scope)
scopes["user"] = &authpb.Scope{
Resource: &types.OpaqueEntry{
Decoder: "json",
Value: []byte("{\"Path\":\"/\"}"),
},
Role: authpb.Role_ROLE_OWNER,
}
claims := jwt.MapClaims{
"exp": nowTime.Add(5 * time.Hour).Unix(),
"iss": "myself",
"aud": "reva",
"iat": nowTime.Unix(),
"user": u,
"scope": scopes,
}
/*
claims := claims{
StandardClaims: jwt.RegisteredClaims{
ExpiresAt: time.Now().Add(5 * time.Hour),
Issuer: "myself",
Audience: "reva",
IssuedAt: time.Now(),
},
User: u,
Scope: scopes,
}
*/
t := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims)
tkn, _ := t.SignedString([]byte(secret))
return tkn
}
var _ = Describe("Discovery", func() {
var (
cfg *config.Config
gatewayClient *cs3mocks.GatewayAPIClient
srv *service.Service
srvTear func()
)
BeforeEach(func() {
cfg = &config.Config{}
gatewayClient = &cs3mocks.GatewayAPIClient{}
gatewaySelector := mocks.NewSelectable[gatewayv1beta1.GatewayAPIClient](GinkgoT())
gatewaySelector.On("Next").Return(gatewayClient, nil)
appURLs := helpers.NewAppURLs()
appURLs.Store(map[string]map[string]string{
"view": {
".pdf": "https://cloud.qsfera.test/hosting/wopi/word/view",
".djvu": "https://cloud.qsfera.test/hosting/wopi/word/view",
".docx": "https://cloud.qsfera.test/hosting/wopi/word/view",
".xls": "https://cloud.qsfera.test/hosting/wopi/cell/view",
".xlsb": "https://cloud.qsfera.test/hosting/wopi/cell/view",
},
"edit": {
".docx": "https://cloud.qsfera.test/hosting/wopi/word/edit",
".invalid": "://cloud.qsfera.test/hosting/wopi/cell/edit",
},
})
srv, srvTear, _ = service.NewHandler(
service.Logger(log.NopLogger()),
service.Config(cfg),
service.AppURLs(appURLs),
service.GatewaySelector(gatewaySelector),
)
})
AfterEach(func() {
srvTear()
})
Describe("OpenInApp", func() {
It("Invalid access token", func() {
ctx := context.Background()
cfg.Wopi.WopiSrc = "https://wopi.qsfera.test"
req := &appproviderv1beta1.OpenInAppRequest{
ResourceInfo: &providerv1beta1.ResourceInfo{
Id: &providerv1beta1.ResourceId{
StorageId: "myStorage",
OpaqueId: "storageOpaque001",
SpaceId: "SpaceA",
},
Path: "/path/to/file",
},
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
AccessToken: "goodAccessToken",
}
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
Status: status.NewOK(ctx),
User: &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "myIdp",
OpaqueId: "opaque001",
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
},
Username: "username",
},
}, nil)
resp, err := srv.OpenInApp(ctx, req)
Expect(err).To(HaveOccurred())
Expect(resp).To(BeNil())
})
DescribeTable(
"Success",
func(appName, lang string, disableChat bool, expectedAppUrl string) {
ctx := context.Background()
nowTime := time.Now()
cfg.Wopi.WopiSrc = "https://wopi.qsfera.test"
cfg.Wopi.Secret = "my_supa_secret"
cfg.Wopi.DisableChat = disableChat
cfg.App.Name = appName
cfg.App.Product = appName
myself := &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "myIdp",
OpaqueId: "opaque001",
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
},
Username: "username",
}
req := &appproviderv1beta1.OpenInAppRequest{
ResourceInfo: &providerv1beta1.ResourceInfo{
Id: &providerv1beta1.ResourceId{
StorageId: "myStorage",
OpaqueId: "storageOpaque001",
SpaceId: "SpaceA",
},
Path: "/path/to/file.docx",
},
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
}
if lang != "" {
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", lang)
}
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
Status: status.NewOK(ctx),
User: myself,
}, nil)
resp, err := srv.OpenInApp(ctx, req)
Expect(err).To(Succeed())
Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK))
Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST"))
Expect(resp.GetAppUrl().GetAppUrl()).To(Equal(expectedAppUrl))
Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10)))
},
Entry("Microsoft chat no lang", "Microsoft", "", false, "https://cloud.qsfera.test/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
Entry("Collabora chat no lang", "Collabora", "", false, "https://cloud.qsfera.test/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&closebutton=false"),
Entry("OnlyOffice chat no lang", "OnlyOffice", "", false, "https://cloud.qsfera.test/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
Entry("Microsoft chat lang", "Microsoft", "de", false, "https://cloud.qsfera.test/hosting/wopi/word/edit?UI_LLCC=de-DE&WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e"),
Entry("Collabora chat lang", "Collabora", "de", false, "https://cloud.qsfera.test/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&closebutton=false&lang=de-DE"),
Entry("OnlyOffice chat lang", "OnlyOffice", "de", false, "https://cloud.qsfera.test/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&ui=de-DE"),
Entry("Microsoft no chat no lang", "Microsoft", "", true, "https://cloud.qsfera.test/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
Entry("Collabora no chat no lang", "Collabora", "", true, "https://cloud.qsfera.test/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&closebutton=false&dchat=1"),
Entry("OnlyOffice no chat no lang", "OnlyOffice", "", true, "https://cloud.qsfera.test/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
Entry("Microsoft no chat lang", "Microsoft", "de", true, "https://cloud.qsfera.test/hosting/wopi/word/edit?UI_LLCC=de-DE&WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1"),
Entry("Collabora no chat lang", "Collabora", "de", true, "https://cloud.qsfera.test/hosting/wopi/word/view?WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&closebutton=false&dchat=1&lang=de-DE"),
Entry("OnlyOffice no chat lang", "OnlyOffice", "de", true, "https://cloud.qsfera.test/hosting/wopi/word/edit?WOPISrc=https%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&dchat=1&ui=de-DE"),
)
It("Success with Wopi Proxy", func() {
ctx := context.Background()
nowTime := time.Now()
cfg.Wopi.WopiSrc = "https://wopi.qsfera.test"
cfg.Wopi.Secret = "my_supa_secret"
cfg.Wopi.ProxyURL = "https://office.proxy.qsfera.test"
cfg.Wopi.ProxySecret = "your_supa_secret"
cfg.App.Name = "Microsoft"
myself := &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "myIdp",
OpaqueId: "opaque001",
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
},
Username: "username",
}
req := &appproviderv1beta1.OpenInAppRequest{
ResourceInfo: &providerv1beta1.ResourceInfo{
Id: &providerv1beta1.ResourceId{
StorageId: "myStorage",
OpaqueId: "storageOpaque001",
SpaceId: "SpaceA",
},
Path: "/path/to/file.docx",
},
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
}
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", "en")
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
Status: status.NewOK(ctx),
User: myself,
}, nil)
resp, err := srv.OpenInApp(ctx, req)
Expect(err).To(Succeed())
Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK))
Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST"))
Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://cloud.qsfera.test/hosting/wopi/word/edit?UI_LLCC=en-GB&WOPISrc=https%3A%2F%2Foffice.proxy.qsfera.test%2Fwopi%2Ffiles%2FeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1IjoiaHR0cHM6Ly93b3BpLm9wZW5jbG91ZC50ZXN0L3dvcGkvZmlsZXMvIiwiZiI6IjJmNmVjMTg2OTZkZDEwMDgxMDY3NDliZDk0MTA2ZTVjZmFkNWMwOWUxNWRlN2I3NzA4OGQwMzg0M2U3MWI0M2UifQ.j873xu7TkqtIokSIQXW5y7-BRRrHgIURqAx4WY_zxTA"))
Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10)))
})
It("Fail with invalid app url", func() {
ctx := context.Background()
nowTime := time.Now()
cfg.Wopi.WopiSrc = "htttps://wopi.qsfera.test"
cfg.Wopi.Secret = "my_supa_secret"
cfg.App.Name = "Microsoft"
myself := &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "myIdp",
OpaqueId: "opaque001",
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
},
Username: "username",
}
req := &appproviderv1beta1.OpenInAppRequest{
ResourceInfo: &providerv1beta1.ResourceInfo{
Id: &providerv1beta1.ResourceId{
StorageId: "myStorage",
OpaqueId: "storageOpaque001",
SpaceId: "SpaceA",
},
Path: "/path/to/file.invalid",
},
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
}
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", "en")
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
Status: status.NewOK(ctx),
User: myself,
}, nil)
resp, err := srv.OpenInApp(ctx, req)
Expect(err).To(Succeed())
Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_INVALID_ARGUMENT))
Expect(resp.GetStatus().GetMessage()).To(Equal("OpenInApp: error parsing appUrl"))
})
It("Fail with invalid template id", func() {
ctx := context.Background()
nowTime := time.Now()
cfg.Wopi.WopiSrc = "htttps://wopi.qsfera.test"
cfg.Wopi.Secret = "my_supa_secret"
cfg.App.Name = "Microsoft"
myself := &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "myIdp",
OpaqueId: "opaque001",
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
},
Username: "username",
}
req := &appproviderv1beta1.OpenInAppRequest{
ResourceInfo: &providerv1beta1.ResourceInfo{
Id: &providerv1beta1.ResourceId{
StorageId: "myStorage",
OpaqueId: "storageOpaque001",
SpaceId: "SpaceA",
},
Path: "/path/to/file.docx",
},
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
}
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", "en")
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "template", "&file_id")
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
Status: status.NewOK(ctx),
User: myself,
}, nil)
resp, err := srv.OpenInApp(ctx, req)
Expect(err).To(Succeed())
Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_INVALID_ARGUMENT))
Expect(resp.GetStatus().GetMessage()).To(Equal("OpenInApp: error parsing templateID"))
})
It("Success with valid template id", func() {
ctx := context.Background()
nowTime := time.Now()
cfg.Wopi.WopiSrc = "htttps://wopi.qsfera.test"
cfg.Wopi.Secret = "my_supa_secret"
cfg.App.Name = "OnlyOffice"
cfg.App.Product = "OnlyOffice"
myself := &userv1beta1.User{
Id: &userv1beta1.UserId{
Idp: "myIdp",
OpaqueId: "opaque001",
Type: userv1beta1.UserType_USER_TYPE_PRIMARY,
},
Username: "username",
}
req := &appproviderv1beta1.OpenInAppRequest{
ResourceInfo: &providerv1beta1.ResourceInfo{
Id: &providerv1beta1.ResourceId{
StorageId: "myStorage",
OpaqueId: "storageOpaque001",
SpaceId: "SpaceA",
},
Path: "/path/to/file.docx",
},
ViewMode: appproviderv1beta1.ViewMode_VIEW_MODE_READ_WRITE,
AccessToken: MintToken(myself, cfg.Wopi.Secret, nowTime),
}
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "lang", "en")
req.Opaque = utils.AppendPlainToOpaque(req.Opaque, "template", "prodiderID$spaceID!opaqueID")
gatewayClient.On("WhoAmI", mock.Anything, mock.Anything).Times(1).Return(&gatewayv1beta1.WhoAmIResponse{
Status: status.NewOK(ctx),
User: myself,
}, nil)
resp, err := srv.OpenInApp(ctx, req)
Expect(err).To(Succeed())
Expect(resp.GetStatus().GetCode()).To(Equal(rpcv1beta1.Code_CODE_OK))
Expect(resp.GetAppUrl().GetMethod()).To(Equal("POST"))
Expect(resp.GetAppUrl().GetAppUrl()).To(Equal("https://cloud.qsfera.test/hosting/wopi/word/edit?WOPISrc=htttps%3A%2F%2Fwopi.qsfera.test%2Fwopi%2Ffiles%2F2f6ec18696dd1008106749bd94106e5cfad5c09e15de7b77088d03843e71b43e&ui=en-GB"))
Expect(resp.GetAppUrl().GetFormParameters()["access_token_ttl"]).To(Equal(strconv.FormatInt(nowTime.Add(5*time.Hour).Unix()*1000, 10)))
})
})
})