github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/hookstate/ctlcmd/refresh_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2021 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package ctlcmd_test 21 22 import ( 23 "time" 24 25 . "gopkg.in/check.v1" 26 27 "github.com/snapcore/snapd/dirs" 28 "github.com/snapcore/snapd/overlord/hookstate" 29 "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" 30 "github.com/snapcore/snapd/overlord/hookstate/hooktest" 31 "github.com/snapcore/snapd/overlord/snapstate" 32 "github.com/snapcore/snapd/overlord/state" 33 "github.com/snapcore/snapd/snap" 34 "github.com/snapcore/snapd/testutil" 35 ) 36 37 type refreshSuite struct { 38 testutil.BaseTest 39 st *state.State 40 mockHandler *hooktest.MockHandler 41 } 42 43 var _ = Suite(&refreshSuite{}) 44 45 func mockRefreshCandidate(snapName, instanceKey, channel, version string, revision snap.Revision) interface{} { 46 sup := &snapstate.SnapSetup{ 47 Channel: channel, 48 InstanceKey: instanceKey, 49 SideInfo: &snap.SideInfo{ 50 Revision: revision, 51 RealName: snapName, 52 }, 53 } 54 return snapstate.MockRefreshCandidate(sup, version) 55 } 56 57 func (s *refreshSuite) SetUpTest(c *C) { 58 s.BaseTest.SetUpTest(c) 59 dirs.SetRootDir(c.MkDir()) 60 s.AddCleanup(func() { dirs.SetRootDir("/") }) 61 s.st = state.New(nil) 62 s.mockHandler = hooktest.NewMockHandler() 63 } 64 65 var refreshFromHookTests = []struct { 66 args []string 67 base, restart bool 68 inhibited bool 69 refreshCandidates map[string]interface{} 70 stdout, stderr, err string 71 exitCode int 72 }{{ 73 args: []string{"refresh", "--proceed", "--hold"}, 74 err: "cannot use --proceed and --hold together", 75 }, { 76 args: []string{"refresh", "--pending"}, 77 refreshCandidates: map[string]interface{}{"snap1": mockRefreshCandidate("snap1", "", "edge", "v1", snap.Revision{N: 3})}, 78 stdout: "pending: ready\nchannel: edge\nversion: v1\nrevision: 3\nbase: false\nrestart: false\n", 79 }, { 80 args: []string{"refresh", "--pending"}, 81 stdout: "pending: none\nchannel: stable\nbase: false\nrestart: false\n", 82 }, { 83 args: []string{"refresh", "--pending"}, 84 base: true, 85 restart: true, 86 stdout: "pending: none\nchannel: stable\nbase: true\nrestart: true\n", 87 }, { 88 args: []string{"refresh", "--pending"}, 89 inhibited: true, 90 stdout: "pending: inhibited\nchannel: stable\nbase: false\nrestart: false\n", 91 }} 92 93 func (s *refreshSuite) TestRefreshFromHook(c *C) { 94 s.st.Lock() 95 task := s.st.NewTask("test-task", "my test task") 96 setup := &hookstate.HookSetup{Snap: "snap1", Revision: snap.R(1), Hook: "gate-auto-refresh"} 97 mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "") 98 c.Check(err, IsNil) 99 s.st.Unlock() 100 101 for _, test := range refreshFromHookTests { 102 mockContext.Lock() 103 mockContext.Set("base", test.base) 104 mockContext.Set("restart", test.restart) 105 s.st.Set("refresh-candidates", test.refreshCandidates) 106 snapst := &snapstate.SnapState{ 107 Active: true, 108 Sequence: []*snap.SideInfo{{RealName: "snap1", Revision: snap.R(1)}}, 109 Current: snap.R(2), 110 TrackingChannel: "stable", 111 } 112 if test.inhibited { 113 snapst.RefreshInhibitedTime = &time.Time{} 114 } 115 snapstate.Set(s.st, "snap1", snapst) 116 mockContext.Unlock() 117 118 stdout, stderr, err := ctlcmd.Run(mockContext, test.args, 0) 119 comment := Commentf("%s", test.args) 120 if test.exitCode > 0 { 121 c.Check(err, DeepEquals, &ctlcmd.UnsuccessfulError{ExitCode: test.exitCode}, comment) 122 } else { 123 if test.err == "" { 124 c.Check(err, IsNil, comment) 125 } else { 126 c.Check(err, ErrorMatches, test.err, comment) 127 } 128 } 129 130 c.Check(string(stdout), Equals, test.stdout, comment) 131 c.Check(string(stderr), Equals, "", comment) 132 } 133 } 134 135 func (s *refreshSuite) TestRefreshHold(c *C) { 136 s.st.Lock() 137 task := s.st.NewTask("test-task", "my test task") 138 setup := &hookstate.HookSetup{Snap: "snap1", Revision: snap.R(1), Hook: "gate-auto-refresh"} 139 mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "") 140 c.Check(err, IsNil) 141 142 mockInstalledSnap(c, s.st, `name: foo 143 version: 1 144 `) 145 146 s.st.Unlock() 147 148 mockContext.Lock() 149 mockContext.Set("affecting-snaps", []string{"foo"}) 150 mockContext.Unlock() 151 152 stdout, stderr, err := ctlcmd.Run(mockContext, []string{"refresh", "--hold"}, 0) 153 c.Assert(err, IsNil) 154 c.Check(string(stdout), Equals, "") 155 c.Check(string(stderr), Equals, "") 156 157 mockContext.Lock() 158 defer mockContext.Unlock() 159 action := mockContext.Cached("action") 160 c.Assert(action, NotNil) 161 c.Check(action, Equals, snapstate.GateAutoRefreshHold) 162 163 var gating map[string]map[string]interface{} 164 c.Assert(s.st.Get("snaps-hold", &gating), IsNil) 165 c.Check(gating["foo"]["snap1"], NotNil) 166 } 167 168 func (s *refreshSuite) TestRefreshProceed(c *C) { 169 s.st.Lock() 170 task := s.st.NewTask("test-task", "my test task") 171 setup := &hookstate.HookSetup{Snap: "snap1", Revision: snap.R(1), Hook: "gate-auto-refresh"} 172 mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "") 173 c.Check(err, IsNil) 174 175 mockInstalledSnap(c, s.st, `name: foo 176 version: 1 177 `) 178 179 // pretend snap foo is held initially 180 c.Check(snapstate.HoldRefresh(s.st, "snap1", 0, "foo"), IsNil) 181 s.st.Unlock() 182 183 // sanity check 184 var gating map[string]map[string]interface{} 185 s.st.Lock() 186 snapsHold := s.st.Get("snaps-hold", &gating) 187 s.st.Unlock() 188 c.Assert(snapsHold, IsNil) 189 c.Check(gating["foo"]["snap1"], NotNil) 190 191 mockContext.Lock() 192 mockContext.Set("affecting-snaps", []string{"foo"}) 193 mockContext.Unlock() 194 195 stdout, stderr, err := ctlcmd.Run(mockContext, []string{"refresh", "--proceed"}, 0) 196 c.Assert(err, IsNil) 197 c.Check(string(stdout), Equals, "") 198 c.Check(string(stderr), Equals, "") 199 200 mockContext.Lock() 201 defer mockContext.Unlock() 202 action := mockContext.Cached("action") 203 c.Assert(action, NotNil) 204 c.Check(action, Equals, snapstate.GateAutoRefreshProceed) 205 206 // and it is still held (for hook handler to execute actual proceed logic). 207 gating = nil 208 c.Assert(s.st.Get("snaps-hold", &gating), IsNil) 209 c.Check(gating["foo"]["snap1"], NotNil) 210 211 mockContext.Cache("action", nil) 212 213 mockContext.Unlock() 214 defer mockContext.Lock() 215 216 // refresh --pending --proceed is the same as just saying --proceed. 217 stdout, stderr, err = ctlcmd.Run(mockContext, []string{"refresh", "--pending", "--proceed"}, 0) 218 c.Assert(err, IsNil) 219 c.Check(string(stdout), Equals, "") 220 c.Check(string(stderr), Equals, "") 221 222 mockContext.Lock() 223 defer mockContext.Unlock() 224 action = mockContext.Cached("action") 225 c.Assert(action, NotNil) 226 c.Check(action, Equals, snapstate.GateAutoRefreshProceed) 227 } 228 229 func (s *refreshSuite) TestRefreshFromUnsupportedHook(c *C) { 230 s.st.Lock() 231 232 task := s.st.NewTask("test-task", "my test task") 233 setup := &hookstate.HookSetup{Snap: "snap", Revision: snap.R(1), Hook: "install"} 234 mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "") 235 c.Check(err, IsNil) 236 s.st.Unlock() 237 238 _, _, err = ctlcmd.Run(mockContext, []string{"refresh"}, 0) 239 c.Check(err, ErrorMatches, `can only be used from gate-auto-refresh hook`) 240 } 241 242 // TODO: support this case 243 func (s *refreshSuite) TestRefreshFromApp(c *C) { 244 s.st.Lock() 245 246 setup := &hookstate.HookSetup{Snap: "snap", Revision: snap.R(1)} 247 mockContext, err := hookstate.NewContext(nil, s.st, setup, s.mockHandler, "") 248 c.Check(err, IsNil) 249 s.st.Unlock() 250 251 _, _, err = ctlcmd.Run(mockContext, []string{"refresh"}, 0) 252 c.Check(err, ErrorMatches, `cannot run outside of gate-auto-refresh hook`) 253 } 254 255 func (s *refreshSuite) TestRefreshRegularUserForbidden(c *C) { 256 s.st.Lock() 257 setup := &hookstate.HookSetup{Snap: "snap", Revision: snap.R(1)} 258 s.st.Unlock() 259 260 mockContext, err := hookstate.NewContext(nil, s.st, setup, s.mockHandler, "") 261 c.Assert(err, IsNil) 262 _, _, err = ctlcmd.Run(mockContext, []string{"refresh"}, 1000) 263 c.Assert(err, ErrorMatches, `cannot use "refresh" with uid 1000, try with sudo`) 264 forbidden, _ := err.(*ctlcmd.ForbiddenCommandError) 265 c.Assert(forbidden, NotNil) 266 }