Skip to content

Daniel Reiser Signing Service Challenge #19

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 124 additions & 1 deletion signing-service-challenge-go/api/device.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,126 @@
package api

// TODO: REST endpoints ...
import (
"encoding/json"
"net/http"

"github.com/fiskaly/coding-challenges/signing-service-challenge/domain"
)

type SignatureDeviceDetails struct {
ID string `json:"id,omitempty"`
Algorithm string `json:"algorithm"`
Label string `json:"label,omitempty"`
}

type CreateSignatureDeviceRequest struct {
SignatureDeviceDetails
}

type GetSignatureDeviceResponse struct {
SignatureDeviceDetails
}

type ListSignatureDeviceResponse struct {
IDs []domain.ID `json:"ids"`
}

type SignTransactionRequest struct {
Data domain.Data `json:"data"`
}

type SignatureResponse struct {
Signature string `json:"signature"`
SignedData domain.Data `json:"signed_data"`
}

type api struct {
d domain.SignatureDomain
}

func (a *api) CreateSignatureDevice(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
panic("Create should only be called with POST")
}
var req CreateSignatureDeviceRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

err := a.d.CreateSignatureDevice(domain.Device{
ID: domain.ID(req.ID),
Algorithm: req.Algorithm,
Label: req.Label,
})
if err == domain.ErrAlreadyExists {
http.Error(w, err.Error(), http.StatusConflict)
return
}
if err == domain.ErrUnsupportedAlgorithm {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusCreated)
}

func (a *api) SignTransaction(w http.ResponseWriter, r *http.Request) {
var req SignTransactionRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

s, err := a.d.SignTransaction(domain.ID(r.PathValue("id")), req.Data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

resp := SignatureResponse{
Signature: s.Signature,
SignedData: domain.Data(s.SignedData),
}
json.NewEncoder(w).Encode(resp)
}

func (a *api) ListSignatureDevices(w http.ResponseWriter, r *http.Request) {
ids, err := a.d.ListSignatureDevices()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(ListSignatureDeviceResponse{
IDs: ids,
})
}

func (a *api) GetSignatureDeviceDetails(w http.ResponseWriter, r *http.Request) {
d, err := a.d.GetSignatureDeviceDetails(domain.ID(r.PathValue("id")))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(GetSignatureDeviceResponse{
SignatureDeviceDetails: SignatureDeviceDetails{
Algorithm: d.Algorithm,
Label: d.Label,
},
})
}

func SignatureDeviceRoutes(d *domain.SignatureDomain) *http.ServeMux {
mux := http.NewServeMux()
a := api{
d: *d,
}
mux.Handle("POST /signature-devices", http.HandlerFunc(a.CreateSignatureDevice))
mux.Handle("GET /signature-devices", http.HandlerFunc(a.ListSignatureDevices))
mux.Handle("GET /signature-devices/{id}", http.HandlerFunc(a.GetSignatureDeviceDetails))
mux.Handle("POST /signature-devices/{id}/sign-transaction", http.HandlerFunc(a.SignTransaction))
return mux
}
200 changes: 200 additions & 0 deletions signing-service-challenge-go/api/device_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package api

import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/fiskaly/coding-challenges/signing-service-challenge/domain"
)

type SignatureDomainStub struct {
CreateSignatureDeviceFunc func(domain.Device) error
ListSignatureDevicesFunc func() ([]domain.ID, error)
SignTransactionFunc func(domain.ID, domain.Data) (*domain.CreatedSignature, error)
GetSignatureDeviceDetailsFunc func(domain.ID) (domain.Device, error)
}

func (s *SignatureDomainStub) CreateSignatureDevice(d domain.Device) error {
return s.CreateSignatureDeviceFunc(d)
}

func (s *SignatureDomainStub) ListSignatureDevices() ([]domain.ID, error) {
return s.ListSignatureDevicesFunc()
}

func (s *SignatureDomainStub) SignTransaction(id domain.ID, data domain.Data) (*domain.CreatedSignature, error) {
return s.SignTransactionFunc(id, data)
}

func (s *SignatureDomainStub) GetSignatureDeviceDetails(id domain.ID) (domain.Device, error) {
return s.GetSignatureDeviceDetailsFunc(id)
}

func TestCreateSignatureDevice(t *testing.T) {
d := domain.SignatureDomain(
&SignatureDomainStub{
CreateSignatureDeviceFunc: func(d domain.Device) error {
return nil
},
})
mux := SignatureDeviceRoutes(&d)
server := httptest.NewServer(mux)
defer server.Close()

reqBody := CreateSignatureDeviceRequest{
SignatureDeviceDetails: SignatureDeviceDetails{
ID: "dev01",
Algorithm: "RSA",
Label: "Test Device",
},
}
body, _ := json.Marshal(reqBody)

resp, err := http.Post(server.URL+"/signature-devices", "application/json", bytes.NewBuffer(body))
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusCreated {
t.Fatalf("expected status %v, got %v", http.StatusCreated, resp.StatusCode)
}
}

func TestSignTransaction(t *testing.T) {
d := domain.SignatureDomain(&SignatureDomainStub{
SignTransactionFunc: func(id domain.ID, data domain.Data) (*domain.CreatedSignature, error) {
return &domain.CreatedSignature{
Signature: "signature",
SignedData: "1_" + string(data) + "last_signature",
}, nil
},
})
mux := SignatureDeviceRoutes(&d)
server := httptest.NewServer(mux)
defer server.Close()

// First, create a signature device
reqBody := CreateSignatureDeviceRequest{
SignatureDeviceDetails: SignatureDeviceDetails{
Algorithm: "RSA",
Label: "Test Device",
},
}
body, _ := json.Marshal(reqBody)
http.Post(server.URL+"/signature-devices", "application/json", bytes.NewBuffer(body))

// Now, sign a transaction
signReqBody := SignTransactionRequest{
Data: domain.Data("testdata"),
}
signBody, _ := json.Marshal(signReqBody)

resp, err := http.Post(server.URL+"/signature-devices/1/sign-transaction", "application/json", bytes.NewBuffer(signBody))
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
t.Fatalf("expected status %v, got %v", http.StatusOK, resp.StatusCode)
}

var signRespBody SignatureResponse
if err := json.NewDecoder(resp.Body).Decode(&signRespBody); err != nil {
t.Fatalf("expected no error, got %v", err)
}

expectedSignature := "signature"
if signRespBody.Signature != expectedSignature {
t.Fatalf("expected %v, got %v", expectedSignature, signRespBody.Signature)
}
}

func TestListSignatureDevices(t *testing.T) {
d := domain.SignatureDomain(&SignatureDomainStub{
ListSignatureDevicesFunc: func() ([]domain.ID, error) {
return []domain.ID{"1"}, nil
},
})
mux := SignatureDeviceRoutes(&d)
server := httptest.NewServer(mux)
defer server.Close()

// Create a signature device
reqBody := CreateSignatureDeviceRequest{
SignatureDeviceDetails: SignatureDeviceDetails{
Algorithm: "RSA",
Label: "Test Device",
},
}
body, _ := json.Marshal(reqBody)
http.Post(server.URL+"/signature-devices", "application/json", bytes.NewBuffer(body))

// List signature devices
resp, err := http.Get(server.URL + "/signature-devices")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
t.Fatalf("expected status %v, got %v", http.StatusOK, resp.StatusCode)
}

var listRespBody ListSignatureDeviceResponse
if err := json.NewDecoder(resp.Body).Decode(&listRespBody); err != nil {
t.Fatalf("expected no error, got %v", err)
}

if len(listRespBody.IDs) != 1 {
t.Fatalf("expected 1 device, got %v", len(listRespBody.IDs))
}
}

func TestGetSignatureDeviceDetails(t *testing.T) {
d := domain.SignatureDomain(&SignatureDomainStub{
GetSignatureDeviceDetailsFunc: func(domain.ID) (domain.Device, error) {
return domain.Device{
Algorithm: "RSA",
Label: "Test Device",
}, nil
},
})
mux := SignatureDeviceRoutes(&d)
server := httptest.NewServer(mux)
defer server.Close()

// Create a signature device
reqBody := CreateSignatureDeviceRequest{
SignatureDeviceDetails: SignatureDeviceDetails{
Algorithm: "RSA",
Label: "Test Device",
},
}
body, _ := json.Marshal(reqBody)
http.Post(server.URL+"/signature-devices", "application/json", bytes.NewBuffer(body))

// Get signature device details
resp, err := http.Get(server.URL + "/signature-devices/1")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
t.Fatalf("expected status %v, got %v", http.StatusOK, resp.StatusCode)
}

var getRespBody GetSignatureDeviceResponse
if err := json.NewDecoder(resp.Body).Decode(&getRespBody); err != nil {
t.Fatalf("expected no error, got %v", err)
}

if getRespBody.Algorithm != "RSA" || getRespBody.Label != "Test Device" {
t.Fatalf("expected Algorithm: RSA, Label: Test Device, got Algorithm: %v, Label: %v", getRespBody.Algorithm, getRespBody.Label)
}
}
14 changes: 8 additions & 6 deletions signing-service-challenge-go/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package api
import (
"encoding/json"
"net/http"

"github.com/fiskaly/coding-challenges/signing-service-challenge/domain"
)

// Response is the generic API response container.
Expand All @@ -17,14 +19,15 @@ type ErrorResponse struct {

// Server manages HTTP requests and dispatches them to the appropriate services.
type Server struct {
listenAddress string
listenAddress string
SignatureDomain domain.SignatureDomain
}

// NewServer is a factory to instantiate a new Server.
func NewServer(listenAddress string) *Server {
func NewServer(listenAddress string, domain domain.SignatureDomain) *Server {
return &Server{
listenAddress: listenAddress,
// TODO: add services / further dependencies here ...
listenAddress: listenAddress,
SignatureDomain: domain,
}
}

Expand All @@ -33,8 +36,7 @@ func (s *Server) Run() error {
mux := http.NewServeMux()

mux.Handle("/api/v0/health", http.HandlerFunc(s.Health))

// TODO: register further HandlerFuncs here ...
mux.Handle("/api/v0/", http.StripPrefix("/api/v0", SignatureDeviceRoutes(&s.SignatureDomain)))

return http.ListenAndServe(s.listenAddress, mux)
}
Expand Down
Loading