

本文属于机器翻译版本。若本译文内容与英语原文存在差异，则一律以英文原文为准。

# 测试 AWS CloudFormation Guard 规则
<a name="testing-rules"></a>

您可以使用 AWS CloudFormation Guard 内置的单元测试框架来验证您的 Guard 规则是否按预期运行。本节提供了有关如何编写单元测试文件以及如何使用该文件通过`test`命令测试规则文件的演练。

您的单元测试文件必须具有以下扩展名之一：`.json`、`.JSON``.jsn`、`.yaml`、`.YAML`、或`.yml`。

**Topics**
+ [先决条件](#testing-rules-prerequisites)
+ [Guard 单元测试文件概述](#testing-rules-overview)
+ [编写守卫规则单元测试文件的演练](#testing-rules-example)

## 先决条件
<a name="testing-rules-prerequisites"></a>

编写 Guard 规则来评估您的输入数据。有关更多信息，请参阅 [编写警卫规则](writing-rules.md)。

## Guard 单元测试文件概述
<a name="testing-rules-overview"></a>

Guard 单元测试文件是 JSON 或 YAML 格式的文件，其中包含多个输入以及写在 Guard 规则文件中的规则的预期结果。可以有多个样本来评估不同的期望。我们建议您首先测试是否存在空白输入，然后逐步添加用于评估各种规则和条款的信息。

此外，我们建议您使用后缀`_test.json`或`_tests.yaml`命名单元测试文件。例如，如果您有一个名为的规则文件`my_rules.guard`，请命名您的单元测试文件`my_rules_tests.yaml`。

### 语法
<a name="testing-rules-syntax"></a>

以下显示了 YAML 格式的单元测试文件的语法。

```
---
- name: <TEST NAME>
  input:
     <SAMPLE INPUT>
   expectations:
     rules:
       <RULE NAME>: [PASS|FAIL|SKIP]
```

### Properties
<a name="testing-rules-properties"></a>

以下是 Guard 测试文件的属性。

`input`  <a name="testing-rules-properties-input"></a>
用于测试规则的数据。我们建议您的第一次测试使用空输入，如以下示例所示。  

```
---
- name: MyTest1
  input {}
```
对于后续测试，请将输入数据添加到测试中。  
 *是否必需*：是 

`expectations`  <a name="testing-rules-properties-expectations"></a>
根据您的输入数据评估特定规则时的预期结果。除了每条规则的预期结果外，还要指定一个或多个要测试的规则。预期结果必须是以下之一：  
+ `PASS`— 当针对您的输入数据运行时，规则的计算结果为`true`。
+ `FAIL`— 当针对您的输入数据运行时，规则的计算结果为`false`。
+ `SKIP`— 当针对您的输入数据运行时，该规则不会被触发。

```
expectations:
    rules:
      check_rest_api_is_private: PASS
```
 *是否必需*：是 

## 编写守卫规则单元测试文件的演练
<a name="testing-rules-example"></a>

以下是名为的规则文件`api_gateway_private.guard`。此规则的目的是检查 CloudFormation 模板中定义的所有 Amazon API Gateway 资源类型是否仅用于私有访问。它还会检查是否至少有一条策略声明允许从虚拟私有云 (VPC) 进行访问。

```
#
# Select all AWS::ApiGateway::RestApi resources
#     present in the Resources section of the template. 
#
let api_gws = Resources.*[ Type == 'AWS::ApiGateway::RestApi']

#
# Rule intent:         
# 1) All AWS::ApiGateway::RestApi resources deployed must be private.                                            
# 2) All AWS::ApiGateway::RestApi resources deployed must have at least one AWS Identity and Access Management (IAM) policy condition key to allow access from a VPC.
#
# Expectations:        
# 1) SKIP when there are no AWS::ApiGateway::RestApi resources in the template.  
# 2) PASS when:
#     ALL AWS::ApiGateway::RestApi resources in the template have the EndpointConfiguration property set to Type: PRIVATE. 
#     ALL AWS::ApiGateway::RestApi resources in the template have one IAM condition key specified in the Policy property with aws:sourceVpc or :SourceVpc.    
# 3) FAIL otherwise.                                                                                  
#
#

rule check_rest_api_is_private when %api_gws !empty {      
    %api_gws {
        Properties.EndpointConfiguration.Types[*] == "PRIVATE"                             
    }  
}       

rule check_rest_api_has_vpc_access when check_rest_api_is_private {
    %api_gws {
        Properties {
            #
            # ALL AWS::ApiGateway::RestApi resources in the template have one IAM condition key specified in the Policy property with 
            #     aws:sourceVpc or :SourceVpc
            #           
            some Policy.Statement[*] {
                Condition.*[ keys == /aws:[sS]ource(Vpc|VPC|Vpce|VPCE)/ ] !empty
            }
        }
    }
}
```

本演练测试了第一个规则意图：部署的所有`AWS::ApiGateway::RestApi`资源都必须是私有的。

1. 创建一个名为的单元测试文件`api_gateway_private_tests.yaml`，其中包含以下初始测试。在初始测试中，添加一个空输入，预计该规则`check_rest_api_is_private`会跳过，因为没有`AWS::ApiGateway::RestApi`资源作为输入。

   ```
   ---
   - name: MyTest1
     input: {}
     expectations:
       rules:
         check_rest_api_is_private: SKIP
   ```

1. 使用`test`命令在终端中运行第一个测试。对于`--rules-file`参数，请指定您的规则文件。对于`--test-data`参数，请指定您的单元测试文件。

   ```
   cfn-guard test --rules-file api_gateway_private.guard --test-data api_gateway_private_tests.yaml
   ```

   第一次测试的结果是`PASS`。

   ```
   Test Case #1
   Name: "MyTest1"
     PASS Rules:
       check_rest_api_is_private: Expected = SKIP, Evaluated = SKIP
   ```

1. 在单元测试文件中添加另一个测试。现在，将测试范围扩展到包括空资源。以下是更新的`api_gateway_private_tests.yaml`文件。

   ```
   ---
   - name: MyTest1
     input: {}
     expectations:
       rules:
         check_rest_api_is_private: SKIP
   - name: MyTest2
     input:
        Resources: {}
     expectations:
       rules:
         check_rest_api_is_private: SKIP
   ```

1. `test`使用更新的单元测试文件运行。

   ```
   cfn-guard test --rules-file api_gateway_private.guard --test-data api_gateway_private_tests.yaml
   ```

   第二次测试的结果是`PASS`。

   ```
   Test Case #1
   Name: "MyTest1"
     PASS Rules:
       check_rest_api_is_private: Expected = SKIP, Evaluated = SKIP
   Test Case #2
   Name: "MyTest2"
     PASS Rules:
       check_rest_api_is_private: Expected = SKIP, Evaluated = SKIP
   ```

1. 在单元测试文件中再添加两个测试。扩展测试范围以包括以下内容：
   + 未指定属性的`AWS::ApiGateway::RestApi`资源。
**注意**  
这不是一个有效的 CloudFormation 模板，但是即使对于格式错误的输入，测试该规则是否正常工作也很有用。

     预计此测试将失败，因为未指定该`EndpointConfiguration`属性，因此未将其设置为`PRIVATE`。
   + 一种`AWS::ApiGateway::RestApi`资源，它满足第一个意图，其`EndpointConfiguration`属性设置为，`PRIVATE`但由于未定义策略语句而未满足第二个意图。预计此测试将通过。

   以下是更新的单元测试文件。

   ```
   ---
   - name: MyTest1
     input: {}
     expectations:
       rules:
         check_rest_api_is_private: SKIP
   - name: MyTest2
     input:
        Resources: {}
     expectations:
       rules:
         check_rest_api_is_private: SKIP
   - name: MyTest3
     input:
       Resources: 
         apiGw:
           Type: AWS::ApiGateway::RestApi
     expectations:
       rules:
         check_rest_api_is_private: FAIL
   - name: MyTest4
     input:
       Resources: 
         apiGw:
           Type: AWS::ApiGateway::RestApi
           Properties:
             EndpointConfiguration:
               Types: "PRIVATE"
     expectations:
       rules:
         check_rest_api_is_private: PASS
   ```

1. `test`使用更新的单元测试文件运行。

   ```
   cfn-guard test --rules-file api_gateway_private.guard --test-data api_gateway_private_tests.yaml \
   ```

   第三个结果是`FAIL`，第四个结果是`PASS`。

   ```
   Test Case #1
   Name: "MyTest1"
     PASS Rules:
       check_rest_api_is_private: Expected = SKIP, Evaluated = SKIP
   
   Test Case #2
   Name: "MyTest2"
     PASS Rules:
       check_rest_api_is_private: Expected = SKIP, Evaluated = SKIP
   
   Test Case #3
   Name: "MyTest3"
     PASS Rules:
       check_rest_api_is_private: Expected = FAIL, Evaluated = FAIL
   
   Test Case #4
   Name: "MyTest4"
     PASS Rules:
       check_rest_api_is_private: Expected = PASS, Evaluated = PASS
   ```

1. 在单元测试文件中注释掉测试 1—3。仅访问第四次测试的详细上下文。以下是更新的单元测试文件。

   ```
   ---
   #- name: MyTest1
   #  input: {}
   #  expectations:
   #    rules:
   #      check_rest_api_is_private_and_has_access: SKIP
   #- name: MyTest2
   #  input:
   #     Resources: {}
   #  expectations:
   #    rules:
   #      check_rest_api_is_private_and_has_access: SKIP
   #- name: MyTest3
   #  input:
   #    Resources: 
   #      apiGw:
   #        Type: AWS::ApiGateway::RestApi
   #  expectations:
   #    rules:
   #      check_rest_api_is_private_and_has_access: FAIL
   - name: MyTest4
     input:
       Resources: 
         apiGw:
           Type: AWS::ApiGateway::RestApi
           Properties:
             EndpointConfiguration:
               Types: "PRIVATE"
     expectations:
       rules:
         check_rest_api_is_private: PASS
   ```

1. 使用`--verbose`标志，在终端中运行`test`命令来检查评估结果。详细上下文对于理解评估很有用。在这种情况下，它提供了有关为什么第四次测试成功并得出`PASS`结果的详细信息。

   ```
   cfn-guard test --rules-file api_gateway_private.guard --test-data api_gateway_private_tests.yaml \
     --verbose
   ```

   这是那次运行的输出。

   ```
   Test Case #1
   Name: "MyTest4"
     PASS Rules:
       check_rest_api_is_private: Expected = PASS, Evaluated = PASS
   Rule(check_rest_api_is_private, PASS)
       |  Message: DEFAULT MESSAGE(PASS)
       Condition(check_rest_api_is_private, PASS)
           |  Message: DEFAULT MESSAGE(PASS)
           Clause(Clause(Location[file:api_gateway_private.guard, line:20, column:37], Check: %api_gws NOT EMPTY ), PASS)
               |  From: Map((Path("/Resources/apiGw"), MapValue { keys: [String((Path("/Resources/apiGw/Type"), "Type")), String((Path("/Resources/apiGw/Properties"), "Properties"))], values: {"Type": String((Path("/Resources/apiGw/Type"), "AWS::ApiGateway::RestApi")), "Properties": Map((Path("/Resources/apiGw/Properties"), MapValue { keys: [String((Path("/Resources/apiGw/Properties/EndpointConfiguration"), "EndpointConfiguration"))], values: {"EndpointConfiguration": Map((Path("/Resources/apiGw/Properties/EndpointConfiguration"), MapValue { keys: [String((Path("/Resources/apiGw/Properties/EndpointConfiguration/Types"), "Types"))], values: {"Types": String((Path("/Resources/apiGw/Properties/EndpointConfiguration/Types"), "PRIVATE"))} }))} }))} }))
               |  Message: (DEFAULT: NO_MESSAGE)
       Conjunction(cfn_guard::rules::exprs::GuardClause, PASS)
           |  Message: DEFAULT MESSAGE(PASS)
           Clause(Clause(Location[file:api_gateway_private.guard, line:22, column:5], Check: Properties.EndpointConfiguration.Types[*]  EQUALS String("PRIVATE")), PASS)
               |  Message: (DEFAULT: NO_MESSAGE)
   ```

   从输出中观察到的关键是线`Clause(Location[file:api_gateway_private.guard, line:22, column:5], Check: Properties.EndpointConfiguration.Types[*] EQUALS String("PRIVATE")), PASS)`，它表示检查已通过。该示例还显示了本应为数组，但给出了单个值的情况`Types`。在这种情况下，Guard继续进行评估并提供了正确的结果。

1. 将像第四个测试用例这样的测试用例添加到具有指定`EndpointConfiguration`属性的`AWS::ApiGateway::RestApi`资源的单元测试文件中。测试用例将失败而不是通过。以下是更新的单元测试文件。

   ```
   ---
   #- name: MyTest1
   #  input: {}
   #  expectations:
   #    rules:
   #      check_rest_api_is_private_and_has_access: SKIP
   #- name: MyTest2
   #  input:
   #     Resources: {}
   #  expectations:
   #    rules:
   #      check_rest_api_is_private_and_has_access: SKIP
   #- name: MyTest3
   #  input:
   #    Resources: 
   #      apiGw:
   #        Type: AWS::ApiGateway::RestApi
   #  expectations:
   #    rules:
   #      check_rest_api_is_private_and_has_access: FAIL
   #- name: MyTest4
   #  input:
   #    Resources: 
   #      apiGw:
   #        Type: AWS::ApiGateway::RestApi
   #        Properties:
   #          EndpointConfiguration:
   #            Types: "PRIVATE"
   #  expectations:
   #    rules:
   #      check_rest_api_is_private: PASS
   - name: MyTest5
     input:
       Resources: 
         apiGw:
           Type: AWS::ApiGateway::RestApi
           Properties:
             EndpointConfiguration:
               Types: [PRIVATE, REGIONAL]
     expectations:
       rules:
         check_rest_api_is_private: FAIL
   ```

1. 使用`--verbose`标志使用更新的单元测试文件运行`test`命令。

   ```
   cfn-guard test --rules-file api_gateway_private.guard --test-data api_gateway_private_tests.yaml \
    --verbose
   ```

   结果符合预期`FAIL`，因为`REGIONAL`已指定，`EndpointConfiguration`但不是预料之中的。

   ```
   Test Case #1
   Name: "MyTest5"
     PASS Rules: 
       check_rest_api_is_private: Expected = FAIL, Evaluated = FAIL
   Rule(check_rest_api_is_private, FAIL)
       |  Message: DEFAULT MESSAGE(FAIL)
       Condition(check_rest_api_is_private, PASS) 
           |  Message: DEFAULT MESSAGE(PASS)
           Clause(Clause(Location[file:api_gateway_private.guard, line:20, column:37], Check: %api_gws NOT EMPTY ), PASS)
               |  From: Map((Path("/Resources/apiGw"), MapValue { keys: [String((Path("/Resources/apiGw/Type"), "Type")), String((Path("/Resources/apiGw/Properties"), "Properties"))], values: {"Type": String((Path("/Resources/apiGw/Type"), "AWS::ApiGateway::RestApi")), "Properties": Map((Path("/Resources/apiGw/Properties"), MapValue { keys: [String((Path("/Resources/apiGw/Properties/EndpointConfiguration"), "EndpointConfiguration"))], values: {"EndpointConfiguration": Map((Path("/Resources/apiGw/Properties/EndpointConfiguration"), MapValue { keys: [String((Path("/Resources/apiGw/Properties/EndpointConfiguration/Types"), "Types"))], values: {"Types": List((Path("/Resources/apiGw/Properties/EndpointConfiguration/Types"), [String((Path("/Resources/apiGw/Properties/EndpointConfiguration/Types/0"), "PRIVATE")), String((Path("/Resources/apiGw/Properties/EndpointConfiguration/Types/1"), "REGIONAL"))]))} }))} }))} }))
               |  Message: DEFAULT MESSAGE(PASS)
       BlockClause(Block[Location[file:api_gateway_private.guard, line:21, column:3]], FAIL)
           |  Message: DEFAULT MESSAGE(FAIL)
           Conjunction(cfn_guard::rules::exprs::GuardClause, FAIL)
               |  Message: DEFAULT MESSAGE(FAIL)
               Clause(Clause(Location[file:api_gateway_private.guard, line:22, column:5], Check: Properties.EndpointConfiguration.Types[*]  EQUALS String("PRIVATE")), FAIL)
                   |  From: String((Path("/Resources/apiGw/Properties/EndpointConfiguration/Types/1"), "REGIONAL"))
                   |  To: String((Path("api_gateway_private.guard/22/5/Clause/"), "PRIVATE"))
                   |  Message: (DEFAULT: NO_MESSAGE)
   ```

   `test`命令的详细输出遵循规则文件的结构。规则文件中的每个块都是详细输出中的一个块。最上面的方块是每条规则。如果存在违反该规则的`when`条件，则它们会出现在同级条件块中。在以下示例中，对条件`%api_gws !empty`进行了测试并通过了。

   ```
   rule check_rest_api_is_private when %api_gws !empty {
   ```

   条件通过后，我们将测试规则子句。

   ```
   %api_gws {
       Properties.EndpointConfiguration.Types[*] == "PRIVATE"                      
   }
   ```

   `%api_gws`是与输出中的`BlockClause`级别相对应的分组规则（第 21 行）。规则子句是一组连词 (AND) 子句，其中每个连词子句都是一组分离词。`OR`连词只有一个子句，`Properties.EndpointConfiguration.Types[*] == "PRIVATE"`。因此，详细输出显示了一个子句。路径`/Resources/apiGw/Properties/EndpointConfiguration/Types/1`显示比较输入中的哪些值，在本例中为`Types`索引为 1 的元素。

在中[根据防护规则验证输入数据](validating-rules.md)，您可以使用本节中的示例使用`validate`命令根据规则评估输入数据。