diff --git a/.drone1.yml b/.drone1.yml
index cf52b6cb..ce40b72b 100644
--- a/.drone1.yml
+++ b/.drone1.yml
@@ -9,7 +9,12 @@ clone:
depth: 50
services:
- - name: test-db
+ - name: test-db-unit
+ image: mariadb:10
+ environment:
+ MYSQL_ROOT_PASSWORD: vikunjatest
+ MYSQL_DATABASE: vikunjatest
+ - name: test-db-integration
image: mariadb:10
environment:
MYSQL_ROOT_PASSWORD: vikunjatest
@@ -68,7 +73,7 @@ steps:
environment:
VIKUNJA_TESTS_USE_CONFIG: 1
VIKUNJA_DATABASE_TYPE: mysql
- VIKUNJA_DATABASE_HOST: test-db
+ VIKUNJA_DATABASE_HOST: test-db-unit
VIKUNJA_DATABASE_USER: root
VIKUNJA_DATABASE_PASSWORD: vikunjatest
VIKUNJA_DATABASE_DATABASE: vikunjatest
@@ -78,6 +83,43 @@ steps:
when:
event: [ push, tag, pull_request ]
+ - name: integration-test
+ image: vikunja/golang-build:latest
+ pull: true
+ commands:
+ - make integration-test
+ depends_on: [ build ]
+ when:
+ event: [ push, tag, pull_request ]
+
+ - name: integration-test-sqlite
+ image: vikunja/golang-build:latest
+ pull: true
+ environment:
+ VIKUNJA_TESTS_USE_CONFIG: 1
+ VIKUNJA_DATABASE_TYPE: sqlite
+ commands:
+ - make integration-test
+ depends_on: [ build ]
+ when:
+ event: [ push, tag, pull_request ]
+
+ - name: integration-test-mysql
+ image: vikunja/golang-build:latest
+ pull: true
+ environment:
+ VIKUNJA_TESTS_USE_CONFIG: 1
+ VIKUNJA_DATABASE_TYPE: mysql
+ VIKUNJA_DATABASE_HOST: test-db-integration
+ VIKUNJA_DATABASE_USER: root
+ VIKUNJA_DATABASE_PASSWORD: vikunjatest
+ VIKUNJA_DATABASE_DATABASE: vikunjatest
+ commands:
+ - make integration-test
+ depends_on: [ build ]
+ when:
+ event: [ push, tag, pull_request ]
+
---
########
# Build a release when pushing to master
@@ -184,6 +226,9 @@ steps:
image: kolaente/fpm
pull: true
commands:
+ - echo $DRONE_TAG
+ - echo $DRONE_BRANCH
+ - echo $VERSION
- make build-deb
depends_on: [ static-build-linux ]
diff --git a/Makefile b/Makefile
index 1d687156..6e37043f 100644
--- a/Makefile
+++ b/Makefile
@@ -21,7 +21,7 @@ EXTRA_GOFLAGS ?=
LDFLAGS := -X "code.vikunja.io/api/pkg/cmd.Version=$(shell git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')" -X "main.Tags=$(TAGS)"
-PACKAGES ?= $(filter-out code.vikunja.io/api/integrations,$(shell go list -mod=vendor ./... | grep -v /vendor/))
+PACKAGES ?= $(filter-out code.vikunja.io/api/pkg/integrations,$(shell go list -mod=vendor ./... | grep -v /vendor/))
SOURCES ?= $(shell find . -name "*.go" -type f)
TAGS ?=
@@ -54,8 +54,6 @@ else
PKGVERSION := $(VERSION)
endif
-VERSION := $(shell echo $(VERSION) | sed 's/\//\-/g')
-
.PHONY: all
all: build
@@ -69,6 +67,10 @@ test:
VIKUNJA_SERVICE_ROOTPATH=$(shell pwd) go test $(GOFLAGS) -cover -coverprofile cover.out $(PACKAGES)
go tool cover -html=cover.out -o cover.html
+.PHONY: integration-test
+integration-test:
+ VIKUNJA_SERVICE_ROOTPATH=$(shell pwd) go test $(GOFLAGS) code.vikunja.io/api/pkg/integrations
+
.PHONY: lint
lint:
@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
diff --git a/REST-Tests/auth.http b/REST-Tests/auth.http
index 33c7c7a6..61695db7 100644
--- a/REST-Tests/auth.http
+++ b/REST-Tests/auth.http
@@ -3,7 +3,7 @@ POST http://localhost:8080/api/v1/login
Content-Type: application/json
{
- "username": "user",
+ "username": "user6",
"password": "1234"
}
@@ -21,3 +21,9 @@ Content-Type: application/json
}
###
+# Token test
+POST http://localhost:8080/api/v1/tokenTest
+Authorization: Bearer {{auth_token}}
+Content-Type: application/json
+
+###
diff --git a/docs/content/doc/development/test.md b/docs/content/doc/development/test.md
index bf6c8e81..1fdcfe89 100644
--- a/docs/content/doc/development/test.md
+++ b/docs/content/doc/development/test.md
@@ -24,4 +24,21 @@ To use the normal config set the enviroment variable `VIKUNJA_TESTS_USE_CONFIG=1
### Show sql queries
-When `UNIT_TESTS_VERBOSE=1` is set, all sql queries will be shown when tests are run.
\ No newline at end of file
+When `UNIT_TESTS_VERBOSE=1` is set, all sql queries will be shown when tests are run.
+
+### Fixtures
+
+All tests are run against a set of db fixtures.
+These fixtures are defined in `pkg/models/fixtures` in YAML-Files which represent the database structure.
+
+When you add a new test case which requires new database entries to test against, update these files.
+
+# Integration tests
+
+All integration tests live in `pkg/integrations`.
+You can run them by executing `make integration-test`.
+
+The integration tests use the same config and fixtures as the unit tests and therefor have the same options available,
+see at the beginning of this document.
+
+To run integration tests, use `make integration-test`.
diff --git a/docs/content/doc/usage/errors.md b/docs/content/doc/usage/errors.md
index 79bb2bf5..80aa2567 100644
--- a/docs/content/doc/usage/errors.md
+++ b/docs/content/doc/usage/errors.md
@@ -24,6 +24,8 @@ This document describes the different errors Vikunja can return.
| 1010 | 412 | Invalid email confirm token. |
| 1011 | 412 | Wrong username or password. |
| 1012 | 412 | Email address of the user not confirmed. |
+| 1013 | 412 | New password is empty. |
+| 1014 | 412 | Old password is empty. |
| 2001 | 400 | ID cannot be empty or 0. |
| 2002 | 400 | Some of the request data was invalid. The response contains an aditional array with all invalid fields. |
| 3001 | 404 | The list does not exist. |
diff --git a/go.mod b/go.mod
index ea0a9c0c..b2800c3a 100644
--- a/go.mod
+++ b/go.mod
@@ -69,6 +69,7 @@ require (
golang.org/x/sys v0.0.0-20190329044733-9eb1bfa1ce65 // indirect
golang.org/x/tools v0.0.0-20181026183834-f60e5f99f081 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
+ gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/testfixtures.v2 v2.5.3
gopkg.in/yaml.v2 v2.2.2 // indirect
diff --git a/go.sum b/go.sum
index a51fcc30..a7025e0f 100644
--- a/go.sum
+++ b/go.sum
@@ -245,6 +245,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/d4l3k/messagediff.v1 v1.2.1 h1:70AthpjunwzUiarMHyED52mj9UwtAnE89l1Gmrt3EU0=
+gopkg.in/d4l3k/messagediff.v1 v1.2.1/go.mod h1:EUzikiKadqXWcD1AzJLagx0j/BeeWGtn++04Xniyg44=
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
diff --git a/pkg/integrations/_test.go.tpl b/pkg/integrations/_test.go.tpl
new file mode 100644
index 00000000..bcf61e27
--- /dev/null
+++ b/pkg/integrations/_test.go.tpl
@@ -0,0 +1,350 @@
+// 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 .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/models"
+ "code.vikunja.io/web/handler"
+ "github.com/labstack/echo"
+ "github.com/stretchr/testify/assert"
+ "net/url"
+ "testing"
+)
+
+func Test${MODEL}(t *testing.T) {
+ testHandler := webHandlerTest{
+ user: &testuser1,
+ strFunc: func() handler.CObject {
+ return &models.${MODEL}{}
+ },
+ t: t,
+ }
+ t.Run("ReadAll", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(nil, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ assert.NotContains(t, rec.Body.String(), ``)
+ })
+ t.Run("Search", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"s": []string{""}}, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ assert.NotContains(t, rec.Body.String(), ``)
+ })
+ })
+ t.Run("ReadOne", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ assert.NotContains(t, rec.Body.String(), ``)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCode)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ // Owned by user3
+ _, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ 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) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ })
+ })
+ t.Run("Update", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCode)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ })
+ })
+ t.Run("Delete", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCode)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"${URL_PLACEHOLDER}": ""})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ })
+ })
+ t.Run("Create", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCode)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+
+ t.Run("Forbidden", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"${URL_PLACEHOLDER}": ""}, `{}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), ``)
+ })
+ })
+ })
+}
diff --git a/pkg/integrations/integrations.go b/pkg/integrations/integrations.go
new file mode 100644
index 00000000..e4dc6622
--- /dev/null
+++ b/pkg/integrations/integrations.go
@@ -0,0 +1,195 @@
+// 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 .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/config"
+ "code.vikunja.io/api/pkg/models"
+ "code.vikunja.io/api/pkg/routes"
+ v1 "code.vikunja.io/api/pkg/routes/api/v1"
+ "code.vikunja.io/web"
+ "code.vikunja.io/web/handler"
+ "github.com/dgrijalva/jwt-go"
+ "github.com/labstack/echo"
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+)
+
+// These are the test users, the same way they are in the test database
+var (
+ testuser1 = models.User{
+ ID: 1,
+ Username: "user1",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
+ Email: "user1@example.com",
+ IsActive: true,
+ }
+ testuser2 = models.User{
+ ID: 2,
+ Username: "user2",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
+ Email: "user2@example.com",
+ }
+ testuser3 = models.User{
+ ID: 3,
+ Username: "user3",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
+ Email: "user3@example.com",
+ PasswordResetToken: "passwordresettesttoken",
+ }
+ testuser4 = models.User{
+ ID: 4,
+ Username: "user4",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
+ Email: "user4@example.com",
+ EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
+ }
+ testuser5 = models.User{
+ ID: 4,
+ Username: "user5",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
+ Email: "user5@example.com",
+ EmailConfirmToken: "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael",
+ IsActive: false,
+ }
+)
+
+func setupTestEnv() (e *echo.Echo, err error) {
+ config.InitConfig()
+ models.SetupTests(viper.GetString("service.rootpath"))
+
+ err = models.LoadFixtures()
+ if err != nil {
+ return
+ }
+
+ e = routes.NewEcho()
+ routes.RegisterRoutes(e)
+ return
+}
+
+func bootstrapTestRequest(t *testing.T, method string, payload string, queryParam url.Values) (c echo.Context, rec *httptest.ResponseRecorder) {
+ // Setup
+ e, err := setupTestEnv()
+ assert.NoError(t, err)
+
+ // Do the actual request
+ req := httptest.NewRequest(method, "/", strings.NewReader(payload))
+ req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
+ req.URL.RawQuery = queryParam.Encode()
+ rec = httptest.NewRecorder()
+
+ c = e.NewContext(req, rec)
+ return
+}
+
+func newTestRequest(t *testing.T, method string, handler func(ctx echo.Context) error, payload string) (rec *httptest.ResponseRecorder, err error) {
+ c, rec := bootstrapTestRequest(t, method, payload, nil)
+ err = handler(c)
+ return
+}
+
+func addTokenToContext(t *testing.T, user *models.User, c echo.Context) {
+ // Get the token as a string
+ token, err := v1.CreateNewJWTTokenForUser(user)
+ 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(viper.GetString("service.JWTSecret")), nil
+ })
+ assert.NoError(t, err)
+ 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) {
+ c, rec := bootstrapTestRequest(t, method, payload, queryParams)
+
+ var paramNames []string
+ var paramValues []string
+ for name, value := range urlParams {
+ paramNames = append(paramNames, name)
+ paramValues = append(paramValues, value)
+ }
+ c.SetParamNames(paramNames...)
+ c.SetParamValues(paramValues...)
+
+ addTokenToContext(t, user, c)
+ err = handler(c)
+ return
+}
+
+func assertHandlerErrorCode(t *testing.T, err error, expectedErrorCode int) {
+ if err == nil {
+ t.Error("Error is nil")
+ t.FailNow()
+ }
+ httperr, ok := err.(*echo.HTTPError)
+ if !ok {
+ t.Error("Error is not *echo.HTTPError")
+ t.FailNow()
+ }
+ webhttperr, ok := httperr.Message.(web.HTTPError)
+ if !ok {
+ t.Error("Error is not *web.HTTPError")
+ t.FailNow()
+ }
+ assert.Equal(t, expectedErrorCode, webhttperr.Code)
+}
+
+type webHandlerTest struct {
+ user *models.User
+ strFunc func() handler.CObject
+ t *testing.T
+}
+
+func (h *webHandlerTest) getHandler() handler.WebHandler {
+ return handler.WebHandler{
+ EmptyStruct: func() handler.CObject {
+ return h.strFunc()
+ },
+ }
+}
+
+func (h *webHandlerTest) testReadAll(queryParams url.Values, urlParams map[string]string) (rec *httptest.ResponseRecorder, err error) {
+ hndl := h.getHandler()
+ 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) {
+ hndl := h.getHandler()
+ 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) {
+ hndl := h.getHandler()
+ 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) {
+ hndl := h.getHandler()
+ 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) {
+ hndl := h.getHandler()
+ return newTestRequestWithUser(h.t, http.MethodDelete, hndl.DeleteWeb, h.user, "", queryParams, urlParams)
+}
diff --git a/pkg/integrations/list_test.go b/pkg/integrations/list_test.go
new file mode 100644
index 00000000..f918f2f2
--- /dev/null
+++ b/pkg/integrations/list_test.go
@@ -0,0 +1,425 @@
+// 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 .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/models"
+ "code.vikunja.io/web/handler"
+ "github.com/labstack/echo"
+ "github.com/stretchr/testify/assert"
+ "net/url"
+ "testing"
+)
+
+func TestList(t *testing.T) {
+ testHandler := webHandlerTest{
+ user: &testuser1,
+ 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 := testHandler.testReadAll(nil, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Test1`)
+ assert.NotContains(t, rec.Body.String(), `Test2`)
+ assert.Contains(t, rec.Body.String(), `Test3`) // Shared directly via users_list
+ assert.Contains(t, rec.Body.String(), `Test4`) // Shared via namespace
+ assert.NotContains(t, rec.Body.String(), `Test5`)
+ })
+ t.Run("Search", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"s": []string{"Test1"}}, nil)
+ assert.NoError(t, err)
+ 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 := testHandler.testReadOne(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"`)
+ assert.Contains(t, rec.Body.String(), `"owner":{"id":1,"username":"user1",`)
+ assert.NotContains(t, rec.Body.String(), `"owner":{"id":2,"username":"user2",`)
+ assert.Contains(t, rec.Body.String(), `"tasks":[{"id":1,"text":"task #1",`)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testReadOne(nil, map[string]string{"list": "9999"})
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ // Owned by user3
+ _, err := testHandler.testReadOne(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 Via Team readonly", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "6"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test6"`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "7"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test7"`)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "8"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test8"`)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "9"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test9"`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "10"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test10"`)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "11"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test11"`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "12"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test12"`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "13"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test13"`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "14"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test14"`)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "15"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test15"`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "16"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test16"`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testReadOne(nil, map[string]string{"list": "17"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Test17"`)
+ })
+ })
+ })
+ t.Run("Update", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ // Check the list was loaded successfully afterwards, see testReadOne
+ rec, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ // The description should not be updated but returned correctly
+ assert.Contains(t, rec.Body.String(), `description":"Lorem Ipsum`)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"list": "9999"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
+ })
+ 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"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum dolor sit amet`)
+ })
+ t.Run("Empty title", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":""}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
+ })
+ t.Run("Almost empty title", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"list": "1"}, `{"title":"nn"}`)
+ assert.Error(t, err)
+ 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) {
+ _, 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"}`)
+ assert.Error(t, err)
+ 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("Forbidden", func(t *testing.T) {
+ // Owned by user3
+ _, err := testHandler.testUpdate(nil, map[string]string{"list": "2"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"list": "6"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"list": "7"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"list": "8"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"list": "9"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"list": "10"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"list": "11"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"list": "12"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"list": "13"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"list": "14"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"list": "15"}, `{"title":"TestLoremIpsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"list": "16"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"list": "17"}, `{"title":"TestLoremIpsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"TestLoremIpsum"`)
+ })
+ })
+ })
+ t.Run("Delete", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"list": "1"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"list": "999"})
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ // Owned by user3
+ _, err := testHandler.testDelete(nil, map[string]string{"list": "2"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"list": "6"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"list": "7"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"list": "8"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"list": "9"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"list": "10"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"list": "11"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"list": "12"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"list": "13"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"list": "14"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"list": "15"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"list": "16"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"list": "17"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"message":"Successfully deleted."`)
+ })
+ })
+ })
+ t.Run("Create", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ // Check the list was loaded successfully after update, see testReadOne
+ rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
+ assert.Contains(t, rec.Body.String(), `"description":""`)
+ assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
+ assert.Contains(t, rec.Body.String(), `"tasks":null`)
+ })
+ t.Run("Normal with description", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"Lorem","description":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
+ assert.Contains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
+ assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
+ assert.Contains(t, rec.Body.String(), `"tasks":null`)
+ })
+ t.Run("Nonexisting Namespace", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"namespace": "999999"}, `{"title":"Lorem"}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeNamespaceDoesNotExist)
+ })
+ t.Run("Empty title", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":""}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message.(models.ValidationHTTPError).InvalidFields, "title: non zero value required")
+ })
+ t.Run("Almost empty title", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"namespace": "1"}, `{"title":"nn"}`)
+ assert.Error(t, err)
+ 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) {
+ _, 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"}`)
+ assert.Error(t, err)
+ 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("Forbidden", func(t *testing.T) {
+ // Owned by user3
+ _, err := testHandler.testCreate(nil, map[string]string{"namespace": "3"}, `{"title":"Lorem"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"namespace": "7"}, `{"title":"Lorem"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "8"}, `{"title":"Lorem"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
+ assert.Contains(t, rec.Body.String(), `"description":""`)
+ assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
+ assert.Contains(t, rec.Body.String(), `"tasks":null`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "9"}, `{"title":"Lorem"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
+ assert.Contains(t, rec.Body.String(), `"description":""`)
+ assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
+ assert.Contains(t, rec.Body.String(), `"tasks":null`)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"namespace": "10"}, `{"title":"Lorem"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "11"}, `{"title":"Lorem"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
+ assert.Contains(t, rec.Body.String(), `"description":""`)
+ assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
+ assert.Contains(t, rec.Body.String(), `"tasks":null`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"namespace": "12"}, `{"title":"Lorem"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"title":"Lorem"`)
+ assert.Contains(t, rec.Body.String(), `"description":""`)
+ assert.Contains(t, rec.Body.String(), `"owner":{"id":1`)
+ assert.Contains(t, rec.Body.String(), `"tasks":null`)
+ })
+ })
+ })
+}
diff --git a/pkg/integrations/login_test.go b/pkg/integrations/login_test.go
new file mode 100644
index 00000000..ef0758a7
--- /dev/null
+++ b/pkg/integrations/login_test.go
@@ -0,0 +1,65 @@
+// 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 .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/models"
+ apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "testing"
+)
+
+func TestLogin(t *testing.T) {
+ t.Run("Normal login", func(t *testing.T) {
+ rec, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{
+ "username": "user1",
+ "password": "1234"
+}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), "token")
+ })
+ t.Run("Empty payload", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeNoUsernamePassword)
+ })
+ t.Run("Not existing user", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{
+ "username": "userWichDoesNotExist",
+ "password": "1234"
+}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeWrongUsernameOrPassword)
+ })
+ t.Run("Wrong password", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{
+ "username": "user1",
+ "password": "wrong"
+}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeWrongUsernameOrPassword)
+ })
+ t.Run("user with unconfirmed email", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.Login, `{
+ "username": "user5",
+ "password": "1234"
+}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeEmailNotConfirmed)
+ })
+}
diff --git a/pkg/integrations/register_test.go b/pkg/integrations/register_test.go
new file mode 100644
index 00000000..2bdb37e8
--- /dev/null
+++ b/pkg/integrations/register_test.go
@@ -0,0 +1,87 @@
+// 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 .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/models"
+ apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "testing"
+)
+
+func TestRegister(t *testing.T) {
+ t.Run("normal register", func(t *testing.T) {
+ rec, err := newTestRequest(t, http.MethodPost, apiv1.RegisterUser, `{
+ "username": "newUser",
+ "password": "1234",
+ "email": "email@example.com"
+}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"username":"newUser"`)
+ })
+ t.Run("Empty payload", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.RegisterUser, `{}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeNoUsernamePassword)
+ })
+ t.Run("Empty username", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.RegisterUser, `{
+ "username": "",
+ "password": "1234",
+ "email": "email@example.com"
+}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeNoUsernamePassword)
+ })
+ t.Run("Empty password", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.RegisterUser, `{
+ "username": "newUser",
+ "password": "",
+ "email": "email@example.com"
+}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeNoUsernamePassword)
+ })
+ t.Run("Empty email", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.RegisterUser, `{
+ "username": "newUser",
+ "password": "1234",
+ "email": ""
+}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeNoUsernamePassword)
+ })
+ t.Run("Already existing username", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.RegisterUser, `{
+ "username": "user1",
+ "password": "1234",
+ "email": "email@example.com"
+}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrorCodeUsernameExists)
+ })
+ t.Run("Already existing email", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.RegisterUser, `{
+ "username": "newUser",
+ "password": "1234",
+ "email": "user1@example.com"
+}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrorCodeUserEmailExists)
+ })
+}
diff --git a/pkg/integrations/task_test.go b/pkg/integrations/task_test.go
new file mode 100644
index 00000000..6dbe927d
--- /dev/null
+++ b/pkg/integrations/task_test.go
@@ -0,0 +1,562 @@
+// 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 .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/models"
+ "code.vikunja.io/web/handler"
+ "github.com/labstack/echo"
+ "github.com/stretchr/testify/assert"
+ "net/url"
+ "testing"
+)
+
+func TestListTask(t *testing.T) {
+ testHandler := webHandlerTest{
+ user: &testuser1,
+ strFunc: func() handler.CObject {
+ return &models.ListTask{}
+ },
+ t: t,
+ }
+ // Only run specific nested tests:
+ // ^TestListTask$/^Update$/^Update_task_items$/^Removing_Assignees_null$
+ t.Run("ReadAll", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(nil, nil)
+ assert.NoError(t, err)
+ // 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 #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`)
+ // TODO: add more tasks, since the whole point of this is to get all tasks in all lists where the user
+ // has at least read access
+ })
+ t.Run("Search", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"s": []string{"task #6"}}, nil)
+ assert.NoError(t, err)
+ assert.NotContains(t, rec.Body.String(), `task #1`)
+ 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.Contains(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`)
+ })
+ t.Run("Sort Order", func(t *testing.T) {
+ // should equal priority desc
+ t.Run("by priority", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priority"}}, nil)
+ assert.NoError(t, err)
+ assert.Contains(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,"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`)
+ })
+ t.Run("by priority desc", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"sort": []string{"prioritydesc"}}, nil)
+ assert.NoError(t, err)
+ assert.Contains(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,"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`)
+ })
+ t.Run("by priority asc", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"sort": []string{"priorityasc"}}, nil)
+ assert.NoError(t, err)
+ assert.Contains(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,"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}}]`)
+ })
+ // should equal duedate desc
+ t.Run("by duedate", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"sort": []string{"dueadate"}}, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
+ })
+ t.Run("by duedate desc", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"sort": []string{"dueadatedesc"}}, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `[{"id":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
+ })
+ t.Run("by duedate asc", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"sort": []string{"duedateasc"}}, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"dueDate":1543616724,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
+ })
+ t.Run("invalid parameter", func(t *testing.T) {
+ // Invalid parameter should not sort at all
+ rec, err := testHandler.testReadAll(url.Values{"sort": []string{"loremipsum"}}, nil)
+ 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,"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,"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":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":6,"text":"task #6 lower due date"`)
+ assert.NotContains(t, rec.Body.String(), `{"id":6,"text":"task #6 lower due date","description":"","done":false,"dueDate":1543616724,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}},{"id":5,"text":"task #5 higher due date","description":"","done":false,"dueDate":1543636724,"reminderDates":null,"repeatAfter":0,"parentTaskID":0,"priority":0,"startDate":0,"endDate":0,"assignees":null,"labels":null,"subtasks":null,"created":1543626724,"updated":1543626724,"createdBy":{"id":0,"username":"","email":"","created":0,"updated":0}}]`)
+ })
+ })
+ t.Run("Date range", 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)
+ assert.NoError(t, err)
+ assert.NotContains(t, rec.Body.String(), `task #1`)
+ 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.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.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`)
+ })
+ t.Run("start date only", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"startdate": []string{"1540000000"}}, nil)
+ assert.NoError(t, err)
+ assert.NotContains(t, rec.Body.String(), `task #1`)
+ 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.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.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`)
+ })
+ t.Run("end date only", func(t *testing.T) {
+ rec, err := testHandler.testReadAll(url.Values{"enddate": []string{"1544700001"}}, nil)
+ assert.NoError(t, err)
+ // 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 >
+ // the current date.
+ assert.Equal(t, "null\n", rec.Body.String())
+ })
+ })
+ })
+ t.Run("Update", func(t *testing.T) {
+ t.Run("Update task items", func(t *testing.T) {
+ t.Run("Text", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ assert.NotContains(t, rec.Body.String(), `"text":"task #1"`)
+ })
+ t.Run("Description", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"description":"Dolor sit amet"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"description":"Dolor sit amet"`)
+ assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
+ })
+ t.Run("Description to empty", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"description":""}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"description":""`)
+ assert.NotContains(t, rec.Body.String(), `"description":"Lorem Ipsum"`)
+ })
+ t.Run("Done", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"done":true}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"done":true`)
+ assert.NotContains(t, rec.Body.String(), `"done":false`)
+ })
+ t.Run("Undone", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "2"}, `{"done":false}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"done":false`)
+ assert.NotContains(t, rec.Body.String(), `"done":true`)
+ })
+ t.Run("Due date", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"dueDate": 123456}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"dueDate":123456`)
+ assert.NotContains(t, rec.Body.String(), `"dueDate":0`)
+ })
+ t.Run("Due date unset", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "5"}, `{"dueDate": 0}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"dueDate":0`)
+ assert.NotContains(t, rec.Body.String(), `"dueDate":1543636724`)
+ })
+ t.Run("Reminders", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"reminderDates": [1555508227,1555511000]}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"reminderDates":[1555508227,1555511000]`)
+ assert.NotContains(t, rec.Body.String(), `"reminderDates": null`)
+ })
+ t.Run("Reminders unset to empty array", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "27"}, `{"reminderDates": []}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"reminderDates":null`)
+ assert.NotContains(t, rec.Body.String(), `"reminderDates":[1543626724,1543626824]`)
+ })
+ t.Run("Reminders unset to null", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "27"}, `{"reminderDates": null}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"reminderDates":null`)
+ assert.NotContains(t, rec.Body.String(), `"reminderDates":[1543626724,1543626824]`)
+ })
+ t.Run("Repeat after", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"repeatAfter":3600}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"repeatAfter":3600`)
+ assert.NotContains(t, rec.Body.String(), `"repeatAfter":0`)
+ })
+ t.Run("Repeat after unset", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "28"}, `{"repeatAfter":0}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"repeatAfter":0`)
+ assert.NotContains(t, rec.Body.String(), `"repeatAfter":3600`)
+ })
+ t.Run("Repeat after update done", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "28"}, `{"done":true}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"done":false`)
+ assert.NotContains(t, rec.Body.String(), `"done":true`)
+ })
+ t.Run("Parent task", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"parentTaskID":2}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"parentTaskID":2`)
+ assert.NotContains(t, rec.Body.String(), `"parentTaskID":0`)
+ })
+ t.Run("Parent task same task", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"parentTaskID":1}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeParentTaskCannotBeTheSame)
+ })
+ t.Run("Parent task unset", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "29"}, `{"parentTaskID":0}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"parentTaskID":0`)
+ assert.NotContains(t, rec.Body.String(), `"parentTaskID":1`)
+ })
+ t.Run("Assignees", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"assignees":[{"id":1}]}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"assignees":[{"id":1`)
+ assert.NotContains(t, rec.Body.String(), `"assignees":[]`)
+ })
+ t.Run("Removing Assignees empty array", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "30"}, `{"assignees":[]}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"assignees":null`)
+ assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`)
+ })
+ t.Run("Removing Assignees null", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "30"}, `{"assignees":null}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"assignees":null`)
+ assert.NotContains(t, rec.Body.String(), `"assignees":[{"id":1`)
+ })
+ t.Run("Priority", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"priority":100}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"priority":100`)
+ assert.NotContains(t, rec.Body.String(), `"priority":0`)
+ })
+ t.Run("Priority to 0", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "3"}, `{"priority":0}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"priority":0`)
+ assert.NotContains(t, rec.Body.String(), `"priority":100`)
+ })
+ t.Run("Start date", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"startDate":1234567}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"startDate":1234567`)
+ assert.NotContains(t, rec.Body.String(), `"startDate":0`)
+ })
+ t.Run("Start date unset", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "7"}, `{"startDate":0}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"startDate":0`)
+ assert.NotContains(t, rec.Body.String(), `"startDate":1544600000`)
+ })
+ t.Run("End date", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "1"}, `{"endDate":123456}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"endDate":123456`)
+ assert.NotContains(t, rec.Body.String(), `"endDate":0`)
+ })
+ t.Run("End date unset", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "8"}, `{"endDate":0}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"endDate":0`)
+ assert.NotContains(t, rec.Body.String(), `"endDate":1544700000`)
+ })
+ })
+
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "99999"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeListTaskDoesNotExist)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "14"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "15"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "16"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "17"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "18"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "19"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "20"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "21"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "22"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "23"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testUpdate(nil, map[string]string{"listtask": "24"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "25"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testUpdate(nil, map[string]string{"listtask": "26"}, `{"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("Normal", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "1"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"listtask": "99999"})
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeListTaskDoesNotExist)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"listtask": "14"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"listtask": "15"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "16"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "17"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"listtask": "18"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "19"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "20"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"listtask": "21"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "22"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "23"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testDelete(nil, map[string]string{"listtask": "24"})
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "25"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testDelete(nil, map[string]string{"listtask": "26"})
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Successfully deleted.`)
+ })
+ })
+ })
+ t.Run("Create", func(t *testing.T) {
+ t.Run("Normal", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"list": "1"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+ t.Run("Nonexisting", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"list": "9999"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeListDoesNotExist)
+ })
+ t.Run("Rights check", func(t *testing.T) {
+ t.Run("Forbidden", func(t *testing.T) {
+ // Owned by user3
+ _, err := testHandler.testCreate(nil, map[string]string{"list": "2"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team readonly", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"list": "6"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via Team write", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"list": "7"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via Team admin", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"list": "8"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+
+ t.Run("Shared Via User readonly", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"list": "9"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via User write", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"list": "10"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via User admin", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"list": "11"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+
+ t.Run("Shared Via NamespaceTeam readonly", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"list": "12"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceTeam write", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"list": "13"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via NamespaceTeam admin", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"list": "14"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+
+ t.Run("Shared Via NamespaceUser readonly", func(t *testing.T) {
+ _, err := testHandler.testCreate(nil, map[string]string{"list": "15"}, `{"text":"Lorem Ipsum"}`)
+ assert.Error(t, err)
+ assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
+ })
+ t.Run("Shared Via NamespaceUser write", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"list": "16"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+ t.Run("Shared Via NamespaceUser admin", func(t *testing.T) {
+ rec, err := testHandler.testCreate(nil, map[string]string{"list": "17"}, `{"text":"Lorem Ipsum"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"text":"Lorem Ipsum"`)
+ })
+ })
+ })
+}
diff --git a/pkg/integrations/token_test.go b/pkg/integrations/token_test.go
new file mode 100644
index 00000000..e30559a1
--- /dev/null
+++ b/pkg/integrations/token_test.go
@@ -0,0 +1,32 @@
+// 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 .
+
+package integrations
+
+import (
+ apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "testing"
+)
+
+func TestCheckToken(t *testing.T) {
+ t.Run("Normal test", func(t *testing.T) {
+ rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.CheckToken, &testuser1, "", nil, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `🍵`)
+ })
+}
diff --git a/pkg/integrations/user_change_password_test.go b/pkg/integrations/user_change_password_test.go
new file mode 100644
index 00000000..ac80c4c4
--- /dev/null
+++ b/pkg/integrations/user_change_password_test.go
@@ -0,0 +1,60 @@
+// 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 .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/models"
+ apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "testing"
+)
+
+func TestUserChangePassword(t *testing.T) {
+ t.Run("Normal test", func(t *testing.T) {
+ rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserChangePassword, &testuser1, `{
+ "new_password": "12345",
+ "old_password": "1234"
+}`, nil, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `The password was updated successfully.`)
+ })
+ t.Run("Wrong old password", func(t *testing.T) {
+ _, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserChangePassword, &testuser1, `{
+ "new_password": "12345",
+ "old_password": "invalid"
+}`, nil, nil)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeWrongUsernameOrPassword)
+ })
+ t.Run("Empty old password", func(t *testing.T) {
+ _, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserChangePassword, &testuser1, `{
+ "new_password": "12345",
+ "old_password": ""
+}`, nil, nil)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeEmptyOldPassword)
+ })
+ t.Run("Empty new password", func(t *testing.T) {
+ _, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserChangePassword, &testuser1, `{
+ "new_password": "",
+ "old_password": "1234"
+}`, nil, nil)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeEmptyNewPassword)
+ })
+}
diff --git a/pkg/integrations/user_confirm_email_test.go b/pkg/integrations/user_confirm_email_test.go
new file mode 100644
index 00000000..57b40781
--- /dev/null
+++ b/pkg/integrations/user_confirm_email_test.go
@@ -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 .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/models"
+ apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
+ "github.com/labstack/echo"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "testing"
+)
+
+func TestUserConfirmEmail(t *testing.T) {
+ t.Run("Normal test", func(t *testing.T) {
+ rec, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{"token": "tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `The email was confirmed successfully.`)
+ })
+ t.Run("Empty payload", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{}`)
+ assert.Error(t, err)
+ assert.Equal(t, http.StatusPreconditionFailed, err.(*echo.HTTPError).Code)
+ assertHandlerErrorCode(t, err, models.ErrCodeInvalidEmailConfirmToken)
+ })
+ t.Run("Empty token", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{"token": ""}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeInvalidEmailConfirmToken)
+ })
+ t.Run("Invalid token", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.UserConfirmEmail, `{"token": "invalidToken"}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeInvalidEmailConfirmToken)
+ })
+}
diff --git a/pkg/integrations/user_list_test.go b/pkg/integrations/user_list_test.go
new file mode 100644
index 00000000..02c9e9ea
--- /dev/null
+++ b/pkg/integrations/user_list_test.go
@@ -0,0 +1,45 @@
+// 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 .
+
+package integrations
+
+import (
+ apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "testing"
+)
+
+func TestUserList(t *testing.T) {
+ t.Run("Normal test", func(t *testing.T) {
+ rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserList, &testuser1, "", nil, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `user1`)
+ assert.Contains(t, rec.Body.String(), `user2`)
+ assert.Contains(t, rec.Body.String(), `user3`)
+ assert.Contains(t, rec.Body.String(), `user4`)
+ assert.Contains(t, rec.Body.String(), `user5`)
+ })
+ t.Run("Search for user3", func(t *testing.T) {
+ rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserList, &testuser1, "", map[string][]string{"s": {"user3"}}, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `user3`)
+ assert.NotContains(t, rec.Body.String(), `user1`)
+ assert.NotContains(t, rec.Body.String(), `user2`)
+ assert.NotContains(t, rec.Body.String(), `user4`)
+ assert.NotContains(t, rec.Body.String(), `user5`)
+ })
+}
diff --git a/pkg/integrations/user_password_request_token_test.go b/pkg/integrations/user_password_request_token_test.go
new file mode 100644
index 00000000..9d91064e
--- /dev/null
+++ b/pkg/integrations/user_password_request_token_test.go
@@ -0,0 +1,49 @@
+// 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 .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/models"
+ apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
+ "github.com/labstack/echo"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "testing"
+)
+
+func TestUserRequestResetPasswordToken(t *testing.T) {
+ t.Run("Normal requesting a password reset token", func(t *testing.T) {
+ rec, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{"email": "user1@example.com"}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `Token was sent.`)
+ })
+ t.Run("Empty payload", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeNoUsernamePassword)
+ })
+ t.Run("Invalid email address", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{"email": "user1example.com"}`)
+ assert.Error(t, err)
+ assert.Equal(t, http.StatusBadRequest, err.(*echo.HTTPError).Code)
+ })
+ t.Run("No user with that email address", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.UserRequestResetPasswordToken, `{"email": "user1000@example.com"}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeUserDoesNotExist)
+ })
+}
diff --git a/pkg/integrations/user_password_reset_test.go b/pkg/integrations/user_password_reset_test.go
new file mode 100644
index 00000000..5c3f9d6b
--- /dev/null
+++ b/pkg/integrations/user_password_reset_test.go
@@ -0,0 +1,58 @@
+// 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 .
+
+package integrations
+
+import (
+ "code.vikunja.io/api/pkg/models"
+ apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
+ "github.com/labstack/echo"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "testing"
+)
+
+func TestUserPasswordReset(t *testing.T) {
+ t.Run("Normal password reset test", func(t *testing.T) {
+ rec, err := newTestRequest(t, http.MethodPost, apiv1.UserResetPassword, `{
+ "new_password": "1234",
+ "token": "passwordresettesttoken"
+}`)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `The password was updated successfully.`)
+ })
+ t.Run("Empty payload", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.UserResetPassword, `{}`)
+ assert.Error(t, err)
+ assert.Equal(t, http.StatusBadRequest, err.(*echo.HTTPError).Code)
+ })
+ t.Run("No new password", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.UserResetPassword, `{
+ "new_password": "",
+ "token": "passwordresettesttoken"
+}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeNoUsernamePassword)
+ })
+ t.Run("Invalid password reset token", func(t *testing.T) {
+ _, err := newTestRequest(t, http.MethodPost, apiv1.UserResetPassword, `{
+ "new_password": "1234",
+ "token": "invalidtoken"
+}`)
+ assert.Error(t, err)
+ assertHandlerErrorCode(t, err, models.ErrCodeInvalidPasswordResetToken)
+ })
+}
diff --git a/pkg/integrations/user_show_test.go b/pkg/integrations/user_show_test.go
new file mode 100644
index 00000000..8afb6fd2
--- /dev/null
+++ b/pkg/integrations/user_show_test.go
@@ -0,0 +1,34 @@
+// 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 .
+
+package integrations
+
+import (
+ apiv1 "code.vikunja.io/api/pkg/routes/api/v1"
+ "github.com/stretchr/testify/assert"
+ "net/http"
+ "testing"
+)
+
+func TestUserShow(t *testing.T) {
+ t.Run("Normal test", func(t *testing.T) {
+ rec, err := newTestRequestWithUser(t, http.MethodPost, apiv1.UserShow, &testuser1, "", nil, nil)
+ assert.NoError(t, err)
+ assert.Contains(t, rec.Body.String(), `"id":1`)
+ assert.Contains(t, rec.Body.String(), `"username":"user1"`)
+ assert.Contains(t, rec.Body.String(), `"email":""`)
+ })
+}
diff --git a/pkg/models/error.go b/pkg/models/error.go
index b1ebbdb7..ab973ff4 100644
--- a/pkg/models/error.go
+++ b/pkg/models/error.go
@@ -247,6 +247,48 @@ func IsErrEmailNotConfirmed(err error) bool {
return ok
}
+// ErrEmptyNewPassword represents a "EmptyNewPassword" kind of error.
+type ErrEmptyNewPassword struct{}
+
+// IsErrEmptyNewPassword checks if an error is a ErrEmptyNewPassword.
+func IsErrEmptyNewPassword(err error) bool {
+ _, ok := err.(ErrEmptyNewPassword)
+ return ok
+}
+
+func (err ErrEmptyNewPassword) Error() string {
+ return fmt.Sprintf("New password is empty")
+}
+
+// ErrCodeEmptyNewPassword holds the unique world-error code of this error
+const ErrCodeEmptyNewPassword = 1013
+
+// HTTPError holds the http error description
+func (err ErrEmptyNewPassword) HTTPError() web.HTTPError {
+ return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeEmptyNewPassword, Message: "Please specify new password."}
+}
+
+// ErrEmptyOldPassword represents a "EmptyOldPassword" kind of error.
+type ErrEmptyOldPassword struct{}
+
+// IsErrEmptyOldPassword checks if an error is a ErrEmptyOldPassword.
+func IsErrEmptyOldPassword(err error) bool {
+ _, ok := err.(ErrEmptyOldPassword)
+ return ok
+}
+
+func (err ErrEmptyOldPassword) Error() string {
+ return fmt.Sprintf("Old password is empty")
+}
+
+// ErrCodeEmptyOldPassword holds the unique world-error code of this error
+const ErrCodeEmptyOldPassword = 1014
+
+// HTTPError holds the http error description
+func (err ErrEmptyOldPassword) HTTPError() web.HTTPError {
+ return web.HTTPError{HTTPCode: http.StatusPreconditionFailed, Code: ErrCodeEmptyOldPassword, Message: "Please specify old password."}
+}
+
// ===================
// Empty things errors
// ===================
@@ -502,6 +544,33 @@ func (err ErrNoRightToSeeTask) HTTPError() web.HTTPError {
}
}
+// ErrParentTaskCannotBeTheSame represents an error where the user tries to set a tasks parent as the same
+type ErrParentTaskCannotBeTheSame struct {
+ TaskID int64
+}
+
+// IsErrParentTaskCannotBeTheSame checks if an error is ErrParentTaskCannotBeTheSame.
+func IsErrParentTaskCannotBeTheSame(err error) bool {
+ _, ok := err.(ErrParentTaskCannotBeTheSame)
+ return ok
+}
+
+func (err ErrParentTaskCannotBeTheSame) Error() string {
+ return fmt.Sprintf("Tried to set a parents task as the same [TaskID: %v]", err.TaskID)
+}
+
+// ErrCodeParentTaskCannotBeTheSame holds the unique world-error code of this error
+const ErrCodeParentTaskCannotBeTheSame = 4006
+
+// HTTPError holds the http error description
+func (err ErrParentTaskCannotBeTheSame) HTTPError() web.HTTPError {
+ return web.HTTPError{
+ HTTPCode: http.StatusForbidden,
+ Code: ErrCodeParentTaskCannotBeTheSame,
+ Message: "You cannot set a parent task to the task itself.",
+ }
+}
+
// =================
// Namespace errors
// =================
diff --git a/pkg/models/fixtures/list.yml b/pkg/models/fixtures/list.yml
index 5a987759..7c08b792 100644
--- a/pkg/models/fixtures/list.yml
+++ b/pkg/models/fixtures/list.yml
@@ -38,3 +38,99 @@
namespace_id: 5
updated: 0
created: 0
+-
+ id: 6
+ title: Test6
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 6
+ updated: 0
+ created: 0
+-
+ id: 7
+ title: Test7
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 6
+ updated: 0
+ created: 0
+-
+ id: 8
+ title: Test8
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 6
+ updated: 0
+ created: 0
+-
+ id: 9
+ title: Test9
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 6
+ updated: 0
+ created: 0
+-
+ id: 10
+ title: Test10
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 6
+ updated: 0
+ created: 0
+-
+ id: 11
+ title: Test11
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 6
+ updated: 0
+ created: 0
+-
+ id: 12
+ title: Test12
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 7
+ updated: 0
+ created: 0
+-
+ id: 13
+ title: Test13
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 8
+ updated: 0
+ created: 0
+-
+ id: 14
+ title: Test14
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 9
+ updated: 0
+ created: 0
+-
+ id: 15
+ title: Test15
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 10
+ updated: 0
+ created: 0
+-
+ id: 16
+ title: Test16
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 11
+ updated: 0
+ created: 0
+-
+ id: 17
+ title: Test17
+ description: Lorem Ipsum
+ owner_id: 6
+ namespace_id: 12
+ updated: 0
+ created: 0
diff --git a/pkg/models/fixtures/namespaces.yml b/pkg/models/fixtures/namespaces.yml
index e1a087c4..a746a841 100644
--- a/pkg/models/fixtures/namespaces.yml
+++ b/pkg/models/fixtures/namespaces.yml
@@ -1,21 +1,60 @@
--
- id: 1
+- id: 1
name: testnamespace
description: Lorem Ipsum
owner_id: 1
updated: 0
created: 0
--
- id: 2
+- id: 2
name: testnamespace2
description: Lorem Ipsum
owner_id: 2
updated: 0
created: 0
--
- id: 3
+- id: 3
name: testnamespace3
description: Lorem Ipsum
owner_id: 3
updated: 0
created: 0
+- id: 6
+ name: testnamespace6
+ description: Lorem Ipsum
+ owner_id: 6
+ updated: 0
+ created: 0
+- id: 7
+ name: testnamespace7
+ description: Lorem Ipsum
+ owner_id: 6
+ updated: 0
+ created: 0
+- id: 8
+ name: testnamespace8
+ description: Lorem Ipsum
+ owner_id: 6
+ updated: 0
+ created: 0
+- id: 9
+ name: testnamespace9
+ description: Lorem Ipsum
+ owner_id: 6
+ updated: 0
+ created: 0
+- id: 10
+ name: testnamespace10
+ description: Lorem Ipsum
+ owner_id: 6
+ updated: 0
+ created: 0
+- id: 11
+ name: testnamespace11
+ description: Lorem Ipsum
+ owner_id: 6
+ updated: 0
+ created: 0
+- id: 12
+ name: testnamespace12
+ description: Lorem Ipsum
+ owner_id: 6
+ updated: 0
+ created: 0
diff --git a/pkg/models/fixtures/task_assignees.yml b/pkg/models/fixtures/task_assignees.yml
new file mode 100644
index 00000000..9104a48c
--- /dev/null
+++ b/pkg/models/fixtures/task_assignees.yml
@@ -0,0 +1,8 @@
+- id: 1
+ task_id: 30
+ user_id: 1
+ created: 0
+- id: 2
+ task_id: 30
+ user_id: 2
+ created: 0
\ No newline at end of file
diff --git a/pkg/models/fixtures/tasks.yml b/pkg/models/fixtures/tasks.yml
index 7ab818c5..7efd1093 100644
--- a/pkg/models/fixtures/tasks.yml
+++ b/pkg/models/fixtures/tasks.yml
@@ -1,5 +1,6 @@
- id: 1
text: 'task #1'
+ description: 'Lorem Ipsum'
created_by_id: 1
list_id: 1
created: 1543626724
@@ -90,4 +91,105 @@
created_by_id: 5
list_id: 5
created: 1543626724
- updated: 1543626724
\ No newline at end of file
+ updated: 1543626724
+- id: 15
+ text: 'task #15'
+ created_by_id: 6
+ list_id: 6
+ created: 1543626724
+ updated: 1543626724
+- id: 16
+ text: 'task #16'
+ created_by_id: 6
+ list_id: 7
+ created: 1543626724
+ updated: 1543626724
+- id: 17
+ text: 'task #17'
+ created_by_id: 6
+ list_id: 8
+ created: 1543626724
+ updated: 1543626724
+- id: 18
+ text: 'task #18'
+ created_by_id: 6
+ list_id: 9
+ created: 1543626724
+ updated: 1543626724
+- id: 19
+ text: 'task #19'
+ created_by_id: 6
+ list_id: 10
+ created: 1543626724
+ updated: 1543626724
+- id: 20
+ text: 'task #20'
+ created_by_id: 6
+ list_id: 11
+ created: 1543626724
+ updated: 1543626724
+- id: 21
+ text: 'task #21'
+ created_by_id: 6
+ list_id: 12
+ created: 1543626724
+ updated: 1543626724
+- id: 22
+ text: 'task #22'
+ created_by_id: 6
+ list_id: 13
+ created: 1543626724
+ updated: 1543626724
+- id: 23
+ text: 'task #23'
+ created_by_id: 6
+ list_id: 14
+ created: 1543626724
+ updated: 1543626724
+- id: 24
+ text: 'task #24'
+ created_by_id: 6
+ list_id: 15
+ created: 1543626724
+ updated: 1543626724
+- id: 25
+ text: 'task #25'
+ created_by_id: 6
+ list_id: 16
+ created: 1543626724
+ updated: 1543626724
+- id: 26
+ text: 'task #26'
+ created_by_id: 6
+ list_id: 17
+ created: 1543626724
+ updated: 1543626724
+- id: 27
+ text: 'task #27 with reminders'
+ created_by_id: 1
+ reminders_unix: '[1543626724,1543626824]'
+ list_id: 1
+ created: 1543626724
+ updated: 1543626724
+- id: 28
+ text: 'task #28 with repeat after'
+ done: false
+ created_by_id: 1
+ repeat_after: 3600
+ list_id: 1
+ created: 1543626724
+ updated: 1543626724
+- id: 29
+ text: 'task #29 with parent task (1)'
+ created_by_id: 1
+ parent_task_id: 1
+ list_id: 1
+ created: 1543626724
+ updated: 1543626724
+- id: 30
+ text: 'task #30 with assignees'
+ created_by_id: 1
+ list_id: 1
+ created: 1543626724
+ updated: 1543626724
+
diff --git a/pkg/models/fixtures/team_list.yml b/pkg/models/fixtures/team_list.yml
index fd9ba938..6852db9f 100644
--- a/pkg/models/fixtures/team_list.yml
+++ b/pkg/models/fixtures/team_list.yml
@@ -1,10 +1,30 @@
- id: 1
team_id: 1
list_id: 3
+ right: 0
updated: 0
created: 0
+
+# This team has read only access on list 6
- id: 2
team_id: 2
- list_id: 3
+ list_id: 6
+ right: 0
updated: 0
created: 0
+
+# This team has write access on list 7
+- id: 3
+ team_id: 3
+ list_id: 7
+ right: 1
+ updated: 0
+ created: 0
+
+# This team has admin access on list 8
+- id: 4
+ team_id: 4
+ list_id: 8
+ right: 2
+ updated: 0
+ created: 0
\ No newline at end of file
diff --git a/pkg/models/fixtures/team_members.yml b/pkg/models/fixtures/team_members.yml
index cb5fe042..bbcecb19 100644
--- a/pkg/models/fixtures/team_members.yml
+++ b/pkg/models/fixtures/team_members.yml
@@ -7,3 +7,27 @@
team_id: 1
user_id: 2
created: 0
+-
+ team_id: 2
+ user_id: 1
+ created: 0
+-
+ team_id: 3
+ user_id: 1
+ created: 0
+-
+ team_id: 4
+ user_id: 1
+ created: 0
+-
+ team_id: 5
+ user_id: 1
+ created: 0
+-
+ team_id: 6
+ user_id: 1
+ created: 0
+-
+ team_id: 7
+ user_id: 1
+ created: 0
diff --git a/pkg/models/fixtures/team_namespaces.yml b/pkg/models/fixtures/team_namespaces.yml
index 6b264c09..4b206b9b 100644
--- a/pkg/models/fixtures/team_namespaces.yml
+++ b/pkg/models/fixtures/team_namespaces.yml
@@ -1,10 +1,34 @@
- id: 1
team_id: 1
namespace_id: 3
+ right: 0
updated: 0
created: 0
+
- id: 2
team_id: 2
namespace_id: 3
+ right: 0
+ updated: 0
+ created: 0
+
+- id: 3
+ team_id: 5
+ namespace_id: 7
+ right: 0
+ updated: 0
+ created: 0
+
+- id: 4
+ team_id: 6
+ namespace_id: 8
+ right: 1
+ updated: 0
+ created: 0
+
+- id: 5
+ team_id: 7
+ namespace_id: 9
+ right: 2
updated: 0
created: 0
diff --git a/pkg/models/fixtures/teams.yml b/pkg/models/fixtures/teams.yml
index dab8f913..3852100b 100644
--- a/pkg/models/fixtures/teams.yml
+++ b/pkg/models/fixtures/teams.yml
@@ -1,5 +1,22 @@
--
- id: 1
+- id: 1
name: testteam1
description: Lorem Ipsum
+ created_by_id: 1
+- id: 2
+ name: testteam2_read_only_on_list6
+ created_by_id: 1
+- id: 3
+ name: testteam3_write_on_list7
+ created_by_id: 1
+- id: 4
+ name: testteam4_admin_on_list8
+ created_by_id: 1
+- id: 5
+ name: testteam2_read_only_on_namespace7
+ created_by_id: 1
+- id: 6
+ name: testteam3_write_on_namespace8
+ created_by_id: 1
+- id: 7
+ name: testteam4_admin_on_namespace9
created_by_id: 1
\ No newline at end of file
diff --git a/pkg/models/fixtures/users.yml b/pkg/models/fixtures/users.yml
index e50b116b..22bba861 100644
--- a/pkg/models/fixtures/users.yml
+++ b/pkg/models/fixtures/users.yml
@@ -1,28 +1,30 @@
-
id: 1
username: 'user1'
- password: '1234'
+ password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user1@example.com'
+ is_active: true
updated: 0
created: 0
-
id: 2
username: 'user2'
- password: '1234'
+ password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user2@example.com'
updated: 0
created: 0
-
id: 3
username: 'user3'
- password: '1234'
+ password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user3@example.com'
+ password_reset_token: passwordresettesttoken
updated: 0
created: 0
-
id: 4
username: 'user4'
- password: '1234'
+ password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
email: 'user4@example.com'
email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael
updated: 0
@@ -30,9 +32,17 @@
-
id: 5
username: 'user5'
- password: '1234'
- email: 'user4@example.com'
+ password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
+ email: 'user5@example.com'
email_confirm_token: tiepiQueed8ahc7zeeFe1eveiy4Ein8osooxegiephauph2Ael
is_active: false
updated: 0
created: 0
+# This use is used to create a whole bunch of lists which are then shared directly with a user
+- id: 6
+ username: 'user6'
+ password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.' # 1234
+ email: 'user6@example.com'
+ is_active: true
+ updated: 0
+ created: 0
diff --git a/pkg/models/fixtures/users_list.yml b/pkg/models/fixtures/users_list.yml
index 8f01206e..4464e6e5 100644
--- a/pkg/models/fixtures/users_list.yml
+++ b/pkg/models/fixtures/users_list.yml
@@ -1,10 +1,34 @@
- id: 1
user_id: 1
list_id: 3
+ right: 0
updated: 0
created: 0
+
- id: 2
user_id: 2
list_id: 3
+ right: 0
+ updated: 0
+ created: 0
+
+- id: 3
+ user_id: 1
+ list_id: 9
+ right: 0
+ updated: 0
+ created: 0
+
+- id: 4
+ user_id: 1
+ list_id: 10
+ right: 1
+ updated: 0
+ created: 0
+
+- id: 5
+ user_id: 1
+ list_id: 11
+ right: 2
updated: 0
created: 0
diff --git a/pkg/models/fixtures/users_namespace.yml b/pkg/models/fixtures/users_namespace.yml
index 1b092875..e12681dd 100644
--- a/pkg/models/fixtures/users_namespace.yml
+++ b/pkg/models/fixtures/users_namespace.yml
@@ -1,10 +1,34 @@
- id: 1
user_id: 1
namespace_id: 3
+ right: 0
updated: 0
created: 0
+
- id: 2
user_id: 2
namespace_id: 3
+ right: 0
+ updated: 0
+ created: 0
+
+- id: 3
+ user_id: 1
+ namespace_id: 10
+ right: 0
+ updated: 0
+ created: 0
+
+- id: 4
+ user_id: 1
+ namespace_id: 11
+ right: 1
+ updated: 0
+ created: 0
+
+- id: 5
+ user_id: 1
+ namespace_id: 12
+ right: 2
updated: 0
created: 0
diff --git a/pkg/models/label_task_test.go b/pkg/models/label_task_test.go
index f4d857a4..fbd0d1f8 100644
--- a/pkg/models/label_task_test.go
+++ b/pkg/models/label_task_test.go
@@ -1,6 +1,7 @@
package models
import (
+ "gopkg.in/d4l3k/messagediff.v1"
"reflect"
"runtime"
"testing"
@@ -48,7 +49,7 @@ func TestLabelTask_ReadAll(t *testing.T) {
CreatedBy: &User{
ID: 2,
Username: "user2",
- Password: "1234",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
},
},
},
@@ -95,8 +96,8 @@ func TestLabelTask_ReadAll(t *testing.T) {
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("LabelTask.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
- if !reflect.DeepEqual(gotLabels, tt.wantLabels) {
- t.Errorf("LabelTask.ReadAll() = %v, want %v", gotLabels, tt.wantLabels)
+ if diff, equal := messagediff.PrettyDiff(gotLabels, tt.wantLabels); !equal {
+ t.Errorf("LabelTask.ReadAll() = %v, want %v, diff: %v", l, tt.wantLabels, diff)
}
})
}
diff --git a/pkg/models/label_test.go b/pkg/models/label_test.go
index 008e345a..41b72c88 100644
--- a/pkg/models/label_test.go
+++ b/pkg/models/label_test.go
@@ -17,6 +17,7 @@
package models
import (
+ "gopkg.in/d4l3k/messagediff.v1"
"reflect"
"runtime"
"testing"
@@ -45,7 +46,8 @@ func TestLabel_ReadAll(t *testing.T) {
user1 := &User{
ID: 1,
Username: "user1",
- Password: "1234",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
+ IsActive: true,
}
tests := []struct {
name string
@@ -85,7 +87,7 @@ func TestLabel_ReadAll(t *testing.T) {
CreatedBy: &User{
ID: 2,
Username: "user2",
- Password: "1234",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
},
},
},
@@ -115,8 +117,8 @@ func TestLabel_ReadAll(t *testing.T) {
t.Errorf("Label.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
return
}
- if !reflect.DeepEqual(gotLs, tt.wantLs) {
- t.Errorf("Label.ReadAll() = %v, want %v", gotLs, tt.wantLs)
+ if diff, equal := messagediff.PrettyDiff(gotLs, tt.wantLs); !equal {
+ t.Errorf("Label.ReadAll() = %v, want %v, diff: %v", gotLs, tt.wantLs, diff)
}
})
}
@@ -138,7 +140,8 @@ func TestLabel_ReadOne(t *testing.T) {
user1 := &User{
ID: 1,
Username: "user1",
- Password: "1234",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
+ IsActive: true,
}
tests := []struct {
name string
@@ -192,7 +195,7 @@ func TestLabel_ReadOne(t *testing.T) {
CreatedBy: &User{
ID: 2,
Username: "user2",
- Password: "1234",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
},
},
auth: &User{ID: 1},
@@ -224,8 +227,8 @@ func TestLabel_ReadOne(t *testing.T) {
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("Label.ReadOne() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
- if !reflect.DeepEqual(l, tt.want) && !tt.wantErr && !tt.wantForbidden {
- t.Errorf("Label.ReadOne() = %v, want %v", l, tt.want)
+ if diff, equal := messagediff.PrettyDiff(l, tt.want); !equal && !tt.wantErr && !tt.wantForbidden {
+ t.Errorf("Label.ReadAll() = %v, want %v, diff: %v", l, tt.want, diff)
}
})
}
diff --git a/pkg/models/list_create_test.go b/pkg/models/list_create_test.go
index b3e24f08..95520804 100644
--- a/pkg/models/list_create_test.go
+++ b/pkg/models/list_create_test.go
@@ -23,7 +23,7 @@ import (
func TestList_Create(t *testing.T) {
// Create test database
- //assert.NoError(t, PrepareTestDatabase())
+ //assert.NoError(t, LoadFixtures())
// Get our doer
doer, err := GetUserByID(1)
@@ -78,17 +78,6 @@ func TestList_Create(t *testing.T) {
assert.Error(t, err)
assert.True(t, IsErrListDoesNotExist(err))
- // Delete a nonexistant list
- err = dummylist.Delete()
- assert.Error(t, err)
- assert.True(t, IsErrListDoesNotExist(err))
-
- // Check failing with no title
- list2 := List{}
- err = list2.Create(&doer)
- assert.Error(t, err)
- assert.True(t, IsErrListTitleCannotBeEmpty(err))
-
// Check creation with a nonexistant namespace
list3 := List{
Title: "test",
diff --git a/pkg/models/list_create_update.go b/pkg/models/list_create_update.go
index bbdf6c24..bfe3bf42 100644
--- a/pkg/models/list_create_update.go
+++ b/pkg/models/list_create_update.go
@@ -24,11 +24,6 @@ import (
// CreateOrUpdateList updates a list or creates it if it doesn't exist
func CreateOrUpdateList(list *List) (err error) {
- // Check we have at least a title
- if list.Title == "" {
- return ErrListTitleCannotBeEmpty{}
- }
-
// Check if the namespace exists
if list.NamespaceID != 0 {
_, err = GetNamespaceByID(list.NamespaceID)
@@ -73,11 +68,6 @@ func CreateOrUpdateList(list *List) (err error) {
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [post]
func (l *List) Update() (err error) {
- // Check if it exists
- lorig := List{ID: l.ID}
- if err = lorig.GetSimpleByID(); err != nil {
- return
- }
return CreateOrUpdateList(l)
}
diff --git a/pkg/models/list_delete.go b/pkg/models/list_delete.go
index 7dbeba26..69538af7 100644
--- a/pkg/models/list_delete.go
+++ b/pkg/models/list_delete.go
@@ -34,10 +34,6 @@ import (
// @Failure 500 {object} models.Message "Internal error"
// @Router /lists/{id} [delete]
func (l *List) Delete() (err error) {
- // Check if the list exists
- if err = l.GetSimpleByID(); err != nil {
- return
- }
// Delete the list
_, err = x.ID(l.ID).Delete(&List{})
diff --git a/pkg/models/list_read_test.go b/pkg/models/list_read_test.go
index 823ebbde..c504e1a2 100644
--- a/pkg/models/list_read_test.go
+++ b/pkg/models/list_read_test.go
@@ -24,7 +24,7 @@ import (
func TestList_ReadAll(t *testing.T) {
// Create test database
- //assert.NoError(t, PrepareTestDatabase())
+ //assert.NoError(t, LoadFixtures())
// Get all lists for our namespace
lists, err := GetListsByNamespaceID(1, &User{})
@@ -40,7 +40,7 @@ func TestList_ReadAll(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(lists3).Kind(), reflect.Slice)
s := reflect.ValueOf(lists3)
- assert.Equal(t, s.Len(), 3)
+ assert.Equal(t, 15, s.Len())
// Try getting lists for a nonexistant user
_, err = lists2.ReadAll("", &User{ID: 984234}, 1)
diff --git a/pkg/models/list_task_assignees.go b/pkg/models/list_task_assignees.go
index 3cef448d..fef70b22 100644
--- a/pkg/models/list_task_assignees.go
+++ b/pkg/models/list_task_assignees.go
@@ -59,6 +59,7 @@ func (t *ListTask) updateTaskAssignees(assignees []*User) (err error) {
if len(assignees) == 0 && len(t.Assignees) > 0 {
_, err = x.Where("task_id = ?", t.ID).
Delete(ListTaskAssginee{})
+ t.setTaskAssignees(assignees)
return err
}
@@ -123,9 +124,19 @@ func (t *ListTask) updateTaskAssignees(assignees []*User) (err error) {
}
}
+ t.setTaskAssignees(assignees)
return
}
+// Small helper functions to set the new assignees in various places
+func (t *ListTask) setTaskAssignees(assignees []*User) {
+ if len(assignees) == 0 {
+ t.Assignees = nil
+ return
+ }
+ t.Assignees = assignees
+}
+
// Delete a task assignee
// @Summary Delete an assignee
// @Description Un-assign a user from a task.
diff --git a/pkg/models/list_task_readall.go b/pkg/models/list_task_readall.go
index 91373ae5..9807aad8 100644
--- a/pkg/models/list_task_readall.go
+++ b/pkg/models/list_task_readall.go
@@ -32,8 +32,8 @@ const (
// @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 tasks by task text."
// @Param sort query string false "The sorting parameter. Possible values to sort by are priority, prioritydesc, priorityasc, dueadate, dueadatedesc, dueadateasc."
-// @Param startdate query int false "The start date parameter to filter by. Expects a unix timestamp."
-// @Param enddate query int false "The end date parameter to filter by. Expects a unix timestamp."
+// @Param startdate query int false "The start date parameter to filter by. Expects a unix timestamp. If no end date, but a start date is specified, the end date is set to the current time."
+// @Param enddate query int false "The end date parameter to filter by. Expects a unix timestamp. If no start date, but an end date is specified, the start date is set to the current time."
// @Security JWTKeyAuth
// @Success 200 {array} models.List "The tasks"
// @Failure 500 {object} models.Message "Internal error"
diff --git a/pkg/models/list_task_readall_test.go b/pkg/models/list_task_readall_test.go
index 75431e9b..ca3bcb3e 100644
--- a/pkg/models/list_task_readall_test.go
+++ b/pkg/models/list_task_readall_test.go
@@ -7,9 +7,8 @@
package models
import (
- "fmt"
"github.com/stretchr/testify/assert"
- "reflect"
+ "gopkg.in/d4l3k/messagediff.v1"
"sort"
"testing"
@@ -21,6 +20,7 @@ func sortTasksForTesting(by SortBy) (tasks []*ListTask) {
{
ID: 1,
Text: "task #1",
+ Description: "Lorem Ipsum",
CreatedByID: 1,
ListID: 1,
Created: 1543626724,
@@ -123,6 +123,128 @@ func sortTasksForTesting(by SortBy) (tasks []*ListTask) {
Created: 1543626724,
Updated: 1543626724,
},
+ {
+ ID: 15,
+ Text: "task #15",
+ CreatedByID: 6,
+ ListID: 6,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 16,
+ Text: "task #16",
+ CreatedByID: 6,
+ ListID: 7,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 17,
+ Text: "task #17",
+ CreatedByID: 6,
+ ListID: 8,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 18,
+ Text: "task #18",
+ CreatedByID: 6,
+ ListID: 9,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 19,
+ Text: "task #19",
+ CreatedByID: 6,
+ ListID: 10,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 20,
+ Text: "task #20",
+ CreatedByID: 6,
+ ListID: 11,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 21,
+ Text: "task #21",
+ CreatedByID: 6,
+ ListID: 12,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 22,
+ Text: "task #22",
+ CreatedByID: 6,
+ ListID: 13,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 23,
+ Text: "task #23",
+ CreatedByID: 6,
+ ListID: 14,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 24,
+ Text: "task #24",
+ CreatedByID: 6,
+ ListID: 15,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 25,
+ Text: "task #25",
+ CreatedByID: 6,
+ ListID: 16,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 26,
+ Text: "task #26",
+ CreatedByID: 6,
+ ListID: 17,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 27,
+ Text: "task #27 with reminders",
+ CreatedByID: 1,
+ RemindersUnix: []int64{1543626724, 1543626824},
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 28,
+ Text: "task #28 with repeat after",
+ CreatedByID: 1,
+ ListID: 1,
+ RepeatAfter: 3600,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 30,
+ Text: "task #30 with assignees",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
}
switch by {
@@ -138,6 +260,10 @@ func sortTasksForTesting(by SortBy) (tasks []*ListTask) {
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].DueDateUnix > tasks[j].DueDateUnix
})
+ // Swap since sqlite seems to sort differently
+ tmp := tasks[5]
+ tasks[5] = tasks[3]
+ tasks[3] = tmp
case SortTasksByDueDateAsc:
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].DueDateUnix < tasks[j].DueDateUnix
@@ -148,7 +274,7 @@ func sortTasksForTesting(by SortBy) (tasks []*ListTask) {
}
func TestListTask_ReadAll(t *testing.T) {
- assert.NoError(t, PrepareTestDatabase())
+ assert.NoError(t, LoadFixtures())
type fields struct {
ID int64
Text string
@@ -221,6 +347,7 @@ func TestListTask_ReadAll(t *testing.T) {
{
ID: 1,
Text: "task #1",
+ Description: "Lorem Ipsum",
CreatedByID: 1,
ListID: 1,
Created: 1543626724,
@@ -304,6 +431,127 @@ func TestListTask_ReadAll(t *testing.T) {
ListID: 1,
Created: 1543626724,
Updated: 1543626724,
+ }, {
+ ID: 15,
+ Text: "task #15",
+ CreatedByID: 6,
+ ListID: 6,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 16,
+ Text: "task #16",
+ CreatedByID: 6,
+ ListID: 7,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 17,
+ Text: "task #17",
+ CreatedByID: 6,
+ ListID: 8,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 18,
+ Text: "task #18",
+ CreatedByID: 6,
+ ListID: 9,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 19,
+ Text: "task #19",
+ CreatedByID: 6,
+ ListID: 10,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 20,
+ Text: "task #20",
+ CreatedByID: 6,
+ ListID: 11,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 21,
+ Text: "task #21",
+ CreatedByID: 6,
+ ListID: 12,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 22,
+ Text: "task #22",
+ CreatedByID: 6,
+ ListID: 13,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 23,
+ Text: "task #23",
+ CreatedByID: 6,
+ ListID: 14,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 24,
+ Text: "task #24",
+ CreatedByID: 6,
+ ListID: 15,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 25,
+ Text: "task #25",
+ CreatedByID: 6,
+ ListID: 16,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 26,
+ Text: "task #26",
+ CreatedByID: 6,
+ ListID: 17,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 27,
+ Text: "task #27 with reminders",
+ CreatedByID: 1,
+ RemindersUnix: []int64{1543626724, 1543626824},
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 28,
+ Text: "task #28 with repeat after",
+ CreatedByID: 1,
+ ListID: 1,
+ RepeatAfter: 3600,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 30,
+ Text: "task #30 with assignees",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
},
{
ID: 4,
@@ -349,23 +597,29 @@ func TestListTask_ReadAll(t *testing.T) {
a: &User{ID: 1},
page: 0,
},
- want: sortTasksForTesting(SortTasksByDueDateDesc),
- wantErr: false,
- },
- {
- name: "ReadAll ListTasks sorted by due date asc",
- fields: fields{
- Sorting: "duedateasc",
- },
- args: args{
- search: "",
- a: &User{ID: 1},
- page: 0,
- },
want: []*ListTask{
+ {
+ ID: 5,
+ Text: "task #5 higher due date",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ DueDateUnix: 1543636724,
+ },
+ {
+ ID: 6,
+ Text: "task #6 lower due date",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ DueDateUnix: 1543616724,
+ },
{
ID: 1,
Text: "task #1",
+ Description: "Lorem Ipsum",
CreatedByID: 1,
ListID: 1,
Created: 1543626724,
@@ -450,6 +704,352 @@ func TestListTask_ReadAll(t *testing.T) {
Created: 1543626724,
Updated: 1543626724,
},
+ {
+ ID: 15,
+ Text: "task #15",
+ CreatedByID: 6,
+ ListID: 6,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 16,
+ Text: "task #16",
+ CreatedByID: 6,
+ ListID: 7,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 17,
+ Text: "task #17",
+ CreatedByID: 6,
+ ListID: 8,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 18,
+ Text: "task #18",
+ CreatedByID: 6,
+ ListID: 9,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 19,
+ Text: "task #19",
+ CreatedByID: 6,
+ ListID: 10,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 20,
+ Text: "task #20",
+ CreatedByID: 6,
+ ListID: 11,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 21,
+ Text: "task #21",
+ CreatedByID: 6,
+ ListID: 12,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 22,
+ Text: "task #22",
+ CreatedByID: 6,
+ ListID: 13,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 23,
+ Text: "task #23",
+ CreatedByID: 6,
+ ListID: 14,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 24,
+ Text: "task #24",
+ CreatedByID: 6,
+ ListID: 15,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 25,
+ Text: "task #25",
+ CreatedByID: 6,
+ ListID: 16,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 26,
+ Text: "task #26",
+ CreatedByID: 6,
+ ListID: 17,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 27,
+ Text: "task #27 with reminders",
+ CreatedByID: 1,
+ RemindersUnix: []int64{1543626724, 1543626824},
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 28,
+ Text: "task #28 with repeat after",
+ CreatedByID: 1,
+ ListID: 1,
+ RepeatAfter: 3600,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 30,
+ Text: "task #30 with assignees",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ },
+ wantErr: false,
+ },
+ {
+ name: "ReadAll ListTasks sorted by due date asc",
+ fields: fields{
+ Sorting: "duedateasc",
+ },
+ args: args{
+ search: "",
+ a: &User{ID: 1},
+ page: 0,
+ },
+ want: []*ListTask{
+ {
+ ID: 1,
+ Text: "task #1",
+ Description: "Lorem Ipsum",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 2,
+ Text: "task #2 done",
+ Done: true,
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 3,
+ Text: "task #3 high prio",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ Priority: 100,
+ },
+ {
+ ID: 4,
+ Text: "task #4 low prio",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ Priority: 1,
+ },
+ {
+ ID: 7,
+ Text: "task #7 with start date",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ StartDateUnix: 1544600000,
+ },
+ {
+ ID: 8,
+ Text: "task #8 with end date",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ EndDateUnix: 1544700000,
+ },
+ {
+ ID: 9,
+ Text: "task #9 with start and end date",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ StartDateUnix: 1544600000,
+ EndDateUnix: 1544700000,
+ },
+ {
+ ID: 10,
+ Text: "task #10 basic",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 11,
+ Text: "task #11 basic",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 12,
+ Text: "task #12 basic",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 15,
+ Text: "task #15",
+ CreatedByID: 6,
+ ListID: 6,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 16,
+ Text: "task #16",
+ CreatedByID: 6,
+ ListID: 7,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 17,
+ Text: "task #17",
+ CreatedByID: 6,
+ ListID: 8,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 18,
+ Text: "task #18",
+ CreatedByID: 6,
+ ListID: 9,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 19,
+ Text: "task #19",
+ CreatedByID: 6,
+ ListID: 10,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 20,
+ Text: "task #20",
+ CreatedByID: 6,
+ ListID: 11,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 21,
+ Text: "task #21",
+ CreatedByID: 6,
+ ListID: 12,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 22,
+ Text: "task #22",
+ CreatedByID: 6,
+ ListID: 13,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 23,
+ Text: "task #23",
+ CreatedByID: 6,
+ ListID: 14,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 24,
+ Text: "task #24",
+ CreatedByID: 6,
+ ListID: 15,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 25,
+ Text: "task #25",
+ CreatedByID: 6,
+ ListID: 16,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 26,
+ Text: "task #26",
+ CreatedByID: 6,
+ ListID: 17,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 27,
+ Text: "task #27 with reminders",
+ CreatedByID: 1,
+ RemindersUnix: []int64{1543626724, 1543626824},
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 28,
+ Text: "task #28 with repeat after",
+ CreatedByID: 1,
+ ListID: 1,
+ RepeatAfter: 3600,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
+ {
+ ID: 30,
+ Text: "task #30 with assignees",
+ CreatedByID: 1,
+ ListID: 1,
+ Created: 1543626724,
+ Updated: 1543626724,
+ },
{
ID: 6,
Text: "task #6 lower due date",
@@ -481,6 +1081,7 @@ func TestListTask_ReadAll(t *testing.T) {
a: &User{ID: 1},
page: 0,
},
+
want: sortTasksForTesting(SortTasksByDueDateDesc),
wantErr: false,
},
@@ -612,25 +1213,11 @@ func TestListTask_ReadAll(t *testing.T) {
}
got, err := lt.ReadAll(tt.args.search, tt.args.a, tt.args.page)
if (err != nil) != tt.wantErr {
- t.Errorf("ListTask.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
+ t.Errorf("Test %s, ListTask.ReadAll() error = %v, wantErr %v", tt.name, err, tt.wantErr)
return
}
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("ListTask.ReadAll() = %v, want %v", got, tt.want)
- fmt.Println("Got:")
- gotslice := got.([]*ListTask)
- for _, g := range gotslice {
- fmt.Println(g.Text)
- //fmt.Println(g.StartDateUnix)
- //fmt.Println(g.EndDateUnix)
- }
- fmt.Println("Want:")
- wantslice := tt.want.([]*ListTask)
- for _, w := range wantslice {
- fmt.Println(w.Text)
- //fmt.Println(w.StartDateUnix)
- //fmt.Println(w.EndDateUnix)
- }
+ if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
+ t.Errorf("Test %s, LabelTask.ReadAll() = %v, want %v, diff: %v", tt.name, got, tt.want, diff)
}
})
}
diff --git a/pkg/models/list_tasks_create_update.go b/pkg/models/list_tasks_create_update.go
index 1703f580..f3e8578d 100644
--- a/pkg/models/list_tasks_create_update.go
+++ b/pkg/models/list_tasks_create_update.go
@@ -96,6 +96,11 @@ func (t *ListTask) Update() (err error) {
return
}
+ // Parent task cannot be the same as the current task
+ if t.ID == t.ParentTaskID {
+ return ErrParentTaskCannotBeTheSame{TaskID: t.ID}
+ }
+
// When a repeating task is marked as done, we update all deadlines and reminders and set it as undone
updateDone(&ot, t)
@@ -128,15 +133,46 @@ func (t *ListTask) Update() (err error) {
return err
}
- // And because a false is considered to be a null value, we need to explicitly check that case here.
+ //////
+ // Mergo does ignore nil values. Because of that, we need to check all parameters and set the updated to
+ // nil/their nil value in the struct which is inserted.
+ ////
+ // Done
if !t.Done {
ot.Done = false
}
-
- // If the priority is 0, we also need to explicitly check that here
+ // Priority
if t.Priority == 0 {
ot.Priority = 0
}
+ // Description
+ if t.Description == "" {
+ ot.Description = ""
+ }
+ // Due date
+ if t.DueDateUnix == 0 {
+ ot.DueDateUnix = 0
+ }
+ // Reminders
+ if len(t.RemindersUnix) == 0 {
+ ot.RemindersUnix = nil
+ }
+ // Repeat after
+ if t.RepeatAfter == 0 {
+ ot.RepeatAfter = 0
+ }
+ // Parent task
+ if t.ParentTaskID == 0 {
+ ot.ParentTaskID = 0
+ }
+ // Start date
+ if t.StartDateUnix == 0 {
+ ot.StartDateUnix = 0
+ }
+ // End date
+ if t.EndDateUnix == 0 {
+ ot.EndDateUnix = 0
+ }
_, err = x.ID(t.ID).
Cols("text",
diff --git a/pkg/models/list_tasks_rights.go b/pkg/models/list_tasks_rights.go
index cf00fb26..b0bf1ba2 100644
--- a/pkg/models/list_tasks_rights.go
+++ b/pkg/models/list_tasks_rights.go
@@ -41,6 +41,7 @@ func (t *ListTask) CanCreate(a web.Auth) (bool, error) {
// CanRead determines if a user can read a task
func (t *ListTask) CanRead(a web.Auth) (canRead bool, err error) {
+ //return t.canDoListTask(a)
// Get the task, error out if it doesn't exist
*t, err = getTaskByIDSimple(t.ID)
if err != nil {
diff --git a/pkg/models/list_tasks_test.go b/pkg/models/list_tasks_test.go
index b13ea09b..7d87070a 100644
--- a/pkg/models/list_tasks_test.go
+++ b/pkg/models/list_tasks_test.go
@@ -22,7 +22,7 @@ import (
)
func TestListTask_Create(t *testing.T) {
- //assert.NoError(t, PrepareTestDatabase())
+ //assert.NoError(t, LoadFixtures())
// Fake list task
listtask := ListTask{
diff --git a/pkg/models/list_users.go b/pkg/models/list_users.go
index 820fac13..4f35f41a 100644
--- a/pkg/models/list_users.go
+++ b/pkg/models/list_users.go
@@ -27,7 +27,7 @@ type ListUser struct {
// The list id.
ListID int64 `xorm:"int(11) not null INDEX" json:"-" param:"list"`
// The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
- Right Right `xorm:"int(11) INDEX null" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
+ Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A unix timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"`
diff --git a/pkg/models/list_users_test.go b/pkg/models/list_users_test.go
index ca7059a8..79684ead 100644
--- a/pkg/models/list_users_test.go
+++ b/pkg/models/list_users_test.go
@@ -17,6 +17,7 @@
package models
import (
+ "gopkg.in/d4l3k/messagediff.v1"
"reflect"
"runtime"
"testing"
@@ -159,7 +160,8 @@ func TestListUser_ReadAll(t *testing.T) {
User: User{
ID: 1,
Username: "user1",
- Password: "1234",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
+ IsActive: true,
},
Right: RightRead,
},
@@ -167,7 +169,7 @@ func TestListUser_ReadAll(t *testing.T) {
User: User{
ID: 2,
Username: "user2",
- Password: "1234",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
},
Right: RightRead,
},
@@ -204,8 +206,8 @@ func TestListUser_ReadAll(t *testing.T) {
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("ListUser.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("ListUser.ReadAll() = %v, want %v", got, tt.want)
+ if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
+ t.Errorf("ListUser.ReadAll() = %v, want %v, diff: %v", got, tt.want, diff)
}
})
}
diff --git a/pkg/models/namespace_test.go b/pkg/models/namespace_test.go
index b3d0be6b..74ae224e 100644
--- a/pkg/models/namespace_test.go
+++ b/pkg/models/namespace_test.go
@@ -24,7 +24,7 @@ import (
func TestNamespace_Create(t *testing.T) {
// Create test database
- //assert.NoError(t, PrepareTestDatabase())
+ //assert.NoError(t, LoadFixtures())
// Dummy namespace
dummynamespace := Namespace{
@@ -122,5 +122,5 @@ func TestNamespace_Create(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(nsps).Kind(), reflect.Slice)
s := reflect.ValueOf(nsps)
- assert.Equal(t, 3, s.Len())
+ assert.Equal(t, 9, s.Len())
}
diff --git a/pkg/models/namespace_users.go b/pkg/models/namespace_users.go
index 7a926a0f..f07e2da7 100644
--- a/pkg/models/namespace_users.go
+++ b/pkg/models/namespace_users.go
@@ -27,7 +27,7 @@ type NamespaceUser struct {
// The namespace id
NamespaceID int64 `xorm:"int(11) not null INDEX" json:"-" param:"namespace"`
// The right this user has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
- Right Right `xorm:"int(11) INDEX null" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
+ Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A unix timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"`
diff --git a/pkg/models/namespace_users_test.go b/pkg/models/namespace_users_test.go
index 492f076d..d241a118 100644
--- a/pkg/models/namespace_users_test.go
+++ b/pkg/models/namespace_users_test.go
@@ -20,6 +20,7 @@ package models
import (
"code.vikunja.io/web"
+ "gopkg.in/d4l3k/messagediff.v1"
"reflect"
"runtime"
"testing"
@@ -160,7 +161,8 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
User: User{
ID: 1,
Username: "user1",
- Password: "1234",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
+ IsActive: true,
},
Right: RightRead,
},
@@ -168,7 +170,7 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
User: User{
ID: 2,
Username: "user2",
- Password: "1234",
+ Password: "$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.",
},
Right: RightRead,
},
@@ -206,8 +208,8 @@ func TestNamespaceUser_ReadAll(t *testing.T) {
if (err != nil) && tt.wantErr && !tt.errType(err) {
t.Errorf("NamespaceUser.ReadAll() Wrong error type! Error = %v, want = %v", err, runtime.FuncForPC(reflect.ValueOf(tt.errType).Pointer()).Name())
}
- if !reflect.DeepEqual(got, tt.want) {
- t.Errorf("NamespaceUser.ReadAll() = %v, want %v", got, tt.want)
+ if diff, equal := messagediff.PrettyDiff(got, tt.want); !equal {
+ t.Errorf("NamespaceUser.ReadAll() = %v, want %v, diff: %v", got, tt.want, diff)
}
})
}
diff --git a/pkg/models/team_list.go b/pkg/models/team_list.go
index 21b3f69a..106e893d 100644
--- a/pkg/models/team_list.go
+++ b/pkg/models/team_list.go
@@ -27,7 +27,7 @@ type TeamList struct {
// The list id.
ListID int64 `xorm:"int(11) not null INDEX" json:"-" param:"list"`
// The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
- Right Right `xorm:"int(11) INDEX null" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
+ Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A unix timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"`
diff --git a/pkg/models/team_list_test.go b/pkg/models/team_list_test.go
index dec28529..16d0f6d3 100644
--- a/pkg/models/team_list_test.go
+++ b/pkg/models/team_list_test.go
@@ -88,7 +88,7 @@ func TestTeamList(t *testing.T) {
// Test read all for a list where the user not has access
tl6 := tl
- tl6.ListID = 4
+ tl6.ListID = 5
_, err = tl6.ReadAll("", &u, 1)
assert.Error(t, err)
assert.True(t, IsErrNeedToHaveListReadAccess(err))
diff --git a/pkg/models/team_namespace.go b/pkg/models/team_namespace.go
index ea25619f..659f12c6 100644
--- a/pkg/models/team_namespace.go
+++ b/pkg/models/team_namespace.go
@@ -27,7 +27,7 @@ type TeamNamespace struct {
// The namespace id.
NamespaceID int64 `xorm:"int(11) not null INDEX" json:"-" param:"namespace"`
// The right this team has. 0 = Read only, 1 = Read & Write, 2 = Admin. See the docs for more details.
- Right Right `xorm:"int(11) INDEX null" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
+ Right Right `xorm:"int(11) INDEX not null default 0" json:"right" valid:"length(0|2)" maximum:"2" default:"0"`
// A unix timestamp when this relation was created. You cannot change this value.
Created int64 `xorm:"created not null" json:"created"`
diff --git a/pkg/models/teams_test.go b/pkg/models/teams_test.go
index b5881756..c02096d1 100644
--- a/pkg/models/teams_test.go
+++ b/pkg/models/teams_test.go
@@ -59,7 +59,7 @@ func TestTeam_Create(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, reflect.TypeOf(ts).Kind(), reflect.Slice)
s := reflect.ValueOf(ts)
- assert.Equal(t, 2, s.Len())
+ assert.Equal(t, 8, s.Len())
// Check inserting it with an empty name
dummyteam.Name = ""
diff --git a/pkg/models/unit_tests.go b/pkg/models/unit_tests.go
index 00270d6e..01621457 100644
--- a/pkg/models/unit_tests.go
+++ b/pkg/models/unit_tests.go
@@ -33,6 +33,13 @@ import (
// MainTest creates the test engine
func MainTest(m *testing.M, pathToRoot string) {
+ SetupTests(pathToRoot)
+ os.Exit(m.Run())
+}
+
+// SetupTests takes care of seting up the db, fixtures etc.
+// This is an extra function to be able to call the fixtures setup from the integration tests.
+func SetupTests(pathToRoot string) {
var err error
fixturesDir := filepath.Join(pathToRoot, "pkg", "models", "fixtures")
if err = createTestEngine(fixturesDir); err != nil {
@@ -43,11 +50,9 @@ func MainTest(m *testing.M, pathToRoot string) {
mail.StartMailDaemon()
// Create test database
- if err = PrepareTestDatabase(); err != nil {
+ if err = LoadFixtures(); err != nil {
log.Log.Fatalf("Error preparing test database: %v", err.Error())
}
-
- os.Exit(m.Run())
}
func createTestEngine(fixturesDir string) error {
@@ -90,8 +95,3 @@ func createTestEngine(fixturesDir string) error {
return InitFixtures(fixturesHelper, fixturesDir)
}
-
-// PrepareTestDatabase load test fixtures into test database
-func PrepareTestDatabase() error {
- return LoadFixtures()
-}
diff --git a/pkg/models/user_add_update.go b/pkg/models/user_add_update.go
index 0490b803..578771fd 100644
--- a/pkg/models/user_add_update.go
+++ b/pkg/models/user_add_update.go
@@ -30,7 +30,7 @@ func CreateUser(user User) (newUser User, err error) {
newUser = user
// Check if we have all needed informations
- if newUser.Password == "" || newUser.Username == "" {
+ if newUser.Password == "" || newUser.Username == "" || newUser.Email == "" {
return User{}, ErrNoUsernamePassword{}
}
@@ -154,6 +154,10 @@ func UpdateUser(user User) (updatedUser User, err error) {
// UpdateUserPassword updates the password of a user
func UpdateUserPassword(user *User, newPassword string) (err error) {
+ if newPassword == "" {
+ return ErrEmptyNewPassword{}
+ }
+
// Get all user details
theUser, err := GetUserByID(user.ID)
if err != nil {
diff --git a/pkg/models/user_password_reset.go b/pkg/models/user_password_reset.go
index 2e5ba353..3f351d4a 100644
--- a/pkg/models/user_password_reset.go
+++ b/pkg/models/user_password_reset.go
@@ -83,6 +83,10 @@ type PasswordTokenRequest struct {
// RequestUserPasswordResetToken inserts a random token to reset a users password into the databsse
func RequestUserPasswordResetToken(tr *PasswordTokenRequest) (err error) {
+ if tr.Email == "" {
+ return ErrNoUsernamePassword{}
+ }
+
// Check if the user exists
user, err := GetUser(User{Email: tr.Email})
if err != nil {
diff --git a/pkg/models/user_test.go b/pkg/models/user_test.go
index 730da45e..b530f5b7 100644
--- a/pkg/models/user_test.go
+++ b/pkg/models/user_test.go
@@ -24,7 +24,7 @@ import (
func TestCreateUser(t *testing.T) {
// Create test database
- //assert.NoError(t, PrepareTestDatabase())
+ //assert.NoError(t, LoadFixtures())
// Get our doer
doer, err := GetUserByID(1)
@@ -50,7 +50,7 @@ func TestCreateUser(t *testing.T) {
assert.Error(t, err)
// Check if it fails to create a user with just the same username
- _, err = CreateUser(User{Username: dummyuser.Username, Password: "fsdf"})
+ _, err = CreateUser(User{Username: dummyuser.Username, Password: "12345", Email: "email@example.com"})
assert.Error(t, err)
assert.True(t, IsErrUsernameExists(err))
diff --git a/pkg/routes/api/v1/login.go b/pkg/routes/api/v1/login.go
index 01ca6fcf..04465f3c 100644
--- a/pkg/routes/api/v1/login.go
+++ b/pkg/routes/api/v1/login.go
@@ -57,10 +57,20 @@ func Login(c echo.Context) error {
}
// Create token
- token := jwt.New(jwt.SigningMethodHS256)
+ t, err := CreateNewJWTTokenForUser(&user)
+ if err != nil {
+ return err
+ }
+
+ 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 := token.Claims.(jwt.MapClaims)
+ claims := t.Claims.(jwt.MapClaims)
claims["username"] = user.Username
claims["email"] = user.Email
claims["id"] = user.ID
@@ -70,10 +80,5 @@ func Login(c echo.Context) error {
claims["avatar"] = hex.EncodeToString(avatar[:])
// Generate encoded token and send it as response.
- t, err := token.SignedString([]byte(viper.GetString("service.JWTSecret")))
- if err != nil {
- return err
- }
-
- return c.JSON(http.StatusOK, Token{Token: t})
+ return t.SignedString([]byte(viper.GetString("service.JWTSecret")))
}
diff --git a/pkg/routes/api/v1/user_update_password.go b/pkg/routes/api/v1/user_update_password.go
index 3b736405..ff839751 100644
--- a/pkg/routes/api/v1/user_update_password.go
+++ b/pkg/routes/api/v1/user_update_password.go
@@ -55,6 +55,10 @@ func UserChangePassword(c echo.Context) error {
return echo.NewHTTPError(http.StatusBadRequest, "No password provided.")
}
+ if newPW.OldPassword == "" {
+ return handler.HandleHTTPError(models.ErrEmptyOldPassword{}, c)
+ }
+
// Check the current password
if _, err = models.CheckUserCredentials(&models.UserLogin{Username: doer.Username, Password: newPW.OldPassword}); err != nil {
return handler.HandleHTTPError(err, c)
diff --git a/vendor/gopkg.in/d4l3k/messagediff.v1/.coveralls.yml b/vendor/gopkg.in/d4l3k/messagediff.v1/.coveralls.yml
new file mode 100644
index 00000000..036d0707
--- /dev/null
+++ b/vendor/gopkg.in/d4l3k/messagediff.v1/.coveralls.yml
@@ -0,0 +1 @@
+repo_token: LWIe7rP7M3hBnAxpsMaZhrVBs2DSyhzoQ
diff --git a/vendor/gopkg.in/d4l3k/messagediff.v1/.gitignore b/vendor/gopkg.in/d4l3k/messagediff.v1/.gitignore
new file mode 100644
index 00000000..daf913b1
--- /dev/null
+++ b/vendor/gopkg.in/d4l3k/messagediff.v1/.gitignore
@@ -0,0 +1,24 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
diff --git a/vendor/gopkg.in/d4l3k/messagediff.v1/.travis.yml b/vendor/gopkg.in/d4l3k/messagediff.v1/.travis.yml
new file mode 100644
index 00000000..f8639351
--- /dev/null
+++ b/vendor/gopkg.in/d4l3k/messagediff.v1/.travis.yml
@@ -0,0 +1,26 @@
+language: go
+
+os:
+ - linux
+
+go:
+ - 1
+ - 1.3
+ - 1.4
+ - 1.5
+ - 1.6
+ - 1.7.x
+ - 1.8.x
+ - tip
+
+before_install:
+ - if ! go get github.com/golang/tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi
+ - go get github.com/axw/gocov/gocov
+ - go get github.com/modocache/gover
+ - go get github.com/mattn/goveralls
+
+script:
+ - go test -v -coverprofile=example.coverprofile ./example
+ - go test -v -coverprofile=main.coverprofile
+ - $HOME/gopath/bin/gover
+ - $HOME/gopath/bin/goveralls -service=travis-ci -coverprofile=gover.coverprofile
diff --git a/vendor/gopkg.in/d4l3k/messagediff.v1/CHANGELOG.md b/vendor/gopkg.in/d4l3k/messagediff.v1/CHANGELOG.md
new file mode 100644
index 00000000..4090fd10
--- /dev/null
+++ b/vendor/gopkg.in/d4l3k/messagediff.v1/CHANGELOG.md
@@ -0,0 +1,14 @@
+## nightly
+* Added support for ignoring fields.
+
+## v1.1.0
+
+* Added support for recursive data structures.
+* Fixed bug with embedded fixed length arrays in structs.
+* Added `example/` directory.
+* Minor test bug fixes for future go versions.
+* Added change log.
+
+## v1.0.0
+
+Initial tagged release release.
diff --git a/vendor/gopkg.in/d4l3k/messagediff.v1/LICENSE b/vendor/gopkg.in/d4l3k/messagediff.v1/LICENSE
new file mode 100644
index 00000000..0074f6ae
--- /dev/null
+++ b/vendor/gopkg.in/d4l3k/messagediff.v1/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Tristan Rice
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/vendor/gopkg.in/d4l3k/messagediff.v1/README.md b/vendor/gopkg.in/d4l3k/messagediff.v1/README.md
new file mode 100644
index 00000000..147c34ad
--- /dev/null
+++ b/vendor/gopkg.in/d4l3k/messagediff.v1/README.md
@@ -0,0 +1,90 @@
+# messagediff [![Build Status](https://travis-ci.org/d4l3k/messagediff.svg?branch=master)](https://travis-ci.org/d4l3k/messagediff) [![Coverage Status](https://coveralls.io/repos/github/d4l3k/messagediff/badge.svg?branch=master)](https://coveralls.io/github/d4l3k/messagediff?branch=master) [![GoDoc](https://godoc.org/github.com/d4l3k/messagediff?status.svg)](https://godoc.org/github.com/d4l3k/messagediff)
+
+A library for doing diffs of arbitrary Golang structs.
+
+If the unsafe package is available messagediff will diff unexported fields in
+addition to exported fields. This is primarily used for testing purposes as it
+allows for providing informative error messages.
+
+Optionally, fields in structs can be tagged as `testdiff:"ignore"` to make
+messagediff skip it when doing the comparison.
+
+
+## Example Usage
+In a normal file:
+```go
+package main
+
+import "gopkg.in/d4l3k/messagediff.v1"
+
+type someStruct struct {
+ A, b int
+ C []int
+}
+
+func main() {
+ a := someStruct{1, 2, []int{1}}
+ b := someStruct{1, 3, []int{1, 2}}
+ diff, equal := messagediff.PrettyDiff(a, b)
+ /*
+ diff =
+ `added: .C[1] = 2
+ modified: .b = 3`
+
+ equal = false
+ */
+}
+
+```
+In a test:
+```go
+import "gopkg.in/d4l3k/messagediff.v1"
+
+...
+
+type someStruct struct {
+ A, b int
+ C []int
+}
+
+func TestSomething(t *testing.T) {
+ want := someStruct{1, 2, []int{1}}
+ got := someStruct{1, 3, []int{1, 2}}
+ if diff, equal := messagediff.PrettyDiff(want, got); !equal {
+ t.Errorf("Something() = %#v\n%s", got, diff)
+ }
+}
+```
+To ignore a field in a struct, just annotate it with testdiff:"ignore" like
+this:
+```go
+package main
+
+import "gopkg.in/d4l3k/messagediff.v1"
+
+type someStruct struct {
+ A int
+ B int `testdiff:"ignore"`
+}
+
+func main() {
+ a := someStruct{1, 2}
+ b := someStruct{1, 3}
+ diff, equal := messagediff.PrettyDiff(a, b)
+ /*
+ equal = true
+ diff = ""
+ */
+}
+```
+
+See the `DeepDiff` function for using the diff results programmatically.
+
+## License
+Copyright (c) 2015 [Tristan Rice](https://fn.lc)
+
+messagediff is licensed under the MIT license. See the LICENSE file for more information.
+
+bypass.go and bypasssafe.go are borrowed from
+[go-spew](https://github.com/davecgh/go-spew) and have a seperate copyright
+notice.
diff --git a/vendor/gopkg.in/d4l3k/messagediff.v1/bypass.go b/vendor/gopkg.in/d4l3k/messagediff.v1/bypass.go
new file mode 100644
index 00000000..8f188265
--- /dev/null
+++ b/vendor/gopkg.in/d4l3k/messagediff.v1/bypass.go
@@ -0,0 +1,151 @@
+// Copyright (c) 2015 Dave Collins
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+// NOTE: Due to the following build constraints, this file will only be compiled
+// when the code is not running on Google App Engine and "-tags disableunsafe"
+// is not added to the go build command line.
+// +build !appengine,!disableunsafe
+
+package messagediff
+
+import (
+ "reflect"
+ "unsafe"
+)
+
+const (
+ // UnsafeDisabled is a build-time constant which specifies whether or
+ // not access to the unsafe package is available.
+ UnsafeDisabled = false
+
+ // ptrSize is the size of a pointer on the current arch.
+ ptrSize = unsafe.Sizeof((*byte)(nil))
+)
+
+var (
+ // offsetPtr, offsetScalar, and offsetFlag are the offsets for the
+ // internal reflect.Value fields. These values are valid before golang
+ // commit ecccf07e7f9d which changed the format. The are also valid
+ // after commit 82f48826c6c7 which changed the format again to mirror
+ // the original format. Code in the init function updates these offsets
+ // as necessary.
+ offsetPtr = uintptr(ptrSize)
+ offsetScalar = uintptr(0)
+ offsetFlag = uintptr(ptrSize * 2)
+
+ // flagKindWidth and flagKindShift indicate various bits that the
+ // reflect package uses internally to track kind information.
+ //
+ // flagRO indicates whether or not the value field of a reflect.Value is
+ // read-only.
+ //
+ // flagIndir indicates whether the value field of a reflect.Value is
+ // the actual data or a pointer to the data.
+ //
+ // These values are valid before golang commit 90a7c3c86944 which
+ // changed their positions. Code in the init function updates these
+ // flags as necessary.
+ flagKindWidth = uintptr(5)
+ flagKindShift = uintptr(flagKindWidth - 1)
+ flagRO = uintptr(1 << 0)
+ flagIndir = uintptr(1 << 1)
+)
+
+func init() {
+ // Older versions of reflect.Value stored small integers directly in the
+ // ptr field (which is named val in the older versions). Versions
+ // between commits ecccf07e7f9d and 82f48826c6c7 added a new field named
+ // scalar for this purpose which unfortunately came before the flag
+ // field, so the offset of the flag field is different for those
+ // versions.
+ //
+ // This code constructs a new reflect.Value from a known small integer
+ // and checks if the size of the reflect.Value struct indicates it has
+ // the scalar field. When it does, the offsets are updated accordingly.
+ vv := reflect.ValueOf(0xf00)
+ if unsafe.Sizeof(vv) == (ptrSize * 4) {
+ offsetScalar = ptrSize * 2
+ offsetFlag = ptrSize * 3
+ }
+
+ // Commit 90a7c3c86944 changed the flag positions such that the low
+ // order bits are the kind. This code extracts the kind from the flags
+ // field and ensures it's the correct type. When it's not, the flag
+ // order has been changed to the newer format, so the flags are updated
+ // accordingly.
+ upf := unsafe.Pointer(uintptr(unsafe.Pointer(&vv)) + offsetFlag)
+ upfv := *(*uintptr)(upf)
+ flagKindMask := uintptr((1<>flagKindShift != uintptr(reflect.Int) {
+ flagKindShift = 0
+ flagRO = 1 << 5
+ flagIndir = 1 << 6
+
+ // Commit adf9b30e5594 modified the flags to separate the
+ // flagRO flag into two bits which specifies whether or not the
+ // field is embedded. This causes flagIndir to move over a bit
+ // and means that flagRO is the combination of either of the
+ // original flagRO bit and the new bit.
+ //
+ // This code detects the change by extracting what used to be
+ // the indirect bit to ensure it's set. When it's not, the flag
+ // order has been changed to the newer format, so the flags are
+ // updated accordingly.
+ if upfv&flagIndir == 0 {
+ flagRO = 3 << 5
+ flagIndir = 1 << 7
+ }
+ }
+}
+
+// unsafeReflectValue converts the passed reflect.Value into a one that bypasses
+// the typical safety restrictions preventing access to unaddressable and
+// unexported data. It works by digging the raw pointer to the underlying
+// value out of the protected value and generating a new unprotected (unsafe)
+// reflect.Value to it.
+//
+// This allows us to check for implementations of the Stringer and error
+// interfaces to be used for pretty printing ordinarily unaddressable and
+// inaccessible values such as unexported struct fields.
+func unsafeReflectValue(v reflect.Value) (rv reflect.Value) {
+ indirects := 1
+ vt := v.Type()
+ upv := unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetPtr)
+ rvf := *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&v)) + offsetFlag))
+ if rvf&flagIndir != 0 {
+ vt = reflect.PtrTo(v.Type())
+ indirects++
+ } else if offsetScalar != 0 {
+ // The value is in the scalar field when it's not one of the
+ // reference types.
+ switch vt.Kind() {
+ case reflect.Uintptr:
+ case reflect.Chan:
+ case reflect.Func:
+ case reflect.Map:
+ case reflect.Ptr:
+ case reflect.UnsafePointer:
+ default:
+ upv = unsafe.Pointer(uintptr(unsafe.Pointer(&v)) +
+ offsetScalar)
+ }
+ }
+
+ pv := reflect.NewAt(vt, upv)
+ rv = pv
+ for i := 0; i < indirects; i++ {
+ rv = rv.Elem()
+ }
+ return rv
+}
diff --git a/vendor/gopkg.in/d4l3k/messagediff.v1/bypasssafe.go b/vendor/gopkg.in/d4l3k/messagediff.v1/bypasssafe.go
new file mode 100644
index 00000000..99f7ffd8
--- /dev/null
+++ b/vendor/gopkg.in/d4l3k/messagediff.v1/bypasssafe.go
@@ -0,0 +1,37 @@
+// Copyright (c) 2015 Dave Collins
+//
+// Permission to use, copy, modify, and distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+// NOTE: Due to the following build constraints, this file will only be compiled
+// when either the code is running on Google App Engine or "-tags disableunsafe"
+// is added to the go build command line.
+// +build appengine disableunsafe
+
+package messagediff
+
+import "reflect"
+
+const (
+ // UnsafeDisabled is a build-time constant which specifies whether or
+ // not access to the unsafe package is available.
+ UnsafeDisabled = true
+)
+
+// unsafeReflectValue typically converts the passed reflect.Value into a one
+// that bypasses the typical safety restrictions preventing access to
+// unaddressable and unexported data. However, doing this relies on access to
+// the unsafe package. This is a stub version which simply returns the passed
+// reflect.Value when the unsafe package is not available.
+func unsafeReflectValue(v reflect.Value) reflect.Value {
+ return v
+}
diff --git a/vendor/gopkg.in/d4l3k/messagediff.v1/messagediff.go b/vendor/gopkg.in/d4l3k/messagediff.v1/messagediff.go
new file mode 100644
index 00000000..cb2f432c
--- /dev/null
+++ b/vendor/gopkg.in/d4l3k/messagediff.v1/messagediff.go
@@ -0,0 +1,242 @@
+package messagediff
+
+import (
+ "fmt"
+ "reflect"
+ "sort"
+ "strings"
+ "unsafe"
+)
+
+// PrettyDiff does a deep comparison and returns the nicely formated results.
+func PrettyDiff(a, b interface{}) (string, bool) {
+ d, equal := DeepDiff(a, b)
+ var dstr []string
+ for path, added := range d.Added {
+ dstr = append(dstr, fmt.Sprintf("added: %s = %#v\n", path.String(), added))
+ }
+ for path, removed := range d.Removed {
+ dstr = append(dstr, fmt.Sprintf("removed: %s = %#v\n", path.String(), removed))
+ }
+ for path, modified := range d.Modified {
+ dstr = append(dstr, fmt.Sprintf("modified: %s = %#v\n", path.String(), modified))
+ }
+ sort.Strings(dstr)
+ return strings.Join(dstr, ""), equal
+}
+
+// DeepDiff does a deep comparison and returns the results.
+func DeepDiff(a, b interface{}) (*Diff, bool) {
+ d := newDiff()
+ return d, d.diff(reflect.ValueOf(a), reflect.ValueOf(b), nil)
+}
+
+func newDiff() *Diff {
+ return &Diff{
+ Added: make(map[*Path]interface{}),
+ Removed: make(map[*Path]interface{}),
+ Modified: make(map[*Path]interface{}),
+ visited: make(map[visit]bool),
+ }
+}
+
+func (d *Diff) diff(aVal, bVal reflect.Value, path Path) bool {
+ // The array underlying `path` could be modified in subsequent
+ // calls. Make sure we have a local copy.
+ localPath := make(Path, len(path))
+ copy(localPath, path)
+
+ // Validity checks. Should only trigger if nil is one of the original arguments.
+ if !aVal.IsValid() && !bVal.IsValid() {
+ return true
+ }
+ if !bVal.IsValid() {
+ d.Modified[&localPath] = nil
+ return false
+ } else if !aVal.IsValid() {
+ d.Modified[&localPath] = bVal.Interface()
+ return false
+ }
+
+ if aVal.Type() != bVal.Type() {
+ d.Modified[&localPath] = bVal.Interface()
+ return false
+ }
+ kind := aVal.Kind()
+
+ // Borrowed from the reflect package to handle recursive data structures.
+ hard := func(k reflect.Kind) bool {
+ switch k {
+ case reflect.Array, reflect.Map, reflect.Slice, reflect.Struct:
+ return true
+ }
+ return false
+ }
+
+ if aVal.CanAddr() && bVal.CanAddr() && hard(kind) {
+ addr1 := unsafe.Pointer(aVal.UnsafeAddr())
+ addr2 := unsafe.Pointer(bVal.UnsafeAddr())
+ if uintptr(addr1) > uintptr(addr2) {
+ // Canonicalize order to reduce number of entries in visited.
+ // Assumes non-moving garbage collector.
+ addr1, addr2 = addr2, addr1
+ }
+
+ // Short circuit if references are already seen.
+ typ := aVal.Type()
+ v := visit{addr1, addr2, typ}
+ if d.visited[v] {
+ return true
+ }
+
+ // Remember for later.
+ d.visited[v] = true
+ }
+ // End of borrowed code.
+
+ equal := true
+ switch kind {
+ case reflect.Map, reflect.Ptr, reflect.Func, reflect.Chan, reflect.Slice:
+ if aVal.IsNil() && bVal.IsNil() {
+ return true
+ }
+ if aVal.IsNil() || bVal.IsNil() {
+ d.Modified[&localPath] = bVal.Interface()
+ return false
+ }
+ }
+
+ switch kind {
+ case reflect.Array, reflect.Slice:
+ aLen := aVal.Len()
+ bLen := bVal.Len()
+ for i := 0; i < min(aLen, bLen); i++ {
+ localPath := append(localPath, SliceIndex(i))
+ if eq := d.diff(aVal.Index(i), bVal.Index(i), localPath); !eq {
+ equal = false
+ }
+ }
+ if aLen > bLen {
+ for i := bLen; i < aLen; i++ {
+ localPath := append(localPath, SliceIndex(i))
+ d.Removed[&localPath] = aVal.Index(i).Interface()
+ equal = false
+ }
+ } else if aLen < bLen {
+ for i := aLen; i < bLen; i++ {
+ localPath := append(localPath, SliceIndex(i))
+ d.Added[&localPath] = bVal.Index(i).Interface()
+ equal = false
+ }
+ }
+ case reflect.Map:
+ for _, key := range aVal.MapKeys() {
+ aI := aVal.MapIndex(key)
+ bI := bVal.MapIndex(key)
+ localPath := append(localPath, MapKey{key.Interface()})
+ if !bI.IsValid() {
+ d.Removed[&localPath] = aI.Interface()
+ equal = false
+ } else if eq := d.diff(aI, bI, localPath); !eq {
+ equal = false
+ }
+ }
+ for _, key := range bVal.MapKeys() {
+ aI := aVal.MapIndex(key)
+ if !aI.IsValid() {
+ bI := bVal.MapIndex(key)
+ localPath := append(localPath, MapKey{key.Interface()})
+ d.Added[&localPath] = bI.Interface()
+ equal = false
+ }
+ }
+ case reflect.Struct:
+ typ := aVal.Type()
+ for i := 0; i < typ.NumField(); i++ {
+ index := []int{i}
+ field := typ.FieldByIndex(index)
+ if field.Tag.Get("testdiff") == "ignore" { // skip fields marked to be ignored
+ continue
+ }
+ localPath := append(localPath, StructField(field.Name))
+ aI := unsafeReflectValue(aVal.FieldByIndex(index))
+ bI := unsafeReflectValue(bVal.FieldByIndex(index))
+ if eq := d.diff(aI, bI, localPath); !eq {
+ equal = false
+ }
+ }
+ case reflect.Ptr:
+ equal = d.diff(aVal.Elem(), bVal.Elem(), localPath)
+ default:
+ if reflect.DeepEqual(aVal.Interface(), bVal.Interface()) {
+ equal = true
+ } else {
+ d.Modified[&localPath] = bVal.Interface()
+ equal = false
+ }
+ }
+ return equal
+}
+
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
+// During deepValueEqual, must keep track of checks that are
+// in progress. The comparison algorithm assumes that all
+// checks in progress are true when it reencounters them.
+// Visited comparisons are stored in a map indexed by visit.
+// This is borrowed from the reflect package.
+type visit struct {
+ a1 unsafe.Pointer
+ a2 unsafe.Pointer
+ typ reflect.Type
+}
+
+// Diff represents a change in a struct.
+type Diff struct {
+ Added, Removed, Modified map[*Path]interface{}
+ visited map[visit]bool
+}
+
+// Path represents a path to a changed datum.
+type Path []PathNode
+
+func (p Path) String() string {
+ var out string
+ for _, n := range p {
+ out += n.String()
+ }
+ return out
+}
+
+// PathNode represents one step in the path.
+type PathNode interface {
+ String() string
+}
+
+// StructField is a path element representing a field of a struct.
+type StructField string
+
+func (n StructField) String() string {
+ return fmt.Sprintf(".%s", string(n))
+}
+
+// MapKey is a path element representing a key of a map.
+type MapKey struct {
+ Key interface{}
+}
+
+func (n MapKey) String() string {
+ return fmt.Sprintf("[%#v]", n.Key)
+}
+
+// SliceIndex is a path element representing a index of a slice.
+type SliceIndex int
+
+func (n SliceIndex) String() string {
+ return fmt.Sprintf("[%d]", n)
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 6519c5b6..01586ca7 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -192,6 +192,8 @@ golang.org/x/tools/internal/fastwalk
google.golang.org/appengine/cloudsql
# gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc
gopkg.in/alexcesaro/quotedprintable.v3
+# gopkg.in/d4l3k/messagediff.v1 v1.2.1
+gopkg.in/d4l3k/messagediff.v1
# gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gopkg.in/gomail.v2
# gopkg.in/testfixtures.v2 v2.5.3