diff --git a/Changes b/Changes index c72f9ce..e098a25 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,11 @@ +0.200 + - refactor constants, stop use dualvars + - validate parameters on method check_publisher_restriction + - add method publisher_restrictions by vendor id + - add prefetch option to cache vendor ids when the consent string is range based. +It is 2x faster check consent on a prefetched vendor than a regular one if the consent is range based. +Without increase the Parsing time. + 0.100 - parse publisher tc section if available - add strict mode (disabled by default) to validate the consent string version diff --git a/MANIFEST b/MANIFEST index 90466be..d726902 100644 --- a/MANIFEST +++ b/MANIFEST @@ -13,10 +13,11 @@ lib/GDPR/IAB/TCFv2/PublisherTC.pm lib/GDPR/IAB/TCFv2/RangeSection.pm LICENSE Makefile.PL -MANIFEST This list of files +MANIFEST README.pod t/00-load.t t/01-parse.t -t/02-json.t -t/03-bugs.t +t/02-json-bitfield.t +t/03-json-range.t +t/90-bugs.t t/99-pod.t diff --git a/README.pod b/README.pod index 2b7df78..5896d94 100644 --- a/README.pod +++ b/README.pod @@ -26,7 +26,7 @@ GDPR::IAB::TCFv2 - Transparency & Consent String version 2 parser =head1 VERSION -Version 0.100 +Version 0.200 =head1 SYNOPSIS @@ -114,14 +114,33 @@ or date_format => '%Y%m%d', # yyymmdd }, strict => 1, + prefetch => 284, ); -Parse may receive an optional hash of parameters: C (boolean) and C (hashref with the following properties): +Parse may receive an optional hash with the following parameters: =over =item * +On C mode we will validate if the version of the consent string is the version 2 (or die with an exception). + +The C mode is disabled by default. + +=item * + +The C option receives one (as scalar) or more (as arrayref) vendor ids. + +This is useful when parsing a range based consent string, since we need to visit all ranges to find a particular id. + +=item * + +C is hashref with the following properties used to customize the json format: + +=over + +=item * + C changes the json encoding. By default we omit some false values such as C to create a compact json representation. With C we will present everything. See L for more details. @@ -150,9 +169,7 @@ except if the option C is true. =back -On C mode we will validate if the version of the consent string is the version 2 (or die with an exception). - -The C mode is disabled by default. +=back =head1 METHODS @@ -308,6 +325,13 @@ It true, there is a publisher restriction of certain type, for a given purpose i # with restriction type 0 'Purpose Flatly Not Allowed by Publisher' my $ok = $instance->check_publisher_restriction(1, 0, 284); +or + + my $ok = $instance->check_publisher_restriction( + purpose_id => 1, + restriction_type => 0, + vendor_id => 284); + Version 2.0 of the Framework introduced the ability for publishers to signal restrictions on how vendors may process personal data. Restrictions can be of two types: =over @@ -340,6 +364,10 @@ For the avoidance of doubt: In case a vendor has declared flexibility for a purpose and there is no legal basis restriction signal it must always apply the default legal basis under which the purpose was registered aside from being registered as flexible. That means if a vendor declared a purpose as legitimate interest and also declared that purpose as flexible it may not apply a "consent" signal without a legal basis restriction signal to require consent. +=head2 publisher_restrictions + +Similar to L but return an hashref of purpose => { restriction type => bool } for a given vendor. + =head2 publisher_tc If the consent string has a C section, we will decode this section as an instance of L. diff --git a/lib/GDPR/IAB/TCFv2.pm b/lib/GDPR/IAB/TCFv2.pm index e82d769..1367283 100644 --- a/lib/GDPR/IAB/TCFv2.pm +++ b/lib/GDPR/IAB/TCFv2.pm @@ -22,7 +22,7 @@ use GDPR::IAB::TCFv2::BitUtils qw { @@ -106,6 +106,14 @@ sub Parse { $options{json}->{date_format} ||= DATE_FORMAT_ISO_8601; $options{json}->{boolean_values} ||= [ _json_false(), _json_true() ]; + if ( exists $opts{prefetch} ) { + my $prefetch = $opts{prefetch}; + + $prefetch = [$prefetch] if ref($prefetch) ne ref( [] ); + + $options{prefetch} = $prefetch; + } + my $self = { core_data => $segments->{core_data}, publisher_tc_data => $segments->{publisher_tc}, @@ -325,10 +333,28 @@ sub vendor_legitimate_interest { } sub check_publisher_restriction { - my ( $self, $purpose_id, $restrict_type, $vendor ) = @_; + my $self = shift; + + my ( $purpose_id, $restriction_type, $vendor_id ); + + if ( scalar(@_) == 6 ) { + my (%opts) = @_; + + $purpose_id = $opts{purpose_id}; + $restriction_type = $opts{restriction_type}; + $vendor_id = $opts{vendor_id}; + } + + ( $purpose_id, $restriction_type, $vendor_id ) = @_; return $self->{publisher} - ->check_restriction( $purpose_id, $restrict_type, $vendor ); + ->check_restriction( $purpose_id, $restriction_type, $vendor_id ); +} + +sub publisher_restrictions { + my ( $self, $vendor_id ) = @_; + + return $self->{publisher}->restrictions($vendor_id); } sub publisher_tc { @@ -493,10 +519,10 @@ sub _parse_vendor_section { # parse vendor legitimate interest - my $pub_restrict_offset = + my $pub_restriction_offset = $self->_parse_vendor_legitimate_interests($legitimate_interest_offset); - return $pub_restrict_offset; + return $pub_restriction_offset; } sub _parse_vendor_consents { @@ -515,22 +541,22 @@ sub _parse_vendor_consents { sub _parse_vendor_legitimate_interests { my ( $self, $legitimate_interest_offset ) = @_; - my ( $vendor_legitimate_interests, $pub_restrict_offset ) = + my ( $vendor_legitimate_interests, $pub_restriction_offset ) = $self->_parse_bitfield_or_range( $legitimate_interest_offset, ); $self->{vendor_legitimate_interests} = $vendor_legitimate_interests; - return $pub_restrict_offset; + return $pub_restriction_offset; } sub _parse_publisher_section { - my ( $self, $pub_restrict_offset ) = @_; + my ( $self, $pub_restriction_offset ) = @_; # parse public restrictions - my $core_data = substr( $self->{core_data}, $pub_restrict_offset ); + my $core_data = substr( $self->{core_data}, $pub_restriction_offset ); my $core_data_size = length( $self->{core_data} ); my $publisher = GDPR::IAB::TCFv2::Publisher->Parse( @@ -652,7 +678,7 @@ GDPR::IAB::TCFv2 - Transparency & Consent String version 2 parser =head1 VERSION -Version 0.100 +Version 0.200 =head1 SYNOPSIS @@ -740,14 +766,33 @@ or date_format => '%Y%m%d', # yyymmdd }, strict => 1, + prefetch => 284, ); -Parse may receive an optional hash of parameters: C (boolean) and C (hashref with the following properties): +Parse may receive an optional hash with the following parameters: =over =item * +On C mode we will validate if the version of the consent string is the version 2 (or die with an exception). + +The C mode is disabled by default. + +=item * + +The C option receives one (as scalar) or more (as arrayref) vendor ids. + +This is useful when parsing a range based consent string, since we need to visit all ranges to find a particular id. + +=item * + +C is hashref with the following properties used to customize the json format: + +=over + +=item * + C changes the json encoding. By default we omit some false values such as C to create a compact json representation. With C we will present everything. See L for more details. @@ -776,9 +821,7 @@ except if the option C is true. =back -On C mode we will validate if the version of the consent string is the version 2 (or die with an exception). - -The C mode is disabled by default. +=back =head1 METHODS @@ -934,6 +977,13 @@ It true, there is a publisher restriction of certain type, for a given purpose i # with restriction type 0 'Purpose Flatly Not Allowed by Publisher' my $ok = $instance->check_publisher_restriction(1, 0, 284); +or + + my $ok = $instance->check_publisher_restriction( + purpose_id => 1, + restriction_type => 0, + vendor_id => 284); + Version 2.0 of the Framework introduced the ability for publishers to signal restrictions on how vendors may process personal data. Restrictions can be of two types: =over @@ -966,6 +1016,10 @@ For the avoidance of doubt: In case a vendor has declared flexibility for a purpose and there is no legal basis restriction signal it must always apply the default legal basis under which the purpose was registered aside from being registered as flexible. That means if a vendor declared a purpose as legitimate interest and also declared that purpose as flexible it may not apply a "consent" signal without a legal basis restriction signal to require consent. +=head2 publisher_restrictions + +Similar to L but return an hashref of purpose => { restriction type => bool } for a given vendor. + =head2 publisher_tc If the consent string has a C section, we will decode this section as an instance of L. diff --git a/lib/GDPR/IAB/TCFv2/Constants/Purpose.pm b/lib/GDPR/IAB/TCFv2/Constants/Purpose.pm index 4feb2ef..ec932da 100644 --- a/lib/GDPR/IAB/TCFv2/Constants/Purpose.pm +++ b/lib/GDPR/IAB/TCFv2/Constants/Purpose.pm @@ -1,30 +1,38 @@ package GDPR::IAB::TCFv2::Constants::Purpose; use strict; use warnings; -use Scalar::Util qw; require Exporter; use base qw; use constant { - InfoStorageAccess => - dualvar( 1, "Store and/or access information on a device" ), - BasicAdserving => dualvar( 2, "Use limited data to select advertising" ), - PersonalizationProfile => - dualvar( 3, "Create profiles for personalised advertising" ), + InfoStorageAccess => 1, + BasicAdserving => 2, + PersonalizationProfile => 3, + PersonalizationSelection => 4, + ContentProfile => 5, + ContentSelection => 6, + AdPerformance => 7, + ContentPerformance => 8, + MarketResearch => 9, + DevelopImprove => 10, + SelectContent => 11, +}; + +use constant PurposeDescription => { + InfoStorageAccess => "Store and/or access information on a device", + BasicAdserving => "Use limited data to select advertising", + PersonalizationProfile => "Create profiles for personalised advertising", PersonalizationSelection => - dualvar( 4, "Use profiles to select personalised advertising" ), - ContentProfile => dualvar( 5, "Create profiles to personalise content" ), - ContentSelection => - dualvar( 6, "Use profiles to select personalised content" ), - AdPerformance => dualvar( 7, "Measure advertising performance" ), - ContentPerformance => dualvar( 8, "Measure content performance" ), - MarketResearch => dualvar( - 9, - "Understand audiences through statistics or combinations of data from different sources" - ), - DevelopImprove => dualvar( 10, "Develop and improve services" ), - SelectContent => dualvar( 11, "Use limited data to select content" ), + "Use profiles to select personalised advertising", + ContentProfile => "Create profiles to personalise content", + ContentSelection => "Use profiles to select personalised content", + AdPerformance => "Measure advertising performance", + ContentPerformance => "Measure content performance", + MarketResearch => + "Understand audiences through statistics or combinations of data from different sources", + DevelopImprove => "Develop and improve services", + SelectContent => "Use limited data to select content", }; our @EXPORT_OK = qw< @@ -39,6 +47,7 @@ our @EXPORT_OK = qw< MarketResearch DevelopImprove SelectContent + PurposeDescription >; our %EXPORT_TAGS = ( all => \@EXPORT_OK ); @@ -58,15 +67,15 @@ GDPR::IAB::TCFv2::Constants::Purpose - TCF v2.2 purposes use feature 'say'; - say "Purpose id is ". (0+InfoStorageAccess), ", and it means " . InfoStorageAccess; + say "Purpose id is ", InfoStorageAccess , ", and it means " . PurposeDescription->{InfoStorageAccess}; # Output: # Purpose id is 1, and it means Store and/or access information on a device =head1 CONSTANTS -All constants are C (see L). +All constants are integers. -Returns a scalar that has the C in a numeric context and the C in a string context. +To find the description of a given id you can use the hashref L =head2 InfoStorageAccess @@ -289,3 +298,7 @@ A travel magazine has published an article on its website about the new online c A sports news mobile app has started a new section of articles covering the most recent football games. Each article includes videos hosted by a separate streaming platform showcasing the highlights of each match. If you fast-forward a video, this information may be used to select a shorter video to play next. =back + +=head2 PurposeDescription + +Returns a hashref with a mapping between all purpose ids and their description. diff --git a/lib/GDPR/IAB/TCFv2/Constants/RestrictionType.pm b/lib/GDPR/IAB/TCFv2/Constants/RestrictionType.pm index 4c6562f..e43bacd 100644 --- a/lib/GDPR/IAB/TCFv2/Constants/RestrictionType.pm +++ b/lib/GDPR/IAB/TCFv2/Constants/RestrictionType.pm @@ -1,21 +1,27 @@ package GDPR::IAB::TCFv2::Constants::RestrictionType; use strict; use warnings; -use Scalar::Util qw; require Exporter; use base qw; use constant { - NotAllowed => dualvar( 0, "Purpose Flatly Not Allowed by Publisher" ), - RequireConsent => dualvar( 1, "Require Consent" ), - RequireLegitimateInterest => dualvar( 2, "Require Legitimate Interest" ), + NotAllowed => 0, + RequireConsent => 1, + RequireLegitimateInterest => 2, +}; + +use constant RestrictionTypeDescription => { + NotAllowed => "Purpose Flatly Not Allowed by Publisher", + RequireConsent =>, "Require Consent", + RequireLegitimateInterest => "Require Legitimate Interest", }; our @EXPORT_OK = qw< NotAllowed RequireConsent RequireLegitimateInterest + RestrictionTypeDescription >; our %EXPORT_TAGS = ( all => \@EXPORT_OK ); @@ -37,15 +43,15 @@ GDPR::IAB::TCFv2::Constants::RestrictionType - TCF v2.2 publisher restriction ty use feature 'say'; - say "Restriction type id is ". (0+NotAllowed), ", and it means " . NotAllowed; + say "Restriction type id is ", NotAllowed, ", and it means " , RestrictionTypeDescription->{NotAllowed}; # Output: # Restriction type id is 0, and it means Purpose Flatly Not Allowed by Publisher =head1 CONSTANTS -All constants are C (see L). +All constants are integers. -Returns a scalar that has the C in a numeric context and the C in a string context. +To find the description of a given id you can use the hashref L =head2 NotAllowed @@ -59,6 +65,10 @@ Restriction type id 1: Require Consent (if Vendor has declared the Purpose IDs l Restriction type id 2: Require Legitimate Interest (if Vendor has declared the Purpose IDs legal basis as Consent and flexible) +=head2 RestrictionTypeDescription + +Returns a hashref with a mapping between all restriction types and their description. + =head1 NOTE Vendors must always respect a 0 (Not Allowed) regardless of whether or not they have not declared that Purpose to be "flexible". Values 1 and 2 are in accordance with a vendor's declared flexibility. Eg. if a vendor has Purpose 2 declared as Legitimate Interest but also declares that Purpose as flexible and this field is set to 1, they must then check for the "consent" signal in the VendorConsents section to make a determination on whether they have the legal basis for processing user personal data under that Purpose. diff --git a/lib/GDPR/IAB/TCFv2/Constants/SpecialFeature.pm b/lib/GDPR/IAB/TCFv2/Constants/SpecialFeature.pm index 1e2772a..f989ef5 100644 --- a/lib/GDPR/IAB/TCFv2/Constants/SpecialFeature.pm +++ b/lib/GDPR/IAB/TCFv2/Constants/SpecialFeature.pm @@ -1,20 +1,24 @@ package GDPR::IAB::TCFv2::Constants::SpecialFeature; use strict; use warnings; -use Scalar::Util qw; require Exporter; use base qw; use constant { - Geolocation => dualvar( 1, "Use precise geolocation data" ), - DeviceScan => - dualvar( 2, "Actively scan device characteristics for identification" ) + Geolocation => 1, + DeviceScan => 2 +}; + +use constant SpecialFeatureDescription => { + Geolocation => "Use precise geolocation data", + DeviceScan => "Actively scan device characteristics for identification" }; our @EXPORT_OK = qw< Geolocation DeviceScan + SpecialFeatureDescription >; our %EXPORT_TAGS = ( all => \@EXPORT_OK ); @@ -36,15 +40,15 @@ GDPR::IAB::TCFv2::Constants::SpecialFeature - TCF v2.2 special features use feature 'say'; - say "Special feature id is ". (0+Geolocation), ", and it means " . Geolocation; + say "Special feature id is ", Geolocation, ", and it means " , SpecialFeatureDescription->{Geolocation}; # Output: # Special feature id is 1, and it means Use precise geolocation data =head1 CONSTANTS -All constants are C (see L). +All constants are integers. -Returns a scalar that has the C in a numeric context and the C in a string context. +To find the description of a given id you can use the hashref L. =head2 Geolocation @@ -58,3 +62,7 @@ Special feature id 2: Actively scan device characteristics for identification With your acceptance, certain characteristics specific to your device might be requested and used to distinguish it from other devices (such as the installed fonts or plugins, the resolution of your screen) in support of the purposes explained in this notice. "description": + +=head2 SpecialFeatureDescription + +Returns a hashref with a mapping between all restriction types and their description. diff --git a/lib/GDPR/IAB/TCFv2/Publisher.pm b/lib/GDPR/IAB/TCFv2/Publisher.pm index 136f0e1..14af9b0 100644 --- a/lib/GDPR/IAB/TCFv2/Publisher.pm +++ b/lib/GDPR/IAB/TCFv2/Publisher.pm @@ -51,10 +51,16 @@ sub Parse { } sub check_restriction { - my ( $self, $purpose_id, $restrict_type, $vendor ) = @_; + my ( $self, $purpose_id, $restriction_type, $vendor_id ) = @_; return $self->{restrictions} - ->check_restriction( $purpose_id, $restrict_type, $vendor ); + ->check_restriction( $purpose_id, $restriction_type, $vendor_id ); +} + +sub restrictions { + my ( $self, $vendor_id ) = @_; + + return $self->{restrictions}->restrictions($vendor_id); } sub publisher_tc { @@ -95,7 +101,7 @@ Combines the creation of L and L { json => ... }, ); - say "there is publisher restriction on purpose id 1, type 0 on vendor 284" + say "there is publisher restriction on purpose id 1, type 0 on vendor_id 284" if $publisher->check_restriction(1, 0, 284); =head1 CONSTRUCTOR @@ -126,12 +132,21 @@ Key C is the L options (includes the C field to =head2 check_restriction -Return true for a given combination of purpose id, restriction type and vendor +Return true for a given combination of purpose id, restriction type and vendor_id my $purpose_id = 1; my $restriction_type = 0; - my $vendor = 284; - $ok = $range->check_restriction($purpose_id, $restriction_type, $vendor); + my $vendor_id = 284; + $ok = $publisher->check_restriction($purpose_id, $restriction_type, $vendor_id); + +=head2 restrictions + +Return a hashref of purpose => { restriction type => bool } for a given vendor id. + +Example, by parsing the consent C we can generate this. + + my $restrictions = $publisher->restrictions(32); + # returns { 7 => { 1 => 1 } } =head2 publisher_tc @@ -155,7 +170,7 @@ Returns a hashref with the following format: # 0 - Not Allowed # 1 - Require Consent # 2 - Require Legitimate Interest - '[vendor id]' => 1, + '[vendor_id id]' => 1, }, } } diff --git a/lib/GDPR/IAB/TCFv2/PublisherRestrictions.pm b/lib/GDPR/IAB/TCFv2/PublisherRestrictions.pm index f77826f..fbdb05f 100644 --- a/lib/GDPR/IAB/TCFv2/PublisherRestrictions.pm +++ b/lib/GDPR/IAB/TCFv2/PublisherRestrictions.pm @@ -66,14 +66,44 @@ sub Parse { return $self; } +sub restrictions { + my ( $self, $vendor_id ) = @_; + + my %restrictions; + + foreach my $purpose_id ( keys %{ $self->{restrictions} } ) { + foreach my $restriction_type ( + keys %{ $self->{restrictions}->{$purpose_id} } ) + { + if ( $self->{restrictions}->{$purpose_id}->{$restriction_type} + ->contains($vendor_id) ) + { + $restrictions{$purpose_id} ||= {}; + $restrictions{$purpose_id}->{$restriction_type} = 1; + } + } + } + + return \%restrictions; +} + sub check_restriction { - my ( $self, $purpose_id, $restrict_type, $vendor ) = @_; + my $self = shift; + + my $nargs = scalar(@_); + + croak "missing arguments: purpose id, restriction type and vendor id" + if $nargs == 0; + croak "missing arguments: restriction type and vendor id" if $nargs == 1; + croak "missing argument: vendor id" if $nargs == 2; + + my ( $purpose_id, $restriction_type, $vendor_id ) = @_; return 0 - unless exists $self->{restrictions}->{$purpose_id}->{$restrict_type}; + unless exists $self->{restrictions}->{$purpose_id}->{$restriction_type}; - return $self->{restrictions}->{$purpose_id}->{$restrict_type} - ->contains($vendor); + return $self->{restrictions}->{$purpose_id}->{$restriction_type} + ->contains($vendor_id); } sub TO_JSON { @@ -86,11 +116,11 @@ sub TO_JSON { my %purpose_restrictions; - foreach my $restrict_type ( keys %{$restriction_map} ) { - my $vendors = $restriction_map->{$restrict_type}->all; + foreach my $restriction_type ( keys %{$restriction_map} ) { + my $vendors = $restriction_map->{$restriction_type}->all; foreach my $vendor ( @{$vendors} ) { - $purpose_restrictions{$vendor} = int($restrict_type); + $purpose_restrictions{$vendor} = int($restriction_type); } } @@ -146,8 +176,17 @@ Return true for a given combination of purpose id, restriction type and vendor my $purpose_id = 1; my $restriction_type = 0; - my $vendor = 284; - $ok = $range->check_restriction($purpose_id, $restriction_type, $vendor); + my $vendor_id = 284; + my $ok = $object->check_restriction($purpose_id, $restriction_type, $vendor_id); + +=head2 restrictions + +Return a hashref of purpose => { restriction type => bool } for a given vendor id. + +Example, by parsing the consent C we can generate this. + + my $restrictions = $object->restrictions(32); + # returns { 7 => { 1 => 1 } } =head2 TO_JSON diff --git a/lib/GDPR/IAB/TCFv2/RangeSection.pm b/lib/GDPR/IAB/TCFv2/RangeSection.pm index 3203df5..3aedcf8 100644 --- a/lib/GDPR/IAB/TCFv2/RangeSection.pm +++ b/lib/GDPR/IAB/TCFv2/RangeSection.pm @@ -29,41 +29,58 @@ sub Parse { "a BitField for vendor consent strings using RangeSections require at least 31 bytes. Got $data_size" if $data_size < 31; - my ( $num_entries, $next_offset ) = get_uint12( $data, $offset ); + my %prefetch; + my %cache; - my @ranges; + if ( exists $options->{prefetch} ) { + my $vendor_ids = $options->{prefetch}; - foreach my $i ( 1 .. $num_entries ) { - my $range; - ( $range, $next_offset ) = _parse_range( - $data, - $data_size, - $next_offset, - $max_id, - $options, - ); - - push @ranges, $range; + foreach my $vendor_id ( @{$vendor_ids} ) { + $prefetch{$vendor_id} = 1; + $cache{$vendor_id} = 0; + } } my $self = { - ranges => \@ranges, + ranges => [], + cache => \%cache, max_id => $max_id, options => $options, }; bless $self, $klass; + my $next_offset = $self->_parse( $data, $data_size, $offset, \%prefetch ); + return ( $self, $next_offset ); } +sub _parse { + my ( $self, $data, $data_size, $offset, $prefetch ) = @_; + + my ( $num_entries, $next_offset ) = get_uint12( $data, $offset ); + + foreach my $i ( 1 .. $num_entries ) { + $next_offset = $self->_parse_range( + $data, + $data_size, + $next_offset, + $prefetch, + ); + } + + return $next_offset; +} + sub _parse_range { - my ( $data, $data_size, $offset, $max_id, $options ) = @_; + my ( $self, $data, $data_size, $offset, $prefetch ) = @_; croak "bit $offset was suppose to start a new range entry, but the consent string was only $data_size bytes long" if $data_size <= $offset / 8; + my $max_id = $self->{max_id}; + # If the first bit is set, it's a Range of IDs my ( $is_range, $next_offset ) = is_set $data, $offset; if ($is_range) { @@ -82,8 +99,14 @@ sub _parse_range { croak "start $start can't be bigger than end $end" if $start > $end; - return [ $start, $end ], - $next_offset; + push @{ $self->{ranges} }, [ $start, $end ]; + + foreach my $vendor_id ( keys %{$prefetch} ) { + $self->{cache}->{$vendor_id} = delete( $prefetch->{$vendor_id} ) + if $start <= $vendor_id && $vendor_id <= $end; + } + + return $next_offset; } my $vendor_id; @@ -94,7 +117,12 @@ sub _parse_range { "bit $offset range entry exclusion vendor $vendor_id, but only vendors [1, $max_id] are valid" if 1 > $vendor_id || $vendor_id > $max_id; - return [ $vendor_id, $vendor_id ], $next_offset; + push @{ $self->{ranges} }, [ $vendor_id, $vendor_id ]; + + $self->{cache}->{$vendor_id} = delete( $prefetch->{$vendor_id} ) + if exists $prefetch->{$vendor_id}; + + return $next_offset; } sub max_id { @@ -109,6 +137,8 @@ sub contains { croak "invalid vendor id $id: must be positive integer bigger than 0" if $id < 1; + return $self->{cache}->{$id} if exists $self->{cache}->{$id}; + return if $id > $self->{max_id}; foreach my $range ( @{ $self->{ranges} } ) { @@ -132,15 +162,7 @@ sub all { sub TO_JSON { my $self = shift; - if ( !!$self->{options}->{json}->{compact} ) { - my @vendors; - - foreach my $range ( @{ $self->{ranges} } ) { - push @vendors, $range->[0] .. $range->[1]; - } - - return \@vendors; - } + return $self->all if !!$self->{options}->{json}->{compact}; my ( $false, $true ) = @{ $self->{options}->{json}->{boolean_values} }; @@ -174,13 +196,14 @@ GDPR::IAB::TCFv2::RangeSection - Transparency & Consent String version 2 range s data_size => length($data), offset => 230, # offset for vendor ranges max_id => $max_id_consent, + prefetch => 284, # will cache the result of vendor id 284 ); say "range section contains id 284" if $range_section->contains(284); =head1 CONSTRUCTOR -Constructor C receives an hash of 5 parameters: +Constructor C receives an hash parameters: =over @@ -204,6 +227,10 @@ Key C is the max id (used to validate the ranges if all data is between Key C is the L options (includes the C field to modify the L method output. +=item * + +Key C is an optional arrayref of vendor ids to populate the result as cache. + =back Will die if any parameter is missing. diff --git a/t/00-load.t b/t/00-load.t index 999318c..d578a3f 100644 --- a/t/00-load.t +++ b/t/00-load.t @@ -42,9 +42,9 @@ subtest "check interfaces" => sub { @role_decoder_methods, qw; can_ok 'GDPR::IAB::TCFv2::PublisherRestrictions', @role_base_methods, - qw; + qw; can_ok 'GDPR::IAB::TCFv2::Publisher', @role_base_methods, - qw; + qw; can_ok 'GDPR::IAB::TCFv2::PublisherTC', @role_base_methods, qw sub { - my $consent; +subtest "bitfield" => sub { + subtest "valid tcf v2 consent string using bitfield" => sub { + my $consent; - my $tc_string = - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA'; - lives_ok { - $consent = GDPR::IAB::TCFv2->Parse($tc_string); - } - 'should not throw exception'; + my $tc_string = + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA'; + lives_ok { + $consent = GDPR::IAB::TCFv2->Parse($tc_string); + } + 'should not throw exception'; - isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; + isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; - is $consent->tc_string, $tc_string, 'should return the original tc string'; + is $consent->tc_string, $tc_string, + 'should return the original tc string'; - is "${consent}", $tc_string, - 'should return the original tc string in string context'; + is "${consent}", $tc_string, + 'should return the original tc string in string context'; - is $consent->version, 2, 'should return version 2'; + is $consent->version, 2, 'should return version 2'; - is $consent->created, 1228644257, - 'should return the creation epoch 07/12/2008'; + is $consent->created, 1228644257, + 'should return the creation epoch 07/12/2008'; - { - my ( $seconds, $nanoseconds ) = $consent->created; - is $seconds, 1228644257, - 'should return the creation epoch 07/12/2008 on list context'; - is $nanoseconds, 700000000, - 'should return the 700000000 nanoseconds of epoch on list context'; - } + { + my ( $seconds, $nanoseconds ) = $consent->created; + is $seconds, 1228644257, + 'should return the creation epoch 07/12/2008 on list context'; + is $nanoseconds, 700000000, + 'should return the 700000000 nanoseconds of epoch on list context'; + } - is $consent->last_updated, 1326215413, - 'should return the last update epoch 10/01/2012'; + is $consent->last_updated, 1326215413, + 'should return the last update epoch 10/01/2012'; - { - my ( $seconds, $nanoseconds ) = $consent->last_updated; - is $seconds, 1326215413, - 'should return the last updated epoch 07/12/2008 on list context'; - is $nanoseconds, 400000000, - 'should return the 400000000 nanoseconds of epoch on list context'; - } + { + my ( $seconds, $nanoseconds ) = $consent->last_updated; + is $seconds, 1326215413, + 'should return the last updated epoch 07/12/2008 on list context'; + is $nanoseconds, 400000000, + 'should return the 400000000 nanoseconds of epoch on list context'; + } - is $consent->cmp_id, 21, 'should return the cmp id 21'; + is $consent->cmp_id, 21, 'should return the cmp id 21'; - is $consent->cmp_version, 7, 'should return the cmp version 7'; + is $consent->cmp_version, 7, 'should return the cmp version 7'; - is $consent->consent_screen, 2, 'should return the consent screen 2'; + is $consent->consent_screen, 2, 'should return the consent screen 2'; - is $consent->consent_language, "EN", - 'should return the consent language "EN"'; + is $consent->consent_language, "EN", + 'should return the consent language "EN"'; - is $consent->vendor_list_version, 23, - 'should return the vendor list version 23'; + is $consent->vendor_list_version, 23, + 'should return the vendor list version 23'; - is $consent->policy_version, 2, - 'should return the policy version 2'; + is $consent->policy_version, 2, + 'should return the policy version 2'; - ok $consent->is_service_specific, - 'should return true for service specific'; + ok $consent->is_service_specific, + 'should return true for service specific'; - ok !$consent->use_non_standard_stacks, - 'should return false for use non standard stacks'; + ok !$consent->use_non_standard_stacks, + 'should return false for use non standard stacks'; - ok !$consent->purpose_one_treatment, - 'should return false for use purpose one treatment'; + ok !$consent->purpose_one_treatment, + 'should return false for use purpose one treatment'; - is $consent->publisher_country_code, "KM", - 'should return the publisher country code "KM"'; + is $consent->publisher_country_code, "KM", + 'should return the publisher country code "KM"'; - is $consent->max_vendor_id_consent, 115, "max vendor id consent is 115"; + is $consent->max_vendor_id_consent, 115, + "max vendor id consent is 115"; - is $consent->max_vendor_id_legitimate_interest, 113, - "max vendor id legitimate interest is 113"; + is $consent->max_vendor_id_legitimate_interest, 113, + "max vendor id legitimate interest is 113"; - subtest "check purpose consent ids" => sub { - plan tests => 24; + subtest "check purpose consent ids" => sub { + plan tests => 24; - my %allowed_purposes = map { $_ => 1 } ( 1, 3, 9, 10 ); + my %allowed_purposes = map { $_ => 1 } ( 1, 3, 9, 10 ); - foreach my $id ( 1 .. 24 ) { - is !!$consent->is_purpose_consent_allowed($id), - !!$allowed_purposes{$id}, - "checking purpose id $id for consent"; - } - }; + foreach my $id ( 1 .. 24 ) { + is !!$consent->is_purpose_consent_allowed($id), + !!$allowed_purposes{$id}, + "checking purpose id $id for consent"; + } + }; - subtest "check purpose legitimate interest ids" => sub { - plan tests => 24; + subtest "check purpose legitimate interest ids" => sub { + plan tests => 24; - my %allowed_purposes = map { $_ => 1 } ( 3, 4, 5, 8, 9, 10 ); + my %allowed_purposes = map { $_ => 1 } ( 3, 4, 5, 8, 9, 10 ); - foreach my $id ( 1 .. 24 ) { - is !!$consent->is_purpose_legitimate_interest_allowed($id), - !!$allowed_purposes{$id}, - "checking purpose id $id for legitimate interest"; - } - }; + foreach my $id ( 1 .. 24 ) { + is !!$consent->is_purpose_legitimate_interest_allowed($id), + !!$allowed_purposes{$id}, + "checking purpose id $id for legitimate interest"; + } + }; - subtest "check special feature opt in" => sub { - plan tests => 12; + subtest "check special feature opt in" => sub { + plan tests => 12; - my %special_feature_opt_in = ( - 2 => 1, - ); + my %special_feature_opt_in = ( + 2 => 1, + ); - foreach my $id ( 1 .. 12 ) { - is !!$consent->is_special_feature_opt_in($id), - !!$special_feature_opt_in{$id}, - "checking special feature id $id opt in"; - } - }; + foreach my $id ( 1 .. 12 ) { + is !!$consent->is_special_feature_opt_in($id), + !!$special_feature_opt_in{$id}, + "checking special feature id $id opt in"; + } + }; - subtest "check vendor consent ids" => sub { - plan tests => 120; - - my %allowed_vendors = - map { $_ => 1 } ( - 2, 3, 6, 7, 8, 10, 12, 13, 14, 15, 16, 21, 25, 27, 30, 31, 34, 35, - 37, 38, 39, 42, 43, 49, 52, 54, 55, 56, 57, 59, 60, 63, 64, 65, - 66, 67, 68, 69, 73, 74, 76, 78, 83, 86, 87, 89, 90, 92, 96, 99, - 100, 106, 109, 110, 114, 115 - ); - - foreach my $id ( 1 .. 120 ) { - is !!$consent->vendor_consent($id), - !!$allowed_vendors{$id}, - "checking vendor id $id for consent"; - } - }; + subtest "check vendor consent ids" => sub { + plan tests => 120; + + my %allowed_vendors = + map { $_ => 1 } ( + 2, 3, 6, 7, 8, 10, 12, 13, 14, 15, 16, 21, 25, 27, 30, 31, 34, + 35, + 37, 38, 39, 42, 43, 49, 52, 54, 55, 56, 57, 59, 60, 63, 64, 65, + 66, 67, 68, 69, 73, 74, 76, 78, 83, 86, 87, 89, 90, 92, 96, 99, + 100, 106, 109, 110, 114, 115 + ); + + foreach my $id ( 1 .. 120 ) { + is !!$consent->vendor_consent($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for consent"; + } + }; - subtest "check vendor legitimate interest ids" => sub { - plan tests => 120; + subtest "check vendor legitimate interest ids" => sub { + plan tests => 120; - my %allowed_vendors = - map { $_ => 1 } ( 1, 9, 26, 27, 30, 36, 37, 43, 86, 97, 110, 113 ); + my %allowed_vendors = + map { $_ => 1 } + ( 1, 9, 26, 27, 30, 36, 37, 43, 86, 97, 110, 113 ); - foreach my $id ( 1 .. 120 ) { - is !!$consent->vendor_legitimate_interest($id), - !!$allowed_vendors{$id}, - "checking vendor id $id for legitimate interest"; - } + foreach my $id ( 1 .. 120 ) { + is !!$consent->vendor_legitimate_interest($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for legitimate interest"; + } + }; + + ok !$consent->check_publisher_restriction( 1, 0, 284 ), + "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher' when called with positional parameters"; + + ok !$consent->check_publisher_restriction( + purpose_id => 1, + restriction_type => 0, vendor_id => 284 + ), + "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher' when called with named parameters"; + + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; + + my $publisher_tc = $consent->publisher_tc; + + ok !defined($publisher_tc), "should not return publisher_tc"; + + done_testing; }; - ok !$consent->check_publisher_restriction( 1, 0, 284 ), - "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; - my $publisher_tc = $consent->publisher_tc; + subtest + "valid tcf v2 consent string using bitfield with publisher TC section" + => sub { - ok !defined($publisher_tc), "should not return publisher_tc"; + subtest "without custom purposes" => sub { + my $consent; - done_testing; -}; + my $tc_string = + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA.argAC0gAAAAAAAAAAAA'; + lives_ok { + $consent = GDPR::IAB::TCFv2->Parse($tc_string); + } + 'should not throw exception'; + isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; -subtest - "valid tcf v2 consent string using bitfield with publisher TC section" => - sub { + is $consent->tc_string, $tc_string, + 'should return the original tc string'; - subtest "without custom purposes" => sub { - my $consent; + is "${consent}", $tc_string, + 'should return the original tc string in string context'; - my $tc_string = - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA.argAC0gAAAAAAAAAAAA'; - lives_ok { - $consent = GDPR::IAB::TCFv2->Parse($tc_string); - } - 'should not throw exception'; + is $consent->version, 2, 'should return version 2'; - isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; + my $publisher_tc = $consent->publisher_tc; - is $consent->tc_string, $tc_string, - 'should return the original tc string'; + ok defined($publisher_tc), "should return publisher_tc"; - is "${consent}", $tc_string, - 'should return the original tc string in string context'; + is $publisher_tc->num_custom_purposes, 0, + "should not have any custom purposes"; - is $consent->version, 2, 'should return version 2'; + subtest "check publisher purpose consent ids" => sub { + plan tests => 24; - my $publisher_tc = $consent->publisher_tc; + my %allowed_purposes = map { $_ => 1 } ( 2, 4, 6, 8, 9, 10 ); - ok defined($publisher_tc), "should return publisher_tc"; + foreach my $id ( 1 .. 24 ) { + is !!$publisher_tc->is_purpose_consent_allowed($id), + !!$allowed_purposes{$id}, + "checking publisher purpose id $id for consent"; + } + }; - is $publisher_tc->num_custom_purposes, 0, - "should not have any custom purposes"; + subtest "check publisher purpose legitimate interest ids" => sub { + plan tests => 24; - subtest "check publisher purpose consent ids" => sub { - plan tests => 24; + my %allowed_purposes = map { $_ => 1 } ( 2, 4, 5, 7, 10 ); - my %allowed_purposes = map { $_ => 1 } ( 2, 4, 6, 8, 9, 10 ); + foreach my $id ( 1 .. 24 ) { + is !!$publisher_tc + ->is_purpose_legitimate_interest_allowed($id), + !!$allowed_purposes{$id}, + "checking publisher purpose id $id for legitimate interest"; + } + }; - foreach my $id ( 1 .. 24 ) { - is !!$publisher_tc->is_purpose_consent_allowed($id), - !!$allowed_purposes{$id}, - "checking publisher purpose id $id for consent"; - } + done_testing; }; - subtest "check publisher purpose legitimate interest ids" => sub { - plan tests => 24; - - my %allowed_purposes = map { $_ => 1 } ( 2, 4, 5, 7, 10 ); + subtest "with custom purposes" => sub { + my $consent; - foreach my $id ( 1 .. 24 ) { - is !!$publisher_tc->is_purpose_legitimate_interest_allowed( - $id), - !!$allowed_purposes{$id}, - "checking publisher purpose id $id for legitimate interest"; + my $tc_string = + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA.YAAAAAAAAXA'; + lives_ok { + $consent = GDPR::IAB::TCFv2->Parse($tc_string); } + 'should not throw exception'; + + isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; + + is $consent->tc_string, $tc_string, + 'should return the original tc string'; + + is "${consent}", $tc_string, + 'should return the original tc string in string context'; + + is $consent->version, 2, 'should return version 2'; + + my $publisher_tc = $consent->publisher_tc; + + ok defined($publisher_tc), "should return publisher_tc"; + + is $publisher_tc->num_custom_purposes, 2, + "should have 2 custom purposes"; + + subtest "check publisher purpose consent ids" => sub { + plan tests => 24; + + my %allowed_purposes; + + foreach my $id ( 1 .. 24 ) { + is !!$publisher_tc->is_purpose_consent_allowed($id), + !!$allowed_purposes{$id}, + "checking publisher purpose id $id for consent"; + } + }; + + subtest "check publisher purpose legitimate interest ids" => sub { + plan tests => 24; + + my %allowed_purposes; + + foreach my $id ( 1 .. 24 ) { + is !!$publisher_tc + ->is_purpose_legitimate_interest_allowed($id), + !!$allowed_purposes{$id}, + "checking publisher purpose id $id for legitimate interest"; + } + }; + + + subtest "check publisher custom purpose consent ids" => sub { + plan tests => 2; + + ok $publisher_tc->is_custom_purpose_consent_allowed(1), + "should have custom purpose 1 allowed"; + ok $publisher_tc->is_custom_purpose_consent_allowed(2), + "should have custom purpose 2 allowed"; + }; + + subtest + "check publisher custom purpose legitimate interest ids" => sub { + plan tests => 2; + + ok $publisher_tc + ->is_custom_purpose_legitimate_interest_allowed(1), + "should have custom purpose 1 allowed"; + ok !$publisher_tc + ->is_custom_purpose_legitimate_interest_allowed(2), + "should not have custom purpose 2 allowed"; + }; + + done_testing; }; done_testing; - }; + }; + done_testing; + +}; - subtest "with custom purposes" => sub { +subtest "range" => sub { + subtest "valid tcf v2 consent string using range" => sub { my $consent; my $tc_string = - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA.YAAAAAAAAXA'; + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA'; lives_ok { $consent = GDPR::IAB::TCFv2->Parse($tc_string); } @@ -238,201 +337,190 @@ subtest is $consent->version, 2, 'should return version 2'; - my $publisher_tc = $consent->publisher_tc; - - ok defined($publisher_tc), "should return publisher_tc"; - - is $publisher_tc->num_custom_purposes, 2, - "should have 2 custom purposes"; - - subtest "check publisher purpose consent ids" => sub { - plan tests => 24; - - my %allowed_purposes; - - foreach my $id ( 1 .. 24 ) { - is !!$publisher_tc->is_purpose_consent_allowed($id), - !!$allowed_purposes{$id}, - "checking publisher purpose id $id for consent"; - } - }; - - subtest "check publisher purpose legitimate interest ids" => sub { - plan tests => 24; - - my %allowed_purposes; + is $consent->created, 1587946020, + 'should return the creation epoch 27/04/2020 on scalar context'; - foreach my $id ( 1 .. 24 ) { - is !!$publisher_tc->is_purpose_legitimate_interest_allowed( - $id), - !!$allowed_purposes{$id}, - "checking publisher purpose id $id for legitimate interest"; - } - }; + { + my ( $seconds, $nanoseconds ) = $consent->created; + is $seconds, 1587946020, + 'should return the creation epoch 27/04/2020 on list context'; + is $nanoseconds, 0, + 'should return the 0 nanoseconds of epoch on list context'; + } + is $consent->last_updated, 1587946020, + 'should return the last update epoch 27/04/2020'; - subtest "check publisher custom purpose consent ids" => sub { - plan tests => 2; + { + my ( $seconds, $nanoseconds ) = $consent->last_updated; + is $seconds, 1587946020, + 'should return the last update epoch 27/04/2020 on list context'; + is $nanoseconds, 0, + 'should return the 0 nanoseconds of epoch on list context'; + } - ok $publisher_tc->is_custom_purpose_consent_allowed(1), - "should have custom purpose 1 allowed"; - ok $publisher_tc->is_custom_purpose_consent_allowed(2), - "should have custom purpose 2 allowed"; - }; + is $consent->cmp_id, 3, 'should return the cmp id 3'; - subtest "check publisher custom purpose legitimate interest ids" => - sub { - plan tests => 2; + is $consent->cmp_version, 2, 'should return the cmp version 2'; - ok $publisher_tc->is_custom_purpose_legitimate_interest_allowed(1), - "should have custom purpose 1 allowed"; - ok !$publisher_tc->is_custom_purpose_legitimate_interest_allowed( - 2), "should not have custom purpose 2 allowed"; - }; + is $consent->consent_screen, 7, 'should return the consent screen 7'; - done_testing; - }; + is $consent->consent_language, "EN", + 'should return the consent language "EN"'; - done_testing; - }; + is $consent->vendor_list_version, 48, + 'should return the vendor list version 23'; + is $consent->policy_version, 2, + 'should return the policy version 2'; -subtest "valid tcf v2 consent string using range" => sub { - my $consent; + ok !$consent->is_service_specific, + 'should return true for service specific'; - my $tc_string = - 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA'; - lives_ok { - $consent = GDPR::IAB::TCFv2->Parse($tc_string); - } - 'should not throw exception'; + ok !$consent->use_non_standard_stacks, + 'should return false for use non standard stacks'; - isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; + ok !$consent->purpose_one_treatment, + 'should return false for use purpose one treatment'; - is $consent->tc_string, $tc_string, 'should return the original tc string'; + is $consent->publisher_country_code, "AA", + 'should return the publisher country code "AA"'; - is "${consent}", $tc_string, - 'should return the original tc string in string context'; + is $consent->max_vendor_id_consent, 626, + "max vendor id consent is 626"; - is $consent->version, 2, 'should return version 2'; + is $consent->max_vendor_id_legitimate_interest, 628, + "max vendor id legitimate interest is 628"; - is $consent->created, 1587946020, - 'should return the creation epoch 27/04/2020 on scalar context'; + subtest "check purpose consent ids" => sub { + plan tests => 24; - { - my ( $seconds, $nanoseconds ) = $consent->created; - is $seconds, 1587946020, - 'should return the creation epoch 27/04/2020 on list context'; - is $nanoseconds, 0, - 'should return the 0 nanoseconds of epoch on list context'; - } + foreach my $id ( 1 .. 24 ) { + ok !$consent->is_purpose_consent_allowed($id), + "checking purpose id $id for consent"; + } + }; - is $consent->last_updated, 1587946020, - 'should return the last update epoch 27/04/2020'; + subtest "check purpose legitimate interest ids" => sub { + plan tests => 24; - { - my ( $seconds, $nanoseconds ) = $consent->last_updated; - is $seconds, 1587946020, - 'should return the last update epoch 27/04/2020 on list context'; - is $nanoseconds, 0, - 'should return the 0 nanoseconds of epoch on list context'; - } + foreach my $id ( 1 .. 24 ) { + ok !$consent->is_purpose_legitimate_interest_allowed($id), + "checking purpose id $id for legitimate interest"; + } + }; - is $consent->cmp_id, 3, 'should return the cmp id 3'; + subtest "check special feature opt in" => sub { + plan tests => 12; - is $consent->cmp_version, 2, 'should return the cmp version 2'; + foreach my $id ( 1 .. 12 ) { + ok !$consent->is_special_feature_opt_in($id), + "checking special feature id $id opt in"; + } + }; - is $consent->consent_screen, 7, 'should return the consent screen 7'; + subtest "check vendor consent ids" => sub { + plan tests => 626; - is $consent->consent_language, "EN", - 'should return the consent language "EN"'; + my %allowed_vendors = + map { $_ => 1 } ( 23, 42, 126, 127, 128, 587, 613, 626 ); - is $consent->vendor_list_version, 48, - 'should return the vendor list version 23'; + foreach my $id ( 1 .. 626 ) { + is !!$consent->vendor_consent($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for consent"; + } + }; - is $consent->policy_version, 2, - 'should return the policy version 2'; + subtest "check vendor legitimate interest ids" => sub { + plan tests => 628; - ok !$consent->is_service_specific, - 'should return true for service specific'; + my %allowed_vendors = + map { $_ => 1 } ( 24, 44, 129, 130, 131, 591, 614, 628 ); - ok !$consent->use_non_standard_stacks, - 'should return false for use non standard stacks'; + foreach my $id ( 1 .. 628 ) { + is !!$consent->vendor_legitimate_interest($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for legitimate interest"; + } + }; - ok !$consent->purpose_one_treatment, - 'should return false for use purpose one treatment'; + ok !$consent->check_publisher_restriction( 1, 0, 284 ), + "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; - is $consent->publisher_country_code, "AA", - 'should return the publisher country code "AA"'; + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; - is $consent->max_vendor_id_consent, 626, "max vendor id consent is 626"; + my $publisher_tc = $consent->publisher_tc; - is $consent->max_vendor_id_legitimate_interest, 628, - "max vendor id legitimate interest is 628"; + ok !defined($publisher_tc), "should not return publisher_tc"; - subtest "check purpose consent ids" => sub { - plan tests => 24; + done_testing; + }; + subtest + "valid tcf v2 consent string using range and prefetch some vendor ids" + => sub { + my $consent; - foreach my $id ( 1 .. 24 ) { - ok !$consent->is_purpose_consent_allowed($id), - "checking purpose id $id for consent"; + my $tc_string = + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA'; + lives_ok { + $consent = GDPR::IAB::TCFv2->Parse( + $tc_string, + prefetch => [ 1, 20 .. 24, 284, 626, 268 ] + ); } - }; + 'should not throw exception'; - subtest "check purpose legitimate interest ids" => sub { - plan tests => 24; + isa_ok $consent, 'GDPR::IAB::TCFv2', 'gdpr iab tcf v2 consent'; - foreach my $id ( 1 .. 24 ) { - ok !$consent->is_purpose_legitimate_interest_allowed($id), - "checking purpose id $id for legitimate interest"; - } - }; + is $consent->tc_string, $tc_string, + 'should return the original tc string'; - subtest "check special feature opt in" => sub { - plan tests => 12; + is "${consent}", $tc_string, + 'should return the original tc string in string context'; - foreach my $id ( 1 .. 12 ) { - ok !$consent->is_special_feature_opt_in($id), - "checking special feature id $id opt in"; - } - }; + is $consent->version, 2, 'should return version 2'; - subtest "check vendor consent ids" => sub { - plan tests => 626; - my %allowed_vendors = - map { $_ => 1 } ( 23, 42, 126, 127, 128, 587, 613, 626 ); + subtest "check vendor consent ids" => sub { + plan tests => 626; - foreach my $id ( 1 .. 626 ) { - is !!$consent->vendor_consent($id), - !!$allowed_vendors{$id}, - "checking vendor id $id for consent"; - } - }; + my %allowed_vendors = + map { $_ => 1 } ( 23, 42, 126, 127, 128, 587, 613, 626 ); - subtest "check vendor legitimate interest ids" => sub { - plan tests => 628; + foreach my $id ( 1 .. 626 ) { + is !!$consent->vendor_consent($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for consent"; + } + }; - my %allowed_vendors = - map { $_ => 1 } ( 24, 44, 129, 130, 131, 591, 614, 628 ); + subtest "check vendor legitimate interest ids" => sub { + plan tests => 628; - foreach my $id ( 1 .. 628 ) { - is !!$consent->vendor_legitimate_interest($id), - !!$allowed_vendors{$id}, - "checking vendor id $id for legitimate interest"; - } - }; + my %allowed_vendors = + map { $_ => 1 } ( 24, 44, 129, 130, 131, 591, 614, 628 ); - ok !$consent->check_publisher_restriction( 1, 0, 284 ), - "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; + foreach my $id ( 1 .. 628 ) { + is !!$consent->vendor_legitimate_interest($id), + !!$allowed_vendors{$id}, + "checking vendor id $id for legitimate interest"; + } + }; - my $publisher_tc = $consent->publisher_tc; + ok !$consent->check_publisher_restriction( 1, 0, 284 ), + "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; - ok !defined($publisher_tc), "should not return publisher_tc"; + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; + done_testing; + }; done_testing; }; - subtest "check publisher restriction" => sub { subtest "check publisher restriction #1" => sub { my $consent; @@ -459,6 +547,14 @@ subtest "check publisher restriction" => sub { ok !$consent->check_publisher_restriction( 5, 1, 32 ), "must have publisher restriction to vendor 32 regarding purpose id 5 of type 1 'Require Consent'"; + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; + + $restrictions = $consent->publisher_restrictions(32); + is_deeply $restrictions, { 7 => { 1 => 1 } }, + "should return the restriction purpose id => restriction map type map"; + done_testing; }; @@ -499,6 +595,18 @@ subtest "check publisher restriction" => sub { ok $consent->check_publisher_restriction( 2, 1, 32 ); ok !$consent->check_publisher_restriction( 2, 1, 42 ); + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; + + $restrictions = $consent->publisher_restrictions(32); + is_deeply $restrictions, + { 1 => { 0 => 1 }, 2 => { 0 => 1, 1 => 1 }, 7 => { 0 => 1, 1 => 1 }, + 10 => { 0 => 1, 1 => 1 } + }, + "should return the restriction purpose id => restriction map type map"; + + done_testing; }; @@ -518,6 +626,10 @@ subtest "check publisher restriction" => sub { ok !$consent->check_publisher_restriction( 1, 0, 284 ), "should have no publisher restriction to vendor 284 regarding purpose id 1 of type 0 'Purpose Flatly Not Allowed by Publisher'"; + my $restrictions = $consent->publisher_restrictions(284); + is_deeply $restrictions, {}, + "should return the restriction purpose id => restriction map type map"; + done_testing; }; diff --git a/t/02-json.t b/t/02-json-bitfield.t similarity index 69% rename from t/02-json.t rename to t/02-json-bitfield.t index 3cb6f34..f30129e 100644 --- a/t/02-json.t +++ b/t/02-json-bitfield.t @@ -9,160 +9,199 @@ use GDPR::IAB::TCFv2; # use DateTime; # use DateTimeX::TO_JSON formatter => 'DateTime::Format::RFC3339'; -subtest - "should convert data to json using compact flag and 0/1 as booleans" => sub { - subtest "should convert data to json using yyyymmdd as date format" => +subtest "bitfield" => sub { + subtest + "should convert data to json using compact flag and 0/1 as booleans" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', - json => { - verbose => 0, - compact => 1, - use_epoch => 0, - boolean_values => [ 0, 1 ], - date_format => '%Y%m%d', # yyymmdd - }, - ); - - - my $got = $consent->TO_JSON(); - my $expected = _fixture_compact( - created => 20081207, - last_updated => 20120110, - ); - is_deeply $got, $expected, "must return the json hashref"; - - done_testing; - }; + subtest "should convert data to json using yyyymmdd as date format" => + sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + date_format => '%Y%m%d', # yyymmdd + }, + ); + + + my $got = $consent->TO_JSON(); + my $expected = _fixture_bitfield_compact( + created => 20081207, + last_updated => 20120110, + ); + is_deeply $got, $expected, "must return the json hashref"; + + done_testing; + }; + + subtest "should convert data to json using epoch date format" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 1, + boolean_values => [ 0, 1 ], + }, + ); - subtest "should convert data to json using epoch date format" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', - json => { - verbose => 0, - compact => 1, - use_epoch => 1, - boolean_values => [ 0, 1 ], - }, - ); + my $got = $consent->TO_JSON(); + my $expected = _fixture_bitfield_compact( + created => 1228644257, + last_updated => 1326215413, + ); - my $got = $consent->TO_JSON(); - my $expected = _fixture_compact( - created => 1228644257, - last_updated => 1326215413, - ); + is_deeply $got, $expected, "must return the json hashref"; - is_deeply $got, $expected, "must return the json hashref"; + done_testing; + }; done_testing; - }; + }; - done_testing; - }; + subtest + "should convert data to json using default (non-compact) and 0/1 as booleans" + => sub { + + subtest "default non verbose, date as iso 8601" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', + json => { + verbose => 0, + compact => 0, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); -subtest - "should convert data to json using default (non-compact) and 0/1 as booleans" - => sub { + ok $consent->vendor_consent(27); - subtest "default non verbose, date as iso 8601" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', - json => { - verbose => 0, - compact => 0, - use_epoch => 0, - boolean_values => [ 0, 1 ], - }, - ); - - ok $consent->vendor_consent(27); + my $got = $consent->TO_JSON(); + my $expected = _fixture_bitfield_default(); + is_deeply $got, $expected, "must return the json hashref"; - my $got = $consent->TO_JSON(); - my $expected = _fixture_default(); - is_deeply $got, $expected, "must return the json hashref"; + done_testing; + }; - done_testing; - }; + subtest "default verbose, date as iso 8601" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', + json => { + verbose => 1, + compact => 0, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); - subtest "default non verbose, date as iso 8601" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', - json => { - verbose => 1, - compact => 0, - use_epoch => 0, - boolean_values => [ 0, 1 ], - }, - ); + ok $consent->vendor_consent(27); - ok $consent->vendor_consent(27); + my $got = $consent->TO_JSON(); + my $expected = _fixture_bitfield_verbose(); + is_deeply $got, $expected, "must return the json hashref"; - my $got = $consent->TO_JSON(); - my $expected = _fixture_verbose(); - is_deeply $got, $expected, "must return the json hashref"; + done_testing; + }; done_testing; - }; - - done_testing; - }; - + }; -subtest "publisher section" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA', - json => { - verbose => 0, - compact => 1, - use_epoch => 0, - boolean_values => [ 0, 1 ], - }, - ); + subtest "publisher section" => sub { + subtest "publisher section without publisher_tc" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); - my $got = $consent->TO_JSON; - my $expected = - { "publisher" => { "restrictions" => { "7" => { "32" => 1 } } } }; + my $got = $consent->TO_JSON; + my $expected = + { "publisher" => { "restrictions" => { "7" => { "32" => 1 } } } + }; - is_deeply $got->{publisher}, $expected->{publisher}, - "must return the same publisher restriction section"; + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; - done_testing; -}; -subtest "publisher section with publisher_tc" => sub { - subtest "without custom purposes" => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.argAC0gAAAAAAAAAAAA', - json => { - verbose => 0, - compact => 1, - use_epoch => 0, - boolean_values => [ 0, 1 ], - }, - ); - - my $got = $consent->TO_JSON; - my $expected = { - "publisher" => { - "consents" => [ 2, 4, 6, 8, 9, 10 ], - "legitimate_interests" => [ 2, 4, 5, 7, 10 ], - "custom_purposes" => { - "consents" => [], - "legitimate_interests" => [], - }, - "restrictions" => { "7" => { "32" => 1 } } - } + done_testing; + }; + subtest "publisher section with publisher_tc" => sub { + subtest "without custom purposes" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.argAC0gAAAAAAAAAAAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + my $got = $consent->TO_JSON; + my $expected = { + "publisher" => { + "consents" => [ 2, 4, 6, 8, 9, 10 ], + "legitimate_interests" => [ 2, 4, 5, 7, 10 ], + "custom_purposes" => { + "consents" => [], + "legitimate_interests" => [], + }, + "restrictions" => { "7" => { "32" => 1 } } + } + }; + + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; + + done_testing; + }; + + subtest "with custom purposes" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.YAAAAAAAAXA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + my $got = $consent->TO_JSON; + my $expected = { + "publisher" => { + "consents" => [], + "legitimate_interests" => [], + "custom_purposes" => { + "consents" => [ 1, 2 ], + "legitimate_interests" => [1], + }, + "restrictions" => { "7" => { "32" => 1 } } + } + }; + + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; + + done_testing; + }; + + done_testing; }; - - is_deeply $got->{publisher}, $expected->{publisher}, - "must return the same publisher restriction section"; done_testing; }; - subtest "with custom purposes" => sub { + subtest "TO_JSON method should return the same hashref " => sub { my $consent = GDPR::IAB::TCFv2->Parse( - 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.YAAAAAAAAXA', + 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', json => { verbose => 0, compact => 1, @@ -171,50 +210,22 @@ subtest "publisher section with publisher_tc" => sub { }, ); - my $got = $consent->TO_JSON; - my $expected = { - "publisher" => { - "consents" => [], - "legitimate_interests" => [], - "custom_purposes" => { - "consents" => [ 1, 2 ], - "legitimate_interests" => [1], - }, - "restrictions" => { "7" => { "32" => 1 } } - } - }; - is_deeply $got->{publisher}, $expected->{publisher}, - "must return the same publisher restriction section"; + my $got1 = $consent->TO_JSON(); + my $got2 = $consent->TO_JSON(); + + is_deeply $got1, $got2, "must return the same hashref"; done_testing; }; done_testing; -}; -subtest "TO_JSON method should return the same hashref " => sub { - my $consent = GDPR::IAB::TCFv2->Parse( - 'CLcVDxRMWfGmWAVAHCENAXCkAKDAADnAABRgA5mdfCKZuYJez-NQm0TBMYA4oCAAGQYIAAAAAAEAIAEgAA', - json => { - verbose => 0, - compact => 1, - use_epoch => 0, - boolean_values => [ 0, 1 ], - }, - ); - - - my $got1 = $consent->TO_JSON(); - my $got2 = $consent->TO_JSON(); - is_deeply $got1, $got2, "must return the same hashref"; - - done_testing; }; done_testing; -sub _fixture_compact { +sub _fixture_bitfield_compact { my (%extra) = @_; return { @@ -332,7 +343,7 @@ sub _fixture_compact { }; } -sub _fixture_default { +sub _fixture_bitfield_default { my (%extra) = @_; return { @@ -450,7 +461,7 @@ sub _fixture_default { }; } -sub _fixture_verbose { +sub _fixture_bitfield_verbose { my (%extra) = @_; return { diff --git a/t/03-json-range.t b/t/03-json-range.t new file mode 100644 index 0000000..d25283d --- /dev/null +++ b/t/03-json-range.t @@ -0,0 +1,1682 @@ +use strict; +use warnings; + +use Test::More; + +use GDPR::IAB::TCFv2; + +# use JSON; +# use DateTime; +# use DateTimeX::TO_JSON formatter => 'DateTime::Format::RFC3339'; + +subtest "range" => sub { + subtest + "should convert data to json using compact flag and 0/1 as booleans" => + sub { + subtest "should convert data to json using yyyymmdd as date format" => + sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + date_format => '%Y%m%d', # yyymmdd + }, + ); + + my $got = $consent->TO_JSON(); + my $expected = _fixture_range_compact( + created => 20200427, + last_updated => 20200427, + ); + + is_deeply $got, $expected, "must return the json hashref"; + + done_testing; + }; + + subtest "should convert data to json using epoch date format" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 1, + boolean_values => [ 0, 1 ], + }, + ); + + + my $got = $consent->TO_JSON(); + my $expected = _fixture_range_compact( + created => 1587946020, + last_updated => 1587946020, + ); + + is_deeply $got, $expected, "must return the json hashref"; + + done_testing; + }; + + done_testing; + }; + + subtest + "should convert data to json using default (non-compact) and 0/1 as booleans" + => sub { + + subtest "default non verbose, date as iso 8601" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + json => { + verbose => 0, + compact => 0, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + ok $consent->vendor_consent(23); + + my $got = $consent->TO_JSON(); + my $expected = _fixture_range_default(); + + is_deeply $got, $expected, "must return the json hashref"; + + done_testing; + }; + + subtest "default verbose, date as iso 8601" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + json => { + verbose => 1, + compact => 0, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + ok $consent->vendor_consent(23); + + my $got = $consent->TO_JSON(); + my $expected = _fixture_range_verbose(); + + is_deeply $got, $expected, "must return the json hashref"; + + done_testing; + }; + + done_testing; + }; + + subtest "publisher section" => sub { + subtest "publisher section without publisher_tc" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + my $got = $consent->TO_JSON; + my $expected = + { "publisher" => { "restrictions" => { "7" => { "32" => 1 } } } + }; + + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; + + done_testing; + }; + subtest "publisher section with publisher_tc" => sub { + subtest "without custom purposes" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.argAC0gAAAAAAAAAAAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + my $got = $consent->TO_JSON; + my $expected = { + "publisher" => { + "consents" => [ 2, 4, 6, 8, 9, 10 ], + "legitimate_interests" => [ 2, 4, 5, 7, 10 ], + "custom_purposes" => { + "consents" => [], + "legitimate_interests" => [], + }, + "restrictions" => { "7" => { "32" => 1 } } + } + }; + + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; + + done_testing; + }; + + subtest "with custom purposes" => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COwAdDhOwAdDhN4ABAENAPCgAAQAAv___wAAAFP_AAp_4AI6ACACAA.YAAAAAAAAXA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + my $got = $consent->TO_JSON; + my $expected = { + "publisher" => { + "consents" => [], + "legitimate_interests" => [], + "custom_purposes" => { + "consents" => [ 1, 2 ], + "legitimate_interests" => [1], + }, + "restrictions" => { "7" => { "32" => 1 } } + } + }; + + is_deeply $got->{publisher}, $expected->{publisher}, + "must return the same publisher restriction section"; + + done_testing; + }; + + done_testing; + }; + + done_testing; + }; + + subtest "TO_JSON method should return the same hashref " => sub { + my $consent = GDPR::IAB::TCFv2->Parse( + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + json => { + verbose => 0, + compact => 1, + use_epoch => 0, + boolean_values => [ 0, 1 ], + }, + ); + + + my $got1 = $consent->TO_JSON(); + my $got2 = $consent->TO_JSON(); + + is_deeply $got1, $got2, "must return the same hashref"; + + done_testing; + }; + + done_testing; + +}; + +done_testing; + +sub _fixture_range_compact { + my (%extra) = @_; + + return { + 'tc_string' => + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + 'consent_language' => 'EN', + 'purpose' => { + 'consents' => [], + 'legitimate_interests' => [] + }, + 'cmp_id' => 3, + 'purpose_one_treatment' => 0, + 'publisher' => { 'restrictions' => {} }, + 'special_features_opt_in' => [], + 'last_updated' => '20200427', + 'use_non_standard_stacks' => 0, + 'policy_version' => 2, + 'version' => 2, + 'is_service_specific' => 0, + 'created' => '20200427', + 'consent_screen' => 7, + 'vendor_list_version' => 48, + 'cmp_version' => 2, + 'publisher_country_code' => 'AA', + 'vendor' => { + 'consents' => [ + 23, + 42, + 126, + 127, + 128, + 587, + 613, + 626 + ], + 'legitimate_interests' => [ + 24, + 44, + 129, + 130, + 131, + 591, + 614, + 628 + ] + }, + %extra + }; +} + +sub _fixture_range_default { + my (%extra) = @_; + + return { + 'tc_string' => + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + 'consent_language' => 'EN', + 'purpose' => { + 'consents' => {}, + 'legitimate_interests' => {} + }, + 'cmp_id' => 3, + 'purpose_one_treatment' => 0, + 'publisher' => { 'restrictions' => {} }, + 'special_features_opt_in' => {}, + 'last_updated' => '2020-04-27T00:07:00Z', + 'use_non_standard_stacks' => 0, + 'policy_version' => 2, + 'version' => 2, + 'is_service_specific' => 0, + 'created' => '2020-04-27T00:07:00Z', + 'consent_screen' => 7, + 'vendor_list_version' => 48, + 'cmp_version' => 2, + 'publisher_country_code' => 'AA', + 'vendor' => { + 'consents' => { + '128' => 1, + '613' => 1, + '127' => 1, + '626' => 1, + '42' => 1, + '23' => 1, + '126' => 1, + '587' => 1 + }, + 'legitimate_interests' => { + '628' => 1, + '591' => 1, + '130' => 1, + '131' => 1, + '24' => 1, + '129' => 1, + '44' => 1, + '614' => 1 + } + }, + %extra + }; +} + +sub _fixture_range_verbose { + my (%extra) = @_; + + return { + 'tc_string' => + 'COyfVVoOyfVVoADACHENAwCAAAAAAAAAAAAAE5QBgALgAqgD8AQACSwEygJyAnSAMABgAFkAgQCDASeAmYBOgAA', + 'consent_language' => 'EN', + 'purpose' => { + 'consents' => { + '11' => 0, + '21' => 0, + '7' => 0, + '17' => 0, + '2' => 0, + '22' => 0, + '1' => 0, + '18' => 0, + '23' => 0, + '16' => 0, + '13' => 0, + '6' => 0, + '3' => 0, + '9' => 0, + '12' => 0, + '20' => 0, + '14' => 0, + '15' => 0, + '8' => 0, + '4' => 0, + '24' => 0, + '19' => 0, + '10' => 0, + '5' => 0 + }, + 'legitimate_interests' => { + '11' => 0, + '21' => 0, + '7' => 0, + '17' => 0, + '2' => 0, + '22' => 0, + '1' => 0, + '18' => 0, + '23' => 0, + '16' => 0, + '13' => 0, + '6' => 0, + '3' => 0, + '9' => 0, + '12' => 0, + '20' => 0, + '14' => 0, + '15' => 0, + '8' => 0, + '4' => 0, + '24' => 0, + '19' => 0, + '10' => 0, + '5' => 0 + } + }, + 'cmp_id' => 3, + 'purpose_one_treatment' => 0, + 'publisher' => { 'restrictions' => {} }, + 'special_features_opt_in' => { + '6' => 0, + '11' => 0, + '3' => 0, + '7' => 0, + '9' => 0, + '12' => 0, + '2' => 0, + '8' => 0, + '1' => 0, + '4' => 0, + '10' => 0, + '5' => 0 + }, + 'last_updated' => '2020-04-27T00:07:00Z', + 'use_non_standard_stacks' => 0, + 'policy_version' => 2, + 'version' => 2, + 'is_service_specific' => 0, + 'created' => '2020-04-27T00:07:00Z', + 'consent_screen' => 7, + 'vendor_list_version' => 48, + 'cmp_version' => 2, + 'publisher_country_code' => 'AA', + 'vendor' => { + 'consents' => { + '559' => 0, + '127' => 1, + '32' => 0, + '443' => 0, + '206' => 0, + '118' => 0, + '71' => 0, + '358' => 0, + '331' => 0, + '560' => 0, + '580' => 0, + '84' => 0, + '512' => 0, + '437' => 0, + '463' => 0, + '194' => 0, + '517' => 0, + '458' => 0, + '451' => 0, + '220' => 0, + '454' => 0, + '31' => 0, + '578' => 0, + '378' => 0, + '325' => 0, + '29' => 0, + '572' => 0, + '350' => 0, + '540' => 0, + '226' => 0, + '58' => 0, + '211' => 0, + '153' => 0, + '15' => 0, + '527' => 0, + '431' => 0, + '382' => 0, + '337' => 0, + '101' => 0, + '340' => 0, + '76' => 0, + '311' => 0, + '62' => 0, + '571' => 0, + '139' => 0, + '389' => 0, + '129' => 0, + '548' => 0, + '495' => 0, + '418' => 0, + '236' => 0, + '218' => 0, + '168' => 0, + '135' => 0, + '14' => 0, + '348' => 0, + '145' => 0, + '49' => 0, + '178' => 0, + '285' => 0, + '124' => 0, + '234' => 0, + '594' => 0, + '23' => 1, + '388' => 0, + '364' => 0, + '96' => 0, + '486' => 0, + '509' => 0, + '160' => 0, + '569' => 0, + '367' => 0, + '8' => 0, + '98' => 0, + '43' => 0, + '485' => 0, + '391' => 0, + '21' => 0, + '523' => 0, + '288' => 0, + '193' => 0, + '460' => 0, + '119' => 0, + '586' => 0, + '453' => 0, + '324' => 0, + '180' => 0, + '244' => 0, + '351' => 0, + '410' => 0, + '595' => 0, + '246' => 0, + '488' => 0, + '61' => 0, + '430' => 0, + '447' => 0, + '536' => 0, + '379' => 0, + '415' => 0, + '113' => 0, + '152' => 0, + '189' => 0, + '452' => 0, + '342' => 0, + '579' => 0, + '295' => 0, + '480' => 0, + '341' => 0, + '438' => 0, + '107' => 0, + '535' => 0, + '87' => 0, + '77' => 0, + '444' => 0, + '541' => 0, + '508' => 0, + '221' => 0, + '39' => 0, + '64' => 0, + '558' => 0, + '417' => 0, + '12' => 0, + '312' => 0, + '45' => 0, + '507' => 0, + '405' => 0, + '260' => 0, + '573' => 0, + '237' => 0, + '370' => 0, + '309' => 0, + '567' => 0, + '1' => 0, + '506' => 0, + '136' => 0, + '116' => 0, + '416' => 0, + '144' => 0, + '380' => 0, + '100' => 0, + '300' => 0, + '286' => 0, + '120' => 0, + '381' => 0, + '581' => 0, + '308' => 0, + '392' => 0, + '254' => 0, + '177' => 0, + '496' => 0, + '605' => 0, + '373' => 0, + '607' => 0, + '205' => 0, + '42' => 1, + '22' => 0, + '399' => 0, + '235' => 0, + '301' => 0, + '436' => 0, + '213' => 0, + '94' => 0, + '51' => 0, + '456' => 0, + '568' => 0, + '296' => 0, + '265' => 0, + '493' => 0, + '171' => 0, + '386' => 0, + '445' => 0, + '200' => 0, + '366' => 0, + '329' => 0, + '525' => 0, + '27' => 0, + '272' => 0, + '161' => 0, + '582' => 0, + '534' => 0, + '400' => 0, + '20' => 0, + '109' => 0, + '151' => 0, + '557' => 0, + '468' => 0, + '287' => 0, + '475' => 0, + '441' => 0, + '78' => 0, + '413' => 0, + '294' => 0, + '349' => 0, + '275' => 0, + '515' => 0, + '197' => 0, + '138' => 0, + '606' => 0, + '137' => 0, + '60' => 0, + '432' => 0, + '519' => 0, + '346' => 0, + '17' => 0, + '427' => 0, + '82' => 0, + '110' => 0, + '333' => 0, + '590' => 0, + '323' => 0, + '69' => 0, + '112' => 0, + '545' => 0, + '191' => 0, + '224' => 0, + '187' => 0, + '588' => 0, + '446' => 0, + '262' => 0, + '617' => 0, + '79' => 0, + '212' => 0, + '352' => 0, + '126' => 1, + '426' => 0, + '251' => 0, + '542' => 0, + '369' => 0, + '279' => 0, + '176' => 0, + '498' => 0, + '483' => 0, + '256' => 0, + '372' => 0, + '574' => 0, + '170' => 0, + '33' => 0, + '428' => 0, + '7' => 0, + '26' => 0, + '227' => 0, + '99' => 0, + '566' => 0, + '526' => 0, + '72' => 0, + '500' => 0, + '264' => 0, + '255' => 0, + '533' => 0, + '359' => 0, + '182' => 0, + '108' => 0, + '604' => 0, + '556' => 0, + '462' => 0, + '414' => 0, + '232' => 0, + '477' => 0, + '225' => 0, + '330' => 0, + '142' => 0, + '207' => 0, + '263' => 0, + '394' => 0, + '167' => 0, + '48' => 0, + '360' => 0, + '610' => 0, + '514' => 0, + '513' => 0, + '615' => 0, + '50' => 0, + '476' => 0, + '510' => 0, + '393' => 0, + '449' => 0, + '293' => 0, + '274' => 0, + '549' => 0, + '322' => 0, + '469' => 0, + '353' => 0, + '575' => 0, + '375' => 0, + '128' => 1, + '28' => 0, + '310' => 0, + '40' => 0, + '589' => 0, + '303' => 0, + '192' => 0, + '250' => 0, + '614' => 0, + '501' => 0, + '215' => 0, + '278' => 0, + '490' => 0, + '150' => 0, + '130' => 0, + '155' => 0, + '387' => 0, + '53' => 0, + '245' => 0, + '626' => 1, + '543' => 0, + '267' => 0, + '354' => 0, + '461' => 0, + '583' => 0, + '257' => 0, + '85' => 0, + '332' => 0, + '9' => 0, + '425' => 0, + '591' => 0, + '34' => 0, + '539' => 0, + '603' => 0, + '90' => 0, + '276' => 0, + '620' => 0, + '565' => 0, + '102' => 0, + '520' => 0, + '532' => 0, + '16' => 0, + '55' => 0, + '233' => 0, + '57' => 0, + '259' => 0, + '368' => 0, + '424' => 0, + '316' => 0, + '163' => 0, + '395' => 0, + '89' => 0, + '611' => 0, + '175' => 0, + '584' => 0, + '35' => 0, + '11' => 0, + '492' => 0, + '208' => 0, + '347' => 0, + '511' => 0, + '434' => 0, + '93' => 0, + '292' => 0, + '291' => 0, + '374' => 0, + '114' => 0, + '199' => 0, + '442' => 0, + '429' => 0, + '73' => 0, + '409' => 0, + '67' => 0, + '241' => 0, + '198' => 0, + '489' => 0, + '585' => 0, + '327' => 0, + '320' => 0, + '280' => 0, + '273' => 0, + '471' => 0, + '622' => 0, + '202' => 0, + '249' => 0, + '361' => 0, + '465' => 0, + '184' => 0, + '24' => 0, + '140' => 0, + '104' => 0, + '131' => 0, + '181' => 0, + '412' => 0, + '385' => 0, + '502' => 0, + '307' => 0, + '314' => 0, + '154' => 0, + '355' => 0, + '553' => 0, + '159' => 0, + '479' => 0, + '326' => 0, + '555' => 0, + '47' => 0, + '619' => 0, + '37' => 0, + '335' => 0, + '270' => 0, + '5' => 0, + '195' => 0, + '621' => 0, + '538' => 0, + '524' => 0, + '554' => 0, + '552' => 0, + '521' => 0, + '598' => 0, + '162' => 0, + '433' => 0, + '74' => 0, + '240' => 0, + '334' => 0, + '440' => 0, + '230' => 0, + '115' => 0, + '299' => 0, + '377' => 0, + '103' => 0, + '602' => 0, + '201' => 0, + '423' => 0, + '612' => 0, + '91' => 0, + '266' => 0, + '467' => 0, + '174' => 0, + '474' => 0, + '481' => 0, + '214' => 0, + '422' => 0, + '564' => 0, + '563' => 0, + '97' => 0, + '41' => 0, + '52' => 0, + '302' => 0, + '229' => 0, + '503' => 0, + '593' => 0, + '68' => 0, + '188' => 0, + '315' => 0, + '402' => 0, + '338' => 0, + '576' => 0, + '616' => 0, + '222' => 0, + '25' => 0, + '83' => 0, + '484' => 0, + '305' => 0, + '623' => 0, + '544' => 0, + '217' => 0, + '328' => 0, + '239' => 0, + '122' => 0, + '143' => 0, + '158' => 0, + '269' => 0, + '281' => 0, + '464' => 0, + '363' => 0, + '46' => 0, + '6' => 0, + '562' => 0, + '36' => 0, + '518' => 0, + '183' => 0, + '497' => 0, + '472' => 0, + '362' => 0, + '439' => 0, + '317' => 0, + '608' => 0, + '132' => 0, + '169' => 0, + '411' => 0, + '478' => 0, + '384' => 0, + '398' => 0, + '546' => 0, + '537' => 0, + '407' => 0, + '18' => 0, + '376' => 0, + '522' => 0, + '125' => 0, + '599' => 0, + '44' => 0, + '609' => 0, + '587' => 1, + '190' => 0, + '95' => 0, + '298' => 0, + '601' => 0, + '313' => 0, + '243' => 0, + '231' => 0, + '551' => 0, + '529' => 0, + '148' => 0, + '343' => 0, + '504' => 0, + '397' => 0, + '106' => 0, + '157' => 0, + '65' => 0, + '203' => 0, + '261' => 0, + '81' => 0, + '321' => 0, + '459' => 0, + '624' => 0, + '86' => 0, + '284' => 0, + '247' => 0, + '371' => 0, + '204' => 0, + '165' => 0, + '289' => 0, + '2' => 0, + '435' => 0, + '401' => 0, + '186' => 0, + '147' => 0, + '339' => 0, + '228' => 0, + '531' => 0, + '268' => 0, + '345' => 0, + '596' => 0, + '172' => 0, + '319' => 0, + '223' => 0, + '404' => 0, + '613' => 1, + '516' => 0, + '282' => 0, + '420' => 0, + '121' => 0, + '344' => 0, + '487' => 0, + '494' => 0, + '238' => 0, + '577' => 0, + '253' => 0, + '561' => 0, + '448' => 0, + '209' => 0, + '216' => 0, + '357' => 0, + '117' => 0, + '63' => 0, + '455' => 0, + '600' => 0, + '80' => 0, + '336' => 0, + '457' => 0, + '179' => 0, + '383' => 0, + '297' => 0, + '277' => 0, + '92' => 0, + '10' => 0, + '550' => 0, + '505' => 0, + '419' => 0, + '133' => 0, + '290' => 0, + '625' => 0, + '592' => 0, + '149' => 0, + '123' => 0, + '304' => 0, + '547' => 0, + '210' => 0, + '406' => 0, + '258' => 0, + '396' => 0, + '482' => 0, + '173' => 0, + '530' => 0, + '56' => 0, + '499' => 0, + '66' => 0, + '19' => 0, + '54' => 0, + '365' => 0, + '306' => 0, + '70' => 0, + '470' => 0, + '166' => 0, + '88' => 0, + '141' => 0, + '30' => 0, + '570' => 0, + '403' => 0, + '252' => 0, + '466' => 0, + '156' => 0, + '134' => 0, + '75' => 0, + '283' => 0, + '618' => 0, + '59' => 0, + '421' => 0, + '450' => 0, + '271' => 0, + '491' => 0, + '219' => 0, + '318' => 0, + '13' => 0, + '105' => 0, + '473' => 0, + '185' => 0, + '3' => 0, + '597' => 0, + '248' => 0, + '390' => 0, + '146' => 0, + '111' => 0, + '38' => 0, + '356' => 0, + '408' => 0, + '4' => 0, + '528' => 0, + '164' => 0, + '196' => 0, + '242' => 0 + }, + 'legitimate_interests' => { + '559' => 0, + '127' => 0, + '32' => 0, + '443' => 0, + '206' => 0, + '118' => 0, + '71' => 0, + '358' => 0, + '331' => 0, + '560' => 0, + '580' => 0, + '84' => 0, + '512' => 0, + '437' => 0, + '463' => 0, + '194' => 0, + '517' => 0, + '458' => 0, + '451' => 0, + '220' => 0, + '454' => 0, + '31' => 0, + '578' => 0, + '378' => 0, + '325' => 0, + '29' => 0, + '572' => 0, + '350' => 0, + '540' => 0, + '226' => 0, + '58' => 0, + '211' => 0, + '153' => 0, + '15' => 0, + '527' => 0, + '431' => 0, + '382' => 0, + '337' => 0, + '101' => 0, + '340' => 0, + '76' => 0, + '311' => 0, + '62' => 0, + '571' => 0, + '139' => 0, + '389' => 0, + '129' => 1, + '548' => 0, + '495' => 0, + '418' => 0, + '236' => 0, + '218' => 0, + '168' => 0, + '135' => 0, + '14' => 0, + '348' => 0, + '145' => 0, + '49' => 0, + '178' => 0, + '285' => 0, + '124' => 0, + '627' => 0, + '234' => 0, + '594' => 0, + '23' => 0, + '388' => 0, + '364' => 0, + '96' => 0, + '486' => 0, + '509' => 0, + '160' => 0, + '569' => 0, + '367' => 0, + '8' => 0, + '98' => 0, + '43' => 0, + '485' => 0, + '391' => 0, + '21' => 0, + '523' => 0, + '288' => 0, + '193' => 0, + '460' => 0, + '119' => 0, + '586' => 0, + '453' => 0, + '324' => 0, + '180' => 0, + '244' => 0, + '351' => 0, + '410' => 0, + '595' => 0, + '246' => 0, + '488' => 0, + '61' => 0, + '430' => 0, + '447' => 0, + '536' => 0, + '379' => 0, + '415' => 0, + '113' => 0, + '152' => 0, + '189' => 0, + '452' => 0, + '342' => 0, + '579' => 0, + '295' => 0, + '480' => 0, + '341' => 0, + '438' => 0, + '107' => 0, + '535' => 0, + '87' => 0, + '77' => 0, + '444' => 0, + '541' => 0, + '508' => 0, + '221' => 0, + '39' => 0, + '64' => 0, + '558' => 0, + '417' => 0, + '12' => 0, + '312' => 0, + '45' => 0, + '507' => 0, + '405' => 0, + '260' => 0, + '573' => 0, + '237' => 0, + '370' => 0, + '309' => 0, + '567' => 0, + '1' => 0, + '506' => 0, + '136' => 0, + '116' => 0, + '416' => 0, + '144' => 0, + '380' => 0, + '100' => 0, + '300' => 0, + '286' => 0, + '120' => 0, + '381' => 0, + '581' => 0, + '308' => 0, + '392' => 0, + '254' => 0, + '177' => 0, + '496' => 0, + '605' => 0, + '373' => 0, + '607' => 0, + '205' => 0, + '42' => 0, + '22' => 0, + '399' => 0, + '235' => 0, + '301' => 0, + '436' => 0, + '213' => 0, + '94' => 0, + '51' => 0, + '456' => 0, + '568' => 0, + '296' => 0, + '265' => 0, + '493' => 0, + '171' => 0, + '386' => 0, + '445' => 0, + '200' => 0, + '366' => 0, + '329' => 0, + '525' => 0, + '27' => 0, + '272' => 0, + '161' => 0, + '582' => 0, + '534' => 0, + '400' => 0, + '20' => 0, + '109' => 0, + '151' => 0, + '557' => 0, + '468' => 0, + '287' => 0, + '475' => 0, + '441' => 0, + '78' => 0, + '413' => 0, + '294' => 0, + '349' => 0, + '275' => 0, + '515' => 0, + '197' => 0, + '138' => 0, + '606' => 0, + '137' => 0, + '60' => 0, + '432' => 0, + '519' => 0, + '346' => 0, + '17' => 0, + '427' => 0, + '82' => 0, + '110' => 0, + '333' => 0, + '590' => 0, + '323' => 0, + '69' => 0, + '112' => 0, + '545' => 0, + '191' => 0, + '224' => 0, + '187' => 0, + '588' => 0, + '446' => 0, + '262' => 0, + '617' => 0, + '79' => 0, + '212' => 0, + '352' => 0, + '126' => 0, + '426' => 0, + '251' => 0, + '542' => 0, + '369' => 0, + '279' => 0, + '176' => 0, + '498' => 0, + '483' => 0, + '256' => 0, + '372' => 0, + '574' => 0, + '170' => 0, + '33' => 0, + '428' => 0, + '7' => 0, + '26' => 0, + '227' => 0, + '99' => 0, + '566' => 0, + '526' => 0, + '72' => 0, + '500' => 0, + '264' => 0, + '255' => 0, + '533' => 0, + '359' => 0, + '182' => 0, + '108' => 0, + '604' => 0, + '556' => 0, + '462' => 0, + '414' => 0, + '232' => 0, + '477' => 0, + '225' => 0, + '330' => 0, + '142' => 0, + '207' => 0, + '263' => 0, + '394' => 0, + '167' => 0, + '48' => 0, + '360' => 0, + '610' => 0, + '514' => 0, + '513' => 0, + '615' => 0, + '50' => 0, + '476' => 0, + '510' => 0, + '393' => 0, + '449' => 0, + '293' => 0, + '274' => 0, + '549' => 0, + '322' => 0, + '469' => 0, + '353' => 0, + '575' => 0, + '375' => 0, + '128' => 0, + '28' => 0, + '310' => 0, + '40' => 0, + '589' => 0, + '303' => 0, + '192' => 0, + '250' => 0, + '614' => 1, + '501' => 0, + '215' => 0, + '278' => 0, + '490' => 0, + '150' => 0, + '130' => 1, + '155' => 0, + '387' => 0, + '53' => 0, + '245' => 0, + '626' => 0, + '543' => 0, + '267' => 0, + '354' => 0, + '461' => 0, + '583' => 0, + '257' => 0, + '85' => 0, + '332' => 0, + '9' => 0, + '425' => 0, + '591' => 1, + '34' => 0, + '539' => 0, + '603' => 0, + '90' => 0, + '276' => 0, + '620' => 0, + '565' => 0, + '102' => 0, + '520' => 0, + '532' => 0, + '16' => 0, + '55' => 0, + '233' => 0, + '57' => 0, + '259' => 0, + '368' => 0, + '424' => 0, + '316' => 0, + '163' => 0, + '395' => 0, + '89' => 0, + '611' => 0, + '175' => 0, + '584' => 0, + '35' => 0, + '11' => 0, + '492' => 0, + '208' => 0, + '347' => 0, + '511' => 0, + '434' => 0, + '93' => 0, + '292' => 0, + '291' => 0, + '374' => 0, + '114' => 0, + '199' => 0, + '442' => 0, + '429' => 0, + '73' => 0, + '409' => 0, + '67' => 0, + '241' => 0, + '198' => 0, + '489' => 0, + '585' => 0, + '327' => 0, + '320' => 0, + '280' => 0, + '273' => 0, + '471' => 0, + '622' => 0, + '202' => 0, + '249' => 0, + '361' => 0, + '465' => 0, + '184' => 0, + '24' => 1, + '140' => 0, + '104' => 0, + '131' => 1, + '181' => 0, + '412' => 0, + '385' => 0, + '502' => 0, + '307' => 0, + '314' => 0, + '154' => 0, + '355' => 0, + '553' => 0, + '159' => 0, + '479' => 0, + '326' => 0, + '555' => 0, + '47' => 0, + '619' => 0, + '37' => 0, + '335' => 0, + '270' => 0, + '5' => 0, + '195' => 0, + '621' => 0, + '538' => 0, + '524' => 0, + '554' => 0, + '552' => 0, + '521' => 0, + '598' => 0, + '162' => 0, + '433' => 0, + '74' => 0, + '240' => 0, + '334' => 0, + '440' => 0, + '230' => 0, + '115' => 0, + '299' => 0, + '377' => 0, + '103' => 0, + '602' => 0, + '201' => 0, + '423' => 0, + '612' => 0, + '91' => 0, + '266' => 0, + '467' => 0, + '174' => 0, + '474' => 0, + '481' => 0, + '214' => 0, + '422' => 0, + '564' => 0, + '563' => 0, + '97' => 0, + '41' => 0, + '52' => 0, + '302' => 0, + '229' => 0, + '503' => 0, + '593' => 0, + '68' => 0, + '188' => 0, + '315' => 0, + '402' => 0, + '338' => 0, + '576' => 0, + '616' => 0, + '222' => 0, + '25' => 0, + '83' => 0, + '484' => 0, + '305' => 0, + '623' => 0, + '544' => 0, + '217' => 0, + '328' => 0, + '239' => 0, + '122' => 0, + '143' => 0, + '628' => 1, + '158' => 0, + '269' => 0, + '281' => 0, + '464' => 0, + '363' => 0, + '46' => 0, + '6' => 0, + '562' => 0, + '36' => 0, + '518' => 0, + '183' => 0, + '497' => 0, + '472' => 0, + '362' => 0, + '439' => 0, + '317' => 0, + '608' => 0, + '132' => 0, + '169' => 0, + '411' => 0, + '478' => 0, + '384' => 0, + '398' => 0, + '546' => 0, + '537' => 0, + '407' => 0, + '18' => 0, + '376' => 0, + '522' => 0, + '125' => 0, + '599' => 0, + '44' => 1, + '609' => 0, + '587' => 0, + '190' => 0, + '95' => 0, + '298' => 0, + '601' => 0, + '313' => 0, + '243' => 0, + '231' => 0, + '551' => 0, + '529' => 0, + '148' => 0, + '343' => 0, + '504' => 0, + '397' => 0, + '106' => 0, + '157' => 0, + '65' => 0, + '203' => 0, + '261' => 0, + '81' => 0, + '321' => 0, + '459' => 0, + '624' => 0, + '86' => 0, + '284' => 0, + '247' => 0, + '371' => 0, + '204' => 0, + '165' => 0, + '289' => 0, + '2' => 0, + '435' => 0, + '401' => 0, + '186' => 0, + '147' => 0, + '339' => 0, + '228' => 0, + '531' => 0, + '268' => 0, + '345' => 0, + '596' => 0, + '172' => 0, + '319' => 0, + '223' => 0, + '404' => 0, + '613' => 0, + '516' => 0, + '282' => 0, + '420' => 0, + '121' => 0, + '344' => 0, + '487' => 0, + '494' => 0, + '238' => 0, + '577' => 0, + '253' => 0, + '561' => 0, + '448' => 0, + '209' => 0, + '216' => 0, + '357' => 0, + '117' => 0, + '63' => 0, + '455' => 0, + '600' => 0, + '80' => 0, + '336' => 0, + '457' => 0, + '179' => 0, + '383' => 0, + '297' => 0, + '277' => 0, + '92' => 0, + '10' => 0, + '550' => 0, + '505' => 0, + '419' => 0, + '133' => 0, + '290' => 0, + '625' => 0, + '592' => 0, + '149' => 0, + '123' => 0, + '304' => 0, + '547' => 0, + '210' => 0, + '406' => 0, + '258' => 0, + '396' => 0, + '482' => 0, + '173' => 0, + '530' => 0, + '56' => 0, + '499' => 0, + '66' => 0, + '19' => 0, + '54' => 0, + '365' => 0, + '306' => 0, + '70' => 0, + '470' => 0, + '166' => 0, + '88' => 0, + '141' => 0, + '30' => 0, + '570' => 0, + '403' => 0, + '252' => 0, + '466' => 0, + '156' => 0, + '134' => 0, + '75' => 0, + '283' => 0, + '618' => 0, + '59' => 0, + '421' => 0, + '450' => 0, + '271' => 0, + '491' => 0, + '219' => 0, + '318' => 0, + '13' => 0, + '105' => 0, + '473' => 0, + '185' => 0, + '3' => 0, + '597' => 0, + '248' => 0, + '390' => 0, + '146' => 0, + '111' => 0, + '38' => 0, + '356' => 0, + '408' => 0, + '4' => 0, + '528' => 0, + '164' => 0, + '196' => 0, + '242' => 0 + } + }, + %extra + }; +} diff --git a/t/03-bugs.t b/t/90-bugs.t similarity index 100% rename from t/03-bugs.t rename to t/90-bugs.t