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