rabbitmq-server/deps/rabbitmq_aws/test/rabbitmq_aws_tests.erl

755 lines
30 KiB
Erlang

-module(rabbitmq_aws_tests).
-include_lib("eunit/include/eunit.hrl").
-include("rabbitmq_aws.hrl").
init_test_() ->
{foreach,
fun() ->
os:putenv("AWS_DEFAULT_REGION", "us-west-3"),
meck:new(rabbitmq_aws_config, [passthrough])
end,
fun(_) ->
os:unsetenv("AWS_DEFAULT_REGION"),
meck:unload(rabbitmq_aws_config)
end,
[
{"ok", fun() ->
os:putenv("AWS_ACCESS_KEY_ID", "Sésame"),
os:putenv("AWS_SECRET_ACCESS_KEY", "ouvre-toi"),
{ok, Pid} = rabbitmq_aws:start_link(),
rabbitmq_aws:set_region("us-west-3"),
rabbitmq_aws:refresh_credentials(),
{ok, State} = gen_server:call(Pid, get_state),
ok = gen_server:stop(Pid),
os:unsetenv("AWS_ACCESS_KEY_ID"),
os:unsetenv("AWS_SECRET_ACCESS_KEY"),
Expectation =
{state, "Sésame", "ouvre-toi", undefined, undefined, "us-west-3", undefined,
undefined},
?assertEqual(Expectation, State)
end},
{"error", fun() ->
meck:expect(rabbitmq_aws_config, credentials, fun() -> {error, test_result} end),
{ok, Pid} = rabbitmq_aws:start_link(),
rabbitmq_aws:set_region("us-west-3"),
rabbitmq_aws:refresh_credentials(),
{ok, State} = gen_server:call(Pid, get_state),
ok = gen_server:stop(Pid),
Expectation =
{state, undefined, undefined, undefined, undefined, "us-west-3", undefined,
test_result},
?assertEqual(Expectation, State),
meck:validate(rabbitmq_aws_config)
end}
]}.
terminate_test() ->
?assertEqual(ok, rabbitmq_aws:terminate(foo, bar)).
code_change_test() ->
?assertEqual({ok, {state, denial}}, rabbitmq_aws:code_change(foo, bar, {state, denial})).
endpoint_test_() ->
[
{"specified", fun() ->
Region = "us-east-3",
Service = "dynamodb",
Path = "/",
Host = "localhost:32767",
Expectation = "https://localhost:32767/",
?assertEqual(
Expectation, rabbitmq_aws:endpoint(#state{region = Region}, Host, Service, Path)
)
end},
{"unspecified", fun() ->
Region = "us-east-3",
Service = "dynamodb",
Path = "/",
Host = undefined,
Expectation = "https://dynamodb.us-east-3.amazonaws.com/",
?assertEqual(
Expectation, rabbitmq_aws:endpoint(#state{region = Region}, Host, Service, Path)
)
end}
].
endpoint_host_test_() ->
[
{"dynamodb service", fun() ->
Expectation = "dynamodb.us-west-2.amazonaws.com",
?assertEqual(Expectation, rabbitmq_aws:endpoint_host("us-west-2", "dynamodb"))
end}
].
cn_endpoint_host_test_() ->
[
{"s3", fun() ->
Expectation = "s3.cn-north-1.amazonaws.com.cn",
?assertEqual(Expectation, rabbitmq_aws:endpoint_host("cn-north-1", "s3"))
end},
{"s3", fun() ->
Expectation = "s3.cn-northwest-1.amazonaws.com.cn",
?assertEqual(Expectation, rabbitmq_aws:endpoint_host("cn-northwest-1", "s3"))
end}
].
expired_credentials_test_() ->
{
foreach,
fun() ->
meck:new(calendar, [passthrough, unstick]),
[calendar]
end,
fun meck:unload/1,
[
{"true", fun() ->
Value = {{2016, 4, 1}, {12, 0, 0}},
Expectation = true,
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) ->
[{{2016, 4, 1}, {12, 0, 0}}]
end),
?assertEqual(Expectation, rabbitmq_aws:expired_credentials(Value)),
meck:validate(calendar)
end},
{"false", fun() ->
Value = {{2016, 5, 1}, {16, 30, 0}},
Expectation = false,
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) ->
[{{2016, 4, 1}, {12, 0, 0}}]
end),
?assertEqual(Expectation, rabbitmq_aws:expired_credentials(Value)),
meck:validate(calendar)
end},
{"undefined", fun() ->
?assertEqual(false, rabbitmq_aws:expired_credentials(undefined))
end}
]
}.
format_response_test_() ->
[
{"ok", fun() ->
Response =
{ok, {
{"HTTP/1.1", 200, "Ok"}, [{"Content-Type", "text/xml"}], "<test>Value</test>"
}},
Expectation = {ok, {[{"Content-Type", "text/xml"}], [{"test", "Value"}]}},
?assertEqual(Expectation, rabbitmq_aws:format_response(Response))
end},
{"error", fun() ->
Response =
{ok, {
{"HTTP/1.1", 500, "Internal Server Error"},
[{"Content-Type", "text/xml"}],
"<error>Boom</error>"
}},
Expectation =
{error, "Internal Server Error",
{[{"Content-Type", "text/xml"}], [{"error", "Boom"}]}},
?assertEqual(Expectation, rabbitmq_aws:format_response(Response))
end}
].
gen_server_call_test_() ->
{
foreach,
fun() ->
% We explicitely set a few defaults, in case the caller has
% something in ~/.aws.
os:putenv("AWS_DEFAULT_REGION", "us-west-3"),
os:putenv("AWS_ACCESS_KEY_ID", "Sésame"),
os:putenv("AWS_SECRET_ACCESS_KEY", "ouvre-toi"),
meck:new(httpc, []),
[httpc]
end,
fun(Mods) ->
meck:unload(Mods),
os:unsetenv("AWS_DEFAULT_REGION"),
os:unsetenv("AWS_ACCESS_KEY_ID"),
os:unsetenv("AWS_SECRET_ACCESS_KEY")
end,
[
{
"request",
fun() ->
State = #state{
access_key = "AKIDEXAMPLE",
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
region = "us-east-1"
},
Service = "ec2",
Method = get,
Headers = [],
Path = "/?Action=DescribeTags&Version=2015-10-01",
Body = "",
Options = [],
Host = undefined,
meck:expect(
httpc,
request,
fun(
get,
{"https://ec2.us-east-1.amazonaws.com/?Action=DescribeTags&Version=2015-10-01",
_Headers},
_Options,
[]
) ->
{ok, {
{"HTTP/1.0", 200, "OK"},
[{"content-type", "application/json"}],
"{\"pass\": true}"
}}
end
),
Expectation =
{reply, {ok, {[{"content-type", "application/json"}], [{"pass", true}]}},
State},
Result = rabbitmq_aws:handle_call(
{request, Service, Method, Headers, Path, Body, Options, Host}, eunit, State
),
?assertEqual(Expectation, Result),
meck:validate(httpc)
end
},
{
"get_state",
fun() ->
State = #state{
access_key = "AKIDEXAMPLE",
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
region = "us-east-1"
},
?assertEqual(
{reply, {ok, State}, State},
rabbitmq_aws:handle_call(get_state, eunit, State)
)
end
},
{
"refresh_credentials",
fun() ->
State = #state{
access_key = "AKIDEXAMPLE",
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
region = "us-east-1"
},
State2 = #state{
access_key = "AKIDEXAMPLE2",
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY2",
region = "us-east-1",
security_token =
"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L2",
expiration = calendar:local_time()
},
meck:new(rabbitmq_aws_config, [passthrough]),
meck:expect(
rabbitmq_aws_config,
credentials,
fun() ->
{ok, State2#state.access_key, State2#state.secret_access_key,
State2#state.expiration, State2#state.security_token}
end
),
?assertEqual(
{reply, ok, State2},
rabbitmq_aws:handle_call(refresh_credentials, eunit, State)
),
meck:validate(rabbitmq_aws_config),
meck:unload(rabbitmq_aws_config)
end
},
{
"set_credentials",
fun() ->
State = #state{
access_key = "AKIDEXAMPLE",
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
region = "us-west-3"
},
?assertEqual(
{reply, ok, State},
rabbitmq_aws:handle_call(
{set_credentials, State#state.access_key,
State#state.secret_access_key},
eunit,
#state{region = "us-west-3"}
)
)
end
},
{
"set_region",
fun() ->
State = #state{
access_key = "Sésame",
secret_access_key = "ouvre-toi",
region = "us-east-5"
},
?assertEqual(
{reply, ok, State},
rabbitmq_aws:handle_call({set_region, "us-east-5"}, eunit, #state{
access_key = "Sésame",
secret_access_key = "ouvre-toi"
})
)
end
}
]
}.
get_content_type_test_() ->
[
{"from headers caps", fun() ->
Headers = [{"Content-Type", "text/xml"}],
Expectation = {"text", "xml"},
?assertEqual(Expectation, rabbitmq_aws:get_content_type(Headers))
end},
{"from headers lower", fun() ->
Headers = [{"content-type", "text/xml"}],
Expectation = {"text", "xml"},
?assertEqual(Expectation, rabbitmq_aws:get_content_type(Headers))
end}
].
has_credentials_test_() ->
[
{"true", fun() ->
?assertEqual(true, rabbitmq_aws:has_credentials(#state{access_key = "TESTVALUE1"}))
end},
{"false", fun() ->
?assertEqual(false, rabbitmq_aws:has_credentials(#state{error = "ERROR"}))
end}
].
local_time_test_() ->
{
foreach,
fun() ->
meck:new(calendar, [passthrough, unstick]),
[calendar]
end,
fun meck:unload/1,
[
{"value", fun() ->
Value = {{2016, 5, 1}, {12, 0, 0}},
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [Value] end),
?assertEqual(Value, rabbitmq_aws:local_time()),
meck:validate(calendar)
end}
]
}.
maybe_decode_body_test_() ->
[
{"application/x-amz-json-1.0", fun() ->
ContentType = {"application", "x-amz-json-1.0"},
Body = "{\"test\": true}",
Expectation = [{"test", true}],
?assertEqual(Expectation, rabbitmq_aws:maybe_decode_body(ContentType, Body))
end},
{"application/json", fun() ->
ContentType = {"application", "json"},
Body = "{\"test\": true}",
Expectation = [{"test", true}],
?assertEqual(Expectation, rabbitmq_aws:maybe_decode_body(ContentType, Body))
end},
{"text/xml", fun() ->
ContentType = {"text", "xml"},
Body = "<test><node>value</node></test>",
Expectation = [{"test", [{"node", "value"}]}],
?assertEqual(Expectation, rabbitmq_aws:maybe_decode_body(ContentType, Body))
end},
{"text/html [unsupported]", fun() ->
ContentType = {"text", "html"},
Body = "<html><head></head><body></body></html>",
?assertEqual(Body, rabbitmq_aws:maybe_decode_body(ContentType, Body))
end}
].
parse_content_type_test_() ->
[
{"application/x-amz-json-1.0", fun() ->
Expectation = {"application", "x-amz-json-1.0"},
?assertEqual(Expectation, rabbitmq_aws:parse_content_type("application/x-amz-json-1.0"))
end},
{"application/xml", fun() ->
Expectation = {"application", "xml"},
?assertEqual(Expectation, rabbitmq_aws:parse_content_type("application/xml"))
end},
{"text/xml;charset=UTF-8", fun() ->
Expectation = {"text", "xml"},
?assertEqual(Expectation, rabbitmq_aws:parse_content_type("text/xml"))
end}
].
perform_request_test_() ->
{
foreach,
fun() ->
meck:new(httpc, []),
meck:new(rabbitmq_aws_config, []),
[httpc, rabbitmq_aws_config]
end,
fun meck:unload/1,
[
{
"has_credentials true",
fun() ->
State = #state{
access_key = "AKIDEXAMPLE",
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
region = "us-east-1"
},
Service = "ec2",
Method = get,
Headers = [],
Path = "/?Action=DescribeTags&Version=2015-10-01",
Body = "",
Options = [],
Host = undefined,
ExpectURI =
"https://ec2.us-east-1.amazonaws.com/?Action=DescribeTags&Version=2015-10-01",
meck:expect(
httpc,
request,
fun(get, {URI, _Headers}, _Options, []) ->
case URI of
ExpectURI ->
{ok, {
{"HTTP/1.0", 200, "OK"},
[{"content-type", "application/json"}],
"{\"pass\": true}"
}};
_ ->
{ok,
{{"HTTP/1.0", 400, "RequestFailure",
[{"content-type", "application/json"}],
"{\"pass\": false}"}}}
end
end
),
Expectation = {
{ok, {[{"content-type", "application/json"}], [{"pass", true}]}}, State
},
Result = rabbitmq_aws:perform_request(
State, Service, Method, Headers, Path, Body, Options, Host
),
?assertEqual(Expectation, Result),
meck:validate(httpc)
end
},
{
"has_credentials false",
fun() ->
State = #state{region = "us-east-1"},
Service = "ec2",
Method = get,
Headers = [],
Path = "/?Action=DescribeTags&Version=2015-10-01",
Body = "",
Options = [],
Host = undefined,
meck:expect(httpc, request, fun(get, {_URI, _Headers}, _Options, []) ->
{ok, {
{"HTTP/1.0", 400, "RequestFailure"},
[{"content-type", "application/json"}],
"{\"pass\": false}"
}}
end),
Expectation = {{error, {credentials, State#state.error}}, State},
Result = rabbitmq_aws:perform_request(
State, Service, Method, Headers, Path, Body, Options, Host
),
?assertEqual(Expectation, Result),
meck:validate(httpc)
end
},
{
"has expired credentials",
fun() ->
State = #state{
access_key = "AKIDEXAMPLE",
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
region = "us-east-1",
security_token =
"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L",
expiration = {{1973, 1, 1}, {10, 20, 30}}
},
Service = "ec2",
Method = get,
Headers = [],
Path = "/?Action=DescribeTags&Version=2015-10-01",
Body = "",
Options = [],
Host = undefined,
meck:expect(rabbitmq_aws_config, credentials, fun() -> {error, unit_test} end),
Expectation = {{error, {credentials, "Credentials expired!"}}, State#state{
error = "Credentials expired!"
}},
Result = rabbitmq_aws:perform_request(
State, Service, Method, Headers, Path, Body, Options, Host
),
?assertEqual(Expectation, Result),
meck:validate(rabbitmq_aws_config)
end
},
{
"creds_error",
fun() ->
State = #state{error = unit_test},
Expectation = {{error, {credentials, State#state.error}}, State},
?assertEqual(Expectation, rabbitmq_aws:perform_request_creds_error(State))
end
}
]
}.
sign_headers_test_() ->
{
foreach,
fun() ->
meck:new(calendar, [passthrough, unstick]),
[calendar]
end,
fun meck:unload/1,
[
{"with security token", fun() ->
Value = {{2016, 5, 1}, {12, 0, 0}},
meck:expect(calendar, local_time_to_universal_time_dst, fun(_) -> [Value] end),
State = #state{
access_key = "AKIDEXAMPLE",
secret_access_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
security_token =
"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L",
region = "us-east-1"
},
Service = "ec2",
Method = get,
Headers = [],
Body = "",
URI = "http://ec2.us-east-1.amazonaws.com/?Action=DescribeTags&Version=2015-10-01",
Expectation = [
{"authorization",
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20160501/us-east-1/ec2/aws4_request, SignedHeaders=content-length;date;host;x-amz-content-sha256;x-amz-security-token, Signature=62d10b4897f7d05e4454b75895b5e372f6c2eb6997943cd913680822e94c6999"},
{"content-length", "0"},
{"date", "20160501T120000Z"},
{"host", "ec2.us-east-1.amazonaws.com"},
{"x-amz-content-sha256",
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
{"x-amz-security-token",
"AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT+FvwqnKwRcOIfrRh3c/L"}
],
?assertEqual(
Expectation,
rabbitmq_aws:sign_headers(State, Service, Method, URI, Headers, Body)
),
meck:validate(calendar)
end}
]
}.
api_get_request_test_() ->
{
foreach,
fun() ->
meck:new(httpc, []),
meck:new(rabbitmq_aws_config, []),
[httpc, rabbitmq_aws_config]
end,
fun meck:unload/1,
[
{"AWS service API request succeeded", fun() ->
State = #state{
access_key = "ExpiredKey",
secret_access_key = "ExpiredAccessKey",
region = "us-east-1",
expiration = {{3016, 4, 1}, {12, 0, 0}}
},
meck:expect(
httpc,
request,
4,
{ok, {
{"HTTP/1.0", 200, "OK"},
[{"content-type", "application/json"}],
"{\"data\": \"value\"}"
}}
),
{ok, Pid} = rabbitmq_aws:start_link(),
rabbitmq_aws:set_region("us-east-1"),
rabbitmq_aws:set_credentials(State),
Result = rabbitmq_aws:api_get_request("AWS", "API"),
ok = gen_server:stop(Pid),
?assertEqual({ok, [{"data", "value"}]}, Result),
meck:validate(httpc)
end},
{"AWS service API request failed - credentials", fun() ->
meck:expect(rabbitmq_aws_config, credentials, 0, {error, undefined}),
{ok, Pid} = rabbitmq_aws:start_link(),
rabbitmq_aws:set_region("us-east-1"),
Result = rabbitmq_aws:api_get_request("AWS", "API"),
ok = gen_server:stop(Pid),
?assertEqual({error, credentials}, Result)
end},
{"AWS service API request failed - API error with persistent failure", fun() ->
State = #state{
access_key = "ExpiredKey",
secret_access_key = "ExpiredAccessKey",
region = "us-east-1",
expiration = {{3016, 4, 1}, {12, 0, 0}}
},
meck:expect(httpc, request, 4, {error, "network error"}),
{ok, Pid} = rabbitmq_aws:start_link(),
rabbitmq_aws:set_region("us-east-1"),
rabbitmq_aws:set_credentials(State),
Result = rabbitmq_aws:api_get_request_with_retries("AWS", "API", 3, 1),
ok = gen_server:stop(Pid),
?assertEqual({error, "AWS service is unavailable"}, Result),
meck:validate(httpc)
end},
{"AWS service API request succeeded after a transient error", fun() ->
State = #state{
access_key = "ExpiredKey",
secret_access_key = "ExpiredAccessKey",
region = "us-east-1",
expiration = {{3016, 4, 1}, {12, 0, 0}}
},
meck:expect(
httpc,
request,
4,
meck:seq([
{error, "network error"},
{ok, {
{"HTTP/1.0", 500, "OK"},
[{"content-type", "application/json"}],
"{\"error\": \"server error\"}"
}},
{ok, {
{"HTTP/1.0", 200, "OK"},
[{"content-type", "application/json"}],
"{\"data\": \"value\"}"
}}
])
),
{ok, Pid} = rabbitmq_aws:start_link(),
rabbitmq_aws:set_region("us-east-1"),
rabbitmq_aws:set_credentials(State),
Result = rabbitmq_aws:api_get_request_with_retries("AWS", "API", 3, 1),
ok = gen_server:stop(Pid),
?assertEqual({ok, [{"data", "value"}]}, Result),
meck:validate(httpc)
end}
]
}.
ensure_credentials_valid_test_() ->
{
foreach,
fun() ->
meck:new(rabbitmq_aws_config, []),
[rabbitmq_aws_config]
end,
fun meck:unload/1,
[
{"expired credentials are refreshed", fun() ->
State = #state{
access_key = "ExpiredKey",
secret_access_key = "ExpiredAccessKey",
region = "us-east-1",
expiration = {{2016, 4, 1}, {12, 0, 0}}
},
State2 = #state{
access_key = "NewKey",
secret_access_key = "NewAccessKey",
region = "us-east-1",
expiration = {{3016, 4, 1}, {12, 0, 0}}
},
meck:expect(
rabbitmq_aws_config,
credentials,
fun() ->
{ok, State2#state.access_key, State2#state.secret_access_key,
State2#state.expiration, State2#state.security_token}
end
),
{ok, Pid} = rabbitmq_aws:start_link(),
rabbitmq_aws:set_region("us-east-1"),
rabbitmq_aws:set_credentials(State),
Result = rabbitmq_aws:ensure_credentials_valid(),
Credentials = gen_server:call(Pid, get_state),
ok = gen_server:stop(Pid),
?assertEqual(ok, Result),
?assertEqual(Credentials, {ok, State2}),
meck:validate(rabbitmq_aws_config)
end},
{"valid credentials are returned", fun() ->
State = #state{
access_key = "GoodKey",
secret_access_key = "GoodAccessKey",
region = "us-east-1",
expiration = {{3016, 4, 1}, {12, 0, 0}}
},
{ok, Pid} = rabbitmq_aws:start_link(),
rabbitmq_aws:set_region("us-east-1"),
rabbitmq_aws:set_credentials(State),
Result = rabbitmq_aws:ensure_credentials_valid(),
Credentials = gen_server:call(Pid, get_state),
ok = gen_server:stop(Pid),
?assertEqual(ok, Result),
?assertEqual(Credentials, {ok, State}),
meck:validate(rabbitmq_aws_config)
end},
{"load credentials if missing", fun() ->
State = #state{
access_key = "GoodKey",
secret_access_key = "GoodAccessKey",
region = "us-east-1",
expiration = {{3016, 4, 1}, {12, 0, 0}}
},
meck:expect(
rabbitmq_aws_config,
credentials,
fun() ->
{ok, State#state.access_key, State#state.secret_access_key,
State#state.expiration, State#state.security_token}
end
),
{ok, Pid} = rabbitmq_aws:start_link(),
rabbitmq_aws:set_region("us-east-1"),
Result = rabbitmq_aws:ensure_credentials_valid(),
Credentials = gen_server:call(Pid, get_state),
ok = gen_server:stop(Pid),
?assertEqual(ok, Result),
?assertEqual(Credentials, {ok, State}),
meck:validate(rabbitmq_aws_config)
end}
]
}.
expired_imdsv2_token_test_() ->
[
{"imdsv2 token is valid", fun() ->
[Value] = calendar:local_time_to_universal_time_dst(calendar:local_time()),
Now = calendar:datetime_to_gregorian_seconds(Value),
Imdsv2Token = #imdsv2token{token = "value", expiration = Now + 100},
?assertEqual(false, rabbitmq_aws:expired_imdsv2_token(Imdsv2Token))
end},
{"imdsv2 token is expired", fun() ->
[Value] = calendar:local_time_to_universal_time_dst(calendar:local_time()),
Now = calendar:datetime_to_gregorian_seconds(Value),
Imdsv2Token = #imdsv2token{token = "value", expiration = Now - 100},
?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(Imdsv2Token))
end},
{"imdsv2 token is not yet initialized", fun() ->
?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(undefined))
end},
{"imdsv2 token is undefined", fun() ->
Imdsv2Token = #imdsv2token{token = undefined, expiration = undefined},
?assertEqual(true, rabbitmq_aws:expired_imdsv2_token(Imdsv2Token))
end}
].