diff --git a/go.mod b/go.mod
index 54dade651c6ae1a26a8368418eae38278148a8df..6ebaaeedfa8b0d377ee0b3e173e7c7b7161e44c3 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	code.vereign.com/gaiax/tsa/golib v0.0.0-20220321093827-5fdf8f34aad9
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/open-policy-agent/opa v0.38.1
+	github.com/stretchr/testify v1.7.0
 	go.mongodb.org/mongo-driver v1.8.4
 	go.uber.org/zap v1.21.0
 	goa.design/goa/v3 v3.7.0
@@ -14,6 +15,7 @@ require (
 
 require (
 	github.com/OneOfOne/xxhash v1.2.8 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 // indirect
 	github.com/dimfeld/httptreemux/v5 v5.4.0 // indirect
 	github.com/ghodss/yaml v1.0.0 // indirect
@@ -27,6 +29,7 @@ require (
 	github.com/klauspost/compress v1.13.6 // indirect
 	github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect
 	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect
 	github.com/sergi/go-diff v1.2.0 // indirect
 	github.com/smartystreets/assertions v1.2.1 // indirect
@@ -47,4 +50,5 @@ require (
 	golang.org/x/tools v0.1.10 // indirect
 	golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
diff --git a/internal/regocache/regocache_test.go b/internal/regocache/regocache_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..fd63e6f1b76f2ecf9e43bf596ebfaa0d8699fb87
--- /dev/null
+++ b/internal/regocache/regocache_test.go
@@ -0,0 +1,72 @@
+package regocache_test
+
+import (
+	"context"
+	"testing"
+
+	"github.com/open-policy-agent/opa/rego"
+	"github.com/stretchr/testify/assert"
+
+	"code.vereign.com/gaiax/tsa/policy/internal/regocache"
+	"code.vereign.com/gaiax/tsa/policy/internal/service/policy"
+)
+
+const regoPolicy = `
+	package test
+
+	allow {
+		input.val == 1
+	}
+`
+
+func TestNew(t *testing.T) {
+	cache := regocache.New()
+	assert.Implements(t, (*policy.RegoCache)(nil), cache)
+}
+
+func TestCache_SetAndGet(t *testing.T) {
+	q1, err := rego.New(
+		rego.Module("filename.rego", regoPolicy),
+		rego.Query("data"),
+	).PrepareForEval(context.Background())
+	assert.NoError(t, err)
+
+	cache := regocache.New()
+	cache.Set("query1", &q1)
+
+	q2, ok := cache.Get("query1")
+	assert.True(t, ok)
+	assert.Equal(t, q1, *q2)
+}
+
+func TestCache_Purge(t *testing.T) {
+	q1, err := rego.New(
+		rego.Module("filename.rego", regoPolicy),
+		rego.Query("data"),
+	).PrepareForEval(context.Background())
+	assert.NoError(t, err)
+
+	cache := regocache.New()
+	cache.Set("query1", &q1)
+
+	cache.Purge()
+	q2, ok := cache.Get("query1")
+	assert.False(t, ok)
+	assert.Nil(t, q2)
+}
+
+func TestCache_PolicyDataChange(t *testing.T) {
+	q1, err := rego.New(
+		rego.Module("filename.rego", regoPolicy),
+		rego.Query("data"),
+	).PrepareForEval(context.Background())
+	assert.NoError(t, err)
+
+	cache := regocache.New()
+	cache.Set("query1", &q1)
+
+	cache.PolicyDataChange()
+	q2, ok := cache.Get("query1")
+	assert.False(t, ok)
+	assert.Nil(t, q2)
+}
diff --git a/internal/service/policy/policyfakes/fake_rego_cache.go b/internal/service/policy/policyfakes/fake_rego_cache.go
new file mode 100644
index 0000000000000000000000000000000000000000..4bc89b3dc24a07e9c32b4302f6cc2ea189665680
--- /dev/null
+++ b/internal/service/policy/policyfakes/fake_rego_cache.go
@@ -0,0 +1,158 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package policyfakes
+
+import (
+	"sync"
+
+	"code.vereign.com/gaiax/tsa/policy/internal/service/policy"
+	"github.com/open-policy-agent/opa/rego"
+)
+
+type FakeRegoCache struct {
+	GetStub        func(string) (*rego.PreparedEvalQuery, bool)
+	getMutex       sync.RWMutex
+	getArgsForCall []struct {
+		arg1 string
+	}
+	getReturns struct {
+		result1 *rego.PreparedEvalQuery
+		result2 bool
+	}
+	getReturnsOnCall map[int]struct {
+		result1 *rego.PreparedEvalQuery
+		result2 bool
+	}
+	SetStub        func(string, *rego.PreparedEvalQuery)
+	setMutex       sync.RWMutex
+	setArgsForCall []struct {
+		arg1 string
+		arg2 *rego.PreparedEvalQuery
+	}
+	invocations      map[string][][]interface{}
+	invocationsMutex sync.RWMutex
+}
+
+func (fake *FakeRegoCache) Get(arg1 string) (*rego.PreparedEvalQuery, bool) {
+	fake.getMutex.Lock()
+	ret, specificReturn := fake.getReturnsOnCall[len(fake.getArgsForCall)]
+	fake.getArgsForCall = append(fake.getArgsForCall, struct {
+		arg1 string
+	}{arg1})
+	stub := fake.GetStub
+	fakeReturns := fake.getReturns
+	fake.recordInvocation("Get", []interface{}{arg1})
+	fake.getMutex.Unlock()
+	if stub != nil {
+		return stub(arg1)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeRegoCache) GetCallCount() int {
+	fake.getMutex.RLock()
+	defer fake.getMutex.RUnlock()
+	return len(fake.getArgsForCall)
+}
+
+func (fake *FakeRegoCache) GetCalls(stub func(string) (*rego.PreparedEvalQuery, bool)) {
+	fake.getMutex.Lock()
+	defer fake.getMutex.Unlock()
+	fake.GetStub = stub
+}
+
+func (fake *FakeRegoCache) GetArgsForCall(i int) string {
+	fake.getMutex.RLock()
+	defer fake.getMutex.RUnlock()
+	argsForCall := fake.getArgsForCall[i]
+	return argsForCall.arg1
+}
+
+func (fake *FakeRegoCache) GetReturns(result1 *rego.PreparedEvalQuery, result2 bool) {
+	fake.getMutex.Lock()
+	defer fake.getMutex.Unlock()
+	fake.GetStub = nil
+	fake.getReturns = struct {
+		result1 *rego.PreparedEvalQuery
+		result2 bool
+	}{result1, result2}
+}
+
+func (fake *FakeRegoCache) GetReturnsOnCall(i int, result1 *rego.PreparedEvalQuery, result2 bool) {
+	fake.getMutex.Lock()
+	defer fake.getMutex.Unlock()
+	fake.GetStub = nil
+	if fake.getReturnsOnCall == nil {
+		fake.getReturnsOnCall = make(map[int]struct {
+			result1 *rego.PreparedEvalQuery
+			result2 bool
+		})
+	}
+	fake.getReturnsOnCall[i] = struct {
+		result1 *rego.PreparedEvalQuery
+		result2 bool
+	}{result1, result2}
+}
+
+func (fake *FakeRegoCache) Set(arg1 string, arg2 *rego.PreparedEvalQuery) {
+	fake.setMutex.Lock()
+	fake.setArgsForCall = append(fake.setArgsForCall, struct {
+		arg1 string
+		arg2 *rego.PreparedEvalQuery
+	}{arg1, arg2})
+	stub := fake.SetStub
+	fake.recordInvocation("Set", []interface{}{arg1, arg2})
+	fake.setMutex.Unlock()
+	if stub != nil {
+		fake.SetStub(arg1, arg2)
+	}
+}
+
+func (fake *FakeRegoCache) SetCallCount() int {
+	fake.setMutex.RLock()
+	defer fake.setMutex.RUnlock()
+	return len(fake.setArgsForCall)
+}
+
+func (fake *FakeRegoCache) SetCalls(stub func(string, *rego.PreparedEvalQuery)) {
+	fake.setMutex.Lock()
+	defer fake.setMutex.Unlock()
+	fake.SetStub = stub
+}
+
+func (fake *FakeRegoCache) SetArgsForCall(i int) (string, *rego.PreparedEvalQuery) {
+	fake.setMutex.RLock()
+	defer fake.setMutex.RUnlock()
+	argsForCall := fake.setArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2
+}
+
+func (fake *FakeRegoCache) Invocations() map[string][][]interface{} {
+	fake.invocationsMutex.RLock()
+	defer fake.invocationsMutex.RUnlock()
+	fake.getMutex.RLock()
+	defer fake.getMutex.RUnlock()
+	fake.setMutex.RLock()
+	defer fake.setMutex.RUnlock()
+	copiedInvocations := map[string][][]interface{}{}
+	for key, value := range fake.invocations {
+		copiedInvocations[key] = value
+	}
+	return copiedInvocations
+}
+
+func (fake *FakeRegoCache) recordInvocation(key string, args []interface{}) {
+	fake.invocationsMutex.Lock()
+	defer fake.invocationsMutex.Unlock()
+	if fake.invocations == nil {
+		fake.invocations = map[string][][]interface{}{}
+	}
+	if fake.invocations[key] == nil {
+		fake.invocations[key] = [][]interface{}{}
+	}
+	fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ policy.RegoCache = new(FakeRegoCache)
diff --git a/internal/service/policy/policyfakes/fake_storage.go b/internal/service/policy/policyfakes/fake_storage.go
new file mode 100644
index 0000000000000000000000000000000000000000..32bf19186b7c957b17f2c04cde9d745ee6e4edbd
--- /dev/null
+++ b/internal/service/policy/policyfakes/fake_storage.go
@@ -0,0 +1,206 @@
+// Code generated by counterfeiter. DO NOT EDIT.
+package policyfakes
+
+import (
+	"context"
+	"sync"
+
+	"code.vereign.com/gaiax/tsa/policy/internal/service/policy"
+	"code.vereign.com/gaiax/tsa/policy/internal/storage"
+)
+
+type FakeStorage struct {
+	PolicyStub        func(context.Context, string, string, string) (*storage.Policy, error)
+	policyMutex       sync.RWMutex
+	policyArgsForCall []struct {
+		arg1 context.Context
+		arg2 string
+		arg3 string
+		arg4 string
+	}
+	policyReturns struct {
+		result1 *storage.Policy
+		result2 error
+	}
+	policyReturnsOnCall map[int]struct {
+		result1 *storage.Policy
+		result2 error
+	}
+	SetPolicyLockStub        func(context.Context, string, string, string, bool) error
+	setPolicyLockMutex       sync.RWMutex
+	setPolicyLockArgsForCall []struct {
+		arg1 context.Context
+		arg2 string
+		arg3 string
+		arg4 string
+		arg5 bool
+	}
+	setPolicyLockReturns struct {
+		result1 error
+	}
+	setPolicyLockReturnsOnCall map[int]struct {
+		result1 error
+	}
+	invocations      map[string][][]interface{}
+	invocationsMutex sync.RWMutex
+}
+
+func (fake *FakeStorage) Policy(arg1 context.Context, arg2 string, arg3 string, arg4 string) (*storage.Policy, error) {
+	fake.policyMutex.Lock()
+	ret, specificReturn := fake.policyReturnsOnCall[len(fake.policyArgsForCall)]
+	fake.policyArgsForCall = append(fake.policyArgsForCall, struct {
+		arg1 context.Context
+		arg2 string
+		arg3 string
+		arg4 string
+	}{arg1, arg2, arg3, arg4})
+	stub := fake.PolicyStub
+	fakeReturns := fake.policyReturns
+	fake.recordInvocation("Policy", []interface{}{arg1, arg2, arg3, arg4})
+	fake.policyMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2, arg3, arg4)
+	}
+	if specificReturn {
+		return ret.result1, ret.result2
+	}
+	return fakeReturns.result1, fakeReturns.result2
+}
+
+func (fake *FakeStorage) PolicyCallCount() int {
+	fake.policyMutex.RLock()
+	defer fake.policyMutex.RUnlock()
+	return len(fake.policyArgsForCall)
+}
+
+func (fake *FakeStorage) PolicyCalls(stub func(context.Context, string, string, string) (*storage.Policy, error)) {
+	fake.policyMutex.Lock()
+	defer fake.policyMutex.Unlock()
+	fake.PolicyStub = stub
+}
+
+func (fake *FakeStorage) PolicyArgsForCall(i int) (context.Context, string, string, string) {
+	fake.policyMutex.RLock()
+	defer fake.policyMutex.RUnlock()
+	argsForCall := fake.policyArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4
+}
+
+func (fake *FakeStorage) PolicyReturns(result1 *storage.Policy, result2 error) {
+	fake.policyMutex.Lock()
+	defer fake.policyMutex.Unlock()
+	fake.PolicyStub = nil
+	fake.policyReturns = struct {
+		result1 *storage.Policy
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeStorage) PolicyReturnsOnCall(i int, result1 *storage.Policy, result2 error) {
+	fake.policyMutex.Lock()
+	defer fake.policyMutex.Unlock()
+	fake.PolicyStub = nil
+	if fake.policyReturnsOnCall == nil {
+		fake.policyReturnsOnCall = make(map[int]struct {
+			result1 *storage.Policy
+			result2 error
+		})
+	}
+	fake.policyReturnsOnCall[i] = struct {
+		result1 *storage.Policy
+		result2 error
+	}{result1, result2}
+}
+
+func (fake *FakeStorage) SetPolicyLock(arg1 context.Context, arg2 string, arg3 string, arg4 string, arg5 bool) error {
+	fake.setPolicyLockMutex.Lock()
+	ret, specificReturn := fake.setPolicyLockReturnsOnCall[len(fake.setPolicyLockArgsForCall)]
+	fake.setPolicyLockArgsForCall = append(fake.setPolicyLockArgsForCall, struct {
+		arg1 context.Context
+		arg2 string
+		arg3 string
+		arg4 string
+		arg5 bool
+	}{arg1, arg2, arg3, arg4, arg5})
+	stub := fake.SetPolicyLockStub
+	fakeReturns := fake.setPolicyLockReturns
+	fake.recordInvocation("SetPolicyLock", []interface{}{arg1, arg2, arg3, arg4, arg5})
+	fake.setPolicyLockMutex.Unlock()
+	if stub != nil {
+		return stub(arg1, arg2, arg3, arg4, arg5)
+	}
+	if specificReturn {
+		return ret.result1
+	}
+	return fakeReturns.result1
+}
+
+func (fake *FakeStorage) SetPolicyLockCallCount() int {
+	fake.setPolicyLockMutex.RLock()
+	defer fake.setPolicyLockMutex.RUnlock()
+	return len(fake.setPolicyLockArgsForCall)
+}
+
+func (fake *FakeStorage) SetPolicyLockCalls(stub func(context.Context, string, string, string, bool) error) {
+	fake.setPolicyLockMutex.Lock()
+	defer fake.setPolicyLockMutex.Unlock()
+	fake.SetPolicyLockStub = stub
+}
+
+func (fake *FakeStorage) SetPolicyLockArgsForCall(i int) (context.Context, string, string, string, bool) {
+	fake.setPolicyLockMutex.RLock()
+	defer fake.setPolicyLockMutex.RUnlock()
+	argsForCall := fake.setPolicyLockArgsForCall[i]
+	return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5
+}
+
+func (fake *FakeStorage) SetPolicyLockReturns(result1 error) {
+	fake.setPolicyLockMutex.Lock()
+	defer fake.setPolicyLockMutex.Unlock()
+	fake.SetPolicyLockStub = nil
+	fake.setPolicyLockReturns = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakeStorage) SetPolicyLockReturnsOnCall(i int, result1 error) {
+	fake.setPolicyLockMutex.Lock()
+	defer fake.setPolicyLockMutex.Unlock()
+	fake.SetPolicyLockStub = nil
+	if fake.setPolicyLockReturnsOnCall == nil {
+		fake.setPolicyLockReturnsOnCall = make(map[int]struct {
+			result1 error
+		})
+	}
+	fake.setPolicyLockReturnsOnCall[i] = struct {
+		result1 error
+	}{result1}
+}
+
+func (fake *FakeStorage) Invocations() map[string][][]interface{} {
+	fake.invocationsMutex.RLock()
+	defer fake.invocationsMutex.RUnlock()
+	fake.policyMutex.RLock()
+	defer fake.policyMutex.RUnlock()
+	fake.setPolicyLockMutex.RLock()
+	defer fake.setPolicyLockMutex.RUnlock()
+	copiedInvocations := map[string][][]interface{}{}
+	for key, value := range fake.invocations {
+		copiedInvocations[key] = value
+	}
+	return copiedInvocations
+}
+
+func (fake *FakeStorage) recordInvocation(key string, args []interface{}) {
+	fake.invocationsMutex.Lock()
+	defer fake.invocationsMutex.Unlock()
+	if fake.invocations == nil {
+		fake.invocations = map[string][][]interface{}{}
+	}
+	if fake.invocations[key] == nil {
+		fake.invocations[key] = [][]interface{}{}
+	}
+	fake.invocations[key] = append(fake.invocations[key], args)
+}
+
+var _ policy.Storage = new(FakeStorage)
diff --git a/internal/service/policy/service.go b/internal/service/policy/service.go
index 8c6d19ea2712a8e1af6edcb43fcbfb5de3d8999e..aff12a10b1ba8603a7d799e0cdc28fc317f63cae 100644
--- a/internal/service/policy/service.go
+++ b/internal/service/policy/service.go
@@ -12,6 +12,9 @@ import (
 	"code.vereign.com/gaiax/tsa/policy/internal/storage"
 )
 
+//go:generate counterfeiter . Storage
+//go:generate counterfeiter . RegoCache
+
 type Storage interface {
 	Policy(ctx context.Context, name, group, version string) (*storage.Policy, error)
 	SetPolicyLock(ctx context.Context, name, group, version string, lock bool) error
diff --git a/internal/service/policy/service_test.go b/internal/service/policy/service_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a5eebd2425e15d1088ff4863ebd4130dac8190c4
--- /dev/null
+++ b/internal/service/policy/service_test.go
@@ -0,0 +1,356 @@
+package policy_test
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/open-policy-agent/opa/rego"
+	"github.com/stretchr/testify/assert"
+	"go.uber.org/zap"
+
+	"code.vereign.com/gaiax/tsa/golib/errors"
+	goapolicy "code.vereign.com/gaiax/tsa/policy/gen/policy"
+	"code.vereign.com/gaiax/tsa/policy/internal/service/policy"
+	"code.vereign.com/gaiax/tsa/policy/internal/service/policy/policyfakes"
+	"code.vereign.com/gaiax/tsa/policy/internal/storage"
+)
+
+func TestNew(t *testing.T) {
+	storage := &policyfakes.FakeStorage{}
+	regocache := &policyfakes.FakeRegoCache{}
+	svc := policy.New(storage, regocache, zap.NewNop())
+	assert.Implements(t, (*goapolicy.Service)(nil), svc)
+}
+
+func TestService_Evaluate(t *testing.T) {
+	// prepare test policy source code that will be evaluated
+	testPolicy := `package testgroup.example allow { input.msg == "yes" }`
+
+	// prepare test query that can be retrieved from rego cache
+	testQuery, err := rego.New(
+		rego.Module("example.rego", testPolicy),
+		rego.Query("data.testgroup.example"),
+	).PrepareForEval(context.Background())
+	assert.NoError(t, err)
+
+	// prepare test request to be used in tests
+	testReq := func() *goapolicy.EvaluateRequest {
+		return &goapolicy.EvaluateRequest{
+			Group:      "testgroup",
+			PolicyName: "example",
+			Version:    "1.0",
+			Input:      map[string]interface{}{"msg": "yes"},
+		}
+	}
+
+	tests := []struct {
+		// test input
+		name      string
+		req       *goapolicy.EvaluateRequest
+		storage   policy.Storage
+		regocache policy.RegoCache
+
+		// expected result
+		res     *goapolicy.EvaluateResult
+		errkind errors.Kind
+		errtext string
+	}{
+		{
+			name: "prepared query is found in cache",
+			req:  testReq(),
+			regocache: &policyfakes.FakeRegoCache{
+				GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
+					q := testQuery
+					return &q, true
+				},
+			},
+			res: &goapolicy.EvaluateResult{Result: map[string]interface{}{"allow": true}},
+		},
+		{
+			name: "policy is not found",
+			req:  testReq(),
+			regocache: &policyfakes.FakeRegoCache{
+				GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
+					return nil, false
+				},
+			},
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return nil, errors.New(errors.NotFound)
+				},
+			},
+			res:     nil,
+			errkind: errors.NotFound,
+			errtext: "not found",
+		},
+		{
+			name: "error getting policy from storage",
+			req:  testReq(),
+			regocache: &policyfakes.FakeRegoCache{
+				GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
+					return nil, false
+				},
+			},
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return nil, errors.New("some error")
+				},
+			},
+			res:     nil,
+			errkind: errors.Unknown,
+			errtext: "some error",
+		},
+		{
+			name: "policy is locked",
+			req:  testReq(),
+			regocache: &policyfakes.FakeRegoCache{
+				GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
+					return nil, false
+				},
+			},
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return &storage.Policy{Locked: true}, nil
+				},
+			},
+			res:     nil,
+			errkind: errors.Forbidden,
+			errtext: "policy is locked",
+		},
+		{
+			name: "policy is found in storage and isn't locked",
+			req:  testReq(),
+			regocache: &policyfakes.FakeRegoCache{
+				GetStub: func(key string) (*rego.PreparedEvalQuery, bool) {
+					return nil, false
+				},
+			},
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return &storage.Policy{
+						Name:       "example",
+						Group:      "testgroup",
+						Version:    "1.0",
+						Rego:       testPolicy,
+						Locked:     false,
+						LastUpdate: time.Now(),
+					}, nil
+				},
+			},
+			res: &goapolicy.EvaluateResult{Result: map[string]interface{}{"allow": true}},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			svc := policy.New(test.storage, test.regocache, zap.NewNop())
+			res, err := svc.Evaluate(context.Background(), test.req)
+			if err == nil {
+				assert.Empty(t, test.errtext)
+				assert.Equal(t, test.res, res)
+			} else {
+				e, ok := err.(*errors.Error)
+				assert.True(t, ok)
+
+				assert.Contains(t, e.Error(), test.errtext)
+				assert.Equal(t, test.errkind, e.Kind)
+				assert.Equal(t, test.res, res)
+			}
+		})
+	}
+}
+
+func TestService_Lock(t *testing.T) {
+	// prepare test request to be used in tests
+	testReq := func() *goapolicy.LockRequest {
+		return &goapolicy.LockRequest{
+			Group:      "testgroup",
+			PolicyName: "example",
+			Version:    "1.0",
+		}
+	}
+
+	tests := []struct {
+		name    string
+		req     *goapolicy.LockRequest
+		storage policy.Storage
+
+		errkind errors.Kind
+		errtext string
+	}{
+		{
+			name: "policy is not found",
+			req:  testReq(),
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return nil, errors.New(errors.NotFound)
+				},
+			},
+			errkind: errors.NotFound,
+			errtext: "not found",
+		},
+		{
+			name: "error getting policy from storage",
+			req:  testReq(),
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return nil, errors.New("some error")
+				},
+			},
+			errkind: errors.Unknown,
+			errtext: "some error",
+		},
+		{
+			name: "policy is already locked",
+			req:  testReq(),
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return &storage.Policy{Locked: true}, nil
+				},
+			},
+			errkind: errors.Forbidden,
+			errtext: "policy is already locked",
+		},
+		{
+			name: "fail to lock policy",
+			req:  testReq(),
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return &storage.Policy{Locked: false}, nil
+				},
+				SetPolicyLockStub: func(ctx context.Context, name, group, version string, lock bool) error {
+					return errors.New(errors.Internal, "error locking policy")
+				},
+			},
+			errkind: errors.Internal,
+			errtext: "error locking policy",
+		},
+		{
+			name: "policy is locked successfully",
+			req:  testReq(),
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return &storage.Policy{Locked: false}, nil
+				},
+				SetPolicyLockStub: func(ctx context.Context, name, group, version string, lock bool) error {
+					return nil
+				},
+			},
+			errtext: "",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			svc := policy.New(test.storage, nil, zap.NewNop())
+			err := svc.Lock(context.Background(), test.req)
+			if err == nil {
+				assert.Empty(t, test.errtext)
+			} else {
+				e, ok := err.(*errors.Error)
+				assert.True(t, ok)
+
+				assert.Contains(t, e.Error(), test.errtext)
+				assert.Equal(t, test.errkind, e.Kind)
+			}
+		})
+	}
+}
+
+func TestService_Unlock(t *testing.T) {
+	// prepare test request to be used in tests
+	testReq := func() *goapolicy.UnlockRequest {
+		return &goapolicy.UnlockRequest{
+			Group:      "testgroup",
+			PolicyName: "example",
+			Version:    "1.0",
+		}
+	}
+
+	tests := []struct {
+		name    string
+		req     *goapolicy.UnlockRequest
+		storage policy.Storage
+
+		errkind errors.Kind
+		errtext string
+	}{
+		{
+			name: "policy is not found",
+			req:  testReq(),
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return nil, errors.New(errors.NotFound)
+				},
+			},
+			errkind: errors.NotFound,
+			errtext: "not found",
+		},
+		{
+			name: "error getting policy from storage",
+			req:  testReq(),
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return nil, errors.New("some error")
+				},
+			},
+			errkind: errors.Unknown,
+			errtext: "some error",
+		},
+		{
+			name: "policy is unlocked",
+			req:  testReq(),
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return &storage.Policy{Locked: false}, nil
+				},
+			},
+			errkind: errors.Forbidden,
+			errtext: "policy is unlocked",
+		},
+		{
+			name: "fail to unlock policy",
+			req:  testReq(),
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return &storage.Policy{Locked: true}, nil
+				},
+				SetPolicyLockStub: func(ctx context.Context, name, group, version string, lock bool) error {
+					return errors.New(errors.Internal, "error unlocking policy")
+				},
+			},
+			errkind: errors.Internal,
+			errtext: "error unlocking policy",
+		},
+		{
+			name: "policy is unlocked successfully",
+			req:  testReq(),
+			storage: &policyfakes.FakeStorage{
+				PolicyStub: func(ctx context.Context, s string, s2 string, s3 string) (*storage.Policy, error) {
+					return &storage.Policy{Locked: true}, nil
+				},
+				SetPolicyLockStub: func(ctx context.Context, name, group, version string, lock bool) error {
+					return nil
+				},
+			},
+			errtext: "",
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			svc := policy.New(test.storage, nil, zap.NewNop())
+			err := svc.Unlock(context.Background(), test.req)
+			if err == nil {
+				assert.Empty(t, test.errtext)
+			} else {
+				e, ok := err.(*errors.Error)
+				assert.True(t, ok)
+
+				assert.Contains(t, e.Error(), test.errtext)
+				assert.Equal(t, test.errkind, e.Kind)
+			}
+		})
+	}
+}
diff --git a/vendor/github.com/davecgh/go-spew/LICENSE b/vendor/github.com/davecgh/go-spew/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..bc52e96f2b0ea97cc450e2fefbbb4cc430d1ac5a
Binary files /dev/null and b/vendor/github.com/davecgh/go-spew/LICENSE differ
diff --git a/vendor/github.com/davecgh/go-spew/spew/bypass.go b/vendor/github.com/davecgh/go-spew/spew/bypass.go
new file mode 100644
index 0000000000000000000000000000000000000000..792994785e36ca74c5545a0d93a2cdecda006678
Binary files /dev/null and b/vendor/github.com/davecgh/go-spew/spew/bypass.go differ
diff --git a/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go
new file mode 100644
index 0000000000000000000000000000000000000000..205c28d68c474e4497e6aa1ce8b9fdeb260f4586
Binary files /dev/null and b/vendor/github.com/davecgh/go-spew/spew/bypasssafe.go differ
diff --git a/vendor/github.com/davecgh/go-spew/spew/common.go b/vendor/github.com/davecgh/go-spew/spew/common.go
new file mode 100644
index 0000000000000000000000000000000000000000..1be8ce9457612e02a64c01b2321d087ebd6415f2
Binary files /dev/null and b/vendor/github.com/davecgh/go-spew/spew/common.go differ
diff --git a/vendor/github.com/davecgh/go-spew/spew/config.go b/vendor/github.com/davecgh/go-spew/spew/config.go
new file mode 100644
index 0000000000000000000000000000000000000000..2e3d22f312026ff2c863bbffcbc88b7f6fb942f5
Binary files /dev/null and b/vendor/github.com/davecgh/go-spew/spew/config.go differ
diff --git a/vendor/github.com/davecgh/go-spew/spew/doc.go b/vendor/github.com/davecgh/go-spew/spew/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..aacaac6f1e1e936ee0022c00e139756c9bdc2b3e
Binary files /dev/null and b/vendor/github.com/davecgh/go-spew/spew/doc.go differ
diff --git a/vendor/github.com/davecgh/go-spew/spew/dump.go b/vendor/github.com/davecgh/go-spew/spew/dump.go
new file mode 100644
index 0000000000000000000000000000000000000000..f78d89fc1f6c454df58cd1e346817db6e30c4299
Binary files /dev/null and b/vendor/github.com/davecgh/go-spew/spew/dump.go differ
diff --git a/vendor/github.com/davecgh/go-spew/spew/format.go b/vendor/github.com/davecgh/go-spew/spew/format.go
new file mode 100644
index 0000000000000000000000000000000000000000..b04edb7d7ac278ae0b873a1335f37822a00bfd7c
Binary files /dev/null and b/vendor/github.com/davecgh/go-spew/spew/format.go differ
diff --git a/vendor/github.com/davecgh/go-spew/spew/spew.go b/vendor/github.com/davecgh/go-spew/spew/spew.go
new file mode 100644
index 0000000000000000000000000000000000000000..32c0e338825308f6b9b4d0407aa5682a23e2dc9c
Binary files /dev/null and b/vendor/github.com/davecgh/go-spew/spew/spew.go differ
diff --git a/vendor/github.com/pmezard/go-difflib/LICENSE b/vendor/github.com/pmezard/go-difflib/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..c67dad612a3dfca2b84599c640798d7be7d46728
Binary files /dev/null and b/vendor/github.com/pmezard/go-difflib/LICENSE differ
diff --git a/vendor/github.com/pmezard/go-difflib/difflib/difflib.go b/vendor/github.com/pmezard/go-difflib/difflib/difflib.go
new file mode 100644
index 0000000000000000000000000000000000000000..003e99fadb4f189565b409b9509ecf30b752d25a
Binary files /dev/null and b/vendor/github.com/pmezard/go-difflib/difflib/difflib.go differ
diff --git a/vendor/github.com/stretchr/testify/LICENSE b/vendor/github.com/stretchr/testify/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..4b0421cf9ee47908beae4b4648babb75b09ee028
Binary files /dev/null and b/vendor/github.com/stretchr/testify/LICENSE differ
diff --git a/vendor/github.com/stretchr/testify/assert/assertion_compare.go b/vendor/github.com/stretchr/testify/assert/assertion_compare.go
new file mode 100644
index 0000000000000000000000000000000000000000..41649d26792461a0e999695e0c91a15d72b5898a
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/assertion_compare.go differ
diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go
new file mode 100644
index 0000000000000000000000000000000000000000..4dfd1229a8617f401e11efa0ad461447f31c1b3e
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/assertion_format.go differ
diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go.tmpl b/vendor/github.com/stretchr/testify/assert/assertion_format.go.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..d2bb0b81778858c364f4b3694c00cdd4c72b1c5b
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/assertion_format.go.tmpl differ
diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go
new file mode 100644
index 0000000000000000000000000000000000000000..25337a6f07e6e05f3f29e5493cc2ba71cc474abb
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/assertion_forward.go differ
diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl b/vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..188bb9e174397295062da708cc9f5207e2331768
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/assertion_forward.go.tmpl differ
diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go
new file mode 100644
index 0000000000000000000000000000000000000000..1c3b47182a726afbfb1890c5119144bad1bcf8c9
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/assertion_order.go differ
diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go
new file mode 100644
index 0000000000000000000000000000000000000000..bcac4401f57fb271d4a0909e607d56d51c606e59
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/assertions.go differ
diff --git a/vendor/github.com/stretchr/testify/assert/doc.go b/vendor/github.com/stretchr/testify/assert/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..c9dccc4d6cd0aad89a9ecf638d8cde1ea043a37a
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/doc.go differ
diff --git a/vendor/github.com/stretchr/testify/assert/errors.go b/vendor/github.com/stretchr/testify/assert/errors.go
new file mode 100644
index 0000000000000000000000000000000000000000..ac9dc9d1d6156b64c31ac0b130e7a2b1ca86f06d
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/errors.go differ
diff --git a/vendor/github.com/stretchr/testify/assert/forward_assertions.go b/vendor/github.com/stretchr/testify/assert/forward_assertions.go
new file mode 100644
index 0000000000000000000000000000000000000000..df189d2348f17a3d16888e2581d2a3b7a9d47e93
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/forward_assertions.go differ
diff --git a/vendor/github.com/stretchr/testify/assert/http_assertions.go b/vendor/github.com/stretchr/testify/assert/http_assertions.go
new file mode 100644
index 0000000000000000000000000000000000000000..4ed341dd28934c102aa7a40c74ee24b6555c1db1
Binary files /dev/null and b/vendor/github.com/stretchr/testify/assert/http_assertions.go differ
diff --git a/vendor/gopkg.in/yaml.v3/LICENSE b/vendor/gopkg.in/yaml.v3/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..2683e4bb1f24c14aa2791e6d48ce0ecf3d8ab756
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/LICENSE differ
diff --git a/vendor/gopkg.in/yaml.v3/NOTICE b/vendor/gopkg.in/yaml.v3/NOTICE
new file mode 100644
index 0000000000000000000000000000000000000000..866d74a7ad79165312a2ce3904b4bdb53e6aedf7
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/NOTICE differ
diff --git a/vendor/gopkg.in/yaml.v3/README.md b/vendor/gopkg.in/yaml.v3/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..08eb1babddfac3d8f4e006448496d0e0d1f8d720
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/README.md differ
diff --git a/vendor/gopkg.in/yaml.v3/apic.go b/vendor/gopkg.in/yaml.v3/apic.go
new file mode 100644
index 0000000000000000000000000000000000000000..ae7d049f182ae2419ded608e4c763487c99dff52
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/apic.go differ
diff --git a/vendor/gopkg.in/yaml.v3/decode.go b/vendor/gopkg.in/yaml.v3/decode.go
new file mode 100644
index 0000000000000000000000000000000000000000..df36e3a30f55508515759037e072f79fc9e9e969
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/decode.go differ
diff --git a/vendor/gopkg.in/yaml.v3/emitterc.go b/vendor/gopkg.in/yaml.v3/emitterc.go
new file mode 100644
index 0000000000000000000000000000000000000000..0f47c9ca8addf8e9d2e454e02842927ae825d0e9
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/emitterc.go differ
diff --git a/vendor/gopkg.in/yaml.v3/encode.go b/vendor/gopkg.in/yaml.v3/encode.go
new file mode 100644
index 0000000000000000000000000000000000000000..de9e72a3e638d166e96ceab3d77ce59afe6e6f8a
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/encode.go differ
diff --git a/vendor/gopkg.in/yaml.v3/parserc.go b/vendor/gopkg.in/yaml.v3/parserc.go
new file mode 100644
index 0000000000000000000000000000000000000000..ac66fccc059e3837d17e2a3a1bec5b6d5c398ab1
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/parserc.go differ
diff --git a/vendor/gopkg.in/yaml.v3/readerc.go b/vendor/gopkg.in/yaml.v3/readerc.go
new file mode 100644
index 0000000000000000000000000000000000000000..b7de0a89c462af605f889bc46ce165e5d4238add
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/readerc.go differ
diff --git a/vendor/gopkg.in/yaml.v3/resolve.go b/vendor/gopkg.in/yaml.v3/resolve.go
new file mode 100644
index 0000000000000000000000000000000000000000..64ae888057a5aa24c5a3a6ca0fcb08a06269e3ad
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/resolve.go differ
diff --git a/vendor/gopkg.in/yaml.v3/scannerc.go b/vendor/gopkg.in/yaml.v3/scannerc.go
new file mode 100644
index 0000000000000000000000000000000000000000..ca0070108f4ebe6a09a222075267e0ffca996e72
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/scannerc.go differ
diff --git a/vendor/gopkg.in/yaml.v3/sorter.go b/vendor/gopkg.in/yaml.v3/sorter.go
new file mode 100644
index 0000000000000000000000000000000000000000..9210ece7e97232891625ed08c549b92c0e9bb169
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/sorter.go differ
diff --git a/vendor/gopkg.in/yaml.v3/writerc.go b/vendor/gopkg.in/yaml.v3/writerc.go
new file mode 100644
index 0000000000000000000000000000000000000000..b8a116bf9a22b9911958f44904289a8c6b482bd2
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/writerc.go differ
diff --git a/vendor/gopkg.in/yaml.v3/yaml.go b/vendor/gopkg.in/yaml.v3/yaml.go
new file mode 100644
index 0000000000000000000000000000000000000000..8cec6da48d3ec4d8858ca622383c75e359faee1f
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/yaml.go differ
diff --git a/vendor/gopkg.in/yaml.v3/yamlh.go b/vendor/gopkg.in/yaml.v3/yamlh.go
new file mode 100644
index 0000000000000000000000000000000000000000..7c6d0077061933c97979f6c84cb659b17391e1a3
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/yamlh.go differ
diff --git a/vendor/gopkg.in/yaml.v3/yamlprivateh.go b/vendor/gopkg.in/yaml.v3/yamlprivateh.go
new file mode 100644
index 0000000000000000000000000000000000000000..e88f9c54aecb54ed42665b2a08b66a4f03d999bc
Binary files /dev/null and b/vendor/gopkg.in/yaml.v3/yamlprivateh.go differ
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 7e675d11d423b11bc6b7fe469edc633414a26d2f..19d0e2d044e2b2340a82df9b249cfe7b182b10fa 100644
Binary files a/vendor/modules.txt and b/vendor/modules.txt differ