From b1b953a5631a5a3a72dc2f085d05b79d7bc64d54 Mon Sep 17 00:00:00 2001
From: pkupczyk <pkupczyk>
Date: Mon, 18 Apr 2016 16:44:07 +0000
Subject: [PATCH] SSDM-3476 : V3 AS API - tags: - createTags finished -
 js-tests for createTags, updateTags and mapTags - bugfixes (missing/incorrect
 annotations for the new methods + some javascript functions missing in DTOs)

SVN: 36221
---
 .../openbis-v3-api-test/html/test/common.js   |  30 ++++
 .../html/test/test-create.js                  |  20 +++
 .../openbis-v3-api-test/html/test/test-get.js | 170 ++++++++++--------
 .../html/test/test-update.js                  |  26 +++
 .../server/asapi/v3/ApplicationServerApi.java |   4 +-
 .../v3/executor/tag/CreateTagExecutor.java    |   4 +
 .../tag/ISetTagMaterialsExecutor.java         |  29 +++
 .../ISetTagMaterialsWithCacheExecutor.java    |  32 ++++
 .../executor/tag/SetTagMaterialsExecutor.java |  78 ++++++++
 .../tag/SetTagMaterialsWithCacheExecutor.java |  48 +++++
 .../api/v3/as/dto/tag/update/TagUpdate.js     |   6 +
 .../public/resources/api/v3/openbis.js        |  15 ++
 .../systemtest/asapi/v3/CreateTagTest.java    |  94 ++++++++++
 .../systemtest/asapi/v3/UpdateTagTest.java    | 104 ++++++++++-
 14 files changed, 580 insertions(+), 80 deletions(-)
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/ISetTagMaterialsExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/ISetTagMaterialsWithCacheExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/SetTagMaterialsExecutor.java
 create mode 100644 openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/SetTagMaterialsWithCacheExecutor.java

diff --git a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/common.js b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/common.js
index 8156a2e68f5..26993ffec79 100644
--- a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/common.js
+++ b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/common.js
@@ -24,6 +24,7 @@ define([ 'jquery', 'openbis', 'underscore', 'test/dtos' ], function($, openbis,
 		this.MaterialCreation = dtos.MaterialCreation;
 		this.AttachmentCreation = dtos.AttachmentCreation;
 		this.VocabularyTermCreation = dtos.VocabularyTermCreation;
+		this.TagCreation = dtos.TagCreation;
 		this.SpaceUpdate = dtos.SpaceUpdate;
 		this.ProjectUpdate = dtos.ProjectUpdate;
 		this.ExperimentUpdate = dtos.ExperimentUpdate;
@@ -32,6 +33,7 @@ define([ 'jquery', 'openbis', 'underscore', 'test/dtos' ], function($, openbis,
 		this.PhysicalDataUpdate = dtos.PhysicalDataUpdate;
 		this.MaterialUpdate = dtos.MaterialUpdate;
 		this.VocabularyTermUpdate = dtos.VocabularyTermUpdate;
+		this.TagUpdate = dtos.TagUpdate;
 		this.SpaceDeletionOptions = dtos.SpaceDeletionOptions;
 		this.ProjectDeletionOptions = dtos.ProjectDeletionOptions;
 		this.ExperimentDeletionOptions = dtos.ExperimentDeletionOptions;
@@ -52,6 +54,7 @@ define([ 'jquery', 'openbis', 'underscore', 'test/dtos' ], function($, openbis,
 		this.MaterialPermId = dtos.MaterialPermId;
 		this.VocabularyPermId = dtos.VocabularyPermId;
 		this.VocabularyTermPermId = dtos.VocabularyTermPermId;
+		this.TagPermId = dtos.TagPermId;
 		this.TagCode = dtos.TagCode;
 		this.SpaceSearchCriteria = dtos.SpaceSearchCriteria;
 		this.ProjectSearchCriteria = dtos.ProjectSearchCriteria;
@@ -60,6 +63,7 @@ define([ 'jquery', 'openbis', 'underscore', 'test/dtos' ], function($, openbis,
 		this.DataSetSearchCriteria = dtos.DataSetSearchCriteria;
 		this.MaterialSearchCriteria = dtos.MaterialSearchCriteria;
 		this.VocabularyTermSearchCriteria = dtos.VocabularyTermSearchCriteria;
+		this.TagSearchCriteria = dtos.TagSearchCriteria;
 		this.SpaceFetchOptions = dtos.SpaceFetchOptions;
 		this.ProjectFetchOptions = dtos.ProjectFetchOptions;
 		this.ExperimentFetchOptions = dtos.ExperimentFetchOptions;
@@ -71,6 +75,7 @@ define([ 'jquery', 'openbis', 'underscore', 'test/dtos' ], function($, openbis,
 		this.MaterialFetchOptions = dtos.MaterialFetchOptions;
 		this.MaterialTypeFetchOptions = dtos.MaterialTypeFetchOptions;
 		this.VocabularyTermFetchOptions = dtos.VocabularyTermFetchOptions;
+		this.TagFetchOptions = dtos.TagFetchOptions;
 		this.DeletionFetchOptions = dtos.DeletionFetchOptions;
 		this.DeletionSearchCriteria = dtos.DeletionSearchCriteria;
 		this.CustomASServiceSearchCriteria = dtos.CustomASServiceSearchCriteria;
@@ -182,6 +187,15 @@ define([ 'jquery', 'openbis', 'underscore', 'test/dtos' ], function($, openbis,
 			});
 		}.bind(this);
 
+		this.createTag = function(facade) {
+			var c = this;
+			var creation = new dtos.TagCreation();
+			creation.setCode(c.generateId("TAG"));
+			return facade.createTags([ creation ]).then(function(permIds) {
+				return permIds[0];
+			});
+		}.bind(this);
+
 		this.findSpace = function(facade, id) {
 			var c = this;
 			return facade.getSpaces([ id ], c.createSpaceFetchOptions()).then(function(spaces) {
@@ -231,6 +245,13 @@ define([ 'jquery', 'openbis', 'underscore', 'test/dtos' ], function($, openbis,
 			});
 		}.bind(this);
 
+		this.findTag = function(facade, id) {
+			var c = this;
+			return facade.getTags([ id ], c.createTagFetchOptions()).then(function(tags) {
+				return tags[id];
+			});
+		}.bind(this);
+
 		this.deleteSpace = function(facade, id) {
 			var c = this;
 			var options = new dtos.SpaceDeletionOptions();
@@ -425,6 +446,15 @@ define([ 'jquery', 'openbis', 'underscore', 'test/dtos' ], function($, openbis,
 			return fo;
 		};
 
+		this.createTagFetchOptions = function() {
+			var fo = new dtos.TagFetchOptions();
+			fo.withExperiments();
+			fo.withSamples();
+			fo.withDataSets();
+			fo.withMaterials();
+			return fo;
+		};
+
 		this.assertNull = function(actual, msg) {
 			this.assertEqual(actual, null, msg)
 		};
diff --git a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-create.js b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-create.js
index 115fbc534c3..6d398fdbe88 100644
--- a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-create.js
+++ b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-create.js
@@ -187,5 +187,25 @@ define([ 'jquery', 'underscore', 'openbis', 'test/common' ], function($, _, open
 			testCreate(c, fCreate, c.findVocabularyTerm, fCheck);
 		});
 
+		QUnit.test("createTags()", function(assert) {
+			var c = new common(assert);
+			var code = c.generateId("TAG");
+			var description = "Description of " + code;
+
+			var fCreate = function(facade) {
+				var tagCreation = new c.TagCreation();
+				tagCreation.setCode(code);
+				tagCreation.setDescription(description);
+				return facade.createTags([ tagCreation ]);
+			}
+
+			var fCheck = function(tag) {
+				c.assertEqual(tag.getCode(), code, "Tag code");
+				c.assertEqual(tag.getDescription(), description, "Tag description");
+			}
+
+			testCreate(c, fCreate, c.findTag, fCheck);
+		});
+
 	}
 });
diff --git a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-get.js b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-get.js
index c2b837268f9..2afedd13778 100644
--- a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-get.js
+++ b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-get.js
@@ -1,7 +1,7 @@
 define([ 'jquery', 'underscore', 'openbis', 'test/common' ], function($, _, openbis, common) {
 	return function() {
 		QUnit.module("Get tests");
-		
+
 		var testGet = function(c, fCreate, fGet, fGetEmptyFetchOptions, fechOptionsTestConfig) {
 			c.start();
 			c.createFacadeAndLogin().then(function(facade) {
@@ -30,29 +30,29 @@ define([ 'jquery', 'underscore', 'openbis', 'test/common' ], function($, _, open
 				c.finish();
 			});
 		}
-		
+
 		var testFetchOptionsAssignation = function(c, fo, toTest) {
-			for(component in toTest) {
-				if(component === "SortBy") {
+			for (component in toTest) {
+				if (component === "SortBy") {
 					fo.sortBy().code();
-					c.assertEqual(true, ((fo.getSortBy())?true:false), "Component " + component + " set on Fetch Options.");
+					c.assertEqual(true, ((fo.getSortBy()) ? true : false), "Component " + component + " set on Fetch Options.");
 				} else {
 					var methodNameWithUsing = "with" + component + "Using";
-					if(typeof fo[methodNameWithUsing] === "function") {
+					if (typeof fo[methodNameWithUsing] === "function") {
 						fo[methodNameWithUsing](null);
 					} else {
 						throw methodNameWithUsing + " should be a method.";
 					}
-					
+
 					var methodNameWith = "with" + component;
-					if(typeof fo[methodNameWith] === "function") {
+					if (typeof fo[methodNameWith] === "function") {
 						fo[methodNameWith]();
 					} else {
 						throw methodNameWith + " should be a method.";
 					}
-					
+
 					var methodNameHas = "has" + component;
-					if(typeof fo[methodNameHas] === "function") {
+					if (typeof fo[methodNameHas] === "function") {
 						c.assertEqual(true, fo[methodNameHas](), "Component " + component + " set on Fetch Options.");
 					} else {
 						throw methodNameHas + " should be a method.";
@@ -60,19 +60,23 @@ define([ 'jquery', 'underscore', 'openbis', 'test/common' ], function($, _, open
 				}
 			}
 		}
-		
+
 		var testFetchOptionsResults = function(c, toTest, expectedShouldSucceed, entity) {
-			for(property in toTest) {
-				if(property !== "SortBy") {
+			for (property in toTest) {
+				if (property !== "SortBy") {
 					var methodName = "get" + property;
 					var errorFound = null;
-					if(typeof entity[methodName] === "function") {
+					if (typeof entity[methodName] === "function") {
 						try {
-							var result = entity[methodName](); //Should not thrown an exception, what it means is right!
-						} catch(error) {
+							var result = entity[methodName](); // Should not
+							// thrown an
+							// exception,
+							// what it means
+							// is right!
+						} catch (error) {
 							errorFound = error;
 						}
-						var msg = (expectedShouldSucceed)?"Succeed":"Fail";
+						var msg = (expectedShouldSucceed) ? "Succeed" : "Fail";
 						c.assertEqual(expectedShouldSucceed, !errorFound, "Calling method " + methodName + " expected to " + msg);
 					} else {
 						throw methodName + " should be a method.";
@@ -80,147 +84,147 @@ define([ 'jquery', 'underscore', 'openbis', 'test/common' ], function($, _, open
 				}
 			}
 		}
-		
+
 		var getMethods = function(obj) {
-			  var result = [];
-			  for (var id in obj) {
-			    try {
-			      if (typeof(obj[id]) == "function") {
-			        result.push(id + ": " + obj[id].toString());
-			      }
-			    } catch (err) {
-			      result.push(id + ": inaccessible");
-			    }
-			  }
-			  return result;
+			var result = [];
+			for ( var id in obj) {
+				try {
+					if (typeof (obj[id]) == "function") {
+						result.push(id + ": " + obj[id].toString());
+					}
+				} catch (err) {
+					result.push(id + ": inaccessible");
+				}
+			}
+			return result;
 		}
-		
+
 		var getConfigForFetchOptions = function(fo) {
 			var components = {};
 			var methods = getMethods(fo);
-			for(var mIdx = 0; mIdx < methods.length; mIdx++) {
+			for (var mIdx = 0; mIdx < methods.length; mIdx++) {
 				var method = methods[mIdx];
-				if(method.startsWith("has")) {
+				if (method.startsWith("has")) {
 					var component = method.substring(3, method.indexOf(':'));
 					components[component] = null;
 				}
 			}
 			return components;
 		}
-		
+
 		QUnit.test("getSpaces()", function(assert) {
 			var c = new common(assert);
 			var fo = new c.SpaceFetchOptions();
 			var fechOptionsTestConfig = getConfigForFetchOptions(fo);
 			fechOptionsTestConfig.SortBy = null;
-			
+
 			var fCreate = function(facade) {
 				return $.when(c.createSpace(facade), c.createSpace(facade)).then(function(permId1, permId2) {
 					return [ permId1, permId2 ];
 				});
 			}
-			
+
 			var fGet = function(facade, permIds) {
 				testFetchOptionsAssignation(c, fo, fechOptionsTestConfig);
 				return facade.getSpaces(permIds, fo);
 			}
-			
+
 			var fGetEmptyFetchOptions = function(facade, permIds) {
 				return facade.getSpaces(permIds, new c.SpaceFetchOptions());
 			}
-			
+
 			testGet(c, fCreate, fGet, fGetEmptyFetchOptions, fechOptionsTestConfig);
 		});
-		
+
 		QUnit.test("getProjects()", function(assert) {
 			var c = new common(assert);
 			var fo = new c.ProjectFetchOptions();
 			var fechOptionsTestConfig = getConfigForFetchOptions(fo);
 			fechOptionsTestConfig.SortBy = null;
-			
+
 			var fCreate = function(facade) {
 				return $.when(c.createProject(facade), c.createProject(facade)).then(function(permId1, permId2) {
 					return [ permId1, permId2 ];
 				});
 			}
-			
+
 			var fGet = function(facade, permIds) {
-				
+
 				testFetchOptionsAssignation(c, fo, fechOptionsTestConfig);
 				return facade.getProjects(permIds, fo);
 			}
-			
+
 			var fGetEmptyFetchOptions = function(facade, permIds) {
 				return facade.getProjects(permIds, new c.ProjectFetchOptions());
 			}
-			
+
 			testGet(c, fCreate, fGet, fGetEmptyFetchOptions, fechOptionsTestConfig);
 		});
-		
+
 		QUnit.test("getExperiments()", function(assert) {
 			var c = new common(assert);
 			var fo = new c.ExperimentFetchOptions();
 			var fechOptionsTestConfig = getConfigForFetchOptions(fo);
 			fechOptionsTestConfig.SortBy = null;
-			
+
 			var fCreate = function(facade) {
 				return $.when(c.createExperiment(facade), c.createExperiment(facade)).then(function(permId1, permId2) {
 					return [ permId1, permId2 ];
 				});
 			}
-			
+
 			var fGet = function(facade, permIds) {
 				testFetchOptionsAssignation(c, fo, fechOptionsTestConfig);
 				return facade.getExperiments(permIds, fo);
 			}
-			
+
 			var fGetEmptyFetchOptions = function(facade, permIds) {
 				return facade.getExperiments(permIds, new c.ExperimentFetchOptions());
 			}
-			
+
 			testGet(c, fCreate, fGet, fGetEmptyFetchOptions, fechOptionsTestConfig);
 		});
-		
+
 		QUnit.test("getSamples()", function(assert) {
 			var c = new common(assert);
 			var fo = new c.SampleFetchOptions();
 			var fechOptionsTestConfig = getConfigForFetchOptions(fo);
 			fechOptionsTestConfig.SortBy = null;
-			
+
 			var fCreate = function(facade) {
 				return $.when(c.createSample(facade), c.createSample(facade)).then(function(permId1, permId2) {
 					return [ permId1, permId2 ];
 				});
 			}
-			
+
 			var fGet = function(facade, permIds) {
 				testFetchOptionsAssignation(c, fo, fechOptionsTestConfig);
 				return facade.getSamples(permIds, fo);
 			}
-			
+
 			var fGetEmptyFetchOptions = function(facade, permIds) {
 				return facade.getSamples(permIds, new c.SampleFetchOptions());
 			}
-			
+
 			testGet(c, fCreate, fGet, fGetEmptyFetchOptions, fechOptionsTestConfig);
 		});
-		
+
 		QUnit.test("getDataSets()", function(assert) {
 			var c = new common(assert);
 			var fo = new c.DataSetFetchOptions();
 			var fechOptionsTestConfig = getConfigForFetchOptions(fo);
 			fechOptionsTestConfig.SortBy = null;
-			
+
 			var fCreate = function(facade) {
 				return $.when(c.createDataSet(facade), c.createDataSet(facade)).then(function(permId1, permId2) {
 					return [ permId1, permId2 ];
 				});
 			}
-			
+
 			var fGet = function(facade, permIds) {
 				testFetchOptionsAssignation(c, fo, fechOptionsTestConfig);
 				var result = facade.getDataSets(permIds, fo);
-				
+
 				result.then(function(map) {
 					permIds.forEach(function(permId) {
 						var entity = map[permId];
@@ -229,10 +233,10 @@ define([ 'jquery', 'underscore', 'openbis', 'test/common' ], function($, _, open
 				});
 				return result;
 			}
-			
+
 			var fGetEmptyFetchOptions = function(facade, permIds) {
 				var result = facade.getDataSets(permIds, new c.DataSetFetchOptions());
-				
+
 				result.then(function(map) {
 					permIds.forEach(function(permId) {
 						var entity = map[permId];
@@ -241,57 +245,81 @@ define([ 'jquery', 'underscore', 'openbis', 'test/common' ], function($, _, open
 				});
 				return result;
 			}
-			
+
 			testGet(c, fCreate, fGet, fGetEmptyFetchOptions, fechOptionsTestConfig);
 		});
-		
+
 		QUnit.test("getMaterials()", function(assert) {
 			var c = new common(assert);
 			var fo = new c.MaterialFetchOptions();
 			var fechOptionsTestConfig = getConfigForFetchOptions(fo);
 			fechOptionsTestConfig.SortBy = null;
-			
+
 			var fCreate = function(facade) {
 				return $.when(c.createMaterial(facade), c.createMaterial(facade)).then(function(permId1, permId2) {
 					return [ permId1, permId2 ];
 				});
 			}
-			
+
 			var fGet = function(facade, permIds) {
 				testFetchOptionsAssignation(c, fo, fechOptionsTestConfig);
 				return facade.getMaterials(permIds, fo);
 			}
-			
+
 			var fGetEmptyFetchOptions = function(facade, permIds) {
 				return facade.getMaterials(permIds, new c.MaterialFetchOptions());
 			}
-			
+
 			testGet(c, fCreate, fGet, fGetEmptyFetchOptions, fechOptionsTestConfig);
 		});
-		
+
 		QUnit.test("getVocabularyTerms()", function(assert) {
 			var c = new common(assert);
 			var fo = new c.VocabularyTermFetchOptions();
 			var fechOptionsTestConfig = getConfigForFetchOptions(fo);
 			fechOptionsTestConfig.SortBy = null;
-			
+
 			var fCreate = function(facade) {
 				return $.when(c.createVocabularyTerm(facade), c.createVocabularyTerm(facade)).then(function(permId1, permId2) {
 					return [ permId1, permId2 ];
 				});
 			}
-			
+
 			var fGet = function(facade, permIds) {
 				testFetchOptionsAssignation(c, fo, fechOptionsTestConfig);
 				return facade.getVocabularyTerms(permIds, fo);
 			}
-			
+
 			var fGetEmptyFetchOptions = function(facade, permIds) {
 				return facade.getVocabularyTerms(permIds, new c.VocabularyTermFetchOptions());
 			}
-			
+
+			testGet(c, fCreate, fGet, fGetEmptyFetchOptions, fechOptionsTestConfig);
+		});
+
+		QUnit.test("getTags()", function(assert) {
+			var c = new common(assert);
+			var fo = new c.TagFetchOptions();
+			var fechOptionsTestConfig = getConfigForFetchOptions(fo);
+			fechOptionsTestConfig.SortBy = null;
+
+			var fCreate = function(facade) {
+				return $.when(c.createTag(facade), c.createTag(facade)).then(function(permId1, permId2) {
+					return [ permId1, permId2 ];
+				});
+			}
+
+			var fGet = function(facade, permIds) {
+				testFetchOptionsAssignation(c, fo, fechOptionsTestConfig);
+				return facade.getTags(permIds, fo);
+			}
+
+			var fGetEmptyFetchOptions = function(facade, permIds) {
+				return facade.getTags(permIds, new c.TagFetchOptions());
+			}
+
 			testGet(c, fCreate, fGet, fGetEmptyFetchOptions, fechOptionsTestConfig);
 		});
-		
+
 	}
 });
diff --git a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-update.js b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-update.js
index a48032a5a43..cd8f0a2ec31 100644
--- a/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-update.js
+++ b/js-test/servers/common/core-plugins/tests/1/as/webapps/openbis-v3-api-test/html/test/test-update.js
@@ -389,5 +389,31 @@ define([ 'jquery', 'underscore', 'openbis', 'test/common' ], function($, _, open
 			testUpdate(c, fCreate, fUpdate, c.findVocabularyTerm, fCheck);
 		});
 
+		QUnit.test("updateTags()", function(assert) {
+			var c = new common(assert);
+			var code = c.generateId("TAG");
+			var description = "Description of " + code;
+
+			var fCreate = function(facade) {
+				var tagCreation = new c.TagCreation();
+				tagCreation.setCode(code);
+				return facade.createTags([ tagCreation ]);
+			}
+
+			var fUpdate = function(facade, permId) {
+				var tagUpdate = new c.TagUpdate();
+				tagUpdate.setTagId(permId);
+				tagUpdate.setDescription(description);
+				return facade.updateTags([ tagUpdate ]);
+			}
+
+			var fCheck = function(tag) {
+				c.assertEqual(tag.getCode(), code, "Tag code");
+				c.assertEqual(tag.getDescription(), description, "Tag description");
+			}
+
+			testUpdate(c, fCreate, fUpdate, c.findTag, fCheck);
+		});
+
 	}
 });
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
index ebaa713841e..3257521f6be 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/ApplicationServerApi.java
@@ -446,9 +446,10 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     }
 
     @Override
+    @Transactional
     @RolesAllowed({ RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
     @Capability("CREATE_TAG")
-    @DatabaseUpdateModification(value = ObjectKind.METAPROJECT)
+    @DatabaseCreateOrDeleteModification(value = ObjectKind.METAPROJECT)
     public List<TagPermId> createTags(String sessionToken, List<TagCreation> creations)
     {
         return createTagExecutor.create(sessionToken, creations);
@@ -527,6 +528,7 @@ public class ApplicationServerApi extends AbstractServer<IApplicationServerApi>
     @Transactional
     @RolesAllowed({ RoleWithHierarchy.SPACE_USER, RoleWithHierarchy.SPACE_ETL_SERVER })
     @Capability("UPDATE_TAG")
+    @DatabaseUpdateModification(value = ObjectKind.METAPROJECT)
     public void updateTags(String sessionToken, List<TagUpdate> tagUpdates)
     {
         updateTagExecutor.update(sessionToken, tagUpdates);
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/CreateTagExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/CreateTagExecutor.java
index 5d474142fd6..593d39c1226 100644
--- a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/CreateTagExecutor.java
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/CreateTagExecutor.java
@@ -55,6 +55,9 @@ public class CreateTagExecutor extends AbstractCreateEntityExecutor<TagCreation,
     @Autowired
     private ISetTagDataSetsExecutor setTagDataSetsExecutor;
 
+    @Autowired
+    private ISetTagMaterialsExecutor setTagMaterialsExecutor;
+
     @Override
     protected List<MetaprojectPE> createEntities(IOperationContext context, Collection<TagCreation> creations)
     {
@@ -112,6 +115,7 @@ public class CreateTagExecutor extends AbstractCreateEntityExecutor<TagCreation,
         setTagExperimentsExecutor.set(context, entitiesMap);
         setTagSamplesExecutor.set(context, entitiesMap);
         setTagDataSetsExecutor.set(context, entitiesMap);
+        setTagMaterialsExecutor.set(context, entitiesMap);
     }
 
     @Override
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/ISetTagMaterialsExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/ISetTagMaterialsExecutor.java
new file mode 100644
index 00000000000..d37ebe06251
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/ISetTagMaterialsExecutor.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2016 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.tag;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.tag.create.TagCreation;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.entity.ISetEntityRelationsExecutor;
+import ch.systemsx.cisd.openbis.generic.shared.dto.MetaprojectPE;
+
+/**
+ * @author pkupczyk
+ */
+public interface ISetTagMaterialsExecutor extends ISetEntityRelationsExecutor<TagCreation, MetaprojectPE>
+{
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/ISetTagMaterialsWithCacheExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/ISetTagMaterialsWithCacheExecutor.java
new file mode 100644
index 00000000000..595d7f560c9
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/ISetTagMaterialsWithCacheExecutor.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2016 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.tag;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.id.IMaterialId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.tag.create.TagCreation;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.entity.ISetEntityRelationsWithCacheExecutor;
+import ch.systemsx.cisd.openbis.generic.shared.dto.MaterialPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.MetaprojectPE;
+
+/**
+ * @author pkupczyk
+ */
+public interface ISetTagMaterialsWithCacheExecutor
+        extends ISetEntityRelationsWithCacheExecutor<TagCreation, MetaprojectPE, IMaterialId, MaterialPE>
+{
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/SetTagMaterialsExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/SetTagMaterialsExecutor.java
new file mode 100644
index 00000000000..8ed55514c1b
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/SetTagMaterialsExecutor.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.tag;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.id.IMaterialId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.tag.create.TagCreation;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.entity.AbstractSetEntityMultipleRelationsExecutor;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.material.IMapMaterialByIdExecutor;
+import ch.systemsx.cisd.openbis.generic.shared.dto.MaterialPE;
+import ch.systemsx.cisd.openbis.generic.shared.dto.MetaprojectPE;
+
+/**
+ * @author pkupczyk
+ */
+@Component
+public class SetTagMaterialsExecutor extends AbstractSetEntityMultipleRelationsExecutor<TagCreation, MetaprojectPE, IMaterialId, MaterialPE>
+        implements ISetTagMaterialsExecutor
+{
+
+    @Autowired
+    private IMapMaterialByIdExecutor mapMaterialExecutor;
+
+    @Autowired
+    private ISetTagMaterialsWithCacheExecutor setTagMaterialsWithCacheExecutor;
+
+    @Override
+    protected void addRelatedIds(Set<IMaterialId> relatedIds, TagCreation creation, MetaprojectPE entity)
+    {
+        addRelatedIds(relatedIds, creation.getMaterialIds());
+    }
+
+    @Override
+    protected void addRelated(Map<IMaterialId, MaterialPE> relatedMap, TagCreation creation, MetaprojectPE entity)
+    {
+        // nothing to do here
+    }
+
+    @Override
+    protected Map<IMaterialId, MaterialPE> map(IOperationContext context, List<IMaterialId> relatedIds)
+    {
+        return mapMaterialExecutor.map(context, relatedIds);
+    }
+
+    @Override
+    protected void check(IOperationContext context, IMaterialId relatedId, MaterialPE related)
+    {
+        // nothing to do here
+    }
+
+    @Override
+    protected void set(IOperationContext context, Map<TagCreation, MetaprojectPE> creationsMap, Map<IMaterialId, MaterialPE> relatedMap)
+    {
+        setTagMaterialsWithCacheExecutor.set(context, creationsMap, relatedMap);
+    }
+
+}
diff --git a/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/SetTagMaterialsWithCacheExecutor.java b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/SetTagMaterialsWithCacheExecutor.java
new file mode 100644
index 00000000000..06a6db69d02
--- /dev/null
+++ b/openbis/source/java/ch/ethz/sis/openbis/generic/server/asapi/v3/executor/tag/SetTagMaterialsWithCacheExecutor.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2016 ETH Zuerich, CISD
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ch.ethz.sis.openbis.generic.server.asapi.v3.executor.tag;
+
+import java.util.Collection;
+
+import org.springframework.stereotype.Component;
+
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.id.IMaterialId;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.tag.create.TagCreation;
+import ch.ethz.sis.openbis.generic.server.asapi.v3.executor.IOperationContext;
+import ch.systemsx.cisd.openbis.generic.shared.dto.MaterialPE;
+
+/**
+ * @author pkupczyk
+ */
+@Component
+public class SetTagMaterialsWithCacheExecutor extends SetTagEntitiesWithCacheExecutor<IMaterialId, MaterialPE>
+        implements ISetTagMaterialsWithCacheExecutor
+{
+
+    @Override
+    protected Class<MaterialPE> getRelatedClass()
+    {
+        return MaterialPE.class;
+    }
+
+    @Override
+    protected Collection<? extends IMaterialId> getRelatedIds(IOperationContext context, TagCreation creation)
+    {
+        return creation.getMaterialIds();
+    }
+
+}
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/as/dto/tag/update/TagUpdate.js b/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/as/dto/tag/update/TagUpdate.js
index c3badc702d4..e49bfd0bf0e 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/as/dto/tag/update/TagUpdate.js
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/as/dto/tag/update/TagUpdate.js
@@ -25,6 +25,12 @@ define([ "stjs", "as/dto/common/update/FieldUpdateValue", "as/dto/common/update/
 		prototype.setTagId = function(tagId) {
 			this.tagId = tagId;
 		};
+		prototype.getDescription = function() {
+			return this.description;
+		};
+		prototype.setDescription = function(description) {
+			this.description.setValue(description);
+		};
 		prototype.getExperimentIds = function() {
 			return this.experimentIds;
 		};
diff --git a/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/openbis.js b/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/openbis.js
index 79a8bd893d0..c6f41e81632 100644
--- a/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/openbis.js
+++ b/openbis/source/java/ch/systemsx/cisd/openbis/public/resources/api/v3/openbis.js
@@ -235,6 +235,21 @@ define([ 'jquery', 'util/Json' ], function(jquery, stjsUtil) {
 			});
 		}
 
+		this.createTags = function(creations) {
+			var thisFacade = this;
+			return thisFacade._private.ajaxRequest({
+				url : openbisUrl,
+				data : {
+					"method" : "createTags",
+					"params" : [ thisFacade._private.sessionToken, creations ]
+				},
+				returnType : {
+					name : "List",
+					arguments : [ "TagPermId" ]
+				}
+			});
+		}
+
 		this.updateSpaces = function(updates) {
 			var thisFacade = this;
 			return thisFacade._private.ajaxRequest({
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/CreateTagTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/CreateTagTest.java
index 8632c4b944c..8e675a49874 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/CreateTagTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/CreateTagTest.java
@@ -24,7 +24,9 @@ import java.util.Map;
 
 import org.testng.annotations.Test;
 
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.dataset.id.DataSetPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.experiment.id.ExperimentIdentifier;
+import ch.ethz.sis.openbis.generic.asapi.v3.dto.material.id.MaterialPermId;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.sample.id.SampleIdentifier;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.tag.Tag;
 import ch.ethz.sis.openbis.generic.asapi.v3.dto.tag.create.TagCreation;
@@ -203,6 +205,98 @@ public class CreateTagTest extends AbstractTest
             }, sampleId);
     }
 
+    @Test
+    public void testCreateWithDataSets()
+    {
+        final DataSetPermId dataSetId = new DataSetPermId("20120619092259000-22");
+
+        TagCreation creation = new TagCreation();
+        creation.setCode("TEST_TAG");
+        creation.setDataSetIds(Arrays.asList(dataSetId));
+
+        Tag tag = createTag(TEST_USER, PASSWORD, creation);
+
+        assertDataSetCodes(tag.getDataSets(), "20120619092259000-22");
+    }
+
+    @Test
+    public void testCreateWithDataSetsUnauthorized()
+    {
+        // data set connected to experiment /CISD/NEMO/EXP-TEST-1
+        final DataSetPermId dataSetId = new DataSetPermId("20081105092159111-1");
+
+        assertUnauthorizedObjectAccessException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    TagCreation creation = new TagCreation();
+                    creation.setCode("TEST_TAG");
+                    creation.setDataSetIds(Arrays.asList(dataSetId));
+
+                    createTag(TEST_SPACE_USER, PASSWORD, creation);
+                }
+            }, dataSetId);
+    }
+
+    @Test
+    public void testCreateWithDataSetsNonexistent()
+    {
+        final DataSetPermId dataSetId = new DataSetPermId("IDONTEXIST");
+
+        assertObjectNotFoundException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    TagCreation creation = new TagCreation();
+                    creation.setCode("TEST_TAG");
+                    creation.setDataSetIds(Arrays.asList(dataSetId));
+
+                    createTag(TEST_USER, PASSWORD, creation);
+                }
+            }, dataSetId);
+    }
+
+    @Test
+    public void testCreateWithMaterials()
+    {
+        MaterialPermId materialId = new MaterialPermId("AD3", "VIRUS");
+
+        TagCreation creation = new TagCreation();
+        creation.setCode("TEST_TAG");
+        creation.setMaterialIds(Arrays.asList(materialId));
+
+        Tag tag = createTag(TEST_USER, PASSWORD, creation);
+
+        assertMaterialPermIds(tag.getMaterials(), materialId);
+    }
+
+    @Test
+    public void testCreateWithMaterialsUnauthorized()
+    {
+        // nothing to test as the materials can be accessed by every user
+    }
+
+    @Test
+    public void testCreateWithMaterialsNonexistent()
+    {
+        final MaterialPermId materialId = new MaterialPermId("IDONTEXIST", "VIRUS");
+
+        assertObjectNotFoundException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    TagCreation creation = new TagCreation();
+                    creation.setCode("TEST_TAG");
+                    creation.setMaterialIds(Arrays.asList(materialId));
+
+                    createTag(TEST_USER, PASSWORD, creation);
+                }
+            }, materialId);
+    }
+
     private Tag createTag(String user, String password, TagCreation creation)
     {
         String sessionToken = v3api.login(user, password);
diff --git a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateTagTest.java b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateTagTest.java
index 6b27b81e92c..fa092215949 100644
--- a/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateTagTest.java
+++ b/openbis/sourceTest/java/ch/ethz/sis/openbis/systemtest/asapi/v3/UpdateTagTest.java
@@ -144,6 +144,8 @@ public class UpdateTagTest extends AbstractTest
     @Test
     public void testUpdateWithExperimentsUnauthorized()
     {
+        final ExperimentIdentifier experimentId = new ExperimentIdentifier("/CISD/NEMO/EXP10");
+
         assertUnauthorizedObjectAccessException(new IDelegatedAction()
             {
                 @Override
@@ -154,11 +156,32 @@ public class UpdateTagTest extends AbstractTest
 
                     TagUpdate update = new TagUpdate();
                     update.setTagId(before.getPermId());
-                    update.getExperimentIds().add(new ExperimentIdentifier("/CISD/NEMO/EXP10"));
+                    update.getExperimentIds().add(experimentId);
 
                     updateTag(TEST_SPACE_USER, PASSWORD, update);
                 }
-            }, new ExperimentIdentifier("/CISD/NEMO/EXP10"));
+            }, experimentId);
+    }
+
+    @Test
+    public void testUpdateWithExperimentsNonexistent()
+    {
+        final ExperimentIdentifier experimentId = new ExperimentIdentifier("/CISD/NEMO/IDONTEXIST");
+
+        assertObjectNotFoundException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    Tag before = getTag(TEST_USER, PASSWORD, new TagPermId(TEST_USER, "TEST_METAPROJECTS"));
+
+                    TagUpdate update = new TagUpdate();
+                    update.setTagId(before.getPermId());
+                    update.getExperimentIds().add(experimentId);
+
+                    updateTag(TEST_USER, PASSWORD, update);
+                }
+            }, experimentId);
     }
 
     @Test
@@ -226,6 +249,27 @@ public class UpdateTagTest extends AbstractTest
             }, new SampleIdentifier("/CISD/CP-TEST-1"));
     }
 
+    @Test
+    public void testUpdateWithSamplesNonexistent()
+    {
+        final SampleIdentifier sampleId = new SampleIdentifier("/CISD/IDONTEXIST");
+
+        assertObjectNotFoundException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    Tag before = getTag(TEST_USER, PASSWORD, new TagPermId(TEST_USER, "TEST_METAPROJECTS"));
+
+                    TagUpdate update = new TagUpdate();
+                    update.setTagId(before.getPermId());
+                    update.getSampleIds().add(sampleId);
+
+                    updateTag(TEST_USER, PASSWORD, update);
+                }
+            }, sampleId);
+    }
+
     @Test
     public void testUpdateWithDataSetsAdd()
     {
@@ -274,6 +318,8 @@ public class UpdateTagTest extends AbstractTest
     @Test
     public void testUpdateWithDataSetsUnauthorized()
     {
+        final DataSetPermId dataSetId = new DataSetPermId("20081105092159111-1");
+
         assertUnauthorizedObjectAccessException(new IDelegatedAction()
             {
                 @Override
@@ -285,15 +331,36 @@ public class UpdateTagTest extends AbstractTest
                     TagUpdate update = new TagUpdate();
                     update.setTagId(before.getPermId());
                     // data set connected to experiment /CISD/NEMO/EXP-TEST-1
-                    update.getDataSetIds().add(new DataSetPermId("20081105092159111-1"));
+                    update.getDataSetIds().add(dataSetId);
 
                     updateTag(TEST_SPACE_USER, PASSWORD, update);
                 }
-            }, new DataSetPermId("20081105092159111-1"));
+            }, dataSetId);
+    }
+
+    @Test
+    public void testUpdateWithDataSetsNonexistent()
+    {
+        final DataSetPermId dataSetId = new DataSetPermId("IDONTEXIST");
+
+        assertObjectNotFoundException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    Tag before = getTag(TEST_USER, PASSWORD, new TagPermId(TEST_USER, "TEST_METAPROJECTS"));
+
+                    TagUpdate update = new TagUpdate();
+                    update.setTagId(before.getPermId());
+                    update.getDataSetIds().add(dataSetId);
+
+                    updateTag(TEST_USER, PASSWORD, update);
+                }
+            }, dataSetId);
     }
 
     @Test
-    public void testUpdateWithMaterialAdd()
+    public void testUpdateWithMaterialsAdd()
     {
         Tag before = getTag(TEST_USER, PASSWORD, new TagPermId(TEST_USER, "TEST_METAPROJECTS"));
         assertMaterialPermIds(before.getMaterials(), new MaterialPermId("AD3", "VIRUS"));
@@ -308,7 +375,7 @@ public class UpdateTagTest extends AbstractTest
     }
 
     @Test
-    public void testUpdateWithMaterialRemove()
+    public void testUpdateWithMaterialsRemove()
     {
         Tag before = getTag(TEST_USER, PASSWORD, new TagPermId(TEST_USER, "TEST_METAPROJECTS"));
         assertMaterialPermIds(before.getMaterials(), new MaterialPermId("AD3", "VIRUS"));
@@ -323,7 +390,7 @@ public class UpdateTagTest extends AbstractTest
     }
 
     @Test
-    public void testUpdateWithMaterialSet()
+    public void testUpdateWithMaterialsSet()
     {
         Tag before = getTag(TEST_USER, PASSWORD, new TagPermId(TEST_USER, "TEST_METAPROJECTS"));
         assertMaterialPermIds(before.getMaterials(), new MaterialPermId("AD3", "VIRUS"));
@@ -338,11 +405,32 @@ public class UpdateTagTest extends AbstractTest
     }
 
     @Test
-    public void testUpdateWithMaterialUnauthorized()
+    public void testUpdateWithMaterialsUnauthorized()
     {
         // nothing to test as the materials can be accessed by every user
     }
 
+    @Test
+    public void testUpdateWithMaterialsNonexistent()
+    {
+        final MaterialPermId materialId = new MaterialPermId("IDONTEXIST", "MENEITHER");
+
+        assertObjectNotFoundException(new IDelegatedAction()
+            {
+                @Override
+                public void execute()
+                {
+                    Tag before = getTag(TEST_USER, PASSWORD, new TagPermId(TEST_USER, "TEST_METAPROJECTS"));
+
+                    TagUpdate update = new TagUpdate();
+                    update.setTagId(before.getPermId());
+                    update.getMaterialIds().add(materialId);
+
+                    updateTag(TEST_USER, PASSWORD, update);
+                }
+            }, materialId);
+    }
+
     private Tag getTag(String user, String password, ITagId tagId)
     {
         TagFetchOptions fetchOptions = new TagFetchOptions();
-- 
GitLab