mirror of https://github.com/vernonkeenan/lib
322 lines
12 KiB
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), "..", "..", ".."))
|
||
|
|
}
|