Add patch to notification method and clean up code

Update docs with new command

Move validation of the period field to the setter for
java api which is called by jackson as it constructs the
command objects.

Add tempest tests for patching

Change-Id: I7f31aa059601c0390c42b0f5bdf4091706f6660d
This commit is contained in:
Michael James Hoppal 2016-06-15 13:22:18 -06:00 committed by Ryan Brandt
parent 4df2cca40f
commit 04d6b5fdfd
12 changed files with 479 additions and 28 deletions

View File

@ -1707,6 +1707,77 @@ Returns a JSON notification method object with the following fields:
````
___
## Patch Notification Method
Patch the specified notification method.
### PATCH /v2.0/notification-methods/{notification_method_id}
#### Headers
* X-Auth-Token (string, required) - Keystone auth token
* Content-Type (string, required) - application/json
* Accept (string) - application/json
#### Path Parameters
* notification_method_id (string, required) - ID of the notification method to update.
#### Query Parameters
None.
#### Request Body
* name (string(250), optional) - A descriptive name of the notifcation method.
* type (string(100), optional) - The type of notification method (`EMAIL`, `WEBHOOK`, or `PAGERDUTY` ).
* address (string(100), optional) - The email/url address to notify.
* period (integer, optional) - The interval in seconds to periodically send the notification. Only can be set as a non zero value for WEBHOOK methods. Allowed periods for Webhooks by default are 0, 60. You can change allow periods for webhooks in the api config. The notification will continue to be sent at the defined interval until the alarm it is associated with changes state.
#### Request Examples
````
PATCH /v2.0/notification-methods/35cc6f1c-3a29-49fb-a6fc-d9d97d190508 HTTP/1.1
Host: 192.168.10.4:8080
Content-Type: application/json
X-Auth-Token: 2b8882ba2ec44295bf300aecb2caa4f7
Cache-Control: no-cache
{
"name":"New name of notification method",
"type":"EMAIL",
"address":"jane.doe@hp.com",
"period":0
}
````
### Response
#### Status Code
* 200 - OK
#### Response Body
Returns a JSON notification method object with the following fields:
* id (string) - ID of notification method
* links ([link])
* name (string) - Name of notification method
* type (string) - Type of notification method
* address (string) - Address of notification method
* period (integer) - Period of notification method
#### Response Examples
````
{
"id":"35cc6f1c-3a29-49fb-a6fc-d9d97d190508",
"links":[
{
"rel":"self",
"href":"http://192.168.10.4:8080/v2.0/notification-methods/35cc6f1c-3a29-49fb-a6fc-d9d97d190508"
}
],
"name":"New name of notification method",
"type":"EMAIL",
"address":"jane.doe@hp.com",
"period":0
}
````
___
## Delete Notification Method
Delete the specified notification method.

View File

@ -129,6 +129,7 @@ public class MonApiApplication extends Application<ApiConfig> {
PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
environment.getObjectMapper().enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
environment.getObjectMapper().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
environment.getObjectMapper().disable(DeserializationFeature.WRAP_EXCEPTIONS);
SimpleModule module = new SimpleModule("SerializationModule");
module.addSerializer(new SubAlarmExpressionSerializer());
environment.getObjectMapper().registerModule(module);

View File

@ -13,13 +13,16 @@
*/
package monasca.api.app.command;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.hibernate.validator.constraints.NotEmpty;
import java.util.List;
import monasca.api.app.validation.NotificationMethodValidation;
import monasca.api.app.validation.Validation;
import monasca.api.domain.model.notificationmethod.NotificationMethodType;
public class CreateNotificationMethodCommand {
@ -32,6 +35,7 @@ public class CreateNotificationMethodCommand {
@Size(min = 1, max = 512)
public String address;
public String period;
private int convertedPeriod = 0;
public CreateNotificationMethodCommand() {this.period = "0";}
@ -39,7 +43,8 @@ public class CreateNotificationMethodCommand {
this.name = name;
this.type = type;
this.address = address;
this.period = period == null ? "0" : period;
period = period == null ? "0" : period;
this.setPeriod(period);
}
@Override
@ -68,11 +73,22 @@ public class CreateNotificationMethodCommand {
return false;
if (type != other.type)
return false;
if (convertedPeriod != other.convertedPeriod)
return false;
return true;
}
public void validate(List<Integer> validPeriods) {
NotificationMethodValidation.validate(type, address, period, validPeriods);
NotificationMethodValidation.validate(type, address, convertedPeriod, validPeriods);
}
public void setPeriod(String period){
this.period = period;
this.convertedPeriod = Validation.parseAndValidateNumber(period, "period");
}
public int getConvertedPeriod(){
return this.convertedPeriod;
}
@Override
@ -83,6 +99,7 @@ public class CreateNotificationMethodCommand {
result = prime * result + ((type == null) ? 0 : type.hashCode());
result = prime * result + ((address == null) ? 0 : address.hashCode());
result = prime * result + ((period == null) ? 0 : period.hashCode());
result = prime * result + convertedPeriod;
return result;
}
}

View File

@ -0,0 +1,99 @@
/*
* (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
*
* 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 monasca.api.app.command;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import javax.validation.constraints.Size;
import monasca.api.app.validation.NotificationMethodValidation;
import monasca.api.app.validation.Validation;
import monasca.api.domain.model.notificationmethod.NotificationMethodType;
public class PatchNotificationMethodCommand {
@Size(min = 1, max = 250)
public String name;
public NotificationMethodType type;
@Size(min = 1, max = 512)
public String address;
public String period;
private int convertedPeriod = 0;
public PatchNotificationMethodCommand() {}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PatchNotificationMethodCommand other = (PatchNotificationMethodCommand) obj;
if (address == null) {
if (other.address != null)
return false;
} else if (!address.equals(other.address))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (period == null) {
if (other.period != null)
return false;
} else if (!period.equals(other.period))
return false;
if (type != other.type)
return false;
if (convertedPeriod != other.convertedPeriod)
return false;
return true;
}
public void validate(List<Integer> validPeriods) {
NotificationMethodValidation.validate(type, address, convertedPeriod, validPeriods);
}
@JsonProperty("period")
public void setPeriod(String period){
this.period = period;
this.convertedPeriod = Validation.parseAndValidateNumber(period, "period");
}
@JsonIgnore
public void setPeriod(int period){
this.convertedPeriod = period;
}
public int getConvertedPeriod(){
return this.convertedPeriod;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + ((type == null) ? 0 : type.hashCode());
result = prime * result + ((address == null) ? 0 : address.hashCode());
result = prime * result + ((period == null) ? 0 : period.hashCode());
result = prime * result + convertedPeriod;
return result;
}
}

View File

@ -20,6 +20,7 @@ import org.hibernate.validator.constraints.NotEmpty;
import java.util.List;
import monasca.api.app.validation.NotificationMethodValidation;
import monasca.api.app.validation.Validation;
import monasca.api.domain.model.notificationmethod.NotificationMethodType;
public class UpdateNotificationMethodCommand {
@ -33,6 +34,7 @@ public class UpdateNotificationMethodCommand {
public String address;
@NotNull
public String period;
private int convertedPeriod = 0;
public UpdateNotificationMethodCommand() {}
@ -40,7 +42,7 @@ public class UpdateNotificationMethodCommand {
this.name = name;
this.type = type;
this.address = address;
this.period = period;
this.setPeriod(period);
}
@Override
@ -69,11 +71,22 @@ public class UpdateNotificationMethodCommand {
return false;
if (type != other.type)
return false;
if (convertedPeriod != other.convertedPeriod)
return false;
return true;
}
public void validate(List<Integer> validPeriods) {
NotificationMethodValidation.validate(type, address, period, validPeriods);
NotificationMethodValidation.validate(type, address, convertedPeriod, validPeriods);
}
public void setPeriod(String period){
this.period = period;
this.convertedPeriod = Validation.parseAndValidateNumber(period, "period");
}
public int getConvertedPeriod(){
return this.convertedPeriod;
}
@Override
@ -84,6 +97,7 @@ public class UpdateNotificationMethodCommand {
result = prime * result + ((type == null) ? 0 : type.hashCode());
result = prime * result + ((address == null) ? 0 : address.hashCode());
result = prime * result + ((period == null) ? 0 : period.hashCode());
result = prime * result + convertedPeriod;
return result;
}
}

View File

@ -31,14 +31,13 @@ public class NotificationMethodValidation {
TEST_TLD_VALIDATOR,
UrlValidator.ALLOW_LOCAL_URLS | UrlValidator.ALLOW_2_SLASHES);
public static void validate(NotificationMethodType type, String address, String period,
public static void validate(NotificationMethodType type, String address, int period,
List<Integer> validPeriods) {
int convertedPeriod = Validation.parseAndValidateNumber(period, "period");
switch (type) {
case EMAIL : {
if (!EmailValidator.getInstance(true).isValid(address))
throw Exceptions.unprocessableEntity("Address %s is not of correct format", address);
if (convertedPeriod != 0)
if (period != 0)
throw Exceptions.unprocessableEntity("Period can not be non zero for Email");
} break;
case WEBHOOK : {
@ -46,12 +45,12 @@ public class NotificationMethodValidation {
throw Exceptions.unprocessableEntity("Address %s is not of correct format", address);
} break;
case PAGERDUTY : {
if (convertedPeriod != 0)
if (period != 0)
throw Exceptions.unprocessableEntity("Period can not be non zero for Pagerduty");
} break;
}
if (convertedPeriod != 0 && !validPeriods.contains(convertedPeriod)){
throw Exceptions.unprocessableEntity("%d is not a valid period", convertedPeriod);
if (period != 0 && !validPeriods.contains(period)){
throw Exceptions.unprocessableEntity("%d is not a valid period", period);
}
}
}

View File

@ -39,11 +39,15 @@ import javax.ws.rs.core.UriInfo;
import monasca.api.ApiConfig;
import monasca.api.app.command.CreateNotificationMethodCommand;
import monasca.api.app.command.PatchNotificationMethodCommand;
import monasca.api.app.command.UpdateNotificationMethodCommand;
import monasca.api.app.validation.NotificationMethodValidation;
import monasca.api.app.validation.Validation;
import monasca.api.domain.model.notificationmethod.NotificationMethod;
import monasca.api.domain.model.notificationmethod.NotificationMethodRepo;
import monasca.api.domain.model.notificationmethod.NotificationMethodType;
import monasca.api.infrastructure.persistence.PersistUtils;
import monasca.api.resource.annotation.PATCH;
/**
* Notification Method resource implementation.
@ -74,11 +78,10 @@ public class NotificationMethodResource {
public Response create(@Context UriInfo uriInfo, @HeaderParam("X-Tenant-Id") String tenantId,
@Valid CreateNotificationMethodCommand command) {
command.validate(this.validPeriods);
int period = Validation.parseAndValidateNumber(command.period, "period");
NotificationMethod notificationMethod =
Links.hydrate(repo.create(tenantId, command.name, command.type,
command.address, period), uriInfo,
command.address, command.getConvertedPeriod()), uriInfo,
false);
return Response.created(URI.create(notificationMethod.getId())).entity(notificationMethod)
.build();
@ -123,14 +126,40 @@ public class NotificationMethodResource {
@PathParam("notification_method_id") String notificationMethodId,
@Valid UpdateNotificationMethodCommand command) {
command.validate(this.validPeriods);
int period = Validation.parseAndValidateNumber(command.period, "period");
return Links.hydrate(
repo.update(tenantId, notificationMethodId, command.name, command.type,
command.address, period),
command.address, command.getConvertedPeriod()),
uriInfo, true);
}
@PATCH
@Timed
@Path("/{notification_method_id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public NotificationMethod patch(@Context UriInfo uriInfo,
@HeaderParam("X-Tenant-Id") String tenantId,
@PathParam("notification_method_id") String notificationMethodId,
@Valid PatchNotificationMethodCommand command) {
NotificationMethod originalNotificationMethod = repo.findById(tenantId, notificationMethodId);
String name = command.name == null ? originalNotificationMethod.getName()
: command.name;
NotificationMethodType type = command.type == null ? originalNotificationMethod.getType()
: command.type;
String address = command.address == null ? originalNotificationMethod.getAddress()
: command.address;
int period = command.period == null ? originalNotificationMethod.getPeriod()
: command.getConvertedPeriod();
NotificationMethodValidation.validate(type, address, period, this.validPeriods);
return Links.hydrate(
repo.update(tenantId, notificationMethodId, name, type,
address, period),
uriInfo, true);
}
@DELETE
@Timed
@Path("/{notification_method_id}")

View File

@ -208,20 +208,6 @@ public class NotificationMethodResourceTest extends AbstractMonApiResourceTest {
"[address size must be between 1 and 512");
}
public void should422OnNonIntPeriod() {
ClientResponse response =
client()
.resource("/v2.0/notification-methods")
.header("X-Tenant-Id", "abc")
.header("Content-Type", MediaType.APPLICATION_JSON)
.post(ClientResponse.class,
new CreateNotificationMethodCommand("MyEmail", NotificationMethodType.EMAIL, "a@a.com", "not a int"));
String e = response.getEntity(String.class);
ErrorMessages.assertThat(e).matches("unprocessable_entity", 422,
"period (not a int) must be valid number");
}
public void should422OnNonZeroPeriodForEmail() {
ClientResponse response =
client()

View File

@ -33,3 +33,6 @@ class NotificationsV2API(object):
def on_put(self, req, res, notification_method_id):
res.status = '501 Not Implemented'
def on_patch(self, req, res, notification_method_id):
res.status = '501 Not Implemented'

View File

@ -169,6 +169,18 @@ class Notifications(notifications_api_v2.NotificationsV2API):
self._notifications_repo.delete_notification(tenant_id,
notification_id)
@resource.resource_try_catch_block
def _patch_get_notification(self, tenant_id, notification_id, notification):
original_notification = self._notifications_repo.list_notification(tenant_id, notification_id)
if 'name' not in notification:
notification['name'] = original_notification['name']
if 'type' not in notification:
notification['type'] = original_notification['type']
if 'address' not in notification:
notification['address'] = original_notification['address']
if 'period' not in notification:
notification['period'] = original_notification['period']
def on_post(self, req, res):
helpers.validate_json_content_type(req)
helpers.validate_authorization(req, self._default_authorized_roles)
@ -225,3 +237,15 @@ class Notifications(notifications_api_v2.NotificationsV2API):
notification, req.uri)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200
def on_patch(self, req, res, notification_method_id):
helpers.validate_json_content_type(req)
helpers.validate_authorization(req, self._default_authorized_roles)
notification = helpers.read_http_resource(req)
tenant_id = helpers.get_tenant_id(req)
self._patch_get_notification(tenant_id, notification_method_id, notification)
self._parse_and_validate_notification(notification, require_all=True)
result = self._update_notification(notification_method_id, tenant_id,
notification, req.uri)
res.body = helpers.dumpit_utf8(result)
res.status = falcon.HTTP_200

View File

@ -126,6 +126,25 @@ class MonascaClient(rest_client.RestClient):
resp, response_body = self.put(uri, json.dumps(request_body))
return resp, json.loads(response_body)
def patch_notification_method(self,
id,
name=None,
type=None,
address=None,
period=None):
uri = 'notification-methods/' + id
request_body = {}
if name is not None:
request_body['name'] = name
if type is not None:
request_body['type'] = type
if address is not None:
request_body['address'] = address
if period is not None:
request_body['period'] = period
resp, response_body = self.patch(uri, json.dumps(request_body))
return resp, json.loads(response_body)
def create_alarm_definitions(self, alarm_definitions):
uri = 'alarm-definitions'
request_body = json.dumps(alarm_definitions)

View File

@ -658,3 +658,192 @@ class TestNotificationMethods(base.BaseMonascaTest):
resp, response_body = \
self.monasca_client.delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
def test_patch_notification_method_name(self):
name = data_utils.rand_name('notification-')
notification = helpers.create_notification(name=name)
resp, response_body = self.monasca_client.create_notifications(
notification)
self.assertEqual(201, resp.status)
self.assertEqual(name, response_body['name'])
id = response_body['id']
new_name = name + 'update'
resp, response_body = self.monasca_client.\
patch_notification_method(id, new_name)
self.assertEqual(200, resp.status)
self.assertEqual(new_name, response_body['name'])
resp, response_body = self.monasca_client.\
delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
def test_patch_notification_method_type(self):
type = 'EMAIL'
notification = helpers.create_notification(type=type)
resp, response_body = self.monasca_client.create_notifications(
notification)
self.assertEqual(201, resp.status)
self.assertEqual(type, response_body['type'])
id = response_body['id']
new_type = 'PAGERDUTY'
resp, response_body = \
self.monasca_client.\
patch_notification_method(id, type=new_type)
self.assertEqual(200, resp.status)
self.assertEqual(new_type, response_body['type'])
resp, response_body = self.monasca_client.\
delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
def test_patch_notification_method_address(self):
address = DEFAULT_EMAIL_ADDRESS
notification = helpers.create_notification(address=address)
resp, response_body = self.monasca_client.create_notifications(
notification)
self.assertEqual(201, resp.status)
self.assertEqual(address, response_body['address'])
id = response_body['id']
new_address = 'jane.doe@domain.com'
resp, response_body = self.monasca_client.\
patch_notification_method(id, address=new_address)
self.assertEqual(200, resp.status)
self.assertEqual(new_address, response_body['address'])
resp, response_body = \
self.monasca_client.delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_patch_notification_method_name_exceeds_max_length(self):
name = data_utils.rand_name('notification-')
notification = helpers.create_notification(name=name)
resp, response_body = self.monasca_client.create_notifications(
notification)
id = response_body['id']
self.assertEqual(201, resp.status)
new_name_long = "x" * (constants.MAX_NOTIFICATION_METHOD_NAME_LENGTH
+ 1)
self.assertRaises((exceptions.BadRequest, exceptions.UnprocessableEntity),
self.monasca_client.patch_notification_method, id,
name=new_name_long)
resp, response_body = \
self.monasca_client.delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_patch_notification_method_invalid_type(self):
name = data_utils.rand_name('notification-')
notification = helpers.create_notification(name=name)
resp, response_body = self.monasca_client.create_notifications(
notification)
id = response_body['id']
self.assertEqual(201, resp.status)
self.assertRaises((exceptions.BadRequest, exceptions.UnprocessableEntity),
self.monasca_client.patch_notification_method, id, type='random')
resp, response_body = \
self.monasca_client.delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_patch_notification_method_address_exceeds_max_length(self):
name = data_utils.rand_name('notification-')
notification = helpers.create_notification(name=name)
resp, response_body = self.monasca_client.create_notifications(
notification)
id = response_body['id']
self.assertEqual(201, resp.status)
new_address_long = "x" * (
constants.MAX_NOTIFICATION_METHOD_ADDRESS_LENGTH + 1)
self.assertRaises((exceptions.BadRequest, exceptions.UnprocessableEntity),
self.monasca_client.patch_notification_method, id, address=new_address_long)
resp, response_body = \
self.monasca_client.delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_patch_email_notification_method_with_nonzero_period(self):
name = data_utils.rand_name('notification-')
notification = helpers.create_notification(name=name)
resp, response_body = self.monasca_client.create_notifications(
notification)
id = response_body['id']
self.assertEqual(201, resp.status)
self.assertRaises((exceptions.BadRequest, exceptions.UnprocessableEntity),
self.monasca_client.patch_notification_method, id, period=60)
resp, response_body = \
self.monasca_client.delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_patch_webhook_notification_method_to_email_with_nonzero_period(self):
name = data_utils.rand_name('notification-')
notification = helpers.create_notification(name=name,
type='WEBHOOK',
address='http://localhost/test01',
period=60)
resp, response_body = self.monasca_client.create_notifications(
notification)
id = response_body['id']
self.assertEqual(201, resp.status)
self.assertRaises((exceptions.BadRequest, exceptions.UnprocessableEntity),
self.monasca_client.patch_notification_method, id, type='EMAIL')
resp, response_body = \
self.monasca_client.delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_patch_webhook_notification_method_to_pagerduty_with_nonzero_period(self):
name = data_utils.rand_name('notification-')
notification = helpers.create_notification(name=name,
type='WEBHOOK',
address='http://localhost/test01',
period=60)
resp, response_body = self.monasca_client.create_notifications(
notification)
id = response_body['id']
self.assertEqual(201, resp.status)
self.assertRaises((exceptions.BadRequest, exceptions.UnprocessableEntity),
self.monasca_client.patch_notification_method, id, type='PAGERDUTY')
resp, response_body = \
self.monasca_client.delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_patch_notification_method_with_non_int_period(self):
name = data_utils.rand_name('notification-')
notification = helpers.create_notification(name=name)
resp, response_body = self.monasca_client.create_notifications(
notification)
id = response_body['id']
self.assertEqual(201, resp.status)
self.assertRaises((exceptions.BadRequest, exceptions.UnprocessableEntity),
self.monasca_client.patch_notification_method, id, period='zero')
resp, response_body = \
self.monasca_client.delete_notification_method(id)
self.assertEqual(204, resp.status)
@test.attr(type="gate")
@test.attr(type=['negative'])
def test_patch_webhook_notification_method_with_invalid_period(self):
name = data_utils.rand_name('notification-')
notification = helpers.create_notification(name=name,
type='WEBHOOK',
address='http://localhost/test01',
period=60)
resp, response_body = self.monasca_client.create_notifications(
notification)
id = response_body['id']
self.assertEqual(201, resp.status)
self.assertRaises((exceptions.BadRequest, exceptions.UnprocessableEntity),
self.monasca_client.patch_notification_method, id, period=5)
resp, response_body = \
self.monasca_client.delete_notification_method(id)
self.assertEqual(204, resp.status)