first commit
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"browser.local/platform/domain"
|
||||
)
|
||||
|
||||
func TestValidateAIProviderRejectsRawSecret(t *testing.T) {
|
||||
provider := validAIProvider()
|
||||
provider.APIKeyRef = "sk-test-secret"
|
||||
|
||||
err := ValidateAIProvider(provider)
|
||||
if err == nil || !strings.Contains(err.Error(), "apiKeyRef must reference secret storage") {
|
||||
t.Fatalf("expected raw secret rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAIProviderRequiresDefaultModelInModels(t *testing.T) {
|
||||
provider := validAIProvider()
|
||||
provider.DefaultModel = "missing-model"
|
||||
|
||||
err := ValidateAIProvider(provider)
|
||||
if err == nil || !strings.Contains(err.Error(), "defaultModel must be included") {
|
||||
t.Fatalf("expected default model validation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginManifestRegistration(t *testing.T) {
|
||||
registration := validGamePluginManifestRegistration()
|
||||
|
||||
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
|
||||
t.Fatalf("expected manifest registration to validate, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginManifestRegistrationRejectsUnsafeRequests(t *testing.T) {
|
||||
registration := validGamePluginManifestRegistration()
|
||||
registration.Manifest.Description = "requires direct run socket and raw AI key material"
|
||||
registration.ManifestRef = "file:///etc/plugin.json"
|
||||
|
||||
err := ValidateGamePluginManifestRegistration(registration)
|
||||
if err == nil {
|
||||
t.Fatal("expected unsafe manifest registration rejection")
|
||||
}
|
||||
message := err.Error()
|
||||
for _, want := range []string{"manifestRef is unsafe", "direct run access", "raw credential or AI/provider key"} {
|
||||
if !strings.Contains(message, want) {
|
||||
t.Fatalf("expected validation error to contain %q, got %v", want, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateGamePluginManifestRegistrationRejectsUnsafeCapabilitiesAndPermissions(t *testing.T) {
|
||||
registration := validGamePluginManifestRegistration()
|
||||
registration.Manifest.Capabilities = append(registration.Manifest.Capabilities, "run.socket")
|
||||
registration.Manifest.Permissions = append(registration.Manifest.Permissions, "provider.key.read")
|
||||
|
||||
err := ValidateGamePluginManifestRegistration(registration)
|
||||
if err == nil || !strings.Contains(err.Error(), "manifest.capabilities") || !strings.Contains(err.Error(), "permissions") {
|
||||
t.Fatalf("expected unsafe capability and permission rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateServerInstanceDependencies(t *testing.T) {
|
||||
instance := domain.ServerInstance{
|
||||
ID: "server-1",
|
||||
PluginID: "server.scum",
|
||||
PluginVersion: "1.0.0",
|
||||
RunEndpointID: "run-local",
|
||||
Name: "SCUM #1",
|
||||
State: domain.ServerInstanceStateDraft,
|
||||
ConfigVersion: 1,
|
||||
}
|
||||
plugin := domain.GamePlugin{
|
||||
ID: "server.scum",
|
||||
Version: "1.0.0",
|
||||
RequiredRunCapabilities: []string{"process.start", "logs.read"},
|
||||
Status: domain.GamePluginStatusInstalled,
|
||||
}
|
||||
endpoint := domain.RunEndpoint{
|
||||
ID: "run-local",
|
||||
Status: domain.RunEndpointStatusOnline,
|
||||
Capabilities: []string{"process.start", "logs.read", "files.read"},
|
||||
}
|
||||
|
||||
if err := ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil {
|
||||
t.Fatalf("expected valid dependencies, got %v", err)
|
||||
}
|
||||
|
||||
endpoint.Capabilities = []string{"process.start"}
|
||||
err := ValidateServerInstanceDependencies(instance, plugin, endpoint)
|
||||
if err == nil || !strings.Contains(err.Error(), "logs.read") {
|
||||
t.Fatalf("expected missing capability error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateServerInstanceRejectsDeletedCreateState(t *testing.T) {
|
||||
instance := domain.ServerInstance{
|
||||
ID: "server-1",
|
||||
PluginID: "server.scum",
|
||||
PluginVersion: "1.0.0",
|
||||
RunEndpointID: "run-local",
|
||||
Name: "SCUM #1",
|
||||
State: domain.ServerInstanceStateDeleted,
|
||||
}
|
||||
|
||||
err := ValidateServerInstance(instance)
|
||||
if err == nil || !strings.Contains(err.Error(), "state must not be deleted") {
|
||||
t.Fatalf("expected deleted state rejection, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateJobBoundsProgress(t *testing.T) {
|
||||
job := domain.Job{
|
||||
ID: "job-1",
|
||||
RunEndpointID: "run-local",
|
||||
Capability: "process.start",
|
||||
IdempotencyKey: "idem-1",
|
||||
State: domain.JobStateQueued,
|
||||
Progress: domain.JobProgress{Percent: 101},
|
||||
}
|
||||
|
||||
err := ValidateJob(job)
|
||||
if err == nil || !strings.Contains(err.Error(), "progress.percent") {
|
||||
t.Fatalf("expected progress bounds error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateArtifactLogAndAudit(t *testing.T) {
|
||||
artifact := domain.Artifact{
|
||||
ID: "artifact-1",
|
||||
OwnerKind: domain.ArtifactOwnerKindJob,
|
||||
OwnerID: "job-1",
|
||||
SizeBytes: 10,
|
||||
Checksum: "sha256:abc",
|
||||
State: domain.ArtifactStateAvailable,
|
||||
}
|
||||
if err := ValidateArtifact(artifact); err != nil {
|
||||
t.Fatalf("expected artifact to validate, got %v", err)
|
||||
}
|
||||
|
||||
stream := domain.LogStream{
|
||||
ID: "log-1",
|
||||
ServerInstanceID: "server-1",
|
||||
Source: domain.LogStreamSourceFile,
|
||||
StreamKey: "server.log",
|
||||
StorageBackend: domain.LogStorageBackendLocalSegments,
|
||||
RetentionPolicy: "default",
|
||||
}
|
||||
if err := ValidateLogStream(stream); err != nil {
|
||||
t.Fatalf("expected log stream to validate, got %v", err)
|
||||
}
|
||||
|
||||
audit := domain.AuditEvent{
|
||||
ID: "audit-1",
|
||||
ActorID: "user-1",
|
||||
Action: "server.create",
|
||||
ResourceKind: "server-instance",
|
||||
ResourceID: "server-1",
|
||||
Result: domain.AuditResultSuccess,
|
||||
Summary: "created server instance",
|
||||
}
|
||||
if err := ValidateAuditEvent(audit); err != nil {
|
||||
t.Fatalf("expected audit event to validate, got %v", err)
|
||||
}
|
||||
|
||||
audit.Summary = "bearer raw-secret"
|
||||
if err := ValidateAuditEvent(audit); err == nil || !strings.Contains(err.Error(), "summary must be redacted") {
|
||||
t.Fatalf("expected audit redaction error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func validAIProvider() domain.AIProvider {
|
||||
return domain.AIProvider{
|
||||
ID: "ai.openai",
|
||||
Name: "OpenAI",
|
||||
Kind: domain.AIProviderKindOpenAI,
|
||||
BaseURL: "https://api.openai.com/v1",
|
||||
APIKeyRef: "secret://providers/openai",
|
||||
Models: []string{"gpt-4.1", "gpt-4.1-mini"},
|
||||
DefaultModel: "gpt-4.1",
|
||||
RelayMode: domain.AIRelayModeDirect,
|
||||
TimeoutMS: 30000,
|
||||
Status: domain.AIProviderStatusActive,
|
||||
RedactionPolicy: "default",
|
||||
}
|
||||
}
|
||||
|
||||
func validGamePluginManifestRegistration() domain.GamePluginManifestRegistration {
|
||||
return domain.GamePluginManifestRegistration{
|
||||
ManifestRef: "artifact://manifests/game.example/0.1.0",
|
||||
Manifest: domain.GamePluginManifest{
|
||||
ID: "game.example",
|
||||
Name: "Example Server",
|
||||
Description: "Development plugin",
|
||||
Version: "0.1.0",
|
||||
Kind: "game-plugin",
|
||||
Tags: []string{"example", "development"},
|
||||
Server: domain.GamePluginManifestServer{
|
||||
Type: "example",
|
||||
DisplayName: "Example Server",
|
||||
SupportedOS: []string{"linux", "darwin"},
|
||||
CreateFormSchema: "schemas/create-form.schema.json",
|
||||
},
|
||||
Capabilities: []string{"process.install", "process.start", "process.stop", "logs.read", "files.read", "artifacts.read", "ai.invoke"},
|
||||
Permissions: []string{"server.read", "server.lifecycle", "server.logs.read", "server.files.read", "server.artifacts.read", "ai.invoke"},
|
||||
Actions: domain.PluginLifecycleActions{
|
||||
Install: "actions/install.json",
|
||||
Start: "actions/start.json",
|
||||
Stop: "actions/stop.json",
|
||||
Restart: "actions/restart.json",
|
||||
},
|
||||
Pages: []domain.GamePluginPage{
|
||||
{Key: "logs", Title: "Logs", Path: "/logs", Permissions: []string{"server.logs.read"}},
|
||||
},
|
||||
AI: domain.GamePluginManifestAI{Purposes: []string{"logs.diagnose"}},
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user