github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/daemon/api_interfaces_test.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-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 "strings" 29 30 "gopkg.in/check.v1" 31 32 "github.com/snapcore/snapd/client" 33 "github.com/snapcore/snapd/daemon" 34 "github.com/snapcore/snapd/interfaces" 35 "github.com/snapcore/snapd/interfaces/builtin" 36 "github.com/snapcore/snapd/interfaces/ifacetest" 37 "github.com/snapcore/snapd/overlord/ifacestate" 38 "github.com/snapcore/snapd/overlord/state" 39 ) 40 41 var _ = check.Suite(&interfacesSuite{}) 42 43 type interfacesSuite struct { 44 apiBaseSuite 45 } 46 47 func mockIface(c *check.C, d *daemon.Daemon, iface interfaces.Interface) { 48 err := d.Overlord().InterfaceManager().Repository().AddInterface(iface) 49 c.Assert(err, check.IsNil) 50 } 51 52 // inverseCaseMapper implements SnapMapper to use lower case internally and upper case externally. 53 type inverseCaseMapper struct { 54 ifacestate.IdentityMapper // Embed the identity mapper to reuse empty state mapping functions. 55 } 56 57 func (m *inverseCaseMapper) RemapSnapFromRequest(snapName string) string { 58 return strings.ToLower(snapName) 59 } 60 61 func (m *inverseCaseMapper) RemapSnapToResponse(snapName string) string { 62 return strings.ToUpper(snapName) 63 } 64 65 func (m *inverseCaseMapper) SystemSnapName() string { 66 return "core" 67 } 68 69 // Tests for POST /v2/interfaces 70 71 const ( 72 consumerYaml = ` 73 name: consumer 74 version: 1 75 apps: 76 app: 77 plugs: 78 plug: 79 interface: test 80 key: value 81 label: label 82 ` 83 84 producerYaml = ` 85 name: producer 86 version: 1 87 apps: 88 app: 89 slots: 90 slot: 91 interface: test 92 key: value 93 label: label 94 ` 95 96 coreProducerYaml = ` 97 name: core 98 version: 1 99 slots: 100 slot: 101 interface: test 102 key: value 103 label: label 104 ` 105 106 differentProducerYaml = ` 107 name: producer 108 version: 1 109 apps: 110 app: 111 slots: 112 slot: 113 interface: different 114 key: value 115 label: label 116 ` 117 ) 118 119 func (s *interfacesSuite) TestConnectPlugSuccess(c *check.C) { 120 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 121 defer restore() 122 // Install an inverse case mapper to exercise the interface mapping at the same time. 123 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 124 defer restore() 125 126 d := s.daemon(c) 127 128 s.mockSnap(c, consumerYaml) 129 s.mockSnap(c, producerYaml) 130 131 d.Overlord().Loop() 132 defer d.Overlord().Stop() 133 134 action := &client.InterfaceAction{ 135 Action: "connect", 136 Plugs: []client.Plug{{Snap: "CONSUMER", Name: "plug"}}, 137 Slots: []client.Slot{{Snap: "PRODUCER", Name: "slot"}}, 138 } 139 text, err := json.Marshal(action) 140 c.Assert(err, check.IsNil) 141 buf := bytes.NewBuffer(text) 142 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 143 c.Assert(err, check.IsNil) 144 rec := httptest.NewRecorder() 145 s.req(c, req, nil).ServeHTTP(rec, req) 146 c.Check(rec.Code, check.Equals, 202) 147 var body map[string]interface{} 148 err = json.Unmarshal(rec.Body.Bytes(), &body) 149 c.Check(err, check.IsNil) 150 id := body["change"].(string) 151 152 st := d.Overlord().State() 153 st.Lock() 154 chg := st.Change(id) 155 st.Unlock() 156 c.Assert(chg, check.NotNil) 157 158 <-chg.Ready() 159 160 st.Lock() 161 err = chg.Err() 162 st.Unlock() 163 c.Assert(err, check.IsNil) 164 165 repo := d.Overlord().InterfaceManager().Repository() 166 ifaces := repo.Interfaces() 167 c.Assert(ifaces.Connections, check.HasLen, 1) 168 c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{ 169 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 170 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 171 }}) 172 } 173 174 func (s *interfacesSuite) TestConnectPlugFailureInterfaceMismatch(c *check.C) { 175 d := s.daemon(c) 176 177 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 178 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "different"}) 179 s.mockSnap(c, consumerYaml) 180 s.mockSnap(c, differentProducerYaml) 181 182 action := &client.InterfaceAction{ 183 Action: "connect", 184 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 185 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 186 } 187 text, err := json.Marshal(action) 188 c.Assert(err, check.IsNil) 189 buf := bytes.NewBuffer(text) 190 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 191 c.Assert(err, check.IsNil) 192 rec := httptest.NewRecorder() 193 s.req(c, req, nil).ServeHTTP(rec, req) 194 c.Check(rec.Code, check.Equals, 400) 195 var body map[string]interface{} 196 err = json.Unmarshal(rec.Body.Bytes(), &body) 197 c.Check(err, check.IsNil) 198 c.Check(body, check.DeepEquals, map[string]interface{}{ 199 "result": map[string]interface{}{ 200 "message": "cannot connect consumer:plug (\"test\" interface) to producer:slot (\"different\" interface)", 201 }, 202 "status": "Bad Request", 203 "status-code": 400.0, 204 "type": "error", 205 }) 206 repo := d.Overlord().InterfaceManager().Repository() 207 ifaces := repo.Interfaces() 208 c.Assert(ifaces.Connections, check.HasLen, 0) 209 } 210 211 func (s *interfacesSuite) TestConnectPlugFailureNoSuchPlug(c *check.C) { 212 d := s.daemon(c) 213 214 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 215 // there is no consumer, no plug defined 216 s.mockSnap(c, producerYaml) 217 s.mockSnap(c, consumerYaml) 218 219 action := &client.InterfaceAction{ 220 Action: "connect", 221 Plugs: []client.Plug{{Snap: "consumer", Name: "missingplug"}}, 222 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 223 } 224 text, err := json.Marshal(action) 225 c.Assert(err, check.IsNil) 226 buf := bytes.NewBuffer(text) 227 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 228 c.Assert(err, check.IsNil) 229 rec := httptest.NewRecorder() 230 s.req(c, req, nil).ServeHTTP(rec, req) 231 c.Check(rec.Code, check.Equals, 400) 232 233 var body map[string]interface{} 234 err = json.Unmarshal(rec.Body.Bytes(), &body) 235 c.Check(err, check.IsNil) 236 c.Check(body, check.DeepEquals, map[string]interface{}{ 237 "result": map[string]interface{}{ 238 "message": "snap \"consumer\" has no plug named \"missingplug\"", 239 }, 240 "status": "Bad Request", 241 "status-code": 400.0, 242 "type": "error", 243 }) 244 245 repo := d.Overlord().InterfaceManager().Repository() 246 ifaces := repo.Interfaces() 247 c.Assert(ifaces.Connections, check.HasLen, 0) 248 } 249 250 func (s *interfacesSuite) TestConnectAlreadyConnected(c *check.C) { 251 d := s.daemon(c) 252 253 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 254 // there is no consumer, no plug defined 255 s.mockSnap(c, producerYaml) 256 s.mockSnap(c, consumerYaml) 257 258 repo := d.Overlord().InterfaceManager().Repository() 259 connRef := &interfaces.ConnRef{ 260 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 261 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 262 } 263 264 d.Overlord().Loop() 265 defer d.Overlord().Stop() 266 267 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 268 c.Assert(err, check.IsNil) 269 conns := map[string]interface{}{ 270 "consumer:plug producer:slot": map[string]interface{}{ 271 "auto": false, 272 }, 273 } 274 st := d.Overlord().State() 275 st.Lock() 276 st.Set("conns", conns) 277 st.Unlock() 278 279 action := &client.InterfaceAction{ 280 Action: "connect", 281 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 282 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 283 } 284 text, err := json.Marshal(action) 285 c.Assert(err, check.IsNil) 286 buf := bytes.NewBuffer(text) 287 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 288 c.Assert(err, check.IsNil) 289 rec := httptest.NewRecorder() 290 s.req(c, req, nil).ServeHTTP(rec, req) 291 c.Check(rec.Code, check.Equals, 202) 292 var body map[string]interface{} 293 err = json.Unmarshal(rec.Body.Bytes(), &body) 294 c.Check(err, check.IsNil) 295 id := body["change"].(string) 296 297 st.Lock() 298 chg := st.Change(id) 299 c.Assert(chg.Tasks(), check.HasLen, 0) 300 c.Assert(chg.Status(), check.Equals, state.DoneStatus) 301 st.Unlock() 302 } 303 304 func (s *interfacesSuite) TestConnectPlugFailureNoSuchSlot(c *check.C) { 305 d := s.daemon(c) 306 307 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 308 s.mockSnap(c, consumerYaml) 309 s.mockSnap(c, producerYaml) 310 // there is no producer, no slot defined 311 312 action := &client.InterfaceAction{ 313 Action: "connect", 314 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 315 Slots: []client.Slot{{Snap: "producer", Name: "missingslot"}}, 316 } 317 text, err := json.Marshal(action) 318 c.Assert(err, check.IsNil) 319 buf := bytes.NewBuffer(text) 320 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 321 c.Assert(err, check.IsNil) 322 rec := httptest.NewRecorder() 323 s.req(c, req, nil).ServeHTTP(rec, req) 324 c.Check(rec.Code, check.Equals, 400) 325 326 var body map[string]interface{} 327 err = json.Unmarshal(rec.Body.Bytes(), &body) 328 c.Check(err, check.IsNil) 329 c.Check(body, check.DeepEquals, map[string]interface{}{ 330 "result": map[string]interface{}{ 331 "message": "snap \"producer\" has no slot named \"missingslot\"", 332 }, 333 "status": "Bad Request", 334 "status-code": 400.0, 335 "type": "error", 336 }) 337 338 repo := d.Overlord().InterfaceManager().Repository() 339 ifaces := repo.Interfaces() 340 c.Assert(ifaces.Connections, check.HasLen, 0) 341 } 342 343 func (s *interfacesSuite) TestConnectPlugChangeConflict(c *check.C) { 344 d := s.daemon(c) 345 346 mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"}) 347 s.mockSnap(c, consumerYaml) 348 s.mockSnap(c, producerYaml) 349 // there is no producer, no slot defined 350 351 s.simulateConflict("consumer") 352 353 action := &client.InterfaceAction{ 354 Action: "connect", 355 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 356 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 357 } 358 text, err := json.Marshal(action) 359 c.Assert(err, check.IsNil) 360 buf := bytes.NewBuffer(text) 361 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 362 c.Assert(err, check.IsNil) 363 rec := httptest.NewRecorder() 364 s.req(c, req, nil).ServeHTTP(rec, req) 365 c.Check(rec.Code, check.Equals, 409) 366 367 var body map[string]interface{} 368 err = json.Unmarshal(rec.Body.Bytes(), &body) 369 c.Check(err, check.IsNil) 370 c.Check(body, check.DeepEquals, map[string]interface{}{ 371 "status-code": 409., 372 "status": "Conflict", 373 "result": map[string]interface{}{ 374 "message": `snap "consumer" has "manip" change in progress`, 375 "kind": "snap-change-conflict", 376 "value": map[string]interface{}{ 377 "change-kind": "manip", 378 "snap-name": "consumer", 379 }, 380 }, 381 "type": "error"}) 382 } 383 384 func (s *interfacesSuite) TestConnectCoreSystemAlias(c *check.C) { 385 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 386 defer revert() 387 d := s.daemon(c) 388 389 s.mockSnap(c, consumerYaml) 390 s.mockSnap(c, coreProducerYaml) 391 392 d.Overlord().Loop() 393 defer d.Overlord().Stop() 394 395 action := &client.InterfaceAction{ 396 Action: "connect", 397 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 398 Slots: []client.Slot{{Snap: "system", Name: "slot"}}, 399 } 400 text, err := json.Marshal(action) 401 c.Assert(err, check.IsNil) 402 buf := bytes.NewBuffer(text) 403 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 404 c.Assert(err, check.IsNil) 405 rec := httptest.NewRecorder() 406 s.req(c, req, nil).ServeHTTP(rec, req) 407 c.Check(rec.Code, check.Equals, 202) 408 var body map[string]interface{} 409 err = json.Unmarshal(rec.Body.Bytes(), &body) 410 c.Check(err, check.IsNil) 411 id := body["change"].(string) 412 413 st := d.Overlord().State() 414 st.Lock() 415 chg := st.Change(id) 416 st.Unlock() 417 c.Assert(chg, check.NotNil) 418 419 <-chg.Ready() 420 421 st.Lock() 422 err = chg.Err() 423 st.Unlock() 424 c.Assert(err, check.IsNil) 425 426 repo := d.Overlord().InterfaceManager().Repository() 427 ifaces := repo.Interfaces() 428 c.Assert(ifaces.Connections, check.HasLen, 1) 429 c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{ 430 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 431 SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}}}) 432 } 433 434 func (s *interfacesSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slotName string) { 435 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 436 defer restore() 437 // Install an inverse case mapper to exercise the interface mapping at the same time. 438 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 439 defer restore() 440 d := s.daemon(c) 441 442 s.mockSnap(c, consumerYaml) 443 s.mockSnap(c, producerYaml) 444 445 repo := d.Overlord().InterfaceManager().Repository() 446 connRef := &interfaces.ConnRef{ 447 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 448 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 449 } 450 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 451 c.Assert(err, check.IsNil) 452 453 st := d.Overlord().State() 454 st.Lock() 455 st.Set("conns", map[string]interface{}{ 456 "consumer:plug producer:slot": map[string]interface{}{ 457 "interface": "test", 458 }, 459 }) 460 st.Unlock() 461 462 d.Overlord().Loop() 463 defer d.Overlord().Stop() 464 465 action := &client.InterfaceAction{ 466 Action: "disconnect", 467 Plugs: []client.Plug{{Snap: plugSnap, Name: plugName}}, 468 Slots: []client.Slot{{Snap: slotSnap, Name: slotName}}, 469 } 470 text, err := json.Marshal(action) 471 c.Assert(err, check.IsNil) 472 buf := bytes.NewBuffer(text) 473 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 474 c.Assert(err, check.IsNil) 475 rec := httptest.NewRecorder() 476 s.req(c, req, nil).ServeHTTP(rec, req) 477 c.Check(rec.Code, check.Equals, 202) 478 var body map[string]interface{} 479 err = json.Unmarshal(rec.Body.Bytes(), &body) 480 c.Check(err, check.IsNil) 481 id := body["change"].(string) 482 483 st.Lock() 484 chg := st.Change(id) 485 st.Unlock() 486 c.Assert(chg, check.NotNil) 487 488 <-chg.Ready() 489 490 st.Lock() 491 err = chg.Err() 492 st.Unlock() 493 c.Assert(err, check.IsNil) 494 495 ifaces := repo.Interfaces() 496 c.Assert(ifaces.Connections, check.HasLen, 0) 497 } 498 499 func (s *interfacesSuite) TestDisconnectPlugSuccess(c *check.C) { 500 s.testDisconnect(c, "CONSUMER", "plug", "PRODUCER", "slot") 501 } 502 503 func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptyPlug(c *check.C) { 504 s.testDisconnect(c, "", "", "PRODUCER", "slot") 505 } 506 507 func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptySlot(c *check.C) { 508 s.testDisconnect(c, "CONSUMER", "plug", "", "") 509 } 510 511 func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) { 512 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 513 defer revert() 514 s.daemon(c) 515 516 // there is no consumer, no plug defined 517 s.mockSnap(c, producerYaml) 518 519 action := &client.InterfaceAction{ 520 Action: "disconnect", 521 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 522 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 523 } 524 text, err := json.Marshal(action) 525 c.Assert(err, check.IsNil) 526 buf := bytes.NewBuffer(text) 527 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 528 c.Assert(err, check.IsNil) 529 rec := httptest.NewRecorder() 530 s.req(c, req, nil).ServeHTTP(rec, req) 531 c.Check(rec.Code, check.Equals, 400) 532 var body map[string]interface{} 533 err = json.Unmarshal(rec.Body.Bytes(), &body) 534 c.Check(err, check.IsNil) 535 c.Check(body, check.DeepEquals, map[string]interface{}{ 536 "result": map[string]interface{}{ 537 "message": "snap \"consumer\" has no plug named \"plug\"", 538 }, 539 "status": "Bad Request", 540 "status-code": 400.0, 541 "type": "error", 542 }) 543 } 544 545 func (s *interfacesSuite) TestDisconnectPlugNothingToDo(c *check.C) { 546 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 547 defer revert() 548 s.daemon(c) 549 550 s.mockSnap(c, consumerYaml) 551 s.mockSnap(c, producerYaml) 552 553 action := &client.InterfaceAction{ 554 Action: "disconnect", 555 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 556 Slots: []client.Slot{{Snap: "", Name: ""}}, 557 } 558 text, err := json.Marshal(action) 559 c.Assert(err, check.IsNil) 560 buf := bytes.NewBuffer(text) 561 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 562 c.Assert(err, check.IsNil) 563 rec := httptest.NewRecorder() 564 s.req(c, req, nil).ServeHTTP(rec, req) 565 c.Check(rec.Code, check.Equals, 400) 566 var body map[string]interface{} 567 err = json.Unmarshal(rec.Body.Bytes(), &body) 568 c.Check(err, check.IsNil) 569 c.Check(body, check.DeepEquals, map[string]interface{}{ 570 "result": map[string]interface{}{ 571 "message": "nothing to do", 572 "kind": "interfaces-unchanged", 573 }, 574 "status": "Bad Request", 575 "status-code": 400.0, 576 "type": "error", 577 }) 578 } 579 580 func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) { 581 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 582 defer revert() 583 s.daemon(c) 584 585 s.mockSnap(c, consumerYaml) 586 // there is no producer, no slot defined 587 588 action := &client.InterfaceAction{ 589 Action: "disconnect", 590 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 591 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 592 } 593 text, err := json.Marshal(action) 594 c.Assert(err, check.IsNil) 595 buf := bytes.NewBuffer(text) 596 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 597 c.Assert(err, check.IsNil) 598 rec := httptest.NewRecorder() 599 s.req(c, req, nil).ServeHTTP(rec, req) 600 601 c.Check(rec.Code, check.Equals, 400) 602 var body map[string]interface{} 603 err = json.Unmarshal(rec.Body.Bytes(), &body) 604 c.Check(err, check.IsNil) 605 c.Check(body, check.DeepEquals, map[string]interface{}{ 606 "result": map[string]interface{}{ 607 "message": "snap \"producer\" has no slot named \"slot\"", 608 }, 609 "status": "Bad Request", 610 "status-code": 400.0, 611 "type": "error", 612 }) 613 } 614 615 func (s *interfacesSuite) TestDisconnectPlugFailureNotConnected(c *check.C) { 616 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 617 defer revert() 618 s.daemon(c) 619 620 s.mockSnap(c, consumerYaml) 621 s.mockSnap(c, producerYaml) 622 623 action := &client.InterfaceAction{ 624 Action: "disconnect", 625 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 626 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 627 } 628 text, err := json.Marshal(action) 629 c.Assert(err, check.IsNil) 630 buf := bytes.NewBuffer(text) 631 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 632 c.Assert(err, check.IsNil) 633 rec := httptest.NewRecorder() 634 s.req(c, req, nil).ServeHTTP(rec, req) 635 636 c.Check(rec.Code, check.Equals, 400) 637 var body map[string]interface{} 638 err = json.Unmarshal(rec.Body.Bytes(), &body) 639 c.Check(err, check.IsNil) 640 c.Check(body, check.DeepEquals, map[string]interface{}{ 641 "result": map[string]interface{}{ 642 "message": "cannot disconnect consumer:plug from producer:slot, it is not connected", 643 }, 644 "status": "Bad Request", 645 "status-code": 400.0, 646 "type": "error", 647 }) 648 } 649 650 func (s *interfacesSuite) TestDisconnectForgetPlugFailureNotConnected(c *check.C) { 651 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 652 defer revert() 653 s.daemon(c) 654 655 s.mockSnap(c, consumerYaml) 656 s.mockSnap(c, producerYaml) 657 658 action := &client.InterfaceAction{ 659 Action: "disconnect", 660 Forget: true, 661 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 662 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 663 } 664 text, err := json.Marshal(action) 665 c.Assert(err, check.IsNil) 666 buf := bytes.NewBuffer(text) 667 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 668 c.Assert(err, check.IsNil) 669 rec := httptest.NewRecorder() 670 s.req(c, req, nil).ServeHTTP(rec, req) 671 672 c.Check(rec.Code, check.Equals, 400) 673 var body map[string]interface{} 674 err = json.Unmarshal(rec.Body.Bytes(), &body) 675 c.Check(err, check.IsNil) 676 c.Check(body, check.DeepEquals, map[string]interface{}{ 677 "result": map[string]interface{}{ 678 "message": "cannot forget connection consumer:plug from producer:slot, it was not connected", 679 }, 680 "status": "Bad Request", 681 "status-code": 400.0, 682 "type": "error", 683 }) 684 } 685 686 func (s *interfacesSuite) TestDisconnectConflict(c *check.C) { 687 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 688 defer revert() 689 d := s.daemon(c) 690 691 s.mockSnap(c, consumerYaml) 692 s.mockSnap(c, producerYaml) 693 694 repo := d.Overlord().InterfaceManager().Repository() 695 connRef := &interfaces.ConnRef{ 696 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 697 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 698 } 699 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 700 c.Assert(err, check.IsNil) 701 702 st := d.Overlord().State() 703 st.Lock() 704 st.Set("conns", map[string]interface{}{ 705 "consumer:plug producer:slot": map[string]interface{}{ 706 "interface": "test", 707 }, 708 }) 709 st.Unlock() 710 711 s.simulateConflict("consumer") 712 713 action := &client.InterfaceAction{ 714 Action: "disconnect", 715 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 716 Slots: []client.Slot{{Snap: "producer", Name: "slot"}}, 717 } 718 text, err := json.Marshal(action) 719 c.Assert(err, check.IsNil) 720 buf := bytes.NewBuffer(text) 721 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 722 c.Assert(err, check.IsNil) 723 rec := httptest.NewRecorder() 724 s.req(c, req, nil).ServeHTTP(rec, req) 725 726 c.Check(rec.Code, check.Equals, 409) 727 728 var body map[string]interface{} 729 err = json.Unmarshal(rec.Body.Bytes(), &body) 730 c.Check(err, check.IsNil) 731 c.Check(body, check.DeepEquals, map[string]interface{}{ 732 "status-code": 409., 733 "status": "Conflict", 734 "result": map[string]interface{}{ 735 "message": `snap "consumer" has "manip" change in progress`, 736 "kind": "snap-change-conflict", 737 "value": map[string]interface{}{ 738 "change-kind": "manip", 739 "snap-name": "consumer", 740 }, 741 }, 742 "type": "error"}) 743 } 744 745 func (s *interfacesSuite) TestDisconnectCoreSystemAlias(c *check.C) { 746 revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 747 defer revert() 748 d := s.daemon(c) 749 750 s.mockSnap(c, consumerYaml) 751 s.mockSnap(c, coreProducerYaml) 752 753 repo := d.Overlord().InterfaceManager().Repository() 754 connRef := &interfaces.ConnRef{ 755 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 756 SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}, 757 } 758 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 759 c.Assert(err, check.IsNil) 760 761 st := d.Overlord().State() 762 st.Lock() 763 st.Set("conns", map[string]interface{}{ 764 "consumer:plug core:slot": map[string]interface{}{ 765 "interface": "test", 766 }, 767 }) 768 st.Unlock() 769 770 d.Overlord().Loop() 771 defer d.Overlord().Stop() 772 773 action := &client.InterfaceAction{ 774 Action: "disconnect", 775 Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}}, 776 Slots: []client.Slot{{Snap: "system", Name: "slot"}}, 777 } 778 text, err := json.Marshal(action) 779 c.Assert(err, check.IsNil) 780 buf := bytes.NewBuffer(text) 781 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 782 c.Assert(err, check.IsNil) 783 rec := httptest.NewRecorder() 784 s.req(c, req, nil).ServeHTTP(rec, req) 785 c.Check(rec.Code, check.Equals, 202) 786 var body map[string]interface{} 787 err = json.Unmarshal(rec.Body.Bytes(), &body) 788 c.Check(err, check.IsNil) 789 id := body["change"].(string) 790 791 st.Lock() 792 chg := st.Change(id) 793 st.Unlock() 794 c.Assert(chg, check.NotNil) 795 796 <-chg.Ready() 797 798 st.Lock() 799 err = chg.Err() 800 st.Unlock() 801 c.Assert(err, check.IsNil) 802 803 ifaces := repo.Interfaces() 804 c.Assert(ifaces.Connections, check.HasLen, 0) 805 } 806 807 func (s *interfacesSuite) TestUnsupportedInterfaceRequest(c *check.C) { 808 s.daemon(c) 809 buf := bytes.NewBuffer([]byte(`garbage`)) 810 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 811 c.Assert(err, check.IsNil) 812 rec := httptest.NewRecorder() 813 s.req(c, req, nil).ServeHTTP(rec, req) 814 c.Check(rec.Code, check.Equals, 400) 815 var body map[string]interface{} 816 err = json.Unmarshal(rec.Body.Bytes(), &body) 817 c.Check(err, check.IsNil) 818 c.Check(body, check.DeepEquals, map[string]interface{}{ 819 "result": map[string]interface{}{ 820 "message": "cannot decode request body into an interface action: invalid character 'g' looking for beginning of value", 821 }, 822 "status": "Bad Request", 823 "status-code": 400.0, 824 "type": "error", 825 }) 826 } 827 828 func (s *interfacesSuite) TestMissingInterfaceAction(c *check.C) { 829 s.daemon(c) 830 action := &client.InterfaceAction{} 831 text, err := json.Marshal(action) 832 c.Assert(err, check.IsNil) 833 buf := bytes.NewBuffer(text) 834 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 835 c.Assert(err, check.IsNil) 836 rec := httptest.NewRecorder() 837 s.req(c, req, nil).ServeHTTP(rec, req) 838 c.Check(rec.Code, check.Equals, 400) 839 var body map[string]interface{} 840 err = json.Unmarshal(rec.Body.Bytes(), &body) 841 c.Check(err, check.IsNil) 842 c.Check(body, check.DeepEquals, map[string]interface{}{ 843 "result": map[string]interface{}{ 844 "message": "interface action not specified", 845 }, 846 "status": "Bad Request", 847 "status-code": 400.0, 848 "type": "error", 849 }) 850 } 851 852 func (s *interfacesSuite) TestUnsupportedInterfaceAction(c *check.C) { 853 s.daemon(c) 854 action := &client.InterfaceAction{Action: "foo"} 855 text, err := json.Marshal(action) 856 c.Assert(err, check.IsNil) 857 buf := bytes.NewBuffer(text) 858 req, err := http.NewRequest("POST", "/v2/interfaces", buf) 859 c.Assert(err, check.IsNil) 860 rec := httptest.NewRecorder() 861 s.req(c, req, nil).ServeHTTP(rec, req) 862 c.Check(rec.Code, check.Equals, 400) 863 var body map[string]interface{} 864 err = json.Unmarshal(rec.Body.Bytes(), &body) 865 c.Check(err, check.IsNil) 866 c.Check(body, check.DeepEquals, map[string]interface{}{ 867 "result": map[string]interface{}{ 868 "message": "unsupported interface action: \"foo\"", 869 }, 870 "status": "Bad Request", 871 "status-code": 400.0, 872 "type": "error", 873 }) 874 } 875 876 // Tests for GET /v2/interfaces 877 878 func (s *interfacesSuite) TestInterfacesLegacy(c *check.C) { 879 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 880 defer restore() 881 // Install an inverse case mapper to exercise the interface mapping at the same time. 882 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 883 defer restore() 884 885 d := s.daemon(c) 886 887 var anotherConsumerYaml = ` 888 name: another-consumer-%s 889 version: 1 890 apps: 891 app: 892 plugs: 893 plug: 894 interface: test 895 key: value 896 label: label 897 ` 898 s.mockSnap(c, consumerYaml) 899 s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "def")) 900 s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "abc")) 901 s.mockSnap(c, producerYaml) 902 903 repo := d.Overlord().InterfaceManager().Repository() 904 connRef := &interfaces.ConnRef{ 905 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 906 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 907 } 908 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 909 c.Assert(err, check.IsNil) 910 911 st := d.Overlord().State() 912 st.Lock() 913 st.Set("conns", map[string]interface{}{ 914 "consumer:plug producer:slot": map[string]interface{}{ 915 "interface": "test", 916 "auto": true, 917 }, 918 "another-consumer-def:plug producer:slot": map[string]interface{}{ 919 "interface": "test", 920 "by-gadget": true, 921 "auto": true, 922 }, 923 "another-consumer-abc:plug producer:slot": map[string]interface{}{ 924 "interface": "test", 925 "by-gadget": true, 926 "auto": true, 927 }, 928 }) 929 st.Unlock() 930 931 req, err := http.NewRequest("GET", "/v2/interfaces", nil) 932 c.Assert(err, check.IsNil) 933 rec := httptest.NewRecorder() 934 s.req(c, req, nil).ServeHTTP(rec, req) 935 c.Check(rec.Code, check.Equals, 200) 936 var body map[string]interface{} 937 err = json.Unmarshal(rec.Body.Bytes(), &body) 938 c.Check(err, check.IsNil) 939 c.Check(body, check.DeepEquals, map[string]interface{}{ 940 "result": map[string]interface{}{ 941 "plugs": []interface{}{ 942 map[string]interface{}{ 943 "snap": "another-consumer-abc", 944 "plug": "plug", 945 "interface": "test", 946 "attrs": map[string]interface{}{"key": "value"}, 947 "apps": []interface{}{"app"}, 948 "label": "label", 949 "connections": []interface{}{ 950 map[string]interface{}{"snap": "producer", "slot": "slot"}, 951 }, 952 }, 953 map[string]interface{}{ 954 "snap": "another-consumer-def", 955 "plug": "plug", 956 "interface": "test", 957 "attrs": map[string]interface{}{"key": "value"}, 958 "apps": []interface{}{"app"}, 959 "label": "label", 960 "connections": []interface{}{ 961 map[string]interface{}{"snap": "producer", "slot": "slot"}, 962 }, 963 }, 964 map[string]interface{}{ 965 "snap": "consumer", 966 "plug": "plug", 967 "interface": "test", 968 "attrs": map[string]interface{}{"key": "value"}, 969 "apps": []interface{}{"app"}, 970 "label": "label", 971 "connections": []interface{}{ 972 map[string]interface{}{"snap": "producer", "slot": "slot"}, 973 }, 974 }, 975 }, 976 "slots": []interface{}{ 977 map[string]interface{}{ 978 "snap": "producer", 979 "slot": "slot", 980 "interface": "test", 981 "attrs": map[string]interface{}{"key": "value"}, 982 "apps": []interface{}{"app"}, 983 "label": "label", 984 "connections": []interface{}{ 985 map[string]interface{}{"snap": "another-consumer-abc", "plug": "plug"}, 986 map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"}, 987 map[string]interface{}{"snap": "consumer", "plug": "plug"}, 988 }, 989 }, 990 }, 991 }, 992 "status": "OK", 993 "status-code": 200.0, 994 "type": "sync", 995 }) 996 } 997 998 func (s *interfacesSuite) TestInterfacesModern(c *check.C) { 999 restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"}) 1000 defer restore() 1001 // Install an inverse case mapper to exercise the interface mapping at the same time. 1002 restore = ifacestate.MockSnapMapper(&inverseCaseMapper{}) 1003 defer restore() 1004 1005 d := s.daemon(c) 1006 1007 s.mockSnap(c, consumerYaml) 1008 s.mockSnap(c, producerYaml) 1009 1010 repo := d.Overlord().InterfaceManager().Repository() 1011 connRef := &interfaces.ConnRef{ 1012 PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, 1013 SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, 1014 } 1015 _, err := repo.Connect(connRef, nil, nil, nil, nil, nil) 1016 c.Assert(err, check.IsNil) 1017 1018 req, err := http.NewRequest("GET", "/v2/interfaces?select=connected&doc=true&plugs=true&slots=true", nil) 1019 c.Assert(err, check.IsNil) 1020 rec := httptest.NewRecorder() 1021 s.req(c, req, nil).ServeHTTP(rec, req) 1022 c.Check(rec.Code, check.Equals, 200) 1023 var body map[string]interface{} 1024 err = json.Unmarshal(rec.Body.Bytes(), &body) 1025 c.Check(err, check.IsNil) 1026 c.Check(body, check.DeepEquals, map[string]interface{}{ 1027 "result": []interface{}{ 1028 map[string]interface{}{ 1029 "name": "test", 1030 "plugs": []interface{}{ 1031 map[string]interface{}{ 1032 "snap": "consumer", 1033 "plug": "plug", 1034 "label": "label", 1035 "attrs": map[string]interface{}{ 1036 "key": "value", 1037 }, 1038 }}, 1039 "slots": []interface{}{ 1040 map[string]interface{}{ 1041 "snap": "producer", 1042 "slot": "slot", 1043 "label": "label", 1044 "attrs": map[string]interface{}{ 1045 "key": "value", 1046 }, 1047 }, 1048 }, 1049 }, 1050 }, 1051 "status": "OK", 1052 "status-code": 200.0, 1053 "type": "sync", 1054 }) 1055 }