Files
snapd/daemon/api_interfaces_test.go
Michael Vogt 277eb06412 many: replace use of "sanity" with more inclusive naming
This commit replaces the use of "sanity" with more inclusive
naming.

When `sanity` is used in a more general sense either `validity`
or `quick` is used.
2022-03-14 18:22:34 +01:00

1188 lines
34 KiB
Go

// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-2021 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package daemon_test
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"gopkg.in/check.v1"
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/daemon"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/builtin"
"github.com/snapcore/snapd/interfaces/ifacetest"
"github.com/snapcore/snapd/overlord/ifacestate"
"github.com/snapcore/snapd/overlord/state"
)
var _ = check.Suite(&interfacesSuite{})
type interfacesSuite struct {
apiBaseSuite
}
func (s *interfacesSuite) SetUpTest(c *check.C) {
s.apiBaseSuite.SetUpTest(c)
s.expectWriteAccess(daemon.AuthenticatedAccess{Polkit: "io.snapcraft.snapd.manage-interfaces"})
}
func mockIface(c *check.C, d *daemon.Daemon, iface interfaces.Interface) {
err := d.Overlord().InterfaceManager().Repository().AddInterface(iface)
c.Assert(err, check.IsNil)
}
// inverseCaseMapper implements SnapMapper to use lower case internally and upper case externally.
type inverseCaseMapper struct {
ifacestate.IdentityMapper // Embed the identity mapper to reuse empty state mapping functions.
}
func (m *inverseCaseMapper) RemapSnapFromRequest(snapName string) string {
return strings.ToLower(snapName)
}
func (m *inverseCaseMapper) RemapSnapToResponse(snapName string) string {
return strings.ToUpper(snapName)
}
func (m *inverseCaseMapper) SystemSnapName() string {
return "core"
}
// Tests for POST /v2/interfaces
const (
consumerYaml = `
name: consumer
version: 1
apps:
app:
plugs:
plug:
interface: test
key: value
label: label
`
producerYaml = `
name: producer
version: 1
apps:
app:
slots:
slot:
interface: test
key: value
label: label
`
coreProducerYaml = `
name: core
version: 1
slots:
slot:
interface: test
key: value
label: label
`
differentProducerYaml = `
name: producer
version: 1
apps:
app:
slots:
slot:
interface: different
key: value
label: label
`
)
func (s *interfacesSuite) TestConnectPlugSuccess(c *check.C) {
restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer restore()
// Install an inverse case mapper to exercise the interface mapping at the same time.
restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
defer restore()
d := s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
d.Overlord().Loop()
defer d.Overlord().Stop()
action := &client.InterfaceAction{
Action: "connect",
Plugs: []client.Plug{{Snap: "CONSUMER", Name: "plug"}},
Slots: []client.Slot{{Snap: "PRODUCER", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 202)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
id := body["change"].(string)
st := d.Overlord().State()
st.Lock()
chg := st.Change(id)
st.Unlock()
c.Assert(chg, check.NotNil)
<-chg.Ready()
st.Lock()
err = chg.Err()
st.Unlock()
c.Assert(err, check.IsNil)
repo := d.Overlord().InterfaceManager().Repository()
ifaces := repo.Interfaces()
c.Assert(ifaces.Connections, check.HasLen, 1)
c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{
PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
}})
}
func (s *interfacesSuite) TestConnectPlugFailureInterfaceMismatch(c *check.C) {
d := s.daemon(c)
mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "different"})
s.mockSnap(c, consumerYaml)
s.mockSnap(c, differentProducerYaml)
action := &client.InterfaceAction{
Action: "connect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "producer", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "cannot connect consumer:plug (\"test\" interface) to producer:slot (\"different\" interface)",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
repo := d.Overlord().InterfaceManager().Repository()
ifaces := repo.Interfaces()
c.Assert(ifaces.Connections, check.HasLen, 0)
}
func (s *interfacesSuite) TestConnectPlugFailureNoSuchPlug(c *check.C) {
d := s.daemon(c)
mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
// there is no consumer, no plug defined
s.mockSnap(c, producerYaml)
s.mockSnap(c, consumerYaml)
action := &client.InterfaceAction{
Action: "connect",
Plugs: []client.Plug{{Snap: "consumer", Name: "missingplug"}},
Slots: []client.Slot{{Snap: "producer", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "snap \"consumer\" has no plug named \"missingplug\"",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
repo := d.Overlord().InterfaceManager().Repository()
ifaces := repo.Interfaces()
c.Assert(ifaces.Connections, check.HasLen, 0)
}
func (s *interfacesSuite) TestConnectAlreadyConnected(c *check.C) {
d := s.daemon(c)
mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
// there is no consumer, no plug defined
s.mockSnap(c, producerYaml)
s.mockSnap(c, consumerYaml)
repo := d.Overlord().InterfaceManager().Repository()
connRef := &interfaces.ConnRef{
PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
}
d.Overlord().Loop()
defer d.Overlord().Stop()
_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
c.Assert(err, check.IsNil)
conns := map[string]interface{}{
"consumer:plug producer:slot": map[string]interface{}{
"auto": false,
},
}
st := d.Overlord().State()
st.Lock()
st.Set("conns", conns)
st.Unlock()
action := &client.InterfaceAction{
Action: "connect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "producer", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 202)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
id := body["change"].(string)
st.Lock()
chg := st.Change(id)
c.Assert(chg.Tasks(), check.HasLen, 0)
c.Assert(chg.Status(), check.Equals, state.DoneStatus)
st.Unlock()
}
func (s *interfacesSuite) TestConnectPlugFailureNoSuchSlot(c *check.C) {
d := s.daemon(c)
mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
// there is no producer, no slot defined
action := &client.InterfaceAction{
Action: "connect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "producer", Name: "missingslot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "snap \"producer\" has no slot named \"missingslot\"",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
repo := d.Overlord().InterfaceManager().Repository()
ifaces := repo.Interfaces()
c.Assert(ifaces.Connections, check.HasLen, 0)
}
func (s *interfacesSuite) testConnectFailureNoSnap(c *check.C, installedSnap string) {
// validity, either consumer or producer needs to be enabled
consumer := installedSnap == "consumer"
producer := installedSnap == "producer"
c.Assert(consumer || producer, check.Equals, true, check.Commentf("installed snap must be consumer or producer"))
d := s.daemon(c)
mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
if consumer {
s.mockSnap(c, consumerYaml)
}
if producer {
s.mockSnap(c, producerYaml)
}
action := &client.InterfaceAction{
Action: "connect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "producer", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
if producer {
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "snap \"consumer\" is not installed",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
} else {
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "snap \"producer\" is not installed",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
}
}
func (s *interfacesSuite) TestConnectPlugFailureNoPlugSnap(c *check.C) {
s.testConnectFailureNoSnap(c, "producer")
}
func (s *interfacesSuite) TestConnectPlugFailureNoSlotSnap(c *check.C) {
s.testConnectFailureNoSnap(c, "consumer")
}
func (s *interfacesSuite) TestConnectPlugChangeConflict(c *check.C) {
d := s.daemon(c)
mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
// there is no producer, no slot defined
s.simulateConflict("consumer")
action := &client.InterfaceAction{
Action: "connect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "producer", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 409)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"status-code": 409.,
"status": "Conflict",
"result": map[string]interface{}{
"message": `snap "consumer" has "manip" change in progress`,
"kind": "snap-change-conflict",
"value": map[string]interface{}{
"change-kind": "manip",
"snap-name": "consumer",
},
},
"type": "error"})
}
func (s *interfacesSuite) TestConnectCoreSystemAlias(c *check.C) {
revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer revert()
d := s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, coreProducerYaml)
d.Overlord().Loop()
defer d.Overlord().Stop()
action := &client.InterfaceAction{
Action: "connect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "system", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 202)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
id := body["change"].(string)
st := d.Overlord().State()
st.Lock()
chg := st.Change(id)
st.Unlock()
c.Assert(chg, check.NotNil)
<-chg.Ready()
st.Lock()
err = chg.Err()
st.Unlock()
c.Assert(err, check.IsNil)
repo := d.Overlord().InterfaceManager().Repository()
ifaces := repo.Interfaces()
c.Assert(ifaces.Connections, check.HasLen, 1)
c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{
PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}}})
}
func (s *interfacesSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slotName string) {
restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer restore()
// Install an inverse case mapper to exercise the interface mapping at the same time.
restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
defer restore()
d := s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
repo := d.Overlord().InterfaceManager().Repository()
connRef := &interfaces.ConnRef{
PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
}
_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
c.Assert(err, check.IsNil)
st := d.Overlord().State()
st.Lock()
st.Set("conns", map[string]interface{}{
"consumer:plug producer:slot": map[string]interface{}{
"interface": "test",
},
})
st.Unlock()
d.Overlord().Loop()
defer d.Overlord().Stop()
action := &client.InterfaceAction{
Action: "disconnect",
Plugs: []client.Plug{{Snap: plugSnap, Name: plugName}},
Slots: []client.Slot{{Snap: slotSnap, Name: slotName}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 202)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
id := body["change"].(string)
st.Lock()
chg := st.Change(id)
st.Unlock()
c.Assert(chg, check.NotNil)
<-chg.Ready()
st.Lock()
err = chg.Err()
st.Unlock()
c.Assert(err, check.IsNil)
ifaces := repo.Interfaces()
c.Assert(ifaces.Connections, check.HasLen, 0)
}
func (s *interfacesSuite) TestDisconnectPlugSuccess(c *check.C) {
s.testDisconnect(c, "CONSUMER", "plug", "PRODUCER", "slot")
}
func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptyPlug(c *check.C) {
s.testDisconnect(c, "", "", "PRODUCER", "slot")
}
func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptySlot(c *check.C) {
s.testDisconnect(c, "CONSUMER", "plug", "", "")
}
func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) {
revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer revert()
s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
action := &client.InterfaceAction{
Action: "disconnect",
Plugs: []client.Plug{{Snap: "consumer", Name: "missingplug"}},
Slots: []client.Slot{{Snap: "producer", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "snap \"consumer\" has no plug named \"missingplug\"",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
}
func (s *interfacesSuite) testDisconnectFailureNoSnap(c *check.C, installedSnap string) {
// validity, either consumer or producer needs to be enabled
consumer := installedSnap == "consumer"
producer := installedSnap == "producer"
c.Assert(consumer || producer, check.Equals, true, check.Commentf("installed snap must be consumer or producer"))
revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer revert()
s.daemon(c)
if consumer {
s.mockSnap(c, consumerYaml)
}
if producer {
s.mockSnap(c, producerYaml)
}
action := &client.InterfaceAction{
Action: "disconnect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "producer", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
if producer {
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "snap \"consumer\" is not installed",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
} else {
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "snap \"producer\" is not installed",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
}
}
func (s *interfacesSuite) TestDisconnectPlugFailureNoPlugSnap(c *check.C) {
s.testDisconnectFailureNoSnap(c, "producer")
}
func (s *interfacesSuite) TestDisconnectPlugFailureNoSlotSnap(c *check.C) {
s.testDisconnectFailureNoSnap(c, "consumer")
}
func (s *interfacesSuite) TestDisconnectPlugNothingToDo(c *check.C) {
revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer revert()
s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
action := &client.InterfaceAction{
Action: "disconnect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "", Name: ""}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "nothing to do",
"kind": "interfaces-unchanged",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
}
func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) {
revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer revert()
s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
action := &client.InterfaceAction{
Action: "disconnect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "producer", Name: "missingslot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "snap \"producer\" has no slot named \"missingslot\"",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
}
func (s *interfacesSuite) TestDisconnectPlugFailureNotConnected(c *check.C) {
revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer revert()
s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
action := &client.InterfaceAction{
Action: "disconnect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "producer", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "cannot disconnect consumer:plug from producer:slot, it is not connected",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
}
func (s *interfacesSuite) TestDisconnectForgetPlugFailureNotConnected(c *check.C) {
revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer revert()
s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
action := &client.InterfaceAction{
Action: "disconnect",
Forget: true,
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "producer", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "cannot forget connection consumer:plug from producer:slot, it was not connected",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
}
func (s *interfacesSuite) TestDisconnectConflict(c *check.C) {
revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer revert()
d := s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
repo := d.Overlord().InterfaceManager().Repository()
connRef := &interfaces.ConnRef{
PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
}
_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
c.Assert(err, check.IsNil)
st := d.Overlord().State()
st.Lock()
st.Set("conns", map[string]interface{}{
"consumer:plug producer:slot": map[string]interface{}{
"interface": "test",
},
})
st.Unlock()
s.simulateConflict("consumer")
action := &client.InterfaceAction{
Action: "disconnect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "producer", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 409)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"status-code": 409.,
"status": "Conflict",
"result": map[string]interface{}{
"message": `snap "consumer" has "manip" change in progress`,
"kind": "snap-change-conflict",
"value": map[string]interface{}{
"change-kind": "manip",
"snap-name": "consumer",
},
},
"type": "error"})
}
func (s *interfacesSuite) TestDisconnectCoreSystemAlias(c *check.C) {
revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer revert()
d := s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, coreProducerYaml)
repo := d.Overlord().InterfaceManager().Repository()
connRef := &interfaces.ConnRef{
PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"},
}
_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
c.Assert(err, check.IsNil)
st := d.Overlord().State()
st.Lock()
st.Set("conns", map[string]interface{}{
"consumer:plug core:slot": map[string]interface{}{
"interface": "test",
},
})
st.Unlock()
d.Overlord().Loop()
defer d.Overlord().Stop()
action := &client.InterfaceAction{
Action: "disconnect",
Plugs: []client.Plug{{Snap: "consumer", Name: "plug"}},
Slots: []client.Slot{{Snap: "system", Name: "slot"}},
}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 202)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
id := body["change"].(string)
st.Lock()
chg := st.Change(id)
st.Unlock()
c.Assert(chg, check.NotNil)
<-chg.Ready()
st.Lock()
err = chg.Err()
st.Unlock()
c.Assert(err, check.IsNil)
ifaces := repo.Interfaces()
c.Assert(ifaces.Connections, check.HasLen, 0)
}
func (s *interfacesSuite) TestUnsupportedInterfaceRequest(c *check.C) {
s.daemon(c)
buf := bytes.NewBuffer([]byte(`garbage`))
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "cannot decode request body into an interface action: invalid character 'g' looking for beginning of value",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
}
func (s *interfacesSuite) TestMissingInterfaceAction(c *check.C) {
s.daemon(c)
action := &client.InterfaceAction{}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "interface action not specified",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
}
func (s *interfacesSuite) TestUnsupportedInterfaceAction(c *check.C) {
s.daemon(c)
action := &client.InterfaceAction{Action: "foo"}
text, err := json.Marshal(action)
c.Assert(err, check.IsNil)
buf := bytes.NewBuffer(text)
req, err := http.NewRequest("POST", "/v2/interfaces", buf)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 400)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"message": "unsupported interface action: \"foo\"",
},
"status": "Bad Request",
"status-code": 400.0,
"type": "error",
})
}
// Tests for GET /v2/interfaces
func (s *interfacesSuite) TestInterfacesLegacy(c *check.C) {
restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer restore()
// Install an inverse case mapper to exercise the interface mapping at the same time.
restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
defer restore()
d := s.daemon(c)
var anotherConsumerYaml = `
name: another-consumer-%s
version: 1
apps:
app:
plugs:
plug:
interface: test
key: value
label: label
`
s.mockSnap(c, consumerYaml)
s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "def"))
s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "abc"))
s.mockSnap(c, producerYaml)
repo := d.Overlord().InterfaceManager().Repository()
connRef := &interfaces.ConnRef{
PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
}
_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
c.Assert(err, check.IsNil)
st := d.Overlord().State()
st.Lock()
st.Set("conns", map[string]interface{}{
"consumer:plug producer:slot": map[string]interface{}{
"interface": "test",
"auto": true,
},
"another-consumer-def:plug producer:slot": map[string]interface{}{
"interface": "test",
"by-gadget": true,
"auto": true,
},
"another-consumer-abc:plug producer:slot": map[string]interface{}{
"interface": "test",
"by-gadget": true,
"auto": true,
},
})
st.Unlock()
req, err := http.NewRequest("GET", "/v2/interfaces", nil)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 200)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": map[string]interface{}{
"plugs": []interface{}{
map[string]interface{}{
"snap": "another-consumer-abc",
"plug": "plug",
"interface": "test",
"attrs": map[string]interface{}{"key": "value"},
"apps": []interface{}{"app"},
"label": "label",
"connections": []interface{}{
map[string]interface{}{"snap": "producer", "slot": "slot"},
},
},
map[string]interface{}{
"snap": "another-consumer-def",
"plug": "plug",
"interface": "test",
"attrs": map[string]interface{}{"key": "value"},
"apps": []interface{}{"app"},
"label": "label",
"connections": []interface{}{
map[string]interface{}{"snap": "producer", "slot": "slot"},
},
},
map[string]interface{}{
"snap": "consumer",
"plug": "plug",
"interface": "test",
"attrs": map[string]interface{}{"key": "value"},
"apps": []interface{}{"app"},
"label": "label",
"connections": []interface{}{
map[string]interface{}{"snap": "producer", "slot": "slot"},
},
},
},
"slots": []interface{}{
map[string]interface{}{
"snap": "producer",
"slot": "slot",
"interface": "test",
"attrs": map[string]interface{}{"key": "value"},
"apps": []interface{}{"app"},
"label": "label",
"connections": []interface{}{
map[string]interface{}{"snap": "another-consumer-abc", "plug": "plug"},
map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"},
map[string]interface{}{"snap": "consumer", "plug": "plug"},
},
},
},
},
"status": "OK",
"status-code": 200.0,
"type": "sync",
})
}
func (s *interfacesSuite) TestInterfacesModern(c *check.C) {
restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
defer restore()
// Install an inverse case mapper to exercise the interface mapping at the same time.
restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
defer restore()
d := s.daemon(c)
s.mockSnap(c, consumerYaml)
s.mockSnap(c, producerYaml)
repo := d.Overlord().InterfaceManager().Repository()
connRef := &interfaces.ConnRef{
PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
}
_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
c.Assert(err, check.IsNil)
req, err := http.NewRequest("GET", "/v2/interfaces?select=connected&doc=true&plugs=true&slots=true", nil)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
s.req(c, req, nil).ServeHTTP(rec, req)
c.Check(rec.Code, check.Equals, 200)
var body map[string]interface{}
err = json.Unmarshal(rec.Body.Bytes(), &body)
c.Check(err, check.IsNil)
c.Check(body, check.DeepEquals, map[string]interface{}{
"result": []interface{}{
map[string]interface{}{
"name": "test",
"plugs": []interface{}{
map[string]interface{}{
"snap": "consumer",
"plug": "plug",
"label": "label",
"attrs": map[string]interface{}{
"key": "value",
},
}},
"slots": []interface{}{
map[string]interface{}{
"snap": "producer",
"slot": "slot",
"label": "label",
"attrs": map[string]interface{}{
"key": "value",
},
},
},
},
},
"status": "OK",
"status-code": 200.0,
"type": "sync",
})
}