Skip to content

Commit

Permalink
Merge pull request #1 from DEXPRO-Solutions-GmbH/feat/base-client
Browse files Browse the repository at this point in the history
Add initial StoreClient implementation including test setup
  • Loading branch information
fabiante authored Dec 9, 2023
2 parents 64f3ac5 + 6c45e62 commit ef80df9
Show file tree
Hide file tree
Showing 11 changed files with 310 additions and 0 deletions.
29 changes: 29 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -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 ./...
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.idea
.env
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
16 changes: 16 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
16 changes: 16 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
72 changes: 72 additions & 0 deletions r_get_store_status.go
Original file line number Diff line number Diff line change
@@ -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
}
21 changes: 21 additions & 0 deletions r_get_store_status_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package easclient_test

import (
"context"
"testing"

"github.com/DEXPRO-Solutions-GmbH/easclient"
"github.com/stretchr/testify/require"
)

func TestStoreClient_GetStoreStatus(t *testing.T) {
testPrelude(t)

ctx := context.Background()
user := easclient.NewUserClaims("[email protected]")
ctx = user.SetOnContext(ctx)

status, err := DefaultClient.GetStoreStatus(ctx)
require.NoError(t, err)
require.NotNil(t, status)
}
30 changes: 30 additions & 0 deletions store_client.go
Original file line number Diff line number Diff line change
@@ -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
}
36 changes: 36 additions & 0 deletions store_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package easclient_test

import (
"errors"
"fmt"
"os"
"testing"

"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)
}

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")
}
}
23 changes: 23 additions & 0 deletions user.go
Original file line number Diff line number Diff line change
@@ -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"
)
59 changes: 59 additions & 0 deletions user_claims.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package easclient

import (
"context"
"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), "")
}
}

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
}

0 comments on commit ef80df9

Please sign in to comment.