mirror of https://github.com/vernonkeenan/lib
244 lines
8.7 KiB
Go
244 lines
8.7 KiB
Go
|
|
package members_client_test
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"os"
|
||
|
|
"path/filepath"
|
||
|
|
"reflect"
|
||
|
|
"regexp"
|
||
|
|
"strings"
|
||
|
|
"testing"
|
||
|
|
|
||
|
|
"code.tnxs.net/vernonkeenan/lib/api/members/members_client"
|
||
|
|
"code.tnxs.net/vernonkeenan/lib/api/members/members_client/transactions"
|
||
|
|
"code.tnxs.net/vernonkeenan/lib/api/members/members_models"
|
||
|
|
openapiruntime "github.com/go-openapi/runtime"
|
||
|
|
"github.com/go-openapi/strfmt"
|
||
|
|
)
|
||
|
|
|
||
|
|
type transactionCaptureTransport struct {
|
||
|
|
operation *openapiruntime.ClientOperation
|
||
|
|
}
|
||
|
|
|
||
|
|
func (transport *transactionCaptureTransport) Submit(
|
||
|
|
operation *openapiruntime.ClientOperation,
|
||
|
|
) (any, error) {
|
||
|
|
return transport.SubmitContext(context.Background(), operation)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (transport *transactionCaptureTransport) SubmitContext(
|
||
|
|
_ context.Context,
|
||
|
|
operation *openapiruntime.ClientOperation,
|
||
|
|
) (any, error) {
|
||
|
|
transport.operation = operation
|
||
|
|
switch operation.ID {
|
||
|
|
case "getTransactions":
|
||
|
|
return transactions.NewGetTransactionsOK(), nil
|
||
|
|
case "postTransactions":
|
||
|
|
return transactions.NewPostTransactionsOK(), nil
|
||
|
|
case "putTransactions":
|
||
|
|
return transactions.NewPutTransactionsOK(), nil
|
||
|
|
default:
|
||
|
|
panic("unexpected generated Transaction operation: " + operation.ID)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGeneratedTransactionOperationContract(t *testing.T) {
|
||
|
|
tenantID := strfmt.UUID("55555555-5555-4555-8555-555555555555")
|
||
|
|
auth := openapiruntime.ClientAuthInfoWriterFunc(
|
||
|
|
func(openapiruntime.ClientRequest, strfmt.Registry) error { return nil },
|
||
|
|
)
|
||
|
|
type operationCall func(openapiruntime.ContextualTransport) error
|
||
|
|
tests := []struct {
|
||
|
|
name, wantID, wantMethod string
|
||
|
|
call operationCall
|
||
|
|
}{
|
||
|
|
{
|
||
|
|
name: "list or get", wantID: "getTransactions", wantMethod: "GET",
|
||
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
||
|
|
_, err := transactions.New(transport, strfmt.Default).GetTransactions(
|
||
|
|
transactions.NewGetTransactionsParams().WithTenantID(tenantID), auth,
|
||
|
|
)
|
||
|
|
return err
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "create", wantID: "postTransactions", wantMethod: "POST",
|
||
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
||
|
|
_, err := transactions.New(transport, strfmt.Default).PostTransactions(
|
||
|
|
transactions.NewPostTransactionsParams().WithTenantID(tenantID), auth,
|
||
|
|
)
|
||
|
|
return err
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
name: "CAS transition", wantID: "putTransactions", wantMethod: "PUT",
|
||
|
|
call: func(transport openapiruntime.ContextualTransport) error {
|
||
|
|
_, err := transactions.New(transport, strfmt.Default).PutTransactions(
|
||
|
|
transactions.NewPutTransactionsParams().WithTenantID(tenantID), auth,
|
||
|
|
)
|
||
|
|
return err
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, test := range tests {
|
||
|
|
t.Run(test.name, func(t *testing.T) {
|
||
|
|
transport := &transactionCaptureTransport{}
|
||
|
|
if err := test.call(transport); err != nil {
|
||
|
|
t.Fatalf("generated client operation failed: %v", err)
|
||
|
|
}
|
||
|
|
if transport.operation == nil {
|
||
|
|
t.Fatal("generated client did not submit an operation")
|
||
|
|
}
|
||
|
|
if transport.operation.ID != test.wantID ||
|
||
|
|
transport.operation.Method != test.wantMethod ||
|
||
|
|
transport.operation.PathPattern != "/transactions" {
|
||
|
|
t.Fatalf(
|
||
|
|
"operation = %s %s %s, want %s %s /transactions",
|
||
|
|
transport.operation.ID, transport.operation.Method,
|
||
|
|
transport.operation.PathPattern, test.wantID, test.wantMethod,
|
||
|
|
)
|
||
|
|
}
|
||
|
|
if transport.operation.AuthInfo == nil {
|
||
|
|
t.Fatal("generated operation discarded its compound auth writer")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTransactionTenantBindingExactScopesAndCAS(t *testing.T) {
|
||
|
|
specText := readMembersLearningSpec(t)
|
||
|
|
pathBlock := learningSpecPathBlock(t, specText, "/transactions")
|
||
|
|
for _, forbidden := range []string{" delete:", " patch:"} {
|
||
|
|
if strings.Contains(pathBlock, forbidden) {
|
||
|
|
t.Errorf("/transactions exposes unsupported %s", strings.TrimSpace(forbidden))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
operations := map[string]struct{ id, scope string }{
|
||
|
|
"get": {id: "getTransactions", scope: "members:transaction:read"},
|
||
|
|
"post": {id: "postTransactions", scope: "members:transaction:create"},
|
||
|
|
"put": {id: "putTransactions", scope: "members:transaction:update"},
|
||
|
|
}
|
||
|
|
const compoundSecurity = " security:\n - ApiKeyAuth: []\n kvSessionCookie: []"
|
||
|
|
for method, expected := range operations {
|
||
|
|
block := learningSpecOperationBlock(t, specText, "/transactions", method)
|
||
|
|
if !strings.Contains(block, " operationId: "+expected.id+"\n") {
|
||
|
|
t.Errorf("%s /transactions lost operationId %q", strings.ToUpper(method), expected.id)
|
||
|
|
}
|
||
|
|
if strings.Count(block, compoundSecurity) != 1 {
|
||
|
|
t.Errorf("%s /transactions does not preserve compound authentication", strings.ToUpper(method))
|
||
|
|
}
|
||
|
|
if !strings.Contains(block, expected.scope) {
|
||
|
|
t.Errorf("%s /transactions lost exact scope %q", strings.ToUpper(method), expected.scope)
|
||
|
|
}
|
||
|
|
if !strings.Contains(block, `- $ref: "#/parameters/transactionTenantId"`) {
|
||
|
|
t.Errorf("%s /transactions is not tenantId-bound", strings.ToUpper(method))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
tenantID := strfmt.UUID("55555555-5555-4555-8555-555555555555")
|
||
|
|
recordID := strfmt.UUID("66666666-6666-4666-8666-666666666666")
|
||
|
|
get := transactions.NewGetTransactionsParams().
|
||
|
|
WithTenantID(tenantID).
|
||
|
|
WithTransactionID(&recordID)
|
||
|
|
if get.TenantID != tenantID || get.TransactionID == nil ||
|
||
|
|
*get.TransactionID != recordID {
|
||
|
|
t.Fatal("Transaction list/get client lost its tenant or record filter")
|
||
|
|
}
|
||
|
|
if transactions.NewPostTransactionsParams().WithTenantID(tenantID).TenantID != tenantID {
|
||
|
|
t.Fatal("Transaction create client lost required tenantId")
|
||
|
|
}
|
||
|
|
if transactions.NewPutTransactionsParams().WithTenantID(tenantID).TenantID != tenantID {
|
||
|
|
t.Fatal("Transaction update client lost required tenantId")
|
||
|
|
}
|
||
|
|
if !transactions.NewPutTransactionsConflict().IsCode(409) {
|
||
|
|
t.Fatal("Transaction update client lost optimistic concurrency response")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTransactionModelIsExactDecimalAndPaymentTokenFree(t *testing.T) {
|
||
|
|
assertExactModelFields(t, members_models.Transaction{}, map[string]string{
|
||
|
|
"Amount": "string", "CreatedByID": "*strfmt.UUID",
|
||
|
|
"CreatedDate": "*strfmt.DateTime", "Currency": "string",
|
||
|
|
"ID": "strfmt.UUID", "LastModifiedByID": "*strfmt.UUID",
|
||
|
|
"LastModifiedDate": "*strfmt.DateTime", "Status": "*string",
|
||
|
|
"TenantID": "*strfmt.UUID", "TransactionDate": "*strfmt.DateTime",
|
||
|
|
})
|
||
|
|
|
||
|
|
valid := &members_models.TransactionRequest{Data: []*members_models.Transaction{{
|
||
|
|
Amount: "10.25", Currency: "USD",
|
||
|
|
}}}
|
||
|
|
if err := valid.Validate(strfmt.Default); err != nil {
|
||
|
|
t.Fatalf("generated Transaction request rejected one valid record: %v", err)
|
||
|
|
}
|
||
|
|
valid.Data = append(valid.Data, &members_models.Transaction{Amount: "11.00", Currency: "USD"})
|
||
|
|
if err := valid.Validate(strfmt.Default); err == nil {
|
||
|
|
t.Fatal("generated Transaction request accepted a bulk payload")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestTransactionSurfaceHasNoDeleteOrProtectedRelationshipMethods(t *testing.T) {
|
||
|
|
contract := reflect.TypeOf((*transactions.ClientService)(nil)).Elem()
|
||
|
|
for index := 0; index < contract.NumMethod(); index++ {
|
||
|
|
method := contract.Method(index).Name
|
||
|
|
for _, forbidden := range []string{
|
||
|
|
"Delete", "Payment", "Processor", "Database", "DSN",
|
||
|
|
} {
|
||
|
|
if strings.Contains(method, forbidden) {
|
||
|
|
t.Errorf("Transaction client exposes forbidden method %s", method)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
root := reflect.TypeOf(members_client.Members{})
|
||
|
|
if _, exists := root.FieldByName("Transactions"); !exists {
|
||
|
|
t.Fatal("root Members client does not expose governed Transactions")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestGeneratedTransactionSurfaceHasNoExternalBypassOrSecrets(t *testing.T) {
|
||
|
|
repoRoot := learningRepoRoot(t)
|
||
|
|
targets := []string{
|
||
|
|
filepath.Join(repoRoot, "api", "members", "members_client", "transactions"),
|
||
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "transaction.go"),
|
||
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "transaction_request.go"),
|
||
|
|
filepath.Join(repoRoot, "api", "members", "members_models", "transaction_response.go"),
|
||
|
|
}
|
||
|
|
credentialLiteral := regexp.MustCompile(
|
||
|
|
`(?i)(api[_-]?key|password|secret)\s*[:=]\s*["'][^"']+["']`,
|
||
|
|
)
|
||
|
|
for _, target := range targets {
|
||
|
|
err := filepath.WalkDir(target, func(
|
||
|
|
path string, entry os.DirEntry, walkErr error,
|
||
|
|
) error {
|
||
|
|
if walkErr != nil {
|
||
|
|
return walkErr
|
||
|
|
}
|
||
|
|
if entry.IsDir() || !strings.HasSuffix(path, ".go") ||
|
||
|
|
strings.HasSuffix(path, "_test.go") {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
content, err := os.ReadFile(path)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
lower := strings.ToLower(string(content))
|
||
|
|
for _, forbidden := range []string{
|
||
|
|
"salesforce", "sf-gate", "go-force", "private key-----",
|
||
|
|
"members_client/databases", "processortoken", "dsn",
|
||
|
|
} {
|
||
|
|
if strings.Contains(lower, forbidden) {
|
||
|
|
t.Errorf("%s contains forbidden boundary %q", path, forbidden)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if credentialLiteral.Match(content) {
|
||
|
|
t.Errorf("%s contains a credential-shaped literal", path)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("scan generated Transaction target %s: %v", target, err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|