package daemon
import (
"bytes"
"mime/multipart"
"net/http"
"net/http/httptest"
"strings"
"testing"
containerdclient "github.com/openeuler/Conch/internal/adapters/containerd/client"
"github.com/openeuler/Conch/internal/conchruntime"
)
func TestDecodeStrictJSON(t *testing.T) {
tests := []struct {
name string
body string
wantErr bool
}{
{name: "known fields", body: `{"template_id":"tmpl_123","volumeMounts":[{"source":"/tmp/data","path":"/data","readonly":true}]}`},
{name: "trailing whitespace", body: "{\"template_id\":\"tmpl_123\"}\n\t"},
{name: "unknown top-level field", body: `{"template_id":"tmpl_123","volume_mounts":[]}`, wantErr: true},
{name: "unknown nested field", body: `{"template_id":"tmpl_123","volumeMounts":[{"source":"/tmp/data","path":"/data","read_only":true}]}`, wantErr: true},
{name: "multiple values", body: `{"template_id":"tmpl_123"}{"sandbox_id":"sandbox-2"}`, wantErr: true},
{name: "trailing garbage", body: `{"template_id":"tmpl_123"} trailing`, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var req sandboxCreateRequest
err := decodeStrictJSON(strings.NewReader(tt.body), &req)
if (err != nil) != tt.wantErr {
t.Fatalf("decodeStrictJSON() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestJSONHandlersRejectUnknownFields(t *testing.T) {
snapshotOps := &fakeSnapshotService{}
sandboxOps := &fakeSandboxOps{}
runtimeService := conchruntime.New(sandboxOps, nil, nil)
runtimeService.Snapshot = snapshotOps
server := &Daemon{
router: http.NewServeMux(),
runtimeService: runtimeService,
daemonClient: &containerdclient.Client{},
}
server.routes()
paths := []string{
"/api/v1/sandboxes",
"/api/sandbox/suspend",
"/api/sandbox/resume",
"/api/sandbox/checkpoint",
"/api/template/pull",
"/api/template/push",
"/api/template/unpack",
"/api/template/list",
"/api/template/inspect",
"/api/template/remove",
"/api/image/pull",
"/api/image/push",
"/api/image/list",
"/api/image/remove",
"/api/snapshot/info",
"/api/snapshot/list",
"/api/snapshot/remove",
}
for _, path := range paths {
t.Run(path, func(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{"unexpected":true}`))
server.router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
}
if !strings.Contains(recorder.Body.String(), `unknown field "unexpected"`) {
t.Fatalf("body = %q", recorder.Body.String())
}
})
}
if sandboxOps.createReq.SandboxID != "" || sandboxOps.suspendReq.SandboxID != "" ||
sandboxOps.resumeReq.SandboxID != "" || sandboxOps.checkpointReq.SandboxID != "" {
t.Fatalf("sandbox backend was called: %#v", sandboxOps)
}
if snapshotOps.infoReq.Key != "" || snapshotOps.removeReq.Key != "" {
t.Fatalf("snapshot backend was called: %#v", snapshotOps)
}
}
func TestTemplateCreateRejectsUnknownMetadataField(t *testing.T) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if err := writer.WriteField("metadata", `{"source":"example.invalid/image:latest","unexpected":true}`); err != nil {
t.Fatalf("write metadata: %v", err)
}
if err := writer.Close(); err != nil {
t.Fatalf("close multipart body: %v", err)
}
server := &Daemon{router: http.NewServeMux(), runtimeService: conchruntime.New(nil, nil, nil)}
server.routes()
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/template/create", &body)
request.Header.Set("Content-Type", writer.FormDataContentType())
server.router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
}
if !strings.Contains(recorder.Body.String(), `unknown field "unexpected"`) {
t.Fatalf("body = %q", recorder.Body.String())
}
}
func TestTemplateCreateAcceptsAllMetadataFields(t *testing.T) {
metadata := `{
"source":"example.invalid/image:latest",
"boot_index_tag":"example.invalid/conch/boot:latest",
"plain_http":true,
"username":"tester",
"password":"secret",
"labels":{"purpose":"strict-json-test"}
}`
var req templateCreateRequest
if err := decodeStrictJSON(strings.NewReader(metadata), &req); err != nil {
t.Fatalf("decode metadata: %v", err)
}
if req.Source != "example.invalid/image:latest" || req.BootIndexTag != "example.invalid/conch/boot:latest" || !req.PlainHTTP ||
req.Username != "tester" || req.Password != "secret" ||
req.Labels["purpose"] != "strict-json-test" {
t.Fatalf("decoded metadata = %#v", req)
}
}
func TestSandboxCreateRejectsUnknownFieldsWithoutSideEffects(t *testing.T) {
tests := []struct {
name string
body string
unknownField string
}{
{
name: "top-level field",
body: `{"template_id":"tmpl_123","sandbox_id":"must-not-exist","volume_mounts":[]}`,
unknownField: "volume_mounts",
},
{
name: "nested field",
body: `{"template_id":"tmpl_123","sandbox_id":"must-not-exist","volumeMounts":[{"source":"/tmp/data","path":"/data","read_only":true}]}`,
unknownField: "read_only",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sandboxOps := &fakeSandboxOps{}
runtimeService := conchruntime.New(sandboxOps, nil, nil)
server := &Daemon{router: http.NewServeMux(), runtimeService: runtimeService}
server.routes()
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/api/v1/sandboxes", strings.NewReader(tt.body))
server.router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body.String())
}
if !strings.Contains(recorder.Body.String(), tt.unknownField) {
t.Fatalf("body = %q, want unknown field %q", recorder.Body.String(), tt.unknownField)
}
if sandboxOps.createReq.SandboxID != "" {
t.Fatalf("sandbox create was called: %#v", sandboxOps.createReq)
}
})
}
}