diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f175a6..0cd1802 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ concurrency: cancel-in-progress: true env: - COVER_MIN: "90" + COVER_MIN: "95" jobs: lint: diff --git a/pkg/pluginsdk/config/config_more_test.go b/pkg/pluginsdk/config/config_more_test.go index 703908a..3b35054 100644 --- a/pkg/pluginsdk/config/config_more_test.go +++ b/pkg/pluginsdk/config/config_more_test.go @@ -35,6 +35,12 @@ func TestValidateManifestNilAndEmptySchema(t *testing.T) { if err := config.ValidateManifestGlobalValue(manifest, "bad", map[string]any{}); err == nil { t.Fatal("expected invalid schema JSON error") } + if err := config.ValidateValue(&pluginv1.ConfigSchema{ + Key: "bad-schema", + JsonSchema: `{"type":42}`, + }, "plugin global config", "bad-schema", map[string]any{}); err == nil { + t.Fatal("expected schema compile error") + } if err := config.ValidateManifestUserValue(manifest, "prefs", map[string]any{"x": 1}); err != nil { t.Fatalf("user value: %v", err) } diff --git a/pkg/pluginsdk/convert/convert_more_test.go b/pkg/pluginsdk/convert/convert_more_test.go index 10f522e..7813541 100644 --- a/pkg/pluginsdk/convert/convert_more_test.go +++ b/pkg/pluginsdk/convert/convert_more_test.go @@ -126,6 +126,34 @@ func TestDecodeCapability_FullMetadataAndErrors(t *testing.T) { t.Fatal("expected error") } }) + + t.Run("nil metadata value becomes empty struct", func(t *testing.T) { + got, err := convert.DecodeCapability(convert.CapabilityRecord{ + Metadata: map[string]any{ + "metadata": nil, + }, + }) + if err != nil { + t.Fatalf("DecodeCapability: %v", err) + } + if got.GetMetadata() == nil || len(got.GetMetadata().GetFields()) != 0 { + t.Fatalf("metadata = %+v, want empty struct", got.GetMetadata()) + } + }) + + t.Run("non map metadata value becomes empty struct", func(t *testing.T) { + got, err := convert.DecodeCapability(convert.CapabilityRecord{ + Metadata: map[string]any{ + "metadata": []any{"ignored"}, + }, + }) + if err != nil { + t.Fatalf("DecodeCapability: %v", err) + } + if got.GetMetadata() == nil || len(got.GetMetadata().GetFields()) != 0 { + t.Fatalf("metadata = %+v, want empty struct", got.GetMetadata()) + } + }) } func TestCapabilityRecordsFromManifest_ErrorsAndExtras(t *testing.T) { diff --git a/pkg/pluginsdk/manifest/checksum_test.go b/pkg/pluginsdk/manifest/checksum_test.go index 9b16404..5d1aa49 100644 --- a/pkg/pluginsdk/manifest/checksum_test.go +++ b/pkg/pluginsdk/manifest/checksum_test.go @@ -60,3 +60,12 @@ func TestLoadWithChecksumAppliesVersionOverrideBeforeValidation(t *testing.T) { t.Fatalf("version override: got %q want 9.9.9", m.GetVersion()) } } + +func TestLoadWithChecksumRejectsInvalidManifest(t *testing.T) { + if _, err := LoadWithChecksum([]byte(`not-json`), ""); err == nil { + t.Fatal("expected decode error") + } + if _, err := LoadWithChecksum([]byte(`{"plugin_id":"prairie.invalid"}`), ""); err == nil { + t.Fatal("expected validation error") + } +} diff --git a/pkg/pluginsdk/manifest/watch_sync_provider_test.go b/pkg/pluginsdk/manifest/watch_sync_provider_test.go index b0b39b6..5123ad6 100644 --- a/pkg/pluginsdk/manifest/watch_sync_provider_test.go +++ b/pkg/pluginsdk/manifest/watch_sync_provider_test.go @@ -46,6 +46,22 @@ func TestValidateWatchSyncProviderRejectsMissingDescriptor(t *testing.T) { } } +func TestValidateWatchSyncProviderRejectsUnsafeID(t *testing.T) { + manifest := validWatchSyncManifest() + manifest.Capabilities[0].Id = "Not Safe" + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected unsafe capability id to fail") + } +} + +func TestValidateWatchSyncProviderRejectsEmptyAuthMethods(t *testing.T) { + manifest := validWatchSyncManifest() + manifest.Capabilities[0].WatchSyncProvider.AuthMethods = nil + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected empty auth methods to fail") + } +} + func TestValidateWatchSyncProviderRejectsUnspecifiedAuthMethod(t *testing.T) { manifest := validWatchSyncManifest() manifest.Capabilities[0].WatchSyncProvider.AuthMethods = []pluginv1.WatchSyncAuthMethod{ @@ -56,6 +72,22 @@ func TestValidateWatchSyncProviderRejectsUnspecifiedAuthMethod(t *testing.T) { } } +func TestValidateWatchSyncProviderRejectsEmptyOperations(t *testing.T) { + manifest := validWatchSyncManifest() + manifest.Capabilities[0].WatchSyncProvider.ExportWatched = false + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected no enabled operations to fail") + } +} + +func TestValidateWatchSyncProviderRejectsInvalidBatchSize(t *testing.T) { + manifest := validWatchSyncManifest() + manifest.Capabilities[0].WatchSyncProvider.MaxBatchSize = 101 + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected invalid batch size to fail") + } +} + func TestValidateWatchSyncProviderRejectsEmptyMediaTypes(t *testing.T) { manifest := validWatchSyncManifest() manifest.Capabilities[0].WatchSyncProvider.SupportedMediaTypes = nil @@ -74,6 +106,14 @@ func TestValidateWatchSyncProviderRejectsUnspecifiedMediaType(t *testing.T) { } } +func TestValidateWatchSyncProviderRejectsInvalidNamespace(t *testing.T) { + manifest := validWatchSyncManifest() + manifest.Capabilities[0].WatchSyncProvider.ExternalIdNamespaces = []string{"bad namespace"} + if err := publicmanifest.Validate(manifest); err == nil { + t.Fatal("expected invalid external id namespace to fail") + } +} + func TestValidateWatchSyncProviderAllowsUnknownFutureEnums(t *testing.T) { manifest := validWatchSyncManifest() manifest.Capabilities[0].WatchSyncProvider.AuthMethods = []pluginv1.WatchSyncAuthMethod{ diff --git a/pkg/pluginsdk/runtimehost/coverage_more_test.go b/pkg/pluginsdk/runtimehost/coverage_more_test.go index 1bf139f..9031bcc 100644 --- a/pkg/pluginsdk/runtimehost/coverage_more_test.go +++ b/pkg/pluginsdk/runtimehost/coverage_more_test.go @@ -174,6 +174,9 @@ func TestClientRPCErrorPaths(t *testing.T) { if _, err := c.CallPluginHTTP(ctx, runtimehost.CallPluginHTTPRequest{InstallationID: 1, Path: "/x"}); err == nil { t.Fatal("CallPluginHTTP") } + if _, err := c.MintScopedStream(ctx, runtimehost.ScopedStreamRequest{MediaFileID: 1}); err == nil { + t.Fatal("MintScopedStream") + } if _, err := c.ResolveCatalogImageURLs(ctx, []string{"a"}, ""); err == nil { t.Fatal("ResolveCatalogImageURLs") } diff --git a/scripts/check-coverage.sh b/scripts/check-coverage.sh index f83e065..715dedd 100755 --- a/scripts/check-coverage.sh +++ b/scripts/check-coverage.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Fail if total statement coverage is below COVER_MIN (percent). set -euo pipefail -COVER_MIN="${COVER_MIN:-70}" +COVER_MIN="${COVER_MIN:-95}" PROFILE="${1:-coverage.out}" if [[ ! -f "$PROFILE" ]]; then echo "coverage profile missing: $PROFILE" >&2