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