// -*- Mode: Go; indent-tabs-mode: t -*-

/*
 * Copyright (C) 2024 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"
	"time"

	. "gopkg.in/check.v1"

	"github.com/snapcore/snapd/client"
	"github.com/snapcore/snapd/daemon"
	"github.com/snapcore/snapd/interfaces/prompting"
	prompting_errors "github.com/snapcore/snapd/interfaces/prompting/errors"
	"github.com/snapcore/snapd/interfaces/prompting/patterns"
	"github.com/snapcore/snapd/interfaces/prompting/requestprompts"
	"github.com/snapcore/snapd/interfaces/prompting/requestrules"
	"github.com/snapcore/snapd/overlord/ifacestate/apparmorprompting"
	"github.com/snapcore/snapd/testutil"
)

var _ = Suite(&promptingSuite{})

type fakeInterfacesRequestsManager struct {
	// Values to return
	ask          prompting.OutcomeType
	prompts      []*requestprompts.Prompt
	rules        []*requestrules.Rule
	prompt       *requestprompts.Prompt
	rule         *requestrules.Rule
	satisfiedIDs []prompting.IDType
	err          error

	// Store most recent received values
	userID               uint32
	snap                 string
	iface                string
	pid                  int32
	cgroup               string
	snapdShuttingDown    <-chan struct{}
	id                   prompting.IDType // used for prompt ID or rule ID
	ruleConstraintsJSON  prompting.ConstraintsJSON
	constraintsPatchJSON prompting.ConstraintsJSON
	replyConstraintsJSON prompting.ConstraintsJSON
	outcome              prompting.OutcomeType
	lifespan             prompting.LifespanType
	duration             string
	clientActivity       bool
}

func (m *fakeInterfacesRequestsManager) Ask(uid uint32, iface, snap string, pid int32, cgroup string, snapdShuttingDown <-chan struct{}) (prompting.OutcomeType, error) {
	m.userID = uid
	m.iface = iface
	m.snap = snap
	m.pid = pid
	m.cgroup = cgroup
	m.snapdShuttingDown = snapdShuttingDown
	return m.ask, m.err
}

func (m *fakeInterfacesRequestsManager) Prompts(userID uint32, clientActivity bool) ([]*requestprompts.Prompt, error) {
	m.userID = userID
	m.clientActivity = clientActivity
	return m.prompts, m.err
}

func (m *fakeInterfacesRequestsManager) PromptWithID(userID uint32, promptID prompting.IDType, clientActivity bool) (*requestprompts.Prompt, error) {
	m.userID = userID
	m.id = promptID
	m.clientActivity = clientActivity
	return m.prompt, m.err
}

func (m *fakeInterfacesRequestsManager) HandleReply(userID uint32, promptID prompting.IDType, replyConstraintsJSON prompting.ConstraintsJSON, outcome prompting.OutcomeType, lifespan prompting.LifespanType, duration string, clientActivity bool) ([]prompting.IDType, error) {
	m.userID = userID
	m.id = promptID
	m.replyConstraintsJSON = replyConstraintsJSON
	m.outcome = outcome
	m.lifespan = lifespan
	m.duration = duration
	m.clientActivity = clientActivity
	return m.satisfiedIDs, m.err
}

func (m *fakeInterfacesRequestsManager) Rules(userID uint32, snap string, iface string) ([]*requestrules.Rule, error) {
	m.userID = userID
	m.snap = snap
	m.iface = iface
	return m.rules, m.err
}

func (m *fakeInterfacesRequestsManager) AddRule(userID uint32, snap string, iface string, constraintsJSON prompting.ConstraintsJSON) (*requestrules.Rule, error) {
	m.userID = userID
	m.snap = snap
	m.iface = iface
	m.ruleConstraintsJSON = constraintsJSON
	return m.rule, m.err
}

func (m *fakeInterfacesRequestsManager) RemoveRules(userID uint32, snap string, iface string) ([]*requestrules.Rule, error) {
	m.userID = userID
	m.snap = snap
	m.iface = iface
	return m.rules, m.err
}

func (m *fakeInterfacesRequestsManager) RuleWithID(userID uint32, ruleID prompting.IDType) (*requestrules.Rule, error) {
	m.userID = userID
	m.id = ruleID
	return m.rule, m.err
}

func (m *fakeInterfacesRequestsManager) PatchRule(userID uint32, ruleID prompting.IDType, constraintsPatchJSON prompting.ConstraintsJSON) (*requestrules.Rule, error) {
	m.userID = userID
	m.id = ruleID
	m.constraintsPatchJSON = constraintsPatchJSON
	return m.rule, m.err
}

func (m *fakeInterfacesRequestsManager) RemoveRule(userID uint32, ruleID prompting.IDType) (*requestrules.Rule, error) {
	m.userID = userID
	m.id = ruleID
	return m.rule, m.err
}

type promptingSuite struct {
	apiBaseSuite

	// Set this to false to disable prompting
	appArmorPromptingRunning bool
	manager                  *fakeInterfacesRequestsManager
}

// Implement daemon.interfaceManager using the suite itself
func (s *promptingSuite) AppArmorPromptingRunning() bool {
	return s.appArmorPromptingRunning
}

func (s *promptingSuite) InterfacesRequestsManager() apparmorprompting.Manager {
	if s.manager == nil {
		return nil
	}
	return s.manager
}

func (s *promptingSuite) SetUpTest(c *C) {
	s.apiBaseSuite.SetUpTest(c)

	// Enable prompting and create fake manager
	s.appArmorPromptingRunning = true
	s.manager = &fakeInterfacesRequestsManager{}

	// Mock getInterfaceManager to return the suite itself
	daemon.MockInterfaceManager(s)

	s.expectReadAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})

}

func (s *promptingSuite) TestGetUserID(c *C) {
	s.daemon(c)

	for _, testCase := range []struct {
		path         string
		uid          string
		expectedUser uint32
		expectedCode int
		expectedErr  string
	}{
		{
			path:         "/v2/interfaces/requests/prompts",
			uid:          "invalid",
			expectedUser: 0,
			expectedCode: 403,
			expectedErr:  "cannot get remote user: ",
		},
		{
			path:         "/v2/interfaces/requests/prompts",
			uid:          "1000",
			expectedUser: 1000,
			expectedCode: 200,
			expectedErr:  "",
		},
		{
			path:         "/v2/interfaces/requests/prompts?user-id=1000",
			uid:          "1000",
			expectedUser: 0,
			expectedCode: 403,
			expectedErr:  `only admins may use the "user-id" parameter`,
		},
		{
			path:         "/v2/interfaces/requests/prompts?user-id=1000&user-id=1234",
			uid:          "0",
			expectedUser: 0,
			expectedCode: 400,
			expectedErr:  `invalid "user-id" parameter: must only include one "user-id"`,
		},
		{
			path:         "/v2/interfaces/requests/prompts?user-id=invalid",
			uid:          "0",
			expectedUser: 0,
			expectedCode: 400,
			expectedErr:  `invalid "user-id" parameter: `,
		},
		{
			path:         "/v2/interfaces/requests/prompts?user-id=-1",
			uid:          "0",
			expectedUser: 0,
			expectedCode: 400,
			expectedErr:  `invalid "user-id" parameter: user ID is not a valid uint32: `,
		},
		{
			path:         fmt.Sprintf("/v2/interfaces/requests/prompts?user-id=4294967296"), // math.MaxUint32 + 1
			uid:          "0",
			expectedUser: 0,
			expectedCode: 400,
			expectedErr:  `invalid "user-id" parameter: user ID is not a valid uint32: `,
		},
		{
			path:         "/v2/interfaces/requests/prompts?user-id=1234",
			uid:          "0",
			expectedUser: 1234,
			expectedCode: 200,
			expectedErr:  "",
		},
		{
			path:         fmt.Sprintf("/v2/interfaces/requests/prompts?user-id=4294967295"), // math.MaxUint32
			uid:          "0",
			expectedUser: 0xffffffff,
			expectedCode: 200,
			expectedErr:  "",
		},
	} {
		req, err := http.NewRequest("GET", testCase.path, nil)
		c.Assert(err, IsNil)
		req.RemoteAddr = fmt.Sprintf("pid=100;uid=%s;socket=;", testCase.uid)

		userID, rsp := daemon.GetUserID(req)
		if testCase.expectedErr == "" {
			c.Check(rsp, IsNil)
		} else {
			rspe, ok := rsp.(*daemon.APIError)
			c.Assert(ok, Equals, true)
			c.Check(rspe.Status, Equals, testCase.expectedCode)
			c.Check(rspe.Message, testutil.Contains, testCase.expectedErr)
		}
		c.Check(userID, Equals, testCase.expectedUser)
	}
}

func (s *promptingSuite) TestPromptingNotRunningError(c *C) {
	apiResp := daemon.PromptingNotRunningError()
	jsonResp := apiResp.JSON()
	rec := httptest.NewRecorder()
	jsonResp.ServeHTTP(rec, nil)
	var body map[string]any
	err := json.Unmarshal(rec.Body.Bytes(), &body)
	c.Check(err, IsNil)
	c.Check(body, DeepEquals, map[string]any{
		"result": map[string]any{
			"message": "AppArmor Prompting is not running",
			"kind":    string(client.ErrorKindAppArmorPromptingNotRunning),
		},
		"status":      "Internal Server Error",
		"status-code": 500.0,
		"type":        "error",
	})
}

func (s *promptingSuite) TestPromptingError(c *C) {
	for _, testCase := range []struct {
		err  error
		body map[string]any
	}{
		{
			err: prompting_errors.ErrPromptNotFound,
			body: map[string]any{
				"result": map[string]any{
					"message": prompting_errors.ErrPromptNotFound.Error(),
					"kind":    string(client.ErrorKindInterfacesRequestsPromptNotFound),
				},
				"status":      "Not Found",
				"status-code": 404.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.ErrRuleNotFound,
			body: map[string]any{
				"result": map[string]any{
					"message": prompting_errors.ErrRuleNotFound.Error(),
					"kind":    string(client.ErrorKindInterfacesRequestsRuleNotFound),
				},
				"status":      "Not Found",
				"status-code": 404.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.ErrRuleNotAllowed,
			body: map[string]any{
				"result": map[string]any{
					"message": prompting_errors.ErrRuleNotAllowed.Error(),
					"kind":    string(client.ErrorKindInterfacesRequestsRuleNotFound),
				},
				"status":      "Not Found",
				"status-code": 404.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.ErrPromptingClosed,
			body: map[string]any{
				"result": map[string]any{
					"message": prompting_errors.ErrPromptingClosed.Error(),
				},
				"status":      "Service Unavailable",
				"status-code": 503.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.ErrTooManyPrompts,
			body: map[string]any{
				"result": map[string]any{
					"message": prompting_errors.ErrTooManyPrompts.Error(),
				},
				"status":      "Internal Server Error",
				"status-code": 500.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.ErrRuleIDConflict,
			body: map[string]any{
				"result": map[string]any{
					"message": prompting_errors.ErrRuleIDConflict.Error(),
				},
				"status":      "Internal Server Error",
				"status-code": 500.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.ErrRuleDBInconsistent,
			body: map[string]any{
				"result": map[string]any{
					"message": prompting_errors.ErrRuleDBInconsistent.Error(),
				},
				"status":      "Internal Server Error",
				"status-code": 500.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.NewInvalidOutcomeError("foo", []string{"bar", "baz"}),
			body: map[string]any{
				"result": map[string]any{
					"message": `invalid outcome: "foo"`,
					"kind":    "interfaces-requests-invalid-fields",
					"value": map[string]any{
						"outcome": map[string]any{
							"reason":    "unsupported-value",
							"supported": []any{"bar", "baz"},
							"value":     []any{"foo"},
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.NewInvalidLifespanError("foo", []string{"bar", "baz"}),
			body: map[string]any{
				"result": map[string]any{
					"message": `invalid lifespan: "foo"`,
					"kind":    "interfaces-requests-invalid-fields",
					"value": map[string]any{
						"lifespan": map[string]any{
							"reason":    "unsupported-value",
							"supported": []any{"bar", "baz"},
							"value":     []any{"foo"},
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.NewRuleLifespanSingleError([]string{"bar", "baz"}),
			body: map[string]any{
				"result": map[string]any{
					"message": `cannot create rule with lifespan "single"`,
					"kind":    "interfaces-requests-invalid-fields",
					"value": map[string]any{
						"lifespan": map[string]any{
							"reason":    "unsupported-value",
							"supported": []any{"bar", "baz"},
							"value":     []any{"single"},
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.NewInvalidInterfaceError("foo", []string{"bar", "baz"}),
			body: map[string]any{
				"result": map[string]any{
					"message": `invalid interface: "foo"`,
					"kind":    "interfaces-requests-invalid-fields",
					"value": map[string]any{
						"interface": map[string]any{
							"reason":    "unsupported-value",
							"supported": []any{"bar", "baz"},
							"value":     []any{"foo"},
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.NewInvalidPermissionsError("foo", []string{"bar", "baz"}, []string{"fizz", "buzz"}),
			body: map[string]any{
				"result": map[string]any{
					"message": `invalid permissions for foo interface: "bar", "baz"`,
					"kind":    "interfaces-requests-invalid-fields",
					"value": map[string]any{
						"permissions": map[string]any{
							"reason":    "unsupported-value",
							"supported": []any{"fizz", "buzz"},
							"value":     []any{"bar", "baz"},
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.NewPermissionsEmptyError("foo", []string{"bar", "baz"}),
			body: map[string]any{
				"result": map[string]any{
					"message": `invalid permissions for foo interface: permissions empty`,
					"kind":    "interfaces-requests-invalid-fields",
					"value": map[string]any{
						"permissions": map[string]any{
							"reason":    "unsupported-value",
							"supported": []any{"bar", "baz"},
							"value":     []any{},
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.NewInvalidDurationError("foo", "really terrible"),
			body: map[string]any{
				"result": map[string]any{
					"message": `invalid duration: really terrible: "foo"`,
					"kind":    "interfaces-requests-invalid-fields",
					"value": map[string]any{
						"duration": map[string]any{
							"reason": "parse-error",
							"value":  "foo",
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.NewInvalidExpirationError(time.Date(1, time.February, 3, 4, 5, 6, 7, time.UTC), "really terrible"),
			body: map[string]any{
				"result": map[string]any{
					"message": `invalid expiration: really terrible: "0001-02-03T04:05:06.000000007Z"`,
					"kind":    "interfaces-requests-invalid-fields",
					"value": map[string]any{
						"expiration": map[string]any{
							"reason": "parse-error",
							"value":  "0001-02-03T04:05:06.000000007Z",
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.NewInvalidPathPatternError(`invalid/pattern`, "must start with '/'"),
			body: map[string]any{
				"result": map[string]any{
					"message": `invalid path pattern: must start with '/': "invalid/pattern"`,
					"kind":    "interfaces-requests-invalid-fields",
					"value": map[string]any{
						"path-pattern": map[string]any{
							"reason": "parse-error",
							"value":  "invalid/pattern",
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.ErrPatchedRuleHasNoPerms,
			body: map[string]any{
				"result": map[string]any{
					"message": "cannot patch rule to have no permissions",
					"kind":    "interfaces-requests-patched-rule-has-no-permissions",
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: prompting_errors.ErrNewSessionRuleNoSession,
			body: map[string]any{
				"result": map[string]any{
					"message": `cannot create rule with lifespan "session" when user session is not present`,
					"kind":    "interfaces-requests-new-session-rule-no-session",
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: &prompting_errors.RequestedPathNotMatchedError{
				Requested: "foo",
				Replied:   "bar",
			},
			body: map[string]any{
				"result": map[string]any{
					"message": fmt.Sprintf(`%v "foo": "bar"`, prompting_errors.ErrReplyNotMatchRequestedPath),
					"kind":    "interfaces-requests-reply-not-match-request",
					"value": map[string]any{
						"path-pattern": map[string]any{
							"requested-path":  "foo",
							"replied-pattern": "bar",
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: &prompting_errors.RequestedPermissionsNotMatchedError{
				Requested: []string{"foo", "bar", "baz"},
				Replied:   []string{"fizz", "buzz"},
			},
			body: map[string]any{
				"result": map[string]any{
					"message": fmt.Sprintf(`%v [foo bar baz]: [fizz buzz]`, prompting_errors.ErrReplyNotMatchRequestedPermissions),
					"kind":    "interfaces-requests-reply-not-match-request",
					"value": map[string]any{
						"permissions": map[string]any{
							"requested-permissions": []any{"foo", "bar", "baz"},
							"replied-permissions":   []any{"fizz", "buzz"},
						},
					},
				},
				"status":      "Bad Request",
				"status-code": 400.0,
				"type":        "error",
			},
		},
		{
			err: &prompting_errors.RuleConflictError{
				Conflicts: []prompting_errors.RuleConflict{
					{
						Permission:    "foo",
						Variant:       "variant 1",
						ConflictingID: "conflicting rule 1",
					},
					{
						Permission:    "bar",
						Variant:       "variant 2",
						ConflictingID: "conflicting rule 2",
					},
				},
			},
			body: map[string]any{
				"result": map[string]any{
					"message": prompting_errors.ErrRuleConflict.Error(),
					"kind":    "interfaces-requests-rule-conflict",
					"value": map[string]any{
						"conflicts": []any{
							map[string]any{
								"permission":     "foo",
								"variant":        "variant 1",
								"conflicting-id": "conflicting rule 1",
							},
							map[string]any{
								"permission":     "bar",
								"variant":        "variant 2",
								"conflicting-id": "conflicting rule 2",
							},
						},
					},
				},
				"status":      "Conflict",
				"status-code": 409.0,
				"type":        "error",
			},
		},
		{
			err: fmt.Errorf("some arbitrary error"),
			body: map[string]any{
				"result": map[string]any{
					"message": "some arbitrary error",
				},
				"status":      "Internal Server Error",
				"status-code": 500.0,
				"type":        "error",
			},
		},
		{
			err: fmt.Errorf("wrapped a handled error: %w", prompting_errors.ErrPromptNotFound),
			body: map[string]any{
				"result": map[string]any{
					"message": fmt.Sprintf("wrapped a handled error: %v", prompting_errors.ErrPromptNotFound.Error()),
					"kind":    string(client.ErrorKindInterfacesRequestsPromptNotFound),
				},
				"status":      "Not Found",
				"status-code": 404.0,
				"type":        "error",
			},
		},
	} {
		apiResp := daemon.PromptingError(testCase.err)
		jsonResp := apiResp.JSON()
		rec := httptest.NewRecorder()
		jsonResp.ServeHTTP(rec, nil)
		var body map[string]any
		err := json.Unmarshal(rec.Body.Bytes(), &body)
		c.Check(err, IsNil)
		c.Check(body, DeepEquals, testCase.body)
	}
}

func (s *promptingSuite) TestPostInterfacesRequestsHappy(c *C) {
	s.expectWriteAccess(daemon.ByActionAccess{
		ByAction: map[string]daemon.AccessChecker{
			"ask": daemon.OpenAccess{},
		},
		Default: daemon.RootAccess{},
	})

	d := s.daemon(c)

	const (
		fakeUID    = uint32(1000)
		fakePID    = int32(1234)
		fakeCgroup = "/path/to/cgroup/snap.foo.bar-someuuid.scope"
		iface      = "audio-record"

		expectedSnap = "foo"
	)

	restore := daemon.MockCgroupProcessPathInTrackingCgroup(func(pid int) (string, error) {
		c.Check(pid, Equals, int(fakePID))
		return fakeCgroup, nil
	})
	defer restore()

	st := d.Overlord().State()
	st.Lock()
	st.Set("conns", map[string]any{
		"foo:audio-record core:audio-record": map[string]any{
			"interface": "audio-record",
		},
	})
	st.Unlock()

	s.manager.ask = prompting.OutcomeAllow

	contents := &daemon.PostInterfacesRequestsRequestBody{
		Action:    "ask",
		Interface: iface,
		PID:       fakePID,
	}
	marshalled, err := json.Marshal(contents)
	c.Assert(err, IsNil)

	rsp := s.makeSyncReq(c, "POST", "/v2/interfaces/requests", 1000, marshalled)

	// Check parameters
	c.Check(s.manager.userID, Equals, fakeUID)
	c.Check(s.manager.iface, Equals, iface)
	c.Check(s.manager.snap, Equals, expectedSnap)
	c.Check(s.manager.pid, Equals, fakePID)
	c.Check(s.manager.cgroup, Equals, fakeCgroup)
	c.Check(s.manager.snapdShuttingDown, NotNil)

	// Check return value
	responseBody, ok := rsp.Result.(daemon.PostInterfacesRequestsResponse)
	c.Assert(ok, Equals, true, Commentf("%t: %+v", rsp.Result, rsp.Result))
	c.Check(responseBody.Outcome, Equals, s.manager.ask)
}

func (s *promptingSuite) TestPostInterfacesRequestsErrors(c *C) {
	s.expectWriteAccess(daemon.ByActionAccess{
		ByAction: map[string]daemon.AccessChecker{
			"ask": daemon.OpenAccess{},
		},
		Default: daemon.RootAccess{},
	})

	d := s.daemon(c)

	const (
		fakeUID    = uint32(1000)
		fakePID    = int32(1234)
		fakeCgroup = "/path/to/cgroup/snap.foo.bar-someuuid.scope"
		iface      = "audio-record"
	)

	restore := daemon.MockCgroupProcessPathInTrackingCgroup(func(pid int) (string, error) {
		c.Check(pid, Equals, int(fakePID))
		return fakeCgroup, nil
	})
	defer restore()

	st := d.Overlord().State()
	st.Lock()
	st.Set("conns", map[string]any{
		"foo:audio-record core:audio-record": map[string]any{
			"interface": "audio-record",
		},
	})
	st.Unlock()

	contents := &daemon.PostInterfacesRequestsRequestBody{
		Action:    "ask",
		Interface: iface,
		PID:       fakePID,
	}
	marshalled, err := json.Marshal(contents)
	c.Assert(err, IsNil)

	// Can't parse userID
	body := bytes.NewReader(marshalled)
	req, err := http.NewRequest("POST", "/v2/interfaces/requests", body)
	c.Assert(err, IsNil)
	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 403)
	c.Check(rspe.Message, Equals, "cannot get remote user: no pid/uid found")

	// Prompting not running
	s.appArmorPromptingRunning = false
	body = bytes.NewReader(marshalled)
	req, err = http.NewRequest("POST", "/v2/interfaces/requests", body)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 500)
	c.Check(rspe.Message, Equals, "AppArmor Prompting is not running")
	c.Check(rspe.Kind, Equals, client.ErrorKindAppArmorPromptingNotRunning)
	s.appArmorPromptingRunning = true

	// Decode body error
	badBody := bytes.NewReader([]byte(`{"action": "ask", "pid": "bad"}`))
	req, err = http.NewRequest("POST", "/v2/interfaces/requests", badBody)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Message, Matches, "cannot decode request body into ask for prompting access.*")

	// Missing interface
	badBody = bytes.NewReader([]byte(`{"action": "ask", "pid": 1234}`))
	req, err = http.NewRequest("POST", "/v2/interfaces/requests", badBody)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Message, Matches, `must have non-empty "interface" field`)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsInvalidFields)

	// Missing pid
	badBody = bytes.NewReader([]byte(`{"action": "ask", "interface": "audio-record"}`))
	req, err = http.NewRequest("POST", "/v2/interfaces/requests", badBody)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Message, Matches, `"pid" field must be a positive integer`)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsInvalidFields)

	// Can't find cgroup
	restore = daemon.MockCgroupProcessPathInTrackingCgroup(func(pid int) (string, error) {
		return "", fmt.Errorf("there's no cgroup")
	})
	body = bytes.NewReader(marshalled)
	req, err = http.NewRequest("POST", "/v2/interfaces/requests", body)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Message, Matches, `cannot find cgroup path for process with PID 1234: there's no cgroup`)
	restore()

	// Can't find security tag from cgroup
	restore = daemon.MockCgroupProcessPathInTrackingCgroup(func(pid int) (string, error) {
		return "/path/to/some/invalid-looking/cgroup", nil
	})
	body = bytes.NewReader(marshalled)
	req, err = http.NewRequest("POST", "/v2/interfaces/requests", body)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Message, Matches, `cannot find snap security tag for process with PID 1234`)
	restore()

	// Can't find valid interface connection for snap
	for _, testCase := range []struct {
		connsMap map[string]any
		expected daemon.Response
	}{
		{
			map[string]any{},
			daemon.BadRequest(`cannot find interface connection for snap "foo": "audio-record"`),
		},
		{
			map[string]any{
				"malformed": map[string]any{
					"interface": "audio-record",
				},
			},
			daemon.InternalError(`cannot get connections: malformed connection identifier: "malformed"`),
		},
	} {
		st := d.Overlord().State()
		st.Lock()
		st.Set("conns", testCase.connsMap)
		st.Unlock()

		body = bytes.NewReader(marshalled)
		req, err = http.NewRequest("POST", "/v2/interfaces/requests", body)
		c.Assert(err, IsNil)
		req.RemoteAddr = "pid=100;uid=1000;socket=;"
		rspe = s.errorReq(c, req, nil, actionIsExpected)
		c.Check(rspe, DeepEquals, testCase.expected)
		restore()
	}

	st.Lock()
	st.Set("conns", map[string]any{
		"foo:audio-record core:audio-record": map[string]any{
			"interface": "audio-record",
		},
	})
	st.Unlock()

	// Error from Ask()
	s.manager.err = prompting_errors.ErrPromptingClosed
	body = bytes.NewReader(marshalled)
	req, err = http.NewRequest("POST", "/v2/interfaces/requests", body)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 503)
	c.Check(rspe.Message, Equals, "prompting subsystem has already been closed")
	s.manager.err = nil
}

func (s *promptingSuite) TestValidateSnapHasInterfaceConnection(c *C) {
	d := s.daemon(c)

	s.mockSnap(c, `
name: core
type: os
version: 1
slots:
  foo:
  bar:
  baz:
`)
	s.mockSnap(c, `
name: some-snap
version: 1
plugs:
  foo:
  bar:
  baz:
`)

	st := d.Overlord().State()
	st.Lock()
	st.Set("conns", map[string]any{
		"some-snap:foo core:foo": map[string]any{
			"interface": "foo",
		},
		"some-snap:bar core:bar": map[string]any{
			"interface": "bar",
			"undesired": true,
		},
	})
	st.Unlock()

	for _, testCase := range []struct {
		snap     string
		iface    string
		expected daemon.Response
	}{
		{
			"some-snap",
			"foo",
			nil,
		},
		{
			"some-snap",
			"bar",
			daemon.BadRequest(`cannot find interface connection for snap "some-snap": "bar"`),
		},
		{
			"some-snap",
			"baz",
			daemon.BadRequest(`cannot find interface connection for snap "some-snap": "baz"`),
		},
		{
			"some-snap",
			"qux",
			daemon.BadRequest(`cannot find interface connection for snap "some-snap": "qux"`),
		},
		{
			"another-snap",
			"foo",
			daemon.BadRequest(`cannot find interface connection for snap "another-snap": "foo"`),
		},
	} {
		result := daemon.ValidateSnapHasInterfaceConnection(d, testCase.snap, testCase.iface)
		c.Check(result, DeepEquals, testCase.expected)
	}

	// Test error
	st.Lock()
	st.Set("conns", map[string]any{
		"is-bad": map[string]any{
			"interface": "foo",
		},
	})
	st.Unlock()

	result := daemon.ValidateSnapHasInterfaceConnection(d, "some-snap", "foo")
	c.Check(result, DeepEquals, daemon.InternalError(`cannot get connections: malformed connection identifier: "is-bad"`))
}

func (s *promptingSuite) TestGetPromptsHappy(c *C) {
	s.daemon(c)

	s.manager.prompts = make([]*requestprompts.Prompt, 3)

	rsp := s.makeSyncReq(c, "GET", "/v2/interfaces/requests/prompts", 1000, nil)

	// Check parameters
	c.Check(s.manager.userID, Equals, uint32(1000))
	c.Check(s.manager.clientActivity, Equals, true)

	// Check return value
	prompts, ok := rsp.Result.([]*requestprompts.Prompt)
	c.Check(ok, Equals, true)
	c.Check(prompts, DeepEquals, s.manager.prompts)
}

func (s *promptingSuite) TestGetPromptsReturnsEmptyList(c *C) {
	s.daemon(c)

	// Pretend manager returned nil prompts
	s.manager.prompts = nil
	rsp := s.makeSyncReq(c, "GET", "/v2/interfaces/requests/prompts", 1000, nil)

	// Daemon remaps nil to empty slice
	prompts, ok := rsp.Result.([]*requestprompts.Prompt)
	c.Check(ok, Equals, true)
	c.Check(prompts, DeepEquals, []*requestprompts.Prompt{})
}

func (s *promptingSuite) TestGetPromptsErrors(c *C) {
	s.daemon(c)

	// Can't parse userID
	req, err := http.NewRequest("GET", "/v2/interfaces/requests/prompts", nil)
	c.Assert(err, IsNil)
	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 403)
	c.Check(rspe.Message, Equals, "cannot get remote user: no pid/uid found")

	// Prompting not running
	s.appArmorPromptingRunning = false
	req, err = http.NewRequest("GET", "/v2/interfaces/requests/prompts", nil)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 500)
	c.Check(rspe.Kind, Equals, client.ErrorKindAppArmorPromptingNotRunning)
	c.Check(rspe.Message, Equals, "AppArmor Prompting is not running")
	s.appArmorPromptingRunning = true

	// Prompting closed
	s.manager.err = prompting_errors.ErrPromptingClosed
	req, err = http.NewRequest("GET", "/v2/interfaces/requests/prompts", nil)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 503)
	c.Check(rspe.Kind, Equals, client.ErrorKind(""))
	c.Check(rspe.Message, Equals, prompting_errors.ErrPromptingClosed.Error())
	s.manager.err = nil
}

func (s *promptingSuite) makeSyncReq(c *C, method string, path string, uid uint32, data []byte) *daemon.RespJSON {
	body := &bytes.Reader{}
	if len(data) > 0 {
		body = bytes.NewReader(data)
	}
	req, err := http.NewRequest(method, path, body)
	c.Assert(err, IsNil)
	req.RemoteAddr = fmt.Sprintf("pid=100;uid=%d;socket=;", uid)
	rsp := s.syncReq(c, req, nil, actionIsExpected)
	c.Check(rsp.Status, Equals, 200)
	return rsp
}

func (s *promptingSuite) TestGetPromptHappy(c *C) {
	s.daemon(c)

	s.manager.prompt = &requestprompts.Prompt{}

	rsp := s.makeSyncReq(c, "GET", "/v2/interfaces/requests/prompts/0123456789ABCDEF", 1000, nil)

	// Check parameters
	c.Check(s.manager.userID, Equals, uint32(1000))
	c.Check(s.manager.id, Equals, prompting.IDType(0x0123456789abcdef))
	c.Check(s.manager.clientActivity, Equals, true)

	// Check return value
	prompt, ok := rsp.Result.(*requestprompts.Prompt)
	c.Check(ok, Equals, true)
	c.Check(prompt, DeepEquals, s.manager.prompt)
}

func (s *promptingSuite) TestGetPromptErrors(c *C) {
	s.daemon(c)

	// Can't parse userID
	req, err := http.NewRequest("GET", "/v2/interfaces/requests/prompts/0123456789ABCDEF", nil)
	c.Assert(err, IsNil)
	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 403)
	c.Check(rspe.Message, Equals, "cannot get remote user: no pid/uid found")

	// Can't parse prompt ID
	req, err = http.NewRequest("GET", "/v2/interfaces/requests/prompts/not-a-valid-id", nil)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 404)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsPromptNotFound)

	// Prompting not running
	s.appArmorPromptingRunning = false
	req, err = http.NewRequest("GET", "/v2/interfaces/requests/prompts/0123456789ABCDEF", nil)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 500)
	c.Check(rspe.Kind, Equals, client.ErrorKindAppArmorPromptingNotRunning)
	c.Check(rspe.Message, Equals, "AppArmor Prompting is not running")
	s.appArmorPromptingRunning = true

	// Errors from manager
	for _, testCase := range []struct {
		err          error
		expectedCode int
		expectedKind client.ErrorKind
		expectedMsg  string
	}{
		{
			err:          prompting_errors.ErrPromptNotFound,
			expectedCode: 404,
			expectedKind: client.ErrorKindInterfacesRequestsPromptNotFound,
			expectedMsg:  prompting_errors.ErrPromptNotFound.Error(),
		},
		{
			err:          prompting_errors.ErrPromptingClosed,
			expectedCode: 503,
			expectedKind: client.ErrorKind(""),
			expectedMsg:  prompting_errors.ErrPromptingClosed.Error(),
		},
	} {
		s.manager.err = testCase.err
		req, err = http.NewRequest("GET", "/v2/interfaces/requests/prompts/0123456789ABCDEF", nil)
		c.Assert(err, IsNil)
		req.RemoteAddr = "pid=100;uid=1000;socket=;"
		rspe = s.errorReq(c, req, nil, actionIsExpected)
		c.Check(rspe.Status, Equals, testCase.expectedCode)
		c.Check(rspe.Kind, Equals, testCase.expectedKind)
		c.Check(rspe.Message, Equals, testCase.expectedMsg)
		s.manager.err = nil
	}
}

func (s *promptingSuite) TestPostPromptHappy(c *C) {
	s.expectWriteAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})

	s.daemon(c)

	s.manager.satisfiedIDs = []prompting.IDType{
		prompting.IDType(1234),
		prompting.IDType(0),
		prompting.IDType(0xFFFFFFFFFFFFFFFF),
		prompting.IDType(0xF00BA4),
	}

	constraintsJSON := prompting.ConstraintsJSON{
		"path-pattern": json.RawMessage(`"/home/test/Pictures/**/*.{png,svg}"`),
		"permissions":  json.RawMessage(`["read","execute"]`),
	}
	contents := &daemon.PostPromptRequestBody{
		Outcome:     prompting.OutcomeAllow,
		Lifespan:    prompting.LifespanTimespan,
		Duration:    "10m",
		Constraints: constraintsJSON,
	}
	marshalled, err := json.Marshal(contents)
	c.Assert(err, IsNil)

	rsp := s.makeSyncReq(c, "POST", "/v2/interfaces/requests/prompts/0123456789ABCDEF", 1000, marshalled)

	// Check parameters
	c.Check(s.manager.userID, Equals, uint32(1000))
	c.Check(s.manager.id, Equals, prompting.IDType(0x0123456789abcdef))
	c.Check(s.manager.replyConstraintsJSON, DeepEquals, contents.Constraints)
	c.Check(s.manager.outcome, Equals, contents.Outcome)
	c.Check(s.manager.lifespan, Equals, contents.Lifespan)
	c.Check(s.manager.duration, Equals, contents.Duration)
	c.Check(s.manager.clientActivity, Equals, true)

	// Check return value
	satisfiedIDs, ok := rsp.Result.([]prompting.IDType)
	c.Check(ok, Equals, true)
	c.Check(satisfiedIDs, DeepEquals, s.manager.satisfiedIDs)
}

func (s *promptingSuite) TestPostPromptDenyHappy(c *C) {
	s.expectWriteAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})

	s.daemon(c)

	s.manager.satisfiedIDs = []prompting.IDType{
		prompting.IDType(1234),
		prompting.IDType(0),
		prompting.IDType(0xFFFFFFFFFFFFFFFF),
		prompting.IDType(0xF00BA4),
	}

	constraintsJSON := prompting.ConstraintsJSON{
		"path-pattern": json.RawMessage(`"/home/test/Pictures/**/*.{png,svg}"`),
		"permissions":  json.RawMessage(`["read","execute"]`),
	}
	contents := &daemon.PostPromptRequestBody{
		Outcome:     prompting.OutcomeDeny,
		Lifespan:    prompting.LifespanTimespan,
		Duration:    "10m",
		Constraints: constraintsJSON,
	}
	marshalled, err := json.Marshal(contents)
	c.Assert(err, IsNil)

	rsp := s.makeSyncReq(c, "POST", "/v2/interfaces/requests/prompts/0123456789ABCDEF", 1000, marshalled)

	// Check parameters
	c.Check(s.manager.userID, Equals, uint32(1000))
	c.Check(s.manager.id, Equals, prompting.IDType(0x0123456789abcdef))
	c.Check(s.manager.replyConstraintsJSON, DeepEquals, contents.Constraints)
	c.Check(s.manager.outcome, Equals, contents.Outcome)
	c.Check(s.manager.lifespan, Equals, contents.Lifespan)
	c.Check(s.manager.duration, Equals, contents.Duration)
	c.Check(s.manager.clientActivity, Equals, true)

	// Check return value
	satisfiedIDs, ok := rsp.Result.([]prompting.IDType)
	c.Check(ok, Equals, true)
	c.Check(satisfiedIDs, DeepEquals, s.manager.satisfiedIDs)
}

func (s *promptingSuite) TestPostPromptReturnsEmptyList(c *C) {
	s.expectWriteAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})

	s.daemon(c)

	constraintsJSON := prompting.ConstraintsJSON{
		"path-pattern": json.RawMessage(`"/home/test/Pictures/**/*.{png,svg}"`),
		"permissions":  json.RawMessage(`["read","execute"]`),
	}
	contents := &daemon.PostPromptRequestBody{
		Outcome:     prompting.OutcomeAllow,
		Lifespan:    prompting.LifespanTimespan,
		Duration:    "10m",
		Constraints: constraintsJSON,
	}
	marshalled, err := json.Marshal(contents)
	c.Assert(err, IsNil)

	// Pretend manager returned nil satisfiedIDs
	s.manager.satisfiedIDs = nil
	rsp := s.makeSyncReq(c, "POST", "/v2/interfaces/requests/prompts/0123456789ABCDEF", 1000, marshalled)

	// Daemon remaps nil to empty slice
	satisfiedIDs, ok := rsp.Result.([]prompting.IDType)
	c.Check(ok, Equals, true)
	c.Check(satisfiedIDs, DeepEquals, []prompting.IDType{})
}

func (s *promptingSuite) TestPostPromptErrors(c *C) {
	s.expectWriteAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})
	s.daemon(c)

	validBody := []byte(`{"action":"allow","lifespan":"timespan","duration":"10m","constraints":{"path-pattern":"/home/test/Pictures/**/*.{png,svg}","permissions":["read","execute"]}}`)

	// Can't parse userID
	req, err := http.NewRequest("POST", "/v2/interfaces/requests/prompts/0123456789ABCDEF", bytes.NewReader(validBody))
	c.Assert(err, IsNil)
	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 403)
	c.Check(rspe.Message, testutil.Contains, "cannot get remote user")

	// Can't parse prompt ID
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/prompts/not-a-valid-id", bytes.NewReader(validBody))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 404)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsPromptNotFound)

	// Prompting not running
	s.appArmorPromptingRunning = false
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/prompts/0123456789ABCDEF", bytes.NewReader(validBody))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 500)
	c.Check(rspe.Kind, Equals, client.ErrorKindAppArmorPromptingNotRunning)
	c.Check(rspe.Message, Equals, "AppArmor Prompting is not running")
	s.appArmorPromptingRunning = true

	// Bad json
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/prompts/0123456789ABCDEF", bytes.NewReader([]byte(`{"action":"allow"`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKind(""))
	c.Check(rspe.Message, Matches, "cannot decode request body into prompt reply:.*")

	// Decode body error
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/prompts/0123456789ABCDEF", bytes.NewReader([]byte(`{"action":"allow","lifespan":10,"constraints":{"path-pattern":"/home/test/Pictures/**/*.{png,svg}","permissions":["read","execute"]}}`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKind(""))
	c.Check(rspe.Message, Matches, "cannot decode request body into prompt reply:.*cannot unmarshal number into Go struct field.*")

	// Invalid outcome (action) or lifespan
	for _, testCase := range []struct {
		body         []byte
		actionKnown  actionExpectedBool
		expectedCode int
		expectedKind client.ErrorKind
		expectedMsg  string
	}{
		{
			body:         []byte(`{"action":"foo","lifespan":"timespan","duration":"10m","constraints":{"path-pattern":"/home/test/Pictures/**/*.{png,svg}","permissions":["read","execute"]}}`),
			actionKnown:  actionIsUnexpected,
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid outcome: "foo"`,
		},
		{
			body:         []byte(`{"action":"","lifespan":"timespan","duration":"10m","constraints":{"path-pattern":"/home/test/Pictures/**/*.{png,svg}","permissions":["read","execute"]}}`),
			actionKnown:  actionIsUnexpected,
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid outcome: ""`,
		},
		{
			body:         []byte(`{"action":"allow","lifespan":"foo","duration":"10m","constraints":{"path-pattern":"/home/test/Pictures/**/*.{png,svg}","permissions":["read","execute"]}}`),
			actionKnown:  actionIsExpected,
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid lifespan: "foo"`,
		},
		{
			body:         []byte(`{"action":"allow","lifespan":"","duration":"10m","constraints":{"path-pattern":"/home/test/Pictures/**/*.{png,svg}","permissions":["read","execute"]}}`),
			actionKnown:  actionIsExpected,
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid lifespan: ""`,
		},
	} {
		req, err = http.NewRequest("POST", "/v2/interfaces/requests/prompts/0123456789ABCDEF", bytes.NewReader(testCase.body))
		c.Assert(err, IsNil)
		req.RemoteAddr = "pid=100;uid=1000;socket=;"
		rspe = s.errorReq(c, req, nil, testCase.actionKnown)
		c.Check(rspe.Status, Equals, testCase.expectedCode)
		c.Check(rspe.Kind, Equals, testCase.expectedKind)
		c.Check(rspe.Message, Equals, testCase.expectedMsg)
	}

	// Errors from manager
	for _, testCase := range []struct {
		err          error
		expectedCode int
		expectedKind client.ErrorKind
		expectedMsg  string
	}{
		{
			err:          prompting_errors.ErrPromptNotFound,
			expectedCode: 404,
			expectedKind: client.ErrorKindInterfacesRequestsPromptNotFound,
			expectedMsg:  prompting_errors.ErrPromptNotFound.Error(),
		},
		{
			err:          prompting_errors.NewInvalidDurationError("foo", "cannot parse duration"),
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid duration: cannot parse duration: "foo"`,
		},
		{
			err:          prompting_errors.NewInvalidPathPatternError("foo", "must start with '/'"),
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid path pattern: must start with '/': "foo"`,
		},
		{
			err:          prompting_errors.NewInvalidPermissionsError("home", []string{"foo"}, []string{"read", "write", "execute"}),
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid permissions for home interface: "foo"`,
		},
		{
			err:          prompting_errors.NewPermissionsEmptyError("home", []string{"read", "write", "execute"}),
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid permissions for home interface: permissions empty`,
		},
		{
			err: &prompting_errors.RequestedPathNotMatchedError{
				Requested: "foo",
				Replied:   "bar",
			},
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsReplyNotMatchRequest,
			expectedMsg:  fmt.Sprintf(`%v "foo": "bar"`, prompting_errors.ErrReplyNotMatchRequestedPath),
		},
		{
			err: &prompting_errors.RequestedPermissionsNotMatchedError{
				Requested: []string{"foo", "bar"},
				Replied:   []string{"fizz"},
			},
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsReplyNotMatchRequest,
			expectedMsg:  fmt.Sprintf(`%v [foo bar]: [fizz]`, prompting_errors.ErrReplyNotMatchRequestedPermissions),
		},
		{
			err: &prompting_errors.RuleConflictError{Conflicts: []prompting_errors.RuleConflict{
				{Permission: "read", Variant: "/path/to/conflict", ConflictingID: "DEADBEEFDEADBEEF"},
			}},
			expectedCode: 409,
			expectedKind: client.ErrorKindInterfacesRequestsRuleConflict,
			expectedMsg:  prompting_errors.ErrRuleConflict.Error(),
		},
		{
			err:          prompting_errors.ErrNewSessionRuleNoSession,
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsNewSessionRuleNoSession,
			expectedMsg:  prompting_errors.ErrNewSessionRuleNoSession.Error(),
		},
		{
			err:          prompting_errors.ErrRuleDBInconsistent,
			expectedCode: 500,
			expectedKind: client.ErrorKind(""),
			expectedMsg:  prompting_errors.ErrRuleDBInconsistent.Error(),
		},
		{
			err:          prompting_errors.ErrPromptingClosed,
			expectedCode: 503,
			expectedKind: client.ErrorKind(""),
			expectedMsg:  prompting_errors.ErrPromptingClosed.Error(),
		},
	} {
		s.manager.err = testCase.err
		req, err = http.NewRequest("POST", "/v2/interfaces/requests/prompts/0123456789ABCDEF", bytes.NewReader(validBody))
		c.Assert(err, IsNil)
		req.RemoteAddr = "pid=100;uid=1000;socket=;"
		rspe = s.errorReq(c, req, nil, actionIsExpected)
		c.Check(rspe.Status, Equals, testCase.expectedCode)
		c.Check(rspe.Kind, Equals, testCase.expectedKind)
		c.Check(rspe.Message, Equals, testCase.expectedMsg)
		s.manager.err = nil
	}
}

func mustParsePathPattern(c *C, pattern string) *patterns.PathPattern {
	parsed, err := patterns.ParsePathPattern(pattern)
	c.Assert(err, IsNil)
	return parsed
}

func (s *promptingSuite) TestGetRulesHappy(c *C) {
	s.daemon(c)

	for _, testCase := range []struct {
		vars  string
		snap  string
		iface string
	}{
		{
			"",
			"",
			"",
		},
		{
			"?snap=firefox",
			"firefox",
			"",
		},
		{
			"?interface=home",
			"",
			"home",
		},
		{
			"?snap=firefox&interface=home",
			"firefox",
			"home",
		},
	} {
		// Make sure manager is zeroed out again
		s.manager = &fakeInterfacesRequestsManager{}

		// Set the rules to return
		s.manager.rules = []*requestrules.Rule{
			{
				ID:        prompting.IDType(0xabcd),
				Timestamp: time.Now(),
				User:      1234,
				Snap:      "firefox",
				Interface: "home",
				Constraints: &prompting.RuleConstraints{
					InterfaceSpecific: &prompting.InterfaceSpecificConstraintsHome{
						Pattern: mustParsePathPattern(c, "/foo/bar"),
					},
					Permissions: prompting.RulePermissionMap{
						"write": &prompting.RulePermissionEntry{
							Outcome:  prompting.OutcomeDeny,
							Lifespan: prompting.LifespanForever,
						},
					},
				},
			},
		}

		rsp := s.makeSyncReq(c, "GET", fmt.Sprintf("/v2/interfaces/requests/rules%s", testCase.vars), 1234, nil)

		// Check parameters
		c.Check(s.manager.userID, Equals, uint32(1234))
		c.Check(s.manager.snap, Equals, testCase.snap)
		c.Check(s.manager.iface, Equals, testCase.iface)

		// Check return value
		rules, ok := rsp.Result.([]*requestrules.Rule)
		c.Check(ok, Equals, true)
		c.Check(rules, DeepEquals, s.manager.rules)
	}
}

func (s *promptingSuite) TestGetRulesReturnsEmptyList(c *C) {
	s.daemon(c)

	// Pretend manager returned nil rules
	s.manager.rules = nil
	rsp := s.makeSyncReq(c, "GET", "/v2/interfaces/requests/rules", 1234, nil)

	// Daemon remaps nil to empty slice
	rules, ok := rsp.Result.([]*requestrules.Rule)
	c.Check(ok, Equals, true)
	c.Check(rules, DeepEquals, []*requestrules.Rule{})
}

func (s *promptingSuite) TestGetRulesErrors(c *C) {
	s.daemon(c)

	// Can't parse userID
	req, err := http.NewRequest("GET", "/v2/interfaces/requests/rules", nil)
	c.Assert(err, IsNil)
	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 403)
	c.Check(rspe.Message, testutil.Contains, "cannot get remote user")

	// Prompting not running
	s.appArmorPromptingRunning = false
	req, err = http.NewRequest("GET", "/v2/interfaces/requests/rules", nil)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 500)
	c.Check(rspe.Kind, Equals, client.ErrorKindAppArmorPromptingNotRunning)
	c.Check(rspe.Message, Equals, "AppArmor Prompting is not running")
	s.appArmorPromptingRunning = true

	// Error from manager
	s.manager.err = fmt.Errorf("something went wrong")
	req, err = http.NewRequest("GET", "/v2/interfaces/requests/rules", nil)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 500)
	c.Check(rspe.Kind, Equals, client.ErrorKind(""))
	c.Check(rspe.Message, Equals, "something went wrong")
	s.manager.err = nil
}

func (s *promptingSuite) TestPostRulesAddHappy(c *C) {
	s.expectWriteAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})

	s.daemon(c)

	s.manager.rule = &requestrules.Rule{
		ID:        prompting.IDType(1234),
		Timestamp: time.Now(),
		User:      11235,
		Snap:      "firefox",
		Interface: "home",
		Constraints: &prompting.RuleConstraints{
			InterfaceSpecific: &prompting.InterfaceSpecificConstraintsHome{
				Pattern: mustParsePathPattern(c, "/foo/bar/baz"),
			},
			Permissions: prompting.RulePermissionMap{
				"write": &prompting.RulePermissionEntry{
					Outcome:  prompting.OutcomeDeny,
					Lifespan: prompting.LifespanForever,
				},
			},
		},
	}

	constraintsJSON := prompting.ConstraintsJSON{
		"path-pattern": json.RawMessage(`"/home/test/{foo,bar,baz}/**/*.{png,svg}"`),
		"permissions":  json.RawMessage(`{"read":{"outcome":"allow","lifespan":"forever"},"write":{"outcome":"allow","lifespan":"forever"}}`),
	}
	contents := &daemon.AddRuleContents{
		Snap:        "thunderbird",
		Interface:   "home",
		Constraints: constraintsJSON,
	}
	postBody := &daemon.PostRulesRequestBody{
		Action:  "add",
		AddRule: contents,
	}
	marshalled, err := json.Marshal(postBody)
	c.Assert(err, IsNil)

	rsp := s.makeSyncReq(c, "POST", "/v2/interfaces/requests/rules", 11235, marshalled)

	// Check parameters
	c.Check(s.manager.userID, Equals, uint32(11235))
	c.Check(s.manager.snap, Equals, contents.Snap)
	c.Check(s.manager.iface, Equals, contents.Interface)
	c.Check(s.manager.ruleConstraintsJSON, DeepEquals, contents.Constraints)

	// Check return value
	rule, ok := rsp.Result.(*requestrules.Rule)
	c.Check(ok, Equals, true)
	c.Check(rule, DeepEquals, s.manager.rule)
}

func (s *promptingSuite) TestPostRulesRemoveHappy(c *C) {
	s.expectWriteAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})

	s.daemon(c)

	for _, testCase := range []struct {
		snap  string
		iface string
	}{
		{
			"thunderbird",
			"",
		},
		{
			"",
			"home",
		},
		{
			"thunderbird",
			"home",
		},
	} {
		// Make sure manager is zeroed out again
		s.manager = &fakeInterfacesRequestsManager{}

		// Set the rules to return
		s.manager.rules = []*requestrules.Rule{
			{
				ID:        prompting.IDType(1234),
				Timestamp: time.Now(),
				User:      1001,
				Snap:      "thunderird",
				Interface: "home",
				Constraints: &prompting.RuleConstraints{
					InterfaceSpecific: &prompting.InterfaceSpecificConstraintsHome{
						Pattern: mustParsePathPattern(c, "/foo/bar/baz/qux"),
					},
					Permissions: prompting.RulePermissionMap{
						"write": &prompting.RulePermissionEntry{
							Outcome:  prompting.OutcomeDeny,
							Lifespan: prompting.LifespanForever,
						},
					},
				},
			},
			{
				ID:        prompting.IDType(5678),
				Timestamp: time.Now(),
				User:      1001,
				Snap:      "thunderbird",
				Interface: "home",
				Constraints: &prompting.RuleConstraints{
					InterfaceSpecific: &prompting.InterfaceSpecificConstraintsHome{
						Pattern: mustParsePathPattern(c, "/fizz/buzz"),
					},
					Permissions: prompting.RulePermissionMap{
						"read": &prompting.RulePermissionEntry{
							Outcome:  prompting.OutcomeAllow,
							Lifespan: prompting.LifespanTimespan,
						},
						"execute": &prompting.RulePermissionEntry{
							Outcome:  prompting.OutcomeAllow,
							Lifespan: prompting.LifespanTimespan,
						},
					},
				},
			},
		}

		contents := &daemon.RemoveRulesSelector{
			Snap:      testCase.snap,
			Interface: testCase.iface,
		}
		postBody := &daemon.PostRulesRequestBody{
			Action:         "remove",
			RemoveSelector: contents,
		}

		marshalled, err := json.Marshal(postBody)
		c.Assert(err, IsNil)

		rsp := s.makeSyncReq(c, "POST", "/v2/interfaces/requests/rules", 1234, marshalled)

		// Check parameters
		c.Check(s.manager.userID, Equals, uint32(1234))
		c.Check(s.manager.snap, Equals, testCase.snap)
		c.Check(s.manager.iface, Equals, testCase.iface)

		// Check return value
		rules, ok := rsp.Result.([]*requestrules.Rule)
		c.Check(ok, Equals, true)
		c.Check(rules, DeepEquals, s.manager.rules)
	}
}

func (s *promptingSuite) TestPostRulesErrors(c *C) {
	s.expectWriteAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})
	s.daemon(c)

	validAddBody := []byte(`{"action":"add","rule":{"snap":"thunderbird","interface":"home","constraints":{"path-pattern":"/home/test/{foo,bar,baz}/**/*.{png,svg}","permissions":{"read":{"outcome":"allow","lifespan":"forever"}}}}}`)
	validRemoveBody := []byte(`{"action":"remove","selector":{"snap":"thunderbird"}}`)

	// Can't parse userID
	req, err := http.NewRequest("POST", "/v2/interfaces/requests/rules", bytes.NewReader(validAddBody))
	c.Assert(err, IsNil)
	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 403)
	c.Check(rspe.Message, testutil.Contains, "cannot get remote user")

	// Prompting not running
	s.appArmorPromptingRunning = false
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules", bytes.NewReader(validAddBody))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 500)
	c.Check(rspe.Kind, Equals, client.ErrorKindAppArmorPromptingNotRunning)
	c.Check(rspe.Message, Equals, "AppArmor Prompting is not running")
	s.appArmorPromptingRunning = true

	// Bad json
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules", bytes.NewReader([]byte(`{"action":"add"`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKind(""))
	c.Check(rspe.Message, Matches, "cannot decode request body for rules endpoint:.*")

	// Decode body error
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules", bytes.NewReader([]byte(`{"action":"add","rule":"0123456789ABCDEF"}`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKind(""))
	c.Check(rspe.Message, Matches, "cannot decode request body for rules endpoint:.*cannot unmarshal string into Go struct field.*")

	// Missing "rule"
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules", bytes.NewReader([]byte(`{"action":"add"}`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsInvalidFields)
	c.Check(rspe.Message, Equals, `must include "rule" field in request body when action is "add"`)

	// Missing "snap"
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules", bytes.NewReader([]byte(`{"action":"add","rule":{"snap":"","interface":"home","constraints":{}}}`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsInvalidFields)
	c.Check(rspe.Message, Equals, `must have non-empty "snap" field`)

	// Missing "interface"
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules", bytes.NewReader([]byte(`{"action":"add","rule":{"snap":"firefox","interface":"","constraints":{}}}`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsInvalidFields)
	c.Check(rspe.Message, Equals, `must have non-empty "interface" field`)

	// Missing "selector"
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules", bytes.NewReader([]byte(`{"action":"remove"}`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsInvalidFields)
	c.Check(rspe.Message, Equals, `must include "selector" field in request body when action is "remove"`)

	// Missing "snap" and "interface"
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules", bytes.NewReader([]byte(`{"action":"remove","selector":{"snap":"","interface":""}}`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKind(""))
	c.Check(rspe.Message, Equals, `must include "snap" and/or "interface" field in "selector"`)

	// Errors from manager
	for _, testCase := range []struct {
		body         []byte
		err          error
		expectedCode int
		expectedKind client.ErrorKind
		expectedMsg  string
	}{
		{
			body:         validAddBody,
			err:          prompting_errors.NewInvalidPathPatternError("foo", "must start with '/'"),
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid path pattern: must start with '/': "foo"`,
		},
		{
			body:         validAddBody,
			err:          prompting_errors.NewInvalidPermissionsError("home", []string{"foo"}, []string{"read", "write", "execute"}),
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid permissions for home interface: "foo"`,
		},
		{
			body:         validAddBody,
			err:          prompting_errors.NewPermissionsEmptyError("home", []string{"read", "write", "execute"}),
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid permissions for home interface: permissions empty`,
		},
		{
			body: validAddBody,
			err: &prompting_errors.RuleConflictError{Conflicts: []prompting_errors.RuleConflict{
				{Permission: "read", Variant: "v1", ConflictingID: "abcd"},
			}},
			expectedCode: 409,
			expectedKind: client.ErrorKindInterfacesRequestsRuleConflict,
			expectedMsg:  prompting_errors.ErrRuleConflict.Error(),
		},
		{
			body:         validAddBody,
			err:          prompting_errors.ErrNewSessionRuleNoSession,
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsNewSessionRuleNoSession,
			expectedMsg:  prompting_errors.ErrNewSessionRuleNoSession.Error(),
		},
		{
			body:         validAddBody,
			err:          prompting_errors.ErrRuleDBInconsistent,
			expectedCode: 500,
			expectedKind: client.ErrorKind(""),
			expectedMsg:  prompting_errors.ErrRuleDBInconsistent.Error(),
		},
		{
			body:         validRemoveBody,
			err:          prompting_errors.ErrPromptingClosed,
			expectedCode: 503,
			expectedKind: client.ErrorKind(""),
			expectedMsg:  prompting_errors.ErrPromptingClosed.Error(),
		},
	} {
		s.manager.err = testCase.err
		req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules", bytes.NewReader(testCase.body))
		c.Assert(err, IsNil)
		req.RemoteAddr = "pid=100;uid=1000;socket=;"
		rspe = s.errorReq(c, req, nil, actionIsExpected)
		c.Check(rspe.Status, Equals, testCase.expectedCode)
		c.Check(rspe.Kind, Equals, testCase.expectedKind)
		c.Check(rspe.Message, Equals, testCase.expectedMsg)
		s.manager.err = nil
	}
}

func (s *promptingSuite) TestGetRuleHappy(c *C) {
	s.daemon(c)

	s.manager.rule = &requestrules.Rule{
		ID:        prompting.IDType(0x12b),
		Timestamp: time.Now(),
		User:      1005,
		Snap:      "thunderbird",
		Interface: "home",
		Constraints: &prompting.RuleConstraints{
			InterfaceSpecific: &prompting.InterfaceSpecificConstraintsHome{
				Pattern: mustParsePathPattern(c, "/home/test/Videos/**/*.{mkv,mp4,mov}"),
			},
			Permissions: prompting.RulePermissionMap{
				"read": &prompting.RulePermissionEntry{
					Outcome:    prompting.OutcomeAllow,
					Lifespan:   prompting.LifespanTimespan,
					Expiration: time.Now().Add(-24 * time.Hour),
				},
			},
		},
	}

	rsp := s.makeSyncReq(c, "GET", "/v2/interfaces/requests/rules/000000000000012B", 1005, nil)

	// Check parameters
	c.Check(s.manager.userID, Equals, uint32(1005))
	c.Check(s.manager.id, Equals, prompting.IDType(0x12b))

	// Check return value
	rule, ok := rsp.Result.(*requestrules.Rule)
	c.Check(ok, Equals, true)
	c.Check(rule, DeepEquals, s.manager.rule)
}

func (s *promptingSuite) TestGetRuleErrors(c *C) {
	s.daemon(c)

	// Can't parse userID
	req, err := http.NewRequest("GET", "/v2/interfaces/requests/rules/000000000000012B", nil)
	c.Assert(err, IsNil)
	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 403)
	c.Check(rspe.Message, testutil.Contains, "cannot get remote user")

	// Can't parse rule ID
	req, err = http.NewRequest("GET", "/v2/interfaces/requests/rules/not-a-valid-id", nil)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 404)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsRuleNotFound)

	// Prompting not running
	s.appArmorPromptingRunning = false
	req, err = http.NewRequest("GET", "/v2/interfaces/requests/rules/000000000000012B", nil)
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 500)
	c.Check(rspe.Kind, Equals, client.ErrorKindAppArmorPromptingNotRunning)
	c.Check(rspe.Message, Equals, "AppArmor Prompting is not running")
	s.appArmorPromptingRunning = true

	// Errors from manager
	for _, testCase := range []struct {
		err          error
		expectedCode int
		expectedKind client.ErrorKind
		expectedMsg  string
	}{
		{
			err:          prompting_errors.ErrRuleNotAllowed,
			expectedCode: 404,
			expectedKind: client.ErrorKindInterfacesRequestsRuleNotFound,
			expectedMsg:  prompting_errors.ErrRuleNotAllowed.Error(),
		},
		{
			err:          prompting_errors.ErrRuleNotFound,
			expectedCode: 404,
			expectedKind: client.ErrorKindInterfacesRequestsRuleNotFound,
			expectedMsg:  prompting_errors.ErrRuleNotFound.Error(),
		},
		{
			err:          prompting_errors.ErrRuleDBInconsistent,
			expectedCode: 500,
			expectedKind: client.ErrorKind(""),
			expectedMsg:  prompting_errors.ErrRuleDBInconsistent.Error(),
		},
		{
			err:          prompting_errors.ErrPromptingClosed,
			expectedCode: 503,
			expectedKind: client.ErrorKind(""),
			expectedMsg:  prompting_errors.ErrPromptingClosed.Error(),
		},
	} {
		s.manager.err = testCase.err
		req, err = http.NewRequest("GET", "/v2/interfaces/requests/rules/000000000000012B", nil)
		c.Assert(err, IsNil)
		req.RemoteAddr = "pid=100;uid=1000;socket=;"
		rspe = s.errorReq(c, req, nil, actionIsExpected)
		c.Check(rspe.Status, Equals, testCase.expectedCode)
		c.Check(rspe.Kind, Equals, testCase.expectedKind)
		c.Check(rspe.Message, Equals, testCase.expectedMsg)
		s.manager.err = nil
	}
}

func (s *promptingSuite) TestPostRulePatchHappy(c *C) {
	s.expectWriteAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})

	s.daemon(c)

	s.manager.rule = &requestrules.Rule{
		ID:        prompting.IDType(0x01123581321),
		Timestamp: time.Now(),
		User:      999,
		Snap:      "gimp",
		Interface: "home",
		Constraints: &prompting.RuleConstraints{
			InterfaceSpecific: &prompting.InterfaceSpecificConstraintsHome{
				Pattern: mustParsePathPattern(c, "/home/test/Pictures/**/*.{png,jpg}"),
			},
			Permissions: prompting.RulePermissionMap{
				"read": &prompting.RulePermissionEntry{
					Outcome:  prompting.OutcomeAllow,
					Lifespan: prompting.LifespanForever,
				},
				"write": &prompting.RulePermissionEntry{
					Outcome:  prompting.OutcomeAllow,
					Lifespan: prompting.LifespanForever,
				},
			},
		},
	}

	constraintsPatchJSON := prompting.ConstraintsJSON{
		"path-pattern": json.RawMessage(`"/home/test/Pictures/**/*.{png,jpg}"`),
		"permissions":  json.RawMessage(`{"read":{"outcome":"allow","lifespan":"forever"},"write":{"outcome":"allow","lifespan":"forever"}}`),
	}
	contents := &daemon.PatchRuleContents{
		Constraints: constraintsPatchJSON,
	}
	postBody := &daemon.PostRuleRequestBody{
		Action:    "patch",
		PatchRule: contents,
	}
	marshalled, err := json.Marshal(postBody)
	c.Assert(err, IsNil)

	rsp := s.makeSyncReq(c, "POST", "/v2/interfaces/requests/rules/0000001123581321", 999, marshalled)

	// Check parameters
	c.Check(s.manager.userID, Equals, uint32(999))
	c.Check(s.manager.constraintsPatchJSON, DeepEquals, contents.Constraints)

	// Check return value
	rule, ok := rsp.Result.(*requestrules.Rule)
	c.Check(ok, Equals, true)
	c.Check(rule, DeepEquals, s.manager.rule)
}

func (s *promptingSuite) TestPostRuleRemoveHappy(c *C) {
	s.expectWriteAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})

	s.daemon(c)

	s.manager.rule = &requestrules.Rule{
		ID:        prompting.IDType(0x01123581321),
		Timestamp: time.Now(),
		User:      100,
		Snap:      "gimp",
		Interface: "home",
		Constraints: &prompting.RuleConstraints{
			InterfaceSpecific: &prompting.InterfaceSpecificConstraintsHome{
				Pattern: mustParsePathPattern(c, "/home/test/Pictures/**/*.{png,jpg}"),
			},
			Permissions: prompting.RulePermissionMap{
				"read": &prompting.RulePermissionEntry{
					Outcome:  prompting.OutcomeAllow,
					Lifespan: prompting.LifespanForever,
				},
				"write": &prompting.RulePermissionEntry{
					Outcome:  prompting.OutcomeAllow,
					Lifespan: prompting.LifespanForever,
				},
			},
		},
	}
	postBody := &daemon.PostRuleRequestBody{
		Action: "remove",
	}
	marshalled, err := json.Marshal(postBody)
	c.Assert(err, IsNil)

	rsp := s.makeSyncReq(c, "POST", "/v2/interfaces/requests/rules/0000001123581321", 100, marshalled)

	// Check parameters
	c.Check(s.manager.userID, Equals, uint32(100))
	c.Check(s.manager.id, Equals, prompting.IDType(0x01123581321))

	// Check return value
	rule, ok := rsp.Result.(*requestrules.Rule)
	c.Check(ok, Equals, true)
	c.Check(rule, DeepEquals, s.manager.rule)
}

func (s *promptingSuite) TestPostRuleErrors(c *C) {
	s.expectWriteAccess(daemon.InterfaceOpenAccess{Interfaces: []string{"snap-interfaces-requests-control"}})
	s.daemon(c)

	validPatchBody := []byte(`{"action":"patch","rule":{"constraints":{"path-pattern":"/home/test/Pictures/**/*.{png,jpg}","permissions":{"read":{"outcome":"allow","lifespan":"forever"}}}}}`)
	validRemoveBody := []byte(`{"action":"remove"}`)

	// Can't parse userID
	req, err := http.NewRequest("POST", "/v2/interfaces/requests/rules/0000001123581321", bytes.NewReader(validPatchBody))
	c.Assert(err, IsNil)
	rspe := s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 403)
	c.Check(rspe.Message, testutil.Contains, "cannot get remote user")

	// Can't parse rule ID
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules/not-a-valid-id", bytes.NewReader(validPatchBody))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 404)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsRuleNotFound)

	// Prompting not running
	s.appArmorPromptingRunning = false
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules/0000001123581321", bytes.NewReader(validPatchBody))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 500)
	c.Check(rspe.Kind, Equals, client.ErrorKindAppArmorPromptingNotRunning)
	c.Check(rspe.Message, Equals, "AppArmor Prompting is not running")
	s.appArmorPromptingRunning = true

	// Bad json
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules/0000001123581321", bytes.NewReader([]byte(`{"action":"patch"`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKind(""))
	c.Check(rspe.Message, Matches, "cannot decode request body into request rule modification or deletion:.*")

	// Decode body error
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules/0000001123581321", bytes.NewReader([]byte(`{"action":"patch","rule":"0000001123581321"}`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKind(""))
	c.Check(rspe.Message, Matches, "cannot decode request body into request rule modification or deletion:.*cannot unmarshal string into Go struct field.*")

	// Missing "rule"
	req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules/0000001123581321", bytes.NewReader([]byte(`{"action":"patch"}`)))
	c.Assert(err, IsNil)
	req.RemoteAddr = "pid=100;uid=1000;socket=;"
	rspe = s.errorReq(c, req, nil, actionIsExpected)
	c.Check(rspe.Status, Equals, 400)
	c.Check(rspe.Kind, Equals, client.ErrorKindInterfacesRequestsInvalidFields)
	c.Check(rspe.Message, Equals, `must include "rule" field in request body when action is "patch"`)

	// Errors from manager
	for _, testCase := range []struct {
		body         []byte
		err          error
		expectedCode int
		expectedKind client.ErrorKind
		expectedMsg  string
	}{
		{
			body:         validPatchBody,
			err:          prompting_errors.NewInvalidPathPatternError("foo", "must start with '/'"),
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid path pattern: must start with '/': "foo"`,
		},
		{
			body:         validPatchBody,
			err:          prompting_errors.NewInvalidPermissionsError("home", []string{"foo"}, []string{"read", "write", "execute"}),
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid permissions for home interface: "foo"`,
		},
		{
			body:         validPatchBody,
			err:          prompting_errors.NewPermissionsEmptyError("home", []string{"read", "write", "execute"}),
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsInvalidFields,
			expectedMsg:  `invalid permissions for home interface: permissions empty`,
		},
		{
			body:         validPatchBody,
			err:          prompting_errors.ErrPatchedRuleHasNoPerms,
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsPatchedRuleHasNoPermissions,
			expectedMsg:  prompting_errors.ErrPatchedRuleHasNoPerms.Error(),
		},
		{
			body: validPatchBody,
			err: &prompting_errors.RuleConflictError{Conflicts: []prompting_errors.RuleConflict{
				{Permission: "read", Variant: "v1", ConflictingID: "abcd"},
			}},
			expectedCode: 409,
			expectedKind: client.ErrorKindInterfacesRequestsRuleConflict,
			expectedMsg:  prompting_errors.ErrRuleConflict.Error(),
		},
		{
			body:         validPatchBody,
			err:          prompting_errors.ErrNewSessionRuleNoSession,
			expectedCode: 400,
			expectedKind: client.ErrorKindInterfacesRequestsNewSessionRuleNoSession,
			expectedMsg:  prompting_errors.ErrNewSessionRuleNoSession.Error(),
		},
		{
			body:         validPatchBody,
			err:          prompting_errors.ErrRuleDBInconsistent,
			expectedCode: 500,
			expectedKind: client.ErrorKind(""),
			expectedMsg:  prompting_errors.ErrRuleDBInconsistent.Error(),
		},
		{
			body:         validPatchBody,
			err:          prompting_errors.ErrPromptingClosed,
			expectedCode: 503,
			expectedKind: client.ErrorKind(""),
			expectedMsg:  prompting_errors.ErrPromptingClosed.Error(),
		},
		{
			body:         validRemoveBody,
			err:          prompting_errors.ErrRuleNotAllowed,
			expectedCode: 404,
			expectedKind: client.ErrorKindInterfacesRequestsRuleNotFound,
			expectedMsg:  prompting_errors.ErrRuleNotAllowed.Error(),
		},
		{
			body:         validRemoveBody,
			err:          prompting_errors.ErrRuleNotFound,
			expectedCode: 404,
			expectedKind: client.ErrorKindInterfacesRequestsRuleNotFound,
			expectedMsg:  prompting_errors.ErrRuleNotFound.Error(),
		},
	} {
		s.manager.err = testCase.err
		req, err = http.NewRequest("POST", "/v2/interfaces/requests/rules/0000001123581321", bytes.NewReader(testCase.body))
		c.Assert(err, IsNil)
		req.RemoteAddr = "pid=100;uid=1000;socket=;"
		rspe = s.errorReq(c, req, nil, actionIsExpected)
		c.Check(rspe.Status, Equals, testCase.expectedCode)
		c.Check(rspe.Kind, Equals, testCase.expectedKind)
		c.Check(rspe.Message, Equals, testCase.expectedMsg)
		s.manager.err = nil
	}
}
