diff --git a/.rbnextrc b/.rbnextrc index 2a7c529..d4cb1e1 100644 --- a/.rbnextrc +++ b/.rbnextrc @@ -1,4 +1,5 @@ nextify: | + ./lib --min-version=2.6 --edge --proposed diff --git a/Gemfile b/Gemfile index eae06d2..0668472 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,7 @@ eval_gemfile "gemfiles/rubocop.gemfile" eval_gemfile "gemfiles/rbs.gemfile" gem "ruby-next", "~> 1.0.0" +gem "activefunction-core", path: "gems/activefunction-core" local_gemfile = "#{File.dirname(__FILE__)}/Gemfile.local" diff --git a/Rakefile b/Rakefile index 97e570b..49ab8a2 100644 --- a/Rakefile +++ b/Rakefile @@ -24,9 +24,23 @@ GEMS_DIRS.each do |gem_dir| Rake::TestTask.new("test_gem:#{gem_name(gem_dir)}") do |t| t.libs << "#{gem_dir}/test" t.libs << "#{gem_dir}/lib" - t.test_files = FileList["#{gem_dir}/test/**/*_test.rb"] t.warning = false t.verbose = true + t.test_files = FileList["#{gem_dir}/test/**/*_test.rb"].then do |test_files| + if RUBY_VERSION >= "3.2" + test_files + else + test_files.exclude("#{gem_dir}/test/functions/aws_lambda/**/*.rb") + end + + test_files.exclude("#{gem_dir}/test/integration/**/*.rb") + end + end + + task "test_gem:#{gem_name(gem_dir)}:integration" do + Dir.glob("#{gem_dir}/test/integration/**/*_test.rb").each do |test_file| + sh(Gem.ruby, "-I#{__dir__}/lib:#{__dir__}/test", test_file) + end || raise("Integration tests failed") end RuboCop::RakeTask.new("rubocop:#{gem_name(gem_dir)}") do |t| @@ -38,6 +52,7 @@ desc "Run All Tests in each gem" task "test:all" do with_all_gems do |name| Rake::Task["test_gem:#{name}"].invoke + Rake::Task["test_gem:#{name}:integration"].invoke end end @@ -61,7 +76,7 @@ end desc "Transpile all gems" task "nextify:all" do with_all_gems(false) do |path| - sh "bundle exec ruby-next nextify #{path}/lib -V" + sh "bundle exec ruby-next nextify #{path} -V" end end diff --git a/gems/activefunction-core/lib/active_function_core/plugins/types.rb b/gems/activefunction-core/lib/active_function_core/plugins/types.rb index c5d4290..3905bb6 100644 --- a/gems/activefunction-core/lib/active_function_core/plugins/types.rb +++ b/gems/activefunction-core/lib/active_function_core/plugins/types.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require "active_function_core/plugins/types/type" -require "active_function_core/plugins/types/validation" - module ActiveFunctionCore module Plugins module Types + require "active_function_core/plugins/types/type" + require "active_function_core/plugins/types/validation" + RawType = Class.new module ClassMethods @@ -34,11 +34,12 @@ def type(hash_attr) raise ArgumentError, "type Class must be a RawType" unless klass < RawType - name = klass.name.split("::").last - remove_const(name.to_sym) + type_name = klass.name.split("::").last.to_sym + type_attributes = attributes.is_a?(Hash) ? Type.define(**attributes) : attributes - @__types ||= Set.new - @__types << const_set(name, Type.define(type_validator: Validation::TypeValidator, **attributes)) + remove_const(type_name).tap do + (@__types ||= Set.new) << const_set(type_name, type_attributes) + end end def set_root_type(type) diff --git a/gems/activefunction-core/lib/active_function_core/plugins/types/type.rb b/gems/activefunction-core/lib/active_function_core/plugins/types/type.rb index 652e513..427c61a 100644 --- a/gems/activefunction-core/lib/active_function_core/plugins/types/type.rb +++ b/gems/activefunction-core/lib/active_function_core/plugins/types/type.rb @@ -4,9 +4,12 @@ module ActiveFunctionCore::Plugins::Types CustomType = Struct.new(:wrapped_type) Boolean = Class.new(CustomType) Nullable = Class.new(CustomType) + Enum = Class.new(CustomType) do + def initialize(*types) = super(types) + end class Type < Data - def self.define(type_validator:, **attributes, &block) + def self.define(type_validator: Validation::TypeValidator, **attributes, &block) nillable_attributes = handle_nillable_attributes!(attributes) super(*attributes.keys, &block).tap do |klass| @@ -57,25 +60,34 @@ def _subtype_class(type) end def _build_attributes!(attributes) - attributes.map(&method(:_prepare_attribute)).to_h + attributes.map do |name, value| + if (type = schema[name]) + type = _subtype_class(type) if type.is_a?(Class) && type < RawType + else + raise ArgumentError, "unknown attribute #{name}" + end + + _prepare_attribute(name, value, type) + end.to_h end - def _prepare_attribute(name, value) - raise ArgumentError, "unknown attribute #{name}" unless (type = schema[name]) - raise(TypeError, "expected #{value} to be a #{type}") unless type_validator[value, type].valid? + def _prepare_attribute(name, value, type) + type_validator[value, type].validate! serialized_value = _process_subtype_values(type, value) { |subtype, attrs| subtype[**attrs] } [name, serialized_value] end - def _process_subtype_values(type, value, &block) - if _subtype?(type) - yield _subtype_class(type), value - elsif type.is_a?(Array) && _subtype?(type.first) - value.map { |it| yield(_subtype_class(type.first), it) } - elsif type.is_a?(Hash) && _subtype?(type.values.first) - value.transform_values { |it| yield(_subtype_class(type.values.first), it) } + def _process_subtype_values(type, value, &transform_values_block) + type = type.wrapped_type if type.respond_to?(:wrapped_type) + + if value && _subtype?(type) + transform_values_block[_subtype_class(type), value] + elsif value.is_a?(Array) && type.is_a?(Array) && _subtype?(type.first) + value.map { |it| transform_values_block[_subtype_class(type.first), it] } + elsif value.is_a?(Hash) && type.is_a?(Hash) && _subtype?(type.values.first) + value.transform_values { |it| transform_values_block[_subtype_class(type.values.first), it] } else value end diff --git a/gems/activefunction-core/lib/active_function_core/plugins/types/validation.rb b/gems/activefunction-core/lib/active_function_core/plugins/types/validation.rb index 7ba7852..1c3fffe 100644 --- a/gems/activefunction-core/lib/active_function_core/plugins/types/validation.rb +++ b/gems/activefunction-core/lib/active_function_core/plugins/types/validation.rb @@ -45,6 +45,10 @@ def hash_type_validation(value, type_hash) key_validator, value_validator = [type_validator_for(key_type), type_validator_for(value_type)] literal_type_validation(value, Hash) && value.all? { |k, v| key_validator[k, key_type] && value_validator[v, value_type] } end + + def enum_type_validation(value, type_enum) + type_enum.any? { |it| literal_type_validation(value, it) } + end end def self.included(base) @@ -59,7 +63,8 @@ def self.included(base) Boolean => method(:boolean_type_validation), Array => method(:array_type_validation), Hash => method(:hash_type_validation), - Nullable => method(:nullable_type_validation) + Nullable => method(:nullable_type_validation), + Enum => method(:enum_type_validation) } private_constant :PRIMITIVE_TYPE_VALIDATORS_MAPPING @@ -68,6 +73,12 @@ def self.included(base) TypeValidator = Data.define(:value, :type) do include ValidationMethods + def validate! + unless valid? + raise TypeError, "expected #{value} to be kind of #{type}" + end + end + def valid? if type.is_a?(CustomType) && type.wrapped_type validator_proc[value, type.wrapped_type] diff --git a/gems/activefunction-core/test/plugins/types/validation_test.rb b/gems/activefunction-core/test/plugins/types/validation_test.rb index 305d9ec..338f7f4 100644 --- a/gems/activefunction-core/test/plugins/types/validation_test.rb +++ b/gems/activefunction-core/test/plugins/types/validation_test.rb @@ -43,6 +43,12 @@ def test_boolean_validation assert @subject.valid? end + + def test_enum_validation + @attrs = {value: "Hello", type: ::ActiveFunctionCore::Plugins::Types::Enum[String, Integer]} + + assert @subject.valid? + end end class ValidationTest < Minitest::Test diff --git a/gems/activefunction-core/test/plugins/types_test.rb b/gems/activefunction-core/test/plugins/types_test.rb index b173544..ef0bd25 100644 --- a/gems/activefunction-core/test/plugins/types_test.rb +++ b/gems/activefunction-core/test/plugins/types_test.rb @@ -22,8 +22,11 @@ class TypesIncludedTestClass describe "::type" do class TypeTestClass < TypesIncludedTestClass type NamedType => { - string_attribute: String + string_attribute: String, + single_value_attribute: SingleValueType } + + type SingleValueType => ActiveFunctionCore::Plugins::Types::Enum[Symbol, String] end let(:klass) { TypeTestClass } @@ -31,10 +34,14 @@ class TypeTestClass < TypesIncludedTestClass it "defines Constant < Type" do _(subject.const_defined?(:NamedType)).must_equal true _(subject.const_get(:NamedType)).must_be :<, described_class::Type + + _(subject.const_defined?(:SingleValueType)).must_equal true + _(subject.const_get(:SingleValueType)).must_equal ActiveFunctionCore::Plugins::Types::Enum[Symbol, String] end it "adds type to types" do _(subject.instance_variable_get(:@__types)).must_be :===, subject::NamedType + _(subject.instance_variable_get(:@__types)).must_be :===, subject::SingleValueType end it "redefines RawType to Type" do diff --git a/gems/activefunction-core/test/test_helper.rb b/gems/activefunction-core/test/test_helper.rb index 84006ff..ccbece7 100644 --- a/gems/activefunction-core/test/test_helper.rb +++ b/gems/activefunction-core/test/test_helper.rb @@ -6,6 +6,6 @@ require "minitest/autorun" require "minitest/reporters" -require "active_function" +require "active_function_core" Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new(color: true)] diff --git a/lib/active_function/functions/aws_lambda.rb b/lib/active_function/functions/aws_lambda.rb new file mode 100644 index 0000000..59c643b --- /dev/null +++ b/lib/active_function/functions/aws_lambda.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module ActiveFunction + module Functions + module AwsLambda + if RUBY_VERSION < "3.2" + raise LoadError, <<~ERROR + Error: Unsupported Ruby version detected. AWS Lambda currently supports only Ruby 3.2. + For seamless integration, ensure your Ruby runtime version is up-to-date. + Explore AWS Lambda documentation for more details: + + https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html + ERROR + end + + ActiveFunction.register_plugin :aws_lambda, self + end + end +end diff --git a/lib/active_function/functions/aws_lambda/api_gateway_v2_http_api_proxy_event.rb b/lib/active_function/functions/aws_lambda/api_gateway_v2_http_api_proxy_event.rb new file mode 100644 index 0000000..8dc301f --- /dev/null +++ b/lib/active_function/functions/aws_lambda/api_gateway_v2_http_api_proxy_event.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "active_function_core/plugins/types" + +module ActiveFunction::Functions::AwsLambda + class ApiGatewayV2HttpApiProxyEvent + include ActiveFunctionCore::Plugins::Types + + define_schema do + type Event => { + version: String, + routeKey: String, + rawPath: String, + rawQueryString: String, + cookies: Array[String], + headers: Hash[Symbol, String], + requestContext: RequestContext, + isBase64Encoded: Boolean, + "?body": String, + "?pathParameters": Hash[Symbol, String], + "?queryStringParameters": Hash[Symbol, String], + "?stageVariables": Hash[Symbol, String] + } + + type RequestContext => { + accountId: String, + apiId: String, + "?authorizer": Authorizer, + "?authentication": Authentication, + domainName: String, + "?domainPrefix": String, + http: Http, + requestId: String, + routeKey: String, + stage: String, + time: String, + timeEpoch: Integer + } + + type Authentication => { + clientCert: ClientCert + } + + type ClientCert => { + clientCertPem: String, + subjectDN: String, + issuerDN: String, + serialNumber: String, + validity: Validity + } + + type Validity => { + notBefore: String, + notAfter: String + } + + type Authorizer => { + jwt: JwtAuthorizer + } + + type JwtAuthorizer => { + claims: Hash[Symbol, String], + scopes: Array[String] + } + + type Http => { + method: String, + path: String, + protocol: String, + "?sourceIp": String, + "?userAgent": String + } + end + end +end diff --git a/lib/active_function/functions/aws_lambda/dynamodb_event.rb b/lib/active_function/functions/aws_lambda/dynamodb_event.rb new file mode 100644 index 0000000..7699b70 --- /dev/null +++ b/lib/active_function/functions/aws_lambda/dynamodb_event.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "active_function_core/plugins/types" + +module ActiveFunction::Functions::AwsLambda + class DynamoDBEvent + include ActiveFunctionCore::Plugins::Types + + define_schema do + type Event => { + Records: Array[Record] + } + + type Record => { + eventID: String, + eventVersion: String, + eventName: String, + eventSource: String, + eventSourceARN: String, + awsRegion: String, + dynamodb: StreamRecord + } + + type StreamRecord => { + Keys: Hash[Symbol, AttributeValue], + SequenceNumber: String, + SizeBytes: Integer, + StreamViewType: String, + "?NewImage": Hash[Symbol, AttributeValue], + "?OldImage": Hash[Symbol, AttributeValue] + } + + type AttributeValue => { + "?B": String, + "?BOOL": Boolean, + "?BS": Array[String], + "?L": Array[AttributeValue], + "?M": Hash[Symbol, AttributeValue], + "?N": String, + "?NS": Array[String], + "?NULL": Boolean, + "?S": String, + "?SS": Array[String] + } + end + end +end diff --git a/lib/active_function/functions/aws_lambda/sqs_event.rb b/lib/active_function/functions/aws_lambda/sqs_event.rb new file mode 100644 index 0000000..e9af193 --- /dev/null +++ b/lib/active_function/functions/aws_lambda/sqs_event.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require "active_function_core/plugins/types" + +module ActiveFunction::Functions::AwsLambda + class SqsEvent + include ActiveFunctionCore::Plugins::Types + + define_schema do + type Event => { + Records: Array[Record] + } + + type Record => { + messageId: String, + receiptHandle: String, + body: String, + attributes: RecordAttributes, + messageAttributes: Hash[Symbol, MessageAttribute], + md5OfBody: String, + eventSource: String, + eventSourceARN: String, + awsRegion: String + } + + type RecordAttributes => { + "?AWSTraceHeader": String, + ApproximateReceiveCount: String, + SentTimestamp: String, + SenderId: String, + ApproximateFirstReceiveTimestamp: String, + "?SequenceNumber": String, + "?MessageGroupId": String, + "?MessageDeduplicationId": String, + "?DeadLetterQueueSourceArn": String + } + + type MessageAttribute => { + "?stringValue": String, + "?binaryValue": String, + "?stringListValues": Array[String], + "?binaryListValues": Array[String], + dataType: MessageAttributeDataType + } + + type MessageAttributeDataType => Enum[String, Integer, Boolean] + end + end +end diff --git a/sig/aws_events/apiGatewayV2HTTPEvent.rbs b/sig/aws_events/apiGatewayV2HTTPEvent.rbs index 9ee3ab5..f563937 100644 --- a/sig/aws_events/apiGatewayV2HTTPEvent.rbs +++ b/sig/aws_events/apiGatewayV2HTTPEvent.rbs @@ -4,12 +4,12 @@ type apiGatewayV2HTTPEvent = { rawPath: String, rawQueryString: String, cookies: Array[String], - headers: Hash[Symbol, String], - queryStringParameters: Hash[Symbol, String], - pathParameters: Hash[Symbol, String], + ?headers: Hash[Symbol, String], + ?queryStringParameters: Hash[Symbol, String], + ?pathParameters: Hash[Symbol, String], + ?stageVariables: Hash[Symbol, String], requestContext: apiGatewayV2HTTPRequestContext, - stageVariables: Hash[Symbol, String], - body: String, + ?body: String, isBase64Encoded: bool } @@ -25,7 +25,7 @@ type apiGatewayV2HTTPRequestContext = { timeEpoch: Integer, authorizer: apiGatewayV2HTTPRequestContextAuthorizer, http: apiGatewayV2HTTPRequestContextHTTP, - authentication: apiGatewayV2HTTPRequestContextAuthentication, + ?authentication: apiGatewayV2HTTPRequestContextAuthentication, } type apiGatewayV2HTTPRequestContextAuthorizer = { diff --git a/test/fixtures/aws_events/api_gateway_v2_http.json b/test/fixtures/aws_events/api_gateway_v2_http.json new file mode 100644 index 0000000..89486ff --- /dev/null +++ b/test/fixtures/aws_events/api_gateway_v2_http.json @@ -0,0 +1,69 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/{{{path}}}", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "{{{account_id}}}", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.{{{dns_suffix}}}", + "domainPrefix": "id", + "http": { + "method": "{{{method}}}", + "path": "/{{{path}}}", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1/32", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "{{{stage}}}", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "{{{body}}}", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": true, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} \ No newline at end of file diff --git a/test/fixtures/aws_events/api_gateway_v2_http_api_proxy_event.json b/test/fixtures/aws_events/api_gateway_v2_http_api_proxy_event.json new file mode 100644 index 0000000..59a4bf7 --- /dev/null +++ b/test/fixtures/aws_events/api_gateway_v2_http_api_proxy_event.json @@ -0,0 +1,69 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/path/to/resource", + "rawQueryString": "parameter1=value1¶meter1=value2¶meter2=value", + "cookies": [ + "cookie1", + "cookie2" + ], + "headers": { + "Header1": "value1", + "Header2": "value1,value2" + }, + "queryStringParameters": { + "parameter1": "value1,value2", + "parameter2": "value" + }, + "requestContext": { + "accountId": "123456789012", + "apiId": "api-id", + "authentication": { + "clientCert": { + "clientCertPem": "CERT_CONTENT", + "subjectDN": "www.example.com", + "issuerDN": "Example issuer", + "serialNumber": "a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1:a1", + "validity": { + "notBefore": "May 28 12:30:02 2019 GMT", + "notAfter": "Aug 5 09:36:04 2021 GMT" + } + } + }, + "authorizer": { + "jwt": { + "claims": { + "claim1": "value1", + "claim2": "value2" + }, + "scopes": [ + "scope1", + "scope2" + ] + } + }, + "domainName": "id.execute-api.us-east-1.amazonaws.com", + "domainPrefix": "id", + "http": { + "method": "POST", + "path": "/path/to/resource", + "protocol": "HTTP/1.1", + "sourceIp": "192.168.0.1/32", + "userAgent": "agent" + }, + "requestId": "id", + "routeKey": "$default", + "stage": "$default", + "time": "12/Mar/2020:19:03:58 +0000", + "timeEpoch": 1583348638390 + }, + "body": "eyJ0ZXN0IjoiYm9keSJ9", + "pathParameters": { + "parameter1": "value1" + }, + "isBase64Encoded": true, + "stageVariables": { + "stageVariable1": "value1", + "stageVariable2": "value2" + } +} diff --git a/test/fixtures/aws_events/dynamodb.json b/test/fixtures/aws_events/dynamodb_event.json similarity index 56% rename from test/fixtures/aws_events/dynamodb.json rename to test/fixtures/aws_events/dynamodb_event.json index 1089670..1fad65b 100644 --- a/test/fixtures/aws_events/dynamodb.json +++ b/test/fixtures/aws_events/dynamodb_event.json @@ -1,31 +1,5 @@ { "Records": [ - { - "eventID": "1", - "eventVersion": "1.0", - "dynamodb": { - "Keys": { - "Id": { - "N": "101" - } - }, - "NewImage": { - "Message": { - "S": "New item!" - }, - "Id": { - "N": "101" - } - }, - "StreamViewType": "NEW_AND_OLD_IMAGES", - "SequenceNumber": "111", - "SizeBytes": 26 - }, - "awsRegion": "us-west-2", - "eventName": "INSERT", - "eventSourceARN": "arn:aws:dynamodb:us-west-2:111122223333:table/TestTable/stream/2015-05-11T21:21:33.291", - "eventSource": "aws:dynamodb" - }, { "eventID": "2", "eventVersion": "1.0", diff --git a/test/fixtures/aws_events/sqs_event.json b/test/fixtures/aws_events/sqs_event.json new file mode 100644 index 0000000..5a5678e --- /dev/null +++ b/test/fixtures/aws_events/sqs_event.json @@ -0,0 +1,20 @@ +{ + "Records": [ + { + "messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78", + "receiptHandle": "MessageReceiptHandle", + "body": "Hello from SQS!", + "attributes": { + "ApproximateReceiveCount": "1", + "SentTimestamp": "1523232000000", + "SenderId": "123456789012", + "ApproximateFirstReceiveTimestamp": "1523232000001" + }, + "messageAttributes": {}, + "md5OfBody": "7b270e59b47ff90a553787216d55d91d", + "eventSource": "aws:sqs", + "eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue", + "awsRegion": "us-east-1" + } + ] +} \ No newline at end of file diff --git a/test/functions/aws_lambda/api_gateway_v2_http_api_proxy_event_test.rb b/test/functions/aws_lambda/api_gateway_v2_http_api_proxy_event_test.rb new file mode 100644 index 0000000..f52c522 --- /dev/null +++ b/test/functions/aws_lambda/api_gateway_v2_http_api_proxy_event_test.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/aws_event_helper" +require "active_function" +require "active_function/functions/aws_lambda/api_gateway_v2_http_api_proxy_event" + +describe ActiveFunction::Functions::AwsLambda::ApiGatewayV2HttpApiProxyEvent do + subject { described_class.new(**event_hash) } + + let(:described_class) { ActiveFunction::Functions::AwsLambda::ApiGatewayV2HttpApiProxyEvent } + let(:event_hash) { load_aws_event_fixture(:api_gateway_v2_http_api_proxy) } + + it { _(described_class::Event).must_be :<, described_class::Type } + it { _(described_class::RequestContext).must_be :<, described_class::Type } + it { _(described_class::Authorizer).must_be :<, described_class::Type } + it { _(described_class::JwtAuthorizer).must_be :<, described_class::Type } + it { _(described_class::Authentication).must_be :<, described_class::Type } + it { _(described_class::ClientCert).must_be :<, described_class::Type } + it { _(described_class::Validity).must_be :<, described_class::Type } + it { _(described_class::Http).must_be :<, described_class::Type } + + it { _(subject).must_be_kind_of described_class::Event } + it { _(subject.to_h).must_equal event_hash } + + describe ActiveFunction::Functions::AwsLambda::ApiGatewayV2HttpApiProxyEvent::Event do + subject { described_class::Event.new(**event_hash) } + + it { _(subject).must_respond_to :version } + it { _(subject.version).must_be_kind_of String } + + it { _(subject).must_respond_to :routeKey } + it { _(subject.routeKey).must_be_kind_of String } + + it { _(subject).must_respond_to :rawPath } + it { _(subject.rawPath).must_be_kind_of String } + + it { _(subject).must_respond_to :rawQueryString } + it { _(subject.rawQueryString).must_be_kind_of String } + + it { _(subject).must_respond_to :headers } + it { _(subject.headers).must_be_kind_of Hash } + + it { _(subject).must_respond_to :requestContext } + it { _(subject.requestContext).must_be_kind_of described_class::RequestContext } + + it { _(subject).must_respond_to :isBase64Encoded } + it { _(subject.isBase64Encoded).must_be_kind_of TrueClass } + + it { _(subject).must_respond_to :body } + it { _(subject.body).must_be_kind_of String } + + it { _(subject).must_respond_to :queryStringParameters } + it { _(subject.queryStringParameters).must_be_kind_of Hash } + + it { _(subject).must_respond_to :pathParameters } + it { _(subject.pathParameters).must_be_kind_of Hash } + + it { _(subject).must_respond_to :stageVariables } + it { _(subject.stageVariables).must_be_kind_of Hash } + + describe "when nullable fields are not present" do + let(:nullable_fields) { %i[queryStringParameters pathParameters stageVariables body] } + let(:expected_hash) { event_hash.dup.merge(nullable_fields.product([nil]).to_h) } + + before do + nullable_fields.each { |field| event_hash.delete(field) } + end + + it { _(subject.to_h).must_equal expected_hash } + end + end + + describe ActiveFunction::Functions::AwsLambda::ApiGatewayV2HttpApiProxyEvent::RequestContext do + subject { described_class::RequestContext.new(**event_hash[:requestContext]) } + + it { _(subject).must_respond_to :accountId } + it { _(subject.accountId).must_be_kind_of String } + + it { _(subject).must_respond_to :apiId } + it { _(subject.apiId).must_be_kind_of String } + + it { _(subject).must_respond_to :authorizer } + it { _(subject.authorizer).must_be_kind_of described_class::Authorizer } + + it { _(subject).must_respond_to :domainName } + it { _(subject.domainName).must_be_kind_of String } + + it { _(subject).must_respond_to :domainPrefix } + it { _(subject.domainPrefix).must_be_kind_of String } + + it { _(subject).must_respond_to :http } + it { _(subject.http).must_be_kind_of described_class::Http } + + it { _(subject).must_respond_to :requestId } + it { _(subject.requestId).must_be_kind_of String } + + it { _(subject).must_respond_to :routeKey } + it { _(subject.routeKey).must_be_kind_of String } + + it { _(subject).must_respond_to :stage } + it { _(subject.stage).must_be_kind_of String } + + it { _(subject).must_respond_to :time } + it { _(subject.time).must_be_kind_of String } + + it { _(subject).must_respond_to :timeEpoch } + it { _(subject.timeEpoch).must_be_kind_of Integer } + + describe "when nullable fields are not present" do + let(:nullable_fields) { %i[domainPrefix authorizer authentication] } + let(:expected_hash) { event_hash[:requestContext].dup.merge(nullable_fields.product([nil]).to_h) } + + before do + nullable_fields.each { |field| event_hash[:requestContext].delete(field) } + end + + it { _(subject.to_h).must_equal expected_hash } + end + end + + describe ActiveFunction::Functions::AwsLambda::ApiGatewayV2HttpApiProxyEvent::Authorizer do + subject { described_class::Authorizer.new(**event_hash[:requestContext][:authorizer]) } + + it { _(subject).must_respond_to :jwt } + it { _(subject.jwt).must_be_kind_of described_class::JwtAuthorizer } + end + + describe ActiveFunction::Functions::AwsLambda::ApiGatewayV2HttpApiProxyEvent::JwtAuthorizer do + subject { described_class::JwtAuthorizer.new(**event_hash[:requestContext][:authorizer][:jwt]) } + + it { _(subject).must_respond_to :claims } + it { _(subject.claims).must_be_kind_of Hash } + + it { _(subject).must_respond_to :scopes } + it { _(subject.scopes).must_be_kind_of Array } + end + + describe ActiveFunction::Functions::AwsLambda::ApiGatewayV2HttpApiProxyEvent::Authentication do + subject { described_class::Authentication.new(**event_hash[:requestContext][:authentication]) } + + it { _(subject).must_respond_to :clientCert } + it { _(subject.clientCert).must_be_kind_of described_class::ClientCert } + end + + describe ActiveFunction::Functions::AwsLambda::ApiGatewayV2HttpApiProxyEvent::ClientCert do + subject { described_class::ClientCert.new(**event_hash[:requestContext][:authentication][:clientCert]) } + + it { _(subject).must_respond_to :clientCertPem } + it { _(subject.clientCertPem).must_be_kind_of String } + + it { _(subject).must_respond_to :subjectDN } + it { _(subject.subjectDN).must_be_kind_of String } + + it { _(subject).must_respond_to :issuerDN } + it { _(subject.issuerDN).must_be_kind_of String } + + it { _(subject).must_respond_to :serialNumber } + it { _(subject.serialNumber).must_be_kind_of String } + + it { _(subject).must_respond_to :validity } + it { _(subject.validity).must_be_kind_of described_class::Validity } + end + + describe ActiveFunction::Functions::AwsLambda::ApiGatewayV2HttpApiProxyEvent::Http do + subject { described_class::Http.new(**event_hash[:requestContext][:http]) } + + it { _(subject).must_respond_to :method } + it { _(subject.method).must_be_kind_of String } + + it { _(subject).must_respond_to :path } + it { _(subject.path).must_be_kind_of String } + + it { _(subject).must_respond_to :protocol } + it { _(subject.protocol).must_be_kind_of String } + + it { _(subject).must_respond_to :sourceIp } + it { _(subject.sourceIp).must_be_kind_of String } + + it { _(subject).must_respond_to :userAgent } + it { _(subject.userAgent).must_be_kind_of String } + + describe "when nullable fields are not present" do + let(:nullable_fields) { %i[sourceIp userAgent] } + let(:expected_hash) { event_hash[:requestContext][:http].dup.merge(nullable_fields.product([nil]).to_h) } + + before do + nullable_fields.each { |field| event_hash[:requestContext][:http].delete(field) } + end + + it { _(subject.to_h).must_equal expected_hash } + end + end +end diff --git a/test/functions/aws_lambda/dynamodb_event_test.rb b/test/functions/aws_lambda/dynamodb_event_test.rb new file mode 100644 index 0000000..5839a93 --- /dev/null +++ b/test/functions/aws_lambda/dynamodb_event_test.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/aws_event_helper" +require "active_function" +require "active_function/functions/aws_lambda/dynamodb_event" + +describe ActiveFunction::Functions::AwsLambda::DynamoDBEvent do + subject { described_class.new(event_hash) } + + let(:described_class) { ActiveFunction::Functions::AwsLambda::DynamoDBEvent } + let(:event_hash) { load_aws_event_fixture(:dynamodb) } + + it { _(subject).must_be_kind_of described_class::Event } + + it { _(subject).must_respond_to :Records } + it { _(subject.Records).must_be_kind_of Array } + it { _(subject.Records.first).must_be_kind_of described_class::Record } + + describe "Record" do + subject { described_class.new(event_hash).Records[0] } + + it { _(subject).must_be_kind_of described_class::Record } + + it { _(subject).must_respond_to :eventID } + it { _(subject.eventID).must_be_kind_of String } + + it { _(subject).must_respond_to :eventVersion } + it { _(subject.eventVersion).must_be_kind_of String } + + it { _(subject).must_respond_to :eventName } + it { _(subject.eventName).must_be_kind_of String } + + it { _(subject).must_respond_to :eventSource } + it { _(subject.eventSource).must_be_kind_of String } + + it { _(subject).must_respond_to :eventSourceARN } + it { _(subject.eventSourceARN).must_be_kind_of String } + + it { _(subject).must_respond_to :awsRegion } + it { _(subject.awsRegion).must_be_kind_of String } + + it { _(subject).must_respond_to :dynamodb } + it { _(subject.dynamodb).must_be_kind_of described_class::StreamRecord } + end + + describe "Record" do + subject { described_class::Record.new(**record_hash) } + let(:record_hash) { event_hash[:Records][0] } + + it { _(subject).must_be_kind_of described_class::Record } + + it { _(subject).must_respond_to :eventID } + it { _(subject.eventID).must_be_kind_of String } + + it { _(subject).must_respond_to :eventVersion } + it { _(subject.eventVersion).must_be_kind_of String } + + it { _(subject).must_respond_to :eventName } + it { _(subject.eventName).must_be_kind_of String } + + it { _(subject).must_respond_to :eventSource } + it { _(subject.eventSource).must_be_kind_of String } + + it { _(subject).must_respond_to :eventSourceARN } + it { _(subject.eventSourceARN).must_be_kind_of String } + + it { _(subject).must_respond_to :awsRegion } + it { _(subject.awsRegion).must_be_kind_of String } + + it { _(subject).must_respond_to :dynamodb } + it { _(subject.dynamodb).must_be_kind_of described_class::StreamRecord } + end + + describe "StreamRecord" do + subject { described_class::StreamRecord.new(**stream_record_hash) } + let(:stream_record_hash) { event_hash[:Records][0][:dynamodb] } + + it { _(subject).must_be_kind_of described_class::StreamRecord } + + it { _(subject).must_respond_to :Keys } + it { _(subject.Keys).must_be_kind_of Hash } + it { _(subject.Keys.keys.first).must_be_kind_of Symbol } + it { _(subject.Keys.values.first).must_be_kind_of described_class::AttributeValue } + + it { _(subject).must_respond_to :SequenceNumber } + it { _(subject.SequenceNumber).must_be_kind_of String } + + it { _(subject).must_respond_to :SizeBytes } + it { _(subject.SizeBytes).must_be_kind_of Integer } + + it { _(subject).must_respond_to :StreamViewType } + it { _(subject.StreamViewType).must_be_kind_of String } + + it { _(subject).must_respond_to :NewImage } + it { _(subject.NewImage).must_be_kind_of Hash } + it { _(subject.NewImage.keys.first).must_be_kind_of Symbol } + it { _(subject.NewImage.values.first).must_be_kind_of described_class::AttributeValue } + + it { _(subject).must_respond_to :OldImage } + it { _(subject.OldImage).must_be_kind_of Hash } + it { _(subject.OldImage.keys.first).must_be_kind_of Symbol } + it { _(subject.OldImage.values.first).must_be_kind_of described_class::AttributeValue } + + describe "when nullable attribute is missing" do + before do + stream_record_hash.delete(:OldImage) + end + + it { _(subject.OldImage).must_be_nil } + end + end + + describe "AttributeValue" do + subject { described_class::AttributeValue.new(**attribute_value_hash) } + let(:attribute_value_hash) { event_hash[:Records][0][:dynamodb][:OldImage][:Message] } + + it { _(subject).must_be_kind_of described_class::AttributeValue } + + it { _(subject).must_respond_to :S } + it { _(subject.S).must_be_kind_of String } + + it { _(subject).must_respond_to :N } + it { _(subject.N).must_be_nil } + + it { _(subject).must_respond_to :B } + it { _(subject.B).must_be_nil } + + it { _(subject).must_respond_to :SS } + it { _(subject.SS).must_be_nil } + + it { _(subject).must_respond_to :NS } + it { _(subject.NS).must_be_nil } + + it { _(subject).must_respond_to :BS } + it { _(subject.BS).must_be_nil } + + it { _(subject).must_respond_to :M } + it { _(subject.M).must_be_nil } + + it { _(subject).must_respond_to :L } + it { _(subject.L).must_be_nil } + end +end diff --git a/test/functions/aws_lambda/sqs_event_test.rb b/test/functions/aws_lambda/sqs_event_test.rb new file mode 100644 index 0000000..c509281 --- /dev/null +++ b/test/functions/aws_lambda/sqs_event_test.rb @@ -0,0 +1,152 @@ +# frozen_string_literal: true + +require "test_helper" +require "support/aws_event_helper" +require "active_function" +require "active_function/functions/aws_lambda/sqs_event" + +describe ActiveFunction::Functions::AwsLambda::SqsEvent do + subject { described_class.new(**event_hash) } + + let(:described_class) { ActiveFunction::Functions::AwsLambda::SqsEvent } + let(:event_hash) { load_aws_event_fixture(:sqs) } + let(:expected_response_hash) { load_aws_event_fixture(:sqs) } + let(:nillable_attributes) do + %i[AWSTraceHeader SequenceNumber MessageGroupId MessageDeduplicationId DeadLetterQueueSourceArn].product([nil]).to_h + end + + before do + expected_response_hash[:Records][0][:attributes].merge! nillable_attributes + end + + it { _(subject).must_be_kind_of described_class::Event } + it { _(subject.to_h).must_equal expected_response_hash } + + describe ActiveFunction::Functions::AwsLambda::SqsEvent::Event do + subject { described_class::Event.new(**event_hash) } + + it { _(subject).must_respond_to :Records } + it { _(subject.Records).must_be_kind_of Array } + it { _(subject.Records.first).must_be_kind_of described_class::Record } + end + + describe ActiveFunction::Functions::AwsLambda::SqsEvent::Record do + subject { described_class::Record.new(**event_hash[:Records][0]) } + + it { _(subject).must_respond_to :messageId } + it { _(subject.messageId).must_be_kind_of String } + + it { _(subject).must_respond_to :receiptHandle } + it { _(subject.receiptHandle).must_be_kind_of String } + + it { _(subject).must_respond_to :body } + it { _(subject.body).must_be_kind_of String } + + it { _(subject).must_respond_to :attributes } + it { _(subject.attributes).must_be_kind_of described_class::RecordAttributes } + + it { _(subject).must_respond_to :messageAttributes } + it { _(subject.messageAttributes).must_be_kind_of Hash } + + it { _(subject).must_respond_to :md5OfBody } + it { _(subject.md5OfBody).must_be_kind_of String } + + it { _(subject).must_respond_to :eventSource } + it { _(subject.eventSource).must_be_kind_of String } + + it { _(subject).must_respond_to :eventSourceARN } + it { _(subject.eventSourceARN).must_be_kind_of String } + + it { _(subject).must_respond_to :awsRegion } + it { _(subject.awsRegion).must_be_kind_of String } + end + + describe ActiveFunction::Functions::AwsLambda::SqsEvent::RecordAttributes do + subject { described_class.new(**event_hash).Records.first.attributes } + + it { _(subject).must_respond_to :ApproximateReceiveCount } + it { _(subject).must_respond_to :SentTimestamp } + it { _(subject).must_respond_to :SenderId } + it { _(subject).must_respond_to :AWSTraceHeader } + it { _(subject).must_respond_to :ApproximateFirstReceiveTimestamp } + it { _(subject).must_respond_to :SequenceNumber } + it { _(subject).must_respond_to :MessageGroupId } + it { _(subject).must_respond_to :MessageDeduplicationId } + it { _(subject).must_respond_to :DeadLetterQueueSourceArn } + + it { _(subject.ApproximateReceiveCount).must_be_kind_of String } + it { _(subject.SentTimestamp).must_be_kind_of String } + it { _(subject.SenderId).must_be_kind_of String } + + describe "Nullable fields" do + describe "when nullable fields are present" do + before do + event_hash[:Records][0][:attributes].merge!( + AWSTraceHeader: "example_trace_header", + SequenceNumber: "example_sequence_number", + MessageGroupId: "example_message_group_id", + MessageDeduplicationId: "example_message_deduplication_id", + DeadLetterQueueSourceArn: "example_dead_letter_queue_source_arn" + ) + end + + it { _(subject.AWSTraceHeader).must_be_kind_of String } + it { _(subject.ApproximateFirstReceiveTimestamp).must_be_kind_of String } + it { _(subject.SequenceNumber).must_be_kind_of String } + it { _(subject.MessageGroupId).must_be_kind_of String } + it { _(subject.MessageDeduplicationId).must_be_kind_of String } + it { _(subject.DeadLetterQueueSourceArn).must_be_kind_of String } + end + + describe "when nullable fields are not present" do + it { _(subject.AWSTraceHeader).must_be_kind_of NilClass } + it { _(subject.SequenceNumber).must_be_kind_of NilClass } + it { _(subject.MessageGroupId).must_be_kind_of NilClass } + it { _(subject.MessageDeduplicationId).must_be_kind_of NilClass } + it { _(subject.DeadLetterQueueSourceArn).must_be_kind_of NilClass } + end + end + end + + describe ActiveFunction::Functions::AwsLambda::SqsEvent::MessageAttribute do + subject { described_class.new(**event_hash).Records.first.messageAttributes[:example_attribute] } + + it { _(subject).must_respond_to :dataType } + it { _(subject).must_respond_to :stringValue } + it { _(subject).must_respond_to :binaryValue } + it { _(subject).must_respond_to :stringListValues } + it { _(subject).must_respond_to :binaryListValues } + + before do + event_hash[:Records][0][:messageAttributes][:example_attribute] = { + dataType: "String" + } + end + + describe "Nullable fields" do + describe "when nullable fields are present" do + before do + event_hash[:Records][0][:messageAttributes][:example_attribute] = { + dataType: "String", + stringValue: "example_string_value", + binaryValue: "example_binary_value", + stringListValues: ["example_string_list_value"], + binaryListValues: ["example_binary_list_value"] + } + end + + it { _(subject.stringValue).must_be_kind_of String } + it { _(subject.binaryValue).must_be_kind_of String } + it { _(subject.stringListValues).must_be_kind_of Array } + it { _(subject.binaryListValues).must_be_kind_of Array } + end + + describe "when nullable fields are not present" do + it { _(subject.stringValue).must_be_kind_of NilClass } + it { _(subject.binaryValue).must_be_kind_of NilClass } + it { _(subject.stringListValues).must_be_kind_of NilClass } + it { _(subject.binaryListValues).must_be_kind_of NilClass } + end + end + end +end diff --git a/test/integration/function_with_all_plugins_test.rb b/test/integration/function_with_all_plugins_test.rb index 4271591..e57442f 100644 --- a/test/integration/function_with_all_plugins_test.rb +++ b/test/integration/function_with_all_plugins_test.rb @@ -2,58 +2,56 @@ require "test_helper" -fork do - require "active_function" +require "active_function" - ActiveFunction.config do - plugin :strong_parameters - plugin :rendering - plugin :callbacks - end +ActiveFunction.config do + plugin :strong_parameters + plugin :rendering + plugin :callbacks +end - class Function < ActiveFunction::Base - before_action :parse_request, only: [:index], if: :request_valid? +class Function < ActiveFunction::Base + before_action :parse_request, only: [:index], if: :request_valid? - def index - render status: 500 unless @parsed_request + def index + render status: 500 unless @parsed_request - render json: @parsed_request, status: 301 unless performed? - end + render json: @parsed_request, status: 301 unless performed? + end - private + private - def parse_request - @parsed_request = params.require(:data).permit(:a, :b).to_h - end + def parse_request + @parsed_request = params.require(:data).permit(:a, :b).to_h + end - def request_valid? - params[:valid] - end + def request_valid? + params[:valid] end +end + +describe Function do + subject { Function.process(:index, request) } - describe Function do - subject { Function.process(:index, request) } + let(:request) { {data: {a: 1, b: 2, c: 3}, valid: true} } + + it "should return response object" do + _(subject).must_equal({ + statusCode: 301, + body: {a: 1, b: 2}.to_json, + headers: {"Content-Type" => "application/json"} + }) + end - let(:request) { {data: {a: 1, b: 2, c: 3}, valid: true} } + describe "when request is invalid" do + let(:request) { {data: {a: 1, b: 2, c: 3}, valid: false} } - it "should return response object" do + it "should return 500 status response" do _(subject).must_equal({ - statusCode: 301, - body: {a: 1, b: 2}.to_json, + statusCode: 500, + body: "{}", headers: {"Content-Type" => "application/json"} }) end - - describe "when request is invalid" do - let(:request) { {data: {a: 1, b: 2, c: 3}, valid: false} } - - it "should return 500 status response" do - _(subject).must_equal({ - statusCode: 500, - body: "{}", - headers: {"Content-Type" => "application/json"} - }) - end - end end end diff --git a/test/integration/function_with_aws_lambda_plugin_test.rb b/test/integration/function_with_aws_lambda_plugin_test.rb new file mode 100644 index 0000000..fc794b9 --- /dev/null +++ b/test/integration/function_with_aws_lambda_plugin_test.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# require "test_helper" + +# ActiveFunction.config do +# plugin :aws_lambda +# end + +# class LambdaFunction < ActiveFunction::Base +# aws_event :api_gw_v2_http, action: :index + +# def index +# end +# end + +# describe LambdaFunction do +# subject { LambdaFunction.handler(event: event, context: context) } +# end diff --git a/test/integration/function_with_callbacks_plugin_test.rb b/test/integration/function_with_callbacks_plugin_test.rb index f1e741f..2f5c6f4 100644 --- a/test/integration/function_with_callbacks_plugin_test.rb +++ b/test/integration/function_with_callbacks_plugin_test.rb @@ -2,116 +2,112 @@ require "test_helper" -fork do - require "active_function" +ActiveFunction.config do + plugin :callbacks +end - ActiveFunction.config do - plugin :callbacks +class BaseFunction < ActiveFunction::Base + def index + # do nothing end - class BaseFunction < ActiveFunction::Base - def index - # do nothing - end - - private + private - def set_before - @response.body = {before: 1} - end + def set_before + @response.body = {before: 1} + end - def set_after - if @response.body.nil? - @response.body = {after: 2} - else - @response.body.merge!({after: 2}) - end + def set_after + if @response.body.nil? + @response.body = {after: 2} + else + @response.body.merge!({after: 2}) end end +end - class FunctionWithCallbacks < BaseFunction - before_action :set_before - after_action :set_after +class FunctionWithCallbacks < BaseFunction + before_action :set_before + after_action :set_after +end + +describe FunctionWithCallbacks do + let(:expected_response) do + { + statusCode: 200, + body: {before: 1, after: 2}, + headers: {} + } end - describe FunctionWithCallbacks do - let(:expected_response) do - { - statusCode: 200, - body: {before: 1, after: 2}, - headers: {} - } - end + it "should process request & return response object as hash" do + _(FunctionWithCallbacks.process(:index, {})).must_equal(expected_response) + end - it "should process request & return response object as hash" do - _(FunctionWithCallbacks.process(:index, {})).must_equal(expected_response) - end + it "should inherit callbacks" do + klass = Class.new(FunctionWithCallbacks) - it "should inherit callbacks" do - klass = Class.new(FunctionWithCallbacks) + _(klass.process(:index, {})).must_equal(expected_response) + end - _(klass.process(:index, {})).must_equal(expected_response) + it "should inherit callbacks alternative way" do + class InheritedFunctionWithCallbacks < FunctionWithCallbacks end - it "should inherit callbacks alternative way" do - class InheritedFunctionWithCallbacks < FunctionWithCallbacks - end - - _(InheritedFunctionWithCallbacks.process(:index, {})).must_equal(expected_response) - end + _(InheritedFunctionWithCallbacks.process(:index, {})).must_equal(expected_response) end +end - class FunctionWithOptionedCallbacks < BaseFunction - before_action :set_before, only: %i[index], if: :request_valid? - after_action :set_after, unless: :request_valid? +class FunctionWithOptionedCallbacks < BaseFunction # provides :set_before, :set_after, :index instance methods + before_action :set_before, only: %i[index], if: :request_valid? + after_action :set_after, unless: :request_valid? - def show - # do nothing - end + def show + # do nothing + end - private + private - def request_valid? - @request[:valid] == true - end + def request_valid? + @request[:valid] == true end +end - describe FunctionWithOptionedCallbacks do - subject { FunctionWithOptionedCallbacks.process(action, request) } - - let(:action) { :index } - let(:request) { {valid: true} } - let(:expected_response) do - { - statusCode: 200, - body: {before: 1}, - headers: {} - } - end +describe FunctionWithOptionedCallbacks do + subject { FunctionWithOptionedCallbacks.process(action, request) } + + let(:action) { :index } + let(:request) { {valid: true} } + let(:expected_response) do + { + statusCode: 200, + body: {before: 1}, + headers: {} + } + end - it "should process before callbacks" do - _(subject).must_equal(expected_response) - end + it "should process before callbacks" do + _(subject).must_equal(expected_response) + end - describe "when before callbacks skipped" do - let(:request) { {valid: false} } + describe "when before callbacks skipped" do + let(:request) { {valid: false} } - before do - expected_response[:body] = {after: 2} - end + before do + expected_response[:body] = {after: 2} + end - describe "when :only option invalid" do - let(:action) { :show } + describe "when :only option invalid" do + let(:action) { :show } - it "should NOT process before callback" do - _(subject).must_equal(expected_response) - end + it "should NOT process before callback" do + _(subject).must_equal(expected_response) end + end - describe "when :if option invalid" do - it "should NOT process before callback" do - _(subject).must_equal(expected_response) - end + describe "when :if option invalid" do + it "should NOT process before callback" do + _(subject).must_equal(expected_response) end end end diff --git a/test/integration/function_with_rendering_plugin_test.rb b/test/integration/function_with_rendering_plugin_test.rb index 0e66d83..89244d4 100644 --- a/test/integration/function_with_rendering_plugin_test.rb +++ b/test/integration/function_with_rendering_plugin_test.rb @@ -2,79 +2,75 @@ require "test_helper" -fork do - require "active_function" +ActiveFunction.config do + plugin :rendering +end - ActiveFunction.config do - plugin :rendering +class FunctionWithRendering < ActiveFunction::Base + def full_response + render json: @request[:data], status: 201, head: {"X-Test" => "test"} end - class FunctionWithRendering < ActiveFunction::Base - def full_response - render json: @request[:data], status: 201, head: {"X-Test" => "test"} - end - - def status_response - render status: 301 - end + def status_response + render status: 301 + end - def head_response - render head: {"X-Test" => "test"} - end + def head_response + render head: {"X-Test" => "test"} + end - def json_response - render json: @request[:data] - end + def json_response + render json: @request[:data] end +end - describe FunctionWithRendering do - subject { FunctionWithRendering.process(action, request) } +describe FunctionWithRendering do + subject { FunctionWithRendering.process(action, request) } - let(:action) { :full_response } - let(:request) { {data: {a: 1, b: 2}} } + let(:action) { :full_response } + let(:request) { {data: {a: 1, b: 2}} } + + it "should return response object" do + _(subject).must_equal({ + statusCode: 201, + body: {a: 1, b: 2}.to_json, + headers: {"X-Test" => "test", "Content-Type" => "application/json"} + }) + end + + describe "when only status rendered" do + let(:action) { :status_response } it "should return response object" do _(subject).must_equal({ - statusCode: 201, - body: {a: 1, b: 2}.to_json, - headers: {"X-Test" => "test", "Content-Type" => "application/json"} + statusCode: 301, + body: "{}", + headers: {"Content-Type" => "application/json"} }) end + end - describe "when only status rendered" do - let(:action) { :status_response } - - it "should return response object" do - _(subject).must_equal({ - statusCode: 301, - body: "{}", - headers: {"Content-Type" => "application/json"} - }) - end - end - - describe "when only head rendered" do - let(:action) { :head_response } + describe "when only head rendered" do + let(:action) { :head_response } - it "should return response object" do - _(subject).must_equal({ - statusCode: 200, - body: "{}", - headers: {"X-Test" => "test", "Content-Type" => "application/json"} - }) - end + it "should return response object" do + _(subject).must_equal({ + statusCode: 200, + body: "{}", + headers: {"X-Test" => "test", "Content-Type" => "application/json"} + }) end + end - describe "when only json rendered" do - let(:action) { :json_response } + describe "when only json rendered" do + let(:action) { :json_response } - it "should return response object" do - _(subject).must_equal({ - statusCode: 200, - body: {a: 1, b: 2}.to_json, - headers: {"Content-Type" => "application/json"} - }) - end + it "should return response object" do + _(subject).must_equal({ + statusCode: 200, + body: {a: 1, b: 2}.to_json, + headers: {"Content-Type" => "application/json"} + }) end end end diff --git a/test/integration/function_with_strong_parameters_plugin_test.rb b/test/integration/function_with_strong_parameters_plugin_test.rb index 5eb1b7b..5c8101f 100644 --- a/test/integration/function_with_strong_parameters_plugin_test.rb +++ b/test/integration/function_with_strong_parameters_plugin_test.rb @@ -2,74 +2,72 @@ require "test_helper" -fork do - require "active_function" - - ActiveFunction.config do - plugin :strong_parameters - end - - class FunctionWithStrongParameters < ActiveFunction::Base - PERMITTED_PARAMS = [ - :id, - user: [:name, :email], - org: [:id, :name, address: [:city, :street]] - ] +ActiveFunction.config do + plugin :strong_parameters +end - def index - @response.body = request_body.permit(*PERMITTED_PARAMS).to_h - end +class FunctionWithStrongParameters < ActiveFunction::Base + PERMITTED_PARAMS = [ + :id, + user: [:name, :email], + org: [:id, :name, address: [:city, :street]] + ] - private def request_body = params.require(:body) + def index + @response.body = request_body.permit(*PERMITTED_PARAMS).to_h end - describe FunctionWithStrongParameters do - subject { FunctionWithStrongParameters.process(:index, request) } + private def request_body + params.require(:body) + end +end - let(:request) { {body: body} } +describe FunctionWithStrongParameters do + subject { FunctionWithStrongParameters.process(:index, request) } - let(:body) do - {id: request_id, user: user_data, org: org_data} - end + let(:request) { {body: body} } - let(:request_id) { 1 } - let(:user_data) { {name: "Pupa", email: "pupa@acc.com"} } - let(:org_data) { {id: 1, name: "ACC", address: address_data} } - let(:address_data) { {city: "Moscow", street: "Lenina"} } + let(:body) do + {id: request_id, user: user_data, org: org_data} + end - let(:expected_response) do - { - statusCode: 200, - headers: {}, - body: { - id: 1, - user: {name: "Pupa", email: "pupa@acc.com"}, - org: {id: 1, name: "ACC", address: {city: "Moscow", street: "Lenina"}} - } + let(:request_id) { 1 } + let(:user_data) { {name: "Pupa", email: "pupa@acc.com"} } + let(:org_data) { {id: 1, name: "ACC", address: address_data} } + let(:address_data) { {city: "Moscow", street: "Lenina"} } + + let(:expected_response) do + { + statusCode: 200, + headers: {}, + body: { + id: 1, + user: {name: "Pupa", email: "pupa@acc.com"}, + org: {id: 1, name: "ACC", address: {city: "Moscow", street: "Lenina"}} } + } + end + + it "should return response object with permitted params" do + _(subject).must_equal(expected_response) + end + + describe "when request contains not permitted params" do + before do + request[:body][:user][:age] = 18 + request[:body][:org].merge! phone: "123456789" end it "should return response object with permitted params" do _(subject).must_equal(expected_response) end + end - describe "when request contains not permitted params" do - before do - request[:body][:user][:age] = 18 - request[:body][:org].merge! phone: "123456789" - end - - it "should return response object with permitted params" do - _(subject).must_equal(expected_response) - end - end - - describe "when request is invalid" do - let(:request) { {no_body: false} } + describe "when request is invalid" do + let(:request) { {no_body: false} } - it "should raise error" do - assert_raises { subject } - end + it "should raise error" do + assert_raises { subject } end end end diff --git a/test/support/aws_event_helper.rb b/test/support/aws_event_helper.rb new file mode 100644 index 0000000..4a5d0b3 --- /dev/null +++ b/test/support/aws_event_helper.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require "json" + +module AwsEventHelper + def load_aws_event_fixture(filename) + file_path = File.join("test", "fixtures", "aws_events", filename.to_s + "_event.json") + JSON.parse(File.read(file_path), symbolize_names: true) + end +end + +Minitest::Test.include AwsEventHelper diff --git a/test/test_helper.rb b/test/test_helper.rb index 28ef3f6..84006ff 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -6,4 +6,6 @@ require "minitest/autorun" require "minitest/reporters" +require "active_function" + Minitest::Reporters.use! [Minitest::Reporters::SpecReporter.new(color: true)]