package steme import ( "encoding/hex" "testing" ) // Note: context is used in the skipped integration test // TestSignerGeneration tests keypair generation and serialization. func TestSignerGeneration(t *testing.T) { signer, err := GenerateSigner() if err != nil { t.Fatalf("GenerateSigner() failed: %v", err) } // Check public key is 64 hex chars (32 bytes) pubKey := signer.PublicKey() if len(pubKey) != 64 { t.Errorf("PublicKey() length = %d, want 64", len(pubKey)) } // Check seed is 64 hex chars (32 bytes) seed := signer.Seed() if len(seed) != 64 { t.Errorf("Seed() length = %d, want 64", len(seed)) } // Check we can reconstruct signer from seed signer2, err := NewSignerFromHex(seed) if err != nil { t.Fatalf("NewSignerFromHex() failed: %v", err) } if signer.PublicKey() != signer2.PublicKey() { t.Errorf("PublicKey mismatch after reconstruction") } } // TestSignerSignAndVerify tests signature creation and verification. func TestSignerSignAndVerify(t *testing.T) { signer, err := GenerateSigner() if err != nil { t.Fatalf("GenerateSigner() failed: %v", err) } message := []byte("test message") signature := signer.Sign(message) // Signature should be 128 hex chars (64 bytes) if len(signature) != 128 { t.Errorf("Sign() signature length = %d, want 128", len(signature)) } // Verify the signature err = Verify(signer.PublicKey(), signature, message) if err != nil { t.Errorf("Verify() failed: %v", err) } // Verify fails with wrong message err = Verify(signer.PublicKey(), signature, []byte("wrong message")) if err == nil { t.Errorf("Verify() should fail with wrong message") } } // TestAssertionBuilder tests the fluent assertion builder API. func TestAssertionBuilder(t *testing.T) { lifecycle := LifecycleApproved sourceClass := SourceClassClinical assertion := NewAssertion("Tesla_Inc", "has_revenue"). WithNumber(96.7). WithConfidence(0.95). WithLifecycle(lifecycle). WithSourceClass(sourceClass). WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000"). Build() if assertion.Subject != "Tesla_Inc" { t.Errorf("Subject = %s, want Tesla_Inc", assertion.Subject) } if assertion.Predicate != "has_revenue" { t.Errorf("Predicate = %s, want has_revenue", assertion.Predicate) } if assertion.Object.Type != "Number" { t.Errorf("Object.Type = %s, want Number", assertion.Object.Type) } if assertion.Object.Value != 96.7 { t.Errorf("Object.Value = %v, want 96.7", assertion.Object.Value) } if assertion.Confidence != 0.95 { t.Errorf("Confidence = %f, want 0.95", assertion.Confidence) } if assertion.Lifecycle == nil || *assertion.Lifecycle != LifecycleApproved { t.Errorf("Lifecycle = %v, want Approved", assertion.Lifecycle) } if assertion.SourceClass == nil || *assertion.SourceClass != SourceClassClinical { t.Errorf("SourceClass = %v, want Clinical", assertion.SourceClass) } } // TestAssertionValidation tests assertion validation. func TestAssertionValidation(t *testing.T) { tests := []struct { name string build func() Assertion wantErr bool }{ { name: "valid assertion", build: func() Assertion { return NewAssertion("Tesla_Inc", "has_revenue"). WithNumber(96.7). WithConfidence(0.95). WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000"). Build() }, wantErr: false, }, { name: "confidence too high", build: func() Assertion { return NewAssertion("Tesla_Inc", "has_revenue"). WithNumber(96.7). WithConfidence(1.5). WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000"). Build() }, wantErr: true, }, { name: "confidence negative", build: func() Assertion { return NewAssertion("Tesla_Inc", "has_revenue"). WithNumber(96.7). WithConfidence(-0.1). WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000"). Build() }, wantErr: true, }, { name: "missing source_hash", build: func() Assertion { return NewAssertion("Tesla_Inc", "has_revenue"). WithNumber(96.7). Build() }, wantErr: true, }, { name: "invalid source_hash length", build: func() Assertion { return NewAssertion("Tesla_Inc", "has_revenue"). WithNumber(96.7). WithSourceHash("00"). Build() }, wantErr: true, }, { name: "invalid source_hash hex", build: func() Assertion { return NewAssertion("Tesla_Inc", "has_revenue"). WithNumber(96.7). WithSourceHash("zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"). Build() }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assertion := tt.build() err := assertion.Validate() if (err != nil) != tt.wantErr { t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) } }) } } // TestQueryBuilder tests the fluent query builder API. func TestQueryBuilder(t *testing.T) { lens := LensConsensus lifecycle := LifecycleApproved params := NewQuery(). WithSubject("Tesla_Inc"). WithPredicate("has_revenue"). WithLens(lens). WithLifecycle(lifecycle). WithLimit(10). Build() if params.Subject == nil || *params.Subject != "Tesla_Inc" { t.Errorf("Subject = %v, want Tesla_Inc", params.Subject) } if params.Predicate == nil || *params.Predicate != "has_revenue" { t.Errorf("Predicate = %v, want has_revenue", params.Predicate) } if params.Lens == nil || *params.Lens != LensConsensus { t.Errorf("Lens = %v, want Consensus", params.Lens) } if params.Lifecycle == nil || *params.Lifecycle != LifecycleApproved { t.Errorf("Lifecycle = %v, want Approved", params.Lifecycle) } if params.Limit != 10 { t.Errorf("Limit = %d, want 10", params.Limit) } } // TestCanonicalMessage tests the canonical message generation for signing. func TestCanonicalMessage(t *testing.T) { assertion := NewAssertion("Tesla_Inc", "has_revenue"). WithNumber(96.7). WithConfidence(0.95). WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000"). Build() msg1, err := canonicalAssertionMessage(&assertion) if err != nil { t.Fatalf("canonicalAssertionMessage() failed: %v", err) } // Same assertion should produce same message msg2, err := canonicalAssertionMessage(&assertion) if err != nil { t.Fatalf("canonicalAssertionMessage() failed: %v", err) } if hex.EncodeToString(msg1) != hex.EncodeToString(msg2) { t.Errorf("Canonical message not deterministic") } // Different assertion should produce different message assertion2 := NewAssertion("Tesla_Inc", "has_revenue"). WithNumber(97.0). // Different value WithConfidence(0.95). WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000"). Build() msg3, err := canonicalAssertionMessage(&assertion2) if err != nil { t.Fatalf("canonicalAssertionMessage() failed: %v", err) } if hex.EncodeToString(msg1) == hex.EncodeToString(msg3) { t.Errorf("Different assertions produced same canonical message") } } // TestClientIntegration is a placeholder for integration tests. // // This would test against a real StemeDB server. For now, it's skipped // unless the STEMEDB_URL environment variable is set. func TestClientIntegration(t *testing.T) { t.Skip("Integration test requires running StemeDB server") // Example integration test structure: /* baseURL := os.Getenv("STEMEDB_URL") if baseURL == "" { t.Skip("STEMEDB_URL not set") } signer, err := GenerateSigner() if err != nil { t.Fatalf("GenerateSigner() failed: %v", err) } client := NewClient(baseURL, signer) // Test health check health, err := client.Health(context.Background()) if err != nil { t.Fatalf("Health() failed: %v", err) } if health.Status != "healthy" { t.Errorf("Health.Status = %s, want healthy", health.Status) } // Test assertion creation assertion := NewAssertion("test_entity", "test_predicate"). WithText("test value"). WithConfidence(0.95). WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000"). Build() hash, err := client.Assert(context.Background(), assertion) if err != nil { t.Fatalf("Assert() failed: %v", err) } if len(hash) != 64 { t.Errorf("Assert() hash length = %d, want 64", len(hash)) } // Test query params := NewQuery(). WithSubject("test_entity"). WithPredicate("test_predicate"). Build() result, err := client.Query(context.Background(), params) if err != nil { t.Fatalf("Query() failed: %v", err) } if result.TotalCount == 0 { t.Errorf("Query() returned no results") } */ } // TestSignerFromEnvNotSet tests that SignerFromEnv fails when var is not set. func TestSignerFromEnvNotSet(t *testing.T) { _, err := SignerFromEnv("NONEXISTENT_STEME_VAR_12345") if err == nil { t.Error("SignerFromEnv() should fail when env var is not set") } } // TestCanonicalMessageAllObjectTypes tests canonical message with all object types. func TestCanonicalMessageAllObjectTypes(t *testing.T) { types := []struct { name string obj ObjectValue }{ {"text", NewTextValue("hello")}, {"number", NewNumberValue(42.5)}, {"boolean", NewBooleanValue(true)}, {"reference", NewReferenceValue("Entity_123")}, } for _, tt := range types { t.Run(tt.name, func(t *testing.T) { a := NewAssertion("Subject", "Predicate"). WithConfidence(0.5). WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000"). Build() a.Object = tt.obj msg, err := canonicalAssertionMessage(&a) if err != nil { t.Errorf("canonicalAssertionMessage() failed for %s: %v", tt.name, err) } if len(msg) != 32 { // SHA256 output t.Errorf("Expected 32 bytes, got %d", len(msg)) } }) } } // TestCanonicalMessageEdgeCases tests edge cases in canonical message generation. func TestCanonicalMessageEdgeCases(t *testing.T) { tests := []struct { name string build func() Assertion wantErr bool }{ { name: "zero confidence", build: func() Assertion { return NewAssertion("S", "P"). WithText("v"). WithConfidence(0.0). WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000"). Build() }, wantErr: false, }, { name: "max confidence", build: func() Assertion { return NewAssertion("S", "P"). WithText("v"). WithConfidence(1.0). WithSourceHash("0000000000000000000000000000000000000000000000000000000000000000"). Build() }, wantErr: false, }, { name: "invalid source_hash hex", build: func() Assertion { a := NewAssertion("S", "P").WithText("v").Build() a.SourceHash = "zzzz" // Invalid hex return a }, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { a := tt.build() _, err := canonicalAssertionMessage(&a) if (err != nil) != tt.wantErr { t.Errorf("error = %v, wantErr %v", err, tt.wantErr) } }) } } // TestNewSignerInvalidSeed tests that NewSigner fails with wrong seed size. func TestNewSignerInvalidSeed(t *testing.T) { tests := []struct { name string seedLen int wantErr bool }{ {"empty seed", 0, true}, {"short seed", 16, true}, {"correct seed", 32, false}, {"long seed", 64, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { seed := make([]byte, tt.seedLen) _, err := NewSigner(seed) if (err != nil) != tt.wantErr { t.Errorf("NewSigner() error = %v, wantErr %v", err, tt.wantErr) } }) } } // TestVerifyInvalidInputs tests Verify with invalid inputs. func TestVerifyInvalidInputs(t *testing.T) { signer, _ := GenerateSigner() message := []byte("test") validSig := signer.Sign(message) tests := []struct { name string pubKey string sig string wantErr bool }{ {"valid", signer.PublicKey(), validSig, false}, {"invalid pubkey hex", "zzzz", validSig, true}, {"short pubkey", "abcd", validSig, true}, {"invalid sig hex", signer.PublicKey(), "zzzz", true}, {"short sig", signer.PublicKey(), "abcd", true}, {"wrong sig", signer.PublicKey(), hex.EncodeToString(make([]byte, 64)), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := Verify(tt.pubKey, tt.sig, message) if (err != nil) != tt.wantErr { t.Errorf("Verify() error = %v, wantErr %v", err, tt.wantErr) } }) } } // TestObjectValueConstructors tests the object value constructor functions. func TestObjectValueConstructors(t *testing.T) { tests := []struct { name string value ObjectValue wantType string wantValue any }{ { name: "text value", value: NewTextValue("hello"), wantType: "Text", wantValue: "hello", }, { name: "number value", value: NewNumberValue(42.5), wantType: "Number", wantValue: 42.5, }, { name: "boolean value", value: NewBooleanValue(true), wantType: "Boolean", wantValue: true, }, { name: "reference value", value: NewReferenceValue("Entity_Id"), wantType: "Reference", wantValue: "Entity_Id", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.value.Type != tt.wantType { t.Errorf("Type = %s, want %s", tt.value.Type, tt.wantType) } if tt.value.Value != tt.wantValue { t.Errorf("Value = %v, want %v", tt.value.Value, tt.wantValue) } }) } }