github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/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) testConnectFailureNoSnap(c *check.C, installedSnap string) {
   344  	// sanity, either consumer or producer needs to be enabled
   345  	consumer := installedSnap == "consumer"
   346  	producer := installedSnap == "producer"
   347  	c.Assert(consumer || producer, check.Equals, true, check.Commentf("installed snap must be consumer or producer"))
   348  
   349  	d := s.daemon(c)
   350  
   351  	mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
   352  
   353  	if consumer {
   354  		s.mockSnap(c, consumerYaml)
   355  	}
   356  	if producer {
   357  		s.mockSnap(c, producerYaml)
   358  	}
   359  
   360  	action := &client.InterfaceAction{
   361  		Action: "connect",
   362  		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
   363  		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
   364  	}
   365  	text, err := json.Marshal(action)
   366  	c.Assert(err, check.IsNil)
   367  	buf := bytes.NewBuffer(text)
   368  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   369  	c.Assert(err, check.IsNil)
   370  	rec := httptest.NewRecorder()
   371  	s.req(c, req, nil).ServeHTTP(rec, req)
   372  	c.Check(rec.Code, check.Equals, 400)
   373  
   374  	var body map[string]interface{}
   375  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   376  	c.Check(err, check.IsNil)
   377  	if producer {
   378  		c.Check(body, check.DeepEquals, map[string]interface{}{
   379  			"result": map[string]interface{}{
   380  				"message": "snap \"consumer\" is not installed",
   381  			},
   382  			"status":      "Bad Request",
   383  			"status-code": 400.0,
   384  			"type":        "error",
   385  		})
   386  	} else {
   387  		c.Check(body, check.DeepEquals, map[string]interface{}{
   388  			"result": map[string]interface{}{
   389  				"message": "snap \"producer\" is not installed",
   390  			},
   391  			"status":      "Bad Request",
   392  			"status-code": 400.0,
   393  			"type":        "error",
   394  		})
   395  	}
   396  }
   397  
   398  func (s *interfacesSuite) TestConnectPlugFailureNoPlugSnap(c *check.C) {
   399  	s.testConnectFailureNoSnap(c, "producer")
   400  }
   401  
   402  func (s *interfacesSuite) TestConnectPlugFailureNoSlotSnap(c *check.C) {
   403  	s.testConnectFailureNoSnap(c, "consumer")
   404  }
   405  
   406  func (s *interfacesSuite) TestConnectPlugChangeConflict(c *check.C) {
   407  	d := s.daemon(c)
   408  
   409  	mockIface(c, d, &ifacetest.TestInterface{InterfaceName: "test"})
   410  	s.mockSnap(c, consumerYaml)
   411  	s.mockSnap(c, producerYaml)
   412  	// there is no producer, no slot defined
   413  
   414  	s.simulateConflict("consumer")
   415  
   416  	action := &client.InterfaceAction{
   417  		Action: "connect",
   418  		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
   419  		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
   420  	}
   421  	text, err := json.Marshal(action)
   422  	c.Assert(err, check.IsNil)
   423  	buf := bytes.NewBuffer(text)
   424  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   425  	c.Assert(err, check.IsNil)
   426  	rec := httptest.NewRecorder()
   427  	s.req(c, req, nil).ServeHTTP(rec, req)
   428  	c.Check(rec.Code, check.Equals, 409)
   429  
   430  	var body map[string]interface{}
   431  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   432  	c.Check(err, check.IsNil)
   433  	c.Check(body, check.DeepEquals, map[string]interface{}{
   434  		"status-code": 409.,
   435  		"status":      "Conflict",
   436  		"result": map[string]interface{}{
   437  			"message": `snap "consumer" has "manip" change in progress`,
   438  			"kind":    "snap-change-conflict",
   439  			"value": map[string]interface{}{
   440  				"change-kind": "manip",
   441  				"snap-name":   "consumer",
   442  			},
   443  		},
   444  		"type": "error"})
   445  }
   446  
   447  func (s *interfacesSuite) TestConnectCoreSystemAlias(c *check.C) {
   448  	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
   449  	defer revert()
   450  	d := s.daemon(c)
   451  
   452  	s.mockSnap(c, consumerYaml)
   453  	s.mockSnap(c, coreProducerYaml)
   454  
   455  	d.Overlord().Loop()
   456  	defer d.Overlord().Stop()
   457  
   458  	action := &client.InterfaceAction{
   459  		Action: "connect",
   460  		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
   461  		Slots:  []client.Slot{{Snap: "system", Name: "slot"}},
   462  	}
   463  	text, err := json.Marshal(action)
   464  	c.Assert(err, check.IsNil)
   465  	buf := bytes.NewBuffer(text)
   466  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   467  	c.Assert(err, check.IsNil)
   468  	rec := httptest.NewRecorder()
   469  	s.req(c, req, nil).ServeHTTP(rec, req)
   470  	c.Check(rec.Code, check.Equals, 202)
   471  	var body map[string]interface{}
   472  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   473  	c.Check(err, check.IsNil)
   474  	id := body["change"].(string)
   475  
   476  	st := d.Overlord().State()
   477  	st.Lock()
   478  	chg := st.Change(id)
   479  	st.Unlock()
   480  	c.Assert(chg, check.NotNil)
   481  
   482  	<-chg.Ready()
   483  
   484  	st.Lock()
   485  	err = chg.Err()
   486  	st.Unlock()
   487  	c.Assert(err, check.IsNil)
   488  
   489  	repo := d.Overlord().InterfaceManager().Repository()
   490  	ifaces := repo.Interfaces()
   491  	c.Assert(ifaces.Connections, check.HasLen, 1)
   492  	c.Check(ifaces.Connections, check.DeepEquals, []*interfaces.ConnRef{{
   493  		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
   494  		SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"}}})
   495  }
   496  
   497  func (s *interfacesSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slotName string) {
   498  	restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
   499  	defer restore()
   500  	// Install an inverse case mapper to exercise the interface mapping at the same time.
   501  	restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
   502  	defer restore()
   503  	d := s.daemon(c)
   504  
   505  	s.mockSnap(c, consumerYaml)
   506  	s.mockSnap(c, producerYaml)
   507  
   508  	repo := d.Overlord().InterfaceManager().Repository()
   509  	connRef := &interfaces.ConnRef{
   510  		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
   511  		SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
   512  	}
   513  	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
   514  	c.Assert(err, check.IsNil)
   515  
   516  	st := d.Overlord().State()
   517  	st.Lock()
   518  	st.Set("conns", map[string]interface{}{
   519  		"consumer:plug producer:slot": map[string]interface{}{
   520  			"interface": "test",
   521  		},
   522  	})
   523  	st.Unlock()
   524  
   525  	d.Overlord().Loop()
   526  	defer d.Overlord().Stop()
   527  
   528  	action := &client.InterfaceAction{
   529  		Action: "disconnect",
   530  		Plugs:  []client.Plug{{Snap: plugSnap, Name: plugName}},
   531  		Slots:  []client.Slot{{Snap: slotSnap, Name: slotName}},
   532  	}
   533  	text, err := json.Marshal(action)
   534  	c.Assert(err, check.IsNil)
   535  	buf := bytes.NewBuffer(text)
   536  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   537  	c.Assert(err, check.IsNil)
   538  	rec := httptest.NewRecorder()
   539  	s.req(c, req, nil).ServeHTTP(rec, req)
   540  	c.Check(rec.Code, check.Equals, 202)
   541  	var body map[string]interface{}
   542  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   543  	c.Check(err, check.IsNil)
   544  	id := body["change"].(string)
   545  
   546  	st.Lock()
   547  	chg := st.Change(id)
   548  	st.Unlock()
   549  	c.Assert(chg, check.NotNil)
   550  
   551  	<-chg.Ready()
   552  
   553  	st.Lock()
   554  	err = chg.Err()
   555  	st.Unlock()
   556  	c.Assert(err, check.IsNil)
   557  
   558  	ifaces := repo.Interfaces()
   559  	c.Assert(ifaces.Connections, check.HasLen, 0)
   560  }
   561  
   562  func (s *interfacesSuite) TestDisconnectPlugSuccess(c *check.C) {
   563  	s.testDisconnect(c, "CONSUMER", "plug", "PRODUCER", "slot")
   564  }
   565  
   566  func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptyPlug(c *check.C) {
   567  	s.testDisconnect(c, "", "", "PRODUCER", "slot")
   568  }
   569  
   570  func (s *interfacesSuite) TestDisconnectPlugSuccessWithEmptySlot(c *check.C) {
   571  	s.testDisconnect(c, "CONSUMER", "plug", "", "")
   572  }
   573  
   574  func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchPlug(c *check.C) {
   575  	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
   576  	defer revert()
   577  	s.daemon(c)
   578  
   579  	s.mockSnap(c, consumerYaml)
   580  	s.mockSnap(c, producerYaml)
   581  
   582  	action := &client.InterfaceAction{
   583  		Action: "disconnect",
   584  		Plugs:  []client.Plug{{Snap: "consumer", Name: "missingplug"}},
   585  		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
   586  	}
   587  	text, err := json.Marshal(action)
   588  	c.Assert(err, check.IsNil)
   589  	buf := bytes.NewBuffer(text)
   590  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   591  	c.Assert(err, check.IsNil)
   592  	rec := httptest.NewRecorder()
   593  	s.req(c, req, nil).ServeHTTP(rec, req)
   594  	c.Check(rec.Code, check.Equals, 400)
   595  	var body map[string]interface{}
   596  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   597  	c.Check(err, check.IsNil)
   598  	c.Check(body, check.DeepEquals, map[string]interface{}{
   599  		"result": map[string]interface{}{
   600  			"message": "snap \"consumer\" has no plug named \"missingplug\"",
   601  		},
   602  		"status":      "Bad Request",
   603  		"status-code": 400.0,
   604  		"type":        "error",
   605  	})
   606  }
   607  
   608  func (s *interfacesSuite) testDisconnectFailureNoSnap(c *check.C, installedSnap string) {
   609  	// sanity, either consumer or producer needs to be enabled
   610  	consumer := installedSnap == "consumer"
   611  	producer := installedSnap == "producer"
   612  	c.Assert(consumer || producer, check.Equals, true, check.Commentf("installed snap must be consumer or producer"))
   613  
   614  	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
   615  	defer revert()
   616  	s.daemon(c)
   617  
   618  	if consumer {
   619  		s.mockSnap(c, consumerYaml)
   620  	}
   621  	if producer {
   622  		s.mockSnap(c, producerYaml)
   623  	}
   624  
   625  	action := &client.InterfaceAction{
   626  		Action: "disconnect",
   627  		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
   628  		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
   629  	}
   630  	text, err := json.Marshal(action)
   631  	c.Assert(err, check.IsNil)
   632  	buf := bytes.NewBuffer(text)
   633  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   634  	c.Assert(err, check.IsNil)
   635  	rec := httptest.NewRecorder()
   636  	s.req(c, req, nil).ServeHTTP(rec, req)
   637  	c.Check(rec.Code, check.Equals, 400)
   638  	var body map[string]interface{}
   639  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   640  	c.Check(err, check.IsNil)
   641  
   642  	if producer {
   643  		c.Check(body, check.DeepEquals, map[string]interface{}{
   644  			"result": map[string]interface{}{
   645  				"message": "snap \"consumer\" is not installed",
   646  			},
   647  			"status":      "Bad Request",
   648  			"status-code": 400.0,
   649  			"type":        "error",
   650  		})
   651  	} else {
   652  		c.Check(body, check.DeepEquals, map[string]interface{}{
   653  			"result": map[string]interface{}{
   654  				"message": "snap \"producer\" is not installed",
   655  			},
   656  			"status":      "Bad Request",
   657  			"status-code": 400.0,
   658  			"type":        "error",
   659  		})
   660  	}
   661  }
   662  
   663  func (s *interfacesSuite) TestDisconnectPlugFailureNoPlugSnap(c *check.C) {
   664  	s.testDisconnectFailureNoSnap(c, "producer")
   665  }
   666  
   667  func (s *interfacesSuite) TestDisconnectPlugFailureNoSlotSnap(c *check.C) {
   668  	s.testDisconnectFailureNoSnap(c, "consumer")
   669  }
   670  
   671  func (s *interfacesSuite) TestDisconnectPlugNothingToDo(c *check.C) {
   672  	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
   673  	defer revert()
   674  	s.daemon(c)
   675  
   676  	s.mockSnap(c, consumerYaml)
   677  	s.mockSnap(c, producerYaml)
   678  
   679  	action := &client.InterfaceAction{
   680  		Action: "disconnect",
   681  		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
   682  		Slots:  []client.Slot{{Snap: "", Name: ""}},
   683  	}
   684  	text, err := json.Marshal(action)
   685  	c.Assert(err, check.IsNil)
   686  	buf := bytes.NewBuffer(text)
   687  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   688  	c.Assert(err, check.IsNil)
   689  	rec := httptest.NewRecorder()
   690  	s.req(c, req, nil).ServeHTTP(rec, req)
   691  	c.Check(rec.Code, check.Equals, 400)
   692  	var body map[string]interface{}
   693  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   694  	c.Check(err, check.IsNil)
   695  	c.Check(body, check.DeepEquals, map[string]interface{}{
   696  		"result": map[string]interface{}{
   697  			"message": "nothing to do",
   698  			"kind":    "interfaces-unchanged",
   699  		},
   700  		"status":      "Bad Request",
   701  		"status-code": 400.0,
   702  		"type":        "error",
   703  	})
   704  }
   705  
   706  func (s *interfacesSuite) TestDisconnectPlugFailureNoSuchSlot(c *check.C) {
   707  	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
   708  	defer revert()
   709  	s.daemon(c)
   710  
   711  	s.mockSnap(c, consumerYaml)
   712  	s.mockSnap(c, producerYaml)
   713  
   714  	action := &client.InterfaceAction{
   715  		Action: "disconnect",
   716  		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
   717  		Slots:  []client.Slot{{Snap: "producer", Name: "missingslot"}},
   718  	}
   719  	text, err := json.Marshal(action)
   720  	c.Assert(err, check.IsNil)
   721  	buf := bytes.NewBuffer(text)
   722  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   723  	c.Assert(err, check.IsNil)
   724  	rec := httptest.NewRecorder()
   725  	s.req(c, req, nil).ServeHTTP(rec, req)
   726  
   727  	c.Check(rec.Code, check.Equals, 400)
   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  		"result": map[string]interface{}{
   733  			"message": "snap \"producer\" has no slot named \"missingslot\"",
   734  		},
   735  		"status":      "Bad Request",
   736  		"status-code": 400.0,
   737  		"type":        "error",
   738  	})
   739  }
   740  
   741  func (s *interfacesSuite) TestDisconnectPlugFailureNotConnected(c *check.C) {
   742  	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
   743  	defer revert()
   744  	s.daemon(c)
   745  
   746  	s.mockSnap(c, consumerYaml)
   747  	s.mockSnap(c, producerYaml)
   748  
   749  	action := &client.InterfaceAction{
   750  		Action: "disconnect",
   751  		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
   752  		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
   753  	}
   754  	text, err := json.Marshal(action)
   755  	c.Assert(err, check.IsNil)
   756  	buf := bytes.NewBuffer(text)
   757  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   758  	c.Assert(err, check.IsNil)
   759  	rec := httptest.NewRecorder()
   760  	s.req(c, req, nil).ServeHTTP(rec, req)
   761  
   762  	c.Check(rec.Code, check.Equals, 400)
   763  	var body map[string]interface{}
   764  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   765  	c.Check(err, check.IsNil)
   766  	c.Check(body, check.DeepEquals, map[string]interface{}{
   767  		"result": map[string]interface{}{
   768  			"message": "cannot disconnect consumer:plug from producer:slot, it is not connected",
   769  		},
   770  		"status":      "Bad Request",
   771  		"status-code": 400.0,
   772  		"type":        "error",
   773  	})
   774  }
   775  
   776  func (s *interfacesSuite) TestDisconnectForgetPlugFailureNotConnected(c *check.C) {
   777  	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
   778  	defer revert()
   779  	s.daemon(c)
   780  
   781  	s.mockSnap(c, consumerYaml)
   782  	s.mockSnap(c, producerYaml)
   783  
   784  	action := &client.InterfaceAction{
   785  		Action: "disconnect",
   786  		Forget: true,
   787  		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
   788  		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
   789  	}
   790  	text, err := json.Marshal(action)
   791  	c.Assert(err, check.IsNil)
   792  	buf := bytes.NewBuffer(text)
   793  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   794  	c.Assert(err, check.IsNil)
   795  	rec := httptest.NewRecorder()
   796  	s.req(c, req, nil).ServeHTTP(rec, req)
   797  
   798  	c.Check(rec.Code, check.Equals, 400)
   799  	var body map[string]interface{}
   800  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   801  	c.Check(err, check.IsNil)
   802  	c.Check(body, check.DeepEquals, map[string]interface{}{
   803  		"result": map[string]interface{}{
   804  			"message": "cannot forget connection consumer:plug from producer:slot, it was not connected",
   805  		},
   806  		"status":      "Bad Request",
   807  		"status-code": 400.0,
   808  		"type":        "error",
   809  	})
   810  }
   811  
   812  func (s *interfacesSuite) TestDisconnectConflict(c *check.C) {
   813  	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
   814  	defer revert()
   815  	d := s.daemon(c)
   816  
   817  	s.mockSnap(c, consumerYaml)
   818  	s.mockSnap(c, producerYaml)
   819  
   820  	repo := d.Overlord().InterfaceManager().Repository()
   821  	connRef := &interfaces.ConnRef{
   822  		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
   823  		SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
   824  	}
   825  	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
   826  	c.Assert(err, check.IsNil)
   827  
   828  	st := d.Overlord().State()
   829  	st.Lock()
   830  	st.Set("conns", map[string]interface{}{
   831  		"consumer:plug producer:slot": map[string]interface{}{
   832  			"interface": "test",
   833  		},
   834  	})
   835  	st.Unlock()
   836  
   837  	s.simulateConflict("consumer")
   838  
   839  	action := &client.InterfaceAction{
   840  		Action: "disconnect",
   841  		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
   842  		Slots:  []client.Slot{{Snap: "producer", Name: "slot"}},
   843  	}
   844  	text, err := json.Marshal(action)
   845  	c.Assert(err, check.IsNil)
   846  	buf := bytes.NewBuffer(text)
   847  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   848  	c.Assert(err, check.IsNil)
   849  	rec := httptest.NewRecorder()
   850  	s.req(c, req, nil).ServeHTTP(rec, req)
   851  
   852  	c.Check(rec.Code, check.Equals, 409)
   853  
   854  	var body map[string]interface{}
   855  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   856  	c.Check(err, check.IsNil)
   857  	c.Check(body, check.DeepEquals, map[string]interface{}{
   858  		"status-code": 409.,
   859  		"status":      "Conflict",
   860  		"result": map[string]interface{}{
   861  			"message": `snap "consumer" has "manip" change in progress`,
   862  			"kind":    "snap-change-conflict",
   863  			"value": map[string]interface{}{
   864  				"change-kind": "manip",
   865  				"snap-name":   "consumer",
   866  			},
   867  		},
   868  		"type": "error"})
   869  }
   870  
   871  func (s *interfacesSuite) TestDisconnectCoreSystemAlias(c *check.C) {
   872  	revert := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
   873  	defer revert()
   874  	d := s.daemon(c)
   875  
   876  	s.mockSnap(c, consumerYaml)
   877  	s.mockSnap(c, coreProducerYaml)
   878  
   879  	repo := d.Overlord().InterfaceManager().Repository()
   880  	connRef := &interfaces.ConnRef{
   881  		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
   882  		SlotRef: interfaces.SlotRef{Snap: "core", Name: "slot"},
   883  	}
   884  	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
   885  	c.Assert(err, check.IsNil)
   886  
   887  	st := d.Overlord().State()
   888  	st.Lock()
   889  	st.Set("conns", map[string]interface{}{
   890  		"consumer:plug core:slot": map[string]interface{}{
   891  			"interface": "test",
   892  		},
   893  	})
   894  	st.Unlock()
   895  
   896  	d.Overlord().Loop()
   897  	defer d.Overlord().Stop()
   898  
   899  	action := &client.InterfaceAction{
   900  		Action: "disconnect",
   901  		Plugs:  []client.Plug{{Snap: "consumer", Name: "plug"}},
   902  		Slots:  []client.Slot{{Snap: "system", Name: "slot"}},
   903  	}
   904  	text, err := json.Marshal(action)
   905  	c.Assert(err, check.IsNil)
   906  	buf := bytes.NewBuffer(text)
   907  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   908  	c.Assert(err, check.IsNil)
   909  	rec := httptest.NewRecorder()
   910  	s.req(c, req, nil).ServeHTTP(rec, req)
   911  	c.Check(rec.Code, check.Equals, 202)
   912  	var body map[string]interface{}
   913  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   914  	c.Check(err, check.IsNil)
   915  	id := body["change"].(string)
   916  
   917  	st.Lock()
   918  	chg := st.Change(id)
   919  	st.Unlock()
   920  	c.Assert(chg, check.NotNil)
   921  
   922  	<-chg.Ready()
   923  
   924  	st.Lock()
   925  	err = chg.Err()
   926  	st.Unlock()
   927  	c.Assert(err, check.IsNil)
   928  
   929  	ifaces := repo.Interfaces()
   930  	c.Assert(ifaces.Connections, check.HasLen, 0)
   931  }
   932  
   933  func (s *interfacesSuite) TestUnsupportedInterfaceRequest(c *check.C) {
   934  	s.daemon(c)
   935  	buf := bytes.NewBuffer([]byte(`garbage`))
   936  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   937  	c.Assert(err, check.IsNil)
   938  	rec := httptest.NewRecorder()
   939  	s.req(c, req, nil).ServeHTTP(rec, req)
   940  	c.Check(rec.Code, check.Equals, 400)
   941  	var body map[string]interface{}
   942  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   943  	c.Check(err, check.IsNil)
   944  	c.Check(body, check.DeepEquals, map[string]interface{}{
   945  		"result": map[string]interface{}{
   946  			"message": "cannot decode request body into an interface action: invalid character 'g' looking for beginning of value",
   947  		},
   948  		"status":      "Bad Request",
   949  		"status-code": 400.0,
   950  		"type":        "error",
   951  	})
   952  }
   953  
   954  func (s *interfacesSuite) TestMissingInterfaceAction(c *check.C) {
   955  	s.daemon(c)
   956  	action := &client.InterfaceAction{}
   957  	text, err := json.Marshal(action)
   958  	c.Assert(err, check.IsNil)
   959  	buf := bytes.NewBuffer(text)
   960  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   961  	c.Assert(err, check.IsNil)
   962  	rec := httptest.NewRecorder()
   963  	s.req(c, req, nil).ServeHTTP(rec, req)
   964  	c.Check(rec.Code, check.Equals, 400)
   965  	var body map[string]interface{}
   966  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   967  	c.Check(err, check.IsNil)
   968  	c.Check(body, check.DeepEquals, map[string]interface{}{
   969  		"result": map[string]interface{}{
   970  			"message": "interface action not specified",
   971  		},
   972  		"status":      "Bad Request",
   973  		"status-code": 400.0,
   974  		"type":        "error",
   975  	})
   976  }
   977  
   978  func (s *interfacesSuite) TestUnsupportedInterfaceAction(c *check.C) {
   979  	s.daemon(c)
   980  	action := &client.InterfaceAction{Action: "foo"}
   981  	text, err := json.Marshal(action)
   982  	c.Assert(err, check.IsNil)
   983  	buf := bytes.NewBuffer(text)
   984  	req, err := http.NewRequest("POST", "/v2/interfaces", buf)
   985  	c.Assert(err, check.IsNil)
   986  	rec := httptest.NewRecorder()
   987  	s.req(c, req, nil).ServeHTTP(rec, req)
   988  	c.Check(rec.Code, check.Equals, 400)
   989  	var body map[string]interface{}
   990  	err = json.Unmarshal(rec.Body.Bytes(), &body)
   991  	c.Check(err, check.IsNil)
   992  	c.Check(body, check.DeepEquals, map[string]interface{}{
   993  		"result": map[string]interface{}{
   994  			"message": "unsupported interface action: \"foo\"",
   995  		},
   996  		"status":      "Bad Request",
   997  		"status-code": 400.0,
   998  		"type":        "error",
   999  	})
  1000  }
  1001  
  1002  // Tests for GET /v2/interfaces
  1003  
  1004  func (s *interfacesSuite) TestInterfacesLegacy(c *check.C) {
  1005  	restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
  1006  	defer restore()
  1007  	// Install an inverse case mapper to exercise the interface mapping at the same time.
  1008  	restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
  1009  	defer restore()
  1010  
  1011  	d := s.daemon(c)
  1012  
  1013  	var anotherConsumerYaml = `
  1014  name: another-consumer-%s
  1015  version: 1
  1016  apps:
  1017   app:
  1018  plugs:
  1019   plug:
  1020    interface: test
  1021    key: value
  1022    label: label
  1023  `
  1024  	s.mockSnap(c, consumerYaml)
  1025  	s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "def"))
  1026  	s.mockSnap(c, fmt.Sprintf(anotherConsumerYaml, "abc"))
  1027  	s.mockSnap(c, producerYaml)
  1028  
  1029  	repo := d.Overlord().InterfaceManager().Repository()
  1030  	connRef := &interfaces.ConnRef{
  1031  		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
  1032  		SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
  1033  	}
  1034  	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
  1035  	c.Assert(err, check.IsNil)
  1036  
  1037  	st := d.Overlord().State()
  1038  	st.Lock()
  1039  	st.Set("conns", map[string]interface{}{
  1040  		"consumer:plug producer:slot": map[string]interface{}{
  1041  			"interface": "test",
  1042  			"auto":      true,
  1043  		},
  1044  		"another-consumer-def:plug producer:slot": map[string]interface{}{
  1045  			"interface": "test",
  1046  			"by-gadget": true,
  1047  			"auto":      true,
  1048  		},
  1049  		"another-consumer-abc:plug producer:slot": map[string]interface{}{
  1050  			"interface": "test",
  1051  			"by-gadget": true,
  1052  			"auto":      true,
  1053  		},
  1054  	})
  1055  	st.Unlock()
  1056  
  1057  	req, err := http.NewRequest("GET", "/v2/interfaces", nil)
  1058  	c.Assert(err, check.IsNil)
  1059  	rec := httptest.NewRecorder()
  1060  	s.req(c, req, nil).ServeHTTP(rec, req)
  1061  	c.Check(rec.Code, check.Equals, 200)
  1062  	var body map[string]interface{}
  1063  	err = json.Unmarshal(rec.Body.Bytes(), &body)
  1064  	c.Check(err, check.IsNil)
  1065  	c.Check(body, check.DeepEquals, map[string]interface{}{
  1066  		"result": map[string]interface{}{
  1067  			"plugs": []interface{}{
  1068  				map[string]interface{}{
  1069  					"snap":      "another-consumer-abc",
  1070  					"plug":      "plug",
  1071  					"interface": "test",
  1072  					"attrs":     map[string]interface{}{"key": "value"},
  1073  					"apps":      []interface{}{"app"},
  1074  					"label":     "label",
  1075  					"connections": []interface{}{
  1076  						map[string]interface{}{"snap": "producer", "slot": "slot"},
  1077  					},
  1078  				},
  1079  				map[string]interface{}{
  1080  					"snap":      "another-consumer-def",
  1081  					"plug":      "plug",
  1082  					"interface": "test",
  1083  					"attrs":     map[string]interface{}{"key": "value"},
  1084  					"apps":      []interface{}{"app"},
  1085  					"label":     "label",
  1086  					"connections": []interface{}{
  1087  						map[string]interface{}{"snap": "producer", "slot": "slot"},
  1088  					},
  1089  				},
  1090  				map[string]interface{}{
  1091  					"snap":      "consumer",
  1092  					"plug":      "plug",
  1093  					"interface": "test",
  1094  					"attrs":     map[string]interface{}{"key": "value"},
  1095  					"apps":      []interface{}{"app"},
  1096  					"label":     "label",
  1097  					"connections": []interface{}{
  1098  						map[string]interface{}{"snap": "producer", "slot": "slot"},
  1099  					},
  1100  				},
  1101  			},
  1102  			"slots": []interface{}{
  1103  				map[string]interface{}{
  1104  					"snap":      "producer",
  1105  					"slot":      "slot",
  1106  					"interface": "test",
  1107  					"attrs":     map[string]interface{}{"key": "value"},
  1108  					"apps":      []interface{}{"app"},
  1109  					"label":     "label",
  1110  					"connections": []interface{}{
  1111  						map[string]interface{}{"snap": "another-consumer-abc", "plug": "plug"},
  1112  						map[string]interface{}{"snap": "another-consumer-def", "plug": "plug"},
  1113  						map[string]interface{}{"snap": "consumer", "plug": "plug"},
  1114  					},
  1115  				},
  1116  			},
  1117  		},
  1118  		"status":      "OK",
  1119  		"status-code": 200.0,
  1120  		"type":        "sync",
  1121  	})
  1122  }
  1123  
  1124  func (s *interfacesSuite) TestInterfacesModern(c *check.C) {
  1125  	restore := builtin.MockInterface(&ifacetest.TestInterface{InterfaceName: "test"})
  1126  	defer restore()
  1127  	// Install an inverse case mapper to exercise the interface mapping at the same time.
  1128  	restore = ifacestate.MockSnapMapper(&inverseCaseMapper{})
  1129  	defer restore()
  1130  
  1131  	d := s.daemon(c)
  1132  
  1133  	s.mockSnap(c, consumerYaml)
  1134  	s.mockSnap(c, producerYaml)
  1135  
  1136  	repo := d.Overlord().InterfaceManager().Repository()
  1137  	connRef := &interfaces.ConnRef{
  1138  		PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"},
  1139  		SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"},
  1140  	}
  1141  	_, err := repo.Connect(connRef, nil, nil, nil, nil, nil)
  1142  	c.Assert(err, check.IsNil)
  1143  
  1144  	req, err := http.NewRequest("GET", "/v2/interfaces?select=connected&doc=true&plugs=true&slots=true", nil)
  1145  	c.Assert(err, check.IsNil)
  1146  	rec := httptest.NewRecorder()
  1147  	s.req(c, req, nil).ServeHTTP(rec, req)
  1148  	c.Check(rec.Code, check.Equals, 200)
  1149  	var body map[string]interface{}
  1150  	err = json.Unmarshal(rec.Body.Bytes(), &body)
  1151  	c.Check(err, check.IsNil)
  1152  	c.Check(body, check.DeepEquals, map[string]interface{}{
  1153  		"result": []interface{}{
  1154  			map[string]interface{}{
  1155  				"name": "test",
  1156  				"plugs": []interface{}{
  1157  					map[string]interface{}{
  1158  						"snap":  "consumer",
  1159  						"plug":  "plug",
  1160  						"label": "label",
  1161  						"attrs": map[string]interface{}{
  1162  							"key": "value",
  1163  						},
  1164  					}},
  1165  				"slots": []interface{}{
  1166  					map[string]interface{}{
  1167  						"snap":  "producer",
  1168  						"slot":  "slot",
  1169  						"label": "label",
  1170  						"attrs": map[string]interface{}{
  1171  							"key": "value",
  1172  						},
  1173  					},
  1174  				},
  1175  			},
  1176  		},
  1177  		"status":      "OK",
  1178  		"status-code": 200.0,
  1179  		"type":        "sync",
  1180  	})
  1181  }