diff --git a/internal/service/policy/bundle_test.go b/internal/service/policy/bundle_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..663fc440256071febc113888df813707995e9a90
--- /dev/null
+++ b/internal/service/policy/bundle_test.go
@@ -0,0 +1,94 @@
+package policy
+
+import (
+	"archive/zip"
+	"bytes"
+	"encoding/json"
+	"io"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"gitlab.eclipse.org/eclipse/xfsc/tsa/policy/internal/storage"
+)
+
+// should not be modified, read only
+var testPolicy = &storage.Policy{
+	Repository: "myrepo",
+	Name:       "mypolicy",
+	Group:      "example",
+	Version:    "1.0",
+	Rego:       "package test",
+	Data:       `{"hello":"static data"}`,
+	DataConfig: `{"cfg":"static data config"}`,
+	Locked:     true,
+	LastUpdate: time.Date(2023, 11, 7, 1, 0, 0, 0, time.UTC),
+}
+
+var testMetadata = Metadata{
+	Policy: struct {
+		Name       string    `json:"name"`
+		Group      string    `json:"group"`
+		Version    string    `json:"version"`
+		Repository string    `json:"repository"`
+		Locked     bool      `json:"locked"`
+		LastUpdate time.Time `json:"lastUpdate"`
+	}{
+		Name:       "mypolicy",
+		Group:      "example",
+		Version:    "1.0",
+		Repository: "myrepo",
+		Locked:     true,
+		LastUpdate: time.Date(2023, 11, 7, 1, 0, 0, 0, time.UTC),
+	},
+}
+
+func TestPolicy_createPolicyBundle(t *testing.T) {
+	bundle, err := createPolicyBundle(testPolicy)
+	assert.NoError(t, err)
+	assert.NotNil(t, bundle)
+
+	reader, err := zip.NewReader(bytes.NewReader(bundle), int64(len(bundle)))
+	assert.NoError(t, err)
+	assert.NotNil(t, reader)
+
+	// check metadata
+	require.NotNil(t, reader.File[0])
+	require.Equal(t, "metadata.json", reader.File[0].Name)
+	metaFile, err := reader.File[0].Open()
+	require.NoError(t, err)
+
+	var meta Metadata
+	err = json.NewDecoder(metaFile).Decode(&meta)
+	require.NoError(t, err)
+	assert.Equal(t, testMetadata, meta)
+
+	// check policy source code
+	assert.NotNil(t, reader.File[1])
+	assert.Equal(t, "policy.rego", reader.File[1].Name)
+	sourceFile, err := reader.File[1].Open()
+	require.NoError(t, err)
+	source, err := io.ReadAll(sourceFile)
+	require.NoError(t, err)
+	assert.Equal(t, "package test", string(source))
+
+	// check static data
+	assert.NotNil(t, reader.File[2])
+	assert.Equal(t, "data.json", reader.File[2].Name)
+	dataFile, err := reader.File[2].Open()
+	require.NoError(t, err)
+	data, err := io.ReadAll(dataFile)
+	require.NoError(t, err)
+	assert.Equal(t, `{"hello":"static data"}`, string(data))
+
+	// check static data configuration
+	assert.NotNil(t, reader.File[3])
+	assert.Equal(t, "data-config.json", reader.File[3].Name)
+	dataConfigFile, err := reader.File[3].Open()
+	require.NoError(t, err)
+	dataConfig, err := io.ReadAll(dataConfigFile)
+	require.NoError(t, err)
+	assert.Equal(t, `{"cfg":"static data config"}`, string(dataConfig))
+}
diff --git a/internal/service/policy/service_test.go b/internal/service/policy/service_test.go
index 9a22ad40cadeb1ec14784fbc83a4087cf6fe77f8..2819b0174cf4d8d277dcd3764e5c3f04f821967f 100644
--- a/internal/service/policy/service_test.go
+++ b/internal/service/policy/service_test.go
@@ -1,14 +1,18 @@
 package policy_test
 
 import (
+	"archive/zip"
+	"bytes"
 	"context"
 	"fmt"
+	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"go.uber.org/zap"
 
 	"gitlab.eclipse.org/eclipse/xfsc/tsa/golib/errors"
@@ -881,3 +885,90 @@ func TestService_SubscribeForPolicyChange(t *testing.T) {
 		})
 	}
 }
+
+func TestService_ExportBundle(t *testing.T) {
+	t.Run("policy not found in storage", func(t *testing.T) {
+		storage := &policyfakes.FakeStorage{
+			PolicyStub: func(ctx context.Context, s string, s2 string, s3 string, s4 string) (*storage.Policy, error) {
+				return nil, errors.New(errors.NotFound, "policy not found")
+			},
+		}
+		svc := policy.New(storage, nil, nil, zap.NewNop())
+		res, reader, err := svc.ExportBundle(context.Background(), &goapolicy.ExportBundleRequest{})
+		assert.Nil(t, res)
+		assert.Nil(t, reader)
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "policy not found")
+		e, ok := err.(*errors.Error)
+		assert.True(t, ok)
+		assert.True(t, errors.Is(errors.NotFound, e))
+	})
+
+	t.Run("error getting policy from storage", func(t *testing.T) {
+		storage := &policyfakes.FakeStorage{
+			PolicyStub: func(ctx context.Context, s string, s2 string, s3 string, s4 string) (*storage.Policy, error) {
+				return nil, errors.New("unexpected error")
+			},
+		}
+		svc := policy.New(storage, nil, nil, zap.NewNop())
+		res, reader, err := svc.ExportBundle(context.Background(), &goapolicy.ExportBundleRequest{})
+		assert.Nil(t, res)
+		assert.Nil(t, reader)
+		require.Error(t, err)
+		assert.ErrorContains(t, err, "unexpected error")
+		e, ok := err.(*errors.Error)
+		assert.True(t, ok)
+		assert.True(t, errors.Is(errors.Unknown, e))
+	})
+
+	t.Run("successful export of policy bundle", func(t *testing.T) {
+		storage := &policyfakes.FakeStorage{
+			PolicyStub: func(ctx context.Context, s string, s2 string, s3 string, s4 string) (*storage.Policy, error) {
+				return &storage.Policy{
+					Repository: "myrepo",
+					Name:       "myname",
+					Group:      "mygroup",
+					Version:    "1.52",
+					Rego:       "package test",
+					Data:       `{"key":"value"}`,
+					DataConfig: `{"new":"value"}`,
+					Locked:     false,
+					LastUpdate: time.Date(2023, 10, 8, 0, 0, 0, 0, time.UTC),
+				}, nil
+			},
+		}
+		svc := policy.New(storage, nil, nil, zap.NewNop())
+		res, reader, err := svc.ExportBundle(context.Background(), &goapolicy.ExportBundleRequest{})
+		require.NoError(t, err)
+		require.NotNil(t, res)
+		require.NotNil(t, reader)
+
+		assert.Equal(t, "application/zip", res.ContentType)
+		assert.Equal(t, `attachment; filename="myrepo_mygroup_myname_1.52.zip"`, res.ContentDisposition)
+		assert.NotZero(t, res.ContentLength)
+
+		archive, err := io.ReadAll(reader)
+		require.NoError(t, err)
+		require.NotNil(t, archive)
+
+		r, err := zip.NewReader(bytes.NewReader(archive), int64(res.ContentLength))
+		require.NoError(t, err)
+		require.NotNil(t, r)
+
+		// check metadata
+		require.NotNil(t, r.File[0])
+		require.Equal(t, "metadata.json", r.File[0].Name)
+
+		// check policy source code
+		assert.NotNil(t, r.File[1])
+		assert.Equal(t, "policy.rego", r.File[1].Name)
+
+		// check static data
+		assert.NotNil(t, r.File[2])
+		assert.Equal(t, "data.json", r.File[2].Name)
+
+		// check static data configuration
+		assert.NotNil(t, r.File[3])
+		assert.Equal(t, "data-config.json", r.File[3].Name)
+	})
+}
diff --git a/vendor/github.com/stretchr/testify/require/doc.go b/vendor/github.com/stretchr/testify/require/doc.go
new file mode 100644
index 0000000000000000000000000000000000000000..968434724559c180f09d0dadb0736282796c7a0d
Binary files /dev/null and b/vendor/github.com/stretchr/testify/require/doc.go differ
diff --git a/vendor/github.com/stretchr/testify/require/forward_requirements.go b/vendor/github.com/stretchr/testify/require/forward_requirements.go
new file mode 100644
index 0000000000000000000000000000000000000000..1dcb2338c4c4627f67b6a981a6fd38a76833b20c
Binary files /dev/null and b/vendor/github.com/stretchr/testify/require/forward_requirements.go differ
diff --git a/vendor/github.com/stretchr/testify/require/require.go b/vendor/github.com/stretchr/testify/require/require.go
new file mode 100644
index 0000000000000000000000000000000000000000..63f8521476758e26809961c907c0dad8cedee517
Binary files /dev/null and b/vendor/github.com/stretchr/testify/require/require.go differ
diff --git a/vendor/github.com/stretchr/testify/require/require.go.tmpl b/vendor/github.com/stretchr/testify/require/require.go.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..55e42ddebdc45db4a80e88cf35b10b9b852e1460
Binary files /dev/null and b/vendor/github.com/stretchr/testify/require/require.go.tmpl differ
diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go b/vendor/github.com/stretchr/testify/require/require_forward.go
new file mode 100644
index 0000000000000000000000000000000000000000..3b5b09330a435238401036daf4f36d93801ac352
Binary files /dev/null and b/vendor/github.com/stretchr/testify/require/require_forward.go differ
diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl b/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl
new file mode 100644
index 0000000000000000000000000000000000000000..54124df1d3bbed65addd5df5f9c57c4ea07d4253
Binary files /dev/null and b/vendor/github.com/stretchr/testify/require/require_forward.go.tmpl differ
diff --git a/vendor/github.com/stretchr/testify/require/requirements.go b/vendor/github.com/stretchr/testify/require/requirements.go
new file mode 100644
index 0000000000000000000000000000000000000000..91772dfeb919224e3f35b7e9f13d4b866a8339f7
Binary files /dev/null and b/vendor/github.com/stretchr/testify/require/requirements.go differ
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 266b4c667d82e0104cc24fc5adf3dd2d7a9b2b8b..8468837fd6dc462c625fb2729eeac8f06d01f8db 100644
Binary files a/vendor/modules.txt and b/vendor/modules.txt differ