github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/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.syncReq(c, req, nil) 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.syncReq(c, req, nil) 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.errorReq(c, req, nil) 303 c.Check(rsp.Status, check.Equals, tc.status) 304 c.Check(rsp.ErrorResult().Message, check.Matches, tc.error) 305 } 306 } 307 308 func (s *systemsSuite) TestSystemActionRequestWithSeeded(c *check.C) { 309 bt := bootloadertest.Mock("mock", c.MkDir()) 310 bootloader.Force(bt) 311 defer func() { bootloader.Force(nil) }() 312 313 cmd := testutil.MockCommand(c, "shutdown", "") 314 defer cmd.Restore() 315 316 restore := s.mockSystemSeeds(c) 317 defer restore() 318 319 model := s.Brands.Model("my-brand", "pc", map[string]interface{}{ 320 "architecture": "amd64", 321 // UC20 322 "grade": "dangerous", 323 "base": "core20", 324 "snaps": []interface{}{ 325 map[string]interface{}{ 326 "name": "pc-kernel", 327 "id": snaptest.AssertedSnapID("oc-kernel"), 328 "type": "kernel", 329 "default-channel": "20", 330 }, 331 map[string]interface{}{ 332 "name": "pc", 333 "id": snaptest.AssertedSnapID("pc"), 334 "type": "gadget", 335 "default-channel": "20", 336 }, 337 }, 338 }) 339 340 currentSystem := []map[string]interface{}{{ 341 "system": "20191119", "model": "my-model", "brand-id": "my-brand", 342 "revision": 2, "timestamp": "2009-11-10T23:00:00Z", 343 "seed-time": "2009-11-10T23:00:00Z", 344 }} 345 346 tt := []struct { 347 currentMode string 348 actionMode string 349 expUnsupported bool 350 expRestart bool 351 comment string 352 }{ 353 { 354 // from run mode -> install mode works to reinstall the system 355 currentMode: "run", 356 actionMode: "install", 357 expRestart: true, 358 comment: "run mode to install mode", 359 }, 360 { 361 // from run mode -> recover mode works to recover the system 362 currentMode: "run", 363 actionMode: "recover", 364 expRestart: true, 365 comment: "run mode to recover mode", 366 }, 367 { 368 // from run mode -> run mode is no-op 369 currentMode: "run", 370 actionMode: "run", 371 comment: "run mode to run mode", 372 }, 373 { 374 // from recover mode -> run mode works to stop recovering and "restore" the system to normal 375 currentMode: "recover", 376 actionMode: "run", 377 expRestart: true, 378 comment: "recover mode to run mode", 379 }, 380 { 381 // from recover mode -> install mode works to stop recovering and reinstall the system if all is lost 382 currentMode: "recover", 383 actionMode: "install", 384 expRestart: true, 385 comment: "recover mode to install mode", 386 }, 387 { 388 // from recover mode -> recover mode is no-op 389 currentMode: "recover", 390 actionMode: "recover", 391 expUnsupported: true, 392 comment: "recover mode to recover mode", 393 }, 394 { 395 // from install mode -> install mode is no-no 396 currentMode: "install", 397 actionMode: "install", 398 expUnsupported: true, 399 comment: "install mode to install mode not supported", 400 }, 401 { 402 // from install mode -> run mode is no-no 403 currentMode: "install", 404 actionMode: "run", 405 expUnsupported: true, 406 comment: "install mode to run mode not supported", 407 }, 408 { 409 // from install mode -> recover mode is no-no 410 currentMode: "install", 411 actionMode: "recover", 412 expUnsupported: true, 413 comment: "install mode to recover mode not supported", 414 }, 415 } 416 417 for _, tc := range tt { 418 c.Logf("tc: %v", tc.comment) 419 // daemon setup - need to do this per-test because we need to re-read 420 // the modeenv during devicemgr startup 421 m := boot.Modeenv{ 422 Mode: tc.currentMode, 423 } 424 if tc.currentMode != "run" { 425 m.RecoverySystem = "20191119" 426 } 427 err := m.WriteTo("") 428 c.Assert(err, check.IsNil) 429 d := s.daemon(c) 430 st := d.Overlord().State() 431 st.Lock() 432 // devicemgr needs boot id to request a reboot 433 st.VerifyReboot("boot-id-0") 434 // device model 435 assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey("")) 436 assertstatetest.AddMany(st, s.Brands.AccountsAndKeys("my-brand")...) 437 s.mockModel(c, st, model) 438 if tc.currentMode == "run" { 439 // only set in run mode 440 st.Set("seeded-systems", currentSystem) 441 } 442 // the seeding is done 443 st.Set("seeded", true) 444 st.Unlock() 445 446 body := map[string]string{ 447 "action": "do", 448 "mode": tc.actionMode, 449 } 450 b, err := json.Marshal(body) 451 c.Assert(err, check.IsNil, check.Commentf(tc.comment)) 452 buf := bytes.NewBuffer(b) 453 req, err := http.NewRequest("POST", "/v2/systems/20191119", buf) 454 c.Assert(err, check.IsNil, check.Commentf(tc.comment)) 455 // as root 456 req.RemoteAddr = "pid=100;uid=0;socket=;" 457 rec := httptest.NewRecorder() 458 s.serveHTTP(c, rec, req) 459 if tc.expUnsupported { 460 c.Check(rec.Code, check.Equals, 400, check.Commentf(tc.comment)) 461 } else { 462 c.Check(rec.Code, check.Equals, 200, check.Commentf(tc.comment)) 463 } 464 465 var rspBody map[string]interface{} 466 err = json.Unmarshal(rec.Body.Bytes(), &rspBody) 467 c.Assert(err, check.IsNil, check.Commentf(tc.comment)) 468 469 var expResp map[string]interface{} 470 if tc.expUnsupported { 471 expResp = map[string]interface{}{ 472 "result": map[string]interface{}{ 473 "message": fmt.Sprintf("requested action is not supported by system %q", "20191119"), 474 }, 475 "status": "Bad Request", 476 "status-code": 400.0, 477 "type": "error", 478 } 479 } else { 480 expResp = map[string]interface{}{ 481 "result": nil, 482 "status": "OK", 483 "status-code": 200.0, 484 "type": "sync", 485 } 486 if tc.expRestart { 487 expResp["maintenance"] = map[string]interface{}{ 488 "kind": "system-restart", 489 "message": "system is restarting", 490 } 491 492 // daemon is not started, only check whether reboot was scheduled as expected 493 494 // reboot flag 495 c.Check(d.RequestedRestart(), check.Equals, state.RestartSystemNow, check.Commentf(tc.comment)) 496 // slow reboot schedule 497 c.Check(cmd.Calls(), check.DeepEquals, [][]string{ 498 {"shutdown", "-r", "+10", "reboot scheduled to update the system"}, 499 }, 500 check.Commentf(tc.comment), 501 ) 502 } 503 } 504 505 c.Assert(rspBody, check.DeepEquals, expResp, check.Commentf(tc.comment)) 506 507 cmd.ForgetCalls() 508 s.resetDaemon() 509 } 510 511 } 512 513 func (s *systemsSuite) TestSystemActionBrokenSeed(c *check.C) { 514 m := boot.Modeenv{ 515 Mode: "run", 516 } 517 err := m.WriteTo("") 518 c.Assert(err, check.IsNil) 519 520 d := s.daemonWithOverlordMockAndStore(c) 521 hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner()) 522 c.Assert(err, check.IsNil) 523 mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil) 524 c.Assert(err, check.IsNil) 525 d.Overlord().AddManager(mgr) 526 527 // the seeding is done 528 st := d.Overlord().State() 529 st.Lock() 530 st.Set("seeded", true) 531 st.Unlock() 532 533 restore := s.mockSystemSeeds(c) 534 defer restore() 535 536 err = os.Remove(filepath.Join(dirs.SnapSeedDir, "systems", "20191119", "model")) 537 c.Assert(err, check.IsNil) 538 539 body := `{"action":"do","title":"reinstall","mode":"install"}` 540 req, err := http.NewRequest("POST", "/v2/systems/20191119", strings.NewReader(body)) 541 c.Assert(err, check.IsNil) 542 rsp := s.errorReq(c, req, nil) 543 c.Check(rsp.Status, check.Equals, 500) 544 c.Check(rsp.ErrorResult().Message, check.Matches, `cannot load seed system: cannot load assertions: .*`) 545 } 546 547 func (s *systemsSuite) TestSystemActionNonRoot(c *check.C) { 548 d := s.daemonWithOverlordMockAndStore(c) 549 hookMgr, err := hookstate.Manager(d.Overlord().State(), d.Overlord().TaskRunner()) 550 c.Assert(err, check.IsNil) 551 mgr, err := devicestate.Manager(d.Overlord().State(), hookMgr, d.Overlord().TaskRunner(), nil) 552 c.Assert(err, check.IsNil) 553 d.Overlord().AddManager(mgr) 554 555 body := `{"action":"do","title":"reinstall","mode":"install"}` 556 557 // pretend to be a simple user 558 req, err := http.NewRequest("POST", "/v2/systems/20191119", strings.NewReader(body)) 559 c.Assert(err, check.IsNil) 560 // non root 561 req.RemoteAddr = "pid=100;uid=1234;socket=;" 562 563 rec := httptest.NewRecorder() 564 s.serveHTTP(c, rec, req) 565 c.Assert(rec.Code, check.Equals, 401) 566 567 var rspBody map[string]interface{} 568 err = json.Unmarshal(rec.Body.Bytes(), &rspBody) 569 c.Check(err, check.IsNil) 570 c.Check(rspBody, check.DeepEquals, map[string]interface{}{ 571 "result": map[string]interface{}{ 572 "message": "access denied", 573 "kind": "login-required", 574 }, 575 "status": "Unauthorized", 576 "status-code": 401.0, 577 "type": "error", 578 }) 579 } 580 581 func (s *systemsSuite) TestSystemRebootNeedsRoot(c *check.C) { 582 s.daemon(c) 583 584 restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error { 585 c.Fatalf("request reboot should not get called") 586 return nil 587 }) 588 defer restore() 589 590 body := `{"action":"reboot"}` 591 url := "/v2/systems" 592 req, err := http.NewRequest("POST", url, strings.NewReader(body)) 593 c.Assert(err, check.IsNil) 594 req.RemoteAddr = "pid=100;uid=1000;socket=;" 595 596 rec := httptest.NewRecorder() 597 s.serveHTTP(c, rec, req) 598 c.Check(rec.Code, check.Equals, 401) 599 } 600 601 func (s *systemsSuite) TestSystemRebootHappy(c *check.C) { 602 s.daemon(c) 603 604 for _, tc := range []struct { 605 systemLabel, mode string 606 }{ 607 {"", ""}, 608 {"20200101", ""}, 609 {"", "run"}, 610 {"", "recover"}, 611 {"20200101", "run"}, 612 {"20200101", "recover"}, 613 } { 614 called := 0 615 restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error { 616 called++ 617 c.Check(dm, check.NotNil) 618 c.Check(systemLabel, check.Equals, tc.systemLabel) 619 c.Check(mode, check.Equals, tc.mode) 620 return nil 621 }) 622 defer restore() 623 624 body := fmt.Sprintf(`{"action":"reboot", "mode":"%s"}`, tc.mode) 625 url := "/v2/systems" 626 if tc.systemLabel != "" { 627 url += "/" + tc.systemLabel 628 } 629 req, err := http.NewRequest("POST", url, strings.NewReader(body)) 630 c.Assert(err, check.IsNil) 631 req.RemoteAddr = "pid=100;uid=0;socket=;" 632 633 rec := httptest.NewRecorder() 634 s.serveHTTP(c, rec, req) 635 c.Check(rec.Code, check.Equals, 200) 636 c.Check(called, check.Equals, 1) 637 } 638 } 639 640 func (s *systemsSuite) TestSystemRebootUnhappy(c *check.C) { 641 s.daemon(c) 642 643 for _, tc := range []struct { 644 rebootErr error 645 expectedHttpCode int 646 expectedErr string 647 }{ 648 {fmt.Errorf("boom"), 500, "boom"}, 649 {os.ErrNotExist, 404, `requested seed system "" does not exist`}, 650 {devicestate.ErrUnsupportedAction, 400, `requested action is not supported by system ""`}, 651 } { 652 called := 0 653 restore := daemon.MockDeviceManagerReboot(func(dm *devicestate.DeviceManager, systemLabel, mode string) error { 654 called++ 655 return tc.rebootErr 656 }) 657 defer restore() 658 659 body := fmt.Sprintf(`{"action":"reboot"}`) 660 url := "/v2/systems" 661 req, err := http.NewRequest("POST", url, strings.NewReader(body)) 662 c.Assert(err, check.IsNil) 663 req.RemoteAddr = "pid=100;uid=0;socket=;" 664 665 rec := httptest.NewRecorder() 666 s.serveHTTP(c, rec, req) 667 c.Check(rec.Code, check.Equals, tc.expectedHttpCode) 668 c.Check(called, check.Equals, 1) 669 670 var rspBody map[string]interface{} 671 err = json.Unmarshal(rec.Body.Bytes(), &rspBody) 672 c.Check(err, check.IsNil) 673 c.Check(rspBody["status-code"], check.Equals, float64(tc.expectedHttpCode)) 674 result := rspBody["result"].(map[string]interface{}) 675 c.Check(result["message"], check.Equals, tc.expectedErr) 676 } 677 }