Files
run/runtime/sqlite_schema_probe_test.go
T

116 lines
5.2 KiB
Go

package runtime
import (
"context"
"database/sql"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"browser.local/run/protocol"
)
func TestSQLiteSchemaProbeExecutesOnlyAgainstScopedLogicalTarget(t *testing.T) {
root := t.TempDir()
assignment := sqliteSchemaProbeAssignment()
scope, err := NewWorkspaceResolver(root).Scope(assignment.ServerInstanceID, assignment.ExecutionInput.WorkspaceScope)
if err != nil {
t.Fatalf("create workspace scope: %v", err)
}
databasePath := filepath.Join(scope, "databases", "current.db")
createSQLiteProbeFixture(t, databasePath)
database, err := sql.Open(sqliteSchemaProbeDriver, "file:"+databasePath+"?mode=ro")
if err != nil {
t.Fatal(err)
}
if _, err := inspectSQLiteSchema(context.Background(), database, assignment.ExecutionInput.SQLiteSchemaProbe.Limits); err != nil {
t.Fatalf("inspect fixture directly: %v", err)
}
database.Close()
result := NewSQLiteSchemaProbeExecutor(root).Execute(context.Background(), assignment)
if result.State != lifecycleResultStateSucceeded || result.ExecutionResult.SQLiteSchemaProbe == nil {
t.Fatalf("expected successful probe envelope, got %+v", result)
}
probe := result.ExecutionResult.SQLiteSchemaProbe
if probe.JobID != assignment.JobID || probe.Binding != assignment.ExecutionInput.SQLiteSchemaProbe.Binding || probe.Status != "succeeded" || probe.ObservedAt.IsZero() || probe.ResultDigest == "" || probe.SourceFingerprint == "" || probe.SchemaFingerprint == "" {
t.Fatalf("unexpected probe identity envelope: %+v", probe)
}
if len(probe.Objects) != 2 || probe.Objects[0].ApproximateRows == nil || len(probe.Objects[1].SampleFingerprints) == 0 {
t.Fatalf("expected bounded table evidence, got %+v", probe.Objects)
}
serialized := mustJSON(t, probe)
for _, value := range []string{"current.db", "members", "alpha", root, "SELECT", "sqlite:"} {
if strings.Contains(serialized, value) {
t.Fatalf("probe leaked protected source material %q: %s", value, serialized)
}
}
if result.ExecutionResult.Content != "" {
t.Fatalf("probe must not return content: %+v", result.ExecutionResult)
}
}
func TestSQLiteSchemaProbeRejectsUnscopedOrUnsafeRequests(t *testing.T) {
assignment := sqliteSchemaProbeAssignment()
assignment.TargetKey = "/tmp/current.db"
result := NewSQLiteSchemaProbeExecutor(t.TempDir()).Execute(context.Background(), assignment)
if result.ErrorCode != "invalid_request" || result.ExecutionResult.SQLiteSchemaProbe == nil {
t.Fatalf("expected safe invalid probe failure, got %+v", result)
}
assignment = sqliteSchemaProbeAssignment()
assignment.ExecutionInput.SQLiteSchemaProbe.Binding.DatabaseIdentity = "C:/host/path"
result = NewSQLiteSchemaProbeExecutor(t.TempDir()).Execute(context.Background(), assignment)
if result.ErrorCode != "invalid_request" || strings.Contains(result.Message, "C:/") {
t.Fatalf("expected bounded binding rejection, got %+v", result)
}
}
func TestSQLiteSchemaProbeEnforcesResultLimit(t *testing.T) {
root := t.TempDir()
assignment := sqliteSchemaProbeAssignment()
scope, err := NewWorkspaceResolver(root).Scope(assignment.ServerInstanceID, assignment.ExecutionInput.WorkspaceScope)
if err != nil {
t.Fatal(err)
}
createSQLiteProbeFixture(t, filepath.Join(scope, "databases", "current.db"))
assignment.ExecutionInput.SQLiteSchemaProbe.Limits.MaxResultBytes = 1
result := NewSQLiteSchemaProbeExecutor(root).Execute(context.Background(), assignment)
if result.ErrorCode != "result_limit_exceeded" || result.State != lifecycleResultStateFailed {
t.Fatalf("expected result bound failure, got %+v", result)
}
}
func sqliteSchemaProbeAssignment() protocol.RunJobAssignment {
assignment := lifecycleAssignment(protocol.RunCapabilityRemoteRunDBSQLiteProbe)
assignment.TargetKey, assignment.MaxAttempts, assignment.FencingToken = "databases/current.db", 1, 7
assignment.ExecutionInput.WorkspaceScope = "profile-default"
assignment.ExecutionInput.SQLiteSchemaProbe = &protocol.SQLiteSchemaProbeRequest{RequestID: "probe-1", Binding: protocol.SQLiteSchemaProbeBinding{ServerInstanceID: assignment.ServerInstanceID, RunBindingID: "binding-1", RunEndpointID: assignment.RunEndpointID, PluginID: "game.example", PluginVersion: "1.0.0", AdapterVersion: "adapter-1", GameVersion: "1.0", DatabaseIdentity: "database-1"}, Limits: protocol.SQLiteSchemaProbeLimits{MaxObjects: 8, MaxColumnsPerObject: 8, MaxIndexesPerObject: 8, MaxForeignKeys: 8, MaxCardinalityReads: 8, MaxSampleRows: 2, TimeoutMS: 1000, MaxResultBytes: 128 * 1024}}
return assignment
}
func createSQLiteProbeFixture(t *testing.T, path string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
t.Fatal(err)
}
database, err := sql.Open(sqliteSchemaProbeDriver, path)
if err != nil {
t.Fatal(err)
}
defer database.Close()
if _, err := database.Exec("CREATE TABLE members (id INTEGER PRIMARY KEY, name TEXT NOT NULL); CREATE TABLE groups (id INTEGER PRIMARY KEY, member_id INTEGER REFERENCES members(id)); CREATE UNIQUE INDEX members_name ON members(name); INSERT INTO members(name) VALUES ('alpha'), ('beta');"); err != nil {
t.Fatal(err)
}
}
func mustJSON(t *testing.T, value any) string {
t.Helper()
body, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
return string(body)
}