use std::collections::HashMap;
use std::sync::Arc;
use bytes::Bytes;
use http::{HeaderMap, Method};
use parking_lot::{Mutex, RwLock};
use fakecloud_core::service::{AwsRequest, AwsResponse};
use super::RedshiftService;
use crate::state::RedshiftAccounts;
fn service() -> RedshiftService {
RedshiftService::new(Arc::new(RwLock::new(RedshiftAccounts::default())))
}
fn req(action: &str, params: &[(&str, &str)]) -> AwsRequest {
let mut query_params = HashMap::new();
query_params.insert("Action".to_string(), action.to_string());
query_params.insert("Version".to_string(), "2012-12-01".to_string());
for (k, v) in params {
query_params.insert((*k).to_string(), (*v).to_string());
}
AwsRequest {
service: "redshift".to_string(),
action: action.to_string(),
region: "us-east-1".to_string(),
account_id: "123456789012".to_string(),
request_id: "test-request".to_string(),
headers: HeaderMap::new(),
query_params,
body: Bytes::new(),
body_stream: Mutex::new(None),
path_segments: vec![],
raw_path: "/".to_string(),
raw_query: String::new(),
method: Method::POST,
is_query_protocol: true,
access_key_id: None,
principal: None,
}
}
fn body(resp: &AwsResponse) -> String {
String::from_utf8(resp.body.expect_bytes().to_vec()).unwrap()
}
fn ok(svc: &RedshiftService, action: &str, params: &[(&str, &str)]) -> String {
match svc.dispatch(&req(action, params)) {
Ok(resp) => {
assert!(
resp.status.is_success(),
"{action} returned {}",
resp.status
);
body(&resp)
}
Err(e) => panic!("{action} should succeed, got {}", e.code()),
}
}
fn err_code(svc: &RedshiftService, action: &str, params: &[(&str, &str)]) -> String {
match svc.dispatch(&req(action, params)) {
Ok(_) => panic!("{action} should have failed"),
Err(e) => e.code().to_string(),
}
}
#[test]
fn cluster_create_describe_delete_lifecycle() {
let svc = service();
let out = ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", "c1"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
("ClusterType", "single-node"),
],
);
assert!(out.contains("<ClusterIdentifier>c1</ClusterIdentifier>"));
assert!(out.contains("<ClusterStatus>available</ClusterStatus>"));
assert!(out.contains(".us-east-1.redshift.amazonaws.com"));
assert!(out.contains("<Port>5439</Port>"));
let listed = ok(&svc, "DescribeClusters", &[]);
assert!(listed.contains("<ClusterIdentifier>c1</ClusterIdentifier>"));
ok(&svc, "DeleteCluster", &[("ClusterIdentifier", "c1")]);
assert_eq!(
err_code(&svc, "DescribeClusters", &[("ClusterIdentifier", "c1")]),
"ClusterNotFound"
);
}
#[test]
fn duplicate_cluster_conflicts() {
let svc = service();
let p = &[
("ClusterIdentifier", "dup"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
];
ok(&svc, "CreateCluster", p);
assert_eq!(err_code(&svc, "CreateCluster", p), "ClusterAlreadyExists");
}
#[test]
fn multi_az_renders_enabled_disabled() {
let svc = service();
let off = ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", "az-off"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
],
);
assert!(off.contains("<MultiAZ>Disabled</MultiAZ>"));
let on = ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", "az-on"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
("MultiAZ", "true"),
],
);
assert!(on.contains("<MultiAZ>Enabled</MultiAZ>"));
}
#[test]
fn parameter_group_source_user_filter() {
let svc = service();
ok(
&svc,
"CreateClusterParameterGroup",
&[
("ParameterGroupName", "pg"),
("ParameterGroupFamily", "redshift-1.0"),
("Description", "d"),
],
);
ok(
&svc,
"ModifyClusterParameterGroup",
&[
("ParameterGroupName", "pg"),
("Parameters.Parameter.1.ParameterName", "require_ssl"),
("Parameters.Parameter.1.ParameterValue", "true"),
],
);
let user = ok(
&svc,
"DescribeClusterParameters",
&[("ParameterGroupName", "pg"), ("Source", "user")],
);
assert!(user.contains("<ParameterName>require_ssl</ParameterName>"));
assert!(!user.contains("max_cursor_result_set_size"));
let all = ok(
&svc,
"DescribeClusterParameters",
&[("ParameterGroupName", "pg")],
);
assert!(all.contains("max_cursor_result_set_size"));
}
#[test]
fn parameters_accept_member_wrapper_too() {
let svc = service();
ok(
&svc,
"CreateClusterParameterGroup",
&[
("ParameterGroupName", "pg2"),
("ParameterGroupFamily", "redshift-1.0"),
("Description", "d"),
],
);
ok(
&svc,
"ModifyClusterParameterGroup",
&[
("ParameterGroupName", "pg2"),
("Parameters.member.1.ParameterName", "require_ssl"),
("Parameters.member.1.ParameterValue", "true"),
],
);
let user = ok(
&svc,
"DescribeClusterParameters",
&[("ParameterGroupName", "pg2"), ("Source", "user")],
);
assert!(user.contains("<ParameterName>require_ssl</ParameterName>"));
}
#[test]
fn snapshot_has_arn_and_filters_by_cluster() {
let svc = service();
ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", "snapc"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
],
);
let snap = ok(
&svc,
"CreateClusterSnapshot",
&[("SnapshotIdentifier", "s1"), ("ClusterIdentifier", "snapc")],
);
assert!(snap.contains(
"<SnapshotArn>arn:aws:redshift:us-east-1:123456789012:snapshot:snapc/s1</SnapshotArn>"
));
let listed = ok(
&svc,
"DescribeClusterSnapshots",
&[("ClusterIdentifier", "snapc")],
);
assert!(listed.contains("<SnapshotIdentifier>s1</SnapshotIdentifier>"));
let other = ok(
&svc,
"DescribeClusterSnapshots",
&[("ClusterIdentifier", "nope")],
);
assert!(!other.contains("<SnapshotIdentifier>s1</SnapshotIdentifier>"));
}
#[test]
fn snapshot_copy_status_round_trips() {
let svc = service();
ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", "scc"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
],
);
let enabled = ok(
&svc,
"EnableSnapshotCopy",
&[
("ClusterIdentifier", "scc"),
("DestinationRegion", "us-west-2"),
("RetentionPeriod", "14"),
],
);
assert!(enabled.contains("<ClusterSnapshotCopyStatus>"));
assert!(enabled.contains("<DestinationRegion>us-west-2</DestinationRegion>"));
assert!(enabled.contains("<RetentionPeriod>14</RetentionPeriod>"));
let described = ok(&svc, "DescribeClusters", &[("ClusterIdentifier", "scc")]);
assert!(described.contains("<DestinationRegion>us-west-2</DestinationRegion>"));
let disabled = ok(&svc, "DisableSnapshotCopy", &[("ClusterIdentifier", "scc")]);
assert!(!disabled.contains("<ClusterSnapshotCopyStatus>"));
}
#[test]
fn logging_log_exports_round_trip() {
let svc = service();
ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", "logc"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
],
);
ok(
&svc,
"EnableLogging",
&[
("ClusterIdentifier", "logc"),
("LogDestinationType", "cloudwatch"),
("LogExports.member.1", "connectionlog"),
("LogExports.member.2", "userlog"),
],
);
let status = ok(
&svc,
"DescribeLoggingStatus",
&[("ClusterIdentifier", "logc")],
);
assert!(status.contains("<LoggingEnabled>true</LoggingEnabled>"));
assert!(status.contains(
"<LogExports><member>connectionlog</member><member>userlog</member></LogExports>"
));
}
#[test]
fn snapshot_schedule_association_visible_on_describe() {
let svc = service();
ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", "schedc"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
],
);
ok(
&svc,
"CreateSnapshotSchedule",
&[
("ScheduleIdentifier", "sch1"),
("ScheduleDefinitions.ScheduleDefinition.1", "rate(12 hours)"),
],
);
ok(
&svc,
"ModifyClusterSnapshotSchedule",
&[
("ClusterIdentifier", "schedc"),
("ScheduleIdentifier", "sch1"),
],
);
let described = ok(
&svc,
"DescribeSnapshotSchedules",
&[
("ScheduleIdentifier", "sch1"),
("ClusterIdentifier", "schedc"),
],
);
assert!(described.contains("<ScheduleIdentifier>sch1</ScheduleIdentifier>"));
assert!(described.contains("<ClusterIdentifier>schedc</ClusterIdentifier>"));
assert!(described.contains("<AssociatedClusterCount>1</AssociatedClusterCount>"));
}
#[test]
fn endpoint_access_inherits_default_sg_and_vpc_endpoint() {
let svc = service();
ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", "epc"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
],
);
let created = ok(
&svc,
"CreateEndpointAccess",
&[
("EndpointName", "ep"),
("ClusterIdentifier", "epc"),
("SubnetGroupName", "sg"),
],
);
assert!(created.contains("<EndpointStatus>active</EndpointStatus>"));
assert!(created.contains("<VpcSecurityGroupId>"));
assert!(created.contains("<VpcEndpoint><VpcEndpointId>vpce-"));
let one = ok(&svc, "DescribeEndpointAccess", &[("EndpointName", "ep")]);
assert!(one.contains("<EndpointName>ep</EndpointName>"));
let none = ok(&svc, "DescribeEndpointAccess", &[("EndpointName", "other")]);
assert!(!none.contains("<EndpointName>ep</EndpointName>"));
}
#[test]
fn authentication_profile_list_uses_member_wrapper() {
let svc = service();
ok(
&svc,
"CreateAuthenticationProfile",
&[
("AuthenticationProfileName", "ap"),
("AuthenticationProfileContent", "{\"a\":\"b\"}"),
],
);
let listed = ok(
&svc,
"DescribeAuthenticationProfiles",
&[("AuthenticationProfileName", "ap")],
);
assert!(listed.contains(
"<AuthenticationProfiles><member><AuthenticationProfileName>ap</AuthenticationProfileName>"
));
}
#[test]
fn subnet_group_crud_and_tags() {
let svc = service();
let created = ok(
&svc,
"CreateClusterSubnetGroup",
&[
("ClusterSubnetGroupName", "sng"),
("Description", "desc"),
("SubnetIds.SubnetIdentifier.1", "subnet-1"),
("SubnetIds.SubnetIdentifier.2", "subnet-2"),
("Tags.Tag.1.Key", "env"),
("Tags.Tag.1.Value", "test"),
],
);
assert!(created.contains("<SubnetIdentifier>subnet-1</SubnetIdentifier>"));
assert!(created.contains("<SubnetIdentifier>subnet-2</SubnetIdentifier>"));
assert!(created.contains("<Key>env</Key><Value>test</Value>"));
ok(
&svc,
"DeleteClusterSubnetGroup",
&[("ClusterSubnetGroupName", "sng")],
);
assert_eq!(
err_code(
&svc,
"DescribeClusterSubnetGroups",
&[("ClusterSubnetGroupName", "sng")],
),
"ClusterSubnetGroupNotFoundFault"
);
}
#[test]
fn unknown_cluster_operations_error_cleanly() {
let svc = service();
for action in [
"ModifyCluster",
"RebootCluster",
"DeleteCluster",
"EnableLogging",
] {
assert_eq!(
err_code(&svc, action, &[("ClusterIdentifier", "ghost")]),
"ClusterNotFound",
"{action}"
);
}
}
#[test]
fn endpoint_authorization_round_trip() {
let svc = service();
ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", "eac"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
],
);
let authed = ok(
&svc,
"AuthorizeEndpointAccess",
&[("ClusterIdentifier", "eac"), ("Account", "210987654321")],
);
assert!(authed.contains("<Grantee>210987654321</Grantee>"));
assert!(authed.contains("<Status>Authorized</Status>"));
assert!(authed.contains("<AllowedAllVPCs>true</AllowedAllVPCs>"));
let listed = ok(&svc, "DescribeEndpointAuthorization", &[]);
assert!(
listed.contains("<EndpointAuthorizationList><member><Grantor>"),
"expected member wrapper, got: {listed}"
);
assert!(listed.contains("<Grantee>210987654321</Grantee>"));
assert!(listed.contains("<ClusterIdentifier>eac</ClusterIdentifier>"));
assert_eq!(
err_code(
&svc,
"AuthorizeEndpointAccess",
&[("ClusterIdentifier", "eac"), ("Account", "210987654321")],
),
"EndpointAuthorizationAlreadyExists"
);
let revoked = ok(
&svc,
"RevokeEndpointAccess",
&[("ClusterIdentifier", "eac"), ("Account", "210987654321")],
);
assert!(revoked.contains("<Status>Revoking</Status>"));
let empty = ok(&svc, "DescribeEndpointAuthorization", &[]);
assert!(!empty.contains("<Grantee>210987654321</Grantee>"));
assert_eq!(
err_code(
&svc,
"RevokeEndpointAccess",
&[("ClusterIdentifier", "eac"), ("Account", "210987654321")],
),
"EndpointAuthorizationNotFound"
);
}
#[test]
fn endpoint_authorization_unknown_cluster_and_filters() {
let svc = service();
for id in ["ea1", "ea2"] {
ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", id),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
],
);
}
assert_eq!(
err_code(
&svc,
"AuthorizeEndpointAccess",
&[("ClusterIdentifier", "ghost"), ("Account", "210987654321")],
),
"ClusterNotFound"
);
ok(
&svc,
"AuthorizeEndpointAccess",
&[("ClusterIdentifier", "ea1"), ("Account", "210987654321")],
);
ok(
&svc,
"AuthorizeEndpointAccess",
&[("ClusterIdentifier", "ea2"), ("Account", "310987654321")],
);
let by_cluster = ok(
&svc,
"DescribeEndpointAuthorization",
&[("ClusterIdentifier", "ea1")],
);
assert!(by_cluster.contains("<Grantee>210987654321</Grantee>"));
assert!(!by_cluster.contains("<Grantee>310987654321</Grantee>"));
let by_account = ok(
&svc,
"DescribeEndpointAuthorization",
&[("Account", "310987654321")],
);
assert!(by_account.contains("<ClusterIdentifier>ea2</ClusterIdentifier>"));
assert!(!by_account.contains("<Grantee>210987654321</Grantee>"));
let as_grantee = ok(
&svc,
"DescribeEndpointAuthorization",
&[("Grantee", "true")],
);
assert!(!as_grantee.contains("<Grantee>"));
}
#[test]
fn custom_domain_associations_use_association_wrapper() {
let svc = service();
ok(
&svc,
"CreateCluster",
&[
("ClusterIdentifier", "cdc"),
("NodeType", "ra3.xlplus"),
("MasterUsername", "admin"),
("MasterUserPassword", "Passw0rd123"),
],
);
ok(
&svc,
"CreateCustomDomainAssociation",
&[
("ClusterIdentifier", "cdc"),
("CustomDomainName", "redshift.example.com"),
(
"CustomDomainCertificateArn",
"arn:aws:acm:us-east-1:123456789012:certificate/abc123def456",
),
],
);
let listed = ok(&svc, "DescribeCustomDomainAssociations", &[]);
assert!(
listed.contains("<Associations><Association>"),
"expected <Association> list wrapper, got: {listed}"
);
assert!(!listed.contains("<CustomDomainAssociation>"));
assert!(listed.contains("<CustomDomainName>redshift.example.com</CustomDomainName>"));
assert!(listed.contains("<ClusterIdentifier>cdc</ClusterIdentifier>"));
}
#[test]
fn tag_operations_round_trip() {
let svc = service();
ok(
&svc,
"CreateClusterParameterGroup",
&[
("ParameterGroupName", "tagpg"),
("ParameterGroupFamily", "redshift-1.0"),
("Description", "d"),
],
);
let arn = "arn:aws:redshift:us-east-1:123456789012:parametergroup:tagpg";
ok(
&svc,
"CreateTags",
&[
("ResourceName", arn),
("Tags.Tag.1.Key", "team"),
("Tags.Tag.1.Value", "data"),
],
);
let listed = ok(&svc, "DescribeTags", &[("ResourceName", arn)]);
assert!(listed.contains("<Key>team</Key>"));
assert!(listed.contains("<Value>data</Value>"));
}