Files
mesh-catalog/modules/keycloak/cmd/keycloak-provider/moved_test.go
T

194 lines
6.9 KiB
Go

package main
import (
"context"
"encoding/json"
"os"
"path/filepath"
"reflect"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/provider"
"git.novox.be/novox/mesh-sdk/go/provider/harness"
)
const testIssuer = "https://id.example.org/realms/Novox"
// keycloak's own part of the moved path (issue 849): the shared driving of it is proven in the SDK's harness package.
// What a consumer is told is what it contributes, with the names the controller composed for it over them (issue 792).
func TestAConsumersValuesAreWhatItContributesWithItsComposedNames(t *testing.T) {
got := valuesOf(provider.Entry{Contributes: map[string]any{"label": "x", "name": "old"},
Names: map[string]string{"name": "x.example.org"}})
if want := map[string]any{"label": "x", "name": "x.example.org"}; !reflect.DeepEqual(got, want) {
t.Fatalf("%v, want %v", got, want)
}
if served := servedValues(testIssuer); served["issuer"] != testIssuer {
t.Fatalf("%v", served)
}
}
// Where the file is not there, keycloak takes the grant directory, as before; where it is, the helper.
func TestTheGrantPathIsTakenWhereTheConsumersFileIsAbsent(t *testing.T) {
dir := t.TempDir()
at := filepath.Join(dir, "consumers.json")
env := func(k string) string {
if k == "MESH_CONSUMERS_FILE" {
return at
}
return ""
}
if got := handedConsumersFile(env, os.Stat); got != "" {
t.Fatalf("took %q for a file that is not there", got)
}
if got := handedConsumersFile(func(string) string { return "" }, os.Stat); got != "" {
t.Fatalf("took %q with no environment", got)
}
if err := os.WriteFile(at, []byte("[]"), 0o600); err != nil {
t.Fatal(err)
}
if got := handedConsumersFile(env, os.Stat); got != at {
t.Fatalf("took %q, want %q", got, at)
}
}
// The grant path restarts itself when the consumers file is first filled.
func TestTheGrantPathLeavesWhenTheConsumersFileIsFilled(t *testing.T) {
at := filepath.Join(t.TempDir(), "consumers.json")
done := make(chan struct{})
go exitWhenFilled(at, os.Stat, time.Millisecond, func() { close(done) })
select {
case <-done:
t.Fatal("left before the file was there")
case <-time.After(20 * time.Millisecond):
}
if err := os.WriteFile(at, []byte("[]"), 0o600); err != nil {
t.Fatal(err)
}
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("did not leave when the file was filled")
}
}
// bucketBus is the helper's bus over in-memory buckets.
type bucketBus struct {
mu sync.Mutex
buckets map[string]map[string][]byte
}
func (b *bucketBus) Watch(_ context.Context, bucket string, handle func(string, []byte)) error {
b.mu.Lock()
defer b.mu.Unlock()
for k, v := range b.buckets[bucket] {
handle(k, v)
}
return nil
}
func (b *bucketBus) Put(_ context.Context, bucket, key string, value []byte) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.buckets[bucket] == nil {
b.buckets[bucket] = map[string][]byte{}
}
b.buckets[bucket][key] = append([]byte(nil), value...)
return nil
}
func (b *bucketBus) Delete(_ context.Context, bucket, key string) error {
b.mu.Lock()
defer b.mu.Unlock()
delete(b.buckets[bucket], key)
return nil
}
// grafanaContributes and grafanaNames are what grafana's plan entry carries for keycloak: its contribution, and the
// names the controller composed for it (issues 776, 792).
var grafanaContributes = map[string]any{"label": "grafana", "endpoint": "web", "callback": "/login/generic_oauth"}
var grafanaNames = map[string]string{"name": "grafana.example.org", "internal-name": "grafana.home.internal"}
// Against a Keycloak, both paths leave the same client: the grant file through the harness, and the consumers' file
// through the SDK's moved path with keycloak's spec (issue 849).
func TestBothPathsLeaveTheSameClientInKeycloak(t *testing.T) {
grafana := provider.ConsumerIdentity("home", "grafana")
values := map[string]any{"name": grafanaNames["name"], "internal-name": grafanaNames["internal-name"]}
for k, v := range grafanaContributes {
values[k] = v
}
now := time.Date(2026, 10, 15, 12, 0, 0, 0, time.UTC)
made := func(through func(p provisioner)) Rep {
f, o := oidcWorld(t)
through(provisioner{clients: o, announce: func(string, map[string]any) {}, log: func(string, ...any) {}})
return only(t, f, grafana)
}
viaGrant := made(func(p provisioner) {
dir := t.TempDir()
secret := filepath.Join(dir, grafana+".secret")
if err := os.WriteFile(secret, []byte("pw-"+grafana+"\n"), 0o600); err != nil {
t.Fatal(err)
}
raw, _ := json.Marshal(map[string]any{"requirement": ProvisionName, "given": []map[string]any{
{"as": grafana, "node": "home", "values": values, "secret": secret}}})
receives := filepath.Join(dir, "mesh.json")
if err := os.WriteFile(receives, raw, 0o600); err != nil {
t.Fatal(err)
}
h := &harness.Harness{Resource: ProvisionName, Receives: receives, Adapter: p,
Now: func() time.Time { return now }, Log: func(string, ...any) {}}
h.Reconcile(ctx)
})
viaFile := made(func(p provisioner) {
list, _ := json.Marshal([]provider.ConsumerCredential{{Consumer: "home", Module: "grafana",
Credential: provider.PairName("novox", ProvisionName, "home", "grafana", ""), Value: "pw-" + grafana}})
file := filepath.Join(t.TempDir(), "consumers.json")
if err := os.WriteFile(file, list, 0o600); err != nil {
t.Fatal(err)
}
h := &harness.Harness{Resource: ProvisionName, Adapter: p, Node: "novox",
Now: func() time.Time { return now }, Log: func(string, ...any) {}}
run, _, _, err := moved(file, h, testIssuer)
if err != nil {
t.Fatal(err)
}
bus := &bucketBus{buckets: map[string]map[string][]byte{}}
plan, _ := json.Marshal(map[string]any{"node": "home", "module": "grafana", "provisioning": map[string]any{
"provisions": []any{map[string]any{"provision": ProvisionName, "provider": "novox",
"provider-module": "keycloak", "contributes": grafanaContributes, "names": grafanaNames}}}})
_ = bus.Put(ctx, provider.AssignmentsBucket, "home.grafana", plan)
pr := provider.New(provider.Options{Node: "novox", Module: "keycloak", Bus: bus,
Vault: provider.ConsumersFile{Paths: []string{file}},
Offers: map[string]provider.Offer{ProvisionName: {Credential: provider.PerConsumer,
Serves: servedValues(testIssuer)}},
Make: run.Make, Gone: run.Gone, Logf: func(string, ...any) {}})
out, err := pr.Pass(ctx)
if err != nil || len(out) != 1 || out[0].Status != provider.Answered {
t.Fatalf("%+v %v", out, err)
}
})
// Keycloak's own ids differ between two runs; nothing the mesh sets does.
for _, c := range []Rep{viaGrant, viaFile} {
delete(c, "id")
raw, _ := json.Marshal(c)
var again Rep
_ = json.Unmarshal(raw, &again)
for k := range c {
delete(c, k)
}
for k, v := range again {
c[k] = v
}
if mappers, ok := c["protocolMappers"].([]any); ok {
for _, m := range mappers {
delete(m.(map[string]any), "id")
}
}
}
if !reflect.DeepEqual(viaGrant, viaFile) {
t.Fatalf("through the file %v, through the grant %v", viaFile, viaGrant)
}
}