test: target fixture resources in generated conformance requests

Salts restart per operation, so tag, untag and update cases only reached a resource when the same-numbered create case had made one. Create an IAM, ECS and ECR fixture per run and point every operation but a create or delete at it, and give ECR's policy and manifest documents values AWS accepts.
This commit is contained in:
Julian Sommer
2026-09-29 11:14:37 +10:00
parent b3be052311
commit bfad1dc004
4 changed files with 92 additions and 2 deletions
+5
View File
@@ -75,6 +75,11 @@ func GenerateRequests(service Service, operationName string, options RequestOpti
return RequestPlan{Cases: []RequestCase{{Operation: operationName, Input: map[string]any{}}}, DeclaredErrors: declared}, nil
}
// A create or delete names a resource of its own, so it neither collides
// with a fixture nor removes one.
if strings.HasPrefix(operationName, "Create") || strings.HasPrefix(operationName, "Delete") {
options.Fixtures = nil
}
first := model.newRequestGenerator(0, options)
full, ok := first.structure(operation.Input.Shape, nil, 0)
plan := RequestPlan{Skipped: first.skipped, Unbroken: first.unbroken, DeclaredErrors: declared}
+18 -1
View File
@@ -18,10 +18,12 @@ import (
// RequestOptions places generated ARNs in the account and region the
// requests are sent as. AccessKeyID is the key the requests are signed with.
// Fixtures maps a member to an existing resource it can name.
type RequestOptions struct {
AccountID string
Region string
AccessKeyID string
Fixtures map[string]string
}
// Models constrain most identifiers only by length, so "spxa" is a model-valid
@@ -82,6 +84,11 @@ var conditionallyRequired = map[Service]map[string][]string{
}
const (
// imageManifest is well formed but names blobs no repository holds.
imageManifest = `{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/vnd.docker.container.image.v1+json","size":2,"digest":"sha256:0000000000000000000000000000000000000000000000000000000000000000"},"layers":[]}`
lifecyclePolicy = `{"rules":[{"rulePriority":1,"description":"expire untagged","selection":{"tagStatus":"untagged","countType":"sinceImagePushed","countUnit":"days","countNumber":14},"action":{"type":"expire"}}]}`
repositoryPolicy = `{"Version":"2012-10-17","Statement":[{"Sid":"pull","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::%s:root"},"Action":"ecr:BatchGetImage"}]}`
policyDocument = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`
trustPolicyDocument = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}`
)
@@ -94,6 +101,9 @@ func (g *requestGenerator) hint(path []pathStep) (string, bool) {
return "", false
}
options := g.options
if fixture, ok := options.Fixtures[member]; ok {
return fixture, true
}
suffix := fmt.Sprintf("%017x", g.saltNumber)
name := "spx" + g.salt
@@ -157,8 +167,15 @@ func (g *requestGenerator) hint(path []pathStep) (string, bool) {
return name + ".example.com", true
}
case ECR:
if member == "resourceArn" {
switch member {
case "resourceArn":
return fmt.Sprintf("arn:aws:ecr:%s:%s:repository/%s", options.Region, options.AccountID, name), true
case "imageManifest":
return imageManifest, true
case "lifecyclePolicyText":
return lifecyclePolicy, true
case "policyText":
return fmt.Sprintf(repositoryPolicy, options.AccountID), true
}
case EKS:
switch member {
+15
View File
@@ -226,6 +226,21 @@ func TestGenerateRequestsSeedsConditionallyRequiredMembers(t *testing.T) {
require.NotZero(t, nested)
}
func TestGenerateRequestsTargetsFixturesOutsideCreateAndDelete(t *testing.T) {
options := testRequestOptions
options.Fixtures = map[string]string{"RoleName": "fixture"}
for operation, wantFixture := range map[string]bool{"TagRole": true, "CreateRole": false, "DeleteRole": false} {
plan, err := GenerateRequests(IAM, operation, options)
require.NoError(t, err)
for _, request := range plan.Cases {
if request.Path == "$.RoleName" {
continue
}
require.Equal(t, wantFixture, request.Input["RoleName"] == "fixture", "%s %s %s", operation, request.Constraint, request.Path)
}
}
}
func TestGenerateRequestsAttributesRejections(t *testing.T) {
plan, err := GenerateRequests(IAM, "CreateRole", testRequestOptions)
require.NoError(t, err)
+54 -1
View File
@@ -8,6 +8,7 @@ import (
"io"
"net"
"net/http"
"strings"
"testing"
"time"
@@ -50,8 +51,10 @@ func TestRequestConformance(t *testing.T) {
startRequestConformanceBackends(t, gw)
sender, err := newRequestSender(gw, service)
require.NoError(t, err)
options := awsmodel.RequestOptions{AccountID: gw.AccountID, Region: testRegion, AccessKeyID: testAccessKeyID}
options.Fixtures = createRequestFixtures(t, sender, service, gw.AccountID)
for _, operation := range coverage.Implemented {
plan, err := awsmodel.GenerateRequests(service, operation, awsmodel.RequestOptions{AccountID: gw.AccountID, Region: testRegion, AccessKeyID: testAccessKeyID})
plan, err := awsmodel.GenerateRequests(service, operation, options)
require.NoError(t, err, operation)
suiteRequestConformance.recordOperation(service, operation, plan)
judge := plan.NewJudge()
@@ -82,6 +85,56 @@ func startRequestConformanceBackends(t *testing.T, gw *Gateway) {
StartServiceDaemonLite(t, gw)
}
// requestFixture is the name every fixture resource takes.
const requestFixture = "conformance-fixture"
// createRequestFixtures creates one resource of each kind the service's
// generated requests name, so a case reaches its resource instead of ending
// in not-found. It returns the members that name them.
func createRequestFixtures(t *testing.T, sender *requestSender, service awsmodel.Service, accountID string) map[string]string {
t.Helper()
create := func(operation string, input map[string]any) {
t.Helper()
result, err := sender.send(awsmodel.RequestCase{Operation: operation, Input: input})
require.NoError(t, err, operation)
require.True(t, result.Status >= 200 && result.Status < 300, "create fixture: %s: %d %s: %s", operation, result.Status, result.Code, result.Message)
}
switch service {
case awsmodel.IAM:
const oidcHost = "fixture.example.com"
create("CreateRole", map[string]any{"RoleName": requestFixture, "AssumeRolePolicyDocument": `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}]}`})
create("CreatePolicy", map[string]any{"PolicyName": requestFixture, "PolicyDocument": `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}`})
create("CreateOpenIDConnectProvider", map[string]any{"Url": "https://" + oidcHost, "ClientIDList": []any{"sts.amazonaws.com"}, "ThumbprintList": []any{strings.Repeat("a", 40)}})
create("CreateInstanceProfile", map[string]any{"InstanceProfileName": requestFixture})
create("CreateGroup", map[string]any{"GroupName": requestFixture})
create("CreateUser", map[string]any{"UserName": requestFixture})
return map[string]string{
"RoleName": requestFixture,
"PolicyArn": "arn:aws:iam::" + accountID + ":policy/" + requestFixture,
"OpenIDConnectProviderArn": "arn:aws:iam::" + accountID + ":oidc-provider/" + oidcHost,
"InstanceProfileName": requestFixture,
"GroupName": requestFixture,
"UserName": requestFixture,
}
case awsmodel.ECS:
create("CreateCluster", map[string]any{"clusterName": requestFixture})
return map[string]string{
"cluster": requestFixture,
"clusters": requestFixture,
"resourceArn": "arn:aws:ecs:" + testRegion + ":" + accountID + ":cluster/" + requestFixture,
}
case awsmodel.ECR:
create("CreateRepository", map[string]any{"repositoryName": requestFixture})
return map[string]string{
"repositoryName": requestFixture,
"repositoryNames": requestFixture,
"resourceArn": "arn:aws:ecr:" + testRegion + ":" + accountID + ":repository/" + requestFixture,
}
default:
return nil
}
}
type requestSender struct {
endpoint string
service awsmodel.Service