From 8326a0edfc64c29db9ad852ee39ad7eae043e1eb Mon Sep 17 00:00:00 2001 From: fabiante Date: Fri, 8 Dec 2023 20:49:29 +0100 Subject: [PATCH 01/11] Add GitHub Action --- .github/workflows/build.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..82b039a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,29 @@ +name: Build + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v3 + with: + go-version: 1.21 + + - name: Build + run: go build -v ./... + + - name: Install Test Runner + run: go install github.com/mfridman/tparse@latest + - name: Test + run: set -o pipefail && go test -json -v ./... | tparse -all + + - name: Lint + run: go vet -v ./... From 4c58df82a0638d14cbe935cf18c17686e18a7b54 Mon Sep 17 00:00:00 2001 From: fabiante Date: Fri, 8 Dec 2023 21:45:20 +0100 Subject: [PATCH 02/11] Add custom types: User and RequestHeader for technical constants --- user.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 user.go diff --git a/user.go b/user.go new file mode 100644 index 0000000..7fa665d --- /dev/null +++ b/user.go @@ -0,0 +1,23 @@ +package easclient + +// User is a custom type to represent one of the four EAS user instances. +type User string + +const ( + // UserAdmin is the administrator which most permissions. + UserAdmin User = "eas_administrator" + // UserKeeper is responsible for auditing, moderating, etc. + UserKeeper User = "eas_keeper" + // UserUser is the default user being able to read and write. + UserUser User = "eas_user" + // UserGuest is a guest user. + UserGuest User = "eas_guest" +) + +type RequestHeader string + +const ( + HeaderUser RequestHeader = "x-otris-eas-user" + HeaderUserFullname RequestHeader = "x-otris-eas-user-fullname" + HeaderTokens RequestHeader = "x-otris-eas-tokens" +) From 22aece4a87b7f20075b061f5453447398545e661 Mon Sep 17 00:00:00 2001 From: fabiante Date: Fri, 8 Dec 2023 21:51:27 +0100 Subject: [PATCH 03/11] Add UserClaims type --- user_claims.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 user_claims.go diff --git a/user_claims.go b/user_claims.go new file mode 100644 index 0000000..80757fa --- /dev/null +++ b/user_claims.go @@ -0,0 +1,40 @@ +package easclient + +import ( + "net/http" + "strings" +) + +// UserClaims aggregates all claims that are scoped to an EAS archive user. +type UserClaims struct { + // UserId - Technischer Identifikator des Nutzers, der im Host-System die Anfrage ausgelöst hat. + UserId string `json:"user,omitempty"` + + // UserFullname - Optionaler, vollständiger Name des Nutzers, der im Host-System die Anfrage ausgelöst hat + UserFullname string `json:"user_fullname,omitempty"` + + // Tokens - Securitytokens (Gruppenzugehörigkeit) -getrennt durch Kommata- des Nutzers, der im Host-System den Request + // ausgelöst hat. + Tokens []string `json:"tokens,omitempty"` +} + +func NewUserClaims(userId string) *UserClaims { + return &UserClaims{UserId: userId, Tokens: []string{}} +} + +// SetOnHeader sets all possible eas.RequestHeader based on this UserClaims +func (claims *UserClaims) SetOnHeader(header http.Header) { + header.Set(string(HeaderUser), claims.UserId) + + // Set optional fullname + if claims.UserFullname != "" { + header.Set(string(HeaderUserFullname), claims.UserFullname) + } + + // Set optional tokens + if len(claims.Tokens) > 0 { + header.Set(string(HeaderTokens), strings.Join(claims.Tokens, ",")) + } else { + header.Set(string(HeaderTokens), "") + } +} From 6c331900afcb002c5442a72a6f6747e70cf71172 Mon Sep 17 00:00:00 2001 From: fabiante Date: Fri, 8 Dec 2023 21:53:42 +0100 Subject: [PATCH 04/11] Add UserClaims ctx getter/setter --- user_claims.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/user_claims.go b/user_claims.go index 80757fa..a24be97 100644 --- a/user_claims.go +++ b/user_claims.go @@ -1,6 +1,7 @@ package easclient import ( + "context" "net/http" "strings" ) @@ -38,3 +39,21 @@ func (claims *UserClaims) SetOnHeader(header http.Header) { header.Set(string(HeaderTokens), "") } } + +const UserClaimsKey = "easclient-user-claims-key" + +func (claims *UserClaims) SetOnContext(ctx context.Context) context.Context { + return context.WithValue(ctx, UserClaimsKey, claims) +} + +func UserClaimsFromContext(ctx context.Context) *UserClaims { + if ctx == nil { + return nil + } + + if claims, ok := ctx.Value(UserClaimsKey).(*UserClaims); ok { + return claims + } + + return nil +} From 9515e694399ccc4a39ef14ed2636256acfa77866 Mon Sep 17 00:00:00 2001 From: fabiante Date: Fri, 8 Dec 2023 20:49:15 +0100 Subject: [PATCH 05/11] Add Client type --- store_client.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 store_client.go diff --git a/store_client.go b/store_client.go new file mode 100644 index 0000000..44b8051 --- /dev/null +++ b/store_client.go @@ -0,0 +1,30 @@ +package easclient + +import ( + "context" + "errors" + + "gopkg.in/resty.v1" +) + +type StoreClient struct { + c *resty.Client +} + +func NewStoreClient(c *resty.Client) *StoreClient { + return &StoreClient{c: c} +} + +func (c *StoreClient) newRequest(ctx context.Context) (*resty.Request, error) { + claims := UserClaimsFromContext(ctx) + if claims == nil { + return nil, errors.New("missing user claims in context object") + } + + req := c.c.NewRequest() + req.SetContext(ctx) + req.SetHeader("Accept", "application/json") + claims.SetOnHeader(req.Header) + + return req, nil +} From 1b3060e14b06701620150aadd2abbde322d3e525 Mon Sep 17 00:00:00 2001 From: fabiante Date: Fri, 8 Dec 2023 22:10:32 +0100 Subject: [PATCH 06/11] Add GetStoreStatus function as first client method --- r_get_store_status.go | 72 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 r_get_store_status.go diff --git a/r_get_store_status.go b/r_get_store_status.go new file mode 100644 index 0000000..b3d87a9 --- /dev/null +++ b/r_get_store_status.go @@ -0,0 +1,72 @@ +package easclient + +import ( + "context" + "fmt" + "time" +) + +type StoreStatus struct { + Registry struct { + AllRecords int `json:"allRecords"` + IndexedRecords int `json:"indexedRecords"` + AllAttachments int `json:"allAttachments"` + IndexedAttachments int `json:"indexedAttachments"` + } `json:"registry"` + Index struct { + Documents int `json:"documents"` + IsCurrent bool `json:"isCurrent"` + HasDeletions bool `json:"hasDeletions"` + Records int `json:"records"` + Attachments int `json:"attachments"` + } `json:"index"` + Capacity struct { + Maximum int64 `json:"maximum"` + Utilized float64 `json:"utilized"` + GrowthRate float64 `json:"growthRate"` + ExpectedEnd time.Time `json:"expectedEnd"` + Lifetime int `json:"lifetime"` + } `json:"capacity"` + Periods []struct { + Start string `json:"start"` + End string `json:"end"` + Registry struct { + AllRecords int `json:"allRecords"` + IndexedRecords int `json:"indexedRecords"` + AllAttachments int `json:"allAttachments"` + IndexedAttachments int `json:"indexedAttachments"` + } `json:"registry"` + Index struct { + Records int `json:"records"` + Attachments int `json:"attachments"` + } `json:"index"` + Capacity struct { + Utilized float64 `json:"utilized"` + } `json:"capacity"` + } `json:"periods"` +} + +func (c *StoreClient) GetStoreStatus(ctx context.Context) (*StoreStatus, error) { + req, err := c.newRequest(ctx) + if err != nil { + return nil, err + } + + type Res struct { + Status *StoreStatus `json:"status"` + } + + var result Res + + req.SetResult(&result) + res, err := req.Get("/status") + if err != nil { + return nil, err + } + + if status := res.StatusCode(); status != 200 { + return nil, fmt.Errorf("unexpected response status %v", status) + } + + return result.Status, nil +} From f67db48ba94301ade07886b166af2f9f35e878e7 Mon Sep 17 00:00:00 2001 From: fabiante Date: Fri, 8 Dec 2023 22:19:17 +0100 Subject: [PATCH 07/11] Add test init function --- .gitignore | 1 + store_client_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 store_client_test.go diff --git a/.gitignore b/.gitignore index 485dee6..de03f38 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .idea +.env diff --git a/store_client_test.go b/store_client_test.go new file mode 100644 index 0000000..d2869ce --- /dev/null +++ b/store_client_test.go @@ -0,0 +1,29 @@ +package easclient_test + +import ( + "errors" + "fmt" + "os" + + "github.com/DEXPRO-Solutions-GmbH/easclient" + "github.com/joho/godotenv" + "gopkg.in/resty.v1" +) + +var DefaultClient *easclient.StoreClient + +// init is run before any tests are executed. +// it loads the environment variables from .env and creates +// a default client for all tests to use. +func init() { + err := godotenv.Load(".env") + if errors.Is(err, os.ErrNotExist) { + return + } + + client := resty.New() + client.SetHostURL(fmt.Sprintf("http://%s/eas/archives/%s", os.Getenv("EAS_HOST"), os.Getenv("EAS_STORE"))) + client.SetBasicAuth(os.Getenv("EAS_USER"), os.Getenv("EAS_PASSWORD")) + + DefaultClient = easclient.NewStoreClient(client) +} From 531c6c9926d4c1752b7877c2dde9c788c5e97b0f Mon Sep 17 00:00:00 2001 From: fabiante Date: Fri, 8 Dec 2023 22:19:27 +0100 Subject: [PATCH 08/11] Add test case for GetStore function --- r_get_store_status_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 r_get_store_status_test.go diff --git a/r_get_store_status_test.go b/r_get_store_status_test.go new file mode 100644 index 0000000..07febfb --- /dev/null +++ b/r_get_store_status_test.go @@ -0,0 +1,19 @@ +package easclient_test + +import ( + "context" + "testing" + + "github.com/DEXPRO-Solutions-GmbH/easclient" + "github.com/stretchr/testify/require" +) + +func TestStoreClient_GetStoreStatus(t *testing.T) { + ctx := context.Background() + user := easclient.NewUserClaims("test@dexpro.de") + ctx = user.SetOnContext(ctx) + + status, err := DefaultClient.GetStoreStatus(ctx) + require.NoError(t, err) + require.NotNil(t, status) +} From cb8576eaeeec3bacdd9c1b49c4f9870428b3aa97 Mon Sep 17 00:00:00 2001 From: fabiante Date: Fri, 8 Dec 2023 20:49:08 +0100 Subject: [PATCH 09/11] Add go module --- go.mod | 16 ++++++++++++++++ go.sum | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 go.mod create mode 100644 go.sum diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..afda176 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module github.com/DEXPRO-Solutions-GmbH/easclient + +go 1.21.4 + +require ( + github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.8.4 + gopkg.in/resty.v1 v1.12.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7ec77b2 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 08748433ffc008b2586fbfec026a8a0fe422de11 Mon Sep 17 00:00:00 2001 From: fabiante Date: Fri, 8 Dec 2023 22:28:05 +0100 Subject: [PATCH 10/11] Disable tests which can't run in CI yet. --- r_get_store_status_test.go | 2 ++ store_client_test.go | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/r_get_store_status_test.go b/r_get_store_status_test.go index 07febfb..14c0494 100644 --- a/r_get_store_status_test.go +++ b/r_get_store_status_test.go @@ -9,6 +9,8 @@ import ( ) func TestStoreClient_GetStoreStatus(t *testing.T) { + testPrelude(t) + ctx := context.Background() user := easclient.NewUserClaims("test@dexpro.de") ctx = user.SetOnContext(ctx) diff --git a/store_client_test.go b/store_client_test.go index d2869ce..22f6dd8 100644 --- a/store_client_test.go +++ b/store_client_test.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "os" + "testing" "github.com/DEXPRO-Solutions-GmbH/easclient" "github.com/joho/godotenv" @@ -27,3 +28,9 @@ func init() { DefaultClient = easclient.NewStoreClient(client) } + +func testPrelude(t *testing.T) { + if os.Getenv("GITHUB_ACTION") != "" { + t.Skip("Tests can't currently be run in the GitHub Action environment. We will first have to make it possible to run the EAS or a mocked variant in CI") + } +} From 6c45e62732274a41e586dccd009a3d54dace6aaa Mon Sep 17 00:00:00 2001 From: fabiante Date: Sat, 9 Dec 2023 11:39:13 +0100 Subject: [PATCH 11/11] Add documentation about not testing EAS in CI --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index ea256ee..d941557 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,10 @@ This project implements an HTTP client for the API of the Otris EAS developed by Otris AG. + +## Testing + +Since this is an API client for a proprietary software, we have decided to not include any docker containers / images +which would allow you to test the client against a running EAS instance. + +That is why the CI pipeline skips tests which require a running EAS instance.