Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion portal-backend/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/stretchr/objx v0.5.2 // indirect
Expand All @@ -33,7 +34,6 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/text v0.21.0 // indirect
Expand Down
33 changes: 32 additions & 1 deletion portal-backend/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
v1handlers "github.com/gov-dx-sandbox/portal-backend/v1/handlers"
v1middleware "github.com/gov-dx-sandbox/portal-backend/v1/middleware"
v1models "github.com/gov-dx-sandbox/portal-backend/v1/models"
v1services "github.com/gov-dx-sandbox/portal-backend/v1/services"
"github.com/joho/godotenv"
)

Expand All @@ -35,8 +36,24 @@ func main() {
os.Exit(1)
}

// Initialize PDP service (used by handlers and worker)
pdpServiceURL := os.Getenv("CHOREO_PDP_CONNECTION_SERVICEURL")
if pdpServiceURL == "" {
slog.Error("CHOREO_PDP_CONNECTION_SERVICEURL environment variable not set")
os.Exit(1)
}

pdpServiceAPIKey := os.Getenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY")
if pdpServiceAPIKey == "" {
slog.Error("CHOREO_PDP_CONNECTION_CHOREOAPIKEY environment variable not set")
os.Exit(1)
}

pdpService := v1services.NewPDPService(pdpServiceURL, pdpServiceAPIKey)
slog.Info("PDP Service initialized", "url", pdpServiceURL)

// Initialize V1 handlers
v1Handler, err := v1handlers.NewV1Handler(gormDB)
v1Handler, err := v1handlers.NewV1Handler(gormDB, pdpService)
if err != nil {
slog.Error("Failed to initialize V1 handler", "error", err)
os.Exit(1)
Expand Down Expand Up @@ -256,6 +273,16 @@ func main() {
IdleTimeout: 60 * time.Second,
}

// Create and start PDP worker
// Initialize alert notifier (using logging for now, can be extended to PagerDuty/Slack)
alertNotifier := v1services.NewLoggingAlertNotifier()
pdpWorker := v1services.NewPDPWorker(gormDB, pdpService, alertNotifier)
workerCtx, workerCancel := context.WithCancel(context.Background())
defer workerCancel()

go pdpWorker.Start(workerCtx)
slog.Info("PDP worker started in background")

// Start server in a goroutine
go func() {
slog.Info("Portal Backend starting", "port", port, "addr", addr)
Expand All @@ -272,6 +299,10 @@ func main() {

slog.Info("Shutting down Portal Backend...")

// Stop the PDP worker
workerCancel()
slog.Info("PDP worker stopped")

// Create a deadline to wait for
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
Expand Down
23 changes: 6 additions & 17 deletions portal-backend/v1/handlers/v1_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package handlers
import (
"encoding/json"
"fmt"
"log/slog"

"net/http"
"os"
"strings"
Expand Down Expand Up @@ -57,7 +57,7 @@ func (h *V1Handler) getUserMemberID(r *http.Request, user *models.AuthenticatedU
}

// NewV1Handler creates a new V1 handler
func NewV1Handler(db *gorm.DB) (*V1Handler, error) {
func NewV1Handler(db *gorm.DB, pdpService services.PDPClient) (*V1Handler, error) {
// Get scopes from environment variable, fallback to default if not set
asgScopesEnv := os.Getenv("ASGARDEO_SCOPES")
var scopes []string
Expand Down Expand Up @@ -86,19 +86,6 @@ func NewV1Handler(db *gorm.DB) (*V1Handler, error) {
}
memberService := services.NewMemberService(db, idpProvider)

pdpServiceURL := os.Getenv("CHOREO_PDP_CONNECTION_SERVICEURL")
if pdpServiceURL == "" {
return nil, fmt.Errorf("CHOREO_PDP_CONNECTION_SERVICEURL environment variable not set")
}

pdpServiceAPIKey := os.Getenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY")
if pdpServiceAPIKey == "" {
return nil, fmt.Errorf("CHOREO_PDP_CONNECTION_CHOREOAPIKEY environment variable not set")
}

pdpService := services.NewPDPService(pdpServiceURL, pdpServiceAPIKey)
slog.Info("PDP Service URL", "url", pdpServiceURL)

return &V1Handler{
memberService: memberService,
schemaService: services.NewSchemaService(db, pdpService),
Expand Down Expand Up @@ -808,7 +795,8 @@ func (h *V1Handler) createSchema(w http.ResponseWriter, r *http.Request) {
// Log audit event
middleware.LogAuditEvent(r, string(models.ResourceTypeSchemas), &schema.SchemaID, string(models.AuditStatusSuccess))

utils.RespondWithSuccess(w, http.StatusCreated, schema)
// Return 202 Accepted - job is queued, will be processed asynchronously
utils.RespondWithSuccess(w, http.StatusAccepted, schema)
}

func (h *V1Handler) updateSchema(w http.ResponseWriter, r *http.Request, schemaId string) {
Expand Down Expand Up @@ -1195,7 +1183,8 @@ func (h *V1Handler) createApplication(w http.ResponseWriter, r *http.Request) {
// Log audit event
middleware.LogAuditEvent(r, string(models.ResourceTypeApplications), &application.ApplicationID, string(models.AuditStatusSuccess))

utils.RespondWithSuccess(w, http.StatusCreated, application)
// Return 202 Accepted - job is queued, will be processed asynchronously
utils.RespondWithSuccess(w, http.StatusAccepted, application)
}

func (h *V1Handler) updateApplication(w http.ResponseWriter, r *http.Request, applicationId string) {
Expand Down
23 changes: 3 additions & 20 deletions portal-backend/v1/handlers/v1_handler_initialization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ func TestNewV1Handler_MissingEnvVars(t *testing.T) {

// We need a DB connection
db := services.SetupSQLiteTestDB(t)
pdpService := services.NewPDPService("http://dummy", "dummy")

// Case 1: Missing IDP config (BaseURL)
handler, err := NewV1Handler(db)
handler, err := NewV1Handler(db, pdpService)
assert.Error(t, err)
assert.Nil(t, handler)
assert.Contains(t, err.Error(), "failed to create IDP provider")
Expand All @@ -53,26 +54,8 @@ func TestNewV1Handler_MissingEnvVars(t *testing.T) {
os.Setenv("ASGARDEO_CLIENT_ID", "client-id")
os.Setenv("ASGARDEO_CLIENT_SECRET", "client-secret")

// Case 2: Missing PDP URL
handler, err = NewV1Handler(db)
assert.Error(t, err)
assert.Nil(t, handler)
assert.Contains(t, err.Error(), "CHOREO_PDP_CONNECTION_SERVICEURL environment variable not set")

// Set PDP URL
os.Setenv("CHOREO_PDP_CONNECTION_SERVICEURL", "http://pdp:8080")

// Case 3: Missing PDP Key
handler, err = NewV1Handler(db)
assert.Error(t, err)
assert.Nil(t, handler)
assert.Contains(t, err.Error(), "CHOREO_PDP_CONNECTION_CHOREOAPIKEY environment variable not set")

// Set PDP Key
os.Setenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY", "api-key")

// Case 4: Success
handler, err = NewV1Handler(db)
handler, err = NewV1Handler(db, pdpService)
assert.NoError(t, err)
assert.NotNil(t, handler)
}
Expand Down
157 changes: 79 additions & 78 deletions portal-backend/v1/handlers/v1_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,38 @@ func TestMemberEndpoints(t *testing.T) {
assert.Equal(t, http.StatusNotFound, w.Code)
})

t.Run("PUT /api/v1/members/:id - UpdateMember_Success", func(t *testing.T) {
// Create a member first
email := fmt.Sprintf("test-%d@example.com", time.Now().UnixNano())
memberID := createTestMember(t, testHandler.db, email)

// Get the member to find the IDP user ID
var member models.Member
err := testHandler.db.Where("member_id = ?", memberID).First(&member).Error
assert.NoError(t, err)

// Setup mock IDP for member update
setupMockIDPForMemberUpdate(member.IdpUserID, email)

name := "Updated Name"
req := models.UpdateMemberRequest{
Name: &name,
}
reqBody, _ := json.Marshal(req)
httpReq := NewAdminRequest(http.MethodPut, fmt.Sprintf("/api/v1/members/%s", memberID), bytes.NewBuffer(reqBody))
httpReq.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
mux := http.NewServeMux()
testHandler.handler.SetupV1Routes(mux)
mux.ServeHTTP(w, httpReq)

assert.Equal(t, http.StatusOK, w.Code)
var response models.MemberResponse
err = json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, name, response.Name)
})

t.Run("GET /api/v1/members - GetAllMembers", func(t *testing.T) {
httpReq := NewAdminRequest(http.MethodGet, "/api/v1/members", nil)
w := httptest.NewRecorder()
Expand Down Expand Up @@ -1066,6 +1098,47 @@ func TestApplicationSubmissionEndpoints(t *testing.T) {

assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
})

t.Run("POST /api/v1/application-submissions - Invalid JSON", func(t *testing.T) {
httpReq := NewAdminRequest(http.MethodPost, "/api/v1/application-submissions", bytes.NewBufferString("invalid json"))
httpReq.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
mux := http.NewServeMux()
testHandler.handler.SetupV1Routes(mux)
mux.ServeHTTP(w, httpReq)

assert.Equal(t, http.StatusBadRequest, w.Code)
})

t.Run("GET /api/v1/application-submissions - WithStatusFilter", func(t *testing.T) {
httpReq := NewAdminRequest(http.MethodGet, "/api/v1/application-submissions?status=pending&status=approved", nil)
w := httptest.NewRecorder()
mux := http.NewServeMux()
testHandler.handler.SetupV1Routes(mux)
mux.ServeHTTP(w, httpReq)

assert.Equal(t, http.StatusOK, w.Code)
})

t.Run("GET /api/v1/schema-submissions - WithStatusFilter", func(t *testing.T) {
httpReq := NewAdminRequest(http.MethodGet, "/api/v1/schema-submissions?status=pending&status=approved", nil)
w := httptest.NewRecorder()
mux := http.NewServeMux()
testHandler.handler.SetupV1Routes(mux)
mux.ServeHTTP(w, httpReq)

assert.Equal(t, http.StatusOK, w.Code)
})

t.Run("GET /api/v1/schemas - WithMemberID", func(t *testing.T) {
httpReq := NewAdminRequest(http.MethodGet, "/api/v1/schemas?memberId=test-member-123", nil)
w := httptest.NewRecorder()
mux := http.NewServeMux()
testHandler.handler.SetupV1Routes(mux)
mux.ServeHTTP(w, httpReq)

assert.Equal(t, http.StatusOK, w.Code)
})
}

// TestSchemaEndpoints_EdgeCases tests edge cases for schema endpoints
Expand Down Expand Up @@ -1242,81 +1315,6 @@ func TestSchemaSubmissionEndpoints_EdgeCases(t *testing.T) {

// TestNewV1Handler tests the NewV1Handler constructor
func TestNewV1Handler(t *testing.T) {
t.Run("NewV1Handler_MissingPDPURL", func(t *testing.T) {
originalURL := os.Getenv("CHOREO_PDP_CONNECTION_SERVICEURL")
originalKey := os.Getenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY")
defer func() {
if originalURL != "" {
os.Setenv("CHOREO_PDP_CONNECTION_SERVICEURL", originalURL)
} else {
os.Unsetenv("CHOREO_PDP_CONNECTION_SERVICEURL")
}
if originalKey != "" {
os.Setenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY", originalKey)
} else {
os.Unsetenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY")
}
}()

os.Unsetenv("CHOREO_PDP_CONNECTION_SERVICEURL")
os.Unsetenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY")

// Set IDP env vars to pass IDP check
os.Setenv("ASGARDEO_BASE_URL", "https://example.com")
os.Setenv("ASGARDEO_CLIENT_ID", "client-id")
os.Setenv("ASGARDEO_CLIENT_SECRET", "client-secret")
defer os.Unsetenv("ASGARDEO_BASE_URL")
defer os.Unsetenv("ASGARDEO_CLIENT_ID")
defer os.Unsetenv("ASGARDEO_CLIENT_SECRET")

db := services.SetupSQLiteTestDB(t)
if db == nil {
return
}

handler, err := NewV1Handler(db)
assert.Error(t, err)
assert.Nil(t, handler)
assert.Contains(t, err.Error(), "CHOREO_PDP_CONNECTION_SERVICEURL")
})

t.Run("NewV1Handler_MissingPDPKey", func(t *testing.T) {
originalURL := os.Getenv("CHOREO_PDP_CONNECTION_SERVICEURL")
originalKey := os.Getenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY")
defer func() {
if originalURL != "" {
os.Setenv("CHOREO_PDP_CONNECTION_SERVICEURL", originalURL)
} else {
os.Unsetenv("CHOREO_PDP_CONNECTION_SERVICEURL")
}
if originalKey != "" {
os.Setenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY", originalKey)
} else {
os.Unsetenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY")
}
}()

os.Setenv("CHOREO_PDP_CONNECTION_SERVICEURL", "http://localhost:9999")
os.Unsetenv("CHOREO_PDP_CONNECTION_CHOREOAPIKEY")

// Set IDP env vars to pass IDP check
os.Setenv("ASGARDEO_BASE_URL", "https://example.com")
os.Setenv("ASGARDEO_CLIENT_ID", "client-id")
os.Setenv("ASGARDEO_CLIENT_SECRET", "client-secret")
defer os.Unsetenv("ASGARDEO_BASE_URL")
defer os.Unsetenv("ASGARDEO_CLIENT_ID")
defer os.Unsetenv("ASGARDEO_CLIENT_SECRET")

db := services.SetupSQLiteTestDB(t)
if db == nil {
return
}

handler, err := NewV1Handler(db)
assert.Error(t, err)
assert.Nil(t, handler)
assert.Contains(t, err.Error(), "CHOREO_PDP_CONNECTION_CHOREOAPIKEY")
})

t.Run("NewV1Handler_Success", func(t *testing.T) {
originalURL := os.Getenv("CHOREO_PDP_CONNECTION_SERVICEURL")
Expand Down Expand Up @@ -1370,7 +1368,8 @@ func TestNewV1Handler(t *testing.T) {
return
}

handler, err := NewV1Handler(db)
mockPDP := services.NewPDPService("http://localhost:9999", "test-key")
handler, err := NewV1Handler(db, mockPDP)
assert.NoError(t, err)
assert.NotNil(t, handler)
assert.NotNil(t, handler.memberService)
Expand Down Expand Up @@ -1430,7 +1429,8 @@ func TestNewV1Handler(t *testing.T) {
return
}

handler, err := NewV1Handler(db)
mockPDP := services.NewPDPService("http://localhost:9999", "test-key")
handler, err := NewV1Handler(db, mockPDP)
assert.NoError(t, err)
assert.NotNil(t, handler)
})
Expand Down Expand Up @@ -1479,7 +1479,8 @@ func TestV1Handler_SetupV1Routes(t *testing.T) {
os.Setenv("ASGARDEO_CLIENT_ID", "test-client-id")
os.Setenv("ASGARDEO_CLIENT_SECRET", "test-client-secret")

handler, err := NewV1Handler(db)
mockPDP := services.NewPDPService("http://localhost:9999", "test-key")
handler, err := NewV1Handler(db, mockPDP)
if err != nil {
t.Fatalf("Failed to create handler: %v", err)
}
Expand Down
Loading
Loading