Google-RestApi

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN

Once you have successfully created your OAuth2 token, you can run the tutorials
to ensure everything is working correctly. Set the environment variable
`GOOGLE_RESTAPI_CONFIG` to the path to your auth config file. See the
`tutorial/` directory for step-by-step tutorials covering Sheets, Drive,
Calendar, Documents, Gmail, and Tasks. These will help you understand how the
API interacts with Google.

## Chained API Calls

Every Google API module has an `api()` method. Sub-resource objects
(see [Google::RestApi::SubResource](https://metacpan.org/pod/Google%3A%3ARestApi%3A%3ASubResource)) don't call the Google endpoint
directly; instead, each `api()` prepends its own URI segment and
delegates to its parent's `api()`. The calls chain upward until they
reach the top-level API module (e.g. DriveApi3), which prepends the
endpoint base URL and hands the fully-assembled URI to
`Google::RestApi` for the actual HTTP request.

For example, deleting a reply on a comment on a file produces this chain:

    $reply->api(method => 'delete')
      # Reply prepends "replies/$reply_id"
      -> $comment->api(uri => "replies/$reply_id", method => 'delete')
        # Comment prepends "comments/$comment_id"
        -> $file->api(uri => "comments/$comment_id/replies/$reply_id", ...)
          # File prepends "files/$file_id"

README.md  view on Meta::CPAN

        auth:
          class: OAuth2Client
          config_file: <path_to_oauth_config_file>

    This allows you the option to keep the auth file in a separate, more secure place.

- api(%args);

    The ultimate Google API call for the underlying classes. Handles timeouts and retries etc. %args consists of:

    - `uri` &lt;uri\_string>: The Google API endpoint such as https://www.googleapis.com/drive/v3 along with any path segments added.
    - `method` &lt;http\_method\_string>: The http method being used get|head|put|patch|post|delete.
    - `headers` &lt;headers\_string\_array>: Array ref of http headers.
    - `params` &lt;query\_parameters\_hash>: Http query params to be added to the uri.
    - `content` &lt;payload hash>: The body being sent for post/put etc. Will be encoded to JSON.

    You would not normally call this directly unless you were making a Google API call not currently supported by this API framework.

    Returns the response hash from Google API.

- api\_callback(&lt;coderef>);

lib/Google/RestApi.pm  view on Meta::CPAN


  if ($self->{log4perl_config} && !Log::Log4perl->initialized()) {
    Log::Log4perl->init($self->{log4perl_config});
  }

  $self->{ua} = Furl->new(timeout => $self->{timeout});

  return bless $self, $class;
}

# this is the actual call to the google api endpoint. handles retries, error checking etc.
# this would normally be called via Drive or Sheets objects.
sub api {
  my $self = shift;

  state $check = signature(
    bless => !!0,
    named => [
      uri     => StrMatch[qr(^https://)],
      method  => StrMatch[qr/^(get|head|put|patch|post|delete)$/i], { default => 'get' },
      params  => HashRef[Str|ArrayRef[Str]], { default => {} },   # uri param string.

lib/Google/RestApi.pm  view on Meta::CPAN

  my ($transaction) = @_;
  my $details = eval { $transaction->{decoded_response}{error}{details} };
  return unless ref $details eq 'ARRAY';
  for my $detail (@$details) {
    my $url = eval { $detail->{metadata}{activationUrl} };
    return $url if $url;
  }
  return;
}

# the maximum number of attempts to call the google api endpoint before giving up.
# undef returns current value. postitive int sets and returns new value.
# 0 sets and returns default value.
sub max_attempts {
  my $self = shift;
  state $check = signature(positional => [PositiveOrZeroInt->where(sub { $_ < 10; }), { optional => 1 }]);
  my ($max_attempts) = $check->(@_);
  $self->{max_attempts} = $max_attempts if $max_attempts;
  $self->{max_attempts} = 4 if defined $max_attempts && $max_attempts == 0;
  return $self->{max_attempts};
}

lib/Google/RestApi.pm  view on Meta::CPAN

Once you have successfully created your OAuth2 token, you can run the tutorials
to ensure everything is working correctly. Set the environment variable
C<GOOGLE_RESTAPI_CONFIG> to the path to your auth config file. See the
C<tutorial/> directory for step-by-step tutorials covering Sheets, Drive,
Calendar, Documents, Gmail, and Tasks. These will help you understand how the
API interacts with Google.

=head2 Chained API Calls

Every Google API module has an C<api()> method. Sub-resource objects
(see L<Google::RestApi::SubResource>) don't call the Google endpoint
directly; instead, each C<api()> prepends its own URI segment and
delegates to its parent's C<api()>. The calls chain upward until they
reach the top-level API module (e.g. DriveApi3), which prepends the
endpoint base URL and hands the fully-assembled URI to
C<Google::RestApi> for the actual HTTP request.

For example, deleting a reply on a comment on a file produces this chain:

 $reply->api(method => 'delete')
   # Reply prepends "replies/$reply_id"
   -> $comment->api(uri => "replies/$reply_id", method => 'delete')
     # Comment prepends "comments/$comment_id"
     -> $file->api(uri => "comments/$comment_id/replies/$reply_id", ...)
       # File prepends "files/$file_id"

lib/Google/RestApi.pm  view on Meta::CPAN

    config_file: <path_to_oauth_config_file>

This allows you the option to keep the auth file in a separate, more secure place.

=item api(%args);

The ultimate Google API call for the underlying classes. Handles timeouts and retries etc. %args consists of:

=over

=item * C<uri> <uri_string>: The Google API endpoint such as https://www.googleapis.com/drive/v3 along with any path segments added.

=item * C<method> <http_method_string>: The http method being used get|head|put|patch|post|delete.

=item * C<headers> <headers_string_array>: Array ref of http headers.

=item * C<params> <query_parameters_hash>: Http query params to be added to the uri.

=item * C<content> <payload hash>: The body being sent for post/put etc. Will be encoded to JSON.

=back

lib/Google/RestApi/CalendarApi3.pm  view on Meta::CPAN


Readonly our $Calendar_Endpoint => 'https://www.googleapis.com/calendar/v3';
Readonly our $Calendar_Id       => '[a-zA-Z0-9._@-]+';

sub new {
  my $class = shift;
  state $check = signature(
    bless => !!0,
    named => [
      api      => HasApi,
      endpoint => Str, { default => $Calendar_Endpoint },
    ],
  );
  return bless $check->(@_), $class;
}

sub api {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      uri     => Str, { optional => 1 },
      _extra_ => slurpy HashRef,
    ],
  );
  my $p = named_extra($check->(@_));
  my $uri = "$self->{endpoint}/";
  $uri .= delete $p->{uri} if defined $p->{uri};
  return $self->{api}->api(%$p, uri => $uri);
}

sub calendar {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      id => Str,

lib/Google/RestApi/CalendarApi3.pm  view on Meta::CPAN

Creates a new CalendarApi3 instance.

 my $cal_api = Google::RestApi::CalendarApi3->new(api => $rest_api);

%args consists of:

=over

=item * C<api> L<Google::RestApi>: Required. A configured RestApi instance.

=item * C<endpoint> <string>: Optional. Override the default Calendar API endpoint.

=back

=head2 api(%args)

Low-level method to make API calls. You would not normally call this directly
unless making a Google API call not currently supported by this framework.

%args consists of:

=over

=item * C<uri> <string>: Path segments to append to the Calendar endpoint.

=item * C<%args>: Additional arguments passed to L<Google::RestApi>'s api() (content, params, method, etc).

=back

Returns the response hash from the Google API.

=head2 calendar(%args)

Returns a Calendar object for the given calendar ID.

lib/Google/RestApi/DocsApi1.pm  view on Meta::CPAN

Readonly our $Document_Filter  => "mimeType = 'application/vnd.google-apps.document'";

sub new {
  my $class = shift;

  state $check = signature(
    bless => !!0,
    named => [
      api      => HasApi,
      drive    => HasMethods[qw(list)], { optional => 1 },
      endpoint => Str, { default => $Docs_Endpoint },
    ],
  );
  my $self = $check->(@_);

  return bless $self, $class;
}

sub api {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      uri     => Str, { default => '' },
      _extra_ => slurpy HashRef,
    ],
  );
  my $p = named_extra($check->(@_));
  my $uri = $self->{endpoint};
  $uri .= "/$p->{uri}" if $p->{uri};
  return $self->rest_api()->api(%$p, uri => $uri);
}

sub create_document {
  my $self = shift;

  state $check = signature(
    bless => !!0,
    named => [

lib/Google/RestApi/DocsApi1.pm  view on Meta::CPAN

 my $docs_api = Google::RestApi::DocsApi1->new(api => $rest_api);

%args consists of:

=over

=item * C<api> L<Google::RestApi>: Required. A configured RestApi instance.

=item * C<drive> <object>: Optional. A Drive API instance for listing/deleting documents.

=item * C<endpoint> <string>: Optional. Override the default Docs API endpoint.

=back

=head2 api(%args)

Low-level method to make API calls. You would not normally call this directly
unless making a Google API call not currently supported by this framework.

%args consists of:

=over

=item * C<uri> <string>: Path segments to append to the Docs endpoint.

=item * C<%args>: Additional arguments passed to L<Google::RestApi>'s api() (content, params, method, etc).

=back

Returns the response hash from the Google API.

=head2 create_document(%args)

Creates a new Google Docs document.

lib/Google/RestApi/DocsApi1/Document.pm  view on Meta::CPAN

%args consists of:

=over

=item * C<fields> <string>: Optional. Fields to return (e.g. 'title', 'body').

=back

=item submit_requests()

Submits all queued batch requests to the Google Docs API batchUpdate endpoint.

=item insert_text(%args)

Queues an insertText request.

%args: C<text> (required), C<index> (optional), C<segment_id> (optional).

=item delete_content(%args)

Queues a deleteContentRange request.

lib/Google/RestApi/DriveApi3.pm  view on Meta::CPAN

Readonly our $Drive_Endpoint => 'https://www.googleapis.com/drive/v3';
Readonly our $Drive_File_Id  => '[a-zA-Z0-9-_]+';
Readonly our $Drive_Id       => '[a-zA-Z0-9-_]+';

sub new {
  my $class = shift;
  state $check = signature(
    bless => !!0,
    named => [
      api      => HasApi,
      endpoint => Str, { default => $Drive_Endpoint },
    ],
  );
  return bless $check->(@_), $class;
}

sub api {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      uri     => Str, { optional => 1 },
      _extra_ => slurpy HashRef,
    ],
  );
  my $p = named_extra($check->(@_));
  my $uri = "$self->{endpoint}/";
  $uri .= delete $p->{uri} if defined $p->{uri};
  return $self->{api}->api(%$p, uri => $uri);
}

sub list {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      filter        => Str,

lib/Google/RestApi/DriveApi3.pm  view on Meta::CPAN

    result_key     => 'files',
    default_fields => 'files(id, name)',
    max_pages      => $p->{max_pages},
    params         => $p->{params},
    ($p->{page_callback} ? (page_callback => $p->{page_callback}) : ()),
  );
}
# backward compatibility.
*filter_files = *list{CODE};

sub upload_endpoint {
  my $self = shift;
  my $upload = $self->{endpoint};
  $upload =~ s|googleapis.com/|googleapis.com/upload/|;
  return $upload;
}

sub file { File->new(drive => shift, @_); }

sub about { About->new(drive_api => shift); }

sub changes { Changes->new(drive_api => shift); }

lib/Google/RestApi/DriveApi3.pm  view on Meta::CPAN

Creates a new DriveApi3 instance.

 my $drive = Google::RestApi::DriveApi3->new(api => $rest_api);

%args consists of:

=over

=item * C<api> L<Google::RestApi>: Required. A configured RestApi instance.

=item * C<endpoint> <string>: Optional. Override the default Drive API endpoint.

=back

=head2 api(%args)

Low-level method to make API calls. You would not normally call this directly
unless making a Google API call not currently supported by this framework.

%args consists of:

=over

=item * C<uri> <string>: Path segments to append to the Drive endpoint.

=item * C<%args>: Additional arguments passed to L<Google::RestApi>'s api() (content, params, method, etc).

=back

Returns the response hash from the Google API.

=head2 list(%args)

Lists files matching the given query filter.

lib/Google/RestApi/DriveApi3.pm  view on Meta::CPAN

Returns a list of generated file ID strings.

=head2 empty_trash()

Permanently deletes all files in the user's trash. Use with caution!

 $drive->empty_trash();

Returns the API response (empty on success).

=head2 upload_endpoint()

Returns the upload endpoint URL for file uploads. Used internally.

=head2 rest_api()

Returns the underlying L<Google::RestApi> instance.

=head1 QUERY SYNTAX

The list() method accepts Google Drive query syntax. Common examples:

 # By name

lib/Google/RestApi/GmailApi1.pm  view on Meta::CPAN


Readonly our $Gmail_Endpoint => 'https://gmail.googleapis.com/gmail/v1/users';

sub new {
  my $class = shift;
  state $check = signature(
    bless => !!0,
    named => [
      api      => HasApi,
      user_id  => Str, { default => 'me' },
      endpoint => Str, { default => $Gmail_Endpoint },
    ],
  );
  return bless $check->(@_), $class;
}

sub api {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      uri     => Str, { optional => 1 },
      _extra_ => slurpy HashRef,
    ],
  );
  my $p = named_extra($check->(@_));
  my $uri = "$self->{endpoint}/$self->{user_id}/";
  $uri .= delete $p->{uri} if defined $p->{uri};
  return $self->{api}->api(%$p, uri => $uri);
}

sub message {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      id => Str, { optional => 1 },

lib/Google/RestApi/GmailApi1.pm  view on Meta::CPAN

 my $gmail_api = Google::RestApi::GmailApi1->new(api => $rest_api);

%args consists of:

=over

=item * C<api> L<Google::RestApi>: Required. A configured RestApi instance.

=item * C<user_id> <string>: Optional. The user ID (default 'me' for authenticated user).

=item * C<endpoint> <string>: Optional. Override the default Gmail API endpoint.

=back

=head2 api(%args)

Low-level method to make API calls. You would not normally call this directly
unless making a Google API call not currently supported by this framework.

%args consists of:

=over

=item * C<uri> <string>: Path segments to append to the Gmail endpoint.

=item * C<%args>: Additional arguments passed to L<Google::RestApi>'s api() (content, params, method, etc).

=back

Returns the response hash from the Google API.

=head2 message(%args)

Returns a Message object for the given message ID.

lib/Google/RestApi/SheetsApi4.pm  view on Meta::CPAN

Readonly our $Spreadsheet_Filter => "mimeType = 'application/vnd.google-apps.spreadsheet'";

sub new {
  my $class = shift;

  state $check = signature(
    bless => !!0,
    named => [
      api           => HasApi,                                           # the G::RestApi object that will be used to send http calls.
      drive         => HasMethods[qw(list)], { optional => 1 },  # a drive instnace, could be your own, defaults to G::R::DriveApi3.
      endpoint      => Str, { default => $Sheets_Endpoint },              # this gets tacked on to the api uri to reach the sheets endpoint.
    ],
  );
  my $self = $check->(@_);

  return bless $self, $class;
}

# this gets called by lower-level classes like worksheet and range objects. they
# will have passed thier own uri with params and possible body, we tack on the
# sheets endpoint and pass it up the line to G::RestApi to make the actual call.
sub api {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      uri     => Str, { default => '' },
      _extra_ => slurpy HashRef,              # just pass through any extra params to G::RestApi::api call.
    ],
  );
  my $p = named_extra($check->(@_));
  my $uri = $self->{endpoint};          # tack on the uri endpoint and pass the buck.
  $uri .= "/$p->{uri}" if $p->{uri};
  return $self->rest_api()->api(%$p, uri => $uri);
}

sub create_spreadsheet {
  my $self = shift;

  state $check = signature(
    bless => !!0,
    named => [

lib/Google/RestApi/SheetsApi4.pm  view on Meta::CPAN

=item C<api> L<<Google::RestApi>>: A reference to a configured L<Google::RestApi> instance.

=back

=item api(%args);

%args consists of:

=over

=item * C<uri> <path_segments_string>: Adds this path segment to the Sheets endpoint and calls the L<Google::RestApi>'s C<api> subroutine.

=item * C<%args>: Passes any extra arguments to the L<Google::RestApi>'s C<api> subroutine (content, params, method etc).

=back

This is essentially a pass-through method between lower-level Worksheet/Range objects and L<Google::RestApi>, where this method adds in the Sheets endpoint.
See <Google::RestApi::SheetsApi4::Worksheet>'s C<api> routine for how this is called. You would not normally call this directly unless you were making a Google API call not currently
supported by this API framework.

Returns the response hash from Google API.

=item create_spreadsheet(%args);

Creates a new spreadsheet.

%args consists of:

lib/Google/RestApi/SheetsApi4/Range.pm  view on Meta::CPAN


=back

You would not normally call this directly, you'd use Worksheet::range* methods to create the range object for you. It is recommended and safer to use
the Worksheet's methods to create ranges.

=item api(%args);

Calls the parent Worksheet's 'api' routine with the range added into the URI or content appropriately. This then get's passed to the
Spreadsheet's C<api> routine where the spreadsheet ID is tacked on to the URI. This then gets passed to the SheetsApi4's C<api>
routine where the Sheets endpoint is tacked on to the URI. This then gets passed to RestApi's api routine for actual execution.

You would not normally call this directly unless you were making a Google API call not currently supported by this API framework.

=item clear();

Clears the values using Google API's 'A1:clear' call.

=item refresh_values();

Immediately refreshes and returns the values from the spreadsheet.

lib/Google/RestApi/SheetsApi4/Spreadsheet.pm  view on Meta::CPAN

  $self = bless $self, $class;
  $self->{name} ||= $self->{title};
  delete $self->{title};

  $self->{id} || $self->{name} || $self->{uri} or LOGDIE "At least one of id, name, or uri must be specified";

  return $self;
}

# take the passed uri from worksheet/range/rangegroup etc, and tack on the spreadsheet id,
# then pass it up to G::R::SheetsApi4 which will tack on the endpoint.
sub api {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      uri     => Str, { default => '' },
      _extra_ => slurpy HashRef,             # we'll just pass the params/content etc up for processing.
    ],
  );
  my $p = named_extra($check->(@_));

lib/Google/RestApi/TasksApi1.pm  view on Meta::CPAN

use aliased 'Google::RestApi::TasksApi1::TaskList';

Readonly our $Tasks_Endpoint => 'https://tasks.googleapis.com/tasks/v1';

sub new {
  my $class = shift;
  state $check = signature(
    bless => !!0,
    named => [
      api      => HasApi,
      endpoint => Str, { default => $Tasks_Endpoint },
    ],
  );
  return bless $check->(@_), $class;
}

sub api {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      uri     => Str, { optional => 1 },
      _extra_ => slurpy HashRef,
    ],
  );
  my $p = named_extra($check->(@_));
  my $uri = "$self->{endpoint}/";
  $uri .= delete $p->{uri} if defined $p->{uri};
  return $self->{api}->api(%$p, uri => $uri);
}

sub task_list {
  my $self = shift;
  state $check = signature(
    bless => !!0,
    named => [
      id => Str, { optional => 1 },

lib/Google/RestApi/TasksApi1.pm  view on Meta::CPAN

Creates a new TasksApi1 instance.

 my $tasks_api = Google::RestApi::TasksApi1->new(api => $rest_api);

%args consists of:

=over

=item * C<api> L<Google::RestApi>: Required. A configured RestApi instance.

=item * C<endpoint> <string>: Optional. Override the default Tasks API endpoint.

=back

=head2 api(%args)

Low-level method to make API calls. You would not normally call this directly
unless making a Google API call not currently supported by this framework.

%args consists of:

=over

=item * C<uri> <string>: Path segments to append to the Tasks endpoint.

=item * C<%args>: Additional arguments passed to L<Google::RestApi>'s api() (content, params, method, etc).

=back

Returns the response hash from the Google API.

=head2 task_list(%args)

Returns a TaskList object for the given task list ID.

t/lib/Test/Unit/Utils.pm  view on Meta::CPAN

use File::Spec::Functions qw( catfile );
use Module::Load qw( load );

use Exporter qw(import);
our @EXPORT_OK = qw(
  mock_config_file mock_token_file
  mock_spreadsheet_name mock_spreadsheet_name2
  mock_worksheet_id mock_worksheet_name
  mock_rest_api mock_sheets_api mock_drive_api mock_calendar_api mock_gmail_api mock_tasks_api mock_docs_api
  mock_calendar_id mock_task_list_id mock_document_id
  drive_endpoint sheets_endpoint calendar_endpoint gmail_endpoint tasks_endpoint docs_endpoint
);
our %EXPORT_TAGS = (all => [ @EXPORT_OK ]);

sub mock_config_file { $ENV{GOOGLE_RESTAPI_CONFIG} ? $ENV{GOOGLE_RESTAPI_CONFIG} : catfile($FindBin::RealBin, qw(etc rest_config.yaml)); }
sub mock_token_file { catfile($FindBin::RealBin, qw(etc rest_config.token)); }
sub mock_spreadsheet_name { 'mock_spreadsheet1'; }
sub mock_spreadsheet_name2 { 'mock_spreadsheet2'; }
sub mock_worksheet_id { 0; }
sub mock_worksheet_name { 'Sheet1'; }

t/lib/Test/Unit/Utils.pm  view on Meta::CPAN

sub mock_sheets_api { _load_and_new('Google::RestApi::SheetsApi4', api => mock_rest_api(), @_); }
sub mock_drive_api { _load_and_new('Google::RestApi::DriveApi3', api => mock_rest_api(), @_); }
sub mock_calendar_api { _load_and_new('Google::RestApi::CalendarApi3', api => mock_rest_api(), @_); }
sub mock_gmail_api { _load_and_new('Google::RestApi::GmailApi1', api => mock_rest_api(), @_); }
sub mock_tasks_api { _load_and_new('Google::RestApi::TasksApi1', api => mock_rest_api(), @_); }
sub mock_docs_api { _load_and_new('Google::RestApi::DocsApi1', api => mock_rest_api(), @_); }
sub mock_calendar_id { 'mock_calendar_id@group.calendar.google.com'; }
sub mock_task_list_id { 'mock_task_list_id_12345'; }
sub mock_document_id { 'mock_document_id_12345'; }

sub drive_endpoint { $Google::RestApi::DriveApi3::Drive_Endpoint; }
sub sheets_endpoint { $Google::RestApi::SheetsApi4::Sheets_Endpoint; }
sub calendar_endpoint { $Google::RestApi::CalendarApi3::Calendar_Endpoint; }
sub gmail_endpoint { $Google::RestApi::GmailApi1::Gmail_Endpoint; }
sub tasks_endpoint { $Google::RestApi::TasksApi1::Tasks_Endpoint; }
sub docs_endpoint { $Google::RestApi::DocsApi1::Docs_Endpoint; }

sub _load_and_new {
  my $class = shift;
  load $class;
  return $class->new(@_);
}

1;

t/unit/Test/Google/RestApi/DriveApi3.pm  view on Meta::CPAN

sub _constructor : Tests(4) {
  my $self = shift;

  throws_ok sub { DriveApi3->new() },
    qr/api/i,
    'Constructor without api should throw';

  ok my $drive = DriveApi3->new(api => mock_rest_api()), 'Constructor should succeed';
  isa_ok $drive, DriveApi3, 'Constructor returns';
  can_ok $drive, qw(api list file about changes shared_drive list_drives
                    create_drive generate_ids empty_trash upload_endpoint);

  return;
}

sub file_factory : Tests(3) {
  my $self = shift;

  my $ss = $self->mock_spreadsheet();
  my $file_id = $ss->spreadsheet_id();

t/unit/Test/Google/RestApi/SheetsApi4/Spreadsheet.pm  view on Meta::CPAN


  return;
}

sub api : Tests(2) {
  my $self = shift;

  my $ss = $self->mock_spreadsheet();
  is_valid $ss->api(), HashRef, 'Get returns hashref';
  my $transaction = $ss->rest_api()->transaction();
  is $transaction->{request}->{uri}, sheets_endpoint() . "/" . $self->mock_spreadsheet_id(),
    'Request base spreadsheet uri string is valid';

  return;
}

sub spreadsheet_id : Tests(4) {
  my $self = shift;

  my $ms = $self->mock_spreadsheet();
  my $ms_id = $ms->spreadsheet_id;



( run in 2.981 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )