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