Files
DoGaMa-serv/internal/module/runtime_test.go
T

148 lines
6.2 KiB
Go

package module
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"net/http"
"os"
"strings"
"testing"
"time"
)
type roundTripFunc func(*http.Request) (*http.Response, error)
func (f roundTripFunc) RoundTrip(request *http.Request) (*http.Response, error) { return f(request) }
func TestValidRelativeAPIPath(t *testing.T) {
for _, value := range []string{"http://metadata/v1/api/info", "//other/v1/api/info", "/v1/api/../secret", "/v1/api/info?q=1", "/etc/passwd"} {
if validRelativeAPIPath(value) {
t.Fatalf("unsafe path accepted: %q", value)
}
}
if !validRelativeAPIPath("/v1/api/info") {
t.Fatal("documented Palworld endpoint was rejected")
}
}
func TestPalworldAdapterReportsBoundedFailures(t *testing.T) {
wasm, err := os.ReadFile("../../modules/palworld-rest/module.wasm")
if err != nil {
t.Fatal(err)
}
digest := sha256.Sum256(wasm)
tests := []struct {
name string
transport roundTripFunc
code string
}{
{name: "unauthorized", code: "unauthorized", transport: func(request *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusUnauthorized, Header: make(http.Header), Body: io.NopCloser(strings.NewReader("unauthorized")), Request: request}, nil
}},
{name: "malformed", code: "invalid_response", transport: func(request *http.Request) (*http.Response, error) {
return &http.Response{StatusCode: http.StatusOK, Header: make(http.Header), Body: io.NopCloser(strings.NewReader("not-json")), Request: request}, nil
}},
{name: "offline", code: "unreachable", transport: func(*http.Request) (*http.Response, error) {
return nil, errors.New("offline")
}},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
runtime, err := newWithTransport(wasm, hex.EncodeToString(digest[:]), []string{"server_info"}, Limits{MemoryMB: 32, Timeout: 10 * time.Second, MaxResponseBytes: 1 << 20, MaxConcurrentCall: 2}, Binding{InstanceID: strings.Repeat("a", 20), ContainerPort: 8212, AllowedMethods: map[string]bool{"GET": true}, Configuration: map[string]string{"username": "admin"}, Secrets: map[string]string{"admin_password": "secret"}}, test.transport)
if err != nil {
t.Fatal(err)
}
var response struct {
OK bool `json:"ok"`
Data map[string]any `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
Retryable bool `json:"retryable"`
} `json:"error"`
}
if err := runtime.Call(context.Background(), "get_server_info", struct{}{}, &response); err != nil {
t.Fatal(err)
}
if response.OK || response.Error == nil || response.Error.Code != test.code || strings.Contains(response.Error.Message, "secret") {
t.Fatalf("unexpected safe failure: %+v", response)
}
})
}
}
func TestPalworldAdapterExecutesInSandbox(t *testing.T) {
wasm, err := os.ReadFile("../../modules/palworld-rest/module.wasm")
if err != nil {
t.Fatal(err)
}
digest := sha256.Sum256(wasm)
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
if request.URL.Path != "/v1/api/info" || request.Header.Get("Authorization") == "" {
t.Fatalf("unexpected adapter request: %s", request.URL)
}
return &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": {"application/json"}}, Body: io.NopCloser(strings.NewReader(`{"version":"v1","servername":"test","description":"fixture","worldguid":"world"}`)), Request: request}, nil
})
runtime, err := newWithTransport(wasm, hex.EncodeToString(digest[:]), []string{"server_info"}, Limits{MemoryMB: 32, Timeout: 10 * time.Second, MaxResponseBytes: 1 << 20, MaxConcurrentCall: 2}, Binding{InstanceID: strings.Repeat("a", 20), ContainerPort: 8212, AllowedMethods: map[string]bool{"GET": true, "POST": true}, Configuration: map[string]string{"username": "admin"}, Secrets: map[string]string{"admin_password": "secret"}}, transport)
if err != nil {
t.Fatal(err)
}
var response struct {
OK bool `json:"ok"`
Error any `json:"error"`
Data struct {
Name string `json:"name"`
GameVersion string `json:"game_version"`
Description string `json:"description"`
WorldID string `json:"world_id"`
} `json:"data"`
}
if err := runtime.Call(context.Background(), "get_server_info", struct{}{}, &response); err != nil {
t.Fatal(err)
}
if !response.OK || response.Data.Name != "test" {
t.Fatalf("unexpected normalized response: %+v", response)
}
}
func TestNewRejectsChecksumAndUnknownCapability(t *testing.T) {
limits := Limits{MemoryMB: 32, Timeout: time.Second, MaxResponseBytes: 1024, MaxConcurrentCall: 1}
binding := Binding{InstanceID: strings.Repeat("a", 20), ContainerPort: 8212}
if _, err := newWithTransport([]byte("wasm"), strings.Repeat("0", 64), nil, limits, binding, roundTripFunc(nil)); err == nil {
t.Fatal("checksum mismatch accepted")
}
digest := sha256.Sum256([]byte("wasm"))
if _, err := newWithTransport([]byte("wasm"), hex.EncodeToString(digest[:]), []string{"shell"}, limits, binding, roundTripFunc(nil)); err == nil {
t.Fatal("unknown capability accepted")
}
}
func TestHTTPRequestPinsInstanceOriginAndDisablesRedirects(t *testing.T) {
instanceID := strings.Repeat("A", 20)
seen := false
transport := roundTripFunc(func(request *http.Request) (*http.Response, error) {
seen = true
if request.URL.String() != "http://dogama-aaaaaaaaaaaaaaaaaaaa:8212/v1/api/info" {
t.Fatalf("unexpected destination %q", request.URL)
}
return &http.Response{StatusCode: 200, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(`{"ok":true}`)), Request: request}, nil
})
digest := sha256.Sum256([]byte("wasm"))
runtime, err := newWithTransport([]byte("wasm"), hex.EncodeToString(digest[:]), nil, Limits{MemoryMB: 32, Timeout: time.Second, MaxResponseBytes: 1024, MaxConcurrentCall: 1}, Binding{InstanceID: instanceID, ContainerPort: 8212, AllowedMethods: map[string]bool{"GET": true}}, transport)
if err != nil {
t.Fatal(err)
}
request, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://dogama-aaaaaaaaaaaaaaaaaaaa:8212/v1/api/info", nil)
response, err := runtime.client.Do(request)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if !seen {
t.Fatal("bound transport was not used")
}
}