lib/api/research/research_client/portfolio_reconciliation_co...

296 lines
11 KiB
Go
Raw Normal View History

package research_client_test
import (
"context"
"encoding/json"
"path/filepath"
"reflect"
"runtime"
"strings"
"testing"
"code.tnxs.net/vernonkeenan/lib/api/research/research_client/portfolio_enrichment_runs"
"code.tnxs.net/vernonkeenan/lib/api/research/research_models"
"github.com/go-openapi/loads"
openapiruntime "github.com/go-openapi/runtime"
"github.com/go-openapi/strfmt"
)
type reconciliationCaptureTransport struct {
operation *openapiruntime.ClientOperation
}
func (transport *reconciliationCaptureTransport) Submit(
operation *openapiruntime.ClientOperation,
) (any, error) {
return transport.SubmitContext(context.Background(), operation)
}
func (transport *reconciliationCaptureTransport) SubmitContext(
_ context.Context,
operation *openapiruntime.ClientOperation,
) (any, error) {
transport.operation = operation
return portfolio_enrichment_runs.NewPostPortfolioReconciliationOK(), nil
}
func TestGeneratedPortfolioReconciliationOperationContract(t *testing.T) {
transport := &reconciliationCaptureTransport{}
runID := strfmt.UUID("11111111-1111-4111-8111-111111111111")
request := validServiceUpdateReconciliationRequest()
_, err := portfolio_enrichment_runs.New(transport, strfmt.Default).
PostPortfolioReconciliation(
portfolio_enrichment_runs.NewPostPortfolioReconciliationParams().
WithPortfolioEnrichmentRunID(runID).
WithPortfolioReconciliationRequest(request),
nil,
)
if err != nil {
t.Fatalf("generated reconciliation client failed: %v", err)
}
if transport.operation == nil {
t.Fatal("generated reconciliation client did not submit an operation")
}
if got := transport.operation.ID; got != "postPortfolioReconciliation" {
t.Errorf("operation ID = %q, want postPortfolioReconciliation", got)
}
if got := transport.operation.Method; got != "POST" {
t.Errorf("method = %q, want POST", got)
}
if got := transport.operation.PathPattern; got !=
"/portfolioenrichmentruns/{portfolioEnrichmentRunId}/reconciliations" {
t.Errorf("path = %q, want the portfolio reconciliation path", got)
}
}
func TestGeneratedServiceUpdateReconciliationRequestShape(t *testing.T) {
request := validServiceUpdateReconciliationRequest()
if err := request.Validate(strfmt.Default); err != nil {
t.Fatalf("valid Service update request failed generated validation: %v", err)
}
if got := *request.Action; got != research_models.PortfolioReconciliationRequestActionUpdate {
t.Errorf("Action = %q, want update", got)
}
if got := *request.EntityType; got !=
research_models.PortfolioReconciliationRequestEntityTypeCompanyService {
t.Errorf("EntityType = %q, want company_service", got)
}
encoded, err := json.Marshal(request)
if err != nil {
t.Fatalf("marshal generated Service update request: %v", err)
}
for _, serverOwned := range []string{
"TargetID", "TargetBaseVersion", "TargetBeforeVersion", "TargetResultVersion",
} {
if strings.Contains(string(encoded), `"`+serverOwned+`"`) {
t.Errorf("caller request serialized server-owned %s", serverOwned)
}
}
}
func TestGeneratedReconciliationRequestStrictlyExcludesCallerOwnedTargetAndVersions(
t *testing.T,
) {
requestType := reflect.TypeOf(research_models.PortfolioReconciliationRequest{})
for _, serverOwned := range []string{
"TargetID", "TargetBaseVersion", "TargetBeforeVersion", "TargetResultVersion",
} {
if _, exists := requestType.FieldByName(serverOwned); exists {
t.Errorf("generated request exposes server-owned field %s", serverOwned)
}
}
document, err := loads.Spec(filepath.Join(
portfolioReconciliationRepoRoot(t), "swagger", "research-vernonkeenan.yaml",
))
if err != nil {
t.Fatalf("load generated Research provider projection: %v", err)
}
schema := document.Spec().Definitions["PortfolioReconciliationRequest"]
if schema.AdditionalProperties == nil || schema.AdditionalProperties.Allows {
t.Fatal("PortfolioReconciliationRequest does not preserve additionalProperties: false")
}
for _, serverOwned := range []string{
"TargetID", "TargetBaseVersion", "TargetBeforeVersion", "TargetResultVersion",
} {
if _, exists := schema.Properties[serverOwned]; exists {
t.Errorf("provider request schema exposes server-owned property %s", serverOwned)
}
}
}
func TestGeneratedServiceUpdateReceiptCarriesBeforeAndResultVersions(t *testing.T) {
before, err := strfmt.ParseDateTime("2026-07-27T12:00:00Z")
if err != nil {
t.Fatalf("parse before version: %v", err)
}
result, err := strfmt.ParseDateTime("2026-07-27T12:00:01Z")
if err != nil {
t.Fatalf("parse result version: %v", err)
}
created, err := strfmt.ParseDateTime("2026-07-27T12:00:02Z")
if err != nil {
t.Fatalf("parse receipt created date: %v", err)
}
action := research_models.PortfolioReconciliationReceiptActionUpdate
entityType := research_models.PortfolioReconciliationReceiptEntityTypeCompanyService
mode := research_models.PortfolioReconciliationReceiptModeDiscovery
principalKey := "studio"
policyRevision := int64(2)
receiptID := strfmt.UUID("11111111-1111-4111-8111-111111111111")
runID := strfmt.UUID("22222222-2222-4222-8222-222222222222")
candidateID := strfmt.UUID("33333333-3333-4333-8333-333333333333")
targetID := strfmt.UUID("44444444-4444-4444-8444-444444444444")
actorID := strfmt.UUID("55555555-5555-4555-8555-555555555555")
principalID := strfmt.UUID("66666666-6666-4666-8666-666666666666")
receipt := &research_models.PortfolioReconciliationReceipt{
ID: &receiptID,
RunID: &runID,
CandidateID: &candidateID,
Mode: &mode,
EntityType: &entityType,
Action: &action,
TargetID: &targetID,
TargetBeforeVersion: before,
TargetResultVersion: &result,
PolicyRevision: &policyRevision,
ActorUserID: &actorID,
ClientPrincipalID: &principalID,
ClientPrincipalKey: &principalKey,
CreatedDate: &created,
}
if err := receipt.Validate(strfmt.Default); err != nil {
t.Fatalf("valid Service update receipt failed generated validation: %v", err)
}
encoded, err := json.Marshal(receipt)
if err != nil {
t.Fatalf("marshal generated Service update receipt: %v", err)
}
var decoded research_models.PortfolioReconciliationReceipt
if err := json.Unmarshal(encoded, &decoded); err != nil {
t.Fatalf("unmarshal generated Service update receipt: %v", err)
}
if !reflect.DeepEqual(decoded.TargetBeforeVersion, before) {
t.Errorf("TargetBeforeVersion = %s, want %s", decoded.TargetBeforeVersion, before)
}
if decoded.TargetResultVersion == nil ||
!reflect.DeepEqual(*decoded.TargetResultVersion, result) {
t.Errorf("TargetResultVersion = %v, want %s", decoded.TargetResultVersion, result)
}
}
func TestGeneratedProductUpdateReconciliationRequestShape(t *testing.T) {
request := validProductUpdateReconciliationRequest()
if err := request.Validate(strfmt.Default); err != nil {
t.Fatalf("valid Product update request failed generated validation: %v", err)
}
if got := *request.Action; got != research_models.PortfolioReconciliationRequestActionUpdate {
t.Errorf("Action = %q, want update", got)
}
if got := *request.EntityType; got !=
research_models.PortfolioReconciliationRequestEntityTypeCompanyProduct {
t.Errorf("EntityType = %q, want company_product", got)
}
}
func TestPortfolioReconciliationSupportsProductUpdatesWithoutConvertingLegacyRuns(t *testing.T) {
document, err := loads.Spec(filepath.Join(
portfolioReconciliationRepoRoot(t), "swagger", "research-vernonkeenan.yaml",
))
if err != nil {
t.Fatalf("load generated Research provider projection: %v", err)
}
reconciliation := document.Spec().Paths.Paths["/portfolioenrichmentruns/{portfolioEnrichmentRunId}/reconciliations"]
if reconciliation.Post == nil {
t.Fatal("portfolio reconciliation POST is absent")
}
if !strings.Contains(
reconciliation.Post.Description,
"Create or compare-and-swap update exactly one authoritative Product or Service",
) {
t.Fatal("reconciliation contract does not expose Product direct update")
}
if !strings.Contains(
reconciliation.Post.Description,
"Legacy Product governed-update candidates are not silently converted to direct writes",
) {
t.Fatal("reconciliation contract does not keep legacy Product runs fail-closed")
}
if document.Spec().Paths.Paths["/companyproducts"].Put == nil {
t.Fatal("provider projection is missing direct CompanyProduct PUT")
}
if _, ok := reconciliation.Post.Responses.StatusCodeResponses[422]; !ok {
t.Fatal("reconciliation contract does not expose invalid or legacy Product update rejection")
}
if !portfolio_enrichment_runs.
NewPostPortfolioReconciliationUnprocessableEntity().
IsCode(422) {
t.Fatal("generated client does not preserve Product update rejection status 422")
}
}
func TestPortfolioReconciliationRequiresCompoundAuthentication(t *testing.T) {
document, err := loads.Spec(filepath.Join(
portfolioReconciliationRepoRoot(t), "swagger", "research-vernonkeenan.yaml",
))
if err != nil {
t.Fatalf("load generated Research provider projection: %v", err)
}
operation := document.Spec().Paths.Paths["/portfolioenrichmentruns/{portfolioEnrichmentRunId}/reconciliations"].Post
if len(operation.Security) != 1 {
t.Fatalf("security alternatives = %d, want one compound alternative", len(operation.Security))
}
if _, ok := operation.Security[0]["ApiKeyAuth"]; !ok {
t.Fatal("reconciliation operation is missing ApiKeyAuth")
}
if _, ok := operation.Security[0]["kvSessionCookie"]; !ok {
t.Fatal("reconciliation operation is missing kvSessionCookie")
}
}
func validServiceUpdateReconciliationRequest() *research_models.PortfolioReconciliationRequest {
acknowledged := true
action := research_models.PortfolioReconciliationRequestActionUpdate
entityType := research_models.PortfolioReconciliationRequestEntityTypeCompanyService
mode := research_models.PortfolioReconciliationRequestModeDiscovery
idempotencyKey := "studio-service-update-0001"
candidateID := strfmt.UUID("11111111-1111-4111-8111-111111111111")
fieldID := strfmt.UUID("22222222-2222-4222-8222-222222222222")
fieldName := research_models.PortfolioReconciliationFieldSelectionFieldNameTagline
value := "Bounded service update"
return &research_models.PortfolioReconciliationRequest{
Acknowledged: &acknowledged,
Action: &action,
CandidateID: &candidateID,
EntityType: &entityType,
Fields: []*research_models.PortfolioReconciliationFieldSelection{{
CandidateFieldID: &fieldID,
FieldName: &fieldName,
Value: &value,
}},
IdempotencyKey: &idempotencyKey,
Mode: &mode,
}
}
func validProductUpdateReconciliationRequest() *research_models.PortfolioReconciliationRequest {
request := validServiceUpdateReconciliationRequest()
entityType := research_models.PortfolioReconciliationRequestEntityTypeCompanyProduct
idempotencyKey := "studio-product-update-0001"
request.EntityType = &entityType
request.IdempotencyKey = &idempotencyKey
return request
}
func portfolioReconciliationRepoRoot(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), "..", "..", ".."))
}