Sharing of lists via public links (#94)

This commit is contained in:
konrad 2019-08-31 20:56:41 +00:00 committed by Gitea
parent 88ea66798b
commit 8d57923a7d
41 changed files with 3425 additions and 590 deletions

View file

@ -296,7 +296,7 @@ Sorry for some of them being in German, I'll tranlate them at some point.
* [ ] User online status * [ ] User online status
* [ ] More sharing features (all of these with the already existing permissions) * [ ] More sharing features (all of these with the already existing permissions)
* [ ] Invite users per mail * [ ] Invite users per mail
* [ ] Share a link with/without password * [x] Share a link with/without password
* [ ] Comments on tasks * [ ] Comments on tasks
* [ ] @mention users in tasks or comments to get them notified * [ ] @mention users in tasks or comments to get them notified
* [ ] Summary of tasks to do in a configurable interval (every day/week or so) * [ ] Summary of tasks to do in a configurable interval (every day/week or so)

View file

@ -18,6 +18,8 @@ service:
enablemetrics: false enablemetrics: false
# Enable the caldav endpoint, see the docs for more details # Enable the caldav endpoint, see the docs for more details
enablecaldav: true enablecaldav: true
# Enable sharing of lists via a link
enablelinksharing: true
database: database:
# Database type to use. Supported types are mysql and sqlite. # Database type to use. Supported types are mysql and sqlite.

View file

@ -61,6 +61,8 @@ service:
enablemetrics: false enablemetrics: false
# Enable the caldav endpoint, see the docs for more details # Enable the caldav endpoint, see the docs for more details
enablecaldav: true enablecaldav: true
# Enable sharing of lists via a link
enablelinksharing: true
database: database:
# Database type to use. Supported types are mysql and sqlite. # Database type to use. Supported types are mysql and sqlite.

View file

@ -14,6 +14,7 @@ This document describes the different errors Vikunja can return.
| ErrorCode | HTTP Status Code | Description | | ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------| |-----------|------------------|-------------|
| 0001 | 403 | Generic forbidden error. |
| 1001 | 400 | A user with this username already exists. | | 1001 | 400 | A user with this username already exists. |
| 1002 | 400 | A user with this email address already exists. | | 1002 | 400 | A user with this email address already exists. |
| 1004 | 400 | No username and password specified. | | 1004 | 400 | No username and password specified. |
@ -31,6 +32,7 @@ This document describes the different errors Vikunja can return.
| 3001 | 404 | The list does not exist. | | 3001 | 404 | The list does not exist. |
| 3004 | 403 | The user needs to have read permissions on that list to perform that action. | | 3004 | 403 | The user needs to have read permissions on that list to perform that action. |
| 3005 | 400 | The list title cannot be empty. | | 3005 | 400 | The list title cannot be empty. |
| 3006 | 404 | The list share does not exist. |
| 4001 | 400 | The list task text cannot be empty. | | 4001 | 400 | The list task text cannot be empty. |
| 4002 | 404 | The list task does not exist. | | 4002 | 404 | The list task does not exist. |
| 4003 | 403 | All bulk editing tasks must belong to the same list. | | 4003 | 403 | All bulk editing tasks must belong to the same list. |

View file

@ -33,14 +33,15 @@ type Key string
// These constants hold all config value keys // These constants hold all config value keys
const ( const (
ServiceJWTSecret Key = `service.JWTSecret` ServiceJWTSecret Key = `service.JWTSecret`
ServiceInterface Key = `service.interface` ServiceInterface Key = `service.interface`
ServiceFrontendurl Key = `service.frontendurl` ServiceFrontendurl Key = `service.frontendurl`
ServiceEnableCaldav Key = `service.enablecaldav` ServiceEnableCaldav Key = `service.enablecaldav`
ServiceRootpath Key = `service.rootpath` ServiceRootpath Key = `service.rootpath`
ServicePageCount Key = `service.pagecount` ServicePageCount Key = `service.pagecount`
ServiceEnableMetrics Key = `service.enablemetrics` ServiceEnableMetrics Key = `service.enablemetrics`
ServiceMotd Key = `service.motd` ServiceMotd Key = `service.motd`
ServiceEnableLinkSharing Key = `service.enablelinksharing`
DatabaseType Key = `database.type` DatabaseType Key = `database.type`
DatabaseHost Key = `database.host` DatabaseHost Key = `database.host`
@ -146,6 +147,7 @@ func InitConfig() {
ServicePageCount.setDefault(50) ServicePageCount.setDefault(50)
ServiceEnableMetrics.setDefault(false) ServiceEnableMetrics.setDefault(false)
ServiceMotd.setDefault("") ServiceMotd.setDefault("")
ServiceEnableLinkSharing.setDefault(true)
// Database // Database
DatabaseType.setDefault("sqlite") DatabaseType.setDefault("sqlite")

View file

@ -107,9 +107,9 @@ func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context)
return return
} }
func addTokenToContext(t *testing.T, user *models.User, c echo.Context) { func addUserTokenToContext(t *testing.T, user *models.User, c echo.Context) {
// Get the token as a string // Get the token as a string
token, err := v1.CreateNewJWTTokenForUser(user) token, err := v1.NewUserJWTAuthtoken(user)
assert.NoError(t, err) assert.NoError(t, err)
// We send the string token through the parsing function to get a valid jwt.Token // We send the string token through the parsing function to get a valid jwt.Token
tken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) { tken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
@ -119,8 +119,20 @@ func addTokenToContext(t *testing.T, user *models.User, c echo.Context) {
c.Set("user", tken) c.Set("user", tken)
} }
func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *models.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { func addLinkShareTokenToContext(t *testing.T, share *models.LinkSharing, c echo.Context) {
c, rec := bootstrapTestRequest(t, method, payload, queryParams) // Get the token as a string
token, err := v1.NewLinkShareJWTAuthtoken(share)
assert.NoError(t, err)
// We send the string token through the parsing function to get a valid jwt.Token
tken, err := jwt.Parse(token, func(t *jwt.Token) (interface{}, error) {
return []byte(config.ServiceJWTSecret.GetString()), nil
})
assert.NoError(t, err)
c.Set("user", tken)
}
func testRequestSetup(t *testing.T, method string, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, c echo.Context) {
c, rec = bootstrapTestRequest(t, method, payload, queryParams)
var paramNames []string var paramNames []string
var paramValues []string var paramValues []string
@ -130,8 +142,19 @@ func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFun
} }
c.SetParamNames(paramNames...) c.SetParamNames(paramNames...)
c.SetParamValues(paramValues...) c.SetParamValues(paramValues...)
return
}
addTokenToContext(t, user, c) func newTestRequestWithUser(t *testing.T, method string, handler echo.HandlerFunc, user *models.User, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
addUserTokenToContext(t, user, c)
err = handler(c)
return
}
func newTestRequestWithLinkShare(t *testing.T, method string, handler echo.HandlerFunc, share *models.LinkSharing, payload string, queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
rec, c := testRequestSetup(t, method, payload, queryParams, urlParams)
addLinkShareTokenToContext(t, share, c)
err = handler(c) err = handler(c)
return return
} }
@ -155,9 +178,10 @@ func assertHandlerErrorCode(t *testing.T, err error, expectedErrorCode int) {
} }
type webHandlerTest struct { type webHandlerTest struct {
user *models.User user *models.User
strFunc func() handler.CObject linkShare *models.LinkSharing
t *testing.T strFunc func() handler.CObject
t *testing.T
} }
func (h *webHandlerTest) getHandler() handler.WebHandler { func (h *webHandlerTest) getHandler() handler.WebHandler {
@ -168,27 +192,52 @@ func (h *webHandlerTest) getHandler() handler.WebHandler {
} }
} }
func (h *webHandlerTest) testReadAll(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { func (h *webHandlerTest) testReadAllWithUser(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
hndl := h.getHandler() hndl := h.getHandler()
return newTestRequestWithUser(h.t, http.MethodGet, hndl.ReadAllWeb, h.user, "", queryParams, urlParams) return newTestRequestWithUser(h.t, http.MethodGet, hndl.ReadAllWeb, h.user, "", queryParams, urlParams)
} }
func (h *webHandlerTest) testReadOne(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { func (h *webHandlerTest) testReadOneWithUser(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
hndl := h.getHandler() hndl := h.getHandler()
return newTestRequestWithUser(h.t, http.MethodGet, hndl.ReadOneWeb, h.user, "", queryParams, urlParams) return newTestRequestWithUser(h.t, http.MethodGet, hndl.ReadOneWeb, h.user, "", queryParams, urlParams)
} }
func (h *webHandlerTest) testCreate(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) { func (h *webHandlerTest) testCreateWithUser(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) {
hndl := h.getHandler() hndl := h.getHandler()
return newTestRequestWithUser(h.t, http.MethodPut, hndl.CreateWeb, h.user, payload, queryParams, urlParams) return newTestRequestWithUser(h.t, http.MethodPut, hndl.CreateWeb, h.user, payload, queryParams, urlParams)
} }
func (h *webHandlerTest) testUpdate(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) { func (h *webHandlerTest) testUpdateWithUser(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) {
hndl := h.getHandler() hndl := h.getHandler()
return newTestRequestWithUser(h.t, http.MethodPost, hndl.UpdateWeb, h.user, payload, queryParams, urlParams) return newTestRequestWithUser(h.t, http.MethodPost, hndl.UpdateWeb, h.user, payload, queryParams, urlParams)
} }
func (h *webHandlerTest) testDelete(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) { func (h *webHandlerTest) testDeleteWithUser(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
hndl := h.getHandler() hndl := h.getHandler()
return newTestRequestWithUser(h.t, http.MethodDelete, hndl.DeleteWeb, h.user, "", queryParams, urlParams) return newTestRequestWithUser(h.t, http.MethodDelete, hndl.DeleteWeb, h.user, "", queryParams, urlParams)
} }
func (h *webHandlerTest) testReadAllWithLinkShare(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
hndl := h.getHandler()
return newTestRequestWithLinkShare(h.t, http.MethodGet, hndl.ReadAllWeb, h.linkShare, "", queryParams, urlParams)
}
func (h *webHandlerTest) testReadOneWithLinkShare(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
hndl := h.getHandler()
return newTestRequestWithLinkShare(h.t, http.MethodGet, hndl.ReadOneWeb, h.linkShare, "", queryParams, urlParams)
}
func (h *webHandlerTest) testCreateWithLinkShare(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) {
hndl := h.getHandler()
return newTestRequestWithLinkShare(h.t, http.MethodPut, hndl.CreateWeb, h.linkShare, payload, queryParams, urlParams)
}
func (h *webHandlerTest) testUpdateWithLinkShare(queryParams url.Values, urlParams map[string]string, payload string) (rec *httptest.ResponseRecorder, err error) {
hndl := h.getHandler()
return newTestRequestWithLinkShare(h.t, http.MethodPost, hndl.UpdateWeb, h.linkShare, payload, queryParams, urlParams)
}
func (h *webHandlerTest) testDeleteWithLinkShare(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
hndl := h.getHandler()
return newTestRequestWithLinkShare(h.t, http.MethodDelete, hndl.DeleteWeb, h.linkShare, "", queryParams, urlParams)
}

View file

@ -0,0 +1,989 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package integrations
import (
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/web/handler"
"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
"net/url"
"testing"
)
func TestLinkSharing(t *testing.T) {
linkshareRead := &models.LinkSharing{
ID: 1,
Hash: "test1",
ListID: 1,
Right: models.RightRead,
SharingType: models.SharingTypeWithoutPassword,
SharedByID: 1,
}
linkShareWrite := &models.LinkSharing{
ID: 2,
Hash: "test2",
ListID: 2,
Right: models.RightWrite,
SharingType: models.SharingTypeWithoutPassword,
SharedByID: 1,
}
linkShareAdmin := &models.LinkSharing{
ID: 3,
Hash: "test3",
ListID: 3,
Right: models.RightAdmin,
SharingType: models.SharingTypeWithoutPassword,
SharedByID: 1,
}
t.Run("Lists", func(t *testing.T) {
testHandlerListReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.List{}
},
t: t,
}
testHandlerListWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.List{}
},
t: t,
}
testHandlerListAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.List{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandlerListReadOnly.testReadAllWithLinkShare(nil, nil)
assert.NoError(t, err)
// Should only return the shared list, nothing else
assert.Contains(t, rec.Body.String(), `Test1`)
assert.NotContains(t, rec.Body.String(), `Test2`)
assert.NotContains(t, rec.Body.String(), `Test3`)
assert.NotContains(t, rec.Body.String(), `Test4`)
assert.NotContains(t, rec.Body.String(), `Test5`)
})
t.Run("Search", func(t *testing.T) {
rec, err := testHandlerListReadOnly.testReadAllWithLinkShare(url.Values{"s": []string{"est1"}}, nil)
assert.NoError(t, err)
// Should only return the shared list, nothing else
assert.Contains(t, rec.Body.String(), `Test1`)
assert.NotContains(t, rec.Body.String(), `Test2`)
assert.NotContains(t, rec.Body.String(), `Test3`)
assert.NotContains(t, rec.Body.String(), `Test4`)
assert.NotContains(t, rec.Body.String(), `Test5`)
})
})
t.Run("ReadOne", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test1"`)
assert.NotContains(t, rec.Body.String(), `"title":"Test2"`)
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "9999999"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// List 2, not shared with this token
_, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `You don't have the right to see this`)
})
t.Run("Shared readonly", func(t *testing.T) {
rec, err := testHandlerListReadOnly.testReadOneWithLinkShare(nil, map[string]string{"list": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test1"`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerListWrite.testReadOneWithLinkShare(nil, map[string]string{"list": "2"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test2"`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerListAdmin.testReadOneWithLinkShare(nil, map[string]string{"list": "3"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test3"`)
})
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "9999999"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
_, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerListWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerListAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
})
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "9999999"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
_, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerListAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
})
})
})
// Creating a list should always be forbidden, since users need access to a namespace to create a list
t.Run("Create", func(t *testing.T) {
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandlerListReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListWrite.testCreateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListAdmin.testCreateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
})
t.Run("Right Management", func(t *testing.T) {
t.Run("Users", func(t *testing.T) {
testHandlerListUserReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.ListUser{}
},
t: t,
}
testHandlerListUserWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.ListUser{}
},
t: t,
}
testHandlerListUserAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.ListUser{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
rec, err := testHandlerListUserReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[]`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerListUserWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[]`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerListUserAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"username":"user1"`)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListUserReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListUserWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListUserAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListUserReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListUserWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListUserAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListUserReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListUserWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListUserAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
})
t.Run("Teams", func(t *testing.T) {
testHandlerListTeamReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.TeamList{}
},
t: t,
}
testHandlerListTeamWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.TeamList{}
},
t: t,
}
testHandlerListTeamAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.TeamList{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
rec, err := testHandlerListTeamReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[]`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerListTeamWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[]`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerListTeamAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"name":"testteam1"`)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListTeamReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListTeamWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListTeamAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"list": "1"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"list": "2"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"list": "3"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerListTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"list": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerListTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"list": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerListTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"list": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
})
})
})
t.Run("Tasks", func(t *testing.T) {
testHandlerTaskReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.Task{}
},
t: t,
}
testHandlerTaskWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.Task{}
},
t: t,
}
testHandlerTaskAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.Task{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
rec, err := testHandlerTaskReadOnly.testReadAllWithLinkShare(nil, nil)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `task #1`)
assert.Contains(t, rec.Body.String(), `task #2`)
assert.Contains(t, rec.Body.String(), `task #3`)
assert.Contains(t, rec.Body.String(), `task #4`)
assert.Contains(t, rec.Body.String(), `task #5`)
assert.Contains(t, rec.Body.String(), `task #6`)
assert.Contains(t, rec.Body.String(), `task #7`)
assert.Contains(t, rec.Body.String(), `task #8`)
assert.Contains(t, rec.Body.String(), `task #9`)
assert.Contains(t, rec.Body.String(), `task #10`)
assert.Contains(t, rec.Body.String(), `task #11`)
assert.Contains(t, rec.Body.String(), `task #12`)
assert.NotContains(t, rec.Body.String(), `task #13`)
assert.NotContains(t, rec.Body.String(), `task #14`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerTaskWrite.testReadAllWithLinkShare(nil, nil)
assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `task #2`)
assert.NotContains(t, rec.Body.String(), `task #3`)
assert.NotContains(t, rec.Body.String(), `task #4`)
assert.NotContains(t, rec.Body.String(), `task #5`)
assert.NotContains(t, rec.Body.String(), `task #6`)
assert.NotContains(t, rec.Body.String(), `task #7`)
assert.NotContains(t, rec.Body.String(), `task #8`)
assert.NotContains(t, rec.Body.String(), `task #9`)
assert.NotContains(t, rec.Body.String(), `task #10`)
assert.NotContains(t, rec.Body.String(), `task #11`)
assert.NotContains(t, rec.Body.String(), `task #12`)
assert.Contains(t, rec.Body.String(), `task #13`)
assert.NotContains(t, rec.Body.String(), `task #14`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerTaskAdmin.testReadAllWithLinkShare(nil, nil)
assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `task #2`)
assert.NotContains(t, rec.Body.String(), `task #4`)
assert.NotContains(t, rec.Body.String(), `task #5`)
assert.NotContains(t, rec.Body.String(), `task #6`)
assert.NotContains(t, rec.Body.String(), `task #7`)
assert.NotContains(t, rec.Body.String(), `task #8`)
assert.NotContains(t, rec.Body.String(), `task #9`)
assert.NotContains(t, rec.Body.String(), `task #10`)
assert.NotContains(t, rec.Body.String(), `task #11`)
assert.NotContains(t, rec.Body.String(), `task #12`)
assert.NotContains(t, rec.Body.String(), `task #13`)
assert.NotContains(t, rec.Body.String(), `task #14`)
assert.Contains(t, rec.Body.String(), `task #32`)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerTaskReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerTaskWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerTaskAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerTaskReadOnly.testUpdateWithLinkShare(nil, map[string]string{"listtask": "1"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerTaskWrite.testUpdateWithLinkShare(nil, map[string]string{"listtask": "13"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerTaskAdmin.testUpdateWithLinkShare(nil, map[string]string{"listtask": "32"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerTaskReadOnly.testDeleteWithLinkShare(nil, map[string]string{"listtask": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerTaskWrite.testDeleteWithLinkShare(nil, map[string]string{"listtask": "13"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerTaskAdmin.testDeleteWithLinkShare(nil, map[string]string{"listtask": "32"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
})
})
})
t.Run("Teams", func(t *testing.T) {
testHandlerTeamReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.Team{}
},
t: t,
}
testHandlerTeamWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.Team{}
},
t: t,
}
testHandlerTeamAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.Team{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerTeamReadOnly.testReadAllWithLinkShare(nil, nil)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerTeamWrite.testReadAllWithLinkShare(nil, nil)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerTeamAdmin.testReadAllWithLinkShare(nil, nil)
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"team": "1"}, `{"name":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"team": "2"}, `{"name":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"team": "3"}, `{"name":"Lorem Ipsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"team": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"team": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"team": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
})
t.Run("Linkshare Management", func(t *testing.T) {
testHandlerLinkShareReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.LinkSharing{}
},
t: t,
}
testHandlerLinkShareWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.LinkSharing{}
},
t: t,
}
testHandlerLinkShareAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.LinkSharing{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
rec, err := testHandlerLinkShareReadOnly.testReadAllWithLinkShare(nil, map[string]string{"list": "1"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"hash":"test"`)
})
t.Run("Shared write", func(t *testing.T) {
rec, err := testHandlerLinkShareWrite.testReadAllWithLinkShare(nil, map[string]string{"list": "2"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"hash":"test2"`)
})
t.Run("Shared admin", func(t *testing.T) {
rec, err := testHandlerLinkShareAdmin.testReadAllWithLinkShare(nil, map[string]string{"list": "3"})
assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"hash":"test3"`)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerLinkShareReadOnly.testCreateWithLinkShare(nil, map[string]string{"list": "1"}, `{}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerLinkShareWrite.testCreateWithLinkShare(nil, map[string]string{"list": "2"}, `{}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerLinkShareAdmin.testCreateWithLinkShare(nil, map[string]string{"list": "3"}, `{}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerLinkShareReadOnly.testUpdateWithLinkShare(nil, map[string]string{"share": "1"}, `{}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerLinkShareWrite.testUpdateWithLinkShare(nil, map[string]string{"share": "2"}, `{}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerLinkShareAdmin.testUpdateWithLinkShare(nil, map[string]string{"share": "3"}, `{}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerLinkShareReadOnly.testDeleteWithLinkShare(nil, map[string]string{"share": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerLinkShareWrite.testDeleteWithLinkShare(nil, map[string]string{"share": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerLinkShareAdmin.testDeleteWithLinkShare(nil, map[string]string{"share": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
})
t.Run("Namespace", func(t *testing.T) {
testHandlerNamespaceReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.Namespace{}
},
t: t,
}
testHandlerNamespaceWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.Namespace{}
},
t: t,
}
testHandlerNamespaceAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.Namespace{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrorCodeGenericForbidden)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceReadOnly.testCreateWithLinkShare(nil, nil, `{"name":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceWrite.testCreateWithLinkShare(nil, nil, `{"name":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceAdmin.testCreateWithLinkShare(nil, nil, `{"name":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"name":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"name":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"name":"LoremIpsum"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Right Management", func(t *testing.T) {
t.Run("Users", func(t *testing.T) {
testHandlerNamespaceUserReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.NamespaceUser{}
},
t: t,
}
testHandlerNamespaceUserWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.NamespaceUser{}
},
t: t,
}
testHandlerNamespaceUserAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.NamespaceUser{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceUserReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceUserWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceUserAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceUserReadOnly.testCreateWithLinkShare(nil, nil, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceUserWrite.testCreateWithLinkShare(nil, nil, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceUserAdmin.testCreateWithLinkShare(nil, nil, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceUserReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceUserWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceUserAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"userID":"user1"}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceUserReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceUserWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceUserAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
})
t.Run("Teams", func(t *testing.T) {
testHandlerNamespaceTeamReadOnly := webHandlerTest{
linkShare: linkshareRead,
strFunc: func() handler.CObject {
return &models.TeamNamespace{}
},
t: t,
}
testHandlerNamespaceTeamWrite := webHandlerTest{
linkShare: linkShareWrite,
strFunc: func() handler.CObject {
return &models.TeamNamespace{}
},
t: t,
}
testHandlerNamespaceTeamAdmin := webHandlerTest{
linkShare: linkShareAdmin,
strFunc: func() handler.CObject {
return &models.TeamNamespace{}
},
t: t,
}
t.Run("ReadAll", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceTeamReadOnly.testReadAllWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceTeamWrite.testReadAllWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceTeamAdmin.testReadAllWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNeedToHaveNamespaceReadAccess)
})
})
t.Run("Create", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceTeamReadOnly.testCreateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceTeamWrite.testCreateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceTeamAdmin.testCreateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Update", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceTeamReadOnly.testUpdateWithLinkShare(nil, map[string]string{"namespace": "1"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceTeamWrite.testUpdateWithLinkShare(nil, map[string]string{"namespace": "2"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceTeamAdmin.testUpdateWithLinkShare(nil, map[string]string{"namespace": "3"}, `{"teamID":1}`)
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
t.Run("Delete", func(t *testing.T) {
t.Run("Shared readonly", func(t *testing.T) {
_, err := testHandlerNamespaceTeamReadOnly.testDeleteWithLinkShare(nil, map[string]string{"namespace": "1"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared write", func(t *testing.T) {
_, err := testHandlerNamespaceTeamWrite.testDeleteWithLinkShare(nil, map[string]string{"namespace": "2"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared admin", func(t *testing.T) {
_, err := testHandlerNamespaceTeamAdmin.testDeleteWithLinkShare(nil, map[string]string{"namespace": "3"})
assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
})
})
})
})
}

View file

@ -35,7 +35,7 @@ func TestList(t *testing.T) {
} }
t.Run("ReadAll", func(t *testing.T) { t.Run("ReadAll", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testReadAll(nil, nil) rec, err := testHandler.testReadAllWithUser(nil, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Test1`) assert.Contains(t, rec.Body.String(), `Test1`)
assert.NotContains(t, rec.Body.String(), `Test2`) assert.NotContains(t, rec.Body.String(), `Test2`)
@ -44,7 +44,7 @@ func TestList(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `Test5`) assert.NotContains(t, rec.Body.String(), `Test5`)
}) })
t.Run("Search", func(t *testing.T) { t.Run("Search", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"s": []string{"Test1"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"Test1"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Test1`) assert.Contains(t, rec.Body.String(), `Test1`)
assert.NotContains(t, rec.Body.String(), `Test2`) assert.NotContains(t, rec.Body.String(), `Test2`)
@ -55,7 +55,7 @@ func TestList(t *testing.T) {
}) })
t.Run("ReadOne", func(t *testing.T) { t.Run("ReadOne", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "1"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "1"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test1"`) assert.Contains(t, rec.Body.String(), `"title":"Test1"`)
assert.NotContains(t, rec.Body.String(), `"title":"Test2"`) assert.NotContains(t, rec.Body.String(), `"title":"Test2"`)
@ -64,77 +64,77 @@ func TestList(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"tasks":[{"id":1,"text":"task #1",`) assert.Contains(t, rec.Body.String(), `"tasks":[{"id":1,"text":"task #1",`)
}) })
t.Run("Nonexisting", func(t *testing.T) { t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testReadOne(nil, map[string]string{"list": "9999"}) _, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "9999"})
assert.Error(t, err) assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
}) })
t.Run("Rights check", func(t *testing.T) { t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
// Owned by user13 // Owned by user13
_, err := testHandler.testReadOne(nil, map[string]string{"list": "20"}) _, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "20"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `You don't have the right to see this`) assert.Contains(t, err.(*echo.HTTPError).Message, `You don't have the right to see this`)
}) })
t.Run("Shared Via Team readonly", func(t *testing.T) { t.Run("Shared Via Team readonly", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "6"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "6"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test6"`) assert.Contains(t, rec.Body.String(), `"title":"Test6"`)
}) })
t.Run("Shared Via Team write", func(t *testing.T) { t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "7"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "7"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test7"`) assert.Contains(t, rec.Body.String(), `"title":"Test7"`)
}) })
t.Run("Shared Via Team admin", func(t *testing.T) { t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "8"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "8"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test8"`) assert.Contains(t, rec.Body.String(), `"title":"Test8"`)
}) })
t.Run("Shared Via User readonly", func(t *testing.T) { t.Run("Shared Via User readonly", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "9"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "9"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test9"`) assert.Contains(t, rec.Body.String(), `"title":"Test9"`)
}) })
t.Run("Shared Via User write", func(t *testing.T) { t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "10"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "10"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test10"`) assert.Contains(t, rec.Body.String(), `"title":"Test10"`)
}) })
t.Run("Shared Via User admin", func(t *testing.T) { t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "11"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "11"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test11"`) assert.Contains(t, rec.Body.String(), `"title":"Test11"`)
}) })
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "12"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "12"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test12"`) assert.Contains(t, rec.Body.String(), `"title":"Test12"`)
}) })
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "13"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "13"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test13"`) assert.Contains(t, rec.Body.String(), `"title":"Test13"`)
}) })
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "14"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "14"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test14"`) assert.Contains(t, rec.Body.String(), `"title":"Test14"`)
}) })
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "15"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "15"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test15"`) assert.Contains(t, rec.Body.String(), `"title":"Test15"`)
}) })
t.Run("Shared Via NamespaceUser write", func(t *testing.T) { t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "16"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "16"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test16"`) assert.Contains(t, rec.Body.String(), `"title":"Test16"`)
}) })
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testReadOne(nil, map[string]string{"list": "17"}) rec, err := testHandler.testReadOneWithUser(nil, map[string]string{"list": "17"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Test17"`) assert.Contains(t, rec.Body.String(), `"title":"Test17"`)
}) })
@ -142,106 +142,106 @@ func TestList(t *testing.T) {
}) })
t.Run("Update", func(t *testing.T) { t.Run("Update", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
// Check the list was loaded successfully afterwards, see testReadOne // Check the list was loaded successfully afterwards, see testReadOneWithUser
rec, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
// The description should not be updated but returned correctly // The description should not be updated but returned correctly
assert.Contains(t, rec.Body.String(), `description":"Lorem Ipsum`) assert.Contains(t, rec.Body.String(), `description":"Lorem Ipsum`)
}) })
t.Run("Nonexisting", func(t *testing.T) { t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"list": "9999"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "9999"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err) assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
}) })
t.Run("Normal with updating the description", func(t *testing.T) { t.Run("Normal with updating the description", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum","description":"Lorem Ipsum dolor sit amet"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`) assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`)
}) })
t.Run("Empty title", func(t *testing.T) { t.Run("Empty title", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":""}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":""}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required") assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
}) })
t.Run("Almost empty title", func(t *testing.T) { t.Run("Almost empty title", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":"nn"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"nn"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)") assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)")
}) })
t.Run("Title too long", func(t *testing.T) { t.Run("Title too long", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)") assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)")
}) })
t.Run("Rights check", func(t *testing.T) { t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
// Owned by user13 // Owned by user13
_, err := testHandler.testUpdate(nil, map[string]string{"list": "20"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "20"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team readonly", func(t *testing.T) { t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"list": "6"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "6"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team write", func(t *testing.T) { t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via Team admin", func(t *testing.T) { t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via User readonly", func(t *testing.T) { t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"list": "9"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "9"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User write", func(t *testing.T) { t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via User admin", func(t *testing.T) { t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"list": "12"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "12"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"list": "13"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "13"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"list": "15"}, `{"title":"TestLoremIpsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "15"}, `{"title":"TestLoremIpsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser write", func(t *testing.T) { t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"list": "16"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "16"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`) assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
}) })
@ -249,82 +249,82 @@ func TestList(t *testing.T) {
}) })
t.Run("Delete", func(t *testing.T) { t.Run("Delete", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"list": "1"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "1"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
}) })
t.Run("Nonexisting", func(t *testing.T) { t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"list": "999"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "999"})
assert.Error(t, err) assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
}) })
t.Run("Rights check", func(t *testing.T) { t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
// Owned by user13 // Owned by user13
_, err := testHandler.testDelete(nil, map[string]string{"list": "20"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "20"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team readonly", func(t *testing.T) { t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"list": "6"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "6"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team write", func(t *testing.T) { t.Run("Shared Via Team write", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"list": "7"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "7"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team admin", func(t *testing.T) { t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"list": "8"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "8"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
}) })
t.Run("Shared Via User readonly", func(t *testing.T) { t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"list": "9"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "9"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User write", func(t *testing.T) { t.Run("Shared Via User write", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"list": "10"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "10"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User admin", func(t *testing.T) { t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"list": "11"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "11"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
}) })
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"list": "12"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "12"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"list": "13"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "13"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"list": "14"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "14"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
}) })
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"list": "15"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "15"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser write", func(t *testing.T) { t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"list": "16"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "16"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"list": "17"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"list": "17"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`) assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
}) })
@ -332,8 +332,8 @@ func TestList(t *testing.T) {
}) })
t.Run("Create", func(t *testing.T) { t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
// Check the list was loaded successfully after update, see testReadOne // Check the list was loaded successfully after update, see testReadOneWithUser
rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":""`) assert.Contains(t, rec.Body.String(), `"description":""`)
@ -341,7 +341,7 @@ func TestList(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"tasks":null`) assert.Contains(t, rec.Body.String(), `"tasks":null`)
}) })
t.Run("Normal with description", func(t *testing.T) { t.Run("Normal with description", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
@ -349,22 +349,22 @@ func TestList(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"tasks":null`) assert.Contains(t, rec.Body.String(), `"tasks":null`)
}) })
t.Run("Nonexisting Namespace", func(t *testing.T) { t.Run("Nonexisting Namespace", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`)
assert.Error(t, err) assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeNamespaceDoesNotExist) assertHandlerErrorCode(t, err, models.ErrCodeNamespaceDoesNotExist)
}) })
t.Run("Empty title", func(t *testing.T) { t.Run("Empty title", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":""}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":""}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required") assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
}) })
t.Run("Almost empty title", func(t *testing.T) { t.Run("Almost empty title", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"nn"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"nn"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)") assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)")
}) })
t.Run("Title too long", func(t *testing.T) { t.Run("Title too long", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea taki"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)") assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields[0], "does not validate as runelength(3|250)")
}) })
@ -372,18 +372,18 @@ func TestList(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
// Owned by user13 // Owned by user13
_, err := testHandler.testCreate(nil, map[string]string{"namespace": "15"}, `{"title":"Lorem"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "15"}, `{"title":"Lorem"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"namespace": "7"}, `{"title":"Lorem"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "7"}, `{"title":"Lorem"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "8"}, `{"title":"Lorem"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "8"}, `{"title":"Lorem"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":""`) assert.Contains(t, rec.Body.String(), `"description":""`)
@ -391,7 +391,7 @@ func TestList(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"tasks":null`) assert.Contains(t, rec.Body.String(), `"tasks":null`)
}) })
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "9"}, `{"title":"Lorem"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "9"}, `{"title":"Lorem"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":""`) assert.Contains(t, rec.Body.String(), `"description":""`)
@ -400,12 +400,12 @@ func TestList(t *testing.T) {
}) })
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"namespace": "10"}, `{"title":"Lorem"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "10"}, `{"title":"Lorem"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser write", func(t *testing.T) { t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "11"}, `{"title":"Lorem"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "11"}, `{"title":"Lorem"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":""`) assert.Contains(t, rec.Body.String(), `"description":""`)
@ -413,7 +413,7 @@ func TestList(t *testing.T) {
assert.Contains(t, rec.Body.String(), `"tasks":null`) assert.Contains(t, rec.Body.String(), `"tasks":null`)
}) })
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "12"}, `{"title":"Lorem"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"namespace": "12"}, `{"title":"Lorem"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem"`) assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
assert.Contains(t, rec.Body.String(), `"description":""`) assert.Contains(t, rec.Body.String(), `"description":""`)

View file

@ -37,7 +37,7 @@ func TestTask(t *testing.T) {
// ^TestTask$/^Update$/^Update_task_items$/^Removing_Assignees_null$ // ^TestTask$/^Update$/^Update_task_items$/^Removing_Assignees_null$
t.Run("ReadAll", func(t *testing.T) { t.Run("ReadAll", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testReadAll(nil, nil) rec, err := testHandler.testReadAllWithUser(nil, nil)
assert.NoError(t, err) assert.NoError(t, err)
// Not using assert.Equal to avoid having the tests break every time we add new fixtures // Not using assert.Equal to avoid having the tests break every time we add new fixtures
assert.Contains(t, rec.Body.String(), `task #1`) assert.Contains(t, rec.Body.String(), `task #1`)
@ -58,7 +58,7 @@ func TestTask(t *testing.T) {
// has at least read access // has at least read access
}) })
t.Run("Search", func(t *testing.T) { t.Run("Search", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"s": []string{"task #6"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"s": []string{"task #6"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `task #1`) assert.NotContains(t, rec.Body.String(), `task #1`)
assert.NotContains(t, rec.Body.String(), `task #2`) assert.NotContains(t, rec.Body.String(), `task #2`)
@ -78,39 +78,39 @@ func TestTask(t *testing.T) {
t.Run("Sort Order", func(t *testing.T) { t.Run("Sort Order", func(t *testing.T) {
// should equal priority desc // should equal priority desc
t.Run("by priority", func(t *testing.T) { t.Run("by priority", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priority"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"priority"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`) assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`)
}) })
t.Run("by priority desc", func(t *testing.T) { t.Run("by priority desc", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"prioritydesc"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"prioritydesc"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`) assert.Contains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1`)
}) })
t.Run("by priority asc", func(t *testing.T) { t.Run("by priority asc", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priorityasc"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"priorityasc"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":31,"text":"task #31 with color","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"f0f0f0","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) assert.Contains(t, rec.Body.String(), `{"id":32,"text":"task #32","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":3,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"doneAt":0,"dueDate":0,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`)
}) })
// should equal duedate desc // should equal duedate desc
t.Run("by duedate", func(t *testing.T) { t.Run("by duedate", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedate"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"duedate"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
}) })
t.Run("by duedate desc", func(t *testing.T) { t.Run("by duedate desc", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedatedesc"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"duedatedesc"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`) assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
}) })
t.Run("by duedate asc", func(t *testing.T) { t.Run("by duedate asc", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedateasc"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"duedateasc"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`) assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"doneAt":0,"dueDate":1543616724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"doneAt":0,"dueDate":1543636724,"reminderDates":null,"listID":1,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":1,"username":"user1","avatarUrl":"111d68d06e2d317b5a59c2c6c5bad808","created":0,"updated":0}}]`)
}) })
t.Run("invalid parameter", func(t *testing.T) { t.Run("invalid parameter", func(t *testing.T) {
// Invalid parameter should not sort at all // Invalid parameter should not sort at all
rec, err := testHandler.testReadAll(url.Values{"sort": []string{"loremipsum"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"sort": []string{"loremipsum"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":1`) assert.NotContains(t, rec.Body.String(), `[{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":1`)
assert.NotContains(t, rec.Body.String(), `{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}}]`) assert.NotContains(t, rec.Body.String(), `{"id":4,"text":"task #4 low prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":1,"startDate":0,"endDate":0,"assignees":null,"labels":null,"hexColor":"","subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":3,"text":"task #3 high prio","description":"","done":false,"dueDate":0,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":100,"startDate":0,"endDate":0,"assignees":null,"labels":null,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
@ -120,7 +120,7 @@ func TestTask(t *testing.T) {
}) })
t.Run("Date range", func(t *testing.T) { t.Run("Date range", func(t *testing.T) {
t.Run("start and end date", func(t *testing.T) { t.Run("start and end date", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"startdate": []string{"1540000000"}, "enddate": []string{"1544700001"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"startdate": []string{"1540000000"}, "enddate": []string{"1544700001"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `task #1`) assert.NotContains(t, rec.Body.String(), `task #1`)
assert.NotContains(t, rec.Body.String(), `task #2`) assert.NotContains(t, rec.Body.String(), `task #2`)
@ -138,7 +138,7 @@ func TestTask(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `task #14`) assert.NotContains(t, rec.Body.String(), `task #14`)
}) })
t.Run("start date only", func(t *testing.T) { t.Run("start date only", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"startdate": []string{"1540000000"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"startdate": []string{"1540000000"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotContains(t, rec.Body.String(), `task #1`) assert.NotContains(t, rec.Body.String(), `task #1`)
assert.NotContains(t, rec.Body.String(), `task #2`) assert.NotContains(t, rec.Body.String(), `task #2`)
@ -156,7 +156,7 @@ func TestTask(t *testing.T) {
assert.NotContains(t, rec.Body.String(), `task #14`) assert.NotContains(t, rec.Body.String(), `task #14`)
}) })
t.Run("end date only", func(t *testing.T) { t.Run("end date only", func(t *testing.T) {
rec, err := testHandler.testReadAll(url.Values{"enddate": []string{"1544700001"}}, nil) rec, err := testHandler.testReadAllWithUser(url.Values{"enddate": []string{"1544700001"}}, nil)
assert.NoError(t, err) assert.NoError(t, err)
// If no start date but an end date is specified, this should be null // If no start date but an end date is specified, this should be null
// since we don't have any tasks in the fixtures with an end date > // since we don't have any tasks in the fixtures with an end date >
@ -168,162 +168,162 @@ func TestTask(t *testing.T) {
t.Run("Update", func(t *testing.T) { t.Run("Update", func(t *testing.T) {
t.Run("Update task items", func(t *testing.T) { t.Run("Update task items", func(t *testing.T) {
t.Run("Text", func(t *testing.T) { t.Run("Text", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
assert.NotContains(t, rec.Body.String(), `"text":"task #1"`) assert.NotContains(t, rec.Body.String(), `"text":"task #1"`)
}) })
t.Run("Description", func(t *testing.T) { t.Run("Description", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"description":"Dolor sit amet"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"description":"Dolor sit amet"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"description":"Dolor sit amet"`) assert.Contains(t, rec.Body.String(), `"description":"Dolor sit amet"`)
assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`) assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
}) })
t.Run("Description to empty", func(t *testing.T) { t.Run("Description to empty", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"description":""}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"description":""}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"description":""`) assert.Contains(t, rec.Body.String(), `"description":""`)
assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`) assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
}) })
t.Run("Done", func(t *testing.T) { t.Run("Done", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"done":true}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"done":true}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"done":true`) assert.Contains(t, rec.Body.String(), `"done":true`)
assert.NotContains(t, rec.Body.String(), `"done":false`) assert.NotContains(t, rec.Body.String(), `"done":false`)
}) })
t.Run("Undone", func(t *testing.T) { t.Run("Undone", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "2"}, `{"done":false}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "2"}, `{"done":false}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"done":false`) assert.Contains(t, rec.Body.String(), `"done":false`)
assert.NotContains(t, rec.Body.String(), `"done":true`) assert.NotContains(t, rec.Body.String(), `"done":true`)
}) })
t.Run("Due date", func(t *testing.T) { t.Run("Due date", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"dueDate": 123456}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"dueDate": 123456}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"dueDate":123456`) assert.Contains(t, rec.Body.String(), `"dueDate":123456`)
assert.NotContains(t, rec.Body.String(), `"dueDate":0`) assert.NotContains(t, rec.Body.String(), `"dueDate":0`)
}) })
t.Run("Due date unset", func(t *testing.T) { t.Run("Due date unset", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "5"}, `{"dueDate": 0}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "5"}, `{"dueDate": 0}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"dueDate":0`) assert.Contains(t, rec.Body.String(), `"dueDate":0`)
assert.NotContains(t, rec.Body.String(), `"dueDate":1543636724`) assert.NotContains(t, rec.Body.String(), `"dueDate":1543636724`)
}) })
t.Run("Reminders", func(t *testing.T) { t.Run("Reminders", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"reminderDates": [1555508227,1555511000]}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"reminderDates": [1555508227,1555511000]}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminderDates":[1555508227,1555511000]`) assert.Contains(t, rec.Body.String(), `"reminderDates":[1555508227,1555511000]`)
assert.NotContains(t, rec.Body.String(), `"reminderDates": null`) assert.NotContains(t, rec.Body.String(), `"reminderDates": null`)
}) })
t.Run("Reminders unset to empty array", func(t *testing.T) { t.Run("Reminders unset to empty array", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "27"}, `{"reminderDates": []}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminderDates": []}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminderDates":null`) assert.Contains(t, rec.Body.String(), `"reminderDates":null`)
assert.NotContains(t, rec.Body.String(), `"reminderDates":[1543626724,1543626824]`) assert.NotContains(t, rec.Body.String(), `"reminderDates":[1543626724,1543626824]`)
}) })
t.Run("Reminders unset to null", func(t *testing.T) { t.Run("Reminders unset to null", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "27"}, `{"reminderDates": null}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "27"}, `{"reminderDates": null}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"reminderDates":null`) assert.Contains(t, rec.Body.String(), `"reminderDates":null`)
assert.NotContains(t, rec.Body.String(), `"reminderDates":[1543626724,1543626824]`) assert.NotContains(t, rec.Body.String(), `"reminderDates":[1543626724,1543626824]`)
}) })
t.Run("Repeat after", func(t *testing.T) { t.Run("Repeat after", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"repeatAfter":3600}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"repeatAfter":3600}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"repeatAfter":3600`) assert.Contains(t, rec.Body.String(), `"repeatAfter":3600`)
assert.NotContains(t, rec.Body.String(), `"repeatAfter":0`) assert.NotContains(t, rec.Body.String(), `"repeatAfter":0`)
}) })
t.Run("Repeat after unset", func(t *testing.T) { t.Run("Repeat after unset", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "28"}, `{"repeatAfter":0}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "28"}, `{"repeatAfter":0}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"repeatAfter":0`) assert.Contains(t, rec.Body.String(), `"repeatAfter":0`)
assert.NotContains(t, rec.Body.String(), `"repeatAfter":3600`) assert.NotContains(t, rec.Body.String(), `"repeatAfter":3600`)
}) })
t.Run("Repeat after update done", func(t *testing.T) { t.Run("Repeat after update done", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "28"}, `{"done":true}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "28"}, `{"done":true}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"done":false`) assert.Contains(t, rec.Body.String(), `"done":false`)
assert.NotContains(t, rec.Body.String(), `"done":true`) assert.NotContains(t, rec.Body.String(), `"done":true`)
}) })
t.Run("Parent task", func(t *testing.T) { t.Run("Parent task", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"parentTaskID":2}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"parentTaskID":2}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"parentTaskID":2`) assert.Contains(t, rec.Body.String(), `"parentTaskID":2`)
assert.NotContains(t, rec.Body.String(), `"parentTaskID":0`) assert.NotContains(t, rec.Body.String(), `"parentTaskID":0`)
}) })
t.Run("Parent task same task", func(t *testing.T) { t.Run("Parent task same task", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"parentTaskID":1}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"parentTaskID":1}`)
assert.Error(t, err) assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeParentTaskCannotBeTheSame) assertHandlerErrorCode(t, err, models.ErrCodeParentTaskCannotBeTheSame)
}) })
t.Run("Parent task unset", func(t *testing.T) { t.Run("Parent task unset", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "29"}, `{"parentTaskID":0}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "29"}, `{"parentTaskID":0}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"parentTaskID":0`) assert.Contains(t, rec.Body.String(), `"parentTaskID":0`)
assert.NotContains(t, rec.Body.String(), `"parentTaskID":1`) assert.NotContains(t, rec.Body.String(), `"parentTaskID":1`)
}) })
t.Run("Assignees", func(t *testing.T) { t.Run("Assignees", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"assignees":[{"id":1}]}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"assignees":[{"id":1}]}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"assignees":[{"id":1`) assert.Contains(t, rec.Body.String(), `"assignees":[{"id":1`)
assert.NotContains(t, rec.Body.String(), `"assignees":[]`) assert.NotContains(t, rec.Body.String(), `"assignees":[]`)
}) })
t.Run("Removing Assignees empty array", func(t *testing.T) { t.Run("Removing Assignees empty array", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "30"}, `{"assignees":[]}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "30"}, `{"assignees":[]}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"assignees":null`) assert.Contains(t, rec.Body.String(), `"assignees":null`)
assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`) assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`)
}) })
t.Run("Removing Assignees null", func(t *testing.T) { t.Run("Removing Assignees null", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "30"}, `{"assignees":null}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "30"}, `{"assignees":null}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"assignees":null`) assert.Contains(t, rec.Body.String(), `"assignees":null`)
assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`) assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`)
}) })
t.Run("Priority", func(t *testing.T) { t.Run("Priority", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"priority":100}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"priority":100}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"priority":100`) assert.Contains(t, rec.Body.String(), `"priority":100`)
assert.NotContains(t, rec.Body.String(), `"priority":0`) assert.NotContains(t, rec.Body.String(), `"priority":0`)
}) })
t.Run("Priority to 0", func(t *testing.T) { t.Run("Priority to 0", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "3"}, `{"priority":0}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "3"}, `{"priority":0}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"priority":0`) assert.Contains(t, rec.Body.String(), `"priority":0`)
assert.NotContains(t, rec.Body.String(), `"priority":100`) assert.NotContains(t, rec.Body.String(), `"priority":100`)
}) })
t.Run("Start date", func(t *testing.T) { t.Run("Start date", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"startDate":1234567}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"startDate":1234567}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"startDate":1234567`) assert.Contains(t, rec.Body.String(), `"startDate":1234567`)
assert.NotContains(t, rec.Body.String(), `"startDate":0`) assert.NotContains(t, rec.Body.String(), `"startDate":0`)
}) })
t.Run("Start date unset", func(t *testing.T) { t.Run("Start date unset", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "7"}, `{"startDate":0}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "7"}, `{"startDate":0}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"startDate":0`) assert.Contains(t, rec.Body.String(), `"startDate":0`)
assert.NotContains(t, rec.Body.String(), `"startDate":1544600000`) assert.NotContains(t, rec.Body.String(), `"startDate":1544600000`)
}) })
t.Run("End date", func(t *testing.T) { t.Run("End date", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"endDate":123456}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"endDate":123456}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"endDate":123456`) assert.Contains(t, rec.Body.String(), `"endDate":123456`)
assert.NotContains(t, rec.Body.String(), `"endDate":0`) assert.NotContains(t, rec.Body.String(), `"endDate":0`)
}) })
t.Run("End date unset", func(t *testing.T) { t.Run("End date unset", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "8"}, `{"endDate":0}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "8"}, `{"endDate":0}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"endDate":0`) assert.Contains(t, rec.Body.String(), `"endDate":0`)
assert.NotContains(t, rec.Body.String(), `"endDate":1544700000`) assert.NotContains(t, rec.Body.String(), `"endDate":1544700000`)
}) })
t.Run("Color", func(t *testing.T) { t.Run("Color", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"hexColor":"f0f0f0"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "1"}, `{"hexColor":"f0f0f0"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"hexColor":"f0f0f0"`) assert.Contains(t, rec.Body.String(), `"hexColor":"f0f0f0"`)
assert.NotContains(t, rec.Body.String(), `"hexColor":""`) assert.NotContains(t, rec.Body.String(), `"hexColor":""`)
}) })
t.Run("Color unset", func(t *testing.T) { t.Run("Color unset", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "31"}, `{"hexColor":""}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "31"}, `{"hexColor":""}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"hexColor":""`) assert.Contains(t, rec.Body.String(), `"hexColor":""`)
assert.NotContains(t, rec.Body.String(), `"hexColor":"f0f0f0"`) assert.NotContains(t, rec.Body.String(), `"hexColor":"f0f0f0"`)
@ -331,76 +331,76 @@ func TestTask(t *testing.T) {
}) })
t.Run("Nonexisting", func(t *testing.T) { t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"listtask": "99999"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "99999"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist) assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
}) })
t.Run("Rights check", func(t *testing.T) { t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"listtask": "14"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "14"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team readonly", func(t *testing.T) { t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"listtask": "15"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "15"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team write", func(t *testing.T) { t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "16"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "16"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via Team admin", func(t *testing.T) { t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "17"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "17"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via User readonly", func(t *testing.T) { t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"listtask": "18"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "18"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User write", func(t *testing.T) { t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "19"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "19"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via User admin", func(t *testing.T) { t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "20"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "20"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"listtask": "21"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "21"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "22"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "22"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "23"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "23"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testUpdate(nil, map[string]string{"listtask": "24"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "24"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser write", func(t *testing.T) { t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "25"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "25"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "26"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testUpdateWithUser(nil, map[string]string{"listtask": "26"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
@ -408,81 +408,81 @@ func TestTask(t *testing.T) {
}) })
t.Run("Delete", func(t *testing.T) { t.Run("Delete", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "1"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "1"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
}) })
t.Run("Nonexisting", func(t *testing.T) { t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"listtask": "99999"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "99999"})
assert.Error(t, err) assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist) assertHandlerErrorCode(t, err, models.ErrCodeTaskDoesNotExist)
}) })
t.Run("Rights check", func(t *testing.T) { t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"listtask": "14"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "14"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team readonly", func(t *testing.T) { t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"listtask": "15"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "15"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team write", func(t *testing.T) { t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "16"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "16"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
}) })
t.Run("Shared Via Team admin", func(t *testing.T) { t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "17"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "17"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
}) })
t.Run("Shared Via User readonly", func(t *testing.T) { t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"listtask": "18"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "18"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User write", func(t *testing.T) { t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "19"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "19"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
}) })
t.Run("Shared Via User admin", func(t *testing.T) { t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "20"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "20"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
}) })
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"listtask": "21"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "21"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "22"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "22"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
}) })
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "23"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "23"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
}) })
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testDelete(nil, map[string]string{"listtask": "24"}) _, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "24"})
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser write", func(t *testing.T) { t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "25"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "25"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
}) })
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "26"}) rec, err := testHandler.testDeleteWithUser(nil, map[string]string{"listtask": "26"})
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `Successfully deleted.`) assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
}) })
@ -490,82 +490,82 @@ func TestTask(t *testing.T) {
}) })
t.Run("Create", func(t *testing.T) { t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) { t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Nonexisting", func(t *testing.T) { t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"list": "9999"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9999"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist) assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
}) })
t.Run("Rights check", func(t *testing.T) { t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) { t.Run("Forbidden", func(t *testing.T) {
// Owned by user13 // Owned by user13
_, err := testHandler.testCreate(nil, map[string]string{"list": "20"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "20"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team readonly", func(t *testing.T) { t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"list": "6"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "6"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via Team write", func(t *testing.T) { t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"list": "7"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "7"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via Team admin", func(t *testing.T) { t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"list": "8"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "8"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via User readonly", func(t *testing.T) { t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"list": "9"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "9"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via User write", func(t *testing.T) { t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"list": "10"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "10"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via User admin", func(t *testing.T) { t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"list": "11"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "11"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) { t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"list": "12"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "12"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceTeam write", func(t *testing.T) { t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"list": "13"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "13"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) { t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"list": "14"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "14"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) { t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
_, err := testHandler.testCreate(nil, map[string]string{"list": "15"}, `{"text":"Lorem Ipsum"}`) _, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "15"}, `{"text":"Lorem Ipsum"}`)
assert.Error(t, err) assert.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`) assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
}) })
t.Run("Shared Via NamespaceUser write", func(t *testing.T) { t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"list": "16"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "16"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })
t.Run("Shared Via NamespaceUser admin", func(t *testing.T) { t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
rec, err := testHandler.testCreate(nil, map[string]string{"list": "17"}, `{"text":"Lorem Ipsum"}`) rec, err := testHandler.testCreateWithUser(nil, map[string]string{"list": "17"}, `{"text":"Lorem Ipsum"}`)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`) assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
}) })

View file

@ -0,0 +1,53 @@
// Copyright 2019 Vikunja and contriubtors. All rights reserved.
//
// This file is part of Vikunja.
//
// Vikunja is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Vikunja is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"code.vikunja.io/api/pkg/models"
"github.com/go-xorm/xorm"
"src.techknowlogick.com/xormigrate"
)
type linkSharing20190818210133 struct {
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id"`
Hash string `xorm:"varchar(40) not null unique" json:"hash" param:"hash"`
ListID int64 `xorm:"int(11) not null" json:"list_id"`
Right models.Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
SharingType models.SharingType `xorm:"int(11) INDEX not null default 0" json:"sharing_type" valid:"length(0|2)" maximum:"2" default:"0"`
SharedByID int64 `xorm:"int(11) INDEX not null"`
Created int64 `xorm:"created not null" json:"created"`
Updated int64 `xorm:"updated not null" json:"updated"`
}
// TableName holds the table name for this share
func (linkSharing20190818210133) TableName() string {
return "link_sharing"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20190818210133",
Description: "Add link sharing table",
Migrate: func(tx *xorm.Engine) error {
return tx.Sync2(linkSharing20190818210133{})
},
Rollback: func(tx *xorm.Engine) error {
return tx.DropTables(linkSharing20190818210133{})
},
})
}

View file

@ -22,6 +22,29 @@ import (
"net/http" "net/http"
) )
// Generic
// ErrGenericForbidden represents a "UsernameAlreadyExists" kind of error.
type ErrGenericForbidden struct{}
// IsErrGenericForbidden checks if an error is a ErrGenericForbidden.
func IsErrGenericForbidden(err error) bool {
_, ok := err.(ErrGenericForbidden)
return ok
}
func (err ErrGenericForbidden) Error() string {
return fmt.Sprintf("Forbidden")
}
// ErrorCodeGenericForbidden holds the unique world-error code of this error
const ErrorCodeGenericForbidden = 0001
// HTTPError holds the http error description
func (err ErrGenericForbidden) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusForbidden, Code: ErrorCodeGenericForbidden, Message: "You're not allowed to do this."}
}
// ===================== // =====================
// User Operation Errors // User Operation Errors
// ===================== // =====================
@ -423,6 +446,30 @@ func (err ErrListTitleCannotBeEmpty) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeListTitleCannotBeEmpty, Message: "You must provide at least a list title."} return web.HTTPError{HTTPCode: http.StatusBadRequest, Code: ErrCodeListTitleCannotBeEmpty, Message: "You must provide at least a list title."}
} }
// ErrListShareDoesNotExist represents a "ErrListShareDoesNotExist" kind of error. Used if the list share does not exist.
type ErrListShareDoesNotExist struct {
ID int64
Hash string
}
// IsErrListShareDoesNotExist checks if an error is a ErrListShareDoesNotExist.
func IsErrListShareDoesNotExist(err error) bool {
_, ok := err.(ErrListShareDoesNotExist)
return ok
}
func (err ErrListShareDoesNotExist) Error() string {
return fmt.Sprintf("List share does not exist.")
}
// ErrCodeListShareDoesNotExist holds the unique world-error code of this error
const ErrCodeListShareDoesNotExist = 3006
// HTTPError holds the http error description
func (err ErrListShareDoesNotExist) HTTPError() web.HTTPError {
return web.HTTPError{HTTPCode: http.StatusNotFound, Code: ErrCodeListShareDoesNotExist, Message: "The list share does not exist."}
}
// ================ // ================
// List task errors // List task errors
// ================ // ================

View file

@ -0,0 +1,24 @@
- id: 1
hash: test
list_id: 1
right: 0
sharing_type: 1
shared_by_id: 1
created: 0
updated: 0
- id: 2
hash: test2
list_id: 2
right: 1
sharing_type: 1
shared_by_id: 1
created: 0
updated: 0
- id: 3
hash: test3
list_id: 3
right: 2
sharing_type: 1
shared_by_id: 1
created: 0
updated: 0

View file

@ -198,4 +198,10 @@
hex_color: f0f0f0 hex_color: f0f0f0
created: 1543626724 created: 1543626724
updated: 1543626724 updated: 1543626724
- id: 32
text: 'task #32'
created_by_id: 1
list_id: 3
created: 1543626724
updated: 1543626724

View file

@ -131,6 +131,9 @@ func (l *Label) Delete() (err error) {
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /labels [get] // @Router /labels [get]
func (l *Label) ReadAll(search string, a web.Auth, page int) (ls interface{}, err error) { func (l *Label) ReadAll(search string, a web.Auth, page int) (ls interface{}, err error) {
if _, is := a.(*LinkSharing); is {
return nil, ErrGenericForbidden{}
}
u := &User{ID: a.GetID()} u := &User{ID: a.GetID()}
@ -192,7 +195,18 @@ func getLabelByIDSimple(labelID int64) (*Label, error) {
// Helper method to get all task ids a user has // Helper method to get all task ids a user has
func getUserTaskIDs(u *User) (taskIDs []int64, err error) { func getUserTaskIDs(u *User) (taskIDs []int64, err error) {
tasks, err := GetTasksByUser("", u, -1, SortTasksByUnsorted, time.Unix(0, 0), time.Unix(0, 0))
// Get all lists
lists, err := getRawListsForUser("", u, -1)
if err != nil {
return nil, err
}
tasks, err := getTasksForLists(lists, &taskOptions{
startDate: time.Unix(0, 0),
endDate: time.Unix(0, 0),
sortby: SortTasksByUnsorted,
})
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -39,10 +39,19 @@ func (l *Label) CanRead(a web.Auth) (bool, error) {
// CanCreate checks if the user can create a label // CanCreate checks if the user can create a label
// Currently a dummy. // Currently a dummy.
func (l *Label) CanCreate(a web.Auth) (bool, error) { func (l *Label) CanCreate(a web.Auth) (bool, error) {
if _, is := a.(*LinkSharing); is {
return false, nil
}
return true, nil return true, nil
} }
func (l *Label) isLabelOwner(a web.Auth) (bool, error) { func (l *Label) isLabelOwner(a web.Auth) (bool, error) {
if _, is := a.(*LinkSharing); is {
return false, nil
}
lorig, err := getLabelByIDSimple(l.ID) lorig, err := getLabelByIDSimple(l.ID)
if err != nil { if err != nil {
return false, err return false, err
@ -52,6 +61,9 @@ func (l *Label) isLabelOwner(a web.Auth) (bool, error) {
// Helper method to check if a user can see a specific label // Helper method to check if a user can see a specific label
func (l *Label) hasAccessToLabel(a web.Auth) (bool, error) { func (l *Label) hasAccessToLabel(a web.Auth) (bool, error) {
// TODO: add an extra check for link share handling
// Get all tasks // Get all tasks
taskIDs, err := getUserTaskIDs(&User{ID: a.GetID()}) taskIDs, err := getUserTaskIDs(&User{ID: a.GetID()})
if err != nil { if err != nil {

207
pkg/models/link_sharing.go Normal file
View file

@ -0,0 +1,207 @@
// Copyright 2019 Vikunja and contriubtors. All rights reserved.
//
// This file is part of Vikunja.
//
// Vikunja is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Vikunja is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Vikunja. If not, see <https://www.gnu.org/licenses/>.
package models
import (
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/web"
"github.com/dgrijalva/jwt-go"
)
// SharingType holds the sharing type
type SharingType int
// These consts represent all valid link sharing types
const (
SharingTypeUnknown SharingType = iota
SharingTypeWithoutPassword
SharingTypeWithPassword
)
// LinkSharing represents a shared list
type LinkSharing struct {
// The ID of the shared thing
ID int64 `xorm:"int(11) autoincr not null unique pk" json:"id" param:"share"`
// The public id to get this shared list
Hash string `xorm:"varchar(40) not null unique" json:"hash" param:"hash"`
// The ID of the shared list
ListID int64 `xorm:"int(11) not null" json:"-" param:"list"`
// The right this list is shared with. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
List *List `xorm:"-" json:"list" param:"fullist"`
// The kind of this link. 0 = undefined, 1 = without password, 2 = with password (currently not implemented).
SharingType SharingType `xorm:"int(11) INDEX not null default 0" json:"sharing_type" valid:"length(0|2)" maximum:"2" default:"0"`
// The user who shared this list
SharedBy *User `xorm:"-" json:"shared_by"`
SharedByID int64 `xorm:"int(11) INDEX not null" json:"-"`
// A unix timestamp when this list was shared. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"`
// A unix timestamp when this share was last updated. You cannot change this value.
Updated int64 `xorm:"updated not null" json:"updated"`
web.CRUDable `xorm:"-" json:"-"`
web.Rights `xorm:"-" json:"-"`
}
// TableName holds the table name
func (LinkSharing) TableName() string {
return "link_sharing"
}
// GetID returns the ID of the links sharing object
func (share *LinkSharing) GetID() int64 {
return share.ID
}
// GetLinkShareFromClaims builds a link sharing object from jwt claims
func GetLinkShareFromClaims(claims jwt.MapClaims) (share *LinkSharing, err error) {
share = &LinkSharing{}
share.ID = int64(claims["id"].(float64))
share.Hash = claims["hash"].(string)
share.ListID = int64(claims["listID"].(float64))
share.Right = Right(claims["right"].(float64))
share.SharedByID = int64(claims["sharedByID"].(float64))
return
}
// Create creates a new link share for a given list
// @Summary Share a list via link
// @Description Share a list via link. The user needs to have write-access to the list to be able do this.
// @tags sharing
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param list path int true "List ID"
// @Param label body models.LinkSharing true "The new link share object"
// @Success 200 {object} models.LinkSharing "The created link share object."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid link share object provided."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to add the list share."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "The list does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares [put]
func (share *LinkSharing) Create(a web.Auth) (err error) {
share.SharedByID = a.GetID()
share.Hash = utils.MakeRandomString(40)
_, err = x.Insert(share)
return
}
// ReadOne returns one share
// @Summary Get one link shares for a list
// @Description Returns one link share by its ID.
// @tags sharing
// @Accept json
// @Produce json
// @Param list path int true "List ID"
// @Param share path int true "Share ID"
// @Security JWTKeyAuth
// @Success 200 {object} models.LinkSharing "The share links"
// @Failure 403 {object} code.vikunja.io/web.HTTPError "No access to the list"
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Share Link not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares/{share} [get]
func (share *LinkSharing) ReadOne() (err error) {
exists, err := x.Where("id = ?", share.ID).Get(share)
if err != nil {
return err
}
if !exists {
return ErrListShareDoesNotExist{ID: share.ID, Hash: share.Hash}
}
return
}
// ReadAll returns all shares for a given list
// @Summary Get all link shares for a list
// @Description Returns all link shares which exist for a given list
// @tags sharing
// @Accept json
// @Produce json
// @Param list path int true "List ID"
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param s query string false "Search shares by hash."
// @Security JWTKeyAuth
// @Success 200 {array} models.LinkSharing "The share links"
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares [get]
func (share *LinkSharing) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
list := &List{ID: share.ListID}
can, err := list.CanRead(a)
if err != nil {
return nil, err
}
if !can {
return nil, ErrGenericForbidden{}
}
var shares []*LinkSharing
err = x.
Where("list_id = ? AND hash LIKE ?", share.ListID, "%"+search+"%").
Limit(getLimitFromPageIndex(page)).
Find(&shares)
return shares, err
}
// Delete removes a link share
// @Summary Remove a link share
// @Description Remove a link share. The user needs to have write-access to the list to be able do this.
// @tags sharing
// @Accept json
// @Produce json
// @Security JWTKeyAuth
// @Param list path int true "List ID"
// @Param share path int true "Share Link ID"
// @Success 200 {object} models.Message "The link was successfully removed."
// @Failure 403 {object} code.vikunja.io/web.HTTPError "Not allowed to remove the link."
// @Failure 404 {object} code.vikunja.io/web.HTTPError "Share Link not found."
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{list}/shares/{share} [delete]
func (share *LinkSharing) Delete() (err error) {
_, err = x.Where("id = ?", share.ID).Delete(share)
return
}
// GetLinkShareByHash returns a link share by hash
func GetLinkShareByHash(hash string) (share *LinkSharing, err error) {
share = &LinkSharing{}
has, err := x.Where("hash = ?", hash).Get(share)
if err != nil {
return
}
if !has {
return share, ErrListShareDoesNotExist{Hash: hash}
}
share.List = &List{ID: share.ListID}
return
}
// GetListByShareHash returns a link share by its hash
func GetListByShareHash(hash string) (list *List, err error) {
share, err := GetLinkShareByHash(hash)
if err != nil {
return
}
list = &List{ID: share.ListID}
err = list.GetSimpleByID()
return
}

View file

@ -0,0 +1,61 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package models
import "code.vikunja.io/web"
// CanRead implements the read right check for a link share
func (share *LinkSharing) CanRead(a web.Auth) (bool, error) {
// Don't allow creating link shares if the user itself authenticated with a link share
if _, is := a.(*LinkSharing); is {
return false, nil
}
l, err := GetListByShareHash(share.Hash)
if err != nil {
return false, err
}
return l.CanRead(a)
}
// CanDelete implements the delete right check for a link share
func (share *LinkSharing) CanDelete(a web.Auth) (bool, error) {
return share.canDoLinkShare(a)
}
// CanUpdate implements the update right check for a link share
func (share *LinkSharing) CanUpdate(a web.Auth) (bool, error) {
return share.canDoLinkShare(a)
}
// CanCreate implements the create right check for a link share
func (share *LinkSharing) CanCreate(a web.Auth) (bool, error) {
return share.canDoLinkShare(a)
}
func (share *LinkSharing) canDoLinkShare(a web.Auth) (bool, error) {
// Don't allow creating link shares if the user itself authenticated with a link share
if _, is := a.(*LinkSharing); is {
return false, nil
}
l, err := GetListByShareHash(share.Hash)
if err != nil {
return false, err
}
return l.CanWrite(a)
}

View file

@ -85,14 +85,26 @@ func GetListsByNamespaceID(nID int64, doer *User) (lists []*List, err error) {
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /lists [get] // @Router /lists [get]
func (l *List) ReadAll(search string, a web.Auth, page int) (interface{}, error) { func (l *List) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
shareAuth.List = &List{ID: shareAuth.ListID}
err := shareAuth.List.GetSimpleByID()
if err != nil {
return nil, err
}
lists := []*List{shareAuth.List}
err = AddListDetails(lists)
return lists, err
}
lists, err := getRawListsForUser(search, &User{ID: a.GetID()}, page) lists, err := getRawListsForUser(search, &User{ID: a.GetID()}, page)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Add more list details // Add more list details
AddListDetails(lists) err = AddListDetails(lists)
return lists, err return lists, err
} }

View file

@ -31,6 +31,13 @@ func (l *List) CanWrite(a web.Auth) (bool, error) {
return false, err return false, err
} }
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
return originalList.ID == shareAuth.ListID &&
(shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), nil
}
// Check if the user is either owner or can write to the list // Check if the user is either owner or can write to the list
if originalList.isOwner(&User{ID: a.GetID()}) { if originalList.isOwner(&User{ID: a.GetID()}) {
return true, nil return true, nil
@ -45,6 +52,14 @@ func (l *List) CanRead(a web.Auth) (bool, error) {
if err := l.GetSimpleByID(); err != nil { if err := l.GetSimpleByID(); err != nil {
return false, err return false, err
} }
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
return l.ID == shareAuth.ListID &&
(shareAuth.Right == RightRead || shareAuth.Right == RightWrite || shareAuth.Right == RightAdmin), nil
}
if l.isOwner(&User{ID: a.GetID()}) { if l.isOwner(&User{ID: a.GetID()}) {
return true, nil return true, nil
} }
@ -61,7 +76,7 @@ func (l *List) CanDelete(a web.Auth) (bool, error) {
return l.IsAdmin(a) return l.IsAdmin(a)
} }
// CanCreate checks if the user can update a list // CanCreate checks if the user can create a list
func (l *List) CanCreate(a web.Auth) (bool, error) { func (l *List) CanCreate(a web.Auth) (bool, error) {
// A user can create a list if he has write access to the namespace // A user can create a list if he has write access to the namespace
n := &Namespace{ID: l.NamespaceID} n := &Namespace{ID: l.NamespaceID}
@ -76,6 +91,12 @@ func (l *List) IsAdmin(a web.Auth) (bool, error) {
return false, err return false, err
} }
// Check if we're dealing with a share auth
shareAuth, ok := a.(*LinkSharing)
if ok {
return originalList.ID == shareAuth.ListID && shareAuth.Right == RightAdmin, nil
}
// Check all the things // Check all the things
// Check if the user is either owner or can write to the list // Check if the user is either owner or can write to the list
// Owners are always admins // Owners are always admins

View file

@ -22,20 +22,25 @@ import (
// CanCreate checks if the user can create a new user <-> list relation // CanCreate checks if the user can create a new user <-> list relation
func (lu *ListUser) CanCreate(a web.Auth) (bool, error) { func (lu *ListUser) CanCreate(a web.Auth) (bool, error) {
// Get the list and check if the user has write access on it return lu.canDoListUser(a)
l := List{ID: lu.ListID}
return l.CanWrite(a)
} }
// CanDelete checks if the user can delete a user <-> list relation // CanDelete checks if the user can delete a user <-> list relation
func (lu *ListUser) CanDelete(a web.Auth) (bool, error) { func (lu *ListUser) CanDelete(a web.Auth) (bool, error) {
// Get the list and check if the user has write access on it return lu.canDoListUser(a)
l := List{ID: lu.ListID}
return l.CanWrite(a)
} }
// CanUpdate checks if the user can update a user <-> list relation // CanUpdate checks if the user can update a user <-> list relation
func (lu *ListUser) CanUpdate(a web.Auth) (bool, error) { func (lu *ListUser) CanUpdate(a web.Auth) (bool, error) {
return lu.canDoListUser(a)
}
func (lu *ListUser) canDoListUser(a web.Auth) (bool, error) {
// Link shares aren't allowed to do anything
if _, is := a.(*LinkSharing); is {
return false, nil
}
// Get the list and check if the user has write access on it // Get the list and check if the user has write access on it
l := List{ID: lu.ListID} l := List{ID: lu.ListID}
return l.CanWrite(a) return l.CanWrite(a)

View file

@ -48,6 +48,7 @@ func GetTables() []interface{} {
&Label{}, &Label{},
&LabelTask{}, &LabelTask{},
&TaskReminder{}, &TaskReminder{},
&LinkSharing{},
} }
} }

View file

@ -138,6 +138,10 @@ type NamespaceWithLists struct {
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /namespaces [get] // @Router /namespaces [get]
func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, error) { func (n *Namespace) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
if _, is := a.(*LinkSharing); is {
return nil, ErrGenericForbidden{}
}
doer, err := getUserWithError(a) doer, err := getUserWithError(a)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -48,12 +48,21 @@ func (n *Namespace) CanDelete(a web.Auth) (bool, error) {
// CanCreate checks if the user can create a new namespace // CanCreate checks if the user can create a new namespace
func (n *Namespace) CanCreate(a web.Auth) (bool, error) { func (n *Namespace) CanCreate(a web.Auth) (bool, error) {
if _, is := a.(*LinkSharing); is {
return false, nil
}
// This is currently a dummy function, later on we could imagine global limits etc. // This is currently a dummy function, later on we could imagine global limits etc.
return true, nil return true, nil
} }
func (n *Namespace) checkRight(a web.Auth, rights ...Right) (bool, error) { func (n *Namespace) checkRight(a web.Auth, rights ...Right) (bool, error) {
// If the auth is a link share, don't do anything
if _, is := a.(*LinkSharing); is {
return false, nil
}
// Get the namespace and check the right // Get the namespace and check the right
err := n.GetSimpleByID() err := n.GetSimpleByID()
if err != nil { if err != nil {

View file

@ -221,13 +221,27 @@ func (t *Task) addNewAssigneeByID(newAssigneeID int64, list *List) (err error) {
// @Produce json // @Produce json
// @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned." // @Param p query int false "The page number. Used for pagination. If not provided, the first page of results is returned."
// @Param s query string false "Search assignees by their username." // @Param s query string false "Search assignees by their username."
// @Param taskID path int true "Task ID"
// @Security JWTKeyAuth // @Security JWTKeyAuth
// @Success 200 {array} models.User "The assignees" // @Success 200 {array} models.User "The assignees"
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /labels [get] // @Router /tasks/{taskID}/assignees [get]
func (la *TaskAssginee) ReadAll(search string, a web.Auth, page int) (interface{}, error) { func (la *TaskAssginee) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
task, err := GetListSimplByTaskID(la.TaskID)
if err != nil {
return nil, err
}
can, err := task.CanRead(a)
if err != nil {
return nil, err
}
if !can {
return nil, ErrGenericForbidden{}
}
var taskAssignees []*User var taskAssignees []*User
err := x.Table("task_assignees"). err = x.Table("task_assignees").
Select("users.*"). Select("users.*").
Join("INNER", "users", "task_assignees.user_id = users.id"). Join("INNER", "users", "task_assignees.user_id = users.id").
Where("task_id = ? AND users.username LIKE ?", la.TaskID, "%"+search+"%"). Where("task_id = ? AND users.username LIKE ?", la.TaskID, "%"+search+"%").

View file

@ -41,5 +41,5 @@ func canDoTaskAssingee(taskID int64, a web.Auth) (bool, error) {
if err != nil { if err != nil {
return false, err return false, err
} }
return list.CanCreate(a) return list.CanUpdate(a)
} }

View file

@ -317,6 +317,15 @@ func sortTasksForTesting(by SortBy) (tasks []*Task) {
Created: 1543626724, Created: 1543626724,
Updated: 1543626724, Updated: 1543626724,
}, },
{
ID: 32,
Text: "task #32",
CreatedByID: 1,
CreatedBy: user1,
ListID: 3,
Created: 1543626724,
Updated: 1543626724,
},
} }
switch by { switch by {

View file

@ -147,17 +147,40 @@ func (t *Task) ReadAll(search string, a web.Auth, page int) (interface{}, error)
sortby = SortTasksByUnsorted sortby = SortTasksByUnsorted
} }
return GetTasksByUser(search, &User{ID: a.GetID()}, page, sortby, time.Unix(t.StartDateSortUnix, 0), time.Unix(t.EndDateSortUnix, 0)) taskopts := &taskOptions{
} search: search,
sortby: sortby,
startDate: time.Unix(t.StartDateSortUnix, 0),
endDate: time.Unix(t.EndDateSortUnix, 0),
}
//GetTasksByUser returns all tasks for a user shareAuth, is := a.(*LinkSharing)
func GetTasksByUser(search string, u *User, page int, sortby SortBy, startDate time.Time, endDate time.Time) ([]*Task, error) { if is {
// Get all lists shareAuth.List = &List{ID: shareAuth.ListID}
lists, err := getRawListsForUser("", u, page) err := shareAuth.List.GetSimpleByID()
if err != nil {
return nil, err
}
return getTasksForLists([]*List{shareAuth.List}, taskopts)
}
// Get all lists for the user
lists, err := getRawListsForUser("", &User{ID: a.GetID()}, page)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return getTasksForLists(lists, taskopts)
}
type taskOptions struct {
search string
sortby SortBy
startDate time.Time
endDate time.Time
}
func getTasksForLists(lists []*List, opts *taskOptions) (tasks []*Task, err error) {
// Get all list IDs and get the tasks // Get all list IDs and get the tasks
var listIDs []int64 var listIDs []int64
for _, l := range lists { for _, l := range lists {
@ -165,7 +188,7 @@ func GetTasksByUser(search string, u *User, page int, sortby SortBy, startDate t
} }
var orderby string var orderby string
switch sortby { switch opts.sortby {
case SortTasksByPriorityDesc: case SortTasksByPriorityDesc:
orderby = "priority desc" orderby = "priority desc"
case SortTasksByPriorityAsc: case SortTasksByPriorityAsc:
@ -179,20 +202,20 @@ func GetTasksByUser(search string, u *User, page int, sortby SortBy, startDate t
taskMap := make(map[int64]*Task) taskMap := make(map[int64]*Task)
// Then return all tasks for that lists // Then return all tasks for that lists
if startDate.Unix() != 0 || endDate.Unix() != 0 { if opts.startDate.Unix() != 0 || opts.endDate.Unix() != 0 {
startDateUnix := time.Now().Unix() startDateUnix := time.Now().Unix()
if startDate.Unix() != 0 { if opts.startDate.Unix() != 0 {
startDateUnix = startDate.Unix() startDateUnix = opts.startDate.Unix()
} }
endDateUnix := time.Now().Unix() endDateUnix := time.Now().Unix()
if endDate.Unix() != 0 { if opts.endDate.Unix() != 0 {
endDateUnix = endDate.Unix() endDateUnix = opts.endDate.Unix()
} }
if err := x.In("list_id", listIDs). if err := x.In("list_id", listIDs).
Where("text LIKE ?", "%"+search+"%"). Where("text LIKE ?", "%"+opts.search+"%").
And("((due_date_unix BETWEEN ? AND ?) OR "+ And("((due_date_unix BETWEEN ? AND ?) OR "+
"(start_date_unix BETWEEN ? and ?) OR "+ "(start_date_unix BETWEEN ? and ?) OR "+
"(end_date_unix BETWEEN ? and ?))", startDateUnix, endDateUnix, startDateUnix, endDateUnix, startDateUnix, endDateUnix). "(end_date_unix BETWEEN ? and ?))", startDateUnix, endDateUnix, startDateUnix, endDateUnix, startDateUnix, endDateUnix).
@ -203,7 +226,7 @@ func GetTasksByUser(search string, u *User, page int, sortby SortBy, startDate t
} }
} else { } else {
if err := x.In("list_id", listIDs). if err := x.In("list_id", listIDs).
Where("text LIKE ?", "%"+search+"%"). Where("text LIKE ?", "%"+opts.search+"%").
And("(parent_task_id = 0 OR parent_task_id IS NULL)"). And("(parent_task_id = 0 OR parent_task_id IS NULL)").
OrderBy(orderby). OrderBy(orderby).
Find(&taskMap); err != nil { Find(&taskMap); err != nil {
@ -211,13 +234,13 @@ func GetTasksByUser(search string, u *User, page int, sortby SortBy, startDate t
} }
} }
tasks, err := addMoreInfoToTasks(taskMap) tasks, err = addMoreInfoToTasks(taskMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Because the list is sorted by id which we don't want (since we're dealing with maps) // Because the list is sorted by id which we don't want (since we're dealing with maps)
// we have to manually sort the tasks again here. // we have to manually sort the tasks again here.
sortTasks(tasks, sortby) sortTasks(tasks, opts.sortby)
return tasks, err return tasks, err
} }

View file

@ -47,8 +47,8 @@ func (t *Task) CanRead(a web.Auth) (canRead bool, err error) {
} }
// A user can read a task if it has access to the list // A user can read a task if it has access to the list
list := &List{ID: t.ListID} l := &List{ID: t.ListID}
return list.CanRead(a) return l.CanRead(a)
} }
// Helper function to check if a user can do stuff on a list task // Helper function to check if a user can do stuff on a list task

View file

@ -36,6 +36,11 @@ func (tl *TeamList) CanUpdate(a web.Auth) (bool, error) {
} }
func (tl *TeamList) canDoTeamList(a web.Auth) (bool, error) { func (tl *TeamList) canDoTeamList(a web.Auth) (bool, error) {
// Link shares aren't allowed to do anything
if _, is := a.(*LinkSharing); is {
return false, nil
}
l := List{ID: tl.ListID} l := List{ID: tl.ListID}
return l.IsAdmin(a) return l.IsAdmin(a)
} }

View file

@ -32,6 +32,11 @@ func (tm *TeamMember) CanDelete(a web.Auth) (bool, error) {
// IsAdmin checks if the user is team admin // IsAdmin checks if the user is team admin
func (tm *TeamMember) IsAdmin(a web.Auth) (bool, error) { func (tm *TeamMember) IsAdmin(a web.Auth) (bool, error) {
// Don't allow anything if we're dealing with a list share here
if _, is := a.(*LinkSharing); is {
return false, nil
}
// A user can add a member to a team if he is admin of that team // A user can add a member to a team if he is admin of that team
exists, err := x.Where("user_id = ? AND team_id = ? AND admin = ?", a.GetID(), tm.TeamID, true). exists, err := x.Where("user_id = ? AND team_id = ? AND admin = ?", a.GetID(), tm.TeamID, true).
Get(&TeamMember{}) Get(&TeamMember{})

View file

@ -142,6 +142,10 @@ func (t *Team) ReadOne() (err error) {
// @Failure 500 {object} models.Message "Internal error" // @Failure 500 {object} models.Message "Internal error"
// @Router /teams [get] // @Router /teams [get]
func (t *Team) ReadAll(search string, a web.Auth, page int) (interface{}, error) { func (t *Team) ReadAll(search string, a web.Auth, page int) (interface{}, error) {
if _, is := a.(*LinkSharing); is {
return nil, ErrGenericForbidden{}
}
all := []*Team{} all := []*Team{}
err := x.Select("teams.*"). err := x.Select("teams.*").
Table("teams"). Table("teams").

View file

@ -22,6 +22,10 @@ import (
// CanCreate checks if the user can create a new team // CanCreate checks if the user can create a new team
func (t *Team) CanCreate(a web.Auth) (bool, error) { func (t *Team) CanCreate(a web.Auth) (bool, error) {
if _, is := a.(*LinkSharing); is {
return false, nil
}
// This is currently a dummy function, later on we could imagine global limits etc. // This is currently a dummy function, later on we could imagine global limits etc.
return true, nil return true, nil
} }
@ -38,6 +42,11 @@ func (t *Team) CanDelete(a web.Auth) (bool, error) {
// IsAdmin returns true when the user is admin of a team // IsAdmin returns true when the user is admin of a team
func (t *Team) IsAdmin(a web.Auth) (bool, error) { func (t *Team) IsAdmin(a web.Auth) (bool, error) {
// Don't do anything if we're deadling with a link share auth here
if _, is := a.(*LinkSharing); is {
return false, nil
}
// Check if the team exists to be able to return a proper error message if not // Check if the team exists to be able to return a proper error message if not
_, err := GetTeamByID(t.ID) _, err := GetTeamByID(t.ID)
if err != nil { if err != nil {

View file

@ -189,6 +189,11 @@ func CheckUserCredentials(u *UserLogin) (*User, error) {
func GetCurrentUser(c echo.Context) (user *User, err error) { func GetCurrentUser(c echo.Context) (user *User, err error) {
jwtinf := c.Get("user").(*jwt.Token) jwtinf := c.Get("user").(*jwt.Token)
claims := jwtinf.Claims.(jwt.MapClaims) claims := jwtinf.Claims.(jwt.MapClaims)
return GetUserFromClaims(claims)
}
// GetUserFromClaims Returns a new user from jwt claims
func GetUserFromClaims(claims jwt.MapClaims) (user *User, err error) {
userID, ok := claims["id"].(float64) userID, ok := claims["id"].(float64)
if !ok { if !ok {
return user, ErrCouldNotGetUserID{} return user, ErrCouldNotGetUserID{}

67
pkg/routes/api/v1/auth.go Normal file
View file

@ -0,0 +1,67 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package v1
import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/models"
"github.com/dgrijalva/jwt-go"
"time"
)
// These are all valid auth types
const (
AuthTypeUnknown int = iota
AuthTypeUser
AuthTypeLinkShare
)
// NewUserJWTAuthtoken generates and signes a new jwt token for a user. This is a global function to be able to call it from integration tests.
func NewUserJWTAuthtoken(user *models.User) (token string, err error) {
t := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := t.Claims.(jwt.MapClaims)
claims["type"] = AuthTypeUser
claims["id"] = user.ID
claims["username"] = user.Username
claims["email"] = user.Email
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
claims["avatar"] = user.AvatarURL
// Generate encoded token and send it as response.
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))
}
// NewLinkShareJWTAuthtoken creates a new jwt token from a link share
func NewLinkShareJWTAuthtoken(share *models.LinkSharing) (token string, err error) {
t := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := t.Claims.(jwt.MapClaims)
claims["type"] = AuthTypeLinkShare
claims["id"] = share.ID
claims["hash"] = share.Hash
claims["listID"] = share.ListID
claims["right"] = share.Right
claims["sharedByID"] = share.SharedByID
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
// Generate encoded token and send it as response.
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))
}

View file

@ -24,22 +24,24 @@ import (
) )
type vikunjaInfos struct { type vikunjaInfos struct {
Version string `json:"version"` Version string `json:"version"`
FrontendURL string `json:"frontend_url"` FrontendURL string `json:"frontend_url"`
Motd string `json:"motd"` Motd string `json:"motd"`
LinkSharingEnabled bool `json:"link_sharing_enabled"`
} }
// Info is the handler to get infos about this vikunja instance // Info is the handler to get infos about this vikunja instance
// @Summary Info // @Summary Info
// @Description Returns the version, frontendurl and motd of Vikunja // @Description Returns the version, frontendurl, motd and various settings of Vikunja
// @tags service // @tags service
// @Produce json // @Produce json
// @Success 200 {object} v1.vikunjaInfos // @Success 200 {object} v1.vikunjaInfos
// @Router /info [get] // @Router /info [get]
func Info(c echo.Context) error { func Info(c echo.Context) error {
return c.JSON(http.StatusOK, vikunjaInfos{ return c.JSON(http.StatusOK, vikunjaInfos{
Version: version.Version, Version: version.Version,
FrontendURL: config.ServiceFrontendurl.GetString(), FrontendURL: config.ServiceFrontendurl.GetString(),
Motd: config.ServiceMotd.GetString(), Motd: config.ServiceMotd.GetString(),
LinkSharingEnabled: config.ServiceEnableLinkSharing.GetBool(),
}) })
} }

View file

@ -0,0 +1,50 @@
// Vikunja is a todo-list application to facilitate your life.
// Copyright 2019 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package v1
import (
"code.vikunja.io/api/pkg/models"
"code.vikunja.io/web/handler"
"github.com/labstack/echo/v4"
"net/http"
)
// AuthenticateLinkShare gives a jwt auth token for valid share hashes
// @Summary Get an auth token for a share
// @Description Get a jwt auth token for a shared list from a share hash.
// @tags sharing
// @Accept json
// @Produce json
// @Param share path string true "The share hash"
// @Success 200 {object} v1.Token "The valid jwt auth token."
// @Failure 400 {object} code.vikunja.io/web.HTTPError "Invalid link share object provided."
// @Failure 500 {object} models.Message "Internal error"
// @Router /shares/{share}/auth [post]
func AuthenticateLinkShare(c echo.Context) error {
hash := c.Param("share")
share, err := models.GetLinkShareByHash(hash)
if err != nil {
return handler.HandleHTTPError(err, c)
}
t, err := NewLinkShareJWTAuthtoken(share)
if err != nil {
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, Token{Token: t})
}

View file

@ -17,13 +17,10 @@
package v1 package v1
import ( import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/models"
"code.vikunja.io/web/handler" "code.vikunja.io/web/handler"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"net/http" "net/http"
"time"
) )
// Token represents an authentification token // Token represents an authentification token
@ -55,27 +52,10 @@ func Login(c echo.Context) error {
} }
// Create token // Create token
t, err := CreateNewJWTTokenForUser(user) t, err := NewUserJWTAuthtoken(user)
if err != nil { if err != nil {
return err return err
} }
return c.JSON(http.StatusOK, Token{Token: t}) return c.JSON(http.StatusOK, Token{Token: t})
} }
// CreateNewJWTTokenForUser generates and signes a new jwt token for a user. This is a global function to be able to call it from integration tests.
func CreateNewJWTTokenForUser(user *models.User) (token string, err error) {
t := jwt.New(jwt.SigningMethodHS256)
// Set claims
claims := t.Claims.(jwt.MapClaims)
claims["username"] = user.Username
claims["email"] = user.Email
claims["id"] = user.ID
claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
claims["avatar"] = user.AvatarURL
// Generate encoded token and send it as response.
return t.SignedString([]byte(config.ServiceJWTSecret.GetString()))
}

View file

@ -48,9 +48,11 @@ import (
"code.vikunja.io/web" "code.vikunja.io/web"
"code.vikunja.io/web/handler" "code.vikunja.io/web/handler"
"github.com/asaskevich/govalidator" "github.com/asaskevich/govalidator"
"github.com/dgrijalva/jwt-go"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
elog "github.com/labstack/gommon/log" elog "github.com/labstack/gommon/log"
"net/http"
"strings" "strings"
) )
@ -67,11 +69,11 @@ func (cv *CustomValidator) Validate(i interface{}) error {
} }
httperr := models.ValidationHTTPError{ httperr := models.ValidationHTTPError{
web.HTTPError{ HTTPError: web.HTTPError{
Code: models.ErrCodeInvalidData, Code: models.ErrCodeInvalidData,
Message: "Invalid Data", Message: "Invalid Data",
}, },
errs, InvalidFields: errs,
} }
return httperr return httperr
@ -108,7 +110,16 @@ func NewEcho() *echo.Echo {
// Handler config // Handler config
handler.SetAuthProvider(&web.Auths{ handler.SetAuthProvider(&web.Auths{
AuthObject: func(c echo.Context) (web.Auth, error) { AuthObject: func(c echo.Context) (web.Auth, error) {
return models.GetCurrentUser(c) jwtinf := c.Get("user").(*jwt.Token)
claims := jwtinf.Claims.(jwt.MapClaims)
typ := int(claims["type"].(float64))
if typ == apiv1.AuthTypeLinkShare && config.ServiceEnableLinkSharing.GetBool() {
return models.GetLinkShareFromClaims(claims)
}
if typ == apiv1.AuthTypeUser {
return models.GetUserFromClaims(claims)
}
return nil, echo.NewHTTPError(http.StatusBadRequest, models.Message{Message: "Invalid JWT token."})
}, },
}) })
handler.SetLoggingProvider(log.GetLogger()) handler.SetLoggingProvider(log.GetLogger())
@ -165,6 +176,11 @@ func registerAPIRoutes(a *echo.Group) {
// Info endpoint // Info endpoint
a.GET("/info", apiv1.Info) a.GET("/info", apiv1.Info)
// Link share auth
if config.ServiceEnableLinkSharing.GetBool() {
a.POST("/shares/:share/auth", apiv1.AuthenticateLinkShare)
}
// ===== Routes with Authetication ===== // ===== Routes with Authetication =====
// Authetification // Authetification
a.Use(middleware.JWT([]byte(config.ServiceJWTSecret.GetString()))) a.Use(middleware.JWT([]byte(config.ServiceJWTSecret.GetString())))
@ -194,6 +210,18 @@ func registerAPIRoutes(a *echo.Group) {
a.PUT("/namespaces/:namespace/lists", listHandler.CreateWeb) a.PUT("/namespaces/:namespace/lists", listHandler.CreateWeb)
a.GET("/lists/:list/listusers", apiv1.ListUsersForList) a.GET("/lists/:list/listusers", apiv1.ListUsersForList)
if config.ServiceEnableLinkSharing.GetBool() {
listSharingHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject {
return &models.LinkSharing{}
},
}
a.PUT("/lists/:list/shares", listSharingHandler.CreateWeb)
a.GET("/lists/:list/shares", listSharingHandler.ReadAllWeb)
a.GET("/lists/:list/shares/:share", listSharingHandler.ReadOneWeb)
a.DELETE("/lists/:list/shares/:share", listSharingHandler.DeleteWeb)
}
taskHandler := &handler.WebHandler{ taskHandler := &handler.WebHandler{
EmptyStruct: func() handler.CObject { EmptyStruct: func() handler.CObject {
return &models.Task{} return &models.Task{}

View file

@ -1,6 +1,6 @@
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT // GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by swaggo/swag at // This file was generated by swaggo/swag at
// 2019-07-21 23:56:59.485456403 +0200 CEST m=+0.091837160 // 2019-08-31 22:48:49.201391811 +0200 CEST m=+0.228973511
package swagger package swagger
@ -32,7 +32,7 @@ var doc = `{
"paths": { "paths": {
"/info": { "/info": {
"get": { "get": {
"description": "Returns the version, frontendurl and motd of Vikunja", "description": "Returns the version, frontendurl, motd and various settings of Vikunja",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@ -58,7 +58,7 @@ var doc = `{
"JWTKeyAuth": [] "JWTKeyAuth": []
} }
], ],
"description": "Returns an array with all assignees for this task.", "description": "Returns all labels which are either created by the user or associated with a task the user has at least read-access to.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -66,9 +66,9 @@ var doc = `{
"application/json" "application/json"
], ],
"tags": [ "tags": [
"assignees" "labels"
], ],
"summary": "Get all assignees for a task", "summary": "Get all labels a user has access to",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "integer",
@ -78,18 +78,18 @@ var doc = `{
}, },
{ {
"type": "string", "type": "string",
"description": "Search assignees by their username.", "description": "Search labels by label text.",
"name": "s", "name": "s",
"in": "query" "in": "query"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "The assignees", "description": "The labels",
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.User" "$ref": "#/definitions/models.Label"
} }
} }
}, },
@ -1266,6 +1266,269 @@ var doc = `{
} }
} }
}, },
"/lists/{list}/shares": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns all link shares which exist for a given list",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sharing"
],
"summary": "Get all link shares for a list",
"parameters": [
{
"type": "integer",
"description": "List ID",
"name": "list",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
"name": "p",
"in": "query"
},
{
"type": "string",
"description": "Search shares by hash.",
"name": "s",
"in": "query"
}
],
"responses": {
"200": {
"description": "The share links",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.LinkSharing"
}
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
},
"put": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Share a list via link. The user needs to have write-access to the list to be able do this.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sharing"
],
"summary": "Share a list via link",
"parameters": [
{
"type": "integer",
"description": "List ID",
"name": "list",
"in": "path",
"required": true
},
{
"description": "The new link share object",
"name": "label",
"in": "body",
"required": true,
"schema": {
"type": "object",
"$ref": "#/definitions/models.LinkSharing"
}
}
],
"responses": {
"200": {
"description": "The created link share object.",
"schema": {
"type": "object",
"$ref": "#/definitions/models.LinkSharing"
}
},
"400": {
"description": "Invalid link share object provided.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
}
},
"403": {
"description": "Not allowed to add the list share.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
}
},
"404": {
"description": "The list does not exist.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/lists/{list}/shares/{share}": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns one link share by its ID.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sharing"
],
"summary": "Get one link shares for a list",
"parameters": [
{
"type": "integer",
"description": "List ID",
"name": "list",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Share ID",
"name": "share",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The share links",
"schema": {
"type": "object",
"$ref": "#/definitions/models.LinkSharing"
}
},
"403": {
"description": "No access to the list",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
}
},
"404": {
"description": "Share Link not found.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
},
"delete": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Remove a link share. The user needs to have write-access to the list to be able do this.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sharing"
],
"summary": "Remove a link share",
"parameters": [
{
"type": "integer",
"description": "List ID",
"name": "list",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Share Link ID",
"name": "share",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The link was successfully removed.",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
},
"403": {
"description": "Not allowed to remove the link.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
}
},
"404": {
"description": "Share Link not found.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/login": { "/login": {
"post": { "post": {
"description": "Logs a user in. Returns a JWT-Token to authenticate further requests.", "description": "Logs a user in. Returns a JWT-Token to authenticate further requests.",
@ -2337,6 +2600,53 @@ var doc = `{
} }
} }
}, },
"/shares/{share}/auth": {
"post": {
"description": "Get a jwt auth token for a shared list from a share hash.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sharing"
],
"summary": "Get an auth token for a share",
"parameters": [
{
"type": "string",
"description": "The share hash",
"name": "share",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The valid jwt auth token.",
"schema": {
"type": "object",
"$ref": "#/definitions/v1.Token"
}
},
"400": {
"description": "Invalid link share object provided.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io.web.HTTPError"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/tasks/all": { "/tasks/all": {
"get": { "get": {
"security": [ "security": [
@ -2593,6 +2903,63 @@ var doc = `{
} }
}, },
"/tasks/{taskID}/assignees": { "/tasks/{taskID}/assignees": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns an array with all assignees for this task.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assignees"
],
"summary": "Get all assignees for a task",
"parameters": [
{
"type": "integer",
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
"name": "p",
"in": "query"
},
{
"type": "string",
"description": "Search assignees by their username.",
"name": "s",
"in": "query"
},
{
"type": "integer",
"description": "Task ID",
"name": "taskID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The assignees",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.User"
}
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
},
"put": { "put": {
"security": [ "security": [
{ {
@ -3896,6 +4263,48 @@ var doc = `{
} }
} }
}, },
"models.LinkSharing": {
"type": "object",
"properties": {
"created": {
"description": "A unix timestamp when this list was shared. You cannot change this value.",
"type": "integer"
},
"hash": {
"description": "The public id to get this shared list",
"type": "string"
},
"id": {
"description": "The ID of the shared thing",
"type": "integer"
},
"list": {
"type": "object",
"$ref": "#/definitions/models.List"
},
"right": {
"description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.",
"type": "integer",
"default": 0,
"maximum": 2
},
"shared_by": {
"description": "The user who shared this list",
"type": "object",
"$ref": "#/definitions/models.User"
},
"sharing_type": {
"description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password (currently not implemented).",
"type": "integer",
"default": 0,
"maximum": 2
},
"updated": {
"description": "A unix timestamp when this share was last updated. You cannot change this value.",
"type": "integer"
}
}
},
"models.List": { "models.List": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3935,118 +4344,6 @@ var doc = `{
} }
} }
}, },
"models.Task": {
"type": "object",
"properties": {
"assignees": {
"description": "An array of users who are assigned to this task",
"type": "array",
"items": {
"$ref": "#/definitions/models.User"
}
},
"created": {
"description": "A unix timestamp when this task was created. You cannot change this value.",
"type": "integer"
},
"createdBy": {
"description": "The user who initially created the task.",
"type": "object",
"$ref": "#/definitions/models.User"
},
"description": {
"description": "The task description.",
"type": "string"
},
"done": {
"description": "Whether a task is done or not.",
"type": "boolean"
},
"doneAt": {
"description": "The unix timestamp when a task was marked as done.",
"type": "integer"
},
"dueDate": {
"description": "A unix timestamp when the task is due.",
"type": "integer"
},
"endDate": {
"description": "When this task ends.",
"type": "integer"
},
"hexColor": {
"description": "The task color in hex",
"type": "string",
"maxLength": 6
},
"id": {
"description": "The unique, numeric id of this task.",
"type": "integer"
},
"labels": {
"description": "An array of labels which are associated with this task.",
"type": "array",
"items": {
"$ref": "#/definitions/models.Label"
}
},
"listID": {
"description": "The list this task belongs to.",
"type": "integer"
},
"parentTaskID": {
"description": "If the task is a subtask, this is the id of its parent.",
"type": "integer"
},
"priority": {
"description": "The task priority. Can be anything you want, it is possible to sort by this later.",
"type": "integer"
},
"reminderDates": {
"description": "An array of unix timestamps when the user wants to be reminded of the task.",
"type": "array",
"items": {
"type": "integer"
}
},
"repeatAfter": {
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
"type": "integer"
},
"startDate": {
"description": "When this task starts.",
"type": "integer"
},
"subtasks": {
"description": "An array of subtasks.",
"type": "array",
"items": {
"$ref": "#/definitions/models.Task"
}
},
"text": {
"description": "The task text. This is what you'll see in the list.",
"type": "string",
"maxLength": 250,
"minLength": 3
},
"updated": {
"description": "A unix timestamp when this task was last updated. You cannot change this value.",
"type": "integer"
}
}
},
"models.TaskAssginee": {
"type": "object",
"properties": {
"created": {
"type": "integer"
},
"user_id": {
"type": "integer"
}
}
},
"models.ListUser": { "models.ListUser": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4202,6 +4499,118 @@ var doc = `{
} }
} }
}, },
"models.Task": {
"type": "object",
"properties": {
"assignees": {
"description": "An array of users who are assigned to this task",
"type": "array",
"items": {
"$ref": "#/definitions/models.User"
}
},
"created": {
"description": "A unix timestamp when this task was created. You cannot change this value.",
"type": "integer"
},
"createdBy": {
"description": "The user who initially created the task.",
"type": "object",
"$ref": "#/definitions/models.User"
},
"description": {
"description": "The task description.",
"type": "string"
},
"done": {
"description": "Whether a task is done or not.",
"type": "boolean"
},
"doneAt": {
"description": "The unix timestamp when a task was marked as done.",
"type": "integer"
},
"dueDate": {
"description": "A unix timestamp when the task is due.",
"type": "integer"
},
"endDate": {
"description": "When this task ends.",
"type": "integer"
},
"hexColor": {
"description": "The task color in hex",
"type": "string",
"maxLength": 6
},
"id": {
"description": "The unique, numeric id of this task.",
"type": "integer"
},
"labels": {
"description": "An array of labels which are associated with this task.",
"type": "array",
"items": {
"$ref": "#/definitions/models.Label"
}
},
"listID": {
"description": "The list this task belongs to.",
"type": "integer"
},
"parentTaskID": {
"description": "If the task is a subtask, this is the id of its parent.",
"type": "integer"
},
"priority": {
"description": "The task priority. Can be anything you want, it is possible to sort by this later.",
"type": "integer"
},
"reminderDates": {
"description": "An array of unix timestamps when the user wants to be reminded of the task.",
"type": "array",
"items": {
"type": "integer"
}
},
"repeatAfter": {
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
"type": "integer"
},
"startDate": {
"description": "When this task starts.",
"type": "integer"
},
"subtasks": {
"description": "An array of subtasks.",
"type": "array",
"items": {
"$ref": "#/definitions/models.Task"
}
},
"text": {
"description": "The task text. This is what you'll see in the list.",
"type": "string",
"maxLength": 250,
"minLength": 3
},
"updated": {
"description": "A unix timestamp when this task was last updated. You cannot change this value.",
"type": "integer"
}
}
},
"models.TaskAssginee": {
"type": "object",
"properties": {
"created": {
"type": "integer"
},
"user_id": {
"type": "integer"
}
}
},
"models.Team": { "models.Team": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4499,6 +4908,9 @@ var doc = `{
"frontend_url": { "frontend_url": {
"type": "string" "type": "string"
}, },
"link_sharing_enabled": {
"type": "boolean"
},
"motd": { "motd": {
"type": "string" "type": "string"
}, },

View file

@ -19,7 +19,7 @@
"paths": { "paths": {
"/info": { "/info": {
"get": { "get": {
"description": "Returns the version, frontendurl and motd of Vikunja", "description": "Returns the version, frontendurl, motd and various settings of Vikunja",
"produces": [ "produces": [
"application/json" "application/json"
], ],
@ -45,7 +45,7 @@
"JWTKeyAuth": [] "JWTKeyAuth": []
} }
], ],
"description": "Returns an array with all assignees for this task.", "description": "Returns all labels which are either created by the user or associated with a task the user has at least read-access to.",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -53,9 +53,9 @@
"application/json" "application/json"
], ],
"tags": [ "tags": [
"assignees" "labels"
], ],
"summary": "Get all assignees for a task", "summary": "Get all labels a user has access to",
"parameters": [ "parameters": [
{ {
"type": "integer", "type": "integer",
@ -65,18 +65,18 @@
}, },
{ {
"type": "string", "type": "string",
"description": "Search assignees by their username.", "description": "Search labels by label text.",
"name": "s", "name": "s",
"in": "query" "in": "query"
} }
], ],
"responses": { "responses": {
"200": { "200": {
"description": "The assignees", "description": "The labels",
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.User" "$ref": "#/definitions/models.Label"
} }
} }
}, },
@ -475,7 +475,7 @@
"required": true, "required": true,
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/models.ListTask" "$ref": "#/definitions/models.Task"
} }
} }
], ],
@ -484,7 +484,7 @@
"description": "The created task object.", "description": "The created task object.",
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/models.ListTask" "$ref": "#/definitions/models.Task"
} }
}, },
"400": { "400": {
@ -1253,6 +1253,269 @@
} }
} }
}, },
"/lists/{list}/shares": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns all link shares which exist for a given list",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sharing"
],
"summary": "Get all link shares for a list",
"parameters": [
{
"type": "integer",
"description": "List ID",
"name": "list",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
"name": "p",
"in": "query"
},
{
"type": "string",
"description": "Search shares by hash.",
"name": "s",
"in": "query"
}
],
"responses": {
"200": {
"description": "The share links",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.LinkSharing"
}
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
},
"put": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Share a list via link. The user needs to have write-access to the list to be able do this.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sharing"
],
"summary": "Share a list via link",
"parameters": [
{
"type": "integer",
"description": "List ID",
"name": "list",
"in": "path",
"required": true
},
{
"description": "The new link share object",
"name": "label",
"in": "body",
"required": true,
"schema": {
"type": "object",
"$ref": "#/definitions/models.LinkSharing"
}
}
],
"responses": {
"200": {
"description": "The created link share object.",
"schema": {
"type": "object",
"$ref": "#/definitions/models.LinkSharing"
}
},
"400": {
"description": "Invalid link share object provided.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
}
},
"403": {
"description": "Not allowed to add the list share.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
}
},
"404": {
"description": "The list does not exist.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/lists/{list}/shares/{share}": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns one link share by its ID.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sharing"
],
"summary": "Get one link shares for a list",
"parameters": [
{
"type": "integer",
"description": "List ID",
"name": "list",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Share ID",
"name": "share",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The share links",
"schema": {
"type": "object",
"$ref": "#/definitions/models.LinkSharing"
}
},
"403": {
"description": "No access to the list",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
}
},
"404": {
"description": "Share Link not found.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
},
"delete": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Remove a link share. The user needs to have write-access to the list to be able do this.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sharing"
],
"summary": "Remove a link share",
"parameters": [
{
"type": "integer",
"description": "List ID",
"name": "list",
"in": "path",
"required": true
},
{
"type": "integer",
"description": "Share Link ID",
"name": "share",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The link was successfully removed.",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
},
"403": {
"description": "Not allowed to remove the link.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
}
},
"404": {
"description": "Share Link not found.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/login": { "/login": {
"post": { "post": {
"description": "Logs a user in. Returns a JWT-Token to authenticate further requests.", "description": "Logs a user in. Returns a JWT-Token to authenticate further requests.",
@ -2324,6 +2587,53 @@
} }
} }
}, },
"/shares/{share}/auth": {
"post": {
"description": "Get a jwt auth token for a shared list from a share hash.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"sharing"
],
"summary": "Get an auth token for a share",
"parameters": [
{
"type": "string",
"description": "The share hash",
"name": "share",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The valid jwt auth token.",
"schema": {
"type": "object",
"$ref": "#/definitions/v1.Token"
}
},
"400": {
"description": "Invalid link share object provided.",
"schema": {
"type": "object",
"$ref": "#/definitions/code.vikunja.io/web.HTTPError"
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
}
},
"/tasks/all": { "/tasks/all": {
"get": { "get": {
"security": [ "security": [
@ -2380,7 +2690,7 @@
"schema": { "schema": {
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.ListTask" "$ref": "#/definitions/models.Task"
} }
} }
}, },
@ -2429,7 +2739,7 @@
"description": "The updated task object.", "description": "The updated task object.",
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/models.ListTask" "$ref": "#/definitions/models.Task"
} }
}, },
"400": { "400": {
@ -2489,7 +2799,7 @@
"required": true, "required": true,
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/models.ListTask" "$ref": "#/definitions/models.Task"
} }
} }
], ],
@ -2498,7 +2808,7 @@
"description": "The updated task object.", "description": "The updated task object.",
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/models.ListTask" "$ref": "#/definitions/models.Task"
} }
}, },
"400": { "400": {
@ -2580,6 +2890,63 @@
} }
}, },
"/tasks/{taskID}/assignees": { "/tasks/{taskID}/assignees": {
"get": {
"security": [
{
"JWTKeyAuth": []
}
],
"description": "Returns an array with all assignees for this task.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assignees"
],
"summary": "Get all assignees for a task",
"parameters": [
{
"type": "integer",
"description": "The page number. Used for pagination. If not provided, the first page of results is returned.",
"name": "p",
"in": "query"
},
{
"type": "string",
"description": "Search assignees by their username.",
"name": "s",
"in": "query"
},
{
"type": "integer",
"description": "Task ID",
"name": "taskID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "The assignees",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/models.User"
}
}
},
"500": {
"description": "Internal error",
"schema": {
"type": "object",
"$ref": "#/definitions/models.Message"
}
}
}
},
"put": { "put": {
"security": [ "security": [
{ {
@ -2605,7 +2972,7 @@
"required": true, "required": true,
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/models.ListTaskAssginee" "$ref": "#/definitions/models.TaskAssginee"
} }
}, },
{ {
@ -2621,7 +2988,7 @@
"description": "The created assingee object.", "description": "The created assingee object.",
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/models.ListTaskAssginee" "$ref": "#/definitions/models.TaskAssginee"
} }
}, },
"400": { "400": {
@ -2683,7 +3050,7 @@
"description": "The created assingees object.", "description": "The created assingees object.",
"schema": { "schema": {
"type": "object", "type": "object",
"$ref": "#/definitions/models.ListTaskAssginee" "$ref": "#/definitions/models.TaskAssginee"
} }
}, },
"400": { "400": {
@ -3789,7 +4156,7 @@
"description": "An array of subtasks.", "description": "An array of subtasks.",
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.ListTask" "$ref": "#/definitions/models.Task"
} }
}, },
"task_ids": { "task_ids": {
@ -3882,6 +4249,48 @@
} }
} }
}, },
"models.LinkSharing": {
"type": "object",
"properties": {
"created": {
"description": "A unix timestamp when this list was shared. You cannot change this value.",
"type": "integer"
},
"hash": {
"description": "The public id to get this shared list",
"type": "string"
},
"id": {
"description": "The ID of the shared thing",
"type": "integer"
},
"list": {
"type": "object",
"$ref": "#/definitions/models.List"
},
"right": {
"description": "The right this list is shared with. 0 = Read only, 1 = Read \u0026 Write, 2 = Admin. See the docs for more details.",
"type": "integer",
"default": 0,
"maximum": 2
},
"shared_by": {
"description": "The user who shared this list",
"type": "object",
"$ref": "#/definitions/models.User"
},
"sharing_type": {
"description": "The kind of this link. 0 = undefined, 1 = without password, 2 = with password (currently not implemented).",
"type": "integer",
"default": 0,
"maximum": 2
},
"updated": {
"description": "A unix timestamp when this share was last updated. You cannot change this value.",
"type": "integer"
}
}
},
"models.List": { "models.List": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -3906,7 +4315,7 @@
"description": "An array of tasks which belong to the list.", "description": "An array of tasks which belong to the list.",
"type": "array", "type": "array",
"items": { "items": {
"$ref": "#/definitions/models.ListTask" "$ref": "#/definitions/models.Task"
} }
}, },
"title": { "title": {
@ -3921,118 +4330,6 @@
} }
} }
}, },
"models.ListTask": {
"type": "object",
"properties": {
"assignees": {
"description": "An array of users who are assigned to this task",
"type": "array",
"items": {
"$ref": "#/definitions/models.User"
}
},
"created": {
"description": "A unix timestamp when this task was created. You cannot change this value.",
"type": "integer"
},
"createdBy": {
"description": "The user who initially created the task.",
"type": "object",
"$ref": "#/definitions/models.User"
},
"description": {
"description": "The task description.",
"type": "string"
},
"done": {
"description": "Whether a task is done or not.",
"type": "boolean"
},
"doneAt": {
"description": "The unix timestamp when a task was marked as done.",
"type": "integer"
},
"dueDate": {
"description": "A unix timestamp when the task is due.",
"type": "integer"
},
"endDate": {
"description": "When this task ends.",
"type": "integer"
},
"hexColor": {
"description": "The task color in hex",
"type": "string",
"maxLength": 6
},
"id": {
"description": "The unique, numeric id of this task.",
"type": "integer"
},
"labels": {
"description": "An array of labels which are associated with this task.",
"type": "array",
"items": {
"$ref": "#/definitions/models.Label"
}
},
"listID": {
"description": "The list this task belongs to.",
"type": "integer"
},
"parentTaskID": {
"description": "If the task is a subtask, this is the id of its parent.",
"type": "integer"
},
"priority": {
"description": "The task priority. Can be anything you want, it is possible to sort by this later.",
"type": "integer"
},
"reminderDates": {
"description": "An array of unix timestamps when the user wants to be reminded of the task.",
"type": "array",
"items": {
"type": "integer"
}
},
"repeatAfter": {
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
"type": "integer"
},
"startDate": {
"description": "When this task starts.",
"type": "integer"
},
"subtasks": {
"description": "An array of subtasks.",
"type": "array",
"items": {
"$ref": "#/definitions/models.ListTask"
}
},
"text": {
"description": "The task text. This is what you'll see in the list.",
"type": "string",
"maxLength": 250,
"minLength": 3
},
"updated": {
"description": "A unix timestamp when this task was last updated. You cannot change this value.",
"type": "integer"
}
}
},
"models.ListTaskAssginee": {
"type": "object",
"properties": {
"created": {
"type": "integer"
},
"user_id": {
"type": "integer"
}
}
},
"models.ListUser": { "models.ListUser": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4188,6 +4485,118 @@
} }
} }
}, },
"models.Task": {
"type": "object",
"properties": {
"assignees": {
"description": "An array of users who are assigned to this task",
"type": "array",
"items": {
"$ref": "#/definitions/models.User"
}
},
"created": {
"description": "A unix timestamp when this task was created. You cannot change this value.",
"type": "integer"
},
"createdBy": {
"description": "The user who initially created the task.",
"type": "object",
"$ref": "#/definitions/models.User"
},
"description": {
"description": "The task description.",
"type": "string"
},
"done": {
"description": "Whether a task is done or not.",
"type": "boolean"
},
"doneAt": {
"description": "The unix timestamp when a task was marked as done.",
"type": "integer"
},
"dueDate": {
"description": "A unix timestamp when the task is due.",
"type": "integer"
},
"endDate": {
"description": "When this task ends.",
"type": "integer"
},
"hexColor": {
"description": "The task color in hex",
"type": "string",
"maxLength": 6
},
"id": {
"description": "The unique, numeric id of this task.",
"type": "integer"
},
"labels": {
"description": "An array of labels which are associated with this task.",
"type": "array",
"items": {
"$ref": "#/definitions/models.Label"
}
},
"listID": {
"description": "The list this task belongs to.",
"type": "integer"
},
"parentTaskID": {
"description": "If the task is a subtask, this is the id of its parent.",
"type": "integer"
},
"priority": {
"description": "The task priority. Can be anything you want, it is possible to sort by this later.",
"type": "integer"
},
"reminderDates": {
"description": "An array of unix timestamps when the user wants to be reminded of the task.",
"type": "array",
"items": {
"type": "integer"
}
},
"repeatAfter": {
"description": "An amount in seconds this task repeats itself. If this is set, when marking the task as done, it will mark itself as \"undone\" and then increase all remindes and the due date by its amount.",
"type": "integer"
},
"startDate": {
"description": "When this task starts.",
"type": "integer"
},
"subtasks": {
"description": "An array of subtasks.",
"type": "array",
"items": {
"$ref": "#/definitions/models.Task"
}
},
"text": {
"description": "The task text. This is what you'll see in the list.",
"type": "string",
"maxLength": 250,
"minLength": 3
},
"updated": {
"description": "A unix timestamp when this task was last updated. You cannot change this value.",
"type": "integer"
}
}
},
"models.TaskAssginee": {
"type": "object",
"properties": {
"created": {
"type": "integer"
},
"user_id": {
"type": "integer"
}
}
},
"models.Team": { "models.Team": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -4485,6 +4894,9 @@
"frontend_url": { "frontend_url": {
"type": "string" "type": "string"
}, },
"link_sharing_enabled": {
"type": "boolean"
},
"motd": { "motd": {
"type": "string" "type": "string"
}, },

View file

@ -98,7 +98,7 @@ definitions:
subtasks: subtasks:
description: An array of subtasks. description: An array of subtasks.
items: items:
$ref: '#/definitions/models.ListTask' $ref: '#/definitions/models.Task'
type: array type: array
task_ids: task_ids:
description: A list of task ids to update description: A list of task ids to update
@ -170,6 +170,42 @@ definitions:
$ref: '#/definitions/models.Label' $ref: '#/definitions/models.Label'
type: array type: array
type: object type: object
models.LinkSharing:
properties:
created:
description: A unix timestamp when this list was shared. You cannot change
this value.
type: integer
hash:
description: The public id to get this shared list
type: string
id:
description: The ID of the shared thing
type: integer
list:
$ref: '#/definitions/models.List'
type: object
right:
default: 0
description: The right this list is shared with. 0 = Read only, 1 = Read &
Write, 2 = Admin. See the docs for more details.
maximum: 2
type: integer
shared_by:
$ref: '#/definitions/models.User'
description: The user who shared this list
type: object
sharing_type:
default: 0
description: The kind of this link. 0 = undefined, 1 = without password, 2
= with password (currently not implemented).
maximum: 2
type: integer
updated:
description: A unix timestamp when this share was last updated. You cannot
change this value.
type: integer
type: object
models.List: models.List:
properties: properties:
created: created:
@ -189,7 +225,7 @@ definitions:
tasks: tasks:
description: An array of tasks which belong to the list. description: An array of tasks which belong to the list.
items: items:
$ref: '#/definitions/models.ListTask' $ref: '#/definitions/models.Task'
type: array type: array
title: title:
description: The title of the list. You'll see this in the namespace overview. description: The title of the list. You'll see this in the namespace overview.
@ -201,94 +237,6 @@ definitions:
change this value. change this value.
type: integer type: integer
type: object type: object
models.ListTask:
properties:
assignees:
description: An array of users who are assigned to this task
items:
$ref: '#/definitions/models.User'
type: array
created:
description: A unix timestamp when this task was created. You cannot change
this value.
type: integer
createdBy:
$ref: '#/definitions/models.User'
description: The user who initially created the task.
type: object
description:
description: The task description.
type: string
done:
description: Whether a task is done or not.
type: boolean
doneAt:
description: The unix timestamp when a task was marked as done.
type: integer
dueDate:
description: A unix timestamp when the task is due.
type: integer
endDate:
description: When this task ends.
type: integer
hexColor:
description: The task color in hex
maxLength: 6
type: string
id:
description: The unique, numeric id of this task.
type: integer
labels:
description: An array of labels which are associated with this task.
items:
$ref: '#/definitions/models.Label'
type: array
listID:
description: The list this task belongs to.
type: integer
parentTaskID:
description: If the task is a subtask, this is the id of its parent.
type: integer
priority:
description: The task priority. Can be anything you want, it is possible to
sort by this later.
type: integer
reminderDates:
description: An array of unix timestamps when the user wants to be reminded
of the task.
items:
type: integer
type: array
repeatAfter:
description: An amount in seconds this task repeats itself. If this is set,
when marking the task as done, it will mark itself as "undone" and then
increase all remindes and the due date by its amount.
type: integer
startDate:
description: When this task starts.
type: integer
subtasks:
description: An array of subtasks.
items:
$ref: '#/definitions/models.ListTask'
type: array
text:
description: The task text. This is what you'll see in the list.
maxLength: 250
minLength: 3
type: string
updated:
description: A unix timestamp when this task was last updated. You cannot
change this value.
type: integer
type: object
models.ListTaskAssginee:
properties:
created:
type: integer
user_id:
type: integer
type: object
models.ListUser: models.ListUser:
properties: properties:
created: created:
@ -412,6 +360,94 @@ definitions:
maxLength: 250 maxLength: 250
type: string type: string
type: object type: object
models.Task:
properties:
assignees:
description: An array of users who are assigned to this task
items:
$ref: '#/definitions/models.User'
type: array
created:
description: A unix timestamp when this task was created. You cannot change
this value.
type: integer
createdBy:
$ref: '#/definitions/models.User'
description: The user who initially created the task.
type: object
description:
description: The task description.
type: string
done:
description: Whether a task is done or not.
type: boolean
doneAt:
description: The unix timestamp when a task was marked as done.
type: integer
dueDate:
description: A unix timestamp when the task is due.
type: integer
endDate:
description: When this task ends.
type: integer
hexColor:
description: The task color in hex
maxLength: 6
type: string
id:
description: The unique, numeric id of this task.
type: integer
labels:
description: An array of labels which are associated with this task.
items:
$ref: '#/definitions/models.Label'
type: array
listID:
description: The list this task belongs to.
type: integer
parentTaskID:
description: If the task is a subtask, this is the id of its parent.
type: integer
priority:
description: The task priority. Can be anything you want, it is possible to
sort by this later.
type: integer
reminderDates:
description: An array of unix timestamps when the user wants to be reminded
of the task.
items:
type: integer
type: array
repeatAfter:
description: An amount in seconds this task repeats itself. If this is set,
when marking the task as done, it will mark itself as "undone" and then
increase all remindes and the due date by its amount.
type: integer
startDate:
description: When this task starts.
type: integer
subtasks:
description: An array of subtasks.
items:
$ref: '#/definitions/models.Task'
type: array
text:
description: The task text. This is what you'll see in the list.
maxLength: 250
minLength: 3
type: string
updated:
description: A unix timestamp when this task was last updated. You cannot
change this value.
type: integer
type: object
models.TaskAssginee:
properties:
created:
type: integer
user_id:
type: integer
type: object
models.Team: models.Team:
properties: properties:
created: created:
@ -652,6 +688,8 @@ definitions:
properties: properties:
frontend_url: frontend_url:
type: string type: string
link_sharing_enabled:
type: boolean
motd: motd:
type: string type: string
version: version:
@ -678,7 +716,8 @@ info:
paths: paths:
/info: /info:
get: get:
description: Returns the version, frontendurl and motd of Vikunja description: Returns the version, frontendurl, motd and various settings of
Vikunja
produces: produces:
- application/json - application/json
responses: responses:
@ -694,14 +733,15 @@ paths:
get: get:
consumes: consumes:
- application/json - application/json
description: Returns an array with all assignees for this task. description: Returns all labels which are either created by the user or associated
with a task the user has at least read-access to.
parameters: parameters:
- description: The page number. Used for pagination. If not provided, the first - description: The page number. Used for pagination. If not provided, the first
page of results is returned. page of results is returned.
in: query in: query
name: p name: p
type: integer type: integer
- description: Search assignees by their username. - description: Search labels by label text.
in: query in: query
name: s name: s
type: string type: string
@ -709,10 +749,10 @@ paths:
- application/json - application/json
responses: responses:
"200": "200":
description: The assignees description: The labels
schema: schema:
items: items:
$ref: '#/definitions/models.User' $ref: '#/definitions/models.Label'
type: array type: array
"500": "500":
description: Internal error description: Internal error
@ -721,9 +761,9 @@ paths:
type: object type: object
security: security:
- JWTKeyAuth: [] - JWTKeyAuth: []
summary: Get all assignees for a task summary: Get all labels a user has access to
tags: tags:
- assignees - labels
put: put:
consumes: consumes:
- application/json - application/json
@ -1057,7 +1097,7 @@ paths:
name: task name: task
required: true required: true
schema: schema:
$ref: '#/definitions/models.ListTask' $ref: '#/definitions/models.Task'
type: object type: object
produces: produces:
- application/json - application/json
@ -1065,7 +1105,7 @@ paths:
"200": "200":
description: The created task object. description: The created task object.
schema: schema:
$ref: '#/definitions/models.ListTask' $ref: '#/definitions/models.Task'
type: object type: object
"400": "400":
description: Invalid task object provided. description: Invalid task object provided.
@ -1320,6 +1360,184 @@ paths:
summary: Add a user to a list summary: Add a user to a list
tags: tags:
- sharing - sharing
/lists/{list}/shares:
get:
consumes:
- application/json
description: Returns all link shares which exist for a given list
parameters:
- description: List ID
in: path
name: list
required: true
type: integer
- description: The page number. Used for pagination. If not provided, the first
page of results is returned.
in: query
name: p
type: integer
- description: Search shares by hash.
in: query
name: s
type: string
produces:
- application/json
responses:
"200":
description: The share links
schema:
items:
$ref: '#/definitions/models.LinkSharing'
type: array
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
type: object
security:
- JWTKeyAuth: []
summary: Get all link shares for a list
tags:
- sharing
put:
consumes:
- application/json
description: Share a list via link. The user needs to have write-access to the
list to be able do this.
parameters:
- description: List ID
in: path
name: list
required: true
type: integer
- description: The new link share object
in: body
name: label
required: true
schema:
$ref: '#/definitions/models.LinkSharing'
type: object
produces:
- application/json
responses:
"200":
description: The created link share object.
schema:
$ref: '#/definitions/models.LinkSharing'
type: object
"400":
description: Invalid link share object provided.
schema:
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
type: object
"403":
description: Not allowed to add the list share.
schema:
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
type: object
"404":
description: The list does not exist.
schema:
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
type: object
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
type: object
security:
- JWTKeyAuth: []
summary: Share a list via link
tags:
- sharing
/lists/{list}/shares/{share}:
delete:
consumes:
- application/json
description: Remove a link share. The user needs to have write-access to the
list to be able do this.
parameters:
- description: List ID
in: path
name: list
required: true
type: integer
- description: Share Link ID
in: path
name: share
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The link was successfully removed.
schema:
$ref: '#/definitions/models.Message'
type: object
"403":
description: Not allowed to remove the link.
schema:
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
type: object
"404":
description: Share Link not found.
schema:
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
type: object
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
type: object
security:
- JWTKeyAuth: []
summary: Remove a link share
tags:
- sharing
get:
consumes:
- application/json
description: Returns one link share by its ID.
parameters:
- description: List ID
in: path
name: list
required: true
type: integer
- description: Share ID
in: path
name: share
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The share links
schema:
$ref: '#/definitions/models.LinkSharing'
type: object
"403":
description: No access to the list
schema:
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
type: object
"404":
description: Share Link not found.
schema:
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
type: object
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
type: object
security:
- JWTKeyAuth: []
summary: Get one link shares for a list
tags:
- sharing
/lists/{listID}/teams/{teamID}: /lists/{listID}/teams/{teamID}:
delete: delete:
description: Delets a team from a list. The team won't have access to the list description: Delets a team from a list. The team won't have access to the list
@ -2232,6 +2450,38 @@ paths:
summary: Register summary: Register
tags: tags:
- user - user
/shares/{share}/auth:
post:
consumes:
- application/json
description: Get a jwt auth token for a shared list from a share hash.
parameters:
- description: The share hash
in: path
name: share
required: true
type: string
produces:
- application/json
responses:
"200":
description: The valid jwt auth token.
schema:
$ref: '#/definitions/v1.Token'
type: object
"400":
description: Invalid link share object provided.
schema:
$ref: '#/definitions/code.vikunja.io/web.HTTPError'
type: object
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
type: object
summary: Get an auth token for a share
tags:
- sharing
/tasks/{id}: /tasks/{id}:
delete: delete:
description: Deletes a task from a list. This does not mean "mark it done". description: Deletes a task from a list. This does not mean "mark it done".
@ -2286,7 +2536,7 @@ paths:
name: task name: task
required: true required: true
schema: schema:
$ref: '#/definitions/models.ListTask' $ref: '#/definitions/models.Task'
type: object type: object
produces: produces:
- application/json - application/json
@ -2294,7 +2544,7 @@ paths:
"200": "200":
description: The updated task object. description: The updated task object.
schema: schema:
$ref: '#/definitions/models.ListTask' $ref: '#/definitions/models.Task'
type: object type: object
"400": "400":
description: Invalid task object provided. description: Invalid task object provided.
@ -2452,6 +2702,44 @@ paths:
tags: tags:
- labels - labels
/tasks/{taskID}/assignees: /tasks/{taskID}/assignees:
get:
consumes:
- application/json
description: Returns an array with all assignees for this task.
parameters:
- description: The page number. Used for pagination. If not provided, the first
page of results is returned.
in: query
name: p
type: integer
- description: Search assignees by their username.
in: query
name: s
type: string
- description: Task ID
in: path
name: taskID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: The assignees
schema:
items:
$ref: '#/definitions/models.User'
type: array
"500":
description: Internal error
schema:
$ref: '#/definitions/models.Message'
type: object
security:
- JWTKeyAuth: []
summary: Get all assignees for a task
tags:
- assignees
put: put:
consumes: consumes:
- application/json - application/json
@ -2463,7 +2751,7 @@ paths:
name: assignee name: assignee
required: true required: true
schema: schema:
$ref: '#/definitions/models.ListTaskAssginee' $ref: '#/definitions/models.TaskAssginee'
type: object type: object
- description: Task ID - description: Task ID
in: path in: path
@ -2476,7 +2764,7 @@ paths:
"200": "200":
description: The created assingee object. description: The created assingee object.
schema: schema:
$ref: '#/definitions/models.ListTaskAssginee' $ref: '#/definitions/models.TaskAssginee'
type: object type: object
"400": "400":
description: Invalid assignee object provided. description: Invalid assignee object provided.
@ -2559,7 +2847,7 @@ paths:
"200": "200":
description: The created assingees object. description: The created assingees object.
schema: schema:
$ref: '#/definitions/models.ListTaskAssginee' $ref: '#/definitions/models.TaskAssginee'
type: object type: object
"400": "400":
description: Invalid assignee object provided. description: Invalid assignee object provided.
@ -2659,7 +2947,7 @@ paths:
description: The tasks description: The tasks
schema: schema:
items: items:
$ref: '#/definitions/models.ListTask' $ref: '#/definitions/models.Task'
type: array type: array
"500": "500":
description: Internal error description: Internal error
@ -2693,7 +2981,7 @@ paths:
"200": "200":
description: The updated task object. description: The updated task object.
schema: schema:
$ref: '#/definitions/models.ListTask' $ref: '#/definitions/models.Task'
type: object type: object
"400": "400":
description: Invalid task object provided. description: Invalid task object provided.