diff --git a/.make/test.mk b/.make/test.mk
index 622384532a..37129d135e 100644
--- a/.make/test.mk
+++ b/.make/test.mk
@@ -124,6 +124,12 @@ ALL_PKGS_EXCLUDE_PATTERN = "vendor\|account\/tenant\|app\'\|tool\/cli\|design\|c
GOANALYSIS_PKGS_EXCLUDE_PATTERN="vendor|account/tenant|app|client|tool/cli"
GOANALYSIS_DIRS=$(shell go list -f {{.Dir}} ./... | grep -v -E $(GOANALYSIS_PKGS_EXCLUDE_PATTERN))
+# temporary directory for fabric8-test
+FABRIC8_E2E_TEST_DIR = $(TMP_PATH)/fabric8-test
+
+# fabric8-test reporsitory
+FABRIC8_E2E_TEST_REPO = "https://github.com/fabric8io/fabric8-test.git"
+
#-------------------------------------------------------------------------------
# Normal test targets
#
@@ -187,13 +193,50 @@ test-remote-no-coverage: prebuild-check $(SOURCES)
test-migration: prebuild-check migration/sqlbindata.go migration/sqlbindata_test.go
F8_RESOURCE_DATABASE=1 F8_LOG_LEVEL=$(F8_LOG_LEVEL) go test $(GO_TEST_VERBOSITY_FLAG) github.com/fabric8-services/fabric8-wit/migration
+# Starts the WIT server and waits until its running
+define start-wit
+ echo "Starting WIT and ensuring that it is running..."; \
+ F8_LOG_LEVEL=ERROR ./wit+pmcd.sh &
+ while ! nc -z localhost 8080 < /dev/null; do \
+ printf .; \
+ sleep 5 ; \
+ done; \
+ echo "WIT is RUNNING!";
+endef
+
+.PHONY: test-e2e
+## Runs the end-to-end tests WITHOUT producing coverage files for each package.
+test-e2e: $(BINARY_SERVER_BIN)
+ $(call log-info,"Running tests: $@")
+ifeq ($(OS),Windows_NT)
+ $(error End to end tests currently cannot run on Windows based operating systems.)
+endif
+ $(call download-docker-compose)
+ifeq ($(UNAME_S),Darwin)
+ @echo "Running docker-compose with macOS network settings"
+ $(DOCKER_COMPOSE_BIN_ALT) -f docker-compose.macos.yml up -d db auth
+else
+ @echo "Running docker-compose with Linux network settings"
+ $(DOCKER_COMPOSE_BIN_ALT) up -d db auth
+endif
+ # Start the WIT server
+ $(call start-wit)
+ # Clone the fabric8-test repo
+ @if [ "$(FABRIC8_E2E_TEST_DIR)" ]; then \
+ echo "Removing any existing dir $(FABRIC8_E2E_TEST_DIR)"; \
+ rm -rf $(FABRIC8_E2E_TEST_DIR); \
+ fi
+ $(GIT_BIN_NAME) clone --depth=1 $(FABRIC8_E2E_TEST_REPO) $(FABRIC8_E2E_TEST_DIR)
+ # Install e2e test deps and run the tests
+ $(FABRIC8_E2E_TEST_DIR)/EE_API_automation/cico_run_EE_tests_wit.sh
+
# Downloads docker-compose to tmp/docker-compose if it does not already exist.
define download-docker-compose
@if [ ! -f "$(DOCKER_COMPOSE_BIN_ALT)" ]; then \
echo "Downloading docker-compose to $(DOCKER_COMPOSE_BIN_ALT)"; \
UNAME_S=$(shell uname -s); \
UNAME_M=$(shell uname -m); \
- URL="https://github.com/docker/compose/releases/download/1.11.2/docker-compose-$${UNAME_S}-$${UNAME_M}"; \
+ URL="https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$${UNAME_S}-$${UNAME_M}"; \
curl --silent -L $${URL} > $(DOCKER_COMPOSE_BIN_ALT); \
chmod +x $(DOCKER_COMPOSE_BIN_ALT); \
fi
@@ -542,3 +585,14 @@ CLEAN_TARGETS += clean-coverage-remote
## Removes remote test coverage file
clean-coverage-remote:
-@rm -f $(COV_PATH_REMOTE)
+
+CLEAN_TARGETS += clean-e2e
+.PHONY: clean-e2e
+## Cleans up environment of e2e tests
+clean-e2e:
+ ## Kills the WIT process
+ $(shell ps aux | grep 'bin/[w]it' | awk '{print $$2}' | xargs kill)
+ ## Removes the end-to-end (e2e) test directory
+ -rm -rf $(FABRIC8_E2E_TEST_DIR)
+
+
diff --git a/cico_run_e2e_tests.sh b/cico_run_e2e_tests.sh
index b6ee4586a3..67d6783c29 100755
--- a/cico_run_e2e_tests.sh
+++ b/cico_run_e2e_tests.sh
@@ -1,8 +1,17 @@
#!/bin/bash
-echo "The end to end tests check will be implemented in"
-echo "https://github.com/fabric8-services/fabric8-wit/pull/2197 "
-echo "but for the CI to execute the task in an individual job we need"
-echo "to have an \".sh\" file in place."
+. cico_setup.sh
-exit 1
+load_jenkins_vars;
+
+install_deps;
+
+make docker-start
+
+make docker-build
+
+trap "make clean-e2e" EXIT
+
+make test-e2e
+
+echo "CICO: ran e2e-tests"
\ No newline at end of file
diff --git a/cico_setup.sh b/cico_setup.sh
index 59673f42be..f00af10ffd 100644
--- a/cico_setup.sh
+++ b/cico_setup.sh
@@ -38,7 +38,8 @@ function install_deps() {
docker \
make \
git \
- curl
+ curl \
+ nc
service docker start
diff --git a/controller/codebase.go b/controller/codebase.go
index 67e4496060..9c98ec30dd 100644
--- a/controller/codebase.go
+++ b/controller/codebase.go
@@ -474,6 +474,21 @@ func getBranch(projects []che.WorkspaceProject, codebaseURL string) string {
return ""
}
+// ConvertCodebaseSimple converts a simple codebase ID into a Generic Relationship
+func ConvertCodebaseSimple(request *http.Request, id interface{}) (*app.GenericData, *app.GenericLinks) {
+ i := fmt.Sprint(id)
+ data := &app.GenericData{
+ Type: ptr.String(APIStringTypeCodebase),
+ ID: &i,
+ }
+ relatedURL := rest.AbsoluteURL(request, app.CodebaseHref(i))
+ links := &app.GenericLinks{
+ Self: &relatedURL,
+ Related: &relatedURL,
+ }
+ return data, links
+}
+
// ConvertCodebase converts between internal and external REST representation
func ConvertCodebase(request *http.Request, codebase codebase.Codebase, options ...CodebaseConvertFunc) *app.Codebase {
relatedURL := rest.AbsoluteURL(request, app.CodebaseHref(codebase.ID))
diff --git a/controller/comments.go b/controller/comments.go
index 93038d3268..5cc7d00790 100644
--- a/controller/comments.go
+++ b/controller/comments.go
@@ -3,7 +3,6 @@ package controller
import (
"context"
"fmt"
- "html"
"net/http"
"github.com/fabric8-services/fabric8-wit/app"
@@ -216,7 +215,7 @@ func ConvertComment(request *http.Request, comment comment.Comment, additional .
ID: &comment.ID,
Attributes: &app.CommentAttributes{
Body: &comment.Body,
- BodyRendered: ptr.String(rendering.RenderMarkupToHTML(html.EscapeString(comment.Body), comment.Markup)),
+ BodyRendered: ptr.String(rendering.RenderMarkupToHTML(comment.Body, comment.Markup)),
Markup: ptr.String(rendering.NilSafeGetMarkup(&comment.Markup)),
CreatedAt: &comment.CreatedAt,
UpdatedAt: &comment.UpdatedAt,
diff --git a/controller/comments_blackbox_test.go b/controller/comments_blackbox_test.go
index 48c2a427c1..e93dab962c 100644
--- a/controller/comments_blackbox_test.go
+++ b/controller/comments_blackbox_test.go
@@ -3,7 +3,6 @@ package controller_test
import (
"context"
"fmt"
- "html"
"strings"
"testing"
"time"
@@ -155,7 +154,7 @@ func assertComment(t *testing.T, resultData *app.Comment, expectedIdentity accou
assert.Equal(t, expectedBody, *resultData.Attributes.Body)
require.NotNil(t, resultData.Attributes.Markup)
assert.Equal(t, expectedMarkup, *resultData.Attributes.Markup)
- assert.Equal(t, rendering.RenderMarkupToHTML(html.EscapeString(expectedBody), expectedMarkup), *resultData.Attributes.BodyRendered)
+ assert.Equal(t, rendering.RenderMarkupToHTML(expectedBody, expectedMarkup), *resultData.Attributes.BodyRendered)
require.NotNil(t, resultData.Relationships)
require.NotNil(t, resultData.Relationships.Creator)
require.NotNil(t, resultData.Relationships.Creator.Data)
@@ -302,6 +301,21 @@ func (s *CommentsSuite) TestShowCommentWithEscapedScriptInjection() {
assertComment(s.T(), result.Data, s.testIdentity, "", rendering.SystemMarkupPlainText)
}
+func (s *CommentsSuite) TestShowCommentWithTextAndCodeblock() {
+ // given
+ fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
+ wiID := fxt.WorkItems[0].ID
+ body := "Hello, World \n```\n { \"foo\":\"bar\" } \n``` "
+ expectedBody := "
Hello, World
\n\n { "foo":"bar" } \n
\n"
+ c := s.createWorkItemComment(s.testIdentity, wiID, body, &markdownMarkup, nil)
+ // when
+ userSvc, _, _, _, commentsCtrl := s.securedControllers(s.testIdentity)
+ _, result := test.ShowCommentsOK(s.T(), userSvc.Context, userSvc, commentsCtrl, *c.Data.ID, nil, nil)
+ // then
+ assert.Equal(s.T(), body, *result.Data.Attributes.Body)
+ assert.Equal(s.T(), expectedBody, *result.Data.Attributes.BodyRendered)
+}
+
func (s *CommentsSuite) TestUpdateCommentWithoutAuth() {
// given
fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
diff --git a/controller/label.go b/controller/label.go
index fc14a90908..3fda67facc 100644
--- a/controller/label.go
+++ b/controller/label.go
@@ -11,6 +11,7 @@ import (
"github.com/fabric8-services/fabric8-wit/jsonapi"
"github.com/fabric8-services/fabric8-wit/label"
"github.com/fabric8-services/fabric8-wit/login"
+ "github.com/fabric8-services/fabric8-wit/ptr"
"github.com/fabric8-services/fabric8-wit/rest"
"github.com/fabric8-services/fabric8-wit/space"
"github.com/goadesign/goa"
@@ -164,10 +165,9 @@ func ConvertLabelsSimple(request *http.Request, labelIDs []interface{}) []*app.G
// ConvertLabelSimple converts a Label ID into a Generic Relationship
func ConvertLabelSimple(request *http.Request, labelID interface{}) *app.GenericData {
- t := label.APIStringTypeLabels
i := fmt.Sprint(labelID)
return &app.GenericData{
- Type: &t,
+ Type: ptr.String(label.APIStringTypeLabels),
ID: &i,
}
}
diff --git a/controller/test-files/event/list/ok-area.res.payload.golden.json b/controller/test-files/event/list/ok-area.res.payload.golden.json
index d7c771214c..8912ec5895 100755
--- a/controller/test-files/event/list/ok-area.res.payload.golden.json
+++ b/controller/test-files/event/list/ok-area.res.payload.golden.json
@@ -3,8 +3,6 @@
{
"attributes": {
"name": "system.area",
- "newValue": null,
- "oldValue": null,
"timestamp": "0001-01-01T00:00:00Z"
},
"id": "00000000-0000-0000-0000-000000000001",
@@ -27,13 +25,14 @@
}
]
},
- "oldValue": {
- "data": [
- {
- "id": "",
- "type": "areas"
- }
- ]
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000004",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000004"
+ }
}
},
"type": "events"
diff --git a/controller/test-files/event/list/ok-assignees.res.payload.golden.json b/controller/test-files/event/list/ok-assignees.res.payload.golden.json
index d8eae8db09..eca63d59a8 100755
--- a/controller/test-files/event/list/ok-assignees.res.payload.golden.json
+++ b/controller/test-files/event/list/ok-assignees.res.payload.golden.json
@@ -3,8 +3,6 @@
{
"attributes": {
"name": "system.assignees",
- "newValue": null,
- "oldValue": null,
"timestamp": "0001-01-01T00:00:00Z"
},
"id": "00000000-0000-0000-0000-000000000001",
@@ -27,7 +25,15 @@
}
]
},
- "oldValue": {}
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
},
"type": "events"
}
diff --git a/controller/test-files/event/list/ok-description.res.payload.golden.json b/controller/test-files/event/list/ok-description.res.payload.golden.json
index a39dc4169a..348ca4f130 100755
--- a/controller/test-files/event/list/ok-description.res.payload.golden.json
+++ b/controller/test-files/event/list/ok-description.res.payload.golden.json
@@ -3,8 +3,10 @@
{
"attributes": {
"name": "system.description",
- "newValue": null,
- "oldValue": null,
+ "newValue": {
+ "content": "# Description is modified1",
+ "markup": "Markdown"
+ },
"timestamp": "0001-01-01T00:00:00Z"
},
"id": "00000000-0000-0000-0000-000000000001",
@@ -18,6 +20,15 @@
"related": "http:///api/users/00000000-0000-0000-0000-000000000002",
"self": "http:///api/users/00000000-0000-0000-0000-000000000002"
}
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
}
},
"type": "events"
diff --git a/controller/test-files/event/list/ok-iteration.res.payload.golden.json b/controller/test-files/event/list/ok-iteration.res.payload.golden.json
index 916df8e482..3b38532632 100755
--- a/controller/test-files/event/list/ok-iteration.res.payload.golden.json
+++ b/controller/test-files/event/list/ok-iteration.res.payload.golden.json
@@ -3,8 +3,6 @@
{
"attributes": {
"name": "system.iteration",
- "newValue": null,
- "oldValue": null,
"timestamp": "0001-01-01T00:00:00Z"
},
"id": "00000000-0000-0000-0000-000000000001",
@@ -27,13 +25,14 @@
}
]
},
- "oldValue": {
- "data": [
- {
- "id": "",
- "type": "iterations"
- }
- ]
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000004",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000004"
+ }
}
},
"type": "events"
diff --git a/controller/test-files/event/list/ok-kindFloat.res.payload.golden.json b/controller/test-files/event/list/ok-kindFloat.res.payload.golden.json
index beb666860c..c908311315 100755
--- a/controller/test-files/event/list/ok-kindFloat.res.payload.golden.json
+++ b/controller/test-files/event/list/ok-kindFloat.res.payload.golden.json
@@ -18,6 +18,15 @@
"related": "http:///api/users/00000000-0000-0000-0000-000000000002",
"self": "http:///api/users/00000000-0000-0000-0000-000000000002"
}
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
}
},
"type": "events"
diff --git a/controller/test-files/event/list/ok-kindInt.res.payload.golden.json b/controller/test-files/event/list/ok-kindInt.res.payload.golden.json
index 3de057d70b..1e40fe9e36 100755
--- a/controller/test-files/event/list/ok-kindInt.res.payload.golden.json
+++ b/controller/test-files/event/list/ok-kindInt.res.payload.golden.json
@@ -18,6 +18,15 @@
"related": "http:///api/users/00000000-0000-0000-0000-000000000002",
"self": "http:///api/users/00000000-0000-0000-0000-000000000002"
}
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
}
},
"type": "events"
diff --git a/controller/test-files/event/list/ok-labels.res.payload.golden.json b/controller/test-files/event/list/ok-labels.res.payload.golden.json
index 0619561160..a8d3d9e061 100755
--- a/controller/test-files/event/list/ok-labels.res.payload.golden.json
+++ b/controller/test-files/event/list/ok-labels.res.payload.golden.json
@@ -3,8 +3,6 @@
{
"attributes": {
"name": "system.labels",
- "newValue": null,
- "oldValue": null,
"timestamp": "0001-01-01T00:00:00Z"
},
"id": "00000000-0000-0000-0000-000000000001",
@@ -31,7 +29,15 @@
}
]
},
- "oldValue": {}
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000005",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000005"
+ }
+ }
},
"type": "events"
}
diff --git a/controller/test-files/event/list/ok-state.res.payload.golden.json b/controller/test-files/event/list/ok-state.res.payload.golden.json
index 75801cf193..2ca0facef7 100755
--- a/controller/test-files/event/list/ok-state.res.payload.golden.json
+++ b/controller/test-files/event/list/ok-state.res.payload.golden.json
@@ -18,6 +18,15 @@
"related": "http:///api/users/00000000-0000-0000-0000-000000000002",
"self": "http:///api/users/00000000-0000-0000-0000-000000000002"
}
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
}
},
"type": "events"
diff --git a/controller/test-files/event/list/ok-title.res.payload.golden.json b/controller/test-files/event/list/ok-title.res.payload.golden.json
index 2edf31bb0d..cff4a3f909 100755
--- a/controller/test-files/event/list/ok-title.res.payload.golden.json
+++ b/controller/test-files/event/list/ok-title.res.payload.golden.json
@@ -18,6 +18,15 @@
"related": "http:///api/users/00000000-0000-0000-0000-000000000003",
"self": "http:///api/users/00000000-0000-0000-0000-000000000003"
}
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000004",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000004"
+ }
}
},
"type": "events"
diff --git a/controller/test-files/event/list/ok.bool_list.res.payload.golden.json b/controller/test-files/event/list/ok.bool_list.res.payload.golden.json
new file mode 100755
index 0000000000..6957208e6a
--- /dev/null
+++ b/controller/test-files/event/list/ok.bool_list.res.payload.golden.json
@@ -0,0 +1,74 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "bool_list",
+ "newValue": [
+ true,
+ false
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "bool_list",
+ "newValue": [
+ false,
+ true
+ ],
+ "oldValue": [
+ true,
+ false
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.bool_single.res.payload.golden.json b/controller/test-files/event/list/ok.bool_single.res.payload.golden.json
new file mode 100755
index 0000000000..f43e89a1da
--- /dev/null
+++ b/controller/test-files/event/list/ok.bool_single.res.payload.golden.json
@@ -0,0 +1,65 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "bool_single",
+ "newValue": true,
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "bool_single",
+ "newValue": false,
+ "oldValue": true,
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.float_list.res.payload.golden.json b/controller/test-files/event/list/ok.float_list.res.payload.golden.json
new file mode 100755
index 0000000000..33a67b2a50
--- /dev/null
+++ b/controller/test-files/event/list/ok.float_list.res.payload.golden.json
@@ -0,0 +1,74 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "float_list",
+ "newValue": [
+ 0.1,
+ -1111.1
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "float_list",
+ "newValue": [
+ -1111.1,
+ 0.1
+ ],
+ "oldValue": [
+ 0.1,
+ -1111.1
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.float_single.res.payload.golden.json b/controller/test-files/event/list/ok.float_single.res.payload.golden.json
new file mode 100755
index 0000000000..f022cf6883
--- /dev/null
+++ b/controller/test-files/event/list/ok.float_single.res.payload.golden.json
@@ -0,0 +1,65 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "float_single",
+ "newValue": 0.1,
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "float_single",
+ "newValue": -1111.1,
+ "oldValue": 0.1,
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.integer_list.res.payload.golden.json b/controller/test-files/event/list/ok.integer_list.res.payload.golden.json
new file mode 100755
index 0000000000..281c0a881e
--- /dev/null
+++ b/controller/test-files/event/list/ok.integer_list.res.payload.golden.json
@@ -0,0 +1,74 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "integer_list",
+ "newValue": [
+ 0,
+ 333
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "integer_list",
+ "newValue": [
+ 333,
+ 0
+ ],
+ "oldValue": [
+ 0,
+ 333
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.integer_single.res.payload.golden.json b/controller/test-files/event/list/ok.integer_single.res.payload.golden.json
new file mode 100755
index 0000000000..52e28659fd
--- /dev/null
+++ b/controller/test-files/event/list/ok.integer_single.res.payload.golden.json
@@ -0,0 +1,65 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "integer_single",
+ "newValue": 0,
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "integer_single",
+ "newValue": 333,
+ "oldValue": 0,
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.markup_list.res.payload.golden.json b/controller/test-files/event/list/ok.markup_list.res.payload.golden.json
new file mode 100755
index 0000000000..53b4e34b69
--- /dev/null
+++ b/controller/test-files/event/list/ok.markup_list.res.payload.golden.json
@@ -0,0 +1,92 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "markup_list",
+ "newValue": [
+ {
+ "content": "plain text",
+ "markup": "PlainText"
+ },
+ {
+ "content": "default",
+ "markup": "PlainText"
+ }
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "markup_list",
+ "newValue": [
+ {
+ "content": "default",
+ "markup": "PlainText"
+ },
+ {
+ "content": "plain text",
+ "markup": "PlainText"
+ }
+ ],
+ "oldValue": [
+ {
+ "content": "plain text",
+ "markup": "PlainText"
+ },
+ {
+ "content": "default",
+ "markup": "PlainText"
+ }
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.markup_single.res.payload.golden.json b/controller/test-files/event/list/ok.markup_single.res.payload.golden.json
new file mode 100755
index 0000000000..09b3e49148
--- /dev/null
+++ b/controller/test-files/event/list/ok.markup_single.res.payload.golden.json
@@ -0,0 +1,74 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "markup_single",
+ "newValue": {
+ "content": "plain text",
+ "markup": "PlainText"
+ },
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "markup_single",
+ "newValue": {
+ "content": "default",
+ "markup": "PlainText"
+ },
+ "oldValue": {
+ "content": "plain text",
+ "markup": "PlainText"
+ },
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.string_list.res.payload.golden.json b/controller/test-files/event/list/ok.string_list.res.payload.golden.json
new file mode 100755
index 0000000000..9d7b6932b8
--- /dev/null
+++ b/controller/test-files/event/list/ok.string_list.res.payload.golden.json
@@ -0,0 +1,74 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "string_list",
+ "newValue": [
+ "foo",
+ "bar"
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "string_list",
+ "newValue": [
+ "bar",
+ "foo"
+ ],
+ "oldValue": [
+ "foo",
+ "bar"
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.string_single.res.payload.golden.json b/controller/test-files/event/list/ok.string_single.res.payload.golden.json
new file mode 100755
index 0000000000..fb6c30bf63
--- /dev/null
+++ b/controller/test-files/event/list/ok.string_single.res.payload.golden.json
@@ -0,0 +1,65 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "string_single",
+ "newValue": "foo",
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "string_single",
+ "newValue": "bar",
+ "oldValue": "foo",
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.url_list.res.payload.golden.json b/controller/test-files/event/list/ok.url_list.res.payload.golden.json
new file mode 100755
index 0000000000..44e953389f
--- /dev/null
+++ b/controller/test-files/event/list/ok.url_list.res.payload.golden.json
@@ -0,0 +1,74 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "url_list",
+ "newValue": [
+ "127.0.0.1",
+ "http://www.openshift.io"
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "url_list",
+ "newValue": [
+ "http://www.openshift.io",
+ "127.0.0.1"
+ ],
+ "oldValue": [
+ "127.0.0.1",
+ "http://www.openshift.io"
+ ],
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/event/list/ok.url_single.res.payload.golden.json b/controller/test-files/event/list/ok.url_single.res.payload.golden.json
new file mode 100755
index 0000000000..1819ed75d1
--- /dev/null
+++ b/controller/test-files/event/list/ok.url_single.res.payload.golden.json
@@ -0,0 +1,65 @@
+{
+ "data": [
+ {
+ "attributes": {
+ "name": "url_single",
+ "newValue": "127.0.0.1",
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000001",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ },
+ {
+ "attributes": {
+ "name": "url_single",
+ "newValue": "http://www.openshift.io",
+ "oldValue": "127.0.0.1",
+ "timestamp": "0001-01-01T00:00:00Z"
+ },
+ "id": "00000000-0000-0000-0000-000000000004",
+ "relationships": {
+ "modifier": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000002",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000002"
+ }
+ },
+ "workItemType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ }
+ },
+ "type": "events"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/controller/test-files/work_item/update/workitem_type.res.payload.golden.json b/controller/test-files/work_item/update/workitem_type.res.payload.golden.json
new file mode 100755
index 0000000000..58462c84e9
--- /dev/null
+++ b/controller/test-files/work_item/update/workitem_type.res.payload.golden.json
@@ -0,0 +1,95 @@
+{
+ "data": {
+ "attributes": {
+ "bar": null,
+ "fooBar": "alpha",
+ "fooo": 2.5,
+ "integer-or-float-list": [],
+ "system.created_at": "0001-01-01T00:00:00Z",
+ "system.description": "```\nMissing fields in workitem type: Second WorkItem Type\n\nType1 Assigned To : First User (jon_doe), Second User (lorem_ipsum)\nType1 bar : hello\nType1 fooBar : open\nType1 integer-or-float-list : 101\nType1 reporter : First User (jon_doe)\n```\ndescription1\n",
+ "system.description.markup": "Markdown",
+ "system.description.rendered": "\u003cpre\u003e\u003ccode class=\"prettyprint\"\u003e\u003cspan class=\"typ\"\u003eMissing\u003c/span\u003e \u003cspan class=\"pln\"\u003efields\u003c/span\u003e \u003cspan class=\"kwd\"\u003ein\u003c/span\u003e \u003cspan class=\"pln\"\u003eworkitem\u003c/span\u003e \u003cspan class=\"kwd\"\u003etype\u003c/span\u003e\u003cspan class=\"pun\"\u003e:\u003c/span\u003e \u003cspan class=\"typ\"\u003eSecond\u003c/span\u003e \u003cspan class=\"typ\"\u003eWorkItem\u003c/span\u003e \u003cspan class=\"typ\"\u003eType\u003c/span\u003e\n\n\u003cspan class=\"typ\"\u003eType1\u003c/span\u003e \u003cspan class=\"typ\"\u003eAssigned\u003c/span\u003e \u003cspan class=\"typ\"\u003eTo\u003c/span\u003e \u003cspan class=\"pun\"\u003e:\u003c/span\u003e \u003cspan class=\"typ\"\u003eFirst\u003c/span\u003e \u003cspan class=\"typ\"\u003eUser\u003c/span\u003e \u003cspan class=\"pun\"\u003e(\u003c/span\u003e\u003cspan class=\"pln\"\u003ejon_doe\u003c/span\u003e\u003cspan class=\"pun\"\u003e)\u003c/span\u003e\u003cspan class=\"pun\"\u003e,\u003c/span\u003e \u003cspan class=\"typ\"\u003eSecond\u003c/span\u003e \u003cspan class=\"typ\"\u003eUser\u003c/span\u003e \u003cspan class=\"pun\"\u003e(\u003c/span\u003e\u003cspan class=\"pln\"\u003elorem_ipsum\u003c/span\u003e\u003cspan class=\"pun\"\u003e)\u003c/span\u003e\n\u003cspan class=\"typ\"\u003eType1\u003c/span\u003e \u003cspan class=\"pln\"\u003ebar\u003c/span\u003e \u003cspan class=\"pun\"\u003e:\u003c/span\u003e \u003cspan class=\"pln\"\u003ehello\u003c/span\u003e\n\u003cspan class=\"typ\"\u003eType1\u003c/span\u003e \u003cspan class=\"pln\"\u003efooBar\u003c/span\u003e \u003cspan class=\"pun\"\u003e:\u003c/span\u003e \u003cspan class=\"pln\"\u003eopen\u003c/span\u003e\n\u003cspan class=\"typ\"\u003eType1\u003c/span\u003e \u003cspan class=\"pln\"\u003einteger\u003c/span\u003e\u003cspan class=\"pun\"\u003e-\u003c/span\u003e\u003cspan class=\"kwd\"\u003eor\u003c/span\u003e\u003cspan class=\"pun\"\u003e-\u003c/span\u003e\u003cspan class=\"kwd\"\u003efloat\u003c/span\u003e\u003cspan class=\"pun\"\u003e-\u003c/span\u003e\u003cspan class=\"pln\"\u003elist\u003c/span\u003e \u003cspan class=\"pun\"\u003e:\u003c/span\u003e \u003cspan class=\"dec\"\u003e101\u003c/span\u003e\n\u003cspan class=\"typ\"\u003eType1\u003c/span\u003e \u003cspan class=\"pln\"\u003ereporter\u003c/span\u003e \u003cspan class=\"pun\"\u003e:\u003c/span\u003e \u003cspan class=\"typ\"\u003eFirst\u003c/span\u003e \u003cspan class=\"typ\"\u003eUser\u003c/span\u003e \u003cspan class=\"pun\"\u003e(\u003c/span\u003e\u003cspan class=\"pln\"\u003ejon_doe\u003c/span\u003e\u003cspan class=\"pun\"\u003e)\u003c/span\u003e\n\u003c/code\u003e\u003c/pre\u003e\n\n\u003cp\u003edescription1\u003c/p\u003e\n",
+ "system.metastate": null,
+ "system.number": 1,
+ "system.order": 1000,
+ "system.remote_item_id": null,
+ "system.state": "new",
+ "system.title": "work item 00000000-0000-0000-0000-000000000001",
+ "system.updated_at": "0001-01-01T00:00:00Z",
+ "version": 1
+ },
+ "id": "00000000-0000-0000-0000-000000000002",
+ "links": {
+ "related": "http:///api/workitems/00000000-0000-0000-0000-000000000002",
+ "self": "http:///api/workitems/00000000-0000-0000-0000-000000000002"
+ },
+ "relationships": {
+ "area": {},
+ "assignees": {},
+ "baseType": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000003",
+ "type": "workitemtypes"
+ },
+ "links": {
+ "self": "http:///api/workitemtypes/00000000-0000-0000-0000-000000000003"
+ }
+ },
+ "children": {
+ "links": {
+ "related": "http:///api/workitems/00000000-0000-0000-0000-000000000002/children"
+ },
+ "meta": {
+ "hasChildren": false
+ }
+ },
+ "comments": {
+ "links": {
+ "related": "http:///api/workitems/00000000-0000-0000-0000-000000000002/comments",
+ "self": "http:///api/workitems/00000000-0000-0000-0000-000000000002/relationships/comments"
+ }
+ },
+ "creator": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000004",
+ "type": "users"
+ },
+ "links": {
+ "related": "http:///api/users/00000000-0000-0000-0000-000000000004",
+ "self": "http:///api/users/00000000-0000-0000-0000-000000000004"
+ }
+ },
+ "events": {
+ "links": {
+ "related": "http:///api/workitems/00000000-0000-0000-0000-000000000002/events"
+ }
+ },
+ "iteration": {},
+ "labels": {
+ "links": {
+ "related": "http:///api/workitems/00000000-0000-0000-0000-000000000002/labels"
+ }
+ },
+ "space": {
+ "data": {
+ "id": "00000000-0000-0000-0000-000000000005",
+ "type": "spaces"
+ },
+ "links": {
+ "related": "http:///api/spaces/00000000-0000-0000-0000-000000000005",
+ "self": "http:///api/spaces/00000000-0000-0000-0000-000000000005"
+ }
+ },
+ "system.boardcolumns": {},
+ "workItemLinks": {
+ "links": {
+ "related": "http:///api/workitems/00000000-0000-0000-0000-000000000002/links"
+ }
+ }
+ },
+ "type": "workitems"
+ },
+ "links": {
+ "self": "http:///api/workitems/00000000-0000-0000-0000-000000000002"
+ }
+}
\ No newline at end of file
diff --git a/controller/work_item_events.go b/controller/work_item_events.go
index c6d9e1292e..e9c088aa83 100644
--- a/controller/work_item_events.go
+++ b/controller/work_item_events.go
@@ -1,11 +1,14 @@
package controller
import (
+ "context"
"net/http"
"github.com/fabric8-services/fabric8-wit/app"
"github.com/fabric8-services/fabric8-wit/application"
"github.com/fabric8-services/fabric8-wit/jsonapi"
+ "github.com/fabric8-services/fabric8-wit/ptr"
+ "github.com/fabric8-services/fabric8-wit/rest"
"github.com/fabric8-services/fabric8-wit/workitem"
"github.com/fabric8-services/fabric8-wit/workitem/event"
"github.com/goadesign/goa"
@@ -41,165 +44,189 @@ func (c *EventsController) List(ctx *app.ListWorkItemEventsContext) error {
eventList, err = appl.Events().List(ctx, ctx.WiID)
return errs.Wrap(err, "list events model failed")
})
+
if err != nil {
return jsonapi.JSONErrorResponse(ctx, err)
}
+
+ var convertedEvents []*app.Event
return ctx.ConditionalEntities(eventList, c.config.GetCacheControlEvents, func() error {
- res := &app.EventList{}
- res.Data = ConvertEvents(c.db, ctx.Request, eventList, ctx.WiID)
- return ctx.OK(res)
+ convertedEvents, err = ConvertEvents(ctx, c.db, ctx.Request, eventList, ctx.WiID)
+ if err != nil {
+ return jsonapi.JSONErrorResponse(ctx, errs.Wrapf(err, "failed to convert events"))
+ }
+ return ctx.OK(&app.EventList{
+ Data: convertedEvents,
+ })
})
}
// ConvertEvents from internal to external REST representation
-func ConvertEvents(appl application.Application, request *http.Request, eventList []event.Event, wiID uuid.UUID) []*app.Event {
+func ConvertEvents(ctx context.Context, appl application.Application, request *http.Request, eventList []event.Event, wiID uuid.UUID) ([]*app.Event, error) {
var ls = []*app.Event{}
for _, i := range eventList {
- ls = append(ls, ConvertEvent(appl, request, i, wiID))
+ converted, err := ConvertEvent(ctx, appl, request, i, wiID)
+ if err != nil {
+ return nil, errs.Wrapf(err, "failed to convert event: %+v", i)
+ }
+ ls = append(ls, converted)
}
- return ls
+ return ls, nil
}
// ConvertEvent converts from internal to external REST representation
-func ConvertEvent(appl application.Application, request *http.Request, wiEvent event.Event, wiID uuid.UUID) *app.Event {
- modifierData, modifierLinks := ConvertUserSimple(request, wiEvent.Modifier)
- modifier := &app.RelationGeneric{
- Data: modifierData,
- Links: modifierLinks,
+func ConvertEvent(ctx context.Context, appl application.Application, req *http.Request, wiEvent event.Event, wiID uuid.UUID) (*app.Event, error) {
+ // find out about background details on the field that was modified
+ wit, err := appl.WorkItemTypes().Load(ctx, wiEvent.WorkItemTypeID)
+ if err != nil {
+ return nil, errs.Wrapf(err, "failed to load work item type: %s", wiEvent.WorkItemTypeID)
}
-
- var e *app.Event
- switch wiEvent.Name {
- case workitem.SystemState, workitem.SystemTitle:
- e = &app.Event{
- Type: event.APIStringTypeEvents,
- ID: &wiEvent.ID,
- Attributes: map[string]interface{}{
- "name": wiEvent.Name,
- "newValue": wiEvent.New,
- "oldValue": wiEvent.Old,
- "timestamp": wiEvent.Timestamp,
- },
-
- Relationships: &app.EventRelations{
- Modifier: modifier,
- },
- }
- case workitem.SystemDescription:
- e = &app.Event{
- Type: event.APIStringTypeEvents,
- ID: &wiEvent.ID,
- Attributes: map[string]interface{}{
- "name": wiEvent.Name,
- "newValue": nil,
- "oldValue": nil,
- "timestamp": wiEvent.Timestamp,
- },
-
- Relationships: &app.EventRelations{
- Modifier: modifier,
- },
- }
- case workitem.SystemArea:
- old, _ := ConvertAreaSimple(request, wiEvent.Old)
- new, _ := ConvertAreaSimple(request, wiEvent.New)
- e = &app.Event{
- Type: event.APIStringTypeEvents,
- ID: &wiEvent.ID,
- Attributes: map[string]interface{}{
- "name": wiEvent.Name,
- "newValue": nil,
- "oldValue": nil,
- "timestamp": wiEvent.Timestamp,
+ fieldName := wiEvent.Name
+ fieldDef, ok := wit.Fields[fieldName]
+ if !ok {
+ return nil, errs.Errorf("failed to find field \"%s\" in work item type: %s (%s)", fieldName, wit.Name, wit.ID)
+ }
+ modifierData, modifierLinks := ConvertUserSimple(req, wiEvent.Modifier)
+ e := app.Event{
+ Type: event.APIStringTypeEvents,
+ ID: wiEvent.ID,
+ Attributes: &app.EventAttributes{
+ Name: wiEvent.Name,
+ Timestamp: wiEvent.Timestamp,
+ },
+ Relationships: &app.EventRelations{
+ Modifier: &app.RelationGeneric{
+ Data: modifierData,
+ Links: modifierLinks,
},
-
- Relationships: &app.EventRelations{
- Modifier: modifier,
- OldValue: &app.RelationGenericList{
- Data: []*app.GenericData{old},
+ WorkItemType: &app.RelationGeneric{
+ Links: &app.GenericLinks{
+ Self: ptr.String(rest.AbsoluteURL(req, app.WorkitemtypeHref(wit.ID))),
},
- NewValue: &app.RelationGenericList{
- Data: []*app.GenericData{new},
+ Data: &app.GenericData{
+ ID: ptr.String(wit.ID.String()),
+ Type: ptr.String(APIStringTypeWorkItemType),
},
},
+ },
+ }
+
+ // convertVal returns the given value converted from storage space to
+ // JSONAPI space. If the given value is supposed to be stored as a
+ // relationship in JSONAPI, the second return value will be true.
+ convertVal := func(kind workitem.Kind, val interface{}) (interface{}, bool) {
+ switch kind {
+ case workitem.KindString,
+ workitem.KindInteger,
+ workitem.KindFloat,
+ workitem.KindBoolean,
+ workitem.KindURL,
+ workitem.KindMarkup,
+ workitem.KindDuration, // TODO(kwk): get rid of duration
+ workitem.KindInstant:
+ return val, false
+ case workitem.KindIteration:
+ data, _ := ConvertIterationSimple(req, val)
+ return data, true
+ case workitem.KindUser:
+ data, _ := ConvertUserSimple(req, val)
+ return data, true
+ case workitem.KindLabel:
+ data := ConvertLabelSimple(req, val)
+ return data, true
+ case workitem.KindBoardColumn:
+ data := ConvertBoardColumnSimple(req, val)
+ return data, true
+ case workitem.KindArea:
+ data, _ := ConvertAreaSimple(req, val)
+ return data, true
+ case workitem.KindCodebase:
+ data, _ := ConvertCodebaseSimple(req, val)
+ return data, true
}
- case workitem.SystemIteration:
- old, _ := ConvertIterationSimple(request, wiEvent.Old)
- new, _ := ConvertIterationSimple(request, wiEvent.New)
- e = &app.Event{
- Type: event.APIStringTypeEvents,
- ID: &wiEvent.ID,
- Attributes: map[string]interface{}{
- "name": wiEvent.Name,
- "newValue": nil,
- "oldValue": nil,
- "timestamp": wiEvent.Timestamp,
- },
+ return nil, false
+ }
- Relationships: &app.EventRelations{
- Modifier: modifier,
- OldValue: &app.RelationGenericList{
- Data: []*app.GenericData{old},
- },
- NewValue: &app.RelationGenericList{
- Data: []*app.GenericData{new},
- },
- },
+ kind := fieldDef.Type.GetKind()
+ if kind == workitem.KindEnum {
+ enumType, ok := fieldDef.Type.(workitem.EnumType)
+ if !ok {
+ return nil, errs.Errorf("failed to convert field \"%s\" to enum type: %+v", fieldName, fieldDef)
}
- case workitem.SystemAssignees:
- e = &app.Event{
- Type: event.APIStringTypeEvents,
- ID: &wiEvent.ID,
- Attributes: map[string]interface{}{
- "name": wiEvent.Name,
- "newValue": nil,
- "oldValue": nil,
- "timestamp": wiEvent.Timestamp,
- },
- Relationships: &app.EventRelations{
- Modifier: modifier,
- OldValue: &app.RelationGenericList{
- Data: ConvertUsersSimple(request, wiEvent.Old.([]interface{})),
- },
- NewValue: &app.RelationGenericList{
- Data: ConvertUsersSimple(request, wiEvent.New.([]interface{})),
- },
- },
+ kind = enumType.BaseType.GetKind()
+ }
+
+ // handle all single value fields (including enums)
+ if kind != workitem.KindList {
+ oldVal, useRel := convertVal(kind, wiEvent.Old)
+ newVal, _ := convertVal(kind, wiEvent.New)
+ if useRel {
+ if wiEvent.Old != nil {
+ e.Relationships.OldValue = &app.RelationGenericList{Data: []*app.GenericData{oldVal.(*app.GenericData)}}
+ }
+ if wiEvent.New != nil {
+ e.Relationships.NewValue = &app.RelationGenericList{Data: []*app.GenericData{newVal.(*app.GenericData)}}
+ }
+ } else {
+ if oldVal != nil {
+ e.Attributes.OldValue = &oldVal
+ }
+ if newVal != nil {
+ e.Attributes.NewValue = &newVal
+ }
}
- case workitem.SystemLabels:
- e = &app.Event{
- Type: event.APIStringTypeEvents,
- ID: &wiEvent.ID,
- Attributes: map[string]interface{}{
- "name": wiEvent.Name,
- "newValue": nil,
- "oldValue": nil,
- "timestamp": wiEvent.Timestamp,
- },
- Relationships: &app.EventRelations{
- Modifier: modifier,
- OldValue: &app.RelationGenericList{
- Data: ConvertLabelsSimple(request, wiEvent.Old.([]interface{})),
- },
- NewValue: &app.RelationGenericList{
- Data: ConvertLabelsSimple(request, wiEvent.New.([]interface{})),
- },
- },
+ return &e, nil
+ }
+
+ // handle multi-value fields
+ listType, ok := fieldDef.Type.(workitem.ListType)
+ if !ok {
+ return nil, errs.Errorf("failed to convert field \"%s\" to list type: %+v", fieldName, fieldDef)
+ }
+ componentTypeKind := listType.ComponentType.GetKind()
+
+ arrOld, ok := wiEvent.Old.([]interface{})
+ if !ok {
+ return nil, errs.Errorf("failed to convert old value of field \"%s\" to []interface{}: %+v", fieldName, wiEvent.Old)
+ }
+ arrNew, ok := wiEvent.New.([]interface{})
+ if !ok {
+ return nil, errs.Errorf("failed to convert new value of field \"%s\" to []interface{}: %+v", fieldName, wiEvent.New)
+ }
+
+ for i, v := range arrOld {
+ oldVal, useRel := convertVal(componentTypeKind, v)
+ if useRel {
+ if i == 0 {
+ e.Relationships.OldValue = &app.RelationGenericList{
+ Data: make([]*app.GenericData, len(arrOld)),
+ }
+ }
+ e.Relationships.OldValue.Data[i] = oldVal.(*app.GenericData)
+ } else {
+ if i == 0 {
+ e.Attributes.OldValue = ptr.Interface(make([]interface{}, len(arrOld)))
+ }
+ (*e.Attributes.OldValue).([]interface{})[i] = oldVal
}
- default:
- e = &app.Event{
- Type: event.APIStringTypeEvents,
- ID: &wiEvent.ID,
- Attributes: map[string]interface{}{
- "name": wiEvent.Name,
- "newValue": wiEvent.New,
- "oldValue": wiEvent.Old,
- "timestamp": wiEvent.Timestamp,
- },
- Relationships: &app.EventRelations{
- Modifier: modifier,
- },
+ }
+
+ for i, v := range arrNew {
+ newVal, useRel := convertVal(componentTypeKind, v)
+ if useRel {
+ if i == 0 {
+ e.Relationships.NewValue = &app.RelationGenericList{
+ Data: make([]*app.GenericData, len(arrNew)),
+ }
+ }
+ e.Relationships.NewValue.Data[i] = newVal.(*app.GenericData)
+ } else {
+ if i == 0 {
+ e.Attributes.NewValue = ptr.Interface(make([]interface{}, len(arrNew)))
+ }
+ (*e.Attributes.NewValue).([]interface{})[i] = newVal
}
}
- return e
+
+ return &e, nil
}
diff --git a/controller/work_item_events_test.go b/controller/work_item_events_test.go
index afa75bf255..d1a559e3f4 100644
--- a/controller/work_item_events_test.go
+++ b/controller/work_item_events_test.go
@@ -10,6 +10,7 @@ import (
. "github.com/fabric8-services/fabric8-wit/controller"
"github.com/fabric8-services/fabric8-wit/gormtestsupport"
"github.com/fabric8-services/fabric8-wit/label"
+ "github.com/fabric8-services/fabric8-wit/rendering"
"github.com/fabric8-services/fabric8-wit/resource"
"github.com/fabric8-services/fabric8-wit/rest"
testsupport "github.com/fabric8-services/fabric8-wit/test"
@@ -38,7 +39,7 @@ func (s *TestEvent) SetupTest() {
func (s *TestEvent) TestListEvent() {
s.T().Run("event list ok - state", func(t *testing.T) {
- fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
+ fxt := tf.NewTestFixture(t, s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
svc := testsupport.ServiceAsSpaceUser("Event-Service", *fxt.Identities[0], &TestSpaceAuthzService{*fxt.Identities[0], ""})
eventCtrl := NewEventsController(svc, s.GormDB, s.Configuration)
workitemCtrl := NewWorkitemController(svc, s.GormDB, s.Configuration)
@@ -66,7 +67,7 @@ func (s *TestEvent) TestListEvent() {
})
s.T().Run("event list ok - title", func(t *testing.T) {
- fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
+ fxt := tf.NewTestFixture(t, s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
svc := testsupport.ServiceAsSpaceUser("Event-Service", *fxt.Identities[0], &TestSpaceAuthzService{*fxt.Identities[0], ""})
eventCtrl := NewEventsController(svc, s.GormDB, s.Configuration)
workitemCtrl := NewWorkitemController(svc, s.GormDB, s.Configuration)
@@ -186,18 +187,23 @@ func (s *TestEvent) TestListEvent() {
})
s.T().Run("event list ok - description", func(t *testing.T) {
- fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
+ fxt := tf.NewTestFixture(t, s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
svc := testsupport.ServiceAsSpaceUser("Event-Service", *fxt.Identities[0], &TestSpaceAuthzService{*fxt.Identities[0], ""})
eventCtrl := NewEventsController(svc, s.GormDB, s.Configuration)
workitemCtrl := NewWorkitemController(svc, s.GormDB, s.Configuration)
spaceSelfURL := rest.AbsoluteURL(&http.Request{Host: "api.service.domain.org"}, app.SpaceHref(fxt.Spaces[0].ID.String()))
+
+ modifiedDescription := "# Description is modified1"
+ modifiedMarkup := rendering.SystemMarkupMarkdown
+
payload := app.UpdateWorkitemPayload{
Data: &app.WorkItem{
Type: APIStringTypeWorkItem,
ID: &fxt.WorkItems[0].ID,
Attributes: map[string]interface{}{
- workitem.SystemDescription: "New Description",
- workitem.SystemVersion: fxt.WorkItems[0].Version,
+ workitem.SystemDescription: modifiedDescription,
+ workitem.SystemDescriptionMarkup: modifiedMarkup,
+ workitem.SystemVersion: fxt.WorkItems[0].Version,
},
Relationships: &app.WorkItemRelationships{
Space: app.NewSpaceRelation(fxt.Spaces[0].ID, spaceSelfURL),
@@ -214,7 +220,7 @@ func (s *TestEvent) TestListEvent() {
})
s.T().Run("event list ok - assigned", func(t *testing.T) {
- fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
+ fxt := tf.NewTestFixture(t, s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
svc := testsupport.ServiceAsSpaceUser("Event-Service", *fxt.Identities[0], &TestSpaceAuthzService{*fxt.Identities[0], ""})
eventCtrl := NewEventsController(svc, s.GormDB, s.Configuration)
assignee := []string{fxt.Identities[0].ID.String()}
@@ -288,7 +294,7 @@ func (s *TestEvent) TestListEvent() {
})
s.T().Run("event list ok - iteration", func(t *testing.T) {
- fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
+ fxt := tf.NewTestFixture(t, s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
svc := testsupport.ServiceAsSpaceUser("Event-Service", *fxt.Identities[0], &TestSpaceAuthzService{*fxt.Identities[0], ""})
eventCtrl := NewEventsController(svc, s.GormDB, s.Configuration)
workitemCtrl := NewWorkitemController(svc, s.GormDB, s.Configuration)
@@ -316,7 +322,7 @@ func (s *TestEvent) TestListEvent() {
})
s.T().Run("event list ok - area", func(t *testing.T) {
- fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
+ fxt := tf.NewTestFixture(t, s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
svc := testsupport.ServiceAsSpaceUser("Event-Service", *fxt.Identities[0], &TestSpaceAuthzService{*fxt.Identities[0], ""})
eventCtrl := NewEventsController(svc, s.GormDB, s.Configuration)
workitemCtrl := NewWorkitemController(svc, s.GormDB, s.Configuration)
@@ -344,7 +350,7 @@ func (s *TestEvent) TestListEvent() {
})
s.T().Run("event list - empty", func(t *testing.T) {
- fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
+ fxt := tf.NewTestFixture(t, s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1))
svc := testsupport.ServiceAsSpaceUser("Event-Service", *fxt.Identities[0], &TestSpaceAuthzService{*fxt.Identities[0], ""})
eventCtrl := NewEventsController(svc, s.GormDB, s.Configuration)
res, eventList := test.ListWorkItemEventsOK(t, svc.Context, svc, eventCtrl, fxt.WorkItems[0].ID, nil, nil)
@@ -354,7 +360,7 @@ func (s *TestEvent) TestListEvent() {
})
s.T().Run("many events", func(t *testing.T) {
- fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1), tf.Iterations(2))
+ fxt := tf.NewTestFixture(t, s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1), tf.Iterations(2))
svc := testsupport.ServiceAsSpaceUser("Event-Service", *fxt.Identities[0], &TestSpaceAuthzService{*fxt.Identities[0], ""})
eventCtrl := NewEventsController(svc, s.GormDB, s.Configuration)
workitemCtrl := NewWorkitemController(svc, s.GormDB, s.Configuration)
@@ -416,4 +422,182 @@ func (s *TestEvent) TestListEvent() {
require.NotEmpty(t, eventList)
require.Len(t, eventList.Data, 3)
})
+
+ s.T().Run("non-relational field kinds", func(t *testing.T) {
+ testData := workitem.GetFieldTypeTestData(t)
+ for _, kind := range testData.GetKinds() {
+ if !kind.IsSimpleType() || kind.IsRelational() {
+ continue
+ }
+
+ // TODO(kwk): Once we got rid of the duration kind remove this skip
+ if kind == workitem.KindDuration {
+ continue
+ }
+
+ // TODO(kwk): Once the new type system enhancements are in, also
+ // test instant fields
+ if kind == workitem.KindInstant {
+ continue
+ }
+
+ fieldNameSingle := kind.String() + "_single"
+ fieldNameList := kind.String() + "_list"
+ // NOTE(kwk): Leave this commented out until we have proper test data
+ // fieldNameEnum := kind.String() + "_enum"
+
+ fxt := tf.NewTestFixture(t, s.DB,
+ tf.CreateWorkItemEnvironment(),
+ tf.WorkItemTypes(2, func(fxt *tf.TestFixture, idx int) error {
+ switch idx {
+ case 0:
+ fxt.WorkItemTypes[idx].Fields[fieldNameSingle] = workitem.FieldDefinition{
+ Label: fieldNameSingle,
+ Description: "A single value of a " + kind.String() + " object",
+ Type: workitem.SimpleType{Kind: kind},
+ }
+ case 1:
+ fxt.WorkItemTypes[idx].Fields[fieldNameList] = workitem.FieldDefinition{
+ Label: fieldNameList,
+ Description: "An array of " + kind.String() + " objects",
+ Type: workitem.ListType{
+ SimpleType: workitem.SimpleType{Kind: workitem.KindList},
+ ComponentType: workitem.SimpleType{Kind: kind},
+ },
+ }
+ // NOTE(kwk): Leave this commented out until we have proper test data
+ // case 3:
+ // fxt.WorkItemTypes[idx].Fields[fieldNameEnum] = workitem.FieldDefinition{
+ // Label: fieldNameEnum,
+ // Description: "An enum value of a " + kind.String() + " object",
+ // Type: workitem.EnumType{
+ // SimpleType: workitem.SimpleType{Kind: workitem.KindEnum},
+ // BaseType: workitem.SimpleType{Kind: kind},
+ // Values: []interface{}{
+ // testData[kind].Valid[0],
+ // testData[kind].Valid[1],
+ // },
+ // },
+ // }
+ }
+ return nil
+ }),
+ tf.WorkItems(2, func(fxt *tf.TestFixture, idx int) error {
+ fxt.WorkItems[idx].Type = fxt.WorkItemTypes[idx].ID
+ return nil
+ }),
+ )
+ svc := testsupport.ServiceAsSpaceUser("Event-Service", *fxt.Identities[0], &TestSpaceAuthzService{*fxt.Identities[0], ""})
+ EventCtrl := NewEventsController(svc, s.GormDB, s.Configuration)
+ workitemCtrl := NewWorkitemController(svc, s.GormDB, s.Configuration)
+ spaceSelfURL := rest.AbsoluteURL(&http.Request{Host: "api.service.domain.org"}, app.SpaceHref(fxt.Spaces[0].ID.String()))
+
+ t.Run(fieldNameSingle, func(t *testing.T) {
+ // NOTE(kwk): Leave this commented out until we have proper test data
+ // fieldDef := fxt.WorkItemTypes[0].Fields[fieldNameSingle]
+ // val, err := fieldDef.ConvertFromModel(fieldNameSingle, testData[kind].Valid[0])
+ // require.NoError(t, err)
+ newValue := testData[kind].Valid[0]
+ payload := app.UpdateWorkitemPayload{
+ Data: &app.WorkItem{
+ Type: APIStringTypeWorkItem,
+ ID: &fxt.WorkItems[0].ID,
+ Attributes: map[string]interface{}{
+ fieldNameSingle: newValue,
+ workitem.SystemVersion: fxt.WorkItems[0].Version,
+ },
+ Relationships: &app.WorkItemRelationships{
+ Space: app.NewSpaceRelation(fxt.Spaces[0].ID, spaceSelfURL),
+ },
+ },
+ }
+ // update work item once
+ test.UpdateWorkitemOK(t, svc.Context, svc, workitemCtrl, fxt.WorkItems[0].ID, &payload)
+ // update it twice
+ payload.Data.Attributes[workitem.SystemVersion] = fxt.WorkItems[0].Version + 1
+ payload.Data.Attributes[fieldNameSingle] = testData[kind].Valid[1]
+ test.UpdateWorkitemOK(t, svc.Context, svc, workitemCtrl, fxt.WorkItems[0].ID, &payload)
+
+ res, eventList := test.ListWorkItemEventsOK(t, svc.Context, svc, EventCtrl, fxt.WorkItems[0].ID, nil, nil)
+ safeOverriteHeader(t, res, app.ETag, "1GmclFDDPcLR1ZWPZnykWw==")
+ require.NotEmpty(t, eventList)
+ require.Len(t, eventList.Data, 2)
+ compareWithGoldenAgnostic(t, filepath.Join(s.testDir, "list", "ok."+fieldNameSingle+".res.payload.golden.json"), eventList)
+ })
+ t.Run(fieldNameList, func(t *testing.T) {
+ // NOTE(kwk): Leave this commented out until we have proper test data
+ // listDef := fxt.WorkItemTypes[0].Fields[fieldNameList]
+ // fieldDef, ok := listDef.Type.(workitem.ListType)
+ // require.True(t, ok, "failed to cast %+v (%[1]T) to workitem.ListType", listDef)
+ // vals, err := fieldDef.ConvertFromModel([]interface{}{testData[kind].Valid[0], testData[kind].Valid[1]})
+ // require.NoError(t, err)
+ newValue := []interface{}{testData[kind].Valid[0], testData[kind].Valid[1]}
+ payload := app.UpdateWorkitemPayload{
+ Data: &app.WorkItem{
+ Type: APIStringTypeWorkItem,
+ ID: &fxt.WorkItems[1].ID,
+ Attributes: map[string]interface{}{
+ fieldNameList: newValue,
+ workitem.SystemVersion: fxt.WorkItems[1].Version,
+ },
+ Relationships: &app.WorkItemRelationships{
+ Space: app.NewSpaceRelation(fxt.Spaces[0].ID, spaceSelfURL),
+ },
+ },
+ }
+ // update work item once
+ test.UpdateWorkitemOK(t, svc.Context, svc, workitemCtrl, fxt.WorkItems[1].ID, &payload)
+ // update it twice
+ payload.Data.Attributes[workitem.SystemVersion] = fxt.WorkItems[1].Version + 1
+ payload.Data.Attributes[fieldNameList] = []interface{}{testData[kind].Valid[1], testData[kind].Valid[0]}
+ test.UpdateWorkitemOK(t, svc.Context, svc, workitemCtrl, fxt.WorkItems[1].ID, &payload)
+ res, eventList := test.ListWorkItemEventsOK(t, svc.Context, svc, EventCtrl, fxt.WorkItems[1].ID, nil, nil)
+ safeOverriteHeader(t, res, app.ETag, "1GmclFDDPcLR1ZWPZnykWw==")
+ require.NotEmpty(t, eventList)
+ require.Len(t, eventList.Data, 2)
+ compareWithGoldenAgnostic(t, filepath.Join(s.testDir, "list", "ok."+fieldNameList+".res.payload.golden.json"), eventList)
+ })
+ // NOTE(kwk): Leave this commented out until we have proper test data
+ // TODO(kwk): Once the new type system enhancements are in, also
+ // test for enum fields here.
+
+ // t.Run(fieldNameEnum, func(t *testing.T) {
+ // // NOTE(kwk): Leave this commented out until we have proper test data
+ // // listDef := fxt.WorkItemTypes[0].Fields[fieldNameEnum]
+ // // fieldDef, ok := listDef.Type.(workitem.EnumType)
+ // // require.True(t, ok, "failed to cast %+v (%[1]T) to workitem.EnumType", listDef)
+ // // val, err := fieldDef.ConvertFromModel(testData[kind].Valid[0])
+ // // require.NoError(t, err)
+
+ // // we have to use the second value because we default to the
+ // // first one upon creation of the work item.
+ // newValue := testData[kind].Valid[1]
+ // payload := app.UpdateWorkitemPayload{
+ // Data: &app.WorkItem{
+ // Type: APIStringTypeWorkItem,
+ // ID: &fxt.WorkItems[2].ID,
+ // Attributes: map[string]interface{}{
+ // fieldNameEnum: newValue,
+ // workitem.SystemVersion: fxt.WorkItems[2].Version,
+ // },
+ // Relationships: &app.WorkItemRelationships{
+ // Space: app.NewSpaceRelation(fxt.Spaces[0].ID, spaceSelfURL),
+ // },
+ // },
+ // }
+ // // update work item once
+ // test.UpdateWorkitemOK(t, svc.Context, svc, workitemCtrl, fxt.WorkItems[2].ID, &payload)
+ // // update it twice
+ // payload.Data.Attributes[workitem.SystemVersion] = fxt.WorkItems[2].Version + 1
+ // payload.Data.Attributes[fieldNameEnum] = testData[kind].Valid[1]
+ // test.UpdateWorkitemOK(t, svc.Context, svc, workitemCtrl, fxt.WorkItems[2].ID, &payload)
+ // res, eventList := test.ListWorkItemEventsOK(t, svc.Context, svc, EventCtrl, fxt.WorkItems[2].ID, nil, nil)
+ // safeOverriteHeader(t, res, app.ETag, "1GmclFDDPcLR1ZWPZnykWw==")
+ // require.NotEmpty(t, eventList)
+ // require.Len(t, eventList.Data, 2)
+ // compareWithGoldenAgnostic(t, filepath.Join(s.testDir, "list", "ok."+fieldNameEnum+".res.payload.golden.json"), eventList)
+ // // compareWithGoldenAgnostic(t, filepath.Join(s.testDir, "list", "ok."+fieldNameEnum+".res.headers.golden.json"), res.Header())
+ // })
+ }
+ })
}
diff --git a/controller/workitem.go b/controller/workitem.go
index af7367a688..0f5bbf3c70 100644
--- a/controller/workitem.go
+++ b/controller/workitem.go
@@ -71,6 +71,25 @@ func NewNotifyingWorkitemController(service *goa.Service, db application.DB, not
config: config}
}
+// authorizeWorkitemTypeEditor returns true if the modifier is allowed to change
+// workitem type else it returns false.
+// Only space owner and workitem creator are allowed to change workitem type
+func (c *WorkitemController) authorizeWorkitemTypeEditor(ctx context.Context, spaceID uuid.UUID, creatorID string, editorID string) (bool, error) {
+ // check if workitem editor is same as workitem creator
+ if editorID == creatorID {
+ return true, nil
+ }
+ space, err := c.db.Spaces().Load(ctx, spaceID)
+ if err != nil {
+ return false, errors.NewNotFoundError("space", spaceID.String())
+ }
+ // check if workitem editor is same as space owner
+ if space != nil && editorID == space.OwnerID.String() {
+ return true, nil
+ }
+ return false, errors.NewUnauthorizedError("user is not allowed to change workitem type")
+}
+
// Returns true if the user is the work item creator or space collaborator
func authorizeWorkitemEditor(ctx context.Context, db application.DB, spaceID uuid.UUID, creatorID string, editorID string) (bool, error) {
if editorID == creatorID {
@@ -115,18 +134,48 @@ func (c *WorkitemController) Update(ctx *app.UpdateWorkitemContext) error {
if !authorized {
return jsonapi.JSONErrorResponse(ctx, errors.NewForbiddenError("user is not authorized to access the space"))
}
+
+ if ctx.Payload.Data.Relationships != nil && ctx.Payload.Data.Relationships.BaseType != nil &&
+ ctx.Payload.Data.Relationships.BaseType.Data != nil && ctx.Payload.Data.Relationships.BaseType.Data.ID != wi.Type {
+
+ authorized, err := c.authorizeWorkitemTypeEditor(ctx, wi.SpaceID, creator.(string), currentUserIdentityID.String())
+ if err != nil {
+ return jsonapi.JSONErrorResponse(ctx, err)
+ }
+ if !authorized {
+ return jsonapi.JSONErrorResponse(ctx, errors.NewForbiddenError("user is not authorized to change the workitemtype"))
+ }
+ // Store new values of type and version
+ newType := ctx.Payload.Data.Relationships.BaseType
+ newVersion := ctx.Payload.Data.Attributes[workitem.SystemVersion]
+
+ // Remove version and base type from payload
+ delete(ctx.Payload.Data.Attributes, workitem.SystemVersion)
+ ctx.Payload.Data.Relationships.BaseType = nil
+
+ // Ensure we do not have any other change in payload except type change
+ if (app.WorkItemRelationships{}) != *ctx.Payload.Data.Relationships || len(ctx.Payload.Data.Attributes) > 0 {
+ // Todo(ibrahim) - Change this error to 422 Unprocessable entity
+ // error once we have this error in our error package. Please see
+ // https://github.com/fabric8-services/fabric8-wit/pull/2202#discussion_r208842063
+ return jsonapi.JSONErrorResponse(ctx, errors.NewBadParameterErrorFromString("cannot update type along with other fields"))
+ }
+
+ // Restore the original values
+ ctx.Payload.Data.Relationships.BaseType = newType
+ ctx.Payload.Data.Attributes[workitem.SystemVersion] = newVersion
+
+ }
err = application.Transactional(c.db, func(appl application.Application) error {
- // The Number and Type of a work item are not allowed to be changed
- // which is why we overwrite those values with their old value after the
- // work item was converted.
+ // The Number of a work item is not allowed to be changed which is why
+ // we overwrite the values with its old value after the work item was
+ // converted.
oldNumber := wi.Number
- oldType := wi.Type
err = ConvertJSONAPIToWorkItem(ctx, ctx.Method, appl, *ctx.Payload.Data, wi, wi.Type, wi.SpaceID)
if err != nil {
return err
}
wi.Number = oldNumber
- wi.Type = oldType
wi, err = appl.WorkItems().Save(ctx, wi.SpaceID, *wi, *currentUserIdentityID)
if err != nil {
return errs.Wrap(err, "Error updating work item")
diff --git a/controller/workitem_blackbox_test.go b/controller/workitem_blackbox_test.go
index 8c0202a913..9250c08b68 100644
--- a/controller/workitem_blackbox_test.go
+++ b/controller/workitem_blackbox_test.go
@@ -965,7 +965,7 @@ func (s *WorkItem2Suite) TestWI2UpdateWithNonExistentID() {
func (s *WorkItem2Suite) TestWI2UpdateSetReadOnlyFields() {
// given
- fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1), tf.WorkItemTypes(2))
+ fxt := tf.NewTestFixture(s.T(), s.DB, tf.CreateWorkItemEnvironment(), tf.WorkItems(1), tf.WorkItemTypes(1))
u := minimumRequiredUpdatePayload()
u.Data.Attributes[workitem.SystemTitle] = "Test title"
@@ -973,7 +973,7 @@ func (s *WorkItem2Suite) TestWI2UpdateSetReadOnlyFields() {
u.Data.Attributes[workitem.SystemNumber] = fxt.WorkItems[0].Number + 666
u.Data.ID = &fxt.WorkItems[0].ID
u.Data.Relationships = &app.WorkItemRelationships{
- BaseType: newRelationBaseType(fxt.WorkItemTypes[1].ID),
+ BaseType: newRelationBaseType(fxt.WorkItemTypes[0].ID),
}
// when
@@ -987,6 +987,139 @@ func (s *WorkItem2Suite) TestWI2UpdateSetReadOnlyFields() {
})
}
+func (s *WorkItem2Suite) TestWI2UpdateWorkItemType() {
+ userFullName := []string{"First User", "Second User"}
+ userUserName := []string{"jon_doe", "lorem_ipsum"}
+ fxt := tf.NewTestFixture(s.T(), s.DB,
+ tf.CreateWorkItemEnvironment(),
+ tf.Users(2, func(fxt *tf.TestFixture, idx int) error {
+ fxt.Users[idx].FullName = userFullName[idx]
+ return nil
+ }),
+ tf.Identities(2, func(fxt *tf.TestFixture, idx int) error {
+ fxt.Identities[idx].Username = userUserName[idx]
+ fxt.Identities[idx].User = *fxt.Users[idx]
+ return nil
+ }),
+ tf.WorkItemTypes(2, func(fxt *tf.TestFixture, idx int) error {
+ switch idx {
+ case 0:
+ fxt.WorkItemTypes[idx].Name = "First WorkItem Type"
+ fxt.WorkItemTypes[idx].Fields = map[string]workitem.FieldDefinition{
+ "fooo": {
+ Label: "Type1 fooo",
+ Type: &workitem.SimpleType{Kind: workitem.KindFloat},
+ },
+ "fooBar": {
+ Label: "Type1 fooBar",
+ Type: workitem.EnumType{
+ BaseType: workitem.SimpleType{Kind: workitem.KindString},
+ SimpleType: workitem.SimpleType{Kind: workitem.KindEnum},
+ Values: []interface{}{"open", "done", "closed"},
+ },
+ },
+ "assigned-to": {
+ Label: "Type1 Assigned To",
+ Type: workitem.ListType{
+ SimpleType: workitem.SimpleType{Kind: workitem.KindList},
+ ComponentType: workitem.SimpleType{Kind: workitem.KindUser},
+ },
+ },
+ "bar": {
+ Label: "Type1 bar",
+ Type: &workitem.SimpleType{Kind: workitem.KindString},
+ },
+ "reporter": {
+ Label: "Type1 reporter",
+ Type: &workitem.SimpleType{Kind: workitem.KindUser},
+ },
+ "integer-or-float-list": {
+ Label: "Type1 integer-or-float-list",
+ Type: workitem.ListType{
+ SimpleType: workitem.SimpleType{Kind: workitem.KindList},
+ ComponentType: workitem.SimpleType{Kind: workitem.KindInteger},
+ },
+ },
+ }
+ case 1:
+ fxt.WorkItemTypes[idx].Name = "Second WorkItem Type"
+ fxt.WorkItemTypes[idx].Fields = map[string]workitem.FieldDefinition{
+ "fooo": {
+ Label: "Type2 fooo",
+ Type: &workitem.SimpleType{Kind: workitem.KindFloat},
+ },
+ "bar": {
+ Label: "Type2 bar",
+ Type: &workitem.SimpleType{Kind: workitem.KindInteger},
+ },
+ "fooBar": {
+ Label: "Type2 fooBar",
+ Type: workitem.EnumType{
+ BaseType: workitem.SimpleType{Kind: workitem.KindString},
+ SimpleType: workitem.SimpleType{Kind: workitem.KindEnum},
+ Values: []interface{}{"alpha", "beta", "gamma"},
+ },
+ },
+ "integer-or-float-list": {
+ Label: "Type2 integer-or-float-list",
+ Type: workitem.ListType{
+ SimpleType: workitem.SimpleType{Kind: workitem.KindList},
+ ComponentType: workitem.SimpleType{Kind: workitem.KindFloat},
+ },
+ },
+ }
+ }
+ return nil
+ }),
+ tf.WorkItems(1, func(fxt *tf.TestFixture, idx int) error {
+ fxt.WorkItems[idx].Type = fxt.WorkItemTypes[0].ID
+ fxt.WorkItems[idx].Fields["integer-or-float-list"] = []int{101}
+ fxt.WorkItems[idx].Fields["fooo"] = 2.5
+ fxt.WorkItems[idx].Fields["fooBar"] = "open"
+ fxt.WorkItems[idx].Fields["bar"] = "hello"
+ fxt.WorkItems[idx].Fields["reporter"] = fxt.Identities[0].ID.String()
+ fxt.WorkItems[idx].Fields["assigned-to"] = []string{fxt.Identities[0].ID.String(), fxt.Identities[1].ID.String()}
+ fxt.WorkItems[idx].Fields[workitem.SystemDescription] = rendering.NewMarkupContentFromLegacy("description1")
+ return nil
+ }),
+ )
+ // when
+ u := minimumRequiredUpdatePayload()
+ u.Data.Attributes[workitem.SystemVersion] = fxt.WorkItems[0].Version
+ u.Data.ID = &fxt.WorkItems[0].ID
+ u.Data.Relationships = &app.WorkItemRelationships{
+ BaseType: newRelationBaseType(fxt.WorkItemTypes[1].ID),
+ }
+ svc := testsupport.ServiceAsUser("TypeChangeService", *fxt.Identities[0])
+ s.T().Run("ok", func(t *testing.T) {
+ _, newWI := test.UpdateWorkitemOK(t, svc.Context, svc, s.workitemCtrl, fxt.WorkItems[0].ID, &u)
+
+ assert.Equal(t, fxt.WorkItemTypes[1].ID, newWI.Data.Relationships.BaseType.Data.ID)
+ newDescription := newWI.Data.Attributes[workitem.SystemDescription]
+ assert.NotNil(t, newDescription)
+ // Type of old and new field is same
+ assert.NotContains(t, newDescription, fxt.WorkItemTypes[0].Fields["fooo"].Label)
+ assert.Contains(t, newDescription, fxt.WorkItemTypes[0].Fields["bar"].Label)
+ assert.Contains(t, newDescription, fxt.WorkItemTypes[0].Fields["fooBar"].Label)
+ assert.Equal(t, "alpha", newWI.Data.Attributes["fooBar"]) // First value of enum for field foobar
+ compareWithGoldenAgnostic(t, filepath.Join(s.testDir, "update", "workitem_type.res.payload.golden.json"), newWI)
+ })
+
+ s.T().Run("disallow update of field along with type", func(t *testing.T) {
+ u.Data.Attributes[workitem.SystemTitle] = "xyz"
+ // TODO (ibrahim) - Check type of error once error 422 has been added.
+ //https://github.com/fabric8-services/fabric8-wit/pull/2202#discussion_r210184092
+ test.UpdateWorkitemConflict(t, svc.Context, svc, s.workitemCtrl, fxt.WorkItems[0].ID, &u)
+ })
+
+ s.T().Run("unauthorized", func(t *testing.T) {
+ // Only Space owner and workitem creator is allowed to change type
+ svcNotAuthorized := testsupport.ServiceAsSpaceUser("TypeChange-Service", *fxt.Identities[1], &TestSpaceAuthzService{*fxt.Identities[0], ""})
+ workitemCtrlNotAuthorized := NewWorkitemController(svcNotAuthorized, s.GormDB, s.Configuration)
+ test.UpdateWorkitemForbidden(t, svcNotAuthorized.Context, svcNotAuthorized, workitemCtrlNotAuthorized, fxt.WorkItems[0].ID, &u)
+ })
+}
+
func (s *WorkItem2Suite) TestWI2UpdateFieldOfDifferentSimpleTypes() {
vals := workitem.GetFieldTypeTestData(s.T())
kinds := vals.GetKinds()
diff --git a/design/work_item_event.go b/design/work_item_event.go
index 0cee367212..9c3c7a389a 100644
--- a/design/work_item_event.go
+++ b/design/work_item_event.go
@@ -13,12 +13,10 @@ var event = a.Type("Event", func() {
a.Attribute("id", d.UUID, "ID of event", func() {
a.Example("40bbdd3d-8b5d-4fd6-ac90-7236b669af04")
})
- a.Attribute("attributes", a.HashOf(d.String, d.Any), func() {
- a.Example(map[string]interface{}{"version": "1", "system.state": "new", "system.title": "Example story"})
- })
+ a.Attribute("attributes", eventAttributes)
a.Attribute("relationships", eventRelationships)
a.Attribute("links", genericLinks)
- a.Required("type")
+ a.Required("type", "relationships", "attributes", "id")
})
var eventAttributes = a.Type("EventAttributes", func() {
@@ -27,7 +25,7 @@ var eventAttributes = a.Type("EventAttributes", func() {
a.Example("2016-11-29T23:18:14Z")
})
a.Attribute("name", d.String, "The name of the event occured", func() {
- a.Example("closed")
+ a.Example("system.title")
})
a.Attribute("oldValue", d.Any, "The user who was assigned to (or unassigned from). Only for 'assigned' and 'unassigned' events.", func() {
a.Example("813a456e-1c8a-48df-ac15-84065ee039f7")
@@ -42,6 +40,9 @@ var eventRelationships = a.Type("EventRelations", func() {
a.Attribute("modifier", relationGeneric, "This defines the modifier of the event")
a.Attribute("oldValue", relationGenericList)
a.Attribute("newValue", relationGenericList)
+ a.Attribute("workItemType", relationGeneric, "The type of the work item at the event's point in time")
+
+ a.Required("workItemType", "modifier")
})
var eventList = JSONList(
diff --git a/migration/migration_blackbox_test.go b/migration/migration_blackbox_test.go
index 36b41bef9c..5b7aa66531 100644
--- a/migration/migration_blackbox_test.go
+++ b/migration/migration_blackbox_test.go
@@ -1235,12 +1235,6 @@ func executeSQLTestFile(filename string, args ...string) fn {
}
}
-func testMigration95Boards(t *testing.T) {
- migrateToVersion(t, sqlDB, migrations[:95], 95)
- assert.True(t, dialect.HasTable("work_item_boards"))
- assert.True(t, dialect.HasTable("work_item_board_columns"))
-}
-
// test that the userspace_data table no longer exists - previously
// used as a temporary solution to get data from tenant jenkins
func testDropUserspacedataTable(t *testing.T) {
diff --git a/test/testfixture/make_functions.go b/test/testfixture/make_functions.go
index d27afeed62..e1a0643b36 100644
--- a/test/testfixture/make_functions.go
+++ b/test/testfixture/make_functions.go
@@ -25,6 +25,30 @@ import (
uuid "github.com/satori/go.uuid"
)
+func makeUsers(fxt *TestFixture) error {
+ if fxt.info[kindUsers] == nil {
+ return nil
+ }
+ userRepo := account.NewUserRepository(fxt.db)
+ fxt.Users = make([]*account.User, fxt.info[kindUsers].numInstances)
+ for i := range fxt.Users {
+ id := uuid.NewV4()
+ fxt.Users[i] = &account.User{
+ ID: id,
+ Email: fmt.Sprintf("%s@example.com", id),
+ FullName: testsupport.CreateRandomValidTestName("user"),
+ }
+ if err := fxt.runCustomizeEntityFuncs(i, kindUsers); err != nil {
+ return errs.WithStack(err)
+ }
+ err := userRepo.Create(fxt.ctx, fxt.Users[i])
+ if err != nil {
+ return errs.Wrapf(err, "failed to create user: %+v", fxt.Users[i])
+ }
+ }
+ return nil
+}
+
func makeIdentities(fxt *TestFixture) error {
if fxt.info[kindIdentities] == nil {
return nil
@@ -34,6 +58,7 @@ func makeIdentities(fxt *TestFixture) error {
fxt.Identities[i] = &account.Identity{
Username: testsupport.CreateRandomValidTestName("John Doe "),
ProviderType: account.KeycloakIDP,
+ User: *fxt.Users[0],
}
if err := fxt.runCustomizeEntityFuncs(i, kindIdentities); err != nil {
return errs.WithStack(err)
diff --git a/test/testfixture/recipe_funcs.go b/test/testfixture/recipe_funcs.go
index b652517928..0bfb5f9f13 100644
--- a/test/testfixture/recipe_funcs.go
+++ b/test/testfixture/recipe_funcs.go
@@ -22,28 +22,55 @@ func (fxt *TestFixture) deps(fns ...RecipeFunction) error {
return nil
}
-// CustomizeIdentityFunc is directly compatible with CustomizeEntityFunc
-// but it can only be used for the Identites() recipe-function.
-type CustomizeIdentityFunc CustomizeEntityFunc
+// CustomizeUserFunc is directly compatible with CustomizeEntityFunc
+// but it can only be used for the Users() recipe-function.
+type CustomizeUserFunc CustomizeEntityFunc
-// Identities tells the test fixture to create at least n identity objects.
+// Users tells the test fixture to create at least n user objects.
//
// If called multiple times with differently n's, the biggest n wins. All
// customize-entitiy-functions fns from all calls will be respected when
// creating the test fixture.
//
-// Here's an example how you can create 42 identites and give them a numbered
-// user name like "John Doe 0", "John Doe 1", and so forth:
-// Identities(42, func(fxt *TestFixture, idx int) error{
-// fxt.Identities[idx].Username = "Jane Doe " + strconv.FormatInt(idx, 10)
+// Here's an example how you can create 10 users and give them a numbered
+// fullname like "John Doe 0", "John Doe 1", and so forth:
+// Users(10, func(fxt *TestFixture, idx int) error{
+// fxt.Users[idx].FullName = "Jane Doe " + strconv.FormatInt(idx, 10)
// return nil
// })
// Notice that the index idx goes from 0 to n-1 and that you have to manually
-// lookup the object from the test fixture. The identity object referenced by
-// fxt.Identities[idx]
+// lookup the object from the test fixture. The User object referenced by
+// fxt.Users[idx]
// is guaranteed to be ready to be used for creation. That means, you don't
// necessarily have to touch it to avoid unique key violation for example. This
// is totally optional.
+func Users(n int, fns ...CustomizeUserFunc) RecipeFunction {
+ return func(fxt *TestFixture) error {
+ fxt.checkFuncs = append(fxt.checkFuncs, func() error {
+ l := len(fxt.Users)
+ if l < n {
+ return errs.Errorf(checkStr, n, kindUsers, l)
+ }
+ return nil
+ })
+ // Convert fns to []CustomizeEntityFunc
+ customFuncs := make([]CustomizeEntityFunc, len(fns))
+ for idx := range fns {
+ customFuncs[idx] = CustomizeEntityFunc(fns[idx])
+ }
+ return fxt.setupInfo(n, kindUsers, customFuncs...)
+ }
+}
+
+// CustomizeIdentityFunc is directly compatible with CustomizeEntityFunc
+// but it can only be used for the Identites() recipe-function.
+type CustomizeIdentityFunc CustomizeEntityFunc
+
+// Identities tells the test fixture to create at least n identity objects.
+// See also the Users() function for more general information on n and fns.
+// When called in NewFixture() this function call will also call
+// Users(1)
+// but with NewFixtureIsolated(), no other objects will be created.
func Identities(n int, fns ...CustomizeIdentityFunc) RecipeFunction {
return func(fxt *TestFixture) error {
fxt.checkFuncs = append(fxt.checkFuncs, func() error {
@@ -58,7 +85,10 @@ func Identities(n int, fns ...CustomizeIdentityFunc) RecipeFunction {
for idx := range fns {
customFuncs[idx] = CustomizeEntityFunc(fns[idx])
}
- return fxt.setupInfo(n, kindIdentities, customFuncs...)
+ if err := fxt.setupInfo(n, kindIdentities, customFuncs...); err != nil {
+ return err
+ }
+ return fxt.deps(Users(1))
}
}
diff --git a/test/testfixture/testfixture.go b/test/testfixture/testfixture.go
index 8a3ce0b796..7209c77549 100644
--- a/test/testfixture/testfixture.go
+++ b/test/testfixture/testfixture.go
@@ -38,7 +38,8 @@ type TestFixture struct {
customLinkCreation bool // on when you've used WorkItemLinksCustom in your recipe
normalLinkCreation bool // on when you've used WorkItemLinks in your recipe
- Identities []*account.Identity // Itentities (if any) that were created for this test fixture.
+ Users []*account.User // Users (if any) that were created for this test fixture.
+ Identities []*account.Identity // Identities (if any) that were created for this test fixture.
Iterations []*iteration.Iteration // Iterations (if any) that were created for this test fixture.
Areas []*area.Area // Areas (if any) that were created for this test fixture.
Spaces []*space.Space // Spaces (if any) that were created for this test fixture.
@@ -154,6 +155,7 @@ func (fxt *TestFixture) Check() error {
type kind string
const (
+ kindUsers kind = "user"
kindIdentities kind = "identity"
kindIterations kind = "iteration"
kindAreas kind = "area"
@@ -221,11 +223,12 @@ func newFixture(db *gorm.DB, isolatedCreation bool, recipeFuncs ...RecipeFunctio
}
makeFuncs := []func(fxt *TestFixture) error{
// make the objects that DON'T have any dependency
- makeIdentities,
+ makeUsers,
makeTrackers,
makeWorkItemLinkCategories,
makeSpaceTemplates,
// actually make the objects that DO have dependencies
+ makeIdentities,
makeSpaces,
makeLabels,
makeQueries,
diff --git a/test/testfixture/testfixture_test.go b/test/testfixture/testfixture_test.go
index 83364ce91d..8c31dfabf8 100644
--- a/test/testfixture/testfixture_test.go
+++ b/test/testfixture/testfixture_test.go
@@ -98,13 +98,13 @@ func checkNewFixture(t *testing.T, db *gorm.DB, n int, isolated bool) {
}
}
- // identity and work item link categories will always work
-
- t.Run("identities", func(t *testing.T) {
+ // user and work item link categories will always work
+ t.Run("users", func(t *testing.T) {
// given
- c, err := fxtCtor(db, tf.Identities(n))
+ c, err := fxtCtor(db, tf.Users(n))
// then
require.NoError(t, err)
+ require.NotNil(t, c)
require.Nil(t, c.Check())
// manual checking
require.Len(t, c.Identities, n)
@@ -118,6 +118,19 @@ func checkNewFixture(t *testing.T, db *gorm.DB, n int, isolated bool) {
// manual checking
require.Len(t, c.WorkItemLinkCategories, n)
})
+
+ t.Run("identities", func(t *testing.T) {
+ // given
+ c, err := fxtCtor(db, tf.Identities(n))
+ // then
+ require.NoError(t, err)
+ require.Nil(t, c.Check())
+ // manual checking
+ require.Len(t, c.Identities, n)
+ if !isolated {
+ require.Len(t, c.Users, 1)
+ }
+ })
t.Run("space_templates", func(t *testing.T) {
// given
c, err := fxtCtor(db, tf.SpaceTemplates(n))
diff --git a/workitem/enum_type.go b/workitem/enum_type.go
index 06a7c9c939..3d9f06e710 100644
--- a/workitem/enum_type.go
+++ b/workitem/enum_type.go
@@ -129,7 +129,7 @@ func (t EnumType) ConvertToModel(value interface{}) (interface{}, error) {
}
if !contains(t.Values, converted) {
- return nil, errs.Errorf("not an enum value: %v", value)
+ return nil, fmt.Errorf("value: %+v (%[1]T) is not part of allowed enum values: %+v", value, t.Values)
}
return converted, nil
}
diff --git a/workitem/event/event.go b/workitem/event/event.go
index 57bfee74d0..546f12fb0a 100644
--- a/workitem/event/event.go
+++ b/workitem/event/event.go
@@ -8,12 +8,13 @@ import (
// Event represents work item event
type Event struct {
- ID uuid.UUID
- Name string
- Timestamp time.Time
- Modifier uuid.UUID
- Old interface{}
- New interface{}
+ ID uuid.UUID
+ Name string
+ WorkItemTypeID uuid.UUID
+ Timestamp time.Time
+ Modifier uuid.UUID
+ Old interface{}
+ New interface{}
}
// GetETagData returns the field values to use to generate the ETag
diff --git a/workitem/event/event_repository.go b/workitem/event/event_repository.go
index a70265e3c8..6adfab9255 100644
--- a/workitem/event/event_repository.go
+++ b/workitem/event/event_repository.go
@@ -51,13 +51,8 @@ func (r *GormEventRepository) List(ctx context.Context, wiID uuid.UUID) ([]Event
if revisionList == nil {
return []Event{}, nil
}
- wi, err := r.workItemRepo.LoadByID(ctx, wiID)
- if err != nil {
- return nil, errs.Wrapf(err, "failed to load work item: %s", wiID)
- }
- wiType, err := r.workItemTypeRepo.Load(ctx, wi.Type)
- if err != nil {
- return nil, errs.Wrapf(err, "failed to load work item type: %s", wiType)
+ if err = r.workItemRepo.CheckExists(ctx, wiID); err != nil {
+ return nil, errs.Wrapf(err, "failed to find work item: %s", wiID)
}
eventList := []Event{}
@@ -66,28 +61,42 @@ func (r *GormEventRepository) List(ctx context.Context, wiID uuid.UUID) ([]Event
oldRev := revisionList[k-1]
newRev := revisionList[k]
+ // If the new and old work item type are different, we're skipping this
+ // revision because it denotes the change of a work item type.
+ //
+ // TODO(kwk): make sure we have a proper "changed work item type"
+ // revision entry in one way or another.
+ if oldRev.WorkItemTypeID != newRev.WorkItemTypeID {
+ continue
+ }
+
+ wit, err := r.workItemTypeRepo.Load(ctx, oldRev.WorkItemTypeID)
+ if err != nil {
+ return nil, errs.Wrapf(err, "failed to load old work item type: %s", oldRev.WorkItemTypeID)
+ }
+
modifierID, err := r.identityRepo.Load(ctx, newRev.ModifierIdentity)
if err != nil {
return nil, errs.Wrapf(err, "failed to load modifier identity %s", newRev.ModifierIdentity)
}
- for fieldName, fieldDef := range wiType.Fields {
+ for fieldName, fieldDef := range wit.Fields {
oldVal := oldRev.WorkItemFields[fieldName]
newVal := newRev.WorkItemFields[fieldName]
event := Event{
- ID: revisionList[k].ID,
- Name: fieldName,
- Timestamp: revisionList[k].Time,
- Modifier: modifierID.ID,
- Old: oldVal,
- New: newVal,
+ ID: newRev.ID,
+ Name: fieldName,
+ WorkItemTypeID: newRev.WorkItemTypeID,
+ Timestamp: newRev.Time,
+ Modifier: modifierID.ID,
+ Old: oldVal,
+ New: newVal,
}
- /// The enum type can be handled by the simple type since it's just
- // an single value after all. Let's overwrite the field type if
- // doable.
+ // The enum type can be handled by the simple type since it's just a
+ // single value after all.
ft := fieldDef.Type
enumType, isEnumType := ft.(workitem.EnumType)
if isEnumType {
@@ -134,39 +143,27 @@ func (r *GormEventRepository) List(ctx context.Context, wiID uuid.UUID) ([]Event
eventList = append(eventList, event)
}
case workitem.SimpleType:
- switch fieldType.GetKind() {
- case workitem.KindString,
- workitem.KindFloat,
- workitem.KindInteger,
- workitem.KindIteration,
- workitem.KindBoardColumn,
- workitem.KindArea,
- workitem.KindLabel,
- workitem.KindMarkup:
-
- // compensate conversion from storage if this really was an enum field
- converter := fieldType.ConvertFromModel
- if isEnumType {
- converter = enumType.ConvertFromModel
- }
-
- p, err := converter(oldVal)
- if err != nil {
- return nil, errs.Wrapf(err, "failed to convert old value for field %s from storage representation: %+v", fieldName, oldVal)
- }
- n, err := converter(newVal)
- if err != nil {
- return nil, errs.Wrapf(err, "failed to convert new value for field %s from storage representation: %+v", fieldName, newVal)
- }
+ // compensate conversion from storage if this really was an enum field
+ converter := fieldType.ConvertFromModel
+ if isEnumType {
+ converter = enumType.ConvertFromModel
+ }
- if !reflect.DeepEqual(p, n) {
- event.Old = p
- event.New = n
- eventList = append(eventList, event)
- }
+ p, err := converter(oldVal)
+ if err != nil {
+ return nil, errs.Wrapf(err, "failed to convert old value for field %s from storage representation: %+v", fieldName, oldVal)
+ }
+ n, err := converter(newVal)
+ if err != nil {
+ return nil, errs.Wrapf(err, "failed to convert new value for field %s from storage representation: %+v", fieldName, newVal)
+ }
+ if !reflect.DeepEqual(p, n) {
+ event.Old = p
+ event.New = n
+ eventList = append(eventList, event)
}
default:
- return nil, errors.NewNotFoundError("unknown field type", fieldName)
+ return nil, errors.NewNotFoundError("unknown field type", fieldType.GetKind().String())
}
}
}
diff --git a/workitem/event/event_repository_blackbox_test.go b/workitem/event/event_repository_blackbox_test.go
index 40d3ea60af..ea013fb892 100644
--- a/workitem/event/event_repository_blackbox_test.go
+++ b/workitem/event/event_repository_blackbox_test.go
@@ -114,7 +114,7 @@ func (s *eventRepoBlackBoxTest) TestList() {
require.Len(t, eventList, 1)
require.Equal(t, oldDescription, eventList[0].Old)
require.Equal(t, newDescription, eventList[0].New)
- require.Equal(t, wiNew.Fields[workitem.SystemDescription], newDescription)
+ require.Equal(t, newDescription, wiNew.Fields[workitem.SystemDescription])
})
s.T().Run("event assignee - new assignee nil", func(t *testing.T) {
@@ -172,7 +172,7 @@ func (s *eventRepoBlackBoxTest) TestList() {
require.NotEmpty(t, eventList)
require.Len(t, eventList, 2)
assert.Equal(t, workitem.SystemAssignees, eventList[1].Name)
- assert.Equal(t, fxt.Identities[1].ID, eventList[1].New.([]interface{})[0])
+ assert.Equal(t, fxt.Identities[1].ID.String(), eventList[1].New.([]interface{})[0])
})
s.T().Run("state change from new to open", func(t *testing.T) {
@@ -194,7 +194,7 @@ func (s *eventRepoBlackBoxTest) TestList() {
fxt := tf.NewTestFixture(t, s.DB, tf.WorkItems(1))
labelID1 := uuid.NewV4()
- labels := []uuid.UUID{labelID1}
+ labels := []string{labelID1.String()}
fxt.WorkItems[0].Fields[workitem.SystemLabels] = labels
wiNew, err := s.wiRepo.Save(s.Ctx, fxt.WorkItems[0].SpaceID, *fxt.WorkItems[0], fxt.Identities[0].ID)
@@ -206,10 +206,10 @@ func (s *eventRepoBlackBoxTest) TestList() {
require.Len(t, eventList, 1)
assert.Equal(t, workitem.SystemLabels, eventList[0].Name)
assert.Empty(t, eventList[0].Old)
- assert.Equal(t, labelID1, eventList[0].New.([]interface{})[0])
+ assert.Equal(t, labelID1.String(), eventList[0].New.([]interface{})[0])
labelID2 := uuid.NewV4()
- labels = []uuid.UUID{labelID2}
+ labels = []string{labelID2.String()}
wiNew.Fields[workitem.SystemLabels] = labels
wiNew.Version = fxt.WorkItems[0].Version + 1
wiNew, err = s.wiRepo.Save(s.Ctx, fxt.WorkItems[0].SpaceID, *wiNew, fxt.Identities[0].ID)
@@ -221,8 +221,8 @@ func (s *eventRepoBlackBoxTest) TestList() {
assert.Equal(t, workitem.SystemLabels, eventList[1].Name)
assert.NotEmpty(t, eventList[1].Old)
assert.NotEmpty(t, eventList[1].New)
- assert.Equal(t, labelID1, eventList[0].New.([]interface{})[0])
- assert.Equal(t, labelID2, eventList[1].New.([]interface{})[0])
+ assert.Equal(t, labelID1.String(), eventList[0].New.([]interface{})[0])
+ assert.Equal(t, labelID2.String(), eventList[1].New.([]interface{})[0])
})
s.T().Run("event label - previous label nil", func(t *testing.T) {
@@ -230,7 +230,7 @@ func (s *eventRepoBlackBoxTest) TestList() {
fxt := tf.NewTestFixture(t, s.DB, tf.WorkItems(1))
labelID1 := uuid.NewV4()
- labels := []uuid.UUID{labelID1}
+ labels := []string{labelID1.String()}
fxt.WorkItems[0].Fields[workitem.SystemLabels] = labels
wiNew, err := s.wiRepo.Save(s.Ctx, fxt.WorkItems[0].SpaceID, *fxt.WorkItems[0], fxt.Identities[0].ID)
@@ -359,7 +359,7 @@ func (s *eventRepoBlackBoxTest) TestList() {
s.T().Run("multiple events", func(t *testing.T) {
fxt := tf.NewTestFixture(t, s.DB, tf.WorkItems(1))
- fxt.WorkItems[0].Fields[workitem.SystemLabels] = []uuid.UUID{uuid.NewV4()}
+ fxt.WorkItems[0].Fields[workitem.SystemLabels] = []string{uuid.NewV4().String()}
fxt.WorkItems[0].Fields[workitem.SystemState] = workitem.SystemStateResolved
_, err := s.wiRepo.Save(s.Ctx, fxt.WorkItems[0].SpaceID, *fxt.WorkItems[0], fxt.Identities[0].ID)
require.NoError(t, err)
diff --git a/workitem/field_definition.go b/workitem/field_definition.go
index eb73412218..4e8117e03a 100644
--- a/workitem/field_definition.go
+++ b/workitem/field_definition.go
@@ -13,22 +13,25 @@ import (
// constants for describing possible field types
const (
- KindString Kind = "string"
- KindInteger Kind = "integer"
- KindFloat Kind = "float"
- KindBoolean Kind = "bool"
- KindInstant Kind = "instant"
- KindDuration Kind = "duration"
- KindURL Kind = "url"
+ // non-relational
+ KindString Kind = "string"
+ KindInteger Kind = "integer"
+ KindFloat Kind = "float"
+ KindBoolean Kind = "bool"
+ KindInstant Kind = "instant"
+ KindDuration Kind = "duration"
+ KindURL Kind = "url"
+ KindMarkup Kind = "markup"
+ // relational
KindIteration Kind = "iteration"
KindUser Kind = "user"
KindLabel Kind = "label"
KindBoardColumn Kind = "boardcolumn"
- KindEnum Kind = "enum"
- KindList Kind = "list"
- KindMarkup Kind = "markup"
KindArea Kind = "area"
KindCodebase Kind = "codebase"
+ // composite
+ KindEnum Kind = "enum"
+ KindList Kind = "list"
)
// Kind is the kind of field type
@@ -39,6 +42,21 @@ func (k Kind) IsSimpleType() bool {
return k != KindEnum && k != KindList
}
+// IsRelational returns 'true' if the kind must be represented with a
+// relationship.
+func (k Kind) IsRelational() bool {
+ switch k {
+ case KindIteration,
+ KindUser,
+ KindLabel,
+ KindBoardColumn,
+ KindArea,
+ KindCodebase:
+ return true
+ }
+ return false
+}
+
// String implements the Stringer interface and returns the kind as a string
// object.
func (k Kind) String() string {
diff --git a/workitem/field_definition_blackbox_test.go b/workitem/field_definition_blackbox_test.go
index 926736176b..b582c0e126 100644
--- a/workitem/field_definition_blackbox_test.go
+++ b/workitem/field_definition_blackbox_test.go
@@ -7,6 +7,8 @@ import (
"github.com/fabric8-services/fabric8-wit/resource"
"github.com/fabric8-services/fabric8-wit/workitem"
+ uuid "github.com/satori/go.uuid"
+ "github.com/stretchr/testify/require"
)
func testFieldDefinitionMarshalUnmarshal(t *testing.T, def workitem.FieldDefinition) {
@@ -74,3 +76,24 @@ func TestFieldDefinition_Marshalling(t *testing.T) {
testFieldDefinitionMarshalUnmarshal(t, def)
})
}
+
+func TestFieldDefinition_IsRelational(t *testing.T) {
+ // relational kinds
+ require.True(t, workitem.KindLabel.IsRelational())
+ require.True(t, workitem.KindArea.IsRelational())
+ require.True(t, workitem.KindIteration.IsRelational())
+ require.True(t, workitem.KindBoardColumn.IsRelational())
+ require.True(t, workitem.KindUser.IsRelational())
+ require.True(t, workitem.KindCodebase.IsRelational())
+ // composite kinds
+ require.False(t, workitem.KindList.IsRelational())
+ require.False(t, workitem.KindEnum.IsRelational())
+ // non-relational kinds
+ require.False(t, workitem.KindString.IsRelational())
+ require.False(t, workitem.KindInteger.IsRelational())
+ require.False(t, workitem.KindInstant.IsRelational())
+ require.False(t, workitem.KindFloat.IsRelational())
+ require.False(t, workitem.KindBoolean.IsRelational())
+ // random
+ require.False(t, workitem.Kind(uuid.NewV4().String()).IsRelational())
+}
diff --git a/workitem/simple_type.go b/workitem/simple_type.go
index 26037091e7..237cfb41fb 100644
--- a/workitem/simple_type.go
+++ b/workitem/simple_type.go
@@ -349,6 +349,12 @@ func (t SimpleType) ConvertToModel(value interface{}) (interface{}, error) {
return nil, errs.Errorf("value %v (%[1]T) has no valid markup type %s", value, markupContent.Markup)
}
return markupContent.ToMap(), nil
+ case map[string]interface{}:
+ markupContent := rendering.NewMarkupContentFromValue(value)
+ if !rendering.IsMarkupSupported(markupContent.Markup) {
+ return nil, errs.Errorf("value %v (%[1]T) has no valid markup type %s", value, markupContent.Markup)
+ }
+ return markupContent.ToMap(), nil
default:
return nil, errs.Errorf("value %v (%[1]T) should be rendering.MarkupContent, but is %s", value, valueType)
}
diff --git a/workitem/workitem_repository.go b/workitem/workitem_repository.go
index a6e8c4d615..4c2218a8f8 100644
--- a/workitem/workitem_repository.go
+++ b/workitem/workitem_repository.go
@@ -1,16 +1,22 @@
package workitem
import (
+ "bytes"
"context"
"fmt"
+ "sort"
"strconv"
"strings"
+ "text/template"
"time"
- "github.com/fabric8-services/fabric8-wit/closeable"
+ "github.com/fabric8-services/fabric8-wit/label"
"github.com/fabric8-services/fabric8-wit/account"
"github.com/fabric8-services/fabric8-wit/application/repository"
+ "github.com/fabric8-services/fabric8-wit/area"
+ "github.com/fabric8-services/fabric8-wit/closeable"
+ "github.com/fabric8-services/fabric8-wit/codebase"
"github.com/fabric8-services/fabric8-wit/criteria"
"github.com/fabric8-services/fabric8-wit/errors"
"github.com/fabric8-services/fabric8-wit/iteration"
@@ -97,25 +103,28 @@ type WorkItemRepository interface {
GetCountsPerIteration(ctx context.Context, spaceID uuid.UUID) (map[string]WICountsPerIteration, error)
GetCountsForIteration(ctx context.Context, itr *iteration.Iteration) (map[string]WICountsPerIteration, error)
Count(ctx context.Context, spaceID uuid.UUID, criteria criteria.Expression) (int, error)
+ ChangeWorkItemType(ctx context.Context, wiStorage *WorkItemStorage, oldWIType *WorkItemType, newWIType *WorkItemType, spaceID uuid.UUID) error
}
// NewWorkItemRepository creates a GormWorkItemRepository
func NewWorkItemRepository(db *gorm.DB) *GormWorkItemRepository {
repository := &GormWorkItemRepository{
- db: db,
- winr: numbersequence.NewWorkItemNumberSequenceRepository(db),
- witr: &GormWorkItemTypeRepository{db},
- wirr: &GormRevisionRepository{db},
+ db: db,
+ winr: numbersequence.NewWorkItemNumberSequenceRepository(db),
+ witr: &GormWorkItemTypeRepository{db},
+ wirr: &GormRevisionRepository{db},
+ space: space.NewRepository(db),
}
return repository
}
// GormWorkItemRepository implements WorkItemRepository using gorm
type GormWorkItemRepository struct {
- db *gorm.DB
- winr *numbersequence.GormWorkItemNumberSequenceRepository
- witr *GormWorkItemTypeRepository
- wirr *GormRevisionRepository
+ db *gorm.DB
+ winr *numbersequence.GormWorkItemNumberSequenceRepository
+ witr *GormWorkItemTypeRepository
+ wirr *GormRevisionRepository
+ space *space.GormRepository
}
// ************************************************
@@ -578,9 +587,7 @@ func (r *GormWorkItemRepository) Save(ctx context.Context, spaceID uuid.UUID, up
return nil, errors.NewVersionConflictError("version conflict")
}
wiStorage.Version = wiStorage.Version + 1
- wiStorage.Type = updatedWorkItem.Type
wiStorage.Fields = Fields{}
-
for fieldName, fieldDef := range wiType.Fields {
if fieldDef.ReadOnly {
continue
@@ -606,6 +613,18 @@ func (r *GormWorkItemRepository) Save(ctx context.Context, spaceID uuid.UUID, up
return nil, errors.NewBadParameterError(fieldName, fieldValue)
}
}
+ // Change of Work Item Type
+ if wiStorage.Type != updatedWorkItem.Type {
+ newWiType, err := r.witr.Load(ctx, updatedWorkItem.Type)
+ if err != nil {
+ return nil, errs.Wrapf(err, "failed to load workitemtype: %s ", updatedWorkItem.Type)
+ }
+ if err := r.ChangeWorkItemType(ctx, wiStorage, wiType, newWiType, spaceID); err != nil {
+ return nil, errs.Wrapf(err, "unable to change workitem type from %s (ID: %s) to %s (ID: %s)", wiType.Name, wiType.ID, newWiType.Name, newWiType.ID)
+ }
+ // This will be used by the ConvertWorkItemStorageToModel function
+ wiType = newWiType
+ }
tx := r.db.Where("Version = ?", updatedWorkItem.Version).Save(&wiStorage)
if err := tx.Error; err != nil {
log.Error(ctx, map[string]interface{}{
@@ -631,42 +650,58 @@ func (r *GormWorkItemRepository) Save(ctx context.Context, spaceID uuid.UUID, up
return ConvertWorkItemStorageToModel(wiType, wiStorage)
}
-// Create creates a new work item in the repository
-// returns BadParameterError, ConversionError or InternalError
-func (r *GormWorkItemRepository) Create(ctx context.Context, spaceID uuid.UUID, typeID uuid.UUID, fields map[string]interface{}, creatorID uuid.UUID) (*WorkItem, error) {
- defer goa.MeasureSince([]string{"goa", "db", "workitem", "create"}, time.Now())
-
- wiType, err := r.witr.Load(ctx, typeID)
- if err != nil {
- return nil, errors.NewBadParameterError("typeID", typeID)
- }
-
+// CheckTypeAndSpaceShareTemplate returns true if the given workitem type (wit)
+// belongs to the same space template as the space (spaceID); otherwise false is
+// returned
+func (r *GormWorkItemRepository) CheckTypeAndSpaceShareTemplate(ctx context.Context, wit *WorkItemType, spaceID uuid.UUID) (bool, error) {
// Prohibit creation of work items from a base type.
- if !wiType.CanConstruct {
- return nil, errors.NewForbiddenError(fmt.Sprintf("cannot construct work items from \"%s\" (%s)", wiType.Name, wiType.ID))
+ if !wit.CanConstruct {
+ return false, errors.NewForbiddenError(fmt.Sprintf("cannot construct work items from \"%s\" (%s)", wit.Name, wit.ID))
}
var exists bool
// Prohibit creation of work items from a type that doesn't belong to current space template
query := fmt.Sprintf(`
- SELECT EXISTS (
- SELECT 1 from %[1]s WHERE id=$1 AND space_template_id = (
- SELECT space_template_id FROM %[2]s WHERE id=$2
- )
- )`, wiType.TableName(), space.Space{}.TableName())
- err = r.db.Raw(query, wiType.ID, spaceID).Row().Scan(&exists)
+ SELECT EXISTS (
+ SELECT 1 from %[1]s WHERE id=$1 AND space_template_id = (
+ SELECT space_template_id FROM %[2]s WHERE id=$2
+ )
+ )`, wit.TableName(), space.Space{}.TableName())
+ err := r.db.Raw(query, wit.ID, spaceID).Row().Scan(&exists)
if err == nil && !exists {
- return nil, errors.NewBadParameterErrorFromString(
- fmt.Sprintf("Workitem Type \"%s\" (ID: %s) does not belong to the current space template", wiType.Name, wiType.ID),
+ return false, errors.NewBadParameterErrorFromString(
+ fmt.Sprintf("Workitem Type \"%s\" (ID: %s) does not belong to the current space template", wit.Name, wit.ID),
)
}
if err != nil {
log.Error(ctx, map[string]interface{}{
"space_id": spaceID,
- "workitem_type_id": wiType.ID,
+ "workitem_type_id": wit.ID,
"err": err,
}, "unable to fetch workitem types related to current space")
- return nil, errors.NewInternalError(ctx, errs.Wrapf(err, "unable to verify if %s exists", wiType.ID))
+ return false, errors.NewInternalError(ctx, errs.Wrapf(err, "unable to verify if %s exists", wit.ID))
+ }
+ return true, nil
+}
+
+// Create creates a new work item in the repository
+// returns BadParameterError, ConversionError or InternalError
+func (r *GormWorkItemRepository) Create(ctx context.Context, spaceID uuid.UUID, typeID uuid.UUID, fields map[string]interface{}, creatorID uuid.UUID) (*WorkItem, error) {
+ defer goa.MeasureSince([]string{"goa", "db", "workitem", "create"}, time.Now())
+
+ wiType, err := r.witr.Load(ctx, typeID)
+ if err != nil {
+ return nil, errors.NewBadParameterError("typeID", typeID)
+ }
+
+ allowedWIT, err := r.CheckTypeAndSpaceShareTemplate(ctx, wiType, spaceID)
+ if err != nil {
+ return nil, err
+
}
+ if !allowedWIT {
+ return nil, err
+ }
+
// The order of workitems are spaced by a factor of 1000.
pos, err := r.LoadHighestOrder(ctx, spaceID)
if err != nil {
@@ -1131,3 +1166,244 @@ func (r *GormWorkItemRepository) LoadByIteration(ctx context.Context, iterationI
}
return workitems, nil
}
+
+// ChangeWorkItemType changes the workitem in wiStorage to newWIType. Returns
+// error if the operation fails
+func (r *GormWorkItemRepository) ChangeWorkItemType(ctx context.Context, wiStorage *WorkItemStorage, oldWIType *WorkItemType, newWIType *WorkItemType, spaceID uuid.UUID) error {
+ allowedWIT, err := r.CheckTypeAndSpaceShareTemplate(ctx, newWIType, spaceID)
+ if err != nil {
+ return errs.Wrap(err, "failed to check workitem type")
+ }
+ if !allowedWIT {
+ return errors.NewBadParameterError("typeID", oldWIType.ID)
+ }
+ var fieldDiff = Fields{}
+ // Loop through old workitem type
+ for oldFieldName := range oldWIType.Fields {
+ // Temporary workaround to not add metastates to the field diff. We need
+ // to have a special handling for fields that shouldn't be set by user
+ // (or affected by type change) MetaState is a system level detail and
+ // that shouldn't be affected by type change, even if it is affected, it
+ // shouldn't show up in the field diff. The purpose of
+ // fieldDiff is to get the list of fields that should be added to the
+ // description. Metastate shouldn't show up in the description
+ if oldFieldName == SystemMetaState {
+ continue
+ }
+ // The field exists in old type and new type
+ if newField, ok := newWIType.Fields[oldFieldName]; ok {
+ // Try to assign the old value to the new field
+ _, err := newField.Type.ConvertToModel(wiStorage.Fields[oldFieldName])
+ if err != nil {
+ // if the new type is a list, stuff the old value in a list and
+ // try to assign it
+ if newField.Type.GetKind() == KindList {
+ var convertedValue interface{}
+ convertedValue, err = newField.Type.ConvertToModel([]interface{}{wiStorage.Fields[oldFieldName]})
+ if err == nil {
+ wiStorage.Fields[oldFieldName] = convertedValue
+ }
+ }
+ // if the old type is a list but the new one isn't check that
+ // the list contains only one element and assign that
+ if oldWIType.Fields[oldFieldName].Type.GetKind() == KindList && newField.Type.GetKind() != KindList {
+ ifArr, ok := wiStorage.Fields[oldFieldName].([]interface{})
+ if !ok {
+ return errs.Errorf("failed to convert field \"%s\" to interface array: %+v", oldFieldName, wiStorage.Fields[oldFieldName])
+ }
+ if len(ifArr) == 1 {
+ var convertedValue interface{}
+ convertedValue, err = newField.Type.ConvertToModel(ifArr[0])
+ if err == nil {
+ wiStorage.Fields[oldFieldName] = convertedValue
+ }
+ }
+ }
+ }
+ // Failed to assign the old value to the new field. Add the field to
+ // the diff and remove it from the old workitem.
+ if err != nil {
+ fieldDiff[oldFieldName] = wiStorage.Fields[oldFieldName]
+ delete(wiStorage.Fields, oldFieldName)
+ }
+ } else { // field doesn't exist in new type
+ if wiStorage.Fields[oldFieldName] != nil {
+ fieldDiff[oldFieldName] = wiStorage.Fields[oldFieldName]
+ delete(wiStorage.Fields, oldFieldName)
+ }
+ }
+ }
+ // We need fieldKeys to show field diff in a defined order. Golang maps
+ // aren't ordered by default.
+ var fieldKeys []string
+ for fieldName := range fieldDiff {
+ fieldKeys = append(fieldKeys, fieldName)
+ }
+ // Sort the field keys to prevent random order of fields
+ sort.Strings(fieldKeys)
+ // Append diff (fields along with their values) between the workitem types
+ // to the description
+ if len(fieldDiff) > 0 {
+ // If description doesn't exists, assign it empty value
+ if wiStorage.Fields[SystemDescription] == nil {
+ wiStorage.Fields[SystemDescription] = ""
+ }
+ originalDescription := rendering.NewMarkupContentFromValue(wiStorage.Fields[SystemDescription])
+ // TemplateData holds the information to be added to the description
+ templateData := struct {
+ NewTypeName string
+ FieldNameValues map[string]string
+ OriginalDescription string
+ }{
+ NewTypeName: newWIType.Name,
+ FieldNameValues: make(map[string]string),
+ OriginalDescription: originalDescription.Content,
+ }
+
+ for _, fieldName := range fieldKeys {
+ fieldDef := oldWIType.Fields[fieldName]
+ oldKind := fieldDef.Type.GetKind()
+ oldValue := fieldDiff[fieldName]
+
+ if oldKind == KindEnum {
+ enumType, ok := fieldDef.Type.(EnumType)
+ if !ok {
+ return errs.Errorf("failed to convert field \"%s\" to enum type: %+v", fieldName, fieldDef)
+ }
+ oldKind = enumType.BaseType.GetKind()
+ }
+ // handle all single value fields (including Enums)
+ if oldKind != KindList {
+ var val string
+ if oldKind.IsRelational() {
+ val, err = getValueOfRelationalKind(r.db, oldValue, oldKind)
+ if err != nil {
+ return errs.Wrapf(err, "failed to get relational value for field %s", fieldName)
+ }
+ } else {
+ val = fmt.Sprint(oldValue)
+ }
+ // Add field information to the description
+ templateData.FieldNameValues[oldWIType.Fields[fieldName].Label] = val
+ continue
+ }
+
+ // Deal with multi value field (KindList)
+ listType, ok := fieldDef.Type.(ListType)
+ if !ok {
+ return errs.Errorf("failed to convert field \"%s\" to list type: %+v", fieldName, fieldDef)
+ }
+ oldKind = listType.ComponentType.GetKind()
+ valList, ok := fieldDiff[fieldName].([]interface{})
+ if !ok {
+ return errs.Errorf("failed to convert list value of field \"%s\" to []interface{}: %+v", fieldName, fieldDiff[fieldName])
+ }
+
+ var tempList []string
+ for _, v := range valList {
+ val := fmt.Sprint(v)
+ if oldKind.IsRelational() {
+ val, err = getValueOfRelationalKind(r.db, v, oldKind)
+ if err != nil {
+ return errs.Wrapf(err, "failed to get relational value for field %s", fieldName)
+ }
+ }
+ tempList = append(tempList, val)
+ }
+ // Convert []string to comma seperated strings and add it to the
+ // description.
+ templateData.FieldNameValues[oldWIType.Fields[fieldName].Label] = strings.Join(tempList, ", ")
+ }
+ descriptionTemplate := template.Must(template.New("test").Parse("```" +
+ `
+Missing fields in workitem type: {{ .NewTypeName }}
+{{range $index, $element := .FieldNameValues }}
+{{$index}} : {{$element}}{{end}}
+` + "```" + `
+{{.OriginalDescription}}
+`))
+ var newDescription bytes.Buffer
+ if err := descriptionTemplate.Execute(&newDescription, templateData); err != nil {
+ return errs.Wrap(err, "failed to populate description template")
+ }
+ wiStorage.Fields[SystemDescription] = rendering.NewMarkupContent(newDescription.String(), rendering.SystemMarkupMarkdown)
+ }
+ // Set default values for all field in newWIType
+ for fieldName, fieldDef := range newWIType.Fields {
+ fieldValue := wiStorage.Fields[fieldName]
+ // Do not assign default value to metastate
+ if fieldName == SystemMetaState {
+ continue
+ }
+ // Assign default only if fieldValue is nil
+ wiStorage.Fields[fieldName], err = fieldDef.ConvertToModel(fieldName, fieldValue)
+ if err != nil {
+ return errs.Wrapf(err, "failed to convert field \"%s\"", fieldName)
+ }
+ }
+ wiStorage.Type = newWIType.ID
+ return nil
+}
+
+// getValueOfRelationKind resolves the relational value stored in val to it's
+// verbose value. Eg: UUID of kind User to username.
+func getValueOfRelationalKind(db *gorm.DB, val interface{}, kind Kind) (string, error) {
+ var result string
+ switch kind {
+ case KindList, KindEnum:
+ return result, errors.NewInternalErrorFromString("cannot resolve relational value for KindList or KindEnum")
+ case KindUser:
+ var identity account.Identity
+ tx := db.Model(&account.Identity{}).Where("id = ?", val).Find(&identity)
+ if tx.Error != nil {
+ return result, errs.Wrap(tx.Error, "failed to find identity")
+ }
+ var user account.User
+ tx = db.Model(&account.User{}).Where("id = ?", identity.UserID).Find(&user)
+ if tx.Error != nil {
+ return result, errs.Wrap(tx.Error, "failed to find user")
+ }
+ result = fmt.Sprintf("%s (%s)", user.FullName, identity.Username)
+ case KindArea:
+ var area area.Area
+ tx := db.Model(area.TableName()).Where("id = ?", val).First(&area)
+ if tx.Error != nil {
+ return result, errs.Wrap(tx.Error, "failed to find area")
+ }
+ result = fmt.Sprintf("%s (%s)", area.Name, area.Path)
+ case KindBoardColumn:
+ var column BoardColumn
+ tx := db.Model(column.TableName()).Where("id = ?", val).First(&column)
+ if tx.Error != nil {
+ return result, errs.Wrap(tx.Error, "failed to find boardcolumn")
+ }
+ result = column.Name
+
+ case KindIteration:
+ var iteration iteration.Iteration
+ tx := db.Model(iteration.TableName()).Where("id = ?", val).First(&iteration)
+ if tx.Error != nil {
+ return result, errs.Wrap(tx.Error, "failed to find iteration")
+ }
+ result = fmt.Sprintf("%s (%s)", iteration.Name, iteration.Path)
+
+ case KindCodebase:
+ var codebase codebase.Codebase
+ tx := db.Model(codebase.TableName()).Where("id = ?", val).First(&codebase)
+ if tx.Error != nil {
+ return result, errs.Wrap(tx.Error, "failed to find codebase")
+ }
+ result = codebase.URL // TODO(ibrahim): Figure out what we should be here. Codebase does not have a name.
+
+ case KindLabel:
+ var label label.Label
+ tx := db.Model(label.TableName()).Where("id = ?", val).First(&label)
+ if tx.Error != nil {
+ return result, errs.Wrap(tx.Error, "failed to find area")
+ }
+ result = label.Name
+ default:
+ return result, errors.NewInternalErrorFromString("unknown field Kind")
+ }
+ return result, nil
+}
diff --git a/workitem/workitem_repository_blackbox_test.go b/workitem/workitem_repository_blackbox_test.go
index 3fff1a2101..ced0b94a45 100644
--- a/workitem/workitem_repository_blackbox_test.go
+++ b/workitem/workitem_repository_blackbox_test.go
@@ -3,6 +3,7 @@ package workitem_test
import (
"context"
"fmt"
+ "strings"
"sync"
"testing"
"time"
@@ -108,16 +109,231 @@ func (s *workItemRepoBlackBoxTest) TestSave() {
})
s.T().Run("change is not prohibited", func(t *testing.T) {
- // tests that you can change the type of a work item. NOTE: This
- // functionality only works on the DB layer and is not exposed to REST.
// given
fxt := tf.NewTestFixture(t, s.DB, tf.WorkItems(1), tf.WorkItemTypes(2))
// when
fxt.WorkItems[0].Type = fxt.WorkItemTypes[1].ID
newWi, err := s.repo.Save(s.Ctx, fxt.WorkItems[0].SpaceID, *fxt.WorkItems[0], fxt.Identities[0].ID)
// then
- require.NoError(s.T(), err)
- assert.Equal(s.T(), fxt.WorkItemTypes[1].ID, newWi.Type)
+ require.NoError(t, err)
+ assert.Equal(t, fxt.WorkItemTypes[1].ID, newWi.Type)
+ })
+
+ s.T().Run("change of type along with field is not prohibited", func(t *testing.T) {
+ // tests that you can change the type of a work item and its fields at the same time.
+ // NOTE: This functionality only works on the DB layer and is not exposed to REST.
+ // given
+ fxt := tf.NewTestFixture(t, s.DB, tf.WorkItems(1, func(fxt *tf.TestFixture, idx int) error {
+ fxt.WorkItems[idx].Fields[workitem.SystemTitle] = "foo"
+ return nil
+ }), tf.WorkItemTypes(2))
+ // when
+ fxt.WorkItems[0].Fields[workitem.SystemTitle] = "bar"
+ fxt.WorkItems[0].Type = fxt.WorkItemTypes[1].ID
+ newWi, err := s.repo.Save(s.Ctx, fxt.WorkItems[0].SpaceID, *fxt.WorkItems[0], fxt.Identities[0].ID)
+ // then
+ require.NoError(t, err)
+ assert.Equal(t, fxt.WorkItemTypes[1].ID, newWi.Type)
+ assert.Equal(t, "bar", newWi.Fields[workitem.SystemTitle])
+ })
+
+ s.T().Run("type change", func(t *testing.T) {
+
+ type testData struct {
+ name string
+
+ initialValue interface{}
+ targetValue interface{}
+
+ initialFieldType workitem.FieldType
+ targetFieldType workitem.FieldType
+
+ fieldConvertible bool
+ }
+
+ k := workitem.KindString
+
+ td := []testData{
+ // valid conversions
+ {"ok - simple type to simple type",
+ "foo1",
+ "foo1",
+ workitem.SimpleType{Kind: k},
+ workitem.SimpleType{Kind: k},
+ true},
+ {"ok - simple type to list",
+ "foo2",
+ []interface{}{"foo2"},
+ workitem.SimpleType{Kind: k},
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: k}},
+ true},
+ {"ok - simple type to enum",
+ "foo3",
+ "foo3",
+ workitem.SimpleType{Kind: k},
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: k}, Values: []interface{}{"red", "foo3", "blue"}},
+ true},
+ {"ok - list to list",
+ []interface{}{"foo4", "foo5"},
+ []interface{}{"foo4", "foo5"},
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: k}},
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: k}},
+ true},
+ {"ok - list to simple type",
+ []interface{}{"foo6"},
+ "foo6",
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: k}},
+ workitem.SimpleType{Kind: k},
+ true},
+ {"ok - list to enum",
+ []interface{}{"foo7"},
+ "foo7",
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: k}},
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: k}, Values: []interface{}{"yellow", "foo7", "cyan"}},
+ true},
+ {"ok - enum to enum",
+ "foo8",
+ "foo8",
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: k}, Values: []interface{}{"Bach", "foo8", "Chapdelaine"}},
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: k}, Values: []interface{}{"Kant", "Hume", "foo8", "Aristoteles"}},
+ true},
+ {"ok - enum to simple type",
+ "foo9",
+ "foo9",
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: k}, Values: []interface{}{"Schopenhauer", "foo9", "Duerer"}},
+ workitem.SimpleType{Kind: k},
+ true},
+ {"ok - enum to list",
+ "foo10",
+ []interface{}{"foo10"},
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: k}, Values: []interface{}{"Sokrates", "foo10", "Fromm"}},
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: k}},
+ true},
+ // invalid conversions
+ {"err - simple type (string) to simple type (int)",
+ "foo11",
+ nil,
+ workitem.SimpleType{Kind: workitem.KindString},
+ workitem.SimpleType{Kind: workitem.KindInteger},
+ false},
+ {"err - simple type (string) to list (integer)",
+ "foo2",
+ ([]interface{})(nil),
+ workitem.SimpleType{Kind: k},
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: workitem.KindInteger}},
+ false},
+ {"err - simple type (string) to enum (float)",
+ "foo3",
+ 11.1,
+ workitem.SimpleType{Kind: k},
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: workitem.KindFloat}, Values: []interface{}{11.1, 22.2, 33.3}},
+ false},
+ {"err - list (string) to list (float)",
+ []interface{}{"foo4", "foo5"},
+ ([]interface{})(nil),
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: k}},
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: workitem.KindFloat}},
+ false},
+ {"err - list (string) to simple type (int)",
+ []interface{}{"foo6"},
+ nil,
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: k}},
+ workitem.SimpleType{Kind: workitem.KindInteger},
+ false},
+ {"err - list (string) to enum (float)",
+ []interface{}{"foo7"},
+ 11.1,
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: k}},
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: workitem.KindFloat}, Values: []interface{}{11.1, 22.2, 33.3}},
+ false},
+ {"err - enum (string) to enum (float)",
+ "foo8",
+ 11.1,
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: k}, Values: []interface{}{"Bach", "foo8", "Chapdelaine"}},
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: workitem.KindFloat}, Values: []interface{}{11.1, 22.2, 33.3}},
+ false},
+ {"err - enum (string) to simple type (float)",
+ "foo9",
+ nil,
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: k}, Values: []interface{}{"Schopenhauer", "foo9", "Duerer"}},
+ workitem.SimpleType{Kind: workitem.KindFloat},
+ false},
+ {"err - enum (string) to list (float)",
+ "foo10",
+ ([]interface{})(nil),
+ workitem.EnumType{SimpleType: workitem.SimpleType{Kind: workitem.KindEnum}, BaseType: workitem.SimpleType{Kind: k}, Values: []interface{}{"Sokrates", "foo10", "Fromm"}},
+ workitem.ListType{SimpleType: workitem.SimpleType{Kind: workitem.KindList}, ComponentType: workitem.SimpleType{Kind: workitem.KindFloat}},
+ false},
+ }
+ for _, d := range td {
+ t.Run(d.name, func(t *testing.T) {
+ fieldName := d.name
+
+ fxt := tf.NewTestFixture(t, s.DB,
+ tf.WorkItemTypes(2, func(fxt *tf.TestFixture, idx int) error {
+ wit := fxt.WorkItemTypes[idx]
+ switch idx {
+ case 0:
+ wit.Fields = workitem.FieldDefinitions{
+ fieldName: workitem.FieldDefinition{
+ Label: "source field",
+ Required: false,
+ Type: d.initialFieldType,
+ },
+ }
+ case 1:
+ wit.Fields = workitem.FieldDefinitions{
+ fieldName: workitem.FieldDefinition{
+ Label: "target field",
+ Required: false,
+ Type: d.targetFieldType,
+ },
+ }
+ }
+ return nil
+ }),
+ tf.WorkItems(1, tf.SetWorkItemField(fieldName, d.initialValue)),
+ )
+
+ // Load the work item from the DB and check that the
+ // initial value was set correctly. We have some special
+ // treatment for lists here.
+ loadedWorkItem, err := s.repo.LoadByID(s.Ctx, fxt.WorkItems[0].ID)
+ require.NoError(t, err)
+ require.Equal(t, d.initialValue, loadedWorkItem.Fields[fieldName])
+
+ // when we update the work item type
+ loadedWorkItem.Type = fxt.WorkItemTypes[1].ID
+ updatedWorkItem, err := s.repo.Save(s.Ctx, fxt.WorkItems[0].SpaceID, *loadedWorkItem, fxt.Identities[0].ID)
+ require.NoError(t, err)
+
+ // then check that the error is as expected or that the
+ // value in the new field type is what we expected.
+ if !d.fieldConvertible {
+ rendered := d.initialValue
+ if d.initialFieldType.GetKind() == workitem.KindList {
+ ifArr := d.initialValue.(interface{}).([]interface{})
+ strArr := make([]string, len(ifArr))
+ for i := range ifArr {
+ strArr[i] = ifArr[i].(string)
+ }
+ rendered = strings.Join(strArr, ", ")
+ }
+ require.Contains(t, updatedWorkItem.Fields[workitem.SystemDescription].(rendering.MarkupContent).Content, fmt.Sprintf("source field : %+v", rendered))
+ } else {
+ require.NotNil(t, updatedWorkItem)
+ require.Equal(t, fxt.WorkItemTypes[1].ID, updatedWorkItem.Type)
+ require.Equal(t, d.targetValue, updatedWorkItem.Fields[fieldName])
+ }
+
+ // also check if the values are the same when work item is
+ // loaded
+ loadedWorkItem, err = s.repo.LoadByID(s.Ctx, fxt.WorkItems[0].ID)
+ require.NoError(t, err)
+ require.Equal(t, fxt.WorkItemTypes[1].ID, loadedWorkItem.Type)
+ require.Equal(t, d.targetValue, loadedWorkItem.Fields[fieldName])
+ })
+ }
})
}
diff --git a/workitem/workitemtype.go b/workitem/workitemtype.go
index 80b84cf304..2541be987c 100644
--- a/workitem/workitemtype.go
+++ b/workitem/workitemtype.go
@@ -37,6 +37,7 @@ const (
SystemCodebase = "system.codebase"
SystemLabels = "system.labels"
SystemBoardcolumns = "system.boardcolumns"
+ SystemMetaState = "system.metastate"
SystemBoard = "Board"