112 lines
4.6 KiB
Go
112 lines
4.6 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
package tentacle
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
|
|
sdk "git.stormux.org/storm/cthulhu/sdk/go/tentacle"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestReaderSDKPeer(t *testing.T) {
|
|
if os.Getenv("CTHULHU_PRIVATE_READER_PEER") != "1" {
|
|
return
|
|
}
|
|
extension := sdk.Extensions{Pages: []sdk.PreferencePage{{ID: "probe", Title: "Reader probe", Command: "echo", Elements: []sdk.Element{{ID: "text", Label: "Example text", Kind: sdk.Entry}}, Values: map[string]sdk.Value{"text": {Text: "Initial"}}}}, QuickSettings: []sdk.QuickSetting{{ID: "probe", Label: "Probe choice", Command: "echo", Selected: "default", Options: []sdk.ListItem{{ID: "default", Label: "Default"}}}}}
|
|
echo := func(ctx context.Context, args json.RawMessage) (any, error) { return args, nil }
|
|
if err := sdk.ServeWithExtensions(os.Stdin, os.Stdout, []sdk.Command{{ID: "echo", Title: "Reader probe", Presentation: true, Interaction: true, Reader: true, Handle: echo}, {ID: "ordinary", Title: "Ordinary probe", Handle: echo}}, extension); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
func TestReaderSDKGrantsCopiesAndArgumentBounds(t *testing.T) {
|
|
t.Setenv("CTHULHU_PRIVATE_READER_PEER", "1")
|
|
for _, language := range []string{"go", "python"} {
|
|
for _, grant := range []bool{false, true} {
|
|
t.Run(language+"/"+map[bool]string{false: "denied", true: "granted"}[grant], func(t *testing.T) {
|
|
var program []string
|
|
if language == "go" {
|
|
executable, _ := os.Executable()
|
|
program = []string{executable, "-test.run=^TestReaderSDKPeer$"}
|
|
} else {
|
|
python, err := exec.LookPath("python3")
|
|
if err != nil {
|
|
t.Skip(err)
|
|
}
|
|
peer, err := filepath.Abs("testdata/sdk_reader_peer.py")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
program = []string{python, "-B", peer}
|
|
}
|
|
manager := test_manager(t, DefaultLimits(), nil)
|
|
session, err := manager.Start(t.Context(), Spec{ID: "probe", Program: program, Presentation: true, Interaction: true, ReaderSettings: grant})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
wait_signal(t, session.Ready())
|
|
state, reason := session.Status()
|
|
if !grant {
|
|
if state == Ready {
|
|
t.Fatal("reader API accepted without grant")
|
|
}
|
|
return
|
|
}
|
|
if state != Ready {
|
|
t.Fatal(state, reason)
|
|
}
|
|
extensions := session.Extensions()
|
|
if len(extensions.Pages) != 1 || len(extensions.QuickSettings) != 1 {
|
|
t.Fatal(extensions)
|
|
}
|
|
extensions.Pages[0].Values["text"] = sdk.Value{Text: "Changed"}
|
|
extensions.Pages[0].Elements[0].Label = "Changed"
|
|
if session.Extensions().Pages[0].Values["text"].Text != "Initial" || session.Extensions().Pages[0].Elements[0].Label != "Example text" {
|
|
t.Fatal("registration shared mutable state")
|
|
}
|
|
if err := session.Activate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
argument := json.RawMessage(`{"text":"` + strings.Repeat("x", 24*1024) + `"}`)
|
|
replies, err := session.Invoke(t.Context(), "echo", argument)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := wait_result(t, replies)
|
|
if result.Err != nil || string(result.Value) != string(argument) {
|
|
t.Fatal("reader continuation did not round trip", result.Err)
|
|
}
|
|
if _, err := session.Invoke(t.Context(), "ordinary", argument); !errors.Is(err, ErrLimit) {
|
|
t.Fatal("ordinary command exceeded its original argument limit", err)
|
|
}
|
|
if _, err := session.Invoke(t.Context(), "echo", json.RawMessage(`"`+strings.Repeat("x", MaxReaderArgumentBytes)+`"`)); !errors.Is(err, ErrLimit) {
|
|
t.Fatal("reader limit not enforced", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
func TestReaderRegistrationRequiresDeclaredReaderCallbacks(t *testing.T) {
|
|
extension := tentaclewire.Extensions{QuickSettings: []tentaclewire.QuickSetting{{ID: "probe", Label: "Probe", Command: "apply", Selected: "default", Options: []sdk.ListItem{{ID: "default", Label: "Default"}}}}}
|
|
registration := Registration{Protocol: Version{Major: 1}, Session: "session", Commands: []Command{{ID: "apply", Title: "Apply", Presentation: true, Interaction: true, Reader: true}}, Extensions: &extension}
|
|
if err := validate_registration(registration, "session"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
registration.Commands[0].Reader = false
|
|
if validate_registration(registration, "session") == nil {
|
|
t.Fatal("accepted an unprivileged extension callback")
|
|
}
|
|
registration.Commands[0].Reader = true
|
|
registration.Commands[0].Settings = true
|
|
if validate_registration(registration, "session") == nil {
|
|
t.Fatal("accepted a settings-form handler as a page callback")
|
|
}
|
|
}
|