Merged in feature/permit-io-demo (pull request #144)
draft pr for the test demo harness for permit.io * harness working for permit.io demo * user is const * aws profile * permit import code for research * cleanup * create user tool poc * Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/permit-io-demo * tests for user creation * add audit logs and dry-run * add tests sanity check the requested roles * update docs for delete * disable/enable users and test * more tests and make user name required only for create operations * clean up * add docs * aws tag tests * audit -> slog * attempt build fix devbox * edit * just devbox * remove lock change for merge * Merge branch 'main' of bitbucket.org:aarete/query-orchestration into feature/permit-io-demo
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
//go:build aws
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// Test configuration
|
||||
const (
|
||||
testCSVPath = "./temp-users.csv"
|
||||
testProjectID = "ebde1e1e9623491cab6f8112e67bd61c"
|
||||
testEnvID = "9d6801123cfd4a0ea2ef1df2f430e9d3"
|
||||
)
|
||||
|
||||
// Test data
|
||||
var testUsers = []map[string]string{
|
||||
{
|
||||
"email": "foo1@gmail.com",
|
||||
"first_name": "john",
|
||||
"last_name": "smith",
|
||||
"roles": "editor,viewer",
|
||||
},
|
||||
{
|
||||
"email": "foo2@gmail.com",
|
||||
"first_name": "jane",
|
||||
"last_name": "smith",
|
||||
"roles": "editor,viewer,admin",
|
||||
},
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Load environment variables
|
||||
if err := godotenv.Overload(); err != nil {
|
||||
fmt.Println("warning - problem loading .env file - ignoring.")
|
||||
}
|
||||
|
||||
// Run tests
|
||||
code := m.Run()
|
||||
|
||||
// Cleanup
|
||||
if err := os.Remove(testCSVPath); err != nil && !os.IsNotExist(err) {
|
||||
fmt.Printf("Warning: failed to cleanup test CSV file: %v\n", err)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
func TestUserLifecycle(t *testing.T) {
|
||||
// Skip test if required environment variables are not set
|
||||
if !checkRequiredEnvVars(t) {
|
||||
return
|
||||
}
|
||||
|
||||
// Create test CSV file
|
||||
if err := createTestCSV(); err != nil {
|
||||
t.Fatalf("Failed to create test CSV: %v", err)
|
||||
}
|
||||
|
||||
// Test user creation
|
||||
t.Run("CreateUsers", func(t *testing.T) {
|
||||
testCreateUsers(t)
|
||||
})
|
||||
|
||||
// Test user verification
|
||||
t.Run("VerifyUsersExist", func(t *testing.T) {
|
||||
testVerifyUsersExist(t)
|
||||
})
|
||||
|
||||
// Test user deletion
|
||||
t.Run("DeleteUsers", func(t *testing.T) {
|
||||
testDeleteUsers(t)
|
||||
})
|
||||
|
||||
// Test user deletion verification
|
||||
t.Run("VerifyUsersDeleted", func(t *testing.T) {
|
||||
testVerifyUsersDeleted(t)
|
||||
})
|
||||
}
|
||||
|
||||
func checkRequiredEnvVars(t *testing.T) bool {
|
||||
required := []string{"PERMIT_KEY", "COGNITO_USER_POOL_ID", "AWS_REGION"}
|
||||
missing := []string{}
|
||||
|
||||
for _, env := range required {
|
||||
if os.Getenv(env) == "" {
|
||||
missing = append(missing, env)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missing) > 0 {
|
||||
t.Skipf("Skipping integration test - missing required environment variables: %v", missing)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func createTestCSV() error {
|
||||
csvContent := "email,first_name,last_name,role1,role2,role3\n"
|
||||
csvContent += "foo1@gmail.com,john,smith,editor,viewer,\n"
|
||||
csvContent += "foo2@gmail.com,jane,smith,editor,viewer,admin\n"
|
||||
|
||||
return os.WriteFile(testCSVPath, []byte(csvContent), 0600)
|
||||
}
|
||||
|
||||
func testCreateUsers(t *testing.T) {
|
||||
// Save original os.Args
|
||||
originalArgs := os.Args
|
||||
|
||||
// Set up args for user creation
|
||||
os.Args = []string{
|
||||
"cognito-permit-sync",
|
||||
"-project", testProjectID,
|
||||
"-env", testEnvID,
|
||||
"-csv", testCSVPath,
|
||||
}
|
||||
|
||||
// Restore os.Args after test
|
||||
defer func() {
|
||||
os.Args = originalArgs
|
||||
}()
|
||||
|
||||
// Reset flags for testing
|
||||
resetFlags()
|
||||
|
||||
// Run the application (this will create its own audit logger internally)
|
||||
err := run()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create users: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Successfully created users")
|
||||
}
|
||||
|
||||
func testVerifyUsersExist(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Initialize clients
|
||||
cognitoClient, err := initializeCognitoClient()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize Cognito client: %v", err)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Timeout: 30 * time.Second}
|
||||
config := AppConfig{
|
||||
PermitAPIKey: os.Getenv("PERMIT_KEY"),
|
||||
PermitProjectID: testProjectID,
|
||||
PermitEnvID: testEnvID,
|
||||
PermitBaseURL: "https://api.permit.io",
|
||||
CognitoUserPoolID: os.Getenv("COGNITO_USER_POOL_ID"),
|
||||
CognitoRegion: os.Getenv("AWS_REGION"),
|
||||
}
|
||||
|
||||
for _, user := range testUsers {
|
||||
t.Run(fmt.Sprintf("VerifyUser_%s", user["email"]), func(t *testing.T) {
|
||||
// Verify user exists in Cognito
|
||||
cognitoSubjectID, err := getCognitoUserSubject(ctx, cognitoClient, config.CognitoUserPoolID, user["email"])
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to find user %s in Cognito: %v", user["email"], err)
|
||||
}
|
||||
if cognitoSubjectID == "" {
|
||||
t.Fatalf("User %s not found in Cognito", user["email"])
|
||||
}
|
||||
t.Logf("User %s found in Cognito with subject ID: %s", user["email"], cognitoSubjectID)
|
||||
|
||||
// Verify user exists in Permit.io
|
||||
permitUserKey, err := findPermitUserByEmail(httpClient, config, user["email"])
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to find user %s in Permit.io: %v", user["email"], err)
|
||||
}
|
||||
if permitUserKey == "" {
|
||||
t.Fatalf("User %s not found in Permit.io", user["email"])
|
||||
}
|
||||
t.Logf("User %s found in Permit.io with key: %s", user["email"], permitUserKey)
|
||||
|
||||
// Verify subject IDs match
|
||||
if cognitoSubjectID != permitUserKey {
|
||||
t.Fatalf("Subject ID mismatch for user %s: Cognito=%s, Permit.io=%s",
|
||||
user["email"], cognitoSubjectID, permitUserKey)
|
||||
}
|
||||
t.Logf("Subject IDs match for user %s: %s", user["email"], cognitoSubjectID)
|
||||
|
||||
// Verify roles are assigned in Permit.io
|
||||
if user["roles"] != "" {
|
||||
expectedRoles := strings.Split(user["roles"], ",")
|
||||
assignedRoles, err := getPermitUserRoles(httpClient, config, permitUserKey)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get roles for user %s: %v", user["email"], err)
|
||||
}
|
||||
|
||||
for _, expectedRole := range expectedRoles {
|
||||
expectedRole = strings.TrimSpace(expectedRole)
|
||||
if expectedRole == "" {
|
||||
continue
|
||||
}
|
||||
found := false
|
||||
for _, assignedRole := range assignedRoles {
|
||||
if assignedRole == expectedRole {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("Expected role %s not found for user %s. Assigned roles: %v",
|
||||
expectedRole, user["email"], assignedRoles)
|
||||
}
|
||||
}
|
||||
t.Logf("All expected roles verified for user %s: %v", user["email"], expectedRoles)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testDeleteUsers(t *testing.T) {
|
||||
// Save original os.Args
|
||||
originalArgs := os.Args
|
||||
|
||||
// Set up args for user deletion
|
||||
os.Args = []string{
|
||||
"cognito-permit-sync",
|
||||
"-project", testProjectID,
|
||||
"-env", testEnvID,
|
||||
"-csv", testCSVPath,
|
||||
"--delete-users",
|
||||
}
|
||||
|
||||
// Restore os.Args after test
|
||||
defer func() {
|
||||
os.Args = originalArgs
|
||||
}()
|
||||
|
||||
// Reset flags for testing
|
||||
resetFlags()
|
||||
|
||||
// Run the application in delete mode
|
||||
err := run()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to delete users: %v", err)
|
||||
}
|
||||
|
||||
t.Logf("Successfully deleted users")
|
||||
}
|
||||
|
||||
func testVerifyUsersDeleted(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
|
||||
// Initialize clients
|
||||
cognitoClient, err := initializeCognitoClient()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to initialize Cognito client: %v", err)
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Timeout: 30 * time.Second}
|
||||
config := AppConfig{
|
||||
PermitAPIKey: os.Getenv("PERMIT_KEY"),
|
||||
PermitProjectID: testProjectID,
|
||||
PermitEnvID: testEnvID,
|
||||
PermitBaseURL: "https://api.permit.io",
|
||||
CognitoUserPoolID: os.Getenv("COGNITO_USER_POOL_ID"),
|
||||
CognitoRegion: os.Getenv("AWS_REGION"),
|
||||
}
|
||||
|
||||
for _, user := range testUsers {
|
||||
t.Run(fmt.Sprintf("VerifyUserDeleted_%s", user["email"]), func(t *testing.T) {
|
||||
// Verify user is deleted from both systems with retry logic
|
||||
const maxRetries = 12 // 1 minute with 5-second intervals
|
||||
const retryInterval = 5 * time.Second
|
||||
|
||||
var cognitoDeleted, permitDeleted bool
|
||||
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
t.Logf("Retry attempt %d/%d for user %s", attempt+1, maxRetries, user["email"])
|
||||
time.Sleep(retryInterval)
|
||||
}
|
||||
|
||||
// Check Cognito deletion
|
||||
if !cognitoDeleted {
|
||||
cognitoSubjectID, err := getCognitoUserSubject(ctx, cognitoClient, config.CognitoUserPoolID, user["email"])
|
||||
cognitoDeleted = (err != nil || cognitoSubjectID == "")
|
||||
}
|
||||
|
||||
// Check Permit.io deletion
|
||||
if !permitDeleted {
|
||||
permitUserKey, err := findPermitUserByEmail(httpClient, config, user["email"])
|
||||
permitDeleted = (err != nil || permitUserKey == "")
|
||||
}
|
||||
|
||||
// If both are deleted, we're done
|
||||
if cognitoDeleted && permitDeleted {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Final verification
|
||||
if !cognitoDeleted {
|
||||
cognitoSubjectID, _ := getCognitoUserSubject(ctx, cognitoClient, config.CognitoUserPoolID, user["email"])
|
||||
t.Fatalf("User %s still exists in Cognito after %d attempts with subject ID: %s",
|
||||
user["email"], maxRetries, cognitoSubjectID)
|
||||
}
|
||||
t.Logf("User %s successfully deleted from Cognito", user["email"])
|
||||
|
||||
if !permitDeleted {
|
||||
permitUserKey, _ := findPermitUserByEmail(httpClient, config, user["email"])
|
||||
t.Fatalf("User %s still exists in Permit.io after %d attempts with key: %s",
|
||||
user["email"], maxRetries, permitUserKey)
|
||||
}
|
||||
t.Logf("User %s successfully deleted from Permit.io", user["email"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to get Cognito user subject ID
|
||||
func getCognitoUserSubject(ctx context.Context, client *cognitoidentityprovider.Client, userPoolID, username string) (string, error) {
|
||||
input := &cognitoidentityprovider.AdminGetUserInput{
|
||||
UserPoolId: aws.String(userPoolID),
|
||||
Username: aws.String(username),
|
||||
}
|
||||
|
||||
result, err := client.AdminGetUser(ctx, input)
|
||||
if err != nil {
|
||||
// Check if it's a UserNotFoundException
|
||||
if strings.Contains(err.Error(), "UserNotFoundException") ||
|
||||
strings.Contains(err.Error(), "User does not exist") {
|
||||
return "", nil // User not found, return empty string
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Extract subject ID from user attributes
|
||||
for _, attr := range result.UserAttributes {
|
||||
if aws.ToString(attr.Name) == "sub" {
|
||||
return aws.ToString(attr.Value), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("subject ID not found for user %s", username)
|
||||
}
|
||||
|
||||
// Helper function to reset flags for testing
|
||||
func resetFlags() {
|
||||
// Reset the default command line flag set
|
||||
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
|
||||
}
|
||||
Reference in New Issue
Block a user