github.com/chipaca/snappy@v0.0.0-20210104084008-1f06296fe8ad/daemon/api_systems_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2020 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 daemon_test 21 22 import ( 23 "bytes" 24 "encoding/json" 25 "fmt" 26 "net/http" 27 "net/http/httptest" 28 "os" 29 "path" 30 "path/filepath" 31 "strings" 32 33 "gopkg.in/check.v1" 34 35 "github.com/snapcore/snapd/asserts/assertstest" 36 "github.com/snapcore/snapd/boot" 37 "github.com/snapcore/snapd/bootloader" 38 "github.com/snapcore/snapd/bootloader/bootloadertest" 39 "github.com/snapcore/snapd/client" 40 "github.com/snapcore/snapd/daemon" 41 "github.com/snapcore/snapd/dirs" 42 "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" 43 "github.com/snapcore/snapd/overlord/devicestate" 44 "github.com/snapcore/snapd/overlord/hookstate" 45 "github.com/snapcore/snapd/overlord/state" 46 "github.com/snapcore/snapd/seed" 47 "github.com/snapcore/snapd/seed/seedtest" 48 "github.com/snapcore/snapd/snap" 49 "github.com/snapcore/snapd/snap/snaptest" 50 "github.com/snapcore/snapd/testutil" 51 ) 52 53 var _ = check.Suite(&systemsSuite{}) 54 55 type systemsSuite struct { 56 apiBaseSuite 57 } 58 59 func (s *systemsSuite) mockSystemSeeds(c *check.C) (restore func()) { 60 // now create a minimal uc20 seed dir with snaps/assertions 61 seed20 := &seedtest.TestingSeed20{ 62 SeedSnaps: seedtest.SeedSnaps{ 63 StoreSigning: s.StoreSigning, 64 Brands: s.Brands, 65 }, 66 SeedDir: dirs.SnapSeedDir, 67 } 68 69 restore = seed.MockTrusted(seed20.StoreSigning.Trusted) 70 71 assertstest.AddMany(s.StoreSigning.Database, s.Brands.AccountsAndKeys("my-brand")...) 72 // add essential snaps 73 seed20.MakeAssertedSnap(c, "name: snapd\nversion: 1\ntype: snapd", nil, snap.R(1), "my-brand", s.StoreSigning.Database) 74 seed20.MakeAssertedSnap(c, "name: pc\nversion: 1\ntype: gadget\nbase: core20", nil, snap.R(1), "my-brand", s.StoreSigning.Database) 75 seed20.MakeAssertedSnap(c, "name: pc-kernel\nversion: 1\ntype: kernel", nil, snap.R(1), "my-brand", s.StoreSigning.Database) 76 seed20.MakeAssertedSnap(c, "name: core20\nversion: 1\ntype: base", nil, snap.R(1), "my-brand", s.StoreSigning.Database) 77 seed20.MakeSeed(c, "20191119", "my-brand", "my-model", map[string]interface{}{ 78 "display-name": "my fancy model", 79 "architecture": "amd64", 80 "base": "core20", 81 "snaps": []interface{}{ 82 map[string]interface{}{ 83 "name": "pc-kernel", 84 "id": seed20.AssertedSnapID("pc-kernel"), 85 "type": "kernel", 86 "default-channel": "20", 87 }, 88 map[string]interface{}{ 89 "name": "pc", 90 "id": seed20.AssertedSnapID("pc"), 91 "type": "gadget", 92 "default-channel": "20", 93 }}, 94 }, nil) 95 seed20.MakeSeed(c, "20200318", "my-brand", "my-model-2", map[string]interface{}{ 96 "display-name": "same brand different model", 97 "architecture": "amd64", 98 "base": "core20", 99 "snaps": []interface{}{ 100 map[string]interface{}{ 101 "name": "pc-kernel", 102 "id": seed20.AssertedSnapID("pc-kernel"), 103 "type": "kernel", 104 "default-channel": "20", 105 }, 106 map[string]interface{}{ 107 "name": "pc", 108 "id": seed20.AssertedSnapID("pc"), 109 "type": "gadget", 110 "default-channel": "20", 111 }}, 112 }, nil) 113 114 return restore 115 } 116 117 func (s *systemsSuite) TestSystemsGetSome(c *check.C) { 118 m := boot.Modeenv{ 119 Mode: "run", 120 } 121 err := m.WriteTo("") 122 c.Assert(err, check.IsNil) 123 124 d := s.daemonWithOverlordMockAndStore(c) 125 hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner()) 126 c.Assert(err, check.IsNil) 127 mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil) 128 c.Assert(err, check.IsNil) 129 d.Overlord().AddManager(mgr) 130 131 st := d.Overlord().State() 132 st.Lock() 133 st.Set("seeded-systems", []map[string]interface{}{{ 134 "system": "20200318", "model": "my-model-2", "brand-id": "my-brand", 135 "revision": 2, "timestamp": "2009-11-10T23:00:00Z", 136 "seed-time": "2009-11-10T23:00:00Z", 137 }}) 138 st.Unlock() 139 140 restore := s.mockSystemSeeds(c) 141 defer restore() 142 143 req, err := http.NewRequest("GET", "/v2/systems", nil) 144 c.Assert(err, check.IsNil) 145 rsp := s.req(c, req, nil).(*daemon.Resp) 146 147 c.Assert(rsp.Status, check.Equals, 200) 148 sys := rsp.Result.(*daemon.SystemsResponse) 149 150 c.Assert(sys, check.DeepEquals, &daemon.SystemsResponse{ 151 Systems: []client.System{ 152 { 153 Current: false, 154 Label: "20191119", 155 Model: client.SystemModelData{ 156 Model: "my-model", 157 BrandID: "my-brand", 158 DisplayName: "my fancy model", 159 }, 160 Brand: snap.StoreAccount{ 161 ID: "my-brand", 162 Username: "my-brand", 163 DisplayName: "My-brand", 164 Validation: "unproven", 165 }, 166 Actions: []client.SystemAction{ 167 {Title: "Install", Mode: "install"}, 168 }, 169 }, { 170 Current: true, 171 Label: "20200318", 172 Model: client.SystemModelData{ 173 Model: "my-model-2", 174 BrandID: "my-brand", 175 DisplayName: "same brand different model", 176 }, 177 Brand: snap.StoreAccount{ 178 ID: "my-brand", 179 Username: "my-brand", 180 DisplayName: "My-brand", 181 Validation: "unproven", 182 }, 183 Actions: []client.SystemAction{ 184 {Title: "Reinstall", Mode: "install"}, 185 {Title: "Recover", Mode: "recover"}, 186 {Title: "Run normally", Mode: "run"}, 187 }, 188 }, 189 }}) 190 } 191 192 func (s *systemsSuite) TestSystemsGetNone(c *check.C) { 193 m := boot.Modeenv{ 194 Mode: "run", 195 } 196 err := m.WriteTo("") 197 c.Assert(err, check.IsNil) 198 199 // model assertion setup 200 d := s.daemonWithOverlordMockAndStore(c) 201 hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner()) 202 c.Assert(err, check.IsNil) 203 mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil) 204 c.Assert(err, check.IsNil) 205 d.Overlord().AddManager(mgr) 206 207 // no system seeds 208 req, err := http.NewRequest("GET", "/v2/systems", nil) 209 c.Assert(err, check.IsNil) 210 rsp := s.req(c, req, nil).(*daemon.Resp) 211 212 c.Assert(rsp.Status, check.Equals, 200) 213 sys := rsp.Result.(*daemon.SystemsResponse) 214 215 c.Assert(sys, check.DeepEquals, &daemon.SystemsResponse{}) 216 } 217 218 func (s *systemsSuite) TestSystemActionRequestErrors(c *check.C) { 219 // modenev must be mocked before daemon is initialized 220 m := boot.Modeenv{ 221 Mode: "run", 222 } 223 err := m.WriteTo("") 224 c.Assert(err, check.IsNil) 225 226 d := s.daemonWithOverlordMockAndStore(c) 227 228 hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner()) 229 c.Assert(err, check.IsNil) 230 mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil) 231 c.Assert(err, check.IsNil) 232 d.Overlord().AddManager(mgr) 233 234 restore := s.mockSystemSeeds(c) 235 defer restore() 236 237 st := d.Overlord().State() 238 239 type table struct { 240 label, body, error string 241 status int 242 unseeded bool 243 } 244 tests := []table{ 245 { 246 label: "foobar", 247 body: `"bogus"`, 248 error: "cannot decode request body into system action:.*", 249 status: 400, 250 }, { 251 label: "", 252 body: `{"action":"do","mode":"install"}`, 253 error: "system action requires the system label to be provided", 254 status: 400, 255 }, { 256 label: "foobar", 257 body: `{"action":"do"}`, 258 error: "system action requires the mode to be provided", 259 status: 400, 260 }, { 261 label: "foobar", 262 body: `{"action":"nope","mode":"install"}`, 263 error: `unsupported action "nope"`, 264 status: 400, 265 }, { 266 label: "foobar", 267 body: `{"action":"do","mode":"install"}`, 268 error: `requested seed system "foobar" does not exist`, 269 status: 404, 270 }, { 271 // valid system label but incorrect action 272 label: "20191119", 273 body: `{"action":"do","mode":"foobar"}`, 274 error: `requested action is not supported by system "20191119"`, 275 status: 400, 276 }, { 277 // valid label and action, but seeding is not complete yet 278 label: "20191119", 279 body: `{"action":"do","mode":"install"}`, 280 error: `cannot request system action, system is seeding`, 281 status: 500, 282 unseeded: true, 283 }, 284 } 285 for _, tc := range tests { 286 st.Lock() 287 if tc.unseeded { 288 st.Set("seeded", nil) 289 m := boot.Modeenv{ 290 Mode: "run", 291 RecoverySystem: tc.label, 292 } 293 err := m.WriteTo("") 294 c.Assert(err, check.IsNil) 295 } else { 296 st.Set("seeded", true) 297 } 298 st.Unlock() 299 c.Logf("tc: %#v", tc) 300 req, err := http.NewRequest("POST", path.Join("/v2/systems", tc.label), strings.NewReader(tc.body)) 301 c.Assert(err, check.IsNil) 302 rsp := s.req(c, req, nil).(*daemon.Resp) 303 c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeError) 304 c.Check(rsp.Status, check.Equals, tc.status) 305 c.Check(rsp.ErrorResult().Message, check.Matches, tc.error) 306 } 307 } 308 309 func (s *systemsSuite) TestSystemActionRequestWithSeeded(c *check.C) { 310 bt := bootloadertest.Mock("mock", c.MkDir()) 311 bootloader.Force(bt) 312 defer func() { bootloader.Force(nil) }() 313 314 cmd := testutil.MockCommand(c, "shutdown", "") 315 defer cmd.Restore() 316 317 restore := s.mockSystemSeeds(c) 318 defer restore() 319 320 model := s.Brands.Model("my-brand", "pc", map[string]interface{}{ 321 "architecture": "amd64", 322 // UC20 323 "grade": "dangerous", 324 "base": "core20", 325 "snaps": []interface{}{ 326 map[string]interface{}{ 327 "name": "pc-kernel", 328 "id": snaptest.AssertedSnapID("oc-kernel"), 329 "type": "kernel", 330 "default-channel": "20", 331 }, 332 map[string]interface{}{ 333 "name": "pc", 334 "id": snaptest.AssertedSnapID("pc"), 335 "type": "gadget", 336 "default-channel": "20", 337 }, 338 }, 339 }) 340 341 currentSystem := []map[string]interface{}{{ 342 "system": "20191119", "model": "my-model", "brand-id": "my-brand", 343 "revision": 2, "timestamp": "2009-11-10T23:00:00Z", 344 "seed-time": "2009-11-10T23:00:00Z", 345 }} 346 347 tt := []struct { 348 currentMode string 349 actionMode string 350 expUnsupported bool 351 expRestart bool 352 comment string 353 }{ 354 { 355 // from run mode -> install mode works to reinstall the system 356 currentMode: "run", 357 actionMode: "install", 358 expRestart: true, 359 comment: "run mode to install mode", 360 }, 361 { 362 // from run mode -> recover mode works to recover the system 363 currentMode: "run", 364 actionMode: "recover", 365 expRestart: true, 366 comment: "run mode to recover mode", 367 }, 368 { 369 // from run mode -> run mode is no-op 370 currentMode: "run", 371 actionMode: "run", 372 comment: "run mode to run mode", 373 }, 374 { 375 // from recover mode -> run mode works to stop recovering and "restore" the system to normal 376 currentMode: "recover", 377 actionMode: "run", 378 expRestart: true, 379 comment: "recover mode to run mode", 380 }, 381 { 382 // from recover mode -> install mode works to stop recovering and reinstall the system if all is lost 383 currentMode: "recover", 384 actionMode: "install", 385 expRestart: true, 386 comment: "recover mode to install mode", 387 }, 388 { 389 // from recover mode -> recover mode is no-op 390 currentMode: "recover", 391 actionMode: "recover", 392 expUnsupported: true, 393 comment: "recover mode to recover mode", 394 }, 395 { 396 // from install mode -> install mode is no-no 397 currentMode: "install", 398 actionMode: "install", 399 expUnsupported: true, 400 comment: "install mode to install mode not supported", 401 }, 402 { 403 // from install mode -> run mode is no-no 404 currentMode: "install", 405 actionMode: "run", 406 expUnsupported: true, 407 comment: "install mode to run mode not supported", 408 }, 409 { 410 // from install mode -> recover mode is no-no 411 currentMode: "install", 412 actionMode: "recover", 413 expUnsupported: true, 414 comment: "install mode to recover mode not supported", 415 }, 416 } 417 418 for _, tc := range tt { 419 c.Logf("tc: %v", tc.comment) 420 // daemon setup - need to do this per-test because we need to re-read 421 // the modeenv during devicemgr startup 422 m := boot.Modeenv{ 423 Mode: tc.currentMode, 424 } 425 if tc.currentMode != "run" { 426 m.RecoverySystem = "20191119" 427 } 428 err := m.WriteTo("") 429 c.Assert(err, check.IsNil) 430 d := s.daemon(c) 431 st := d.Overlord().State() 432 st.Lock() 433 // devicemgr needs boot id to request a reboot 434 st.VerifyReboot("boot-id-0") 435 // device model 436 assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey("")) 437 assertstatetest.AddMany(st, s.Brands.AccountsAndKeys("my-brand")...) 438 s.mockModel(c, st, model) 439 if tc.currentMode == "run" { 440 // only set in run mode 441 st.Set("seeded-systems", currentSystem) 442 } 443 // the seeding is done 444 st.Set("seeded", true) 445 st.Unlock() 446 447 body := map[string]string{ 448 "action": "do", 449 "mode": tc.actionMode, 450 } 451 b, err := json.Marshal(body) 452 c.Assert(err, check.IsNil, check.Commentf(tc.comment)) 453 buf := bytes.NewBuffer(b) 454 req, err := http.NewRequest("POST", "/v2/systems/20191119", buf) 455 c.Assert(err, check.IsNil, check.Commentf(tc.comment)) 456 // as root 457 req.RemoteAddr = "pid=100;uid=0;socket=;" 458 rec := httptest.NewRecorder() 459 s.serveHTTP(c, rec, req) 460 if tc.expUnsupported { 461 c.Check(rec.Code, check.Equals, 400, check.Commentf(tc.comment)) 462 } else { 463 c.Check(rec.Code, check.Equals, 200, check.Commentf(tc.comment)) 464 } 465 466 var rspBody map[string]interface{} 467 err = json.Unmarshal(rec.Body.Bytes(), &rspBody) 468 c.Assert(err, check.IsNil, check.Commentf(tc.comment)) 469 470 var expResp map[string]interface{} 471 if tc.expUnsupported { 472 expResp = map[string]interface{}{ 473 "result": map[string]interface{}{ 474 "message": fmt.Sprintf("requested action is not supported by system %q", "20191119"), 475 }, 476 "status": "Bad Request", 477 "status-code": 400.0, 478 "type": "error", 479 } 480 } else { 481 expResp = map[string]interface{}{ 482 "result": nil, 483 "status": "OK", 484 "status-code": 200.0, 485 "type": "sync", 486 } 487 if tc.expRestart { 488 expResp["maintenance"] = map[string]interface{}{ 489 "kind": "system-restart", 490 "message": "system is restarting", 491 } 492 493 // daemon is not started, only check whether reboot was scheduled as expected 494 495 // reboot flag 496 c.Check(d.RequestedRestart(), check.Equals, state.RestartSystemNow, check.Commentf(tc.comment)) 497 // slow reboot schedule 498 c.Check(cmd.Calls(), check.DeepEquals, [][]string{ 499 {"shutdown", "-r", "+10", "reboot scheduled to update the system"}, 500 }, 501 check.Commentf(tc.comment), 502 ) 503 } 504 } 505 506 c.Assert(rspBody, check.DeepEquals, expResp, check.Commentf(tc.comment)) 507 508 cmd.ForgetCalls() 509 s.resetDaemon() 510 } 511 512 } 513 514 func (s *systemsSuite) TestSystemActionBrokenSeed(c *check.C) { 515 m := boot.Modeenv{ 516 Mode: "run", 517 } 518 err := m.WriteTo("") 519 c.Assert(err, check.IsNil) 520 521 d := s.daemonWithOverlordMockAndStore(c) 522 hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner()) 523 c.Assert(err, check.IsNil) 524 mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil) 525 c.Assert(err, check.IsNil) 526 d.Overlord().AddManager(mgr) 527 528 // the seeding is done 529 st := d.Overlord().State() 530 st.Lock() 531 st.Set("seeded", true) 532 st.Unlock() 533 534 restore := s.mockSystemSeeds(c) 535 defer restore() 536 537 err = os.Remove(filepath.Join(dirs.SnapSeedDir, "systems", "20191119", "model")) 538 c.Assert(err, check.IsNil) 539 540 body := `{"action":"do","title":"reinstall","mode":"install"}` 541 req, err := http.NewRequest("POST", "/v2/systems/20191119", strings.NewReader(body)) 542 c.Assert(err, check.IsNil) 543 rsp := s.req(c, req, nil).(*daemon.Resp) 544 c.Check(rsp.Status, check.Equals, 500) 545 c.Check(rsp.ErrorResult().Message, check.Matches, `cannot load seed system: cannot load assertions: .*`) 546 } 547 548 func (s *systemsSuite) TestSystemActionNonRoot(c *check.C) { 549 d := s.daemonWithOverlordMockAndStore(c) 550 hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner()) 551 c.Assert(err, check.IsNil) 552 mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil) 553 c.Assert(err, check.IsNil) 554 d.Overlord().AddManager(mgr) 555 556 body := `{"action":"do","title":"reinstall","mode":"install"}` 557 558 // pretend to be a simple user 559 req, err := http.NewRequest("POST", "/v2/systems/20191119", strings.NewReader(body)) 560 c.Assert(err, check.IsNil) 561 // non root 562 req.RemoteAddr = "pid=100;uid=1234;socket=;" 563 564 rec := httptest.NewRecorder() 565 s.serveHTTP(c, rec, req) 566 c.Assert(rec.Code, check.Equals, 401) 567 568 var rspBody map[string]interface{} 569 err = json.Unmarshal(rec.Body.Bytes(), &rspBody) 570 c.Check(err, check.IsNil) 571 c.Check(rspBody, check.DeepEquals, map[string]interface{}{ 572 "result": map[string]interface{}{ 573 "message": "access denied", 574 "kind": "login-required", 575 }, 576 "status": "Unauthorized", 577 "status-code": 401.0, 578 "type": "error", 579 }) 580 } 581 582 func (s *systemsSuite) TestSystemRebootNeedsRoot(c *check.C) { 583 s.daemon(c) 584 585 restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error { 586 c.Fatalf("request reboot should not get called") 587 return nil 588 }) 589 defer restore() 590 591 body := `{"action":"reboot"}` 592 url := "/v2/systems" 593 req, err := http.NewRequest("POST", url, strings.NewReader(body)) 594 c.Assert(err, check.IsNil) 595 req.RemoteAddr = "pid=100;uid=1000;socket=;" 596 597 rec := httptest.NewRecorder() 598 s.serveHTTP(c, rec, req) 599 c.Check(rec.Code, check.Equals, 401) 600 } 601 602 func (s *systemsSuite) TestSystemRebootHappy(c *check.C) { 603 s.daemon(c) 604 605 for _, tc := range []struct { 606 systemLabel, mode string 607 }{ 608 {"", ""}, 609 {"20200101", ""}, 610 {"", "run"}, 611 {"", "recover"}, 612 {"20200101", "run"}, 613 {"20200101", "recover"}, 614 } { 615 called := 0 616 restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error { 617 called++ 618 c.Check(dm, check.NotNil) 619 c.Check(systemLabel, check.Equals, tc.systemLabel) 620 c.Check(mode, check.Equals, tc.mode) 621 return nil 622 }) 623 defer restore() 624 625 body := fmt.Sprintf(`{"action":"reboot", "mode":"%s"}`, tc.mode) 626 url := "/v2/systems" 627 if tc.systemLabel != "" { 628 url += "/" + tc.systemLabel 629 } 630 req, err := http.NewRequest("POST", url, strings.NewReader(body)) 631 c.Assert(err, check.IsNil) 632 req.RemoteAddr = "pid=100;uid=0;socket=;" 633 634 rec := httptest.NewRecorder() 635 s.serveHTTP(c, rec, req) 636 c.Check(rec.Code, check.Equals, 200) 637 c.Check(called, check.Equals, 1) 638 } 639 } 640 641 func (s *systemsSuite) TestSystemRebootUnhappy(c *check.C) { 642 s.daemon(c) 643 644 for _, tc := range []struct { 645 rebootErr error 646 expectedHttpCode int 647 expectedErr string 648 }{ 649 {fmt.Errorf("boom"), 500, "boom"}, 650 {os.ErrNotExist, 404, `requested seed system "" does not exist`}, 651 {devicestate.ErrUnsupportedAction, 400, `requested action is not supported by system ""`}, 652 } { 653 called := 0 654 restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error { 655 called++ 656 return tc.rebootErr 657 }) 658 defer restore() 659 660 body := fmt.Sprintf(`{"action":"reboot"}`) 661 url := "/v2/systems" 662 req, err := http.NewRequest("POST", url, strings.NewReader(body)) 663 c.Assert(err, check.IsNil) 664 req.RemoteAddr = "pid=100;uid=0;socket=;" 665 666 rec := httptest.NewRecorder() 667 s.serveHTTP(c, rec, req) 668 c.Check(rec.Code, check.Equals, tc.expectedHttpCode) 669 c.Check(called, check.Equals, 1) 670 671 var rspBody map[string]interface{} 672 err = json.Unmarshal(rec.Body.Bytes(), &rspBody) 673 c.Check(err, check.IsNil) 674 c.Check(rspBody["status-code"], check.Equals, float64(tc.expectedHttpCode)) 675 result := rspBody["result"].(map[string]interface{}) 676 c.Check(result["message"], check.Equals, tc.expectedErr) 677 } 678 }