Skip to content

Commit

Permalink
HMS-2056 WIP test: add smoke test
Browse files Browse the repository at this point in the history
Currently only a very initial changes to allow smoke test for the application
from the local workstation for the developer (by extension later into the
pipeline could be added too).

This same infrastructure could be reused for integration tests which will test
the wrong scenarios for the different endpoints.

Be aware the current SetupTest method is a copy from the cmd/service/main.go
code; this is starting exactly the same as the service (and will need the same
external dependencies: database, [kafka? future maybe], [s3 storage, not our use
case]). Maybe chop and move to `internal/infrastructure/helper.go` so we reuse
from `cmd/service/main.go` and from `internal/test/smoke/main.go` files.

The intention is that calling:
  `make compose-clean clean build compose-up test-smoke`
we would run the smoke tests.

---

A similar approach reusing all the helpers could works for integration tests:
  `make compose-clean clean build compose-up test-integration`
we would run the integration tests.

---

build: update make rules

This change execute separatly tests and smoke tests; the unit tests can be
run in parallel. However the smoke tests, currently cannot run in parallel
because the service is not being configured with different ports, and that evoke
the sockets to fail on setting up.

Said that, it is not impossible to achieve that, as we can choose random ports
for service and metrics; making that, we would only need to run the tests
avoiding data overlaping.

To achieve no data overlaping the easier way is using a different org_id for
each test; in that way the database should not overlap. To implement this,
I suggest to implement a generator, which will be just a counter, starting in a
random start sequence.

Once both of the above two features are added to the smoke tests, that will
allow to run them in parallel with no collision.

---

TODO

- [x] Review current state could works for integration tests.
- [x] Update docs/dev/TESTING.md file with step by step guide (two approaches).
- [.] Implement more use cases.
  - [x] Create token.
  - [.] Register rhel-idm domain.
  - [ ] Read rhel-idm domain.
  - [ ] List rhel-idm domain.
  - [ ] Patch rhel-idm domain.
  - [ ] Update rhel-idm domain.
  - [ ] Delete rhel-idm domain.
- [x] Refactor to reuse common components: integration tests, start/stop sequences.
- [ ] Show coverage for smoke tests.
- [.] Add API builder components.
- [.] Add MODEL builder components.
- [.] Fix situation when running in the pipeline.
- [ ] Reorg the commit.

Signed-off-by: Alejandro Visiedo <[email protected]>
  • Loading branch information
avisiedo committed Nov 22, 2023
1 parent 19447cd commit e3ff46e
Show file tree
Hide file tree
Showing 22 changed files with 1,798 additions and 8 deletions.
26 changes: 25 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,29 @@ permissions:

jobs:
validate:
# https://docs.github.com/en/actions/using-containerized-services/creating-postgresql-service-containers
services:
postgres:
# https://hub.docker.com/_/postgres/
image: postgres:13
env:
POSTGRES_USERNAME: postgres
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
runs-on: "ubuntu-latest"
container: golang:1.19
env:
DATABASE_HOST: postgres
DATABASE_PORT: 5432
DATABASE_USER: postgres
DATABASE_NAME: postgres
DATABASE_PASSWORD: postgres
steps:
- uses: "actions/checkout@v4"
with:
Expand Down Expand Up @@ -52,7 +73,10 @@ jobs:
run: go vet $(go list ./... | grep -v /vendor/)

- name: Run tests
run: make test
run: |
cp -vf configs/config.ci.yaml configs/config.yaml
make db-migrate-up
make test
- name: Process coverage report
run: |
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ tmp.*/**

configs/**
!configs/config.example.yaml
!configs/config.ci.yaml
!configs/bonfire.example.yaml

# File generated when running unit tests
Expand Down
82 changes: 82 additions & 0 deletions configs/config.ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
logging:
level: info # The normal level in production
# level: trace # Will display the sql statements, usefult for development
# level: debug
# Set to false to get a json output for the log
console: true

web:
port: 8000

database:
host: localhost
port: 5432
user: postgres
password: postgres
name: postgres

kafka:
auto:
offset:
reset: latest
commit:
interval:
ms: 5000
bootstrap:
servers: localhost:9092
group:
id: idmsvc
message:
send:
max:
retries: 15
request:
timeout:
ms: 30000
required:
acks: -1
retry:
backoff:
ms: 100
timeout: 10000
topics:
- platform.idmsvc.introspect
# sasl:
# username: someusername
# passowrd: somepassword
# mechanism: somemechanism
# protocol: someprotocol

# cloudwatch:
# region:
# group:
# stream:
# key:
# secret:
# session:
# options:
# paged_rpm_inserts_limit: 100
metrics:
path: "/metrics"
port: 9000

clients:
inventory:
base_url: http://localhost:8010/api/inventory/v1

app:
# Token expiration time in seconds
# default: 2 hours
token_expiration_seconds: 7200
# The pagination default limit for the first list request
pagination_default_limit: 10
# The pagination max limit to avoid bigger values and long requests
pagination_max_limit: 100
# Allow to inject a system identity for development propose
accept_x_rh_fake_identity: false
# Validate API requests and response against the openapi specification
validate_api: true
# main secret for various MAC and encryptions like domain registration
# token and encrypted private JWKs. "random" generates an ephemeral secret.
secret: random
1 change: 1 addition & 0 deletions docs/dev/01-service-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ for the interface, it seems logic to add the implementations at:
## Adding a new `myres` resource to my public api

- Add the data model at: `internal/domain/model/myres.go`
- Add a builder for the above data model at: `internal/test/builder`
- Generate the migration scripts by: `make build && ./bin/db-tool new myres`
- Fill the migration scripts at: `scripts/db/migrations/`

Expand Down
149 changes: 149 additions & 0 deletions docs/dev/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,155 @@ FIXME It was seen that could be complicated to

See: `internal/test/client/server.go`

## Smoke tests

Every new feature we want to create a set of successful tests.
For every new resource we will create a suite test (for instance
for the token we have SuiteToken).

We create a new file at `internal/test/smoke` such as `token_test.go`.

We add a new suite test from the `SuiteBase` type:

```golang
type SuiteToken struct {
SuiteBase
}
```

SuiteBase include the logic below:

- Load the configuration.
- Start/stop the services (API and metrics).
- Generate a user and system XRHID for an arbitrary organization.

If we need to check custom responses that are not deterministic
for the given input or more complex, then add the below to use
BodyFunc field in a more comfortable way:

```golang
// BodyFuncTokenResponse is the function that wrap
type BodyFuncTokenResponse func(t *testing.T, expect *public.DomainRegTokenResponse) error

// WrapBodyFuncTokenResponse allow to implement custom body expectations for the specific type of the response.
// expected is the specific BodyFuncTokenResponse for DomainRegTokenResponse type
// Returns a BodyFunc that wrap the generic expectation function.
func WrapBodyFuncTokenResponse(expected BodyFuncTokenResponse) BodyFunc {
// To allow a generic interface for any body response type
// I have to use `body []byte`; I cannot use `any` because
// the response type is particular for the endpoint.
// That means the input to the function is not in a golang
// structure; to let the tests to be defined with less boilerplate,
// every response type would implement a wrapper function like
// this, which unmarshall the bytes, and call to the more specific
// custom body function.
if expected == nil {
return func(t *testing.T, body []byte) bool {
return len(body) == 0
}
}
return func(t *testing.T, body []byte) bool {
// Unserialize the response to the expected type
var data public.DomainRegTokenResponse
if err := json.Unmarshal(body, &data); err != nil {
require.Fail(t, fmt.Sprintf("Error unmarshalling body:\n"+
"error: %q",
err.Error(),
))
return false
}
// Run body expectetion on the unserialized data
if err := expected(t, &data); err != nil {
require.Fail(t, fmt.Sprintf("Error in body response:\n"+
"error: %q",
err.Error(),
))
return false
}
return true
}
}
```

Now you can define methods that fit `BodyFuncTokenResponse` and use
them into the BodyFunc by calling to `WrapBodyFuncTokenResponse`.

Define your suite test by adding every success request at:

```golang
func (s *SuiteTokenCreate) TestToken() {
xrhidEncoded := header.EncodeXRHID(&s.UserXRHID)

// Prepare the tests
testCases := []TestCase{
{
Name: "TestToken",
Given: TestCaseGiven{
Method: http.MethodPost,
URL: DefaultBaseURL + "/domains/token",
Header: http.Header{
"X-Rh-Insights-Request-Id": {"test_token"},
"X-Rh-Identity": {xrhidEncoded},
},
Body: public.DomainRegTokenRequest{
DomainType: "rhel-idm",
},
},
Expected: TestCaseExpect{
StatusCode: http.StatusInternalServerError,
Header: http.Header{
"X-Rh-Insights-Request-Id": {"test_token"},
"X-Rh-Identity": nil,
},
BodyFunc: WrapBodyFuncTokenResponse(s.bodyExpectationTestToken),
},
},
}

// Execute the test cases
s.RunTestCases(testCases)
}
```

The TestCase has been designed to fit integration tests too, and to
provide flexibility, different Body fields exist:

- Request:

- `Body any` as any golang struct pointer, so the request will
be serialized as a json making easier to define requests.
- `BodyBytes []byte` as an array of bytes, so we can customize
the request content (some use case could be provide
a test with a malformed json document).

- Response:

- `BodyBytes []byte` as the above, to specify a perfect match
with the response received.
- `Body any` as a golang structure that will be serialized and
compared with the array of bytes received as response.
- `BodyFunc BodyFunc` as a custom function, here we will use the
specific `Wrap...` function to remove boilerplate unserializing
the content.

- `Body []byte` as an array of bytes, so we can directly add the
exact matching with the response body.
- `Body`

Once we have defined the test case into the testCase slice, we run
all the test cases by calling to s.RunTestCases(testCases) which is
a method defined into the SuiteBase structure.

Finally add your suite test at `internal/test/smoke/suite_test.go`,
at `TestSuite` function by:

```golang
func TestSuite(t *testing.T) {
// TODO Add here your test suites
suite.Run(t, new(SuiteToken))
}
```

## References

- https://pkg.go.dev/github.com/stretchr/testify
Expand Down
31 changes: 31 additions & 0 deletions internal/test/builder/api/builder_domain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package api

import (
"github.com/google/uuid"
"github.com/openlyinc/pointy"
"github.com/podengo-project/idmsvc-backend/internal/api/public"
builder_helper "github.com/podengo-project/idmsvc-backend/internal/test/builder/helper"
)

type Domain interface {
Build() *public.Domain
}

type domain public.Domain

func NewDomain(domainName string) Domain {
domainID := uuid.New()
return &domain{
DomainId: &domainID,
AutoEnrollmentEnabled: builder_helper.GenRandPointyBool(),
Description: pointy.String(builder_helper.GenLorempIpsum()),
DomainName: domainName,
DomainType: public.RhelIdm,
Title: pointy.String(domainName),
RhelIdm: NewRhelIdmDomain(domainName).Build(),
}
}

func (b *domain) Build() *public.Domain {
return (*public.Domain)(b)
}
59 changes: 59 additions & 0 deletions internal/test/builder/api/builder_domain_rhel_idm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package api

import (
"strings"

"github.com/podengo-project/idmsvc-backend/internal/api/public"
)

type RhelIdmDomain interface {
Build() *public.DomainIpa
AddRealmDomain(value string) RhelIdmDomain
AddCaCert(value public.Certificate) RhelIdmDomain
}

type rhelIdmDomain public.DomainIpa

func NewRhelIdmDomain(domainName string) RhelIdmDomain {
data := &rhelIdmDomain{
RealmName: strings.ToUpper(domainName),
RealmDomains: []string{domainName},
CaCerts: []public.Certificate{},
Locations: []public.Location{},
Servers: []public.DomainIpaServer{},
AutomountLocations: &[]string{},
}
return data
}

func (b *rhelIdmDomain) Build() *public.DomainIpa {
return (*public.DomainIpa)(b)
}

func (b *rhelIdmDomain) AddRealmDomain(value string) RhelIdmDomain {
b.RealmDomains = append(b.RealmDomains, value)
return b
}

func (b *rhelIdmDomain) AddCaCert(value public.Certificate) RhelIdmDomain {
b.CaCerts = append(b.CaCerts, value)
return b
}

func (b *rhelIdmDomain) AddLocation(value public.Location) RhelIdmDomain {
b.Locations = append(b.Locations, value)
return b
}

func (b *rhelIdmDomain) AddServer(value public.DomainIpaServer) RhelIdmDomain {
b.Servers = append(b.Servers, value)
return b
}

func (b *rhelIdmDomain) AutomountLocation(value string) RhelIdmDomain {
if b.AutomountLocations == nil {
panic("'AutomountLocations' is nil")
}
*b.AutomountLocations = append(*b.AutomountLocations, value)
return b
}
Loading

0 comments on commit e3ff46e

Please sign in to comment.