116 lines
5.2 KiB
Go
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 redacted 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)
|
|
}
|