Skip to content

Commit

Permalink
feat: Add legal notice URL to instance settings
Browse files Browse the repository at this point in the history
  When an instance's context has an associated cloudery and the
  instance's partner on the cloudery has defined a legal notice, the URL
  to this notice will be present in the instance settings returned by
  `GET /settings/instance` calls.
  • Loading branch information
taratatach committed Nov 20, 2024
1 parent 7da91ff commit 0b8cc2d
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 83 deletions.
11 changes: 9 additions & 2 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,13 +690,14 @@ Cookie: sessionid=xxxx
"auth_mode": "basic",
"default_redirection": "drive/#/folder",
"context": "dev",
"sponsorships": ["springfield"]
"sponsorships": ["springfield"],
"legal_notice_url": "https://manager.cozycloud.cc/e96388a5-8eed-44cc-81e6-40aad273f0d4.pdf"
}
}
}
```

#### Note about `password_defined`
##### Note about `password_defined`

There are a few fields that are persisted on the instance its-self, not on its
settings document. When they are updated, it won't be reflected in the realtime
Expand All @@ -706,6 +707,12 @@ For `password_defined`, it is possible to be notified when the password is
defined by watching a synthetic document with the doctype `io.cozy.settings`,
and the id `io.cozy.settings.passphrase`.

##### Note about `legal_notice_url`

This attribute will only be present if a manager is associated with the
instance and the instance was created on behalf of a partner with a defined
legal notice.

### POST /settings/instance/deletion

The settings application can use this route if the user wants to delete their
Expand Down
1 change: 1 addition & 0 deletions model/cloudery/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var service Service
type Service interface {
SaveInstance(inst *instance.Instance, cmd *SaveCmd) error
BlockingSubscription(inst *instance.Instance) (*BlockingSubscription, error)
LegalNoticeUrl(inst *instance.Instance) (string, error)
}

func Init(contexts map[string]config.ClouderyConfig) Service {
Expand Down
27 changes: 27 additions & 0 deletions model/cloudery/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,30 @@ func blockingSubscriptionVendor(clouderyInstance map[string]interface{}) (string

return "", fmt.Errorf("invalid blocking subscription vendor")
}

func (s *ClouderyService) LegalNoticeUrl(inst *instance.Instance) (string, error) {
client := instance.APIManagerClient(inst)
if client == nil {
return "", nil
}

url := fmt.Sprintf("/api/v1/instances/%s", url.PathEscape(inst.UUID))
res, err := client.Get(url)
if err != nil {
return "", fmt.Errorf("request failed: %w", err)
}

return legalNoticeUrl(res)
}

func legalNoticeUrl(clouderyInstance map[string]interface{}) (string, error) {
if str, ok := clouderyInstance["legal_notice_url"]; ok {
if url, ok := str.(string); ok {
return url, nil
}

return "", fmt.Errorf("invalid legal notice url")
}

return "", nil
}
10 changes: 10 additions & 0 deletions model/cloudery/service_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ func (m *Mock) BlockingSubscription(inst *instance.Instance) (*BlockingSubscript

return args.Get(0).(*BlockingSubscription), args.Error(1)
}

func (m *Mock) LegalNoticeUrl(inst *instance.Instance) (string, error) {
args := m.Called(inst)

if args.Get(0) == "" {
return "", args.Error(1)
}

return args.Get(0).(string), args.Error(1)
}
4 changes: 4 additions & 0 deletions model/cloudery/service_noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ func (s *NoopService) SaveInstance(inst *instance.Instance, cmd *SaveCmd) error
func (s *NoopService) BlockingSubscription(inst *instance.Instance) (*BlockingSubscription, error) {
return nil, nil
}

func (s *NoopService) LegalNoticeUrl(inst *instance.Instance) (string, error) {
return "", nil
}
1 change: 1 addition & 0 deletions model/settings/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Service interface {
ConfirmEmailUpdate(inst *instance.Instance, tok string) error
CancelEmailUpdate(inst *instance.Instance) error
GetExternalTies(inst *instance.Instance) (*ExternalTies, error)
GetLegalNoticeUrl(inst *instance.Instance) (string, error)
}

func Init(
Expand Down
4 changes: 4 additions & 0 deletions model/settings/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,7 @@ func (s *SettingsService) GetExternalTies(inst *instance.Instance) (*ExternalTie

return &ties, nil
}

func (s *SettingsService) GetLegalNoticeUrl(inst *instance.Instance) (string, error) {
return s.cloudery.LegalNoticeUrl(inst)
}
10 changes: 10 additions & 0 deletions model/settings/service_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,13 @@ func (m *Mock) GetExternalTies(inst *instance.Instance) (*ExternalTies, error) {

return args.Get(0).(*ExternalTies), args.Error(1)
}

func (m *Mock) GetLegalNoticeUrl(inst *instance.Instance) (string, error) {
args := m.Called(inst)

if args.Get(0) == "" {
return "", args.Error(1)
}

return args.Get(0).(string), args.Error(1)
}
136 changes: 59 additions & 77 deletions model/settings/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@ import (
"github.com/stretchr/testify/assert"
)

func TestServiceImplems(t *testing.T) {
assert.Implements(t, (*Service)(nil), new(SettingsService))
assert.Implements(t, (*Service)(nil), new(Mock))
}

func Test_StartEmailUpdate_success(t *testing.T) {
func setupTest(t *testing.T) (*emailer.Mock, *instance.Mock, *token.Mock, *cloudery.Mock, *storageMock, Service) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
return emailerSvc,
instSvc,
tokenSvc,
clouderySvc,
storage,
NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
}

func TestServiceImplems(t *testing.T) {
assert.Implements(t, (*Service)(nil), new(SettingsService))
assert.Implements(t, (*Service)(nil), new(Mock))
}

func Test_StartEmailUpdate_success(t *testing.T) {
emailerSvc, instSvc, tokenSvc, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand Down Expand Up @@ -64,13 +73,7 @@ func Test_StartEmailUpdate_success(t *testing.T) {
}

func Test_StartEmailUpdate_with_an_invalid_password(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, instSvc, _, _, _, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -88,13 +91,7 @@ func Test_StartEmailUpdate_with_an_invalid_password(t *testing.T) {
}

func Test_StartEmailUpdate_with_a_missing_public_name(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
emailerSvc, instSvc, tokenSvc, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand Down Expand Up @@ -135,13 +132,7 @@ func Test_StartEmailUpdate_with_a_missing_public_name(t *testing.T) {
}

func TestConfirmEmailUpdate_success(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, tokenSvc, clouderySvc, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand Down Expand Up @@ -178,13 +169,7 @@ func TestConfirmEmailUpdate_success(t *testing.T) {
}

func TestConfirmEmailUpdate_with_an_invalid_token(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, tokenSvc, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -206,13 +191,7 @@ func TestConfirmEmailUpdate_with_an_invalid_token(t *testing.T) {
}

func TestConfirmEmailUpdate_without_a_pending_email(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, _, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -231,13 +210,7 @@ func TestConfirmEmailUpdate_without_a_pending_email(t *testing.T) {
}

func Test_CancelEmailUpdate_success(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, _, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -264,13 +237,7 @@ func Test_CancelEmailUpdate_success(t *testing.T) {
}

func Test_CancelEmailUpdate_without_pending_email(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, _, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -288,13 +255,7 @@ func Test_CancelEmailUpdate_without_pending_email(t *testing.T) {
}

func Test_ResendEmailUpdate_success(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
emailerSvc, _, tokenSvc, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand Down Expand Up @@ -323,13 +284,7 @@ func Test_ResendEmailUpdate_success(t *testing.T) {
}

func Test_ResendEmailUpdate_with_no_pending_email(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, _, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -347,13 +302,7 @@ func Test_ResendEmailUpdate_with_no_pending_email(t *testing.T) {
}

func Test_GetExternalTies(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, _, clouderySvc, _, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand Down Expand Up @@ -386,3 +335,36 @@ func Test_GetExternalTies(t *testing.T) {
assert.Nil(t, ties)
})
}

func Test_GetLegalNoticeUrl(t *testing.T) {
_, _, _, clouderySvc, _, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
}

t.Run("with a legal notice", func(t *testing.T) {
clouderySvc.On("LegalNoticeUrl", &inst).Return("https://testmanager.cozycloud.cc", nil).Once()

url, err := svc.GetLegalNoticeUrl(&inst)
assert.NoError(t, err)
assert.Equal(t, "https://testmanager.cozycloud.cc", url)
})

t.Run("without a legal notice", func(t *testing.T) {
clouderySvc.On("LegalNoticeUrl", &inst).Return("", nil).Once()

url, err := svc.GetLegalNoticeUrl(&inst)
assert.NoError(t, err)
assert.Equal(t, "", url)
})

t.Run("with error from cloudery", func(t *testing.T) {
unauthorizedError := errors.New("unauthorized")
clouderySvc.On("LegalNoticeUrl", &inst).Return("", unauthorizedError).Once()

url, err := svc.GetLegalNoticeUrl(&inst)
assert.ErrorIs(t, err, unauthorizedError)
assert.Equal(t, "", url)
})
}
8 changes: 8 additions & 0 deletions web/settings/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ func (h *HTTPHandler) getInstance(c echo.Context) error {
return err
}

url, err := h.svc.GetLegalNoticeUrl(inst)
if err != nil {
return err
}
if url != "" {
doc.M["legal_notice_url"] = url
}

return jsonapi.Data(c, http.StatusOK, &apiInstance{doc}, nil)
}

Expand Down
Loading

0 comments on commit 0b8cc2d

Please sign in to comment.