lib/api/research/research_client/insight_contract_test.go

322 lines
12 KiB
Go

package research_client_test
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"reflect"
"regexp"
"runtime"
"strings"
"testing"
"code.tnxs.net/vernonkeenan/lib/api/research/research_client/factors"
"code.tnxs.net/vernonkeenan/lib/api/research/research_client/financial_statements"
"code.tnxs.net/vernonkeenan/lib/api/research/research_client/observations"
"code.tnxs.net/vernonkeenan/lib/api/research/research_client/topics"
openapiruntime "github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
)
const researchV0716SpecSHA256 = "e7739ece74245709a8af8eab7bcffdbf1ab3e1be5763bc71a4c633b699d4df14"
type insightCaptureTransport struct {
operation *openapiruntime.ClientOperation
}
func (transport *insightCaptureTransport) Submit(operation *openapiruntime.ClientOperation) (any, error) {
return transport.SubmitContext(context.Background(), operation)
}
func (transport *insightCaptureTransport) SubmitContext(_ context.Context, operation *openapiruntime.ClientOperation) (any, error) {
transport.operation = operation
switch operation.ID {
case "getTopics":
return topics.NewGetTopicsOK(), nil
case "postTopics":
return topics.NewPostTopicsOK(), nil
case "putTopic":
return topics.NewPutTopicOK(), nil
case "getFactors":
return factors.NewGetFactorsOK(), nil
case "postFactors":
return factors.NewPostFactorsOK(), nil
case "putFactor":
return factors.NewPutFactorOK(), nil
case "getFinancialStatements":
return financial_statements.NewGetFinancialStatementsOK(), nil
case "postFinancialStatements":
return financial_statements.NewPostFinancialStatementsOK(), nil
case "putFinancialStatement":
return financial_statements.NewPutFinancialStatementOK(), nil
case "getObservations":
return observations.NewGetObservationsOK(), nil
case "postObservations":
return observations.NewPostObservationsOK(), nil
case "putObservation":
return observations.NewPutObservationOK(), nil
default:
panic("unexpected generated insight operation: " + operation.ID)
}
}
func TestGeneratedInsightOperationContract(t *testing.T) {
type operationCall func(openapiruntime.ContextualTransport) error
tests := []struct {
name string
wantID string
wantMethod string
wantPath string
call operationCall
}{
{
name: "list topics", wantID: "getTopics", wantMethod: "GET", wantPath: "/topics",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := topics.New(transport, strfmt.Default).GetTopics(topics.NewGetTopicsParams(), nil)
return err
},
},
{
name: "create topic", wantID: "postTopics", wantMethod: "POST", wantPath: "/topics",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := topics.New(transport, strfmt.Default).PostTopics(topics.NewPostTopicsParams(), nil)
return err
},
},
{
name: "update topic", wantID: "putTopic", wantMethod: "PUT", wantPath: "/topics",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := topics.New(transport, strfmt.Default).PutTopic(topics.NewPutTopicParams(), nil)
return err
},
},
{
name: "list factors", wantID: "getFactors", wantMethod: "GET", wantPath: "/factors",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := factors.New(transport, strfmt.Default).GetFactors(factors.NewGetFactorsParams(), nil)
return err
},
},
{
name: "create factor", wantID: "postFactors", wantMethod: "POST", wantPath: "/factors",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := factors.New(transport, strfmt.Default).PostFactors(factors.NewPostFactorsParams(), nil)
return err
},
},
{
name: "update factor", wantID: "putFactor", wantMethod: "PUT", wantPath: "/factors",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := factors.New(transport, strfmt.Default).PutFactor(factors.NewPutFactorParams(), nil)
return err
},
},
{
name: "list financial statements", wantID: "getFinancialStatements", wantMethod: "GET", wantPath: "/financialstatements",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := financial_statements.New(transport, strfmt.Default).GetFinancialStatements(financial_statements.NewGetFinancialStatementsParams(), nil)
return err
},
},
{
name: "create financial statement", wantID: "postFinancialStatements", wantMethod: "POST", wantPath: "/financialstatements",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := financial_statements.New(transport, strfmt.Default).PostFinancialStatements(financial_statements.NewPostFinancialStatementsParams(), nil)
return err
},
},
{
name: "update financial statement", wantID: "putFinancialStatement", wantMethod: "PUT", wantPath: "/financialstatements",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := financial_statements.New(transport, strfmt.Default).PutFinancialStatement(financial_statements.NewPutFinancialStatementParams(), nil)
return err
},
},
{
name: "list observations", wantID: "getObservations", wantMethod: "GET", wantPath: "/observations",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := observations.New(transport, strfmt.Default).GetObservations(observations.NewGetObservationsParams(), nil)
return err
},
},
{
name: "create observation", wantID: "postObservations", wantMethod: "POST", wantPath: "/observations",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := observations.New(transport, strfmt.Default).PostObservations(observations.NewPostObservationsParams(), nil)
return err
},
},
{
name: "update observation", wantID: "putObservation", wantMethod: "PUT", wantPath: "/observations",
call: func(transport openapiruntime.ContextualTransport) error {
_, err := observations.New(transport, strfmt.Default).PutObservation(observations.NewPutObservationParams(), nil)
return err
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
transport := &insightCaptureTransport{}
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 {
t.Errorf("operation ID = %q, want %q", transport.operation.ID, test.wantID)
}
if transport.operation.Method != test.wantMethod {
t.Errorf("method = %q, want %q", transport.operation.Method, test.wantMethod)
}
if transport.operation.PathPattern != test.wantPath {
t.Errorf("path = %q, want %q", transport.operation.PathPattern, test.wantPath)
}
})
}
}
func TestGeneratedInsightGetByIDFiltersAndConflictResponses(t *testing.T) {
recordID := "record-id"
if got := topics.NewGetTopicsParams().WithTopicID(&recordID).TopicID; got == nil || *got != recordID {
t.Fatal("Topic get-by-ID filter is absent")
}
if got := factors.NewGetFactorsParams().WithFactorID(&recordID).FactorID; got == nil || *got != recordID {
t.Fatal("Factor get-by-ID filter is absent")
}
if got := financial_statements.NewGetFinancialStatementsParams().WithFinancialStatementID(&recordID).FinancialStatementID; got == nil || *got != recordID {
t.Fatal("FinancialStatement get-by-ID filter is absent")
}
if got := observations.NewGetObservationsParams().WithObservationID(&recordID).ObservationID; got == nil || *got != recordID {
t.Fatal("Observation get-by-ID filter is absent")
}
conflicts := []interface{ IsCode(int) bool }{
topics.NewPutTopicConflict(),
factors.NewPutFactorConflict(),
financial_statements.NewPutFinancialStatementConflict(),
observations.NewPutObservationConflict(),
}
for _, conflict := range conflicts {
if !conflict.IsCode(409) {
t.Errorf("%T does not preserve the optimistic-conflict response", conflict)
}
}
}
func TestGeneratedInsightSurfaceDoesNotInventUnsupportedOperations(t *testing.T) {
contracts := []reflect.Type{
reflect.TypeOf((*topics.ClientService)(nil)).Elem(),
reflect.TypeOf((*factors.ClientService)(nil)).Elem(),
reflect.TypeOf((*financial_statements.ClientService)(nil)).Elem(),
reflect.TypeOf((*observations.ClientService)(nil)).Elem(),
}
for _, contract := range contracts {
for index := 0; index < contract.NumMethod(); index++ {
method := contract.Method(index).Name
if strings.Contains(strings.ToLower(method), "bulk") {
t.Errorf("%s invented unsupported bulk operation %s", contract, method)
}
}
}
topicContract := reflect.TypeOf((*topics.ClientService)(nil)).Elem()
for _, method := range []string{"DeleteTopic", "DeleteTopicContext", "DeleteTopics", "DeleteTopicsContext"} {
if _, exists := topicContract.MethodByName(method); exists {
t.Errorf("Topic client invented unsupported operation %s", method)
}
}
}
func TestResearchSpecIsPinnedToDeployedV0716Contract(t *testing.T) {
repoRoot := insightRepoRoot(t)
mainSpec, err := os.ReadFile(filepath.Join(repoRoot, "swagger", "research-vernonkeenan.yaml"))
if err != nil {
t.Fatalf("read Research spec: %v", err)
}
sum := sha256.Sum256(mainSpec)
if got := hex.EncodeToString(sum[:]); got != researchV0716SpecSHA256 {
t.Fatalf("Research spec drifted from deployed v0.7.16: SHA-256 = %s, want %s", got, researchV0716SpecSHA256)
}
externalSpec, err := os.ReadFile(filepath.Join(repoRoot, "swagger", "external", "research-vernonkeenan.yaml"))
if err != nil {
t.Fatalf("read external Research spec: %v", err)
}
normalized := bytes.ReplaceAll(externalSpec, []byte(`"https"`), []byte(`"http"`))
normalized = bytes.ReplaceAll(normalized, []byte("gw.tnxs.net"), []byte("research.vernonkeenan.com:8080"))
normalized = bytes.ReplaceAll(normalized, []byte(`"/vk/research/v1"`), []byte(`"/v1"`))
if !bytes.Equal(normalized, mainSpec) {
t.Fatal("external Research spec differs from the conventional host/scheme/base-path rewrite")
}
}
func TestGeneratedInsightSurfaceHasNoSalesforceOrEmbeddedSecrets(t *testing.T) {
repoRoot := insightRepoRoot(t)
clientRoot := filepath.Join(repoRoot, "api", "research", "research_client")
modelRoot := filepath.Join(repoRoot, "api", "research", "research_models")
targets := []string{
filepath.Join(clientRoot, "topics"),
filepath.Join(clientRoot, "factors"),
filepath.Join(clientRoot, "financial_statements"),
filepath.Join(clientRoot, "observations"),
filepath.Join(modelRoot, "topic.go"),
filepath.Join(modelRoot, "topic_request.go"),
filepath.Join(modelRoot, "topic_response.go"),
filepath.Join(modelRoot, "factor.go"),
filepath.Join(modelRoot, "factor_request.go"),
filepath.Join(modelRoot, "factor_response.go"),
filepath.Join(modelRoot, "financial_statement.go"),
filepath.Join(modelRoot, "financial_statement_request.go"),
filepath.Join(modelRoot, "financial_statement_response.go"),
filepath.Join(modelRoot, "observation.go"),
filepath.Join(modelRoot, "observation_request.go"),
filepath.Join(modelRoot, "observation_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-----"} {
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 contract target %s: %v", target, err)
}
}
}
func insightRepoRoot(t *testing.T) string {
t.Helper()
_, testFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("resolve test source path")
}
return filepath.Clean(filepath.Join(filepath.Dir(testFile), "..", "..", ".."))
}