diff --git a/schemas/environment.schema.json b/schemas/environment.schema.json index 10c7b40..567e92b 100644 --- a/schemas/environment.schema.json +++ b/schemas/environment.schema.json @@ -27,20 +27,23 @@ "type": "object", "required": ["bucket", "lock_table"], "properties": { - "bucket": {"type": "string", "description": "S3 state bucket name."}, + "bucket": {"type": "string", "pattern": "^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$", "description": "S3 state bucket name (lowercase, 3-63 chars, dots/hyphens)."}, "lock_table": {"type": "string", "description": "DynamoDB lock table name."} - } + }, + "additionalProperties": false }, "network": { "type": "object", "required": ["vpc_cidr", "azs"], "properties": { - "vpc_cidr": {"type": "string", "description": "VPC CIDR block."}, - "azs": {"type": "array", "items": {"type": "string"}, "description": "Availability zones."} - } + "vpc_cidr": {"type": "string", "pattern": "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}/[0-9]{1,2}$", "description": "VPC CIDR block (e.g. 10.0.0.0/16)."}, + "azs": {"type": "array", "items": {"type": "string"}, "maxItems": 6, "description": "Availability zones (max 6)."} + }, + "additionalProperties": false }, "runner_role_arn": { "type": "string", + "pattern": "^arn:aws:iam::[0-9]{12}:role/.+$", "description": "The IAM role ARN surfaced to the consumer's repo via ABAC." }, "autonomy": { @@ -54,5 +57,6 @@ "maximum": 1, "description": "The confidence gate threshold for this environment (dev 0.50, qa 0.75, prod 0.90, dr 0.95)." } - } + }, + "additionalProperties": false } \ No newline at end of file diff --git a/tests/test_environment_schema.py b/tests/test_environment_schema.py index bba468f..713dc9b 100644 --- a/tests/test_environment_schema.py +++ b/tests/test_environment_schema.py @@ -96,4 +96,64 @@ def test_account_id_is_12_digits(): for env_file in ENV_FILES: env = json.loads((ENV_DIR / env_file).read_text()) assert len(env["account_id"]) == 12 - assert env["account_id"].isdigit() \ No newline at end of file + assert env["account_id"].isdigit() + + +def test_v14_schema_rejects_undocumented_fields(): + """v1.14 (REQ-145): additionalProperties: false rejects unknown fields.""" + schema = json.loads(SCHEMA.read_text()) + bad_env = { + "name": "dev", + "account_id": "123456789012", + "region": "us-east-1", + "state_backend": {"bucket": "test", "lock_table": "test"}, + "network": {"vpc_cidr": "10.0.0.0/16", "azs": ["us-east-1a"]}, + "runner_role_arn": "arn:aws:iam::123456789012:role/test", + "autonomy": "full", + "confidence_threshold": 0.5, + "rogue_field": "should be rejected" + } + with pytest.raises(jsonschema.ValidationError, match="Additional properties are not allowed"): + jsonschema.validate(bad_env, schema) + + +def test_v14_schema_validates_bucket_name_format(): + """v1.14 (REQ-145): state_backend.bucket must match S3 naming rules.""" + schema = json.loads(SCHEMA.read_text()) + bad_env = { + "name": "dev", "account_id": "123456789012", "region": "us-east-1", + "state_backend": {"bucket": "Invalid_Bucket!", "lock_table": "test"}, + "network": {"vpc_cidr": "10.0.0.0/16", "azs": ["us-east-1a"]}, + "runner_role_arn": "arn:aws:iam::123456789012:role/test", + "autonomy": "full", "confidence_threshold": 0.5 + } + with pytest.raises(jsonschema.ValidationError, match="does not match"): + jsonschema.validate(bad_env, schema) + + +def test_v14_schema_validates_arn_format(): + """v1.14 (REQ-145): runner_role_arn must match ARN format.""" + schema = json.loads(SCHEMA.read_text()) + bad_env = { + "name": "dev", "account_id": "123456789012", "region": "us-east-1", + "state_backend": {"bucket": "test", "lock_table": "test"}, + "network": {"vpc_cidr": "10.0.0.0/16", "azs": ["us-east-1a"]}, + "runner_role_arn": "not-an-arn", + "autonomy": "full", "confidence_threshold": 0.5 + } + with pytest.raises(jsonschema.ValidationError, match="does not match"): + jsonschema.validate(bad_env, schema) + + +def test_v14_schema_validates_cidr_format(): + """v1.14 (REQ-145): vpc_cidr must match CIDR format.""" + schema = json.loads(SCHEMA.read_text()) + bad_env = { + "name": "dev", "account_id": "123456789012", "region": "us-east-1", + "state_backend": {"bucket": "test", "lock_table": "test"}, + "network": {"vpc_cidr": "not-a-cidr", "azs": ["us-east-1a"]}, + "runner_role_arn": "arn:aws:iam::123456789012:role/test", + "autonomy": "full", "confidence_threshold": 0.5 + } + with pytest.raises(jsonschema.ValidationError, match="does not match"): + jsonschema.validate(bad_env, schema) \ No newline at end of file