You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by la...@apache.org on 2026/07/22 06:51:47 UTC

(airavata-custos) 10/29: Add live gate variant of the access request flow test

This is an automated email from the ASF dual-hosted git repository.

lahirujayathilake pushed a commit to branch pearc26
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git

commit 13af37b1aa0afc626bb18d18b1fc5c7355c810da
Author: lahiruj <la...@gmail.com>
AuthorDate: Thu Jul 16 05:47:37 2026 -0400

    Add live gate variant of the access request flow test
---
 test/e2e/access_request_e2e_test.go  |  23 +++--
 test/e2e/access_request_live_test.go | 185 +++++++++++++++++++++++++++++++++++
 2 files changed, 198 insertions(+), 10 deletions(-)

diff --git a/test/e2e/access_request_e2e_test.go b/test/e2e/access_request_e2e_test.go
index 4a68d3512..bc5646f08 100644
--- a/test/e2e/access_request_e2e_test.go
+++ b/test/e2e/access_request_e2e_test.go
@@ -69,7 +69,7 @@ func TestAccessRequestE2E(t *testing.T) {
 	ensureE2EClient(t, admin)
 
 	mock := newRegistryMock(t)
-	api := bootBackend(t, database, mock.srv.URL)
+	api := bootBackend(t, database, mockConnectorConfig(mock.srv.URL))
 
 	suffix := fmt.Sprintf("%d", time.Now().UnixNano()%1e8)
 
@@ -210,15 +210,8 @@ type accessRequest struct {
 
 // --- backend boot ---
 
-func bootBackend(t *testing.T, database *sqlx.DB, registryURL string) *httptest.Server {
-	t.Helper()
-	ctx := context.Background()
-	bus := events.New()
-	svc := service.New(database, bus)
-	router := identity.NewRouter(http.NewServeMux())
-	srv := server.New(svc, router)
-
-	cc := &config.ConnectorConfig{
+func mockConnectorConfig(registryURL string) *config.ConnectorConfig {
+	return &config.ConnectorConfig{
 		Type:    "comanage-identity-provisioner",
 		Enabled: true,
 		Config: map[string]interface{}{
@@ -234,6 +227,16 @@ func bootBackend(t *testing.T, database *sqlx.DB, registryURL string) *httptest.
 			},
 		},
 	}
+}
+
+func bootBackend(t *testing.T, database *sqlx.DB, cc *config.ConnectorConfig) *httptest.Server {
+	t.Helper()
+	ctx := context.Background()
+	bus := events.New()
+	svc := service.New(database, bus)
+	router := identity.NewRouter(http.NewServeMux())
+	srv := server.New(svc, router)
+
 	if err := comanage.LoadConnector(ctx, database, bus, svc, nil, router, cc); err != nil {
 		t.Fatalf("load identity-provisioner connector: %v", err)
 	}
diff --git a/test/e2e/access_request_live_test.go b/test/e2e/access_request_live_test.go
new file mode 100644
index 000000000..32b713156
--- /dev/null
+++ b/test/e2e/access_request_live_test.go
@@ -0,0 +1,185 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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.
+
+//go:build e2e && live
+
+// Live gate: the tier-1 flow once against the REAL registry. One-shot by
+// design; every run creates a real person entry that stays behind.
+package e2e
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"net/url"
+	"os"
+	"strconv"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/apache/airavata-custos/internal/config"
+)
+
+func TestAccessRequestLiveGate(t *testing.T) {
+	registryURL := os.Getenv("COMANAGE_REGISTRY_URL")
+	coID, _ := strconv.Atoi(os.Getenv("COMANAGE_CO_ID"))
+	apiUser := os.Getenv("COMANAGE_API_USER")
+	apiKey := os.Getenv("COMANAGE_API_KEY")
+	unixClusterID, _ := strconv.Atoi(os.Getenv("COMANAGE_UNIX_CLUSTER_ID"))
+	personIDType := os.Getenv("COMANAGE_PERSON_ID_TYPE")
+	if registryURL == "" || coID == 0 || apiUser == "" || apiKey == "" || unixClusterID == 0 || personIDType == "" {
+		t.Skip("live gate needs COMANAGE_REGISTRY_URL/CO_ID/API_USER/API_KEY/UNIX_CLUSTER_ID/PERSON_ID_TYPE")
+	}
+	os.Setenv("POSIX_USERNAME_PREFIX", "nexus")
+
+	admin := kcAdminToken(t)
+	database := openTestDB(t)
+	resetAndSeed(t, database)
+	ensureE2EClient(t, admin)
+
+	cc := &config.ConnectorConfig{
+		Type:    "comanage-identity-provisioner",
+		Enabled: true,
+		Config: map[string]interface{}{
+			"registry": map[string]interface{}{
+				"url": registryURL, "co_id": coID, "api_user": apiUser, "api_key": apiKey,
+			},
+			"unix_cluster": map[string]interface{}{
+				"id": unixClusterID, "person_id_type": personIDType,
+			},
+			"provisioning": map[string]interface{}{
+				"custos_cluster_id": clusterID, "default_shell": "/bin/bash",
+				"homedir_prefix": "/home/", "http_timeout": "30s",
+			},
+		},
+	}
+	api := bootBackend(t, database, cc)
+
+	suffix := fmt.Sprintf("%d", time.Now().UnixNano()%1e8)
+	userA := kcNewUser(t, admin, "e2e-live-"+suffix, "Live", "Gate"+suffix)
+	tokenA := ropcToken(t, userA.username)
+	subA := jwtSub(t, tokenA)
+
+	reqBody := map[string]string{"institution": "Live Gate University", "event_code": "PEARC26", "reason": "live gate"}
+	status, body := call(t, api, http.MethodPost, "/access-requests", tokenA, reqBody)
+	if status != http.StatusCreated {
+		t.Fatalf("POST access request: want 201, got %d %s", status, body)
+	}
+	var req accessRequest
+	mustDecode(t, body, &req)
+
+	approver := kcNewUser(t, admin, "e2e-live-appr-"+suffix, "Live", "Approver"+suffix)
+	tokenAppr := ropcToken(t, approver.username)
+	bindIdentity(t, database, "pearc26-approver", jwtSub(t, tokenAppr), approver.email)
+
+	status, body = call(t, api, http.MethodPut, "/access-requests/"+req.ID, tokenAppr, map[string]string{"status": "APPROVED"})
+	if status != http.StatusOK {
+		t.Fatalf("PUT APPROVED: want 200, got %d %s", status, body)
+	}
+	var approved accessRequest
+	mustDecode(t, body, &approved)
+	assertApprovedState(t, database, req.ID, subA, userA.email)
+	username := clusterUsername(t, database, approved.CreatedUserID)
+	t.Logf("LIVE: approved request %s, portal user %s, cluster username %s, email %s",
+		req.ID, approved.CreatedUserID, username, userA.email)
+
+	// Independent registry reads, polled: the subscriber runs async and the
+	// registry itself may lag.
+	get := func(u string) (int, []byte) {
+		httpReq, err := http.NewRequest(http.MethodGet, u, nil)
+		if err != nil {
+			t.Fatalf("build registry request: %v", err)
+		}
+		httpReq.SetBasicAuth(apiUser, apiKey)
+		resp, err := http.DefaultClient.Do(httpReq)
+		if err != nil {
+			t.Fatalf("registry GET %s: %v", u, err)
+		}
+		defer resp.Body.Close()
+		b, _ := io.ReadAll(resp.Body)
+		return resp.StatusCode, b
+	}
+
+	var coPersonID int
+	deadline := time.Now().Add(90 * time.Second)
+	for time.Now().Before(deadline) {
+		code, b := get(fmt.Sprintf("%s/co_people.json?coid=%d&search.mail=%s", registryURL, coID, url.QueryEscape(userA.email)))
+		if code == http.StatusOK {
+			var list struct {
+				CoPeople []struct {
+					Id     int    `json:"Id"`
+					Status string `json:"Status"`
+				} `json:"CoPeople"`
+			}
+			if err := json.Unmarshal(b, &list); err == nil && len(list.CoPeople) > 0 {
+				coPersonID = list.CoPeople[0].Id
+				t.Logf("LIVE: registry CoPerson id=%d status=%s", coPersonID, list.CoPeople[0].Status)
+				break
+			}
+		}
+		time.Sleep(5 * time.Second)
+	}
+	if coPersonID == 0 {
+		t.Fatalf("registry never showed a CoPerson for %s within 90s", userA.email)
+	}
+
+	// The composite endpoint keys on the person_id_type identifier, not the
+	// numeric person id.
+	code, b := get(fmt.Sprintf("%s/identifiers.json?copersonid=%d", registryURL, coPersonID))
+	if code != http.StatusOK {
+		t.Fatalf("identifiers read for CoPerson %d: status %d body %s", coPersonID, code, string(b))
+	}
+	var ids struct {
+		Identifiers []struct {
+			Type       string `json:"Type"`
+			Identifier string `json:"Identifier"`
+		} `json:"Identifiers"`
+	}
+	if err := json.Unmarshal(b, &ids); err != nil {
+		t.Fatalf("decode identifiers: %v", err)
+	}
+	personIdent := ""
+	for _, id := range ids.Identifiers {
+		if id.Type == personIDType {
+			personIdent = id.Identifier
+		}
+	}
+	if personIdent == "" {
+		t.Fatalf("CoPerson %d has no identifier of type %q: %+v", coPersonID, personIDType, ids.Identifiers)
+	}
+
+	code, b = get(fmt.Sprintf("%s/api/co/%d/core/v1/people/%s", registryURL, coID, url.PathEscape(personIdent)))
+	if code != http.StatusOK {
+		t.Fatalf("composite read for CoPerson %d: status %d body %s", coPersonID, code, string(b))
+	}
+	composite := string(b)
+	for _, want := range []string{"UnixClusterAccount", username} {
+		if !strings.Contains(composite, want) {
+			t.Errorf("composite for CoPerson %d missing %q", coPersonID, want)
+		}
+	}
+	if idx := strings.Index(composite, "UnixClusterAccount"); idx >= 0 {
+		end := idx + 600
+		if end > len(composite) {
+			end = len(composite)
+		}
+		t.Logf("LIVE: composite UnixClusterAccount excerpt: %s", composite[idx:end])
+	}
+	t.Logf("LIVE GATE PASS: CoPerson %d carries a UnixClusterAccount for %s; check `id %s` on the cluster next", coPersonID, username, username)
+}