diff options
author | Hakim Cassimally <hakim@mysociety.org> | 2014-03-13 16:56:02 +0000 |
---|---|---|
committer | Hakim Cassimally <hakim@mysociety.org> | 2014-10-16 16:56:26 +0000 |
commit | d1fee928f02dbc30d3a38b746155ce5b12be4a1b (patch) | |
tree | 5e8bdccbd69863e69098b9aa900c1e71745f8eb5 /t/open311 | |
parent | 592f4c0ba0f822b55bb242cb12768ce771599d09 (diff) |
Open311 Endpoint
Subsystems include
* ::Spark encoding conventions for xml/json
* ::Schema using Rx to validate form of inputs and outputs,
including validation for, e.g., dates and CSV as part of Open311
Handles following paths:
* Open311 attributes for Service Definition
http://wiki.open311.org/GeoReport_v2#GET_Service_Definition
* POST service request
* GET Service Requests
* GET Service Request
Objects:
* ::Service
* ::Service::Request
Diffstat (limited to 't/open311')
-rw-r--r-- | t/open311/endpoint.t | 351 | ||||
-rw-r--r-- | t/open311/endpoint/Endpoint1.pm | 114 | ||||
-rw-r--r-- | t/open311/endpoint/ServiceType1.pm | 12 | ||||
-rw-r--r-- | t/open311/endpoint/schema.t | 82 | ||||
-rw-r--r-- | t/open311/endpoint/spark.t | 64 |
5 files changed, 623 insertions, 0 deletions
diff --git a/t/open311/endpoint.t b/t/open311/endpoint.t new file mode 100644 index 000000000..fd794feec --- /dev/null +++ b/t/open311/endpoint.t @@ -0,0 +1,351 @@ +use strict; use warnings; + +use Test::More; +use Test::LongString; +use Test::MockTime ':all'; + +use Open311::Endpoint; +use Data::Dumper; +use JSON; + +use t::open311::endpoint::Endpoint1; + +my $endpoint = t::open311::endpoint::Endpoint1->new; +my $json = JSON->new; + +subtest "GET Service List" => sub { + my $res = $endpoint->run_test_request( GET => '/services.xml' ); + ok $res->is_success, 'xml success' + or diag $res->content; + is_string $res->content, <<CONTENT, 'xml string ok'; +<?xml version="1.0" encoding="utf-8"?> +<services> + <service> + <description>Pothole Repairs Service</description> + <group>highways</group> + <keywords>deep,hole,wow</keywords> + <metadata>true</metadata> + <service_code>POT</service_code> + <service_name>Pothole Repairs</service_name> + <type>realtime</type> + </service> + <service> + <description>Bin Enforcement Service</description> + <group>sanitation</group> + <keywords>bin</keywords> + <metadata>false</metadata> + <service_code>BIN</service_code> + <service_name>Bin Enforcement</service_name> + <type>realtime</type> + </service> +</services> +CONTENT + + $res = $endpoint->run_test_request( GET => '/services.json' ); + ok $res->is_success, 'json success'; + is_deeply $json->decode($res->content), + [ { + "keywords" => "deep,hole,wow", + "group" => "highways", + "service_name" => "Pothole Repairs", + "type" => "realtime", + "metadata" => "true", + "description" => "Pothole Repairs Service", + "service_code" => "POT" + }, { + "keywords" => "bin", + "group" => "sanitation", + "service_name" => "Bin Enforcement", + "type" => "realtime", + "metadata" => "false", + "description" => "Bin Enforcement Service", + "service_code" => "BIN" + } ], 'json structure ok'; + +}; + +subtest "GET Service Definition" => sub { + my $res = $endpoint->run_test_request( GET => '/services/POT.xml' ); + ok $res->is_success, 'xml success', + or diag $res->content; + is_string $res->content, <<CONTENT, 'xml string ok'; +<?xml version="1.0" encoding="utf-8"?> +<service_definition> + <attributes> + <attribute> + <code>depth</code> + <datatype>number</datatype> + <datatype_description>an integer</datatype_description> + <description>depth of pothole, in centimetres</description> + <order>1</order> + <required>true</required> + <variable>true</variable> + </attribute> + <attribute> + <code>shape</code> + <datatype>singlevaluelist</datatype> + <datatype_description>square | circle | triangle</datatype_description> + <description>shape of the pothole</description> + <order>2</order> + <required>false</required> + <values> + <value> + <name>Triangle</name> + <key>triangle</key> + </value> + <value> + <name>Circle</name> + <key>circle</key> + </value> + <value> + <name>Square</name> + <key>square</key> + </value> + </values> + <variable>true</variable> + </attribute> + </attributes> + <service_code>POT</service_code> +</service_definition> +CONTENT + + $res = $endpoint->run_test_request( GET => '/services/POT.json' ); + ok $res->is_success, 'json success'; + is_deeply $json->decode($res->content), + { + "service_code" => "POT", + "attributes" => [ + { + "order" => 1, + "code" => "depth", + "required" => "true", + "variable" => "true", + "datatype_description" => "an integer", + "description" => "depth of pothole, in centimetres", + "datatype" => "number", + }, + { + "order" => 2, + "code" => "shape", + "variable" => "true", + "datatype_description" => "square | circle | triangle", + "description" => "shape of the pothole", + "required" => "false", + "datatype" => "singlevaluelist", + "values" => [ + { + "name" => "Triangle", + "key" => "triangle" + }, + { + "name" => "Circle", + "key" => "circle" + }, + { + "name" => "Square", + "key" => "square" + } + ], + } + ], + }, 'json structure ok'; +}; + +subtest "POST Service Request validation" => sub { + my $res = $endpoint->run_test_request( + POST => '/requests.json', + ); + ok ! $res->is_success, 'no service_code'; + + $res = $endpoint->run_test_request( + POST => '/requests.json', + service_code => 'BIN', + ); + ok ! $res->is_success, 'no api_key'; + + $res = $endpoint->run_test_request( + POST => '/requests.json', + api_key => 'test', + service_code => 'BADGER', # has moved the goalposts + ); + ok ! $res->is_success, 'bad service_code'; + + $res = $endpoint->run_test_request( + POST => '/requests.json', + api_key => 'test', + service_code => 'POT', + address_string => '22 Acacia Avenue', + first_name => 'Bob', + last_name => 'Mould', + ); + ok ! $res->is_success, 'no required attributes'; + + $res = $endpoint->run_test_request( + POST => '/requests.json', + api_key => 'test', + service_code => 'POT', + address_string => '22 Acacia Avenue', + first_name => 'Bob', + last_name => 'Mould', + 'attribute[depth]' => 100, + 'attribute[shape]' => 'starfish', + ); + ok ! $res->is_success, 'bad attribute'; +}; + +subtest "POST Service Request valid test" => sub { + + set_fixed_time('2014-01-01T12:00:00Z'); + my $res = $endpoint->run_test_request( + POST => '/requests.json', + api_key => 'test', + service_code => 'POT', + address_string => '22 Acacia Avenue', + first_name => 'Bob', + last_name => 'Mould', + 'attribute[depth]' => 100, + 'attribute[shape]' => 'triangle', + ); + ok $res->is_success, 'valid request' + or diag $res->content; + + is_deeply $json->decode($res->content), + [ { + "service_notice" => "This is a test service", + "service_request_id" => 0 + } ], 'correct json returned'; + + set_fixed_time('2014-02-01T12:00:00Z'); + $res = $endpoint->run_test_request( + POST => '/requests.xml', + api_key => 'test', + service_code => 'POT', + address_string => '22 Acacia Avenue', + first_name => 'Bob', + last_name => 'Mould', + 'attribute[depth]' => 100, + 'attribute[shape]' => 'triangle', + ); + + ok $res->is_success, 'valid request' + or diag $res->content; + + is_string $res->content, <<CONTENT, 'xml string ok'; +<?xml version="1.0" encoding="utf-8"?> +<service_requests> + <request> + <service_notice>This is a test service</service_notice> + <service_request_id>1</service_request_id> + </request> +</service_requests> +CONTENT +}; + +subtest "GET Service Requests" => sub { + + my $res = $endpoint->run_test_request( GET => '/requests.xml', ); + ok $res->is_success, 'valid request'; + my $xml = <<CONTENT; +<?xml version="1.0" encoding="utf-8"?> +<service_requests> + <request> + <address>22 Acacia Avenue</address> + <address_id></address_id> + <lat>0</lat> + <long>0</long> + <media_url></media_url> + <requested_datetime>2014-01-01T12:00:00Z</requested_datetime> + <service_code>POT</service_code> + <service_name>Pothole Repairs</service_name> + <service_request_id>0</service_request_id> + <status>open</status> + <updated_datetime>2014-01-01T12:00:00Z</updated_datetime> + <zipcode></zipcode> + </request> + <request> + <address>22 Acacia Avenue</address> + <address_id></address_id> + <lat>0</lat> + <long>0</long> + <media_url></media_url> + <requested_datetime>2014-02-01T12:00:00Z</requested_datetime> + <service_code>POT</service_code> + <service_name>Pothole Repairs</service_name> + <service_request_id>1</service_request_id> + <status>open</status> + <updated_datetime>2014-02-01T12:00:00Z</updated_datetime> + <zipcode></zipcode> + </request> +</service_requests> +CONTENT + + is_string $res->content, $xml, 'xml string ok'; + + $res = $endpoint->run_test_request( GET => '/requests.xml?service_code=POT', ); + ok $res->is_success, 'valid request'; + + is_string $res->content, $xml, 'xml string ok POT' + or diag $res->content; + + $res = $endpoint->run_test_request( GET => '/requests.xml?service_code=BIN', ); + ok $res->is_success, 'valid request'; + is_string $res->content, <<CONTENT, 'xml string ok BIN (no requests)'; +<?xml version="1.0" encoding="utf-8"?> +<service_requests> +</service_requests> +CONTENT +}; + +subtest "GET Service Request" => sub { + my @req=(<<REQ0,<<REQ1); +<?xml version="1.0" encoding="utf-8"?> +<service_requests> + <request> + <address>22 Acacia Avenue</address> + <address_id></address_id> + <lat>0</lat> + <long>0</long> + <media_url></media_url> + <requested_datetime>2014-01-01T12:00:00Z</requested_datetime> + <service_code>POT</service_code> + <service_name>Pothole Repairs</service_name> + <service_request_id>0</service_request_id> + <status>open</status> + <updated_datetime>2014-01-01T12:00:00Z</updated_datetime> + <zipcode></zipcode> + </request> +</service_requests> +REQ0 +<?xml version="1.0" encoding="utf-8"?> +<service_requests> + <request> + <address>22 Acacia Avenue</address> + <address_id></address_id> + <lat>0</lat> + <long>0</long> + <media_url></media_url> + <requested_datetime>2014-02-01T12:00:00Z</requested_datetime> + <service_code>POT</service_code> + <service_name>Pothole Repairs</service_name> + <service_request_id>1</service_request_id> + <status>open</status> + <updated_datetime>2014-02-01T12:00:00Z</updated_datetime> + <zipcode></zipcode> + </request> +</service_requests> +REQ1 + + my $res = $endpoint->run_test_request( GET => '/requests/0.xml', ); + ok $res->is_success, 'valid request'; + + is_string $res->content, $req[0], 'Request 0 ok' + or diag $res->content;; + + $res = $endpoint->run_test_request( GET => '/requests/1.xml', ); + ok $res->is_success, 'valid request'; + + is_string $res->content, $req[1], 'Request 1 ok'; +}; + +restore_time(); +done_testing; diff --git a/t/open311/endpoint/Endpoint1.pm b/t/open311/endpoint/Endpoint1.pm new file mode 100644 index 000000000..ccd16c238 --- /dev/null +++ b/t/open311/endpoint/Endpoint1.pm @@ -0,0 +1,114 @@ +package t::open311::endpoint::Endpoint1; +use Web::Simple; +extends 'Open311::Endpoint'; +use Types::Standard ':all'; +use MooX::HandlesVia; + +use Open311::Endpoint::Service; +use t::open311::endpoint::ServiceType1; +use Open311::Endpoint::Service::Attribute; + +sub services { + return ( + t::open311::endpoint::ServiceType1->new( + service_code => 'POT', + service_name => 'Pothole Repairs', + description => 'Pothole Repairs Service', + attributes => [ + Open311::Endpoint::Service::Attribute->new( + code => 'depth', + required => 1, + datatype => 'number', + datatype_description => 'an integer', + description => 'depth of pothole, in centimetres', + ), + Open311::Endpoint::Service::Attribute->new( + code => 'shape', + required => 0, + datatype => 'singlevaluelist', + datatype_description => 'square | circle | triangle', + description => 'shape of the pothole', + values => { + square => 'Square', + circle => 'Circle', + triangle => 'Triangle', + }, + ), + ], + type => 'realtime', + keywords => [qw/ deep hole wow/], + group => 'highways', + ), + t::open311::endpoint::ServiceType1->new( + service_code => 'BIN', + service_name => 'Bin Enforcement', + description => 'Bin Enforcement Service', + attributes => [], + type => 'realtime', + keywords => [qw/ bin /], + group => 'sanitation', + ) + ); +} + +# FOR TESTING, we'll just maintain requests in a *global* array... +# obviously a real Service driver will use a DB or API call! +{ + our @SERVICE_REQUESTS; + has _requests => ( + is => 'ro', + isa => ArrayRef[ InstanceOf[ 'Open311::Endpoint::Service::Request' ] ], + default => sub { \@SERVICE_REQUESTS }, + handles_via => 'Array', + handles => { + next_request_id => 'count', + _add_request => 'push', + get_request => 'get', + get_requests => 'elements', + filter_requests => 'grep', + } + ); +} + +sub post_service_request { + my ($self, $service, $args) = @_; + + my $request = Open311::Endpoint::Service::Request->new( + + # NB: possible race condition between next_request_id and _add_request + # (this is fine for synchronous test-cases) + + service => $service, + service_request_id => $self->next_request_id, + status => 'open', + description => $args->{description}, + agency_responsible => '', + requested_datetime => DateTime->now(), + updated_datetime => DateTime->now(), + address => $args->{address_string} // '', + address_id => $args->{address_id} // '', + media_url => $args->{media_url} // '', + zipcode => $args->{zipcode} // '', + # NB: other info is passed in that would be stored by an Open311 + # endpoint, see Open311::Endpoint::Service::Request for full list, + # but we don't need to handle all of those in this test + ); + $self->_add_request( $request ); + + return ( $request ); +} + +sub get_service_requests { + my ($self, $args) = @_; + + my $service_code = $args->{service_code} or return $self->get_requests; + # we use ~~ as the service_code arg will be an arrayref like ['POT'] + return $self->filter_requests( sub { shift->service->service_code ~~ $service_code }); +} + +sub get_service_request { + my ($self, $service_request_id, $args) = @_; + return $self->get_request( $service_request_id ); +} + +1; diff --git a/t/open311/endpoint/ServiceType1.pm b/t/open311/endpoint/ServiceType1.pm new file mode 100644 index 000000000..e73b15b7d --- /dev/null +++ b/t/open311/endpoint/ServiceType1.pm @@ -0,0 +1,12 @@ +package t::open311::endpoint::ServiceType1; +use Moo; +extends 'Open311::Endpoint::Service'; +use DateTime; + +use Open311::Endpoint::Service::Request; + +has '+default_service_notice' => ( + default => 'This is a test service', +); + +1; diff --git a/t/open311/endpoint/schema.t b/t/open311/endpoint/schema.t new file mode 100644 index 000000000..b669ca4a5 --- /dev/null +++ b/t/open311/endpoint/schema.t @@ -0,0 +1,82 @@ +use strict; use warnings; + +use Test::More; +use Test::Exception; + +use Data::Rx; +use Open311::Endpoint; + +my $endpoint = Open311::Endpoint->new; +my $schema = $endpoint->rx; + +subtest 'comma tests' => sub { + + dies_ok { + my $comma = $schema->make_schema({ + type => '/open311/comma', + }); + } 'Construction dies on no contents'; + + dies_ok { + my $comma = $schema->make_schema({ + type => '/open311/comma', + contents => '/open311/status', + zirble => 'fleem', + }); + } 'Construction dies on extra arguments'; + + my $comma = $schema->make_schema({ + type => '/open311/comma', + contents => '/open311/status', + trim => 1, + }); + + ok ! $comma->check( undef ), 'Undef is not a valid string'; + ok ! $comma->check( [] ), 'Reference is not a valid string'; + + ok ! $comma->check( 'zibble' ), 'invalid string'; + ok ! $comma->check( 'open,zibble' ), 'an invalid element'; + + ok $comma->check( 'open' ), 'single value'; + ok $comma->check( 'open,closed' ), 'multiple values ok'; + ok $comma->check( 'open, closed ' ), 'spaces trimmed ok'; +}; + +subtest 'datetime tests' => sub { + + dies_ok { + my $comma = $schema->make_schema({ + type => '/open311/datetime', + zirble => 'fleem', + }); + } 'Construction dies on extra keys'; + + my $dt = $schema->make_schema({ + type => '/open311/datetime', + }); + + ok ! $dt->check( undef ), 'Undef is not a valid string'; + ok ! $dt->check( [] ), 'Reference is not a valid string'; + + ok ! $dt->check( '9th Feb 2012' ), 'invalid datetime format'; + + ok $dt->check( '1994-11-05T08:15:30-05:00' ), 'datetime format with offset'; + ok $dt->check( '1994-11-05T08:15:30+05:00' ), 'datetime format with positive'; + ok $dt->check( '1994-11-05T13:15:30Z' ), 'datetime format zulu'; +}; + +subtest 'identifier tests' => sub { + my $id = $schema->make_schema( '/open311/example/identifier' ); + + ok ! $id->check( undef ), 'Undef is not a valid string'; + ok ! $id->check( '' ), 'Empty string is not a valid identifier'; + ok ! $id->check( 'foo bar' ), 'String with spaces is not a valid identifier'; + + ok $id->check( 'foo' ), 'Ascii word string is a valid identifier'; + ok $id->check( 'foo_bar' ), 'Ascii word string is a valid identifier'; + ok $id->check( 'foo_123' ), 'Ascii word/num string is a valid identifier'; +}; + +done_testing; + +1; diff --git a/t/open311/endpoint/spark.t b/t/open311/endpoint/spark.t new file mode 100644 index 000000000..589f39baf --- /dev/null +++ b/t/open311/endpoint/spark.t @@ -0,0 +1,64 @@ +use strict; use warnings; + +use Test::More; + +use Open311::Endpoint; +use Data::Dumper; +use JSON; + +my $endpoint = Open311::Endpoint->new; +my $json = JSON->new; + +subtest "Spark test" => sub { + my $spark = $endpoint->spark; + my $struct = { + foo => { + service_requests => [ 1,2,3 ], + quxes => [ + { + values => [1,2], + }, + { + values => [3,4], + }, + ], + }, + }; + is_deeply $spark->process_for_json($struct), + { + service_requests => [ 1,2,3 ], + quxes => [ + { + values => [1,2], + }, + { + values => [3,4], + }, + ], + }; + + my $xml_struct = $spark->process_for_xml($struct); + is_deeply $xml_struct, + { + foo => { + service_requests => { request => [ 1,2,3 ] }, + quxes => { + quxe => [ + { + values => { + value => [1,2], + }, + }, + { + values => { + value => [3,4], + }, + }, + ] + }, + } + } + or warn Dumper($xml_struct); +}; + +done_testing; |