diff --git a/requirements/base.txt b/requirements/base.txt index 60c8a29b14..d947ecc626 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -11,5 +11,6 @@ docker>=3.3.0 dateparser~=0.7 python-dateutil~=2.6 pathlib2~=2.3.2; python_version<"3.4" -requests~=2.20.0 -aws_lambda_builders==0.0.4 +requests==2.20.1 +aws_lambda_builders==0.0.5 +serverlessrepo==0.1.5 diff --git a/requirements/dev.txt b/requirements/dev.txt index af480e2bf1..a920601b7c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -10,7 +10,6 @@ pylint==1.7.2 pytest==3.0.7 py==1.4.33 mock==2.0.0 -requests==2.20.0 parameterized==0.6.1 pathlib2==2.3.2; python_version<"3.4" futures==3.2.0; python_version<"3.2.3" diff --git a/samcli/__init__.py b/samcli/__init__.py index aaedd88760..0151e0ffe5 100644 --- a/samcli/__init__.py +++ b/samcli/__init__.py @@ -2,4 +2,4 @@ SAM CLI version """ -__version__ = '0.9.0' +__version__ = '0.10.0' diff --git a/samcli/cli/command.py b/samcli/cli/command.py index a85c97f808..84fd9c0aad 100644 --- a/samcli/cli/command.py +++ b/samcli/cli/command.py @@ -16,7 +16,8 @@ "samcli.commands.deploy", "samcli.commands.package", "samcli.commands.logs", - "samcli.commands.build" + "samcli.commands.build", + "samcli.commands.publish" } diff --git a/samcli/commands/local/cli_common/invoke_context.py b/samcli/commands/local/cli_common/invoke_context.py index 56ec5f1282..9e6bc99371 100644 --- a/samcli/commands/local/cli_common/invoke_context.py +++ b/samcli/commands/local/cli_common/invoke_context.py @@ -7,6 +7,7 @@ import os import samcli.lib.utils.osutils as osutils +from samcli.lib.utils.stream_writer import StreamWriter from samcli.commands.local.lib.local_lambda import LocalLambdaRunner from samcli.commands.local.lib.debug_context import DebugContext from samcli.local.lambdafn.runtime import LambdaRuntime @@ -202,26 +203,28 @@ def local_lambda_runner(self): @property def stdout(self): """ - Returns a stdout stream to output Lambda function logs to + Returns stream writer for stdout to output Lambda function logs to - :return File like object: Stream where the output of the function is sent to + Returns + ------- + samcli.lib.utils.stream_writer.StreamWriter + Stream writer for stdout """ - if self._log_file_handle: - return self._log_file_handle - - return osutils.stdout() + stream = self._log_file_handle if self._log_file_handle else osutils.stdout() + return StreamWriter(stream, self._is_debugging) @property def stderr(self): """ - Returns stderr stream to output Lambda function errors to + Returns stream writer for stderr to output Lambda function errors to - :return File like object: Stream where the stderr of the function is sent to + Returns + ------- + samcli.lib.utils.stream_writer.StreamWriter + Stream writer for stderr """ - if self._log_file_handle: - return self._log_file_handle - - return osutils.stderr() + stream = self._log_file_handle if self._log_file_handle else osutils.stderr() + return StreamWriter(stream, self._is_debugging) @property def template(self): @@ -256,6 +259,10 @@ def parameter_overrides(self): return self._parameter_overrides + @property + def _is_debugging(self): + return bool(self._debug_context) + @staticmethod def _get_template_data(template_file): """ diff --git a/samcli/commands/local/lib/debug_context.py b/samcli/commands/local/lib/debug_context.py index a7c435f239..27056077dd 100644 --- a/samcli/commands/local/lib/debug_context.py +++ b/samcli/commands/local/lib/debug_context.py @@ -1,7 +1,6 @@ """ Information and debug options for a specific runtime. """ -import os class DebugContext(object): @@ -14,8 +13,6 @@ def __init__(self, self.debug_port = debug_port self.debugger_path = debugger_path self.debug_args = debug_args - if self.debug_port: - os.environ["PYTHONUNBUFFERED"] = "1" def __bool__(self): return bool(self.debug_port) diff --git a/samcli/commands/local/lib/local_lambda.py b/samcli/commands/local/lib/local_lambda.py index 6a3e00f4c7..06aef3b91f 100644 --- a/samcli/commands/local/lib/local_lambda.py +++ b/samcli/commands/local/lib/local_lambda.py @@ -53,11 +53,21 @@ def invoke(self, function_name, event, stdout=None, stderr=None): This function will block until either the function completes or times out. - :param string function_name: Name of the Lambda function to invoke - :param string event: Event data passed to the function. Must be a valid JSON String. - :param io.BaseIO stdout: Stream to write the output of the Lambda function to. - :param io.BaseIO stderr: Stream to write the Lambda runtime logs to. - :raises FunctionNotfound: When we cannot find a function with the given name + Parameters + ---------- + function_name str + Name of the Lambda function to invoke + event str + Event data passed to the function. Must be a valid JSON String. + stdout samcli.lib.utils.stream_writer.StreamWriter + Stream writer to write the output of the Lambda function to. + stderr samcli.lib.utils.stream_writer.StreamWriter + Stream writer to write the Lambda runtime logs to. + + Raises + ------ + FunctionNotfound + When we cannot find a function with the given name """ # Generate the correct configuration based on given inputs diff --git a/samcli/commands/publish/__init__.py b/samcli/commands/publish/__init__.py new file mode 100644 index 0000000000..27bc7bc98e --- /dev/null +++ b/samcli/commands/publish/__init__.py @@ -0,0 +1,4 @@ +"""`sam publish` command.""" + +# Expose the cli object here +from .command import cli # noqa diff --git a/samcli/commands/publish/command.py b/samcli/commands/publish/command.py new file mode 100644 index 0000000000..4697d5b1b2 --- /dev/null +++ b/samcli/commands/publish/command.py @@ -0,0 +1,135 @@ +"""CLI command for "publish" command.""" + +import json + +import click +import boto3 +from botocore.exceptions import ClientError +from serverlessrepo import publish_application +from serverlessrepo.publish import CREATE_APPLICATION +from serverlessrepo.exceptions import ServerlessRepoError + +from samcli.cli.main import pass_context, common_options as cli_framework_options, aws_creds_options +from samcli.commands._utils.options import template_common_option +from samcli.commands._utils.template import get_template_data +from samcli.commands.exceptions import UserException + +HELP_TEXT = """ +Use this command to publish a packaged AWS SAM template to +the AWS Serverless Application Repository to share within your team, +across your organization, or with the community at large.\n +\b +This command expects the template's Metadata section to contain an +AWS::ServerlessRepo::Application section with application metadata +for publishing. For more details on this metadata section, see +https://docs.aws.amazon.com/serverlessrepo/latest/devguide/serverless-app-publishing-applications.html +\b +Examples +-------- +To publish an application +$ sam publish -t packaged.yaml --region +""" +SHORT_HELP = "Publish a packaged AWS SAM template to the AWS Serverless Application Repository." +SERVERLESSREPO_CONSOLE_URL = "https://console.aws.amazon.com/serverlessrepo/home?region={}#/published-applications/{}" + + +@click.command("publish", help=HELP_TEXT, short_help=SHORT_HELP) +@template_common_option +@aws_creds_options +@cli_framework_options +@pass_context +def cli(ctx, template): + # All logic must be implemented in the ``do_cli`` method. This helps with easy unit testing + + do_cli(ctx, template) # pragma: no cover + + +def do_cli(ctx, template): + """Publish the application based on command line inputs.""" + try: + template_data = get_template_data(template) + except ValueError as ex: + click.secho("Publish Failed", fg='red') + raise UserException(str(ex)) + + try: + publish_output = publish_application(template_data) + click.secho("Publish Succeeded", fg="green") + click.secho(_gen_success_message(publish_output), fg="yellow") + except ServerlessRepoError as ex: + click.secho("Publish Failed", fg='red') + raise UserException(str(ex)) + except ClientError as ex: + click.secho("Publish Failed", fg='red') + raise _wrap_s3_uri_exception(ex) + + application_id = publish_output.get('application_id') + _print_console_link(ctx.region, application_id) + + +def _gen_success_message(publish_output): + """ + Generate detailed success message for published applications. + + Parameters + ---------- + publish_output : dict + Output from serverlessrepo publish_application + + Returns + ------- + str + Detailed success message + """ + application_id = publish_output.get('application_id') + details = json.dumps(publish_output.get('details'), indent=2) + + if CREATE_APPLICATION in publish_output.get('actions'): + return "Created new application with the following metadata:\n{}".format(details) + + return 'The following metadata of application "{}" has been updated:\n{}'.format(application_id, details) + + +def _print_console_link(region, application_id): + """ + Print link for the application in AWS Serverless Application Repository console. + + Parameters + ---------- + region : str + AWS region name + application_id : str + The Amazon Resource Name (ARN) of the application + + """ + if not region: + region = boto3.Session().region_name + + console_link = SERVERLESSREPO_CONSOLE_URL.format(region, application_id.replace('/', '~')) + msg = "Click the link below to view your application in AWS console:\n{}".format(console_link) + click.secho(msg, fg="yellow") + + +def _wrap_s3_uri_exception(ex): + """ + Wrap invalid S3 URI exception with a better error message. + + Parameters + ---------- + ex : ClientError + boto3 exception + + Returns + ------- + Exception + UserException if found invalid S3 URI or ClientError + """ + error_code = ex.response.get('Error').get('Code') + message = ex.response.get('Error').get('Message') + + if error_code == 'BadRequestException' and "Invalid S3 URI" in message: + return UserException( + "Your SAM template contains invalid S3 URIs. Please make sure that you have uploaded application " + "artifacts to S3 by packaging the template: 'sam package --template-file '.") + + return ex diff --git a/samcli/lib/build/app_builder.py b/samcli/lib/build/app_builder.py index d1482c7f6a..2a15fa1ef4 100644 --- a/samcli/lib/build/app_builder.py +++ b/samcli/lib/build/app_builder.py @@ -57,6 +57,12 @@ def _get_workflow_config(runtime): dependency_manager="npm", application_framework=None, manifest_name="package.json") + elif runtime.startswith("ruby"): + return config( + language="ruby", + dependency_manager="bundler", + application_framework=None, + manifest_name="Gemfile") else: raise UnsupportedRuntimeException("'{}' runtime is not supported".format(runtime)) diff --git a/samcli/lib/utils/stream_writer.py b/samcli/lib/utils/stream_writer.py new file mode 100644 index 0000000000..223e4d90b8 --- /dev/null +++ b/samcli/lib/utils/stream_writer.py @@ -0,0 +1,37 @@ +""" +This class acts like a wrapper around output streams to provide any flexibility with output we need +""" + + +class StreamWriter(object): + + def __init__(self, stream, auto_flush=False): + """ + Instatiates new StreamWriter to the specified stream + + Parameters + ---------- + stream io.RawIOBase + Stream to wrap + auto_flush bool + Whether to autoflush the stream upon writing + """ + self._stream = stream + self._auto_flush = auto_flush + + def write(self, output): + """ + Writes specified text to the underlying stream + + Parameters + ---------- + output bytes-like object + Bytes to write + """ + self._stream.write(output) + + if self._auto_flush: + self._stream.flush() + + def flush(self): + self._stream.flush() diff --git a/samcli/local/apigw/local_apigw_service.py b/samcli/local/apigw/local_apigw_service.py index 6096085231..fb71e0ed6a 100644 --- a/samcli/local/apigw/local_apigw_service.py +++ b/samcli/local/apigw/local_apigw_service.py @@ -7,6 +7,7 @@ from flask import Flask, request from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser, CaseInsensitiveDict +from samcli.lib.utils.stream_writer import StreamWriter from samcli.local.lambdafn.exceptions import FunctionNotFound from samcli.local.events.api_event import ContextIdentity, RequestContext, ApiGatewayLambdaEvent from .service_error_responses import ServiceErrorResponses @@ -40,16 +41,22 @@ def __init__(self, routing_list, lambda_runner, static_dir=None, port=None, host """ Creates an ApiGatewayService - :param list(ApiGatewayCallModel) routing_list: A list of the Model that represent - the service paths to create. - :param samcli.commands.local.lib.local_lambda.LocalLambdaRunner lambda_runner: The Lambda runner class capable - of invoking the function - :param str static_dir: Directory from which to serve static files - :param int port: Optional. port for the service to start listening on - Defaults to 3000 - :param str host: Optional. host to start the service on - Defaults to '127.0.0.1 - :param io.BaseIO stderr: Optional stream where the stderr from Docker container should be written to + Parameters + ---------- + routing_list list(ApiGatewayCallModel) + A list of the Model that represent the service paths to create. + lambda_runner samcli.commands.local.lib.local_lambda.LocalLambdaRunner + The Lambda runner class capable of invoking the function + static_dir str + Directory from which to serve static files + port int + Optional. port for the service to start listening on + Defaults to 3000 + host str + Optional. host to start the service on + Defaults to '127.0.0.1 + stderr samcli.lib.utils.stream_writer.StreamWriter + Optional stream writer where the stderr from Docker container should be written to """ super(LocalApigwService, self).__init__(lambda_runner.is_debugging(), port=port, host=host) self.routing_list = routing_list @@ -123,9 +130,14 @@ def _request_handler(self, **kwargs): * We then transform the response or errors we get from the Invoke and return the data back to the caller - :param kwargs dict: Keyword Args that are passed to the function from Flask. This happens when we have - Path Parameters. - :return: Response object + Parameters + ---------- + kwargs dict + Keyword Args that are passed to the function from Flask. This happens when we have path parameters + + Returns + ------- + Response object """ route = self._get_current_route(request) @@ -135,9 +147,10 @@ def _request_handler(self, **kwargs): return ServiceErrorResponses.lambda_failure_response() stdout_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream, self.is_debugging) try: - self.lambda_runner.invoke(route.function_name, event, stdout=stdout_stream, stderr=self.stderr) + self.lambda_runner.invoke(route.function_name, event, stdout=stdout_stream_writer, stderr=self.stderr) except FunctionNotFound: return ServiceErrorResponses.lambda_not_found_response() diff --git a/samcli/local/docker/container.py b/samcli/local/docker/container.py index 466a139d92..41121b7c22 100644 --- a/samcli/local/docker/container.py +++ b/samcli/local/docker/container.py @@ -168,9 +168,10 @@ def start(self, input_data=None): It waits for the container to complete, fetches both stdout and stderr logs and returns through the given streams. - :param input_data: Optional. Input data sent to the container through container's stdin. - :param io.StringIO stdout: Optional. IO Stream to that receives stdout text from container. - :param io.StringIO stderr: Optional. IO Stream that receives stderr text from container + Parameters + ---------- + input_data + Optional. Input data sent to the container through container's stdin. """ if input_data: @@ -233,10 +234,10 @@ def _write_container_output(output_itr, stdout=None, stderr=None): ---------- output_itr: Iterator Iterator returned by the Docker Attach command - stdout: io.BaseIO, optional - Stream to write stdout data from Container into - stderr: io.BaseIO, optional - Stream to write stderr data from the Container into + stdout: samcli.lib.utils.stream_writer.StreamWriter, optional + Stream writer to write stdout data from Container into + stderr: samcli.lib.utils.stream_writer.StreamWriter, optional + Stream writer to write stderr data from the Container into """ # Iterator returns a tuple of (frame_type, data) where the frame type determines which stream we write output diff --git a/samcli/local/docker/manager.py b/samcli/local/docker/manager.py index 5d2fcd7f3c..2080acbb08 100644 --- a/samcli/local/docker/manager.py +++ b/samcli/local/docker/manager.py @@ -3,11 +3,13 @@ """ import logging -import sys +import sys import docker import requests +from samcli.lib.utils.stream_writer import StreamWriter + LOG = logging.getLogger(__name__) @@ -107,11 +109,20 @@ def pull_image(self, image_name, stream=None): """ Ask Docker to pull the container image with given name. - :param string image_name: Name of the image - :param stream: Optional stream to write output to. Defaults to stderr - :raises DockerImagePullFailedException: If the Docker image was not available in the server + Parameters + ---------- + image_name str + Name of the image + stream samcli.lib.utils.stream_writer.StreamWriter + Optional stream writer to output to. Defaults to stderr + + Raises + ------ + DockerImagePullFailedException + If the Docker image was not available in the server """ - stream = stream or sys.stderr + stream_writer = stream or StreamWriter(sys.stderr) + try: result_itr = self.docker_client.api.pull(image_name, stream=True, decode=True) except docker.errors.APIError as ex: @@ -119,16 +130,16 @@ def pull_image(self, image_name, stream=None): raise DockerImagePullFailedException(str(ex)) # io streams, especially StringIO, work only with unicode strings - stream.write(u"\nFetching {} Docker container image...".format(image_name)) + stream_writer.write(u"\nFetching {} Docker container image...".format(image_name)) # Each line contains information on progress of the pull. Each line is a JSON string for _ in result_itr: # For every line, print a dot to show progress - stream.write(u'.') - stream.flush() + stream_writer.write(u'.') + stream_writer.flush() # We are done. Go to the next line - stream.write(u"\n") + stream_writer.write(u"\n") def has_image(self, image_name): """ diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/README.md index f13c3e7088..1fee2ad2f0 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/README.md @@ -1,6 +1,6 @@ # Cookiecutter SAM for Ruby Lambda functions -This is a [Cookiecutter](https://github.com/audreyr/cookiecutter) template to create a Serverless Hello World App based on Serverless Application Model (SAM) and Python. +This is a [Cookiecutter](https://github.com/audreyr/cookiecutter) template to create a Serverless Hello World App based on Serverless Application Model (SAM) and Ruby. ## Recommendations diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/Gemfile b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/Gemfile index 3abba1b8b3..b566789dfb 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/Gemfile +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/Gemfile @@ -1,3 +1,8 @@ source "https://rubygems.org" gem "httparty" + +group :test do + gem "test-unit" + gem "mocha" +end diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/README.md b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/README.md index 919fa08da2..f220a6b515 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/README.md +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/README.md @@ -7,7 +7,8 @@ This is a sample template for {{ cookiecutter.project_name }} - Below is a brief ├── README.md <-- This instructions file ├── hello_world <-- Source code for a lambda function │ ├── app.rb <-- Lambda function code -├── Gemfile <-- Ruby dependencies +│ ├── Gemfile <-- Ruby function dependencies +├── Gemfile <-- Ruby test/documentation dependencies ├── template.yaml <-- SAM template └── tests <-- Unit tests └── unit @@ -31,27 +32,27 @@ Setup Ruby Version Manager from [Ruby Version Manager](http://rvm.io/) Run following commands ```bash -rvm install ruby-2.5.0 -rvm use ruby-2.5.0 -rvm --default use 2.5.0 +rvm install ruby-2.5.3 +rvm use ruby-2.5.3 +rvm --default use 2.5.3 ``` +### Building the Project -### Installing dependencies +```sam-app``` comes with a Gemfile that defines the requirements and manages installing them. The `sam build` command will install the dependencies in your function Gemfile and vendor it for deployment. -```sam-app``` comes with a Gemfile that defines the requirements and manages installing them. - -```bash -gem install bundler -bundle install -bundle install --deployment --path hello_world/vendor/bundle +``` +sam build ``` -* Step 1 installs ```bundler```which provides a consistent environment for Ruby projects by tracking and installing the exact gems and versions that are needed. -* Step 2 creates a Gemfile.lock that locks down the versions and creates the full dependency closure. -* Step 3 installs the gems to ```hello_world/vendor/bundle```. +If your dependencies contain native modules that need to be compiled specifically for the operating system running on AWS Lambda, use this command to build inside a Lambda-like Docker container instead: -**NOTE:** As you change your dependencies during development you'll need to make sure these steps are repeated in order to execute your Lambda and/or API Gateway locally. +``` +sam build --use-container +``` +By default, this command writes built artifacts to .aws-sam/build folder. + +**NOTE:** As you change your dependencies during development you'll need to run `sam build` again in order to execute your Lambda and/or API Gateway locally. ### Local development @@ -124,12 +125,10 @@ aws cloudformation describe-stacks \ ## Testing -We use [Mocha](http://gofreerange.com/mocha/docs) for testing our code and you can install it using gem: ``gem install mocha`` - -Next, we run our initial unit tests: +Run our initial unit tests: ```bash -ruby tests/unit/test_hello.rb +ruby tests/unit/test_handler.rb ``` **NOTE**: It is recommended to use a Ruby Version Manager to manage, and work with multiple ruby environments from interpreters to sets of gems @@ -137,7 +136,7 @@ ruby tests/unit/test_hello.rb ## AWS CLI commands -AWS CLI commands to package, deploy and describe outputs defined within the cloudformation stack: +AWS CLI commands to package, deploy and describe outputs defined within the cloudformation stack after building: ```bash sam package \ diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/Gemfile b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/Gemfile new file mode 100644 index 0000000000..3abba1b8b3 --- /dev/null +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "httparty" diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/app.rb b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/app.rb index 89f645d0ff..963182dd7c 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/app.rb +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/hello_world/app.rb @@ -73,11 +73,11 @@ def lambda_handler(event:, context:) raise error end - return { - :statusCode => response.code, - :body => { - :message => "Hello World!", - :location => response.body + { + statusCode: response.code, + body: { + message: "Hello World!", + location: response.body }.to_json } end diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/template.yaml b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/template.yaml index 5617c03472..c1bcb7ba61 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/template.yaml +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/template.yaml @@ -4,7 +4,7 @@ Description: > {{ cookiecutter.project_name }} Sample SAM Template for {{ cookiecutter.project_name }} - + # More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst Globals: Function: diff --git a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/tests/unit/test_handler.rb b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/tests/unit/test_handler.rb index 8ae3a2595e..df66b969a1 100644 --- a/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/tests/unit/test_handler.rb +++ b/samcli/local/init/templates/cookiecutter-aws-sam-hello-ruby/{{cookiecutter.project_name}}/tests/unit/test_handler.rb @@ -1,12 +1,12 @@ require 'json' require 'test/unit' require 'mocha/test_unit' + require_relative '../../hello_world/app' class HelloWorldTest < Test::Unit::TestCase - - def setup - @event = { + def event + { body: 'eyJ0ZXN0IjoiYm9keSJ9', resource: '/{proxy+}', path: '/path/to/resource', @@ -22,24 +22,24 @@ def setup baz: 'qux' }, headers: { - Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', - Accept-Encoding: 'gzip, deflate, sdch', - Accept-Language: 'en-US,en;q=0.8', - Cache-Control: 'max-age=0', - CloudFront-Forwarded-Proto: 'https', - CloudFront-Is-Desktop-Viewer: 'true', - CloudFront-Is-Mobile-Viewer: 'false', - CloudFront-Is-SmartTV-Viewer: 'false', - CloudFront-Is-Tablet-Viewer: 'false', - CloudFront-Viewer-Country: 'US', - Host: '1234567890.execute-api.us-east-1.amazonaws.com', - Upgrade-Insecure-Requests: '1', - User-Agent: 'Custom User Agent String', - Via: '1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)', - X-Amz-Cf-Id: 'cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==', - X-Forwarded-For: '127.0.0.1, 127.0.0.2', - X-Forwarded-Port: '443', - X-Forwarded-Proto: 'https' + 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', + 'Accept-Encoding' => 'gzip, deflate, sdch', + 'Accept-Language' => 'en-US,en;q=0.8', + 'Cache-Control' => 'max-age=0', + 'CloudFront-Forwarded-Proto' => 'https', + 'CloudFront-Is-Desktop-Viewer' => 'true', + 'CloudFront-Is-Mobile-Viewer' => 'false', + 'CloudFront-Is-SmartTV-Viewer' => 'false', + 'CloudFront-Is-Tablet-Viewer' => 'false', + 'CloudFront-Viewer-Country' => 'US', + 'Host' => '1234567890.execute-api.us-east-1.amazonaws.com', + 'Upgrade-Insecure-Requests' => '1', + 'User-Agent' => 'Custom User Agent String', + 'Via' => '1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)', + 'X-Amz-Cf-Id' => 'cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==', + 'X-Forwarded-For' => '127.0.0.1, 127.0.0.2', + 'X-Forwarded-Port' => '443', + 'X-Forwarded-Proto' => 'https' }, requestContext: { accountId: '123456789012', @@ -49,17 +49,17 @@ def setup requestTime: '09/Apr/2015:12:34:56 +0000', requestTimeEpoch: 1428582896000, identity: { - cognitoIdentityPoolId: "null", - accountId: "null", - cognitoIdentityId: "null", - caller: "null", - accessKey: "null", + cognitoIdentityPoolId: 'null', + accountId: 'null', + cognitoIdentityId: 'null', + caller: 'null', + accessKey: 'null', sourceIp: '127.0.0.1', - cognitoAuthenticationType: "null", - cognitoAuthenticationProvider: "null", - userArn: "null", + cognitoAuthenticationType: 'null', + cognitoAuthenticationProvider: 'null', + userArn: 'null', userAgent: 'Custom User Agent String', - user: "null" + user: 'null' }, path: '/prod/path/to/resource', resourcePath: '/{proxy+}', @@ -68,24 +68,27 @@ def setup protocol: 'HTTP/1.1' } } + end - @mock_response = { - :statusCode => 200, - :body => { - message: "Hello World!", - location: "1.1.1.1" + def mock_response + Object.new.tap do |mock| + mock.expects(:code).returns(200) + mock.expects(:body).returns('1.1.1.1') + end + end + + def expected_result + { + statusCode: 200, + body: { + message: 'Hello World!', + location: '1.1.1.1' }.to_json } - end def test_lambda_handler - expects(:lambda_handler).with(event:@event, context:"").returns(@mock_response) - response = lambda_handler(event:@event, context:"") - json_body = JSON.parse(response[:body]) - - assert_equal(200, response[:statusCode]) - assert_equal("Hello World!", json_body["message"]) - assert_equal("1.1.1.1", json_body["location"]) + HTTParty.expects(:get).with('http://checkip.amazonaws.com/').returns(mock_response) + assert_equal(lambda_handler(event: event, context: ''), expected_result) end end diff --git a/samcli/local/lambda_service/local_lambda_invoke_service.py b/samcli/local/lambda_service/local_lambda_invoke_service.py index b080e181b3..7c684f9159 100644 --- a/samcli/local/lambda_service/local_lambda_invoke_service.py +++ b/samcli/local/lambda_service/local_lambda_invoke_service.py @@ -6,7 +6,7 @@ from flask import Flask, request - +from samcli.lib.utils.stream_writer import StreamWriter from samcli.local.services.base_local_service import BaseLocalService, LambdaOutputParser, CaseInsensitiveDict from samcli.local.lambdafn.exceptions import FunctionNotFound from .lambda_error_responses import LambdaErrorResponses @@ -139,9 +139,10 @@ def _invoke_request_handler(self, function_name): request_data = request_data.decode('utf-8') stdout_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream, self.is_debugging) try: - self.lambda_runner.invoke(function_name, request_data, stdout=stdout_stream, stderr=self.stderr) + self.lambda_runner.invoke(function_name, request_data, stdout=stdout_stream_writer, stderr=self.stderr) except FunctionNotFound: LOG.debug('%s was not found to invoke.', function_name) return LambdaErrorResponses.resource_not_found(function_name) diff --git a/tests/functional/commands/local/lib/test_local_lambda.py b/tests/functional/commands/local/lib/test_local_lambda.py index 4a2cf52904..bf68b3c7ea 100644 --- a/tests/functional/commands/local/lib/test_local_lambda.py +++ b/tests/functional/commands/local/lib/test_local_lambda.py @@ -8,6 +8,7 @@ import shutil import logging +from samcli.lib.utils.stream_writer import StreamWriter from samcli.commands.local.lib import provider from samcli.commands.local.lib.local_lambda import LocalLambdaRunner from samcli.local.lambdafn.runtime import LambdaRuntime @@ -82,7 +83,10 @@ def test_must_invoke(self): stdout_stream = io.BytesIO() stderr_stream = io.BytesIO() - runner.invoke(self.function_name, input_event, stdout=stdout_stream, stderr=stderr_stream) + + stdout_stream_writer = StreamWriter(stdout_stream) + stderr_stream_writer = StreamWriter(stderr_stream) + runner.invoke(self.function_name, input_event, stdout=stdout_stream_writer, stderr=stderr_stream_writer) # stderr is where the Lambda container runtime logs are available. It usually contains requestId, start time # etc. So it is non-zero in size @@ -93,4 +97,4 @@ def test_must_invoke(self): for key, value in expected_env_vars.items(): self.assertTrue(key in actual_output, "Key '{}' must be in function output".format(key)) - self.assertEquals(actual_output.get(key), value) + self.assertEqual(actual_output.get(key), value) diff --git a/tests/functional/local/apigw/test_local_apigw_service.py b/tests/functional/local/apigw/test_local_apigw_service.py index ce4c8016cb..96f442fc83 100644 --- a/tests/functional/local/apigw/test_local_apigw_service.py +++ b/tests/functional/local/apigw/test_local_apigw_service.py @@ -630,7 +630,7 @@ def make_service(list_of_routes, function_provider, cwd): def make_service_response(port, method, scheme, resourcePath, resolvedResourcePath, pathParameters=None, body=None, headers=None, queryParams=None, isBase64Encoded=False): - response_str = '{"httpMethod": "GET", "body": null, "resource": "/something/{event}", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/something/{event}", "httpMethod": "GET", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "prod", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/something/{event}"}, "queryStringParameters": null, "headers": {"Host": "0.0.0.0:33651", "User-Agent": "python-requests/2.20.0", "Accept-Encoding": "gzip, deflate", "Accept": "*/*", "Connection": "keep-alive"}, "pathParameters": {"event": "event1"}, "stageVariables": null, "path": "/something/event1", "isBase64Encoded": false}' # NOQA + response_str = '{"httpMethod": "GET", "body": null, "resource": "/something/{event}", "requestContext": {"resourceId": "123456", "apiId": "1234567890", "resourcePath": "/something/{event}", "httpMethod": "GET", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "accountId": "123456789012", "stage": "prod", "identity": {"apiKey": null, "userArn": null, "cognitoAuthenticationType": null, "caller": null, "userAgent": "Custom User Agent String", "user": null, "cognitoIdentityPoolId": null, "cognitoAuthenticationProvider": null, "sourceIp": "127.0.0.1", "accountId": null}, "extendedRequestId": null, "path": "/something/{event}"}, "queryStringParameters": null, "headers": {"Host": "0.0.0.0:33651", "User-Agent": "python-requests/2.20.1", "Accept-Encoding": "gzip, deflate", "Accept": "*/*", "Connection": "keep-alive"}, "pathParameters": {"event": "event1"}, "stageVariables": null, "path": "/something/event1", "isBase64Encoded": false}' # NOQA response = json.loads(response_str) if body: diff --git a/tests/functional/local/docker/test_lambda_container.py b/tests/functional/local/docker/test_lambda_container.py index 2e42253f98..28813f6e8b 100644 --- a/tests/functional/local/docker/test_lambda_container.py +++ b/tests/functional/local/docker/test_lambda_container.py @@ -10,6 +10,7 @@ from contextlib import contextmanager from unittest import TestCase +from samcli.lib.utils.stream_writer import StreamWriter from samcli.commands.local.lib.debug_context import DebugContext from tests.functional.function_code import nodejs_lambda from samcli.local.docker.lambda_container import LambdaContainer @@ -130,13 +131,17 @@ def test_function_result_is_available_in_stdout_and_logs_in_stderr(self): layer_downloader = LayerDownloader("./", "./") image_builder = LambdaImage(layer_downloader, False, False) container = LambdaContainer(self.runtime, self.handler, self.code_dir, self.layers, image_builder) + stdout_stream = io.BytesIO() stderr_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream) + stderr_stream_writer = StreamWriter(stderr_stream) + with self._create(container): container.start() - container.wait_for_logs(stdout=stdout_stream, stderr=stderr_stream) + container.wait_for_logs(stdout=stdout_stream_writer, stderr=stderr_stream_writer) function_output = stdout_stream.getvalue() function_stderr = stderr_stream.getvalue() diff --git a/tests/functional/local/lambdafn/test_runtime.py b/tests/functional/local/lambdafn/test_runtime.py index 17f825a194..ae767cd27c 100644 --- a/tests/functional/local/lambdafn/test_runtime.py +++ b/tests/functional/local/lambdafn/test_runtime.py @@ -11,6 +11,7 @@ from parameterized import parameterized, param from tests.functional.function_code import nodejs_lambda, make_zip, ECHO_CODE, SLEEP_CODE, GET_ENV_VAR +from samcli.lib.utils.stream_writer import StreamWriter from samcli.local.docker.manager import ContainerManager from samcli.local.lambdafn.runtime import LambdaRuntime from samcli.local.lambdafn.config import FunctionConfig @@ -59,7 +60,9 @@ def test_echo_function(self): timeout=timeout) stdout_stream = io.BytesIO() - self.runtime.invoke(config, input_event, stdout=stdout_stream) + stdout_stream_writer = StreamWriter(stdout_stream) + + self.runtime.invoke(config, input_event, stdout=stdout_stream_writer) actual_output = stdout_stream.getvalue() self.assertEquals(actual_output.strip(), expected_output) @@ -69,6 +72,8 @@ def test_function_timeout(self): Setup a short timeout and verify that the container is stopped """ stdout_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream) + timeout = 1 # 1 second timeout sleep_seconds = 20 # Ask the function to sleep for 20 seconds @@ -81,7 +86,7 @@ def test_function_timeout(self): # Measure the actual duration of execution start = timer() - self.runtime.invoke(config, str(sleep_seconds), stdout=stdout_stream) + self.runtime.invoke(config, str(sleep_seconds), stdout=stdout_stream_writer) end = timer() # Make sure that the wall clock duration is around the ballpark of timeout value @@ -118,7 +123,9 @@ def test_echo_function_with_zip_file(self, file_name_extension): timeout=timeout) stdout_stream = io.BytesIO() - self.runtime.invoke(config, input_event, stdout=stdout_stream) + stdout_stream_writer = StreamWriter(stdout_stream) + + self.runtime.invoke(config, input_event, stdout=stdout_stream_writer) actual_output = stdout_stream.getvalue() self.assertEquals(actual_output.strip(), expected_output) @@ -129,7 +136,10 @@ def test_check_environment_variables(self): timeout = 30 input_event = "" + stdout_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream) + expected_output = { "AWS_SAM_LOCAL": "true", "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "1024", @@ -159,7 +169,7 @@ def test_check_environment_variables(self): config.env_vars.variables = variables config.env_vars.aws_creds = aws_creds - self.runtime.invoke(config, input_event, stdout=stdout_stream) + self.runtime.invoke(config, input_event, stdout=stdout_stream_writer) actual_output = json.loads(stdout_stream.getvalue().strip().decode('utf-8')) # Output is a JSON String. Deserialize. @@ -202,6 +212,8 @@ def _invoke_sleep(self, timeout, sleep_duration, check_stdout, exceptions=None): print("Invoking function " + name) try: stdout_stream = io.BytesIO() + stdout_stream_writer = StreamWriter(stdout_stream) + config = FunctionConfig(name=name, runtime=RUNTIME, handler=HANDLER, @@ -210,7 +222,7 @@ def _invoke_sleep(self, timeout, sleep_duration, check_stdout, exceptions=None): memory=1024, timeout=timeout) - self.runtime.invoke(config, sleep_duration, stdout=stdout_stream) + self.runtime.invoke(config, sleep_duration, stdout=stdout_stream_writer) actual_output = stdout_stream.getvalue().strip() # Must output the sleep duration if check_stdout: self.assertEquals(actual_output.decode('utf-8'), str(sleep_duration)) diff --git a/tests/integration/buildcmd/test_build_cmd.py b/tests/integration/buildcmd/test_build_cmd.py index fe7c3d30a4..62c812a42a 100644 --- a/tests/integration/buildcmd/test_build_cmd.py +++ b/tests/integration/buildcmd/test_build_cmd.py @@ -4,6 +4,10 @@ import json import logging +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path from parameterized import parameterized from samcli.yamlhelper import yaml_parse @@ -192,3 +196,75 @@ def _verify_resource_property(self, template_path, logical_id, property, expecte with open(template_path, 'r') as fp: template_dict = yaml_parse(fp.read()) self.assertEquals(expected_value, template_dict["Resources"][logical_id]["Properties"][property]) + + +class TestBuildCommand_RubyFunctions(BuildIntegBase): + + EXPECTED_FILES_GLOBAL_MANIFEST = set() + EXPECTED_FILES_PROJECT_MANIFEST = {'app.rb'} + EXPECTED_RUBY_GEM = 'httparty' + + FUNCTION_LOGICAL_ID = "Function" + + @parameterized.expand([ + ("ruby2.5", False), + ("ruby2.5", "use_container") + ]) + def test_with_default_gemfile(self, runtime, use_container): + overrides = {"Runtime": runtime, "CodeUri": "Ruby"} + cmdlist = self.get_command_list(use_container=use_container, + parameter_overrides=overrides) + + LOG.info("Running Command: {}".format(cmdlist)) + process = subprocess.Popen(cmdlist, cwd=self.working_dir) + process.wait() + + self._verify_built_artifact(self.default_build_dir, self.FUNCTION_LOGICAL_ID, + self.EXPECTED_FILES_PROJECT_MANIFEST, self.EXPECTED_RUBY_GEM) + + self._verify_resource_property(str(self.built_template), + "OtherRelativePathResource", + "BodyS3Location", + os.path.relpath( + os.path.normpath(os.path.join(str(self.test_data_path), "SomeRelativePath")), + str(self.default_build_dir)) + ) + + def _verify_built_artifact(self, build_dir, function_logical_id, expected_files, expected_modules): + + self.assertTrue(build_dir.exists(), "Build directory should be created") + + build_dir_files = os.listdir(str(build_dir)) + self.assertIn("template.yaml", build_dir_files) + self.assertIn(function_logical_id, build_dir_files) + + template_path = build_dir.joinpath("template.yaml") + resource_artifact_dir = build_dir.joinpath(function_logical_id) + + # Make sure the template has correct CodeUri for resource + self._verify_resource_property(str(template_path), + function_logical_id, + "CodeUri", + function_logical_id) + + all_artifacts = set(os.listdir(str(resource_artifact_dir))) + actual_files = all_artifacts.intersection(expected_files) + self.assertEquals(actual_files, expected_files) + + ruby_version = None + ruby_bundled_path = None + + # Walk through ruby version to get to the gem path + for dirpath, dirname, _ in os.walk(str(resource_artifact_dir.joinpath('vendor', 'bundle', 'ruby'))): + ruby_version = dirname + ruby_bundled_path = Path(dirpath) + break + gem_path = ruby_bundled_path.joinpath(ruby_version[0], 'gems') + + self.assertTrue(any([True if self.EXPECTED_RUBY_GEM in gem else False for gem in os.listdir(str(gem_path))])) + + def _verify_resource_property(self, template_path, logical_id, property, expected_value): + + with open(template_path, 'r') as fp: + template_dict = yaml_parse(fp.read()) + self.assertEquals(expected_value, template_dict["Resources"][logical_id]["Properties"][property]) diff --git a/tests/integration/publish/__init__.py b/tests/integration/publish/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/publish/publish_app_integ_base.py b/tests/integration/publish/publish_app_integ_base.py new file mode 100644 index 0000000000..c309db6860 --- /dev/null +++ b/tests/integration/publish/publish_app_integ_base.py @@ -0,0 +1,105 @@ +import os +import json +import uuid +import shutil +import tempfile +from unittest import TestCase + +import boto3 + +try: + from pathlib import Path +except ImportError: + from pathlib2 import Path + + +class PublishAppIntegBase(TestCase): + + @classmethod + def setUpClass(cls): + cls.region_name = os.environ.get("AWS_DEFAULT_REGION") + cls.bucket_name = str(uuid.uuid4()) + cls.bucket_name_placeholder = "" + cls.application_name_placeholder = "" + cls.temp_dir = Path(tempfile.mkdtemp()) + cls.test_data_path = Path(__file__).resolve().parents[1].joinpath("testdata", "publish") + cls.sar_client = boto3.client('serverlessrepo', region_name=cls.region_name) + + # Create S3 bucket + s3 = boto3.resource('s3') + cls.s3_bucket = s3.Bucket(cls.bucket_name) + cls.s3_bucket.create() + + # Grant serverlessrepo read access to the bucket + bucket_policy_template = cls.test_data_path.joinpath("s3_bucket_policy.json").read_text() + bucket_policy = bucket_policy_template.replace(cls.bucket_name_placeholder, cls.bucket_name) + cls.s3_bucket.Policy().put(Policy=bucket_policy) + + # Upload test files to S3 + root_path = Path(__file__).resolve().parents[3] + license_body = root_path.joinpath("LICENSE").read_text() + cls.s3_bucket.put_object(Key="LICENSE", Body=license_body) + + readme_body = root_path.joinpath("README.rst").read_text() + cls.s3_bucket.put_object(Key="README.rst", Body=readme_body) + cls.s3_bucket.put_object(Key="README_UPDATE.rst", Body=readme_body) + + code_body = cls.test_data_path.joinpath("main.py").read_text() + cls.s3_bucket.put_object(Key="main.py", Body=code_body) + + @classmethod + def tearDownClass(cls): + cls.s3_bucket.delete_objects(Delete={ + 'Objects': [ + {'Key': 'LICENSE'}, {'Key': 'README.rst'}, + {'Key': 'README_UPDATE.rst'}, {'Key': 'main.py'} + ] + }) + cls.s3_bucket.delete() + + @classmethod + def replace_template_placeholder(cls, placeholder, replace_text): + for f in cls.temp_dir.iterdir(): + if f.suffix == ".yaml" or f.suffix == ".json": + content = f.read_text() + f.write_text(content.replace(placeholder, replace_text)) + + def setUp(self): + shutil.rmtree(str(self.temp_dir), ignore_errors=True) + shutil.copytree(str(self.test_data_path), str(self.temp_dir)) + + # Replace placeholders with the created S3 bucket name and application name + self.application_name = str(uuid.uuid4()) + self.replace_template_placeholder(self.bucket_name_placeholder, self.bucket_name) + self.replace_template_placeholder(self.application_name_placeholder, self.application_name) + + def tearDown(self): + shutil.rmtree(str(self.temp_dir), ignore_errors=True) + + def assert_metadata_details(self, app_metadata, std_output): + # Strip newlines and spaces in the std output + stripped_std_output = std_output.replace('\n', '').replace('\r', '').replace(' ', '') + # Assert expected app metadata in the std output regardless of key order + for key, value in app_metadata.items(): + self.assertIn('"{}":{}'.format(key, json.dumps(value)), stripped_std_output) + + def base_command(self): + command = "sam" + if os.getenv("SAM_CLI_DEV"): + command = "samdev" + + return command + + def get_command_list(self, template_path=None, region=None, profile=None): + command_list = [self.base_command(), "publish"] + + if template_path: + command_list = command_list + ["-t", str(template_path)] + + if region: + command_list = command_list + ["--region", region] + + if profile: + command_list = command_list + ["--profile", profile] + + return command_list diff --git a/tests/integration/publish/test_command_integ.py b/tests/integration/publish/test_command_integ.py new file mode 100644 index 0000000000..d50c58ba15 --- /dev/null +++ b/tests/integration/publish/test_command_integ.py @@ -0,0 +1,122 @@ +import os +import re +import json +from subprocess import Popen, PIPE + +from unittest import skipIf + +from .publish_app_integ_base import PublishAppIntegBase + +# Publish tests require credentials and Travis will only add credentials to the env if the PR is from the same repo. +# This is to restrict publish tests to run outside of Travis and when the branch is not master. +SKIP_PUBLISH_TESTS = os.environ.get("TRAVIS", False) and os.environ.get("TRAVIS_BRANCH", "master") != "master" + + +@skipIf(SKIP_PUBLISH_TESTS, "Skip publish tests in Travis only") +class TestPublishExistingApp(PublishAppIntegBase): + + def setUp(self): + super(TestPublishExistingApp, self).setUp() + # Create application for each test + app_metadata_text = self.temp_dir.joinpath("metadata_create_app.json").read_text() + app_metadata = json.loads(app_metadata_text) + app_metadata['TemplateBody'] = self.temp_dir.joinpath("template_create_app.yaml").read_text() + response = self.sar_client.create_application(**app_metadata) + self.application_id = response['ApplicationId'] + + def tearDown(self): + super(TestPublishExistingApp, self).tearDown() + # Delete application for each test + self.sar_client.delete_application(ApplicationId=self.application_id) + + def test_update_application(self): + template_path = self.temp_dir.joinpath("template_update_app.yaml") + command_list = self.get_command_list(template_path=template_path, region=self.region_name) + + process = Popen(command_list, stdout=PIPE) + process.wait() + process_stdout = b"".join(process.stdout.readlines()).strip() + + expected_msg = 'The following metadata of application "{}" has been updated:'.format(self.application_id) + self.assertIn(expected_msg, process_stdout.decode('utf-8')) + + app_metadata_text = self.temp_dir.joinpath("metadata_update_app.json").read_text() + app_metadata = json.loads(app_metadata_text) + self.assert_metadata_details(app_metadata, process_stdout.decode('utf-8')) + + def test_create_application_version(self): + template_path = self.temp_dir.joinpath("template_create_app_version.yaml") + command_list = self.get_command_list(template_path=template_path, region=self.region_name) + + process = Popen(command_list, stdout=PIPE) + process.wait() + process_stdout = b"".join(process.stdout.readlines()).strip() + + expected_msg = 'The following metadata of application "{}" has been updated:'.format(self.application_id) + self.assertIn(expected_msg, process_stdout.decode('utf-8')) + + app_metadata_text = self.temp_dir.joinpath("metadata_create_app_version.json").read_text() + app_metadata = json.loads(app_metadata_text) + self.assert_metadata_details(app_metadata, process_stdout.decode('utf-8')) + + +@skipIf(SKIP_PUBLISH_TESTS, "Skip publish tests in Travis only") +class TestPublishNewApp(PublishAppIntegBase): + + def setUp(self): + super(TestPublishNewApp, self).setUp() + self.application_id = None + + def tearDown(self): + super(TestPublishNewApp, self).tearDown() + # Delete application if exists + if self.application_id: + self.sar_client.delete_application(ApplicationId=self.application_id) + + def test_create_application(self): + template_path = self.temp_dir.joinpath("template_create_app.yaml") + command_list = self.get_command_list(template_path=template_path, region=self.region_name) + + process = Popen(command_list, stdout=PIPE) + process.wait() + process_stdout = b"".join(process.stdout.readlines()).strip() + + expected_msg = "Created new application with the following metadata:" + self.assertIn(expected_msg, process_stdout.decode('utf-8')) + + app_metadata_text = self.temp_dir.joinpath("metadata_create_app.json").read_text() + app_metadata = json.loads(app_metadata_text) + self.assert_metadata_details(app_metadata, process_stdout.decode('utf-8')) + + # Get console link application id from stdout + pattern = r'arn:[\w\-]+:serverlessrepo:[\w\-]+:[0-9]+:applications\~[\S]+' + match = re.search(pattern, process_stdout.decode('utf-8')) + self.application_id = match.group().replace('~', '/') + + def test_publish_not_packaged_template(self): + template_path = self.temp_dir.joinpath("template_not_packaged.yaml") + command_list = self.get_command_list(template_path=template_path, region=self.region_name) + + process = Popen(command_list, stderr=PIPE) + process.wait() + process_stderr = b"".join(process.stderr.readlines()).strip() + + expected_msg = "Please make sure that you have uploaded application artifacts to S3" + self.assertIn(expected_msg, process_stderr.decode('utf-8')) + + def test_create_application_infer_region_from_env(self): + template_path = self.temp_dir.joinpath("template_create_app.yaml") + command_list = self.get_command_list(template_path=template_path) + + process = Popen(command_list, stdout=PIPE) + process.wait() + process_stdout = b"".join(process.stdout.readlines()).strip() + + expected_msg = "Created new application with the following metadata:" + self.assertIn(expected_msg, process_stdout.decode('utf-8')) + + # Get console link application id from stdout + pattern = r'arn:[\w\-]+:serverlessrepo:[\w\-]+:[0-9]+:applications\~[\S]+' + match = re.search(pattern, process_stdout.decode('utf-8')) + self.application_id = match.group().replace('~', '/') + self.assertIn(self.region_name, self.application_id) diff --git a/tests/integration/testdata/buildcmd/Ruby/Gemfile b/tests/integration/testdata/buildcmd/Ruby/Gemfile new file mode 100644 index 0000000000..e214f5af12 --- /dev/null +++ b/tests/integration/testdata/buildcmd/Ruby/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "httparty" \ No newline at end of file diff --git a/tests/integration/testdata/buildcmd/Ruby/app.rb b/tests/integration/testdata/buildcmd/Ruby/app.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/integration/testdata/publish/main.py b/tests/integration/testdata/publish/main.py new file mode 100644 index 0000000000..ebceb78d1a --- /dev/null +++ b/tests/integration/testdata/publish/main.py @@ -0,0 +1,2 @@ +def lambda_handler(event, context): + return "Hello world" diff --git a/tests/integration/testdata/publish/metadata_create_app.json b/tests/integration/testdata/publish/metadata_create_app.json new file mode 100644 index 0000000000..b83f69724c --- /dev/null +++ b/tests/integration/testdata/publish/metadata_create_app.json @@ -0,0 +1,13 @@ +{ + "Author": "user", + "Description": "description", + "HomePageUrl": "https://github.com/test/test", + "Labels": [ + "test-app" + ], + "LicenseUrl": "s3:///LICENSE", + "Name": "", + "ReadmeUrl": "s3:///README.rst", + "SemanticVersion": "0.0.1", + "SourceCodeUrl": "https://github.com/test/test" +} diff --git a/tests/integration/testdata/publish/metadata_create_app_version.json b/tests/integration/testdata/publish/metadata_create_app_version.json new file mode 100644 index 0000000000..e535512b0d --- /dev/null +++ b/tests/integration/testdata/publish/metadata_create_app_version.json @@ -0,0 +1,11 @@ +{ + "Author": "user", + "Description": "description", + "HomePageUrl": "https://github.com/test/test", + "Labels": [ + "test-app" + ], + "ReadmeUrl": "s3:///README.rst", + "SemanticVersion": "0.0.2", + "SourceCodeUrl": "https://github.com/test/test-new-version" +} diff --git a/tests/integration/testdata/publish/metadata_update_app.json b/tests/integration/testdata/publish/metadata_update_app.json new file mode 100644 index 0000000000..5f5eff7f75 --- /dev/null +++ b/tests/integration/testdata/publish/metadata_update_app.json @@ -0,0 +1,9 @@ +{ + "Author": "user-update", + "Description": "description-update", + "HomePageUrl": "https://github.com/test/test-update", + "Labels": [ + "test-app-update" + ], + "ReadmeUrl": "s3:///README_UPDATE.rst" +} diff --git a/tests/integration/testdata/publish/s3_bucket_policy.json b/tests/integration/testdata/publish/s3_bucket_policy.json new file mode 100644 index 0000000000..1eb9115c3c --- /dev/null +++ b/tests/integration/testdata/publish/s3_bucket_policy.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "serverlessrepo.amazonaws.com" + }, + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::/*" + } + ] +} diff --git a/tests/integration/testdata/publish/template_create_app.yaml b/tests/integration/testdata/publish/template_create_app.yaml new file mode 100644 index 0000000000..da516883c3 --- /dev/null +++ b/tests/integration/testdata/publish/template_create_app.yaml @@ -0,0 +1,38 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'sam-app + + Sample SAM Template for sam-app + + ' +Globals: + Function: + Timeout: 3 +Metadata: + AWS::ServerlessRepo::Application: + Author: user + Description: description + HomePageUrl: https://github.com/test/test + Labels: + - test-app + LicenseUrl: s3:///LICENSE + Name: + ReadmeUrl: s3:///README.rst + SemanticVersion: 0.0.1 + SourceCodeUrl: https://github.com/test/test +Resources: + HelloWorldFunction: + Properties: + CodeUri: s3:///main.py + Environment: + Variables: + PARAM1: VALUE + Events: + HelloWorld: + Properties: + Method: get + Path: /hello + Type: Api + Handler: main.lambda_handler + Runtime: python3.6 + Type: AWS::Serverless::Function +Transform: AWS::Serverless-2016-10-31 diff --git a/tests/integration/testdata/publish/template_create_app_version.yaml b/tests/integration/testdata/publish/template_create_app_version.yaml new file mode 100644 index 0000000000..4981259ccf --- /dev/null +++ b/tests/integration/testdata/publish/template_create_app_version.yaml @@ -0,0 +1,38 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'sam-app + + Sample SAM Template for sam-app + + ' +Globals: + Function: + Timeout: 3 +Metadata: + AWS::ServerlessRepo::Application: + Author: user + Description: description + HomePageUrl: https://github.com/test/test + Labels: + - test-app + LicenseUrl: s3:///LICENSE + Name: + ReadmeUrl: s3:///README.rst + SemanticVersion: 0.0.2 + SourceCodeUrl: "https://github.com/test/test-new-version" +Resources: + HelloWorldFunction: + Properties: + CodeUri: s3:///main.py + Environment: + Variables: + PARAM1: VALUE + Events: + HelloWorld: + Properties: + Method: get + Path: /hello + Type: Api + Handler: main.lambda_handler + Runtime: python3.6 + Type: AWS::Serverless::Function +Transform: AWS::Serverless-2016-10-31 diff --git a/tests/integration/testdata/publish/template_not_packaged.yaml b/tests/integration/testdata/publish/template_not_packaged.yaml new file mode 100644 index 0000000000..7fb2894e52 --- /dev/null +++ b/tests/integration/testdata/publish/template_not_packaged.yaml @@ -0,0 +1,38 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'sam-app + + Sample SAM Template for sam-app + + ' +Globals: + Function: + Timeout: 3 +Metadata: + AWS::ServerlessRepo::Application: + Author: user + Description: description + HomePageUrl: https://github.com/test/test + Labels: + - test-app + LicenseUrl: ./LICENSE + Name: + ReadmeUrl: ./README.rst + SemanticVersion: 0.0.1 + SourceCodeUrl: https://github.com/test/test +Resources: + HelloWorldFunction: + Properties: + CodeUri: ./main.py + Environment: + Variables: + PARAM1: VALUE + Events: + HelloWorld: + Properties: + Method: get + Path: /hello + Type: Api + Handler: main.lambda_handler + Runtime: python3.6 + Type: AWS::Serverless::Function +Transform: AWS::Serverless-2016-10-31 diff --git a/tests/integration/testdata/publish/template_update_app.yaml b/tests/integration/testdata/publish/template_update_app.yaml new file mode 100644 index 0000000000..5a469a65c1 --- /dev/null +++ b/tests/integration/testdata/publish/template_update_app.yaml @@ -0,0 +1,38 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: 'sam-app + + Sample SAM Template for sam-app + + ' +Globals: + Function: + Timeout: 3 +Metadata: + AWS::ServerlessRepo::Application: + Author: user-update + Description: description-update + HomePageUrl: https://github.com/test/test-update + Labels: + - test-app-update + LicenseUrl: s3:///LICENSE + Name: + ReadmeUrl: s3:///README_UPDATE.rst + SemanticVersion: 0.0.1 + SourceCodeUrl: https://github.com/test/test +Resources: + HelloWorldFunction: + Properties: + CodeUri: s3:///main.py + Environment: + Variables: + PARAM1: VALUE + Events: + HelloWorld: + Properties: + Method: get + Path: /hello + Type: Api + Handler: main.lambda_handler + Runtime: python3.6 + Type: AWS::Serverless::Function +Transform: AWS::Serverless-2016-10-31 diff --git a/tests/unit/commands/local/cli_common/test_invoke_context.py b/tests/unit/commands/local/cli_common/test_invoke_context.py index d7ac2b27ed..597f7a4130 100644 --- a/tests/unit/commands/local/cli_common/test_invoke_context.py +++ b/tests/unit/commands/local/cli_common/test_invoke_context.py @@ -3,7 +3,6 @@ """ import errno import os -import sys from samcli.commands.local.cli_common.user_exceptions import InvokeContextException, DebugContextException from samcli.commands.local.cli_common.invoke_context import InvokeContext @@ -262,40 +261,210 @@ def test_must_create_runner(self, class TestInvokeContext_stdout_property(TestCase): - def test_must_return_log_file_handle(self): + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stdout") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_enable_auto_flush_if_debug(self, SamFunctionProviderMock, StreamWriterMock, + osutils_stdout_mock, ExitMock): + + context = InvokeContext(template_file="template", debug_port=6000) + + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock() + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + context.stdout + + StreamWriterMock.assert_called_once_with(ANY, True) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stdout") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_not_enable_auto_flush_if_not_debug(self, + SamFunctionProviderMock, StreamWriterMock, + osutils_stdout_mock, ExitMock): + context = InvokeContext(template_file="template") - context._log_file_handle = "handle" - self.assertEquals("handle", context.stdout) + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock() + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + context.stdout + + StreamWriterMock.assert_called_once_with(ANY, False) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stdout") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_use_stdout_if_no_log_file_handle(self, + SamFunctionProviderMock, StreamWriterMock, + osutils_stdout_mock, ExitMock): + + stream_writer_mock = Mock() + StreamWriterMock.return_value = stream_writer_mock + + stdout_mock = Mock() + osutils_stdout_mock.return_value = stdout_mock - def test_must_return_sys_stdout(self): context = InvokeContext(template_file="template") - expected_stdout = sys.stdout + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock(return_value=None) + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + stdout = context.stdout + + StreamWriterMock.assert_called_once_with(stdout_mock, ANY) + self.assertEqual(stream_writer_mock, stdout) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_use_log_file_handle(self, StreamWriterMock, SamFunctionProviderMock, ExitMock): + + stream_writer_mock = Mock() + StreamWriterMock.return_value = stream_writer_mock + + context = InvokeContext(template_file="template") + + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + + log_file_handle_mock = Mock() + context._setup_log_file = Mock(return_value=log_file_handle_mock) + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) - if sys.version_info.major > 2: - expected_stdout = sys.stdout.buffer + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + stdout = context.stdout - self.assertEquals(expected_stdout, context.stdout) + StreamWriterMock.assert_called_once_with(log_file_handle_mock, ANY) + self.assertEqual(stream_writer_mock, stdout) class TestInvokeContext_stderr_property(TestCase): - def test_must_return_log_file_handle(self): + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stderr") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_enable_auto_flush_if_debug(self, SamFunctionProviderMock, StreamWriterMock, + osutils_stderr_mock, ExitMock): + + context = InvokeContext(template_file="template", debug_port=6000) + + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock() + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + context.stderr + + StreamWriterMock.assert_called_once_with(ANY, True) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stderr") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_not_enable_auto_flush_if_not_debug(self, + SamFunctionProviderMock, StreamWriterMock, + osutils_stderr_mock, ExitMock): + context = InvokeContext(template_file="template") - context._log_file_handle = "handle" - self.assertEquals("handle", context.stderr) + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock() + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + context.stderr + + StreamWriterMock.assert_called_once_with(ANY, False) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.osutils.stderr") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_use_stderr_if_no_log_file_handle(self, + SamFunctionProviderMock, StreamWriterMock, + osutils_stderr_mock, ExitMock): + + stream_writer_mock = Mock() + StreamWriterMock.return_value = stream_writer_mock + + stderr_mock = Mock() + osutils_stderr_mock.return_value = stderr_mock - def test_must_return_sys_stderr(self): context = InvokeContext(template_file="template") - expected_stderr = sys.stderr + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + context._setup_log_file = Mock(return_value=None) + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) + + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + stderr = context.stderr + + StreamWriterMock.assert_called_once_with(stderr_mock, ANY) + self.assertEqual(stream_writer_mock, stderr) + + @patch.object(InvokeContext, "__exit__") + @patch("samcli.commands.local.cli_common.invoke_context.StreamWriter") + @patch("samcli.commands.local.cli_common.invoke_context.SamFunctionProvider") + def test_must_use_log_file_handle(self, StreamWriterMock, SamFunctionProviderMock, ExitMock): + + stream_writer_mock = Mock() + StreamWriterMock.return_value = stream_writer_mock + + context = InvokeContext(template_file="template") + + context._get_template_data = Mock() + context._get_env_vars_value = Mock() + + log_file_handle_mock = Mock() + context._setup_log_file = Mock(return_value=log_file_handle_mock) + + container_manager_mock = Mock() + context._get_container_manager = Mock(return_value=container_manager_mock) - if sys.version_info.major > 2: - expected_stderr = sys.stderr.buffer + with patch.object(type(container_manager_mock), "is_docker_reachable", create=True, return_value=True): + with context: + stderr = context.stderr - self.assertEquals(expected_stderr, context.stderr) + StreamWriterMock.assert_called_once_with(log_file_handle_mock, ANY) + self.assertEqual(stream_writer_mock, stderr) class TestInvokeContext_template_property(TestCase): diff --git a/tests/unit/commands/local/lib/test_debug_context.py b/tests/unit/commands/local/lib/test_debug_context.py index c7f38f4f91..6d8f0a347a 100644 --- a/tests/unit/commands/local/lib/test_debug_context.py +++ b/tests/unit/commands/local/lib/test_debug_context.py @@ -1,4 +1,3 @@ -import os from unittest import TestCase from parameterized import parameterized @@ -29,7 +28,6 @@ def test_bool_truthy(self, port, debug_path, debug_ars): debug_context = DebugContext(port, debug_path, debug_ars) self.assertTrue(debug_context.__bool__()) - self.assertTrue(os.environ["PYTHONUNBUFFERED"], "1") @parameterized.expand([ (None, 'debuggerpath', 'debug_args'), @@ -41,7 +39,6 @@ def test_bool_falsy(self, port, debug_path, debug_ars): debug_context = DebugContext(port, debug_path, debug_ars) self.assertFalse(debug_context.__bool__()) - self.assertFalse(False, "PYTHONUNBUFFERED" in os.environ.keys()) @parameterized.expand([ ('1000', 'debuggerpath', 'debug_args'), diff --git a/tests/unit/commands/publish/__init__.py b/tests/unit/commands/publish/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/commands/publish/test_command.py b/tests/unit/commands/publish/test_command.py new file mode 100644 index 0000000000..72bdbc6623 --- /dev/null +++ b/tests/unit/commands/publish/test_command.py @@ -0,0 +1,147 @@ +"""Test sam publish CLI.""" +import json +from unittest import TestCase +from mock import patch, call, Mock + +from botocore.exceptions import ClientError +from serverlessrepo.exceptions import ServerlessRepoError +from serverlessrepo.publish import CREATE_APPLICATION, UPDATE_APPLICATION + +from samcli.commands.publish.command import do_cli as publish_cli +from samcli.commands.exceptions import UserException + + +class TestCli(TestCase): + + def setUp(self): + self.template = "./template" + self.application_id = "arn:aws:serverlessrepo:us-east-1:123456789012:applications/hello" + self.ctx_mock = Mock() + self.ctx_mock.region = "us-east-1" + self.console_link = "Click the link below to view your application in AWS console:\n" \ + "https://console.aws.amazon.com/serverlessrepo/home?region={}#/published-applications/{}" + + @patch('samcli.commands.publish.command.get_template_data') + @patch('samcli.commands.publish.command.click') + def test_must_raise_if_value_error(self, click_mock, get_template_data_mock): + get_template_data_mock.side_effect = ValueError("Template not found") + with self.assertRaises(UserException) as context: + publish_cli(self.ctx_mock, self.template) + + message = str(context.exception) + self.assertEqual("Template not found", message) + click_mock.secho.assert_called_with("Publish Failed", fg="red") + + @patch('samcli.commands.publish.command.get_template_data', Mock(return_value={})) + @patch('samcli.commands.publish.command.publish_application') + @patch('samcli.commands.publish.command.click') + def test_must_raise_if_serverlessrepo_error(self, click_mock, publish_application_mock): + publish_application_mock.side_effect = ServerlessRepoError() + with self.assertRaises(UserException): + publish_cli(self.ctx_mock, self.template) + + click_mock.secho.assert_called_with("Publish Failed", fg="red") + + @patch('samcli.commands.publish.command.get_template_data', Mock(return_value={})) + @patch('samcli.commands.publish.command.publish_application') + @patch('samcli.commands.publish.command.click') + def test_must_raise_if_s3_uri_error(self, click_mock, publish_application_mock): + publish_application_mock.side_effect = ClientError( + { + 'Error': { + 'Code': 'BadRequestException', + 'Message': 'Invalid S3 URI' + } + }, + 'create_application' + ) + with self.assertRaises(UserException) as context: + publish_cli(self.ctx_mock, self.template) + + message = str(context.exception) + self.assertIn("Please make sure that you have uploaded application artifacts " + "to S3 by packaging the template", message) + click_mock.secho.assert_called_with("Publish Failed", fg="red") + + @patch('samcli.commands.publish.command.get_template_data', Mock(return_value={})) + @patch('samcli.commands.publish.command.publish_application') + @patch('samcli.commands.publish.command.click') + def test_must_raise_if_not_s3_uri_error(self, click_mock, publish_application_mock): + publish_application_mock.side_effect = ClientError( + {'Error': {'Code': 'OtherError', 'Message': 'OtherMessage'}}, + 'other_operation' + ) + with self.assertRaises(ClientError): + publish_cli(self.ctx_mock, self.template) + + click_mock.secho.assert_called_with("Publish Failed", fg="red") + + @patch('samcli.commands.publish.command.get_template_data', Mock(return_value={})) + @patch('samcli.commands.publish.command.publish_application') + @patch('samcli.commands.publish.command.click') + def test_must_succeed_to_create_application(self, click_mock, publish_application_mock): + publish_application_mock.return_value = { + 'application_id': self.application_id, + 'details': {'attr1': 'value1'}, + 'actions': [CREATE_APPLICATION] + } + + publish_cli(self.ctx_mock, self.template) + details_str = json.dumps({'attr1': 'value1'}, indent=2) + expected_msg = "Created new application with the following metadata:\n{}" + expected_link = self.console_link.format( + self.ctx_mock.region, + self.application_id.replace('/', '~') + ) + click_mock.secho.assert_has_calls([ + call("Publish Succeeded", fg="green"), + call(expected_msg.format(details_str), fg="yellow"), + call(expected_link, fg="yellow") + ]) + + @patch('samcli.commands.publish.command.get_template_data', Mock(return_value={})) + @patch('samcli.commands.publish.command.publish_application') + @patch('samcli.commands.publish.command.click') + def test_must_succeed_to_update_application(self, click_mock, publish_application_mock): + publish_application_mock.return_value = { + 'application_id': self.application_id, + 'details': {'attr1': 'value1'}, + 'actions': [UPDATE_APPLICATION] + } + + publish_cli(self.ctx_mock, self.template) + details_str = json.dumps({'attr1': 'value1'}, indent=2) + expected_msg = 'The following metadata of application "{}" has been updated:\n{}' + expected_link = self.console_link.format( + self.ctx_mock.region, + self.application_id.replace('/', '~') + ) + click_mock.secho.assert_has_calls([ + call("Publish Succeeded", fg="green"), + call(expected_msg.format(self.application_id, details_str), fg="yellow"), + call(expected_link, fg="yellow") + ]) + + @patch('samcli.commands.publish.command.get_template_data', Mock(return_value={})) + @patch('samcli.commands.publish.command.publish_application') + @patch('samcli.commands.publish.command.boto3') + @patch('samcli.commands.publish.command.click') + def test_print_console_link_if_context_region_not_set(self, click_mock, boto3_mock, + publish_application_mock): + self.ctx_mock.region = None + publish_application_mock.return_value = { + 'application_id': self.application_id, + 'details': {'attr1': 'value1'}, + 'actions': [UPDATE_APPLICATION] + } + + session_mock = Mock() + session_mock.region_name = "us-west-1" + boto3_mock.Session.return_value = session_mock + + publish_cli(self.ctx_mock, self.template) + expected_link = self.console_link.format( + session_mock.region_name, + self.application_id.replace('/', '~') + ) + click_mock.secho.assert_called_with(expected_link, fg="yellow") diff --git a/tests/unit/lib/utils/test_stream_writer.py b/tests/unit/lib/utils/test_stream_writer.py new file mode 100644 index 0000000000..67a57b0996 --- /dev/null +++ b/tests/unit/lib/utils/test_stream_writer.py @@ -0,0 +1,52 @@ +""" +Tests for StreamWriter +""" + +from unittest import TestCase + +from samcli.lib.utils.stream_writer import StreamWriter + +from mock import Mock + + +class TestStreamWriter(TestCase): + + def test_must_write_to_stream(self): + buffer = "something" + stream_mock = Mock() + + writer = StreamWriter(stream_mock) + writer.write(buffer) + + stream_mock.write.assert_called_once_with(buffer) + + def test_must_flush_underlying_stream(self): + stream_mock = Mock() + writer = StreamWriter(stream_mock) + + writer.flush() + + stream_mock.flush.assert_called_once_with() + + def test_auto_flush_must_be_off_by_default(self): + stream_mock = Mock() + + writer = StreamWriter(stream_mock) + writer.write("something") + + stream_mock.flush.assert_not_called() + + def test_when_auto_flush_on_flush_after_each_write(self): + stream_mock = Mock() + flush_mock = Mock() + + stream_mock.flush = flush_mock + + lines = ["first", "second", "third"] + + writer = StreamWriter(stream_mock, True) + + for line in lines: + writer.write(line) + flush_mock.assert_called_once_with() + flush_mock.reset_mock()