Adds the failing unit tests for keycloak's provider on the moved path: a grant-file consumer not moved should still be made and never held for retirement, and the moved harness should read MESH_RECEIVES as the grant path does. All three fail on an assertion against today's code, which wants nothing from the grant file on this path.
144 lines
5.6 KiB
Go
144 lines
5.6 KiB
Go
package main
|
|
|
|
// Keycloak runs the moved path with the grant file still open for its consumers not moved (issue 937), fixed for
|
|
// keycloak by issue 1332: mesh-mail-web, grafana and car-hunter are not moved, so without this fix keycloak never
|
|
// made their clients and held them for retirement. These tests drive the SDK's harness.Moved exactly as main.go
|
|
// wires it, against a fake Keycloak and an in-memory bus — no real backend.
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/provider"
|
|
"git.novox.be/novox/mesh-sdk/go/provider/harness"
|
|
)
|
|
|
|
// grantGives writes a grant file (MESH_RECEIVES) naming consumers the plans do not move (issue 937): each one's
|
|
// secret file, and the json the grant path reads.
|
|
func grantGives(t *testing.T, given ...map[string]any) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
for _, g := range given {
|
|
as := g["as"].(string)
|
|
secret := filepath.Join(dir, as+".secret")
|
|
if err := os.WriteFile(secret, []byte("pw-"+as+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g["secret"] = secret
|
|
}
|
|
raw, _ := json.Marshal(map[string]any{"requirement": ProvisionName, "given": given})
|
|
at := filepath.Join(dir, "mesh.json")
|
|
if err := os.WriteFile(at, raw, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return at
|
|
}
|
|
|
|
// noOpPlan puts one assignment the bus can read that binds nothing of keycloak's, so the moved path has plans it
|
|
// could read (m.asked answers "read", issue 241) even when nothing is moved.
|
|
func noOpPlan(bus *bucketBus, node, module string) {
|
|
raw, _ := json.Marshal(map[string]any{"node": node, "module": module})
|
|
_ = bus.Put(ctx, provider.AssignmentsBucket, node+"."+module, raw)
|
|
}
|
|
|
|
// waitUntil polls cond until it is true or the timeout passes.
|
|
func waitUntil(t *testing.T, timeout time.Duration, cond func() bool) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(timeout)
|
|
for {
|
|
if cond() {
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatalf("condition never held within %s", timeout)
|
|
}
|
|
time.Sleep(2 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// movedWorld builds keycloak's moved path, as main.go wires it, against a fake Keycloak and a bucket bus. Nothing
|
|
// runs until start is called, so a test can seed the backend first; it is stopped when the test ends.
|
|
func movedWorld(t *testing.T, h *harness.Harness) (f *fakeKeycloak, start func()) {
|
|
t.Helper()
|
|
var o OidcClients
|
|
f, o = oidcWorld(t)
|
|
h.Adapter = provisioner{clients: o, announce: func(string, map[string]any) {}, log: func(string, ...any) {}}
|
|
if h.Every == 0 {
|
|
h.Every = 5 * time.Millisecond
|
|
}
|
|
consumersFile := filepath.Join(t.TempDir(), "consumers.json")
|
|
if err := os.WriteFile(consumersFile, []byte("[]"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
run, _, _, err := moved(consumersFile, h, testIssuer)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bus := &bucketBus{buckets: map[string]map[string][]byte{}}
|
|
noOpPlan(bus, "elsewhere", "unrelated")
|
|
p := provider.New(provider.Options{Node: h.Node, Module: "keycloak", Bus: bus,
|
|
Vault: provider.ConsumersFile{Paths: []string{consumersFile}},
|
|
Offers: map[string]provider.Offer{ProvisionName: {Credential: provider.PerConsumer, Serves: servedValues(testIssuer)}},
|
|
Make: run.Make, Gone: run.Gone, Every: h.Every, Logf: func(string, ...any) {}})
|
|
return f, func() {
|
|
runCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
|
t.Cleanup(cancel)
|
|
go func() { _ = run.Run(runCtx, p, bus) }()
|
|
}
|
|
}
|
|
|
|
// 1332/AC1: a consumer only the grant file gives (oidc-client for novox.mesh-mail-web, not moved) is wanted and
|
|
// Adapter.Create is asked for its client, though keycloak runs on the moved path.
|
|
func TestAGrantFileConsumerNotMovedIsMadeOnTheMovedPath(t *testing.T) {
|
|
mailWeb := "mesh_novox_mail_web"
|
|
receives := grantGives(t, map[string]any{"as": mailWeb, "node": "novox", "values": map[string]any{
|
|
"label": "mail-web", "endpoint": "web", "callback": "/login/generic_oauth", "name": "mail.example.org"}})
|
|
h := &harness.Harness{Resource: ProvisionName, Receives: receives, Node: "novox"}
|
|
f, start := movedWorld(t, h)
|
|
start()
|
|
waitUntil(t, time.Second, func() bool {
|
|
found := false
|
|
f.set(func() {
|
|
for _, c := range f.clients {
|
|
if c["clientId"] == mailWeb {
|
|
found = true
|
|
}
|
|
}
|
|
})
|
|
return found
|
|
})
|
|
}
|
|
|
|
// 1332/AC2: a grant-file consumer whose client already exists in the backend (mesh_ace_grafana) counts as asked and
|
|
// is not held for retirement on the moved path.
|
|
func TestAGrantFileConsumerAlreadyInKeycloakIsNotRetiredOnTheMovedPath(t *testing.T) {
|
|
grafana := "mesh_ace_grafana"
|
|
receives := grantGives(t, map[string]any{"as": grafana, "node": "ace", "values": map[string]any{
|
|
"label": "grafana", "endpoint": "web", "callback": "/login/generic_oauth", "name": "grafana.example.org"}})
|
|
h := &harness.Harness{Resource: ProvisionName, Receives: receives, Node: "ace",
|
|
StablePasses: 1, StableFor: time.Millisecond}
|
|
f, start := movedWorld(t, h)
|
|
// Seed the backend with the client the mesh made earlier, exactly as grafana's already is — before anything
|
|
// runs, so the harness's one-time seed (issue 1332) sees it.
|
|
f.set(func() {
|
|
f.clients["existing-grafana"] = Rep{"id": "existing-grafana", "clientId": grafana, "enabled": true,
|
|
"attributes": map[string]any{Mark: "true"}}
|
|
})
|
|
start()
|
|
// Many passes, well past StablePasses and StableFor if the login were ever counted unasked.
|
|
time.Sleep(200 * time.Millisecond)
|
|
f.set(func() {
|
|
c := f.clients["existing-grafana"]
|
|
if c["enabled"] == false {
|
|
t.Fatalf("retired %v though its grant still gives it (issue 1332)", c)
|
|
}
|
|
if _, retired := attributes(c)[MarkRetired]; retired {
|
|
t.Fatalf("marked retired %v though its grant still gives it (issue 1332)", c)
|
|
}
|
|
})
|
|
}
|