From 9e3df0905b28c336c3b9df95bd8dfbccd36b7e61 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sat, 11 Oct 2025 02:44:02 +0900 Subject: [PATCH] internal/app/spcontainer: check params init behaviour This change also significantly reduces duplicate information in test case. Signed-off-by: Ophestra --- internal/app/dispatcher_test.go | 21 ++--- internal/app/spaccount_test.go | 24 +++--- internal/app/spcontainer_test.go | 140 +++++++++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 internal/app/spcontainer_test.go diff --git a/internal/app/dispatcher_test.go b/internal/app/dispatcher_test.go index 1f5a9f6..e1d8f53 100644 --- a/internal/app/dispatcher_test.go +++ b/internal/app/dispatcher_test.go @@ -33,19 +33,17 @@ var checkExpectInstanceId = *(*state.ID)(bytes.Repeat([]byte{0xaa}, len(state.ID type opBehaviourTestCase struct { name string - newOp func() outcomeOp + newOp func(isShim, clearUnexported bool) outcomeOp newConfig func() *hst.Config pStateSys func(state *outcomeStateSys) toSystem []stub.Call - wantOpSys outcomeOp wantSys *system.I extraCheckSys func(t *testing.T, state *outcomeStateSys) wantErrSystem error pStateContainer func(state *outcomeStateParams) toContainer []stub.Call - wantOpContainer outcomeOp wantParams *container.Params extraCheckParams func(t *testing.T, state *outcomeStateParams) wantErrContainer error @@ -94,11 +92,11 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { if err := s.populateLocal(k, k); err != nil { t.Fatalf("populateLocal: error = %v", err) } - stateSys := s.newSys(config, system.New(panicMsgContext{}, panicMsgContext{}, checkExpectUid)) + stateSys := s.newSys(config, newI()) if tc.pStateSys != nil { tc.pStateSys(stateSys) } - op := tc.newOp() + op := tc.newOp(false, true) if err := op.toSystem(stateSys); !reflect.DeepEqual(err, tc.wantErrSystem) { t.Errorf("toSystem: error = %v, want %v", err, tc.wantErrSystem) @@ -118,8 +116,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { if tc.extraCheckSys != nil { tc.extraCheckSys(t, stateSys) } - if !reflect.DeepEqual(op, tc.wantOpSys) { - t.Errorf("toSystem: op = %#v, want %#v", op, tc.wantOpSys) + if wantOpSys := tc.newOp(true, false); !reflect.DeepEqual(op, wantOpSys) { + t.Errorf("toSystem: op = %#v, want %#v", op, wantOpSys) } } @@ -133,7 +131,7 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { if tc.pStateContainer != nil { tc.pStateContainer(stateParams) } - op := tc.newOp() + op := tc.newOp(true, true) if err := op.toContainer(stateParams); !reflect.DeepEqual(err, tc.wantErrContainer) { t.Errorf("toContainer: error = %v, want %v", err, tc.wantErrContainer) @@ -144,14 +142,11 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { } if !reflect.DeepEqual(stateParams.params, tc.wantParams) { - t.Errorf("toContainer: %#v, want %#v", stateParams.params, tc.wantParams) + t.Errorf("toContainer:\n%s\nwant\n%s", mustMarshal(stateParams.params), mustMarshal(tc.wantParams)) } if tc.extraCheckParams != nil { tc.extraCheckParams(t, stateParams) } - if !reflect.DeepEqual(op, tc.wantOpContainer) { - t.Errorf("toContainer: op = %#v, want %#v", op, tc.wantOpContainer) - } } out: @@ -167,6 +162,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) { } } +func newI() *system.I { return system.New(panicMsgContext{}, panicMsgContext{}, checkExpectUid) } + type kstub struct { panicDispatcher *stub.Stub[syscallDispatcher] diff --git a/internal/app/spaccount_test.go b/internal/app/spaccount_test.go index 8d96b4b..a587c12 100644 --- a/internal/app/spaccount_test.go +++ b/internal/app/spaccount_test.go @@ -9,45 +9,43 @@ import ( "hakurei.app/container" "hakurei.app/container/stub" "hakurei.app/hst" - "hakurei.app/message" - "hakurei.app/system" ) func TestSpAccountOp(t *testing.T) { config := hst.Template() checkOpBehaviour(t, []opBehaviourTestCase{ - {"invalid state", func() outcomeOp { return spAccountOp{} }, func() *hst.Config { + {"invalid state", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config { c := hst.Template() c.Container.Shell = nil return c }, nil, []stub.Call{ // this op performs basic validation and does not make calls during toSystem - }, spAccountOp{}, system.New(t.Context(), message.NewMsg(nil), checkExpectUid), nil, syscall.ENOTRECOVERABLE, nil, nil, nil, nil, nil, nil}, + }, nil, nil, syscall.ENOTRECOVERABLE, nil, nil, nil, nil, nil}, - {"invalid user name", func() outcomeOp { return spAccountOp{} }, func() *hst.Config { + {"invalid user name", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config { c := hst.Template() c.Container.Username = "9" return c }, nil, []stub.Call{ // this op performs basic validation and does not make calls during toSystem - }, spAccountOp{}, nil, nil, &hst.AppError{ + }, nil, nil, &hst.AppError{ Step: "finalise", Err: os.ErrInvalid, Msg: `invalid user name "9"`, - }, nil, nil, nil, nil, nil, nil}, + }, nil, nil, nil, nil, nil}, - {"success fallback username", func() outcomeOp { return spAccountOp{} }, func() *hst.Config { + {"success fallback username", func(bool, bool) outcomeOp { return spAccountOp{} }, func() *hst.Config { c := hst.Template() c.Container.Username = "" return c }, nil, []stub.Call{ // this op performs basic validation and does not make calls during toSystem - }, spAccountOp{}, system.New(t.Context(), message.NewMsg(nil), checkExpectUid), nil, nil, func(state *outcomeStateParams) { + }, newI(), nil, nil, func(state *outcomeStateParams) { state.params.Ops = new(container.Ops) }, []stub.Call{ // this op configures the container state and does not make calls during toContainer - }, spAccountOp{}, &container.Params{ + }, &container.Params{ Dir: config.Container.Home, Ops: new(container.Ops). Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")). @@ -64,13 +62,13 @@ func TestSpAccountOp(t *testing.T) { } }, nil}, - {"success", func() outcomeOp { return spAccountOp{} }, hst.Template, nil, []stub.Call{ + {"success", func(bool, bool) outcomeOp { return spAccountOp{} }, hst.Template, nil, []stub.Call{ // this op performs basic validation and does not make calls during toSystem - }, spAccountOp{}, system.New(t.Context(), message.NewMsg(nil), checkExpectUid), nil, nil, func(state *outcomeStateParams) { + }, newI(), nil, nil, func(state *outcomeStateParams) { state.params.Ops = new(container.Ops) }, []stub.Call{ // this op configures the container state and does not make calls during toContainer - }, spAccountOp{}, &container.Params{ + }, &container.Params{ Dir: config.Container.Home, Ops: new(container.Ops). Place(m("/etc/passwd"), []byte("chronos:x:1000:100:Hakurei:/data/data/org.chromium.Chromium:/run/current-system/sw/bin/zsh\n")). diff --git a/internal/app/spcontainer_test.go b/internal/app/spcontainer_test.go new file mode 100644 index 0000000..653fba1 --- /dev/null +++ b/internal/app/spcontainer_test.go @@ -0,0 +1,140 @@ +package app + +import ( + "maps" + "os" + "reflect" + "testing" + + "hakurei.app/container" + "hakurei.app/container/bits" + "hakurei.app/container/fhs" + "hakurei.app/container/seccomp" + "hakurei.app/container/stub" + "hakurei.app/hst" +) + +func TestSpParamsOp(t *testing.T) { + config := hst.Template() + + checkOpBehaviour(t, []opBehaviourTestCase{ + {"invalid program path", func(isShim, _ bool) outcomeOp { + if !isShim { + return new(spParamsOp) + } + return &spParamsOp{Term: "xterm", TermSet: true} + }, func() *hst.Config { + c := hst.Template() + c.Container.Path = nil + return c + }, nil, []stub.Call{ + call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil), + }, newI(). + Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{ + // this op configures the container state and does not make calls during toContainer + }, nil, nil, &hst.AppError{ + Step: "finalise", + Err: os.ErrInvalid, + Msg: "invalid program path", + }}, + + {"success defaultargs secure", func(isShim, _ bool) outcomeOp { + if !isShim { + return new(spParamsOp) + } + return &spParamsOp{Term: "xterm", TermSet: true} + }, func() *hst.Config { + c := hst.Template() + c.Container.Args = nil + c.Container.Multiarch = false + c.Container.SeccompCompat = false + c.Container.Devel = false + c.Container.Userns = false + c.Container.Tty = false + c.Container.Device = false + return c + }, nil, []stub.Call{ + call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil), + }, newI(). + Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{ + // this op configures the container state and does not make calls during toContainer + }, &container.Params{ + Hostname: config.Container.Hostname, + HostNet: config.Container.HostNet, + HostAbstract: config.Container.HostAbstract, + Path: config.Container.Path, + Args: []string{config.Container.Path.String()}, + SeccompPresets: bits.PresetExt | bits.PresetDenyDevel | bits.PresetDenyNS | bits.PresetDenyTTY, + Uid: 1000, + Gid: 100, + Ops: new(container.Ops). + Root(m("/var/lib/hakurei/base/org.debian"), bits.BindWritable). + Proc(fhs.AbsProc).Tmpfs(hst.AbsTmp, 1<<12, 0755). + DevWritable(fhs.AbsDev, true). + Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777), + }, func(t *testing.T, state *outcomeStateParams) { + wantEnv := map[string]string{ + "TERM": "xterm", + } + maps.Copy(wantEnv, config.Container.Env) + if !maps.Equal(state.env, wantEnv) { + t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv) + } + + const wantAutoEtcPrefix = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + if state.as.AutoEtcPrefix != wantAutoEtcPrefix { + t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix) + } + + wantFilesystems := config.Container.Filesystem[1:] + if !reflect.DeepEqual(state.filesystem, wantFilesystems) { + t.Errorf("toContainer: filesystem = %#v, want %#v", state.filesystem, wantFilesystems) + } + }, nil}, + + {"success", func(isShim, _ bool) outcomeOp { + if !isShim { + return new(spParamsOp) + } + return &spParamsOp{Term: "xterm", TermSet: true} + }, hst.Template, nil, []stub.Call{ + call("lookupEnv", stub.ExpectArgs{"TERM"}, "xterm", nil), + }, newI(). + Ensure(m(container.Nonexistent+"/tmp/hakurei.0"), 0711), nil, nil, nil, []stub.Call{ + // this op configures the container state and does not make calls during toContainer + }, &container.Params{ + Hostname: config.Container.Hostname, + RetainSession: config.Container.Tty, + HostNet: config.Container.HostNet, + HostAbstract: config.Container.HostAbstract, + Path: config.Container.Path, + Args: config.Container.Args, + SeccompFlags: seccomp.AllowMultiarch, + Uid: 1000, + Gid: 100, + Ops: new(container.Ops). + Root(m("/var/lib/hakurei/base/org.debian"), bits.BindWritable). + Proc(fhs.AbsProc).Tmpfs(hst.AbsTmp, 1<<12, 0755). + Bind(fhs.AbsDev, fhs.AbsDev, bits.BindWritable|bits.BindDevice). + Tmpfs(fhs.AbsDev.Append("shm"), 0, 01777), + }, func(t *testing.T, state *outcomeStateParams) { + wantEnv := map[string]string{ + "TERM": "xterm", + } + maps.Copy(wantEnv, config.Container.Env) + if !maps.Equal(state.env, wantEnv) { + t.Errorf("toContainer: env = %#v, want %#v", state.env, wantEnv) + } + + const wantAutoEtcPrefix = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + if state.as.AutoEtcPrefix != wantAutoEtcPrefix { + t.Errorf("toContainer: as.AutoEtcPrefix = %q, want %q", state.as.AutoEtcPrefix, wantAutoEtcPrefix) + } + + wantFilesystems := config.Container.Filesystem[1:] + if !reflect.DeepEqual(state.filesystem, wantFilesystems) { + t.Errorf("toContainer: filesystem = %#v, want %#v", state.filesystem, wantFilesystems) + } + }, nil}, + }) +}