Repository: TBD54566975/ssi-service
Pull request #113 (2022-09-26 18:30:34)

SIP-4: DWN Routes
New DWN route for publishing a manifest, with dwn config for dwn endpoint

Input:
{{baseUrl}}/v1/dwn/manifests

```
{
  "manifestId": "WA-DL-CLASS-A"
}

```
Output
```
credentialManifest: {...}
published: true
```

<img width="681" alt="image" src="https://user-images.githubusercontent.com/5314059/192352915-ba733412-687e-418a-adbc-d9d9062d2f1c.png">


codecov-commenter: # [Codecov](https://codecov.io/gh/TBD54566975/ssi-service/pull/113?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#113](https://codecov.io/gh/TBD54566975/ssi-service/pull/113?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (dd6ae21) into [main](https://codecov.io/gh/TBD54566975/ssi-service/commit/0232fb77b82b21ebcb14c3c2e0de26bb865f3b7b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (0232fb7) will **decrease** coverage by `0.06%`.
> The diff coverage is `26.56%`.

> :exclamation: Current head dd6ae21 differs from pull request most recent head 1d4984e. Consider uploading reports for the commit 1d4984e to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #113      +/-   ##
==========================================
- Coverage   27.76%   27.69%   -0.07%     
==========================================
  Files          16       17       +1     
  Lines        1059     1123      +64     
==========================================
+ Hits          294      311      +17     
- Misses        723      769      +46     
- Partials       42       43       +1     
```


| [Impacted Files](https://codecov.io/gh/TBD54566975/ssi-service/pull/113?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [config/config.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/113/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-Y29uZmlnL2NvbmZpZy5nbw==) | `11.76% <0.00%> (-0.74%)` | :arrow_down: |
| [pkg/server/router/dwn.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/113/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvZHduLmdv) | `16.66% <16.66%> (ø)` | |
| [pkg/server/server.go](https://codecov.io/gh/TBD54566975/ssi-service/pull/113/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9zZXJ2ZXIuZ28=) | `72.09% <75.00%> (+0.29%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975)


Patch:
diff --git a/config/compose.toml b/config/compose.toml
index e2e0cd0..6597c39 100644
--- a/config/compose.toml
+++ b/config/compose.toml
@@ -30,5 +30,9 @@ methods = ["key"]
 [services.schema]
 name = "schema"
 
-[serivces.credential]
-name = "credential"
\ No newline at end of file
+[services.credential]
+name = "credential"
+
+[services.dwn]
+name = "dwn"
+dwn_endpoint = "http://localhost:4321"
diff --git a/config/config.go b/config/config.go
index d2b26fe..37a9895 100644
--- a/config/config.go
+++ b/config/config.go
@@ -53,6 +53,7 @@ type ServicesConfig struct {
 	CredentialConfig CredentialServiceConfig `toml:"credential,omitempty"`
 	KeyStoreConfig   KeyStoreServiceConfig   `toml:"keystore,omitempty"`
 	ManifestConfig   ManifestServiceConfig   `toml:"manifest,omitempty"`
+	DWNConfig        DWNServiceConfig        `toml:"dwn,omitempty"`
 }
 
 // BaseServiceConfig represents configurable properties for a specific component of the SSI Service
@@ -93,6 +94,11 @@ type ManifestServiceConfig struct {
 	*BaseServiceConfig
 }
 
+type DWNServiceConfig struct {
+	*BaseServiceConfig
+	DWNEndpoint string `toml:"dwn_endpoint"`
+}
+
 type KeyStoreServiceConfig struct {
 	*BaseServiceConfig
 	// Service key password. Used by a KDF whose key is used by a symmetric cypher for key encryption.
@@ -157,6 +163,10 @@ func LoadConfig(path string) (*SSIServiceConfig, error) {
 				BaseServiceConfig:  &BaseServiceConfig{Name: "keystore"},
 				ServiceKeyPassword: "default-password",
 			},
+			DWNConfig: DWNServiceConfig{
+				BaseServiceConfig: &BaseServiceConfig{Name: "dwn"},
+				DWNEndpoint:       "http://localhost:4321",
+			},
 		}
 	} else {
 		// load from TOML file
diff --git a/config/config.toml b/config/config.toml
index df426cb..aa50313 100644
--- a/config/config.toml
+++ b/config/config.toml
@@ -28,5 +28,5 @@ methods = ["key"]
 [services.schema]
 name = "schema"
 
-[serivces.credential]
+[services.credential]
 name = "credential"
\ No newline at end of file
diff --git a/go.mod b/go.mod
index f5bfcb7..c07bdbc 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.19
 
 require (
 	github.com/BurntSushi/toml v1.2.0
-	github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220920154030-cbf6b4665a6d
+	github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220923181557-56fc273d1dcd
 	github.com/ardanlabs/conf v1.5.0
 	github.com/dimfeld/httptreemux/v5 v5.4.0
 	github.com/go-playground/locales v0.14.0
diff --git a/go.sum b/go.sum
index dece70f..f7ee531 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,13 @@
 github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
 github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
+github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220915152202-18c69f474901 h1:7cT1hMDiWQlsQYNP2U0mxpUjc5AxKVElcAdvTt5amnM=
+github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220915152202-18c69f474901/go.mod h1:uXrbtCwqgsbZvL/zM3+DpAOuuIax9qG2aeUhZrGPCck=
 github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220919021815-57be087eca80 h1:1HBBtzkjsgjCYjMVWi6+785SO+Q7kSOOrA0Njw3JON0=
 github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220919021815-57be087eca80/go.mod h1:ZUcjj/CHvtmz48c9GapcbavlTRsN9JZ9pKdT+co/EvY=
 github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220920154030-cbf6b4665a6d h1:NW/9Qg2Z+Yhh6PtzgV/cFwgNz7Sgng3E25IYQiUV34c=
 github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220920154030-cbf6b4665a6d/go.mod h1:jkbaZT2qU+g6weOHpjPdxuHdmfOApdT8CQg41GEyZhA=
+github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220923181557-56fc273d1dcd h1:BTbS7+/QsnxvVhP0LwB3JJxnT/9qHC8msEtku2JF4QY=
+github.com/TBD54566975/ssi-sdk v0.0.1-alpha.0.20220923181557-56fc273d1dcd/go.mod h1:jkbaZT2qU+g6weOHpjPdxuHdmfOApdT8CQg41GEyZhA=
 github.com/ardanlabs/conf v1.5.0 h1:5TwP6Wu9Xi07eLFEpiCUF3oQXh9UzHMDVnD3u/I5d5c=
 github.com/ardanlabs/conf v1.5.0/go.mod h1:ILsMo9dMqYzCxDjDXTiwMI0IgxOJd0MOiucbQY2wlJw=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
diff --git a/pkg/dwn/dwn.go b/pkg/dwn/dwn.go
new file mode 100644
index 0000000..c142b76
--- /dev/null
+++ b/pkg/dwn/dwn.go
@@ -0,0 +1,55 @@
+package dwn
+
+import (
+	"bytes"
+	"github.com/TBD54566975/ssi-sdk/credential/manifest"
+	"github.com/goccy/go-json"
+	"github.com/tbd54566975/ssi-service/internal/util"
+	"io"
+	"net/http"
+)
+
+type DWNPublishManifestRequest struct {
+	Manifest manifest.CredentialManifest `json:"manifest" validate:"required"`
+}
+
+type DWNPublishManifestResponse struct {
+	Status   int    `json:"status" validate:"required"`
+	Response string `json:"response" validate:"required"`
+}
+
+// PublishManifest publishes a CredentialManifest to a DWN
+func PublishManifest(endpoint string, manifest manifest.CredentialManifest) (*DWNPublishManifestResponse, error) {
+
+	dwnReq := DWNPublishManifestRequest{Manifest: manifest}
+	postResp, err := Post(endpoint, dwnReq)
+
+	if err != nil {
+		return nil, util.LoggingErrorMsg(err, "problem with posting to dwn")
+	}
+
+	defer postResp.Body.Close()
+
+	b, _ := io.ReadAll(postResp.Body)
+	body := string(b)
+
+	return &DWNPublishManifestResponse{Status: postResp.StatusCode, Response: body}, nil
+
+}
+
+// Post does a post request with data to provided endpoint
+func Post(endpoint string, data interface{}) (*http.Response, error) {
+	// convert response payload to json
+	jsonData, err := json.Marshal(data)
+	if err != nil {
+		return nil, err
+	}
+
+	resp, err := http.Post(endpoint, "application/json", bytes.NewBuffer(jsonData))
+
+	if err != nil {
+		return nil, err
+	}
+
+	return resp, nil
+}
diff --git a/pkg/server/router/dwn.go b/pkg/server/router/dwn.go
new file mode 100644
index 0000000..8d0a3f2
--- /dev/null
+++ b/pkg/server/router/dwn.go
@@ -0,0 +1,96 @@
+package router
+
+import (
+	"context"
+	"fmt"
+	"github.com/TBD54566975/ssi-sdk/credential/manifest"
+	"github.com/tbd54566975/ssi-service/pkg/service/dwn"
+	"net/http"
+
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+
+	dwnpkg "github.com/tbd54566975/ssi-service/pkg/dwn"
+	"github.com/tbd54566975/ssi-service/pkg/server/framework"
+	svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework"
+)
+
+type DWNRouter struct {
+	service *dwn.Service
+}
+
+func NewDWNRouter(s svcframework.Service) (*DWNRouter, error) {
+	if s == nil {
+		return nil, errors.New("service cannot be nil")
+	}
+	dwnService, ok := s.(*dwn.Service)
+	if !ok {
+		return nil, fmt.Errorf("could not create dwn router with service type: %s", s.Type())
+	}
+
+	return &DWNRouter{
+		service: dwnService,
+	}, nil
+}
+
+type PublishManifestRequest struct {
+	ManifestID string `json:"manifestId" validate:"required"`
+}
+
+func (req PublishManifestRequest) ToServiceRequest() dwn.DWNPublishManifestRequest {
+	return dwn.DWNPublishManifestRequest{
+		ManifestID: req.ManifestID,
+	}
+}
+
+type PublishManifestResponse struct {
+	Manifest    manifest.CredentialManifest       `json:"manifest" validate:"required"`
+	DWNResponse dwnpkg.DWNPublishManifestResponse `json:"dwnResponse" validate:"required"`
+}
+
+// PublishManifest godoc
+// @Summary      Public Manifest to DWN
+// @Description  Public Manifest to DWN
+// @Tags         DWNAPI
+// @Accept       json
+// @Produce      json
+// @Param        request  body      PublishManifestRequest  true  "request body"
+// @Success      201      {object}  PublishManifestResponse
+// @Failure      400      {string}  string  "Bad request"
+// @Failure      500      {string}  string  "Internal server error"
+// @Router       /v1/dwn/manifest [put]
+func (dwnr DWNRouter) PublishManifest(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
+
+	if dwnr.service.Config().DWNEndpoint == "" {
+		errMsg := "could not publish manifest to dwn because dwn endpoint is not configured"
+		logrus.Error(errMsg)
+		return framework.NewRequestError(errors.New(errMsg), http.StatusInternalServerError)
+	}
+
+	var request PublishManifestRequest
+	if err := framework.Decode(r, &request); err != nil {
+		errMsg := "invalid publish manifest message request"
+		logrus.WithError(err).Error(errMsg)
+		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusBadRequest)
+	}
+
+	req := request.ToServiceRequest()
+	publishManifestResponse, err := dwnr.service.GetManifest(req)
+
+	if err != nil || &publishManifestResponse.Manifest == nil {
+		errMsg := "could not retrieve manifest"
+		logrus.WithError(err).Error(errMsg)
+		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError)
+	}
+
+	dwnResp, err := dwnpkg.PublishManifest(dwnr.service.Config().DWNEndpoint, publishManifestResponse.Manifest)
+
+	if err != nil {
+		errMsg := "could not publish manifest to DWN"
+		logrus.WithError(err).Error(errMsg)
+		return framework.NewRequestError(errors.Wrap(err, errMsg), http.StatusInternalServerError)
+	}
+
+	resp := PublishManifestResponse{Manifest: publishManifestResponse.Manifest, DWNResponse: *dwnResp}
+	return framework.Respond(ctx, w, resp, http.StatusAccepted)
+}
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 94edf36..b9f5c6d 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -30,6 +30,7 @@ const (
 	ApplicationsPrefix = "/applications"
 	ResponsesPrefix    = "/responses"
 	KeyStorePrefix     = "/keys"
+	DWNPrefix          = "/dwn"
 )
 
 // SSIServer exposes all dependencies needed to run a http server and all its services
@@ -96,6 +97,8 @@ func (s *SSIServer) instantiateRouter(service svcframework.Service) error {
 		return s.KeyStoreAPI(service)
 	case svcframework.Manifest:
 		return s.ManifestAPI(service)
+	case svcframework.DWN:
+		return s.DWNAPI(service)
 	default:
 		return fmt.Errorf("could not instantiate API for service: %s", serviceType)
 	}
@@ -170,6 +173,7 @@ func (s *SSIServer) ManifestAPI(service svcframework.Service) (err error) {
 	responsesHandlerPath := V1Prefix + ManifestsPrefix + ResponsesPrefix
 
 	s.Handle(http.MethodPut, manifestHandlerPath, manifestRouter.CreateManifest)
+
 	s.Handle(http.MethodGet, manifestHandlerPath, manifestRouter.GetManifests)
 	s.Handle(http.MethodGet, path.Join(manifestHandlerPath, "/:id"), manifestRouter.GetManifest)
 	s.Handle(http.MethodDelete, path.Join(manifestHandlerPath, "/:id"), manifestRouter.DeleteManifest)
@@ -184,3 +188,15 @@ func (s *SSIServer) ManifestAPI(service svcframework.Service) (err error) {
 	s.Handle(http.MethodDelete, path.Join(responsesHandlerPath, "/:id"), manifestRouter.DeleteResponse)
 	return
 }
+
+func (s *SSIServer) DWNAPI(service svcframework.Service) (err error) {
+	dwnRouter, err := router.NewDWNRouter(service)
+	if err != nil {
+		return util.LoggingErrorMsg(err, "could not create dwn router")
+	}
+
+	dwnPath := V1Prefix + DWNPrefix + ManifestsPrefix
+
+	s.Handle(http.MethodPut, dwnPath, dwnRouter.PublishManifest)
+	return
+}
diff --git a/pkg/service/dwn/dwn.go b/pkg/service/dwn/dwn.go
new file mode 100644
index 0000000..de11e23
--- /dev/null
+++ b/pkg/service/dwn/dwn.go
@@ -0,0 +1,61 @@
+package dwn
+
+import (
+	"fmt"
+	"github.com/sirupsen/logrus"
+	"github.com/tbd54566975/ssi-service/config"
+	"github.com/tbd54566975/ssi-service/internal/util"
+	"github.com/tbd54566975/ssi-service/pkg/service/framework"
+	manifeststorage "github.com/tbd54566975/ssi-service/pkg/service/manifest/storage"
+	"github.com/tbd54566975/ssi-service/pkg/storage"
+)
+
+type Service struct {
+	config          config.DWNServiceConfig
+	manifestStorage manifeststorage.Storage
+}
+
+func (s Service) Type() framework.Type {
+	return framework.DWN
+}
+
+func (s Service) Status() framework.Status {
+	if s.manifestStorage == nil {
+		return framework.Status{
+			Status:  framework.StatusNotReady,
+			Message: "no manifestStorage",
+		}
+	}
+
+	return framework.Status{Status: framework.StatusReady}
+}
+
+func (s Service) Config() config.DWNServiceConfig {
+	return s.config
+}
+
+func NewDWNService(config config.DWNServiceConfig, s storage.ServiceStorage) (*Service, error) {
+	manifestStorage, err := manifeststorage.NewManifestStorage(s)
+	if err != nil {
+		errMsg := "could not instantiate manifestStorage for the dwn service"
+		return nil, util.LoggingErrorMsg(err, errMsg)
+	}
+
+	return &Service{
+		config:          config,
+		manifestStorage: manifestStorage,
+	}, nil
+}
+
+func (s Service) GetManifest(request DWNPublishManifestRequest) (*DWNPublishManifestResponse, error) {
+	logrus.Debugf("getting manifest: %s", request.ManifestID)
+
+	gotManifest, err := s.manifestStorage.GetManifest(request.ManifestID)
+	if err != nil {
+		errMsg := fmt.Sprintf("could not get manifest: %s", request.ManifestID)
+		return nil, util.LoggingErrorMsg(err, errMsg)
+	}
+
+	response := DWNPublishManifestResponse{Manifest: gotManifest.Manifest}
+	return &response, nil
+}
diff --git a/pkg/service/dwn/model.go b/pkg/service/dwn/model.go
new file mode 100644
index 0000000..44ff98b
--- /dev/null
+++ b/pkg/service/dwn/model.go
@@ -0,0 +1,11 @@
+package dwn
+
+import manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest"
+
+type DWNPublishManifestRequest struct {
+	ManifestID string
+}
+
+type DWNPublishManifestResponse struct {
+	Manifest manifestsdk.CredentialManifest
+}
diff --git a/pkg/service/framework/framework.go b/pkg/service/framework/framework.go
index 416f020..3175dc8 100644
--- a/pkg/service/framework/framework.go
+++ b/pkg/service/framework/framework.go
@@ -13,6 +13,7 @@ const (
 	Credential Type = "credential"
 	KeyStore   Type = "keystore"
 	Manifest   Type = "manifest"
+	DWN        Type = "DWN"
 
 	StatusReady    StatusState = "ready"
 	StatusNotReady StatusState = "not_ready"
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 111eb89..d31a534 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -7,6 +7,7 @@ import (
 	"github.com/tbd54566975/ssi-service/internal/util"
 	"github.com/tbd54566975/ssi-service/pkg/service/credential"
 	"github.com/tbd54566975/ssi-service/pkg/service/did"
+	"github.com/tbd54566975/ssi-service/pkg/service/dwn"
 	"github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/service/manifest"
 	"github.com/tbd54566975/ssi-service/pkg/service/schema"
@@ -84,5 +85,11 @@ func instantiateServices(config config.ServicesConfig) ([]framework.Service, err
 		return nil, util.LoggingErrorMsg(err, errMsg)
 	}
 
-	return []framework.Service{didService, schemaService, credentialService, manifestService}, nil
+	dwnService, err := dwn.NewDWNService(config.DWNConfig, storageProvider)
+	if err != nil {
+		errMsg := "could not instantiate the dwn service"
+		return nil, util.LoggingErrorMsg(err, errMsg)
+	}
+
+	return []framework.Service{didService, schemaService, credentialService, manifestService, dwnService}, nil
 }


Test patch:
diff --git a/pkg/dwn/dwn_test.go b/pkg/dwn/dwn_test.go
new file mode 100644
index 0000000..c110156
--- /dev/null
+++ b/pkg/dwn/dwn_test.go
@@ -0,0 +1,58 @@
+package dwn
+
+import (
+	"github.com/TBD54566975/ssi-sdk/credential/exchange"
+	manifestsdk "github.com/TBD54566975/ssi-sdk/credential/manifest"
+	"github.com/stretchr/testify/assert"
+	"testing"
+)
+
+func TestPublishManifest(t *testing.T) {
+	t.Run("Test Publish Manifest", func(tt *testing.T) {
+
+		resp, err := PublishManifest("test-endpoint", getValidManifest())
+		assert.Nil(tt, resp)
+		assert.Error(tt, err)
+		assert.ErrorContains(tt, err, "problem with posting to dwn")
+	})
+}
+
+func getValidManifest() manifestsdk.CredentialManifest {
+
+	return manifestsdk.CredentialManifest{
+		ID:          "WA-DL-CLASS-A",
+		SpecVersion: "https://identity.foundation/credential-manifest/spec/v1.0.0/",
+		Issuer: manifestsdk.Issuer{
+			ID: "did:abc:123",
+		},
+		PresentationDefinition: &exchange.PresentationDefinition{
+			ID: "pres-def-id",
+			InputDescriptors: []exchange.InputDescriptor{
+				{
+					ID: "test-id",
+					Constraints: &exchange.Constraints{
+						Fields: []exchange.Field{
+							{
+								Path: []string{".vc.id"},
+							},
+						},
+					},
+				},
+			},
+		},
+		OutputDescriptors: []manifestsdk.OutputDescriptor{
+			{
+				ID:          "id1",
+				Schema:      "https://test.com/schema",
+				Name:        "good ID",
+				Description: "it's all good",
+			},
+			{
+				ID:          "id2",
+				Schema:      "https://test.com/schema",
+				Name:        "good ID",
+				Description: "it's all good",
+			},
+		},
+	}
+}
diff --git a/pkg/server/router/dwn_test.go b/pkg/server/router/dwn_test.go
new file mode 100644
index 0000000..44e1ef4
--- /dev/null
+++ b/pkg/server/router/dwn_test.go
@@ -0,0 +1,48 @@
+package router
+
+import (
+	"github.com/stretchr/testify/assert"
+	"github.com/tbd54566975/ssi-service/config"
+	"github.com/tbd54566975/ssi-service/pkg/service/dwn"
+	"github.com/tbd54566975/ssi-service/pkg/service/framework"
+	"github.com/tbd54566975/ssi-service/pkg/storage"
+	"os"
+	"testing"
+)
+
+func TestDWNRouter(t *testing.T) {
+	// remove the db file after the test
+	t.Cleanup(func() {
+		_ = os.Remove(storage.DBFile)
+	})
+
+	t.Run("Nil Service", func(tt *testing.T) {
+		dwnRouter, err := NewDWNRouter(nil)
+		assert.Error(tt, err)
+		assert.Empty(tt, dwnRouter)
+		assert.Contains(tt, err.Error(), "service cannot be nil")
+	})
+
+	t.Run("Bad Service", func(tt *testing.T) {
+		dwnRouter, err := NewDWNRouter(&testService{})
+		assert.Error(tt, err)
+		assert.Empty(tt, dwnRouter)
+		assert.Contains(tt, err.Error(), "could not create dwn router with service type: test")
+	})
+
+	t.Run("DWN Service Test", func(tt *testing.T) {
+		bolt, err := storage.NewBoltDB()
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, bolt)
+
+		serviceConfig := config.DWNServiceConfig{BaseServiceConfig: &config.BaseServiceConfig{Name: "dwn"}}
+		dwnService, err := dwn.NewDWNService(serviceConfig, bolt)
+		assert.NoError(tt, err)
+		assert.NotEmpty(tt, dwnService)
+
+		// check type and status
+		assert.Equal(tt, framework.DWN, dwnService.Type())
+		assert.Equal(tt, framework.StatusReady, dwnService.Status().Status)
+	})
+
+}
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index b9fccdd..f2d367b 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"fmt"
 	"github.com/TBD54566975/ssi-sdk/credential/exchange"
+	"github.com/tbd54566975/ssi-service/pkg/service/dwn"
 
 	"io"
 	"net/http"
@@ -1157,6 +1158,58 @@ func newManifestService(t *testing.T, bolt *storage.BoltDB) *router.ManifestRout
 	return manifestRouter
 }
 
+func TestDWNAPI(t *testing.T) {
+	t.Run("Test DWN Publish Manifest", func(tt *testing.T) {
+		bolt, err := storage.NewBoltDB()
+
+		// remove the db file after the test
+		tt.Cleanup(func() {
+			_ = bolt.Close()
+			_ = os.Remove(storage.DBFile)
+		})
+
+		dwnService := newDWNService(tt, bolt)
+
+		manifestService := newManifestService(tt, bolt)
+
+		w := httptest.NewRecorder()
+
+		// good request
+		createManifestRequest := getValidManifestRequest()
+
+		requestValue := newRequestValue(tt, createManifestRequest)
+		req := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/manifests", requestValue)
+		err = manifestService.CreateManifest(newRequestContext(), w, req)
+		assert.NoError(tt, err)
+
+		var resp router.CreateManifestResponse
+		err = json.NewDecoder(w.Body).Decode(&resp)
+		assert.NoError(tt, err)
+
+		w = httptest.NewRecorder()
+
+		dwnRequest := router.PublishManifestRequest{ManifestID: "WA-DL-CLASS-A"}
+		dwnRequestValue := newRequestValue(tt, dwnRequest)
+		dwnReq := httptest.NewRequest(http.MethodPut, "https://ssi-service.com/v1/dwn/manifests", dwnRequestValue)
+		err = dwnService.PublishManifest(newRequestContext(), w, dwnReq)
+
+		assert.Error(tt, err)
+		assert.ErrorContains(tt, err, "unsupported protocol scheme")
+	})
+}
+func newDWNService(t *testing.T, bolt *storage.BoltDB) *router.DWNRouter {
+	dwnService, err := dwn.NewDWNService(config.DWNServiceConfig{DWNEndpoint: "test-endpoint"}, bolt)
+	require.NoError(t, err)
+	require.NotEmpty(t, dwnService)
+
+	// create router for service
+	dwnRouter, err := router.NewDWNRouter(dwnService)
+	require.NoError(t, err)
+	require.NotEmpty(t, dwnRouter)
+
+	return dwnRouter
+}
+
 func TestKeyStoreAPI(t *testing.T) {
 	t.Run("Test Store Key", func(tt *testing.T) {
 		bolt, err := storage.NewBoltDB()