Skip to content
This repository has been archived by the owner on Apr 22, 2024. It is now read-only.

Commit

Permalink
Add a test to verify the IDP proxy from the configuration is being us…
Browse files Browse the repository at this point in the history
…ed (#36)
  • Loading branch information
sergicastro authored Feb 22, 2024
1 parent 127a940 commit cfe4795
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 0 deletions.
134 changes: 134 additions & 0 deletions e2e/docker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2024 Tetrate
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package e2e

import (
"fmt"
"os/exec"
"strings"
"time"
)

var (
// DockerServiceExited is a DockerServiceStatus that matches the state "Exited"
DockerServiceExited DockerServiceStatus = containsMatcher{"Exited"}
// DockerServiceContainerUp is a DockerServiceStatus that matches the state "Up"
DockerServiceContainerUp DockerServiceStatus = containsMatcher{"Up"}
// DockerServiceContainerUpAndHealthy is a DockerServiceStatus that matches the state "Up (healthy)"
DockerServiceContainerUpAndHealthy DockerServiceStatus = containsMatcher{"(healthy)"}
)

type (
// DockerCompose is a helper to interact with docker compose command
DockerCompose struct {
log func(...any)
}

// DockerComposeOption is a functional option for DockerCompose initialization
DockerComposeOption func(compose *DockerCompose)
)

// NewDockerCompose creates a new DockerCompose with the given options
func NewDockerCompose(opts ...DockerComposeOption) DockerCompose {
d := DockerCompose{}
d.log = NoopLogFunc // default
for _, opt := range opts {
opt(&d)
}
return d

}

// WithDockerComposeLogFunc sets the log function for the DockerCompose. The default is NoopLogFunc
func WithDockerComposeLogFunc(logFunc func(...any)) DockerComposeOption {
return func(compose *DockerCompose) {
compose.log = logFunc
}
}

// NoopLogFunc is a log function that does nothing
func NoopLogFunc(...any) {}

// StartDockerService starts a docker service or returns an error
func (d DockerCompose) StartDockerService(name string) error {
d.log("Starting docker service", name)
out, err := exec.Command("docker", "compose", "start", name).CombinedOutput()
if err != nil {
return fmt.Errorf("%w: %s", err, string(out))
}
return nil
}

// StopDockerService stops a docker service or returns an error
func (d DockerCompose) StopDockerService(name string) error {
d.log("Stopping docker service", name)
out, err := exec.Command("docker", "compose", "stop", name).CombinedOutput()
if err != nil {
return fmt.Errorf("%w: %s", err, string(out))
}
return nil
}

// WaitForDockerService waits for a docker service to match a status in the given timeout or returns an error
func (d DockerCompose) WaitForDockerService(name string, status DockerServiceStatus, timeout, tick time.Duration) error {
d.log("Waiting for docker service", name, "to match", status)
cmd := exec.Command("docker", "compose", "ps", "-a", "--format", "{{ .Status }}", name)

to := time.NewTimer(timeout)
tk := time.NewTicker(tick)
defer tk.Stop()
defer to.Stop()

for {
select {
case <-to.C:
return fmt.Errorf("timeout waiting for service %s to match: %s", name, status)
case <-tk.C:
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%w: %s", err, string(out))
}
if status.Match(string(out)) {
d.log("Service", name, "matched", status)
return nil
}
}
}
}

type (
// DockerServiceStatus is an interface that matches the status of a docker service
DockerServiceStatus interface {
// Match returns true if the status matches the given docker service status
Match(string) bool
// String returns a string representation of the status
String() string
}

// containsMatcher is a DockerServiceStatus that matches the status if it contains a string
containsMatcher struct {
contains string
}
)

// Match implements DockerServiceStatus
func (c containsMatcher) Match(out string) bool {
return strings.Contains(out, c.contains)
}

// String implements DockerServiceStatus
func (c containsMatcher) String() string {
return c.contains
}
32 changes: 32 additions & 0 deletions e2e/keycloak/keycloak_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,40 @@ var (
"host.docker.internal:9443": "localhost:9443", // Keycloak
"host.docker.internal:8443": "localhost:8443", // Target application
}

idpProxyService = "idp-proxy"
)

func TestOIDCUsesTheConfiguredProxy(t *testing.T) {
client, err := e2e.NewOIDCTestClient(
e2e.WithCustomCA(testCAFile),
e2e.WithLoggingOptions(t.Log, true),
e2e.WithCustomAddressMappings(customAddressMappings),
)
require.NoError(t, err)

docker := e2e.NewDockerCompose(e2e.WithDockerComposeLogFunc(t.Log))

// Stop the IDP proxy and verify that the request is rejected
require.NoError(t, docker.StopDockerService(idpProxyService))
require.NoError(t, docker.WaitForDockerService(idpProxyService, e2e.DockerServiceExited, 10*time.Second, 500*time.Millisecond))

res, err := client.Get(testURL)
require.NoError(t, err)
require.Equal(t, http.StatusForbidden, res.StatusCode)

// Start the IDP proxy and verify that the request is accepted
require.NoError(t, docker.StartDockerService(idpProxyService))
require.NoError(t, docker.WaitForDockerService(idpProxyService, e2e.DockerServiceContainerUp, 10*time.Second, 500*time.Millisecond))

res, err = client.Get(testURL)
require.NoError(t, err)
// As this is the first request with no kind of session, the client is redirected to the IdP login page.
// Assume this redirect as enough to consider the test successful and relay the details into the TestOIDC test.
require.Equal(t, http.StatusOK, res.StatusCode)
require.NoError(t, client.ParseLoginForm(res.Body, keyCloakLoginFormID))
}

func TestOIDC(t *testing.T) {
// Initialize the test OIDC client that will keep track of the state of the OIDC login process
client, err := e2e.NewOIDCTestClient(
Expand Down

0 comments on commit cfe4795

Please sign in to comment.