{"id":34176296,"url":"https://github.com/companyinfo/keycloak","last_synced_at":"2026-03-10T09:40:10.368Z","repository":{"id":319294425,"uuid":"1078131212","full_name":"companyinfo/keycloak","owner":"companyinfo","description":"Keycloak is an idiomatic Go client for the Keycloak Admin API","archived":false,"fork":false,"pushed_at":"2025-11-11T06:42:53.000Z","size":182,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-11T08:30:03.793Z","etag":null,"topics":["go","golang","keycloak","package","pkg"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/companyinfo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-17T08:56:12.000Z","updated_at":"2025-11-11T06:42:57.000Z","dependencies_parsed_at":null,"dependency_job_id":"3dfef576-919c-4f34-b6a8-bdad9143f3fe","html_url":"https://github.com/companyinfo/keycloak","commit_stats":null,"previous_names":["companyinfo/keycloak"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/companyinfo/keycloak","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/companyinfo%2Fkeycloak","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/companyinfo%2Fkeycloak/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/companyinfo%2Fkeycloak/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/companyinfo%2Fkeycloak/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/companyinfo","download_url":"https://codeload.github.com/companyinfo/keycloak/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/companyinfo%2Fkeycloak/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30328901,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T05:25:20.737Z","status":"ssl_error","status_checked_at":"2026-03-10T05:25:17.430Z","response_time":106,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","keycloak","package","pkg"],"created_at":"2025-12-15T12:30:11.877Z","updated_at":"2026-03-10T09:40:10.018Z","avatar_url":"https://github.com/companyinfo.png","language":"Go","readme":"# Keycloak Go Client\n\n[![CI](https://github.com/companyinfo/keycloak/actions/workflows/ci.yaml/badge.svg)](https://github.com/companyinfo/keycloak/actions/workflows/ci.yaml)\n[![codecov](https://codecov.io/gh/companyinfo/keycloak/branch/main/graph/badge.svg)](https://codecov.io/gh/companyinfo/keycloak)\n[![Go Reference](https://pkg.go.dev/badge/go.companyinfo.dev/keycloak.svg)](https://pkg.go.dev/go.companyinfo.dev/keycloak)\n[![Go Report Card](https://goreportcard.com/badge/go.companyinfo.dev/keycloak)](https://goreportcard.com/report/go.companyinfo.dev/keycloak)\n[![Go Version](https://img.shields.io/badge/Go-1.24+-blue)](https://go.dev/dl/)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE)\n\nA production-ready, idiomatic Go client for the Keycloak Admin API.\n\n**Quick Links**: [Installation](#installation) | [Quick Start](#quick-start) | [Examples](examples/) | [API Docs](https://pkg.go.dev/go.companyinfo.dev/keycloak)\n\n## Overview\n\nThe package provides a clean, type-safe interface for interacting with Keycloak's Admin API. Built for production use, it handles authentication, automatic token refresh, retry logic, and provides comprehensive resource management capabilities.\n\n**Why This Library?**\n\n- **Production Ready**: Battle-tested with automatic token management, retry logic, and comprehensive error handling\n- **Type Safe**: Strongly typed models prevent runtime errors and improve code maintainability\n- **Modern Go Patterns**: Context support, functional options, and resource-based client design\n- **Well Tested**: Extensive test coverage with unit, mock, and integration tests\n- **Easy to Use**: Simple API with sensible defaults, yet flexible for advanced use cases\n\n## Table of Contents\n\n- [Features](#features)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Usage](#usage)\n  - [Basic Setup](#basic-setup)\n  - [Advanced Configuration](#advanced-configuration-with-options)\n  - [Group Operations](#creating-a-group)\n  - [Real-World Examples](#real-world-examples)\n- [API Reference](#api-reference)\n- [Error Handling](#error-handling)\n- [Best Practices](#best-practices)\n- [Troubleshooting](#troubleshooting)\n- [Testing](#testing)\n- [Contributing](#contributing)\n- [FAQ](#faq)\n- [License](#license)\n\n## Features\n\n- **Authentication**: OAuth2 client credentials flow with automatic token management and refresh\n- **Group Management**: Complete CRUD operations for groups and subgroups with attribute support\n- **Pagination**: Built-in support for paginated requests with configurable page sizes\n- **Type Safety**: Strongly typed models and interfaces prevent runtime errors\n- **Context Support**: All operations accept context for cancellation, timeout control, and request tracing\n- **Retry Logic**: Configurable exponential backoff for handling transient failures\n- **Flexible Configuration**: Functional options pattern for easy customization\n- **Production Ready**: Debug logging, custom headers, proxy support, and comprehensive error handling\n\n## Requirements\n\n- **Go**: 1.24 or later\n- **Keycloak**: 26.x or later (may work with earlier versions but not officially tested)\n- **Client Credentials**: A Keycloak client with appropriate permissions:\n  - Service accounts enabled\n  - Client authentication: ON (confidential client)\n  - At minimum: `view-users`, `manage-users`, `manage-groups` roles\n  - For full admin operations: `realm-admin` role\n\n\u003e **💡 Tip**: Check [Keycloak Client Setup](#setting-up-keycloak-for-integration-tests) for detailed configuration steps.\n\n## Installation\n\n```bash\ngo get go.companyinfo.dev/keycloak\n```\n\nThe package name is `keycloak`, so you'll use it as:\n\n```go\nimport \"go.companyinfo.dev/keycloak\"\n\nclient, err := keycloak.New(ctx, keycloak.Config{...})\n```\n\n\u003e **📝 Note**: Some examples in the wild may use `keycloak` as an import alias, but it's not necessary. The package name is `keycloak`.\n\n## Quick Start\n\nGet up and running in 60 seconds:\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"fmt\"\n    \"log\"\n    \"time\"\n    \n    \"go.companyinfo.dev/keycloak\"\n)\n\nfunc main() {\n    ctx := context.Background()\n    \n    // 1. Create client with production-ready defaults\n    client, err := keycloak.New(ctx, keycloak.Config{\n        URL:          \"https://keycloak.example.com\",\n        Realm:        \"my-realm\",\n        ClientID:     \"admin-cli\",\n        ClientSecret: \"your-client-secret\",\n    },\n        keycloak.WithTimeout(30*time.Second),                    // Prevent hanging\n        keycloak.WithRetry(3, 1*time.Second, 10*time.Second),   // Handle transient failures\n    )\n    if err != nil {\n        log.Fatalf(\"Failed to initialize client: %v\", err)\n    }\n    \n    // 2. Create a group\n    groupID, err := client.Groups.Create(ctx, \"Engineering\", map[string][]string{\n        \"department\": {\"engineering\"},\n        \"location\":   {\"remote\"},\n    })\n    if err != nil {\n        log.Fatalf(\"Failed to create group: %v\", err)\n    }\n    \n    fmt.Printf(\"✓ Created group: %s\\n\", groupID)\n    \n    // 3. List groups\n    groups, err := client.Groups.List(ctx, nil, false)\n    if err != nil {\n        log.Fatalf(\"Failed to list groups: %v\", err)\n    }\n    \n    fmt.Printf(\"✓ Found %d groups\\n\", len(groups))\n}\n```\n\nThat's it! Continue reading for advanced features and best practices.\n\n## Usage\n\n### Basic Setup\n\n```go\npackage main\n\nimport (\n    \"context\"\n    \"log\"\n    \n    \"go.companyinfo.dev/keycloak\"\n)\n\nfunc main() {\n    ctx := context.Background()\n    \n    config := keycloak.Config{\n        URL:          \"https://keycloak.example.com\",\n        Realm:        \"my-realm\",\n        ClientID:     \"admin-cli\",\n        ClientSecret: \"your-client-secret\",\n    }\n    \n    client, err := keycloak.New(ctx, config)\n    if err != nil {\n        log.Fatalf(\"Failed to create client: %v\", err)\n    }\n    \n    // Access resource-specific clients\n    // client.Groups - for group operations\n    // Future: client.Users, client.Roles, client.Organizations, etc.\n}\n```\n\n### Advanced Configuration with Options\n\nThe client supports functional options for flexible configuration:\n\n```go\nimport (\n    \"context\"\n    \"log\"\n    \"time\"\n    \n    \"go.companyinfo.dev/keycloak\"\n)\n\nfunc main() {\n    ctx := context.Background()\n    \n    config := keycloak.Config{\n        URL:          \"https://keycloak.example.com\",\n        Realm:        \"my-realm\",\n        ClientID:     \"admin-cli\",\n        ClientSecret: \"your-client-secret\",\n    }\n    \n    client, err := keycloak.New(ctx, config,\n        keycloak.WithPageSize(100),                                  // Custom page size\n        keycloak.WithTimeout(30*time.Second),                        // Request timeout\n        keycloak.WithRetry(3, 5*time.Second, 30*time.Second),       // Retry configuration\n        keycloak.WithDebug(true),                                    // Enable debug logging\n        keycloak.WithUserAgent(\"my-app/1.0\"),                        // Custom User-Agent\n        keycloak.WithHeaders(map[string]string{                      // Custom headers\n            \"X-Request-ID\": \"12345\",\n        }),\n    )\n    if err != nil {\n        log.Fatalf(\"Failed to create client: %v\", err)\n    }\n    \n    // Client is ready with custom configuration\n}\n```\n\n#### Available Options\n\n- **`WithPageSize(size int)`** - Set default page size for paginated requests (default: 50)\n- **`WithTimeout(timeout time.Duration)`** - Set request timeout for all API calls\n- **`WithRetry(count int, waitTime, maxWaitTime time.Duration)`** - Configure retry behavior\n- **`WithDebug(debug bool)`** - Enable debug logging for requests and responses\n- **`WithHeaders(headers map[string]string)`** - Add custom headers to all requests\n- **`WithUserAgent(userAgent string)`** - Set custom User-Agent header\n- **`WithProxy(proxyURL string)`** - Set proxy URL for all requests\n- **`WithHTTPClient(httpClient *http.Client)`** - Use custom HTTP client (advanced)\n\n### Creating a Group\n\n```go\nattributes := map[string][]string{\n    \"description\": {\"My group description\"},\n    \"type\":        {\"organization\"},\n}\n\ngroupID, err := client.Groups.Create(ctx, \"My Group\", attributes)\nif err != nil {\n    log.Fatalf(\"Failed to create group: %v\", err)\n}\n\nlog.Printf(\"Created group with ID: %s\", groupID)\n```\n\n### Getting Groups\n\n```go\n// Get all groups\ngroups, err := client.Groups.List(ctx, nil, false)\nif err != nil {\n    log.Fatalf(\"Failed to get groups: %v\", err)\n}\n\n// Get groups with search\nsearchTerm := \"My Group\"\ngroups, err := client.Groups.List(ctx, \u0026searchTerm, false)\nif err != nil {\n    log.Fatalf(\"Failed to search groups: %v\", err)\n}\n\n// Get groups with pagination\ngroups, err := client.Groups.ListPaginated(ctx, nil, false, 0, 10)\nif err != nil {\n    log.Fatalf(\"Failed to get paginated groups: %v\", err)\n}\n\n// Get group by ID\ngroup, err := client.Groups.Get(ctx, groupID)\nif err != nil {\n    log.Fatalf(\"Failed to get group: %v\", err)\n}\n\n// Count groups\ncount, err := client.Groups.Count(ctx, nil, nil)\nif err != nil {\n    log.Fatalf(\"Failed to count groups: %v\", err)\n}\nlog.Printf(\"Total groups: %d\", count)\n```\n\n### Working with Group Attributes\n\n```go\n// Get group by specific attribute\nattribute := \u0026keycloak.GroupAttribute{\n    Key:   \"salesforceID\",\n    Value: \"SF-12345\",\n}\n\ngroup, err := client.Groups.GetByAttribute(ctx, attribute)\nif err != nil {\n    log.Fatalf(\"Failed to find group: %v\", err)\n}\n```\n\n### Managing Subgroups\n\n```go\n// Create a subgroup\nsubGroupID, err := client.Groups.CreateSubGroup(ctx, parentGroupID, \"Sub Group\", attributes)\nif err != nil {\n    log.Fatalf(\"Failed to create subgroup: %v\", err)\n}\n\n// Get subgroups\nsubGroups, err := client.Groups.ListSubGroups(ctx, parentGroupID)\nif err != nil {\n    log.Fatalf(\"Failed to get subgroups: %v\", err)\n}\n\n// Get subgroup by ID\nsubGroup, err := client.Groups.GetSubGroupByID(parentGroup, subGroupID)\nif err != nil {\n    log.Fatalf(\"Failed to find subgroup: %v\", err)\n}\n```\n\n### Updating and Deleting Groups\n\n```go\n// Update a group\ngroup, err := client.Groups.Get(ctx, groupID)\nif err != nil {\n    log.Fatalf(\"Failed to get group: %v\", err)\n}\n\n// Modify group attributes\n(*group.Attributes)[\"updated\"] = []string{\"true\"}\n\nerr = client.Groups.Update(ctx, *group)\nif err != nil {\n    log.Fatalf(\"Failed to update group: %v\", err)\n}\n\n// Delete a group\nerr = client.Groups.Delete(ctx, groupID)\nif err != nil {\n    log.Fatalf(\"Failed to delete group: %v\", err)\n}\n```\n\n### Real-World Examples\n\n#### Example 1: Sync External System with Keycloak Groups\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eView complete example\u003c/b\u003e - Sync departments from HR system to Keycloak\u003c/summary\u003e\n\n```go\n// Sync departments from your HR system to Keycloak\nfunc syncDepartments(ctx context.Context, client *keycloak.Client, departments []Department) error {\n    for _, dept := range departments {\n        // Try to find existing group by external ID\n        attr := \u0026keycloak.GroupAttribute{\n            Key:   \"externalID\",\n            Value: dept.ExternalID,\n        }\n        \n        group, err := client.Groups.GetByAttribute(ctx, attr)\n        if err == keycloak.ErrGroupNotFound {\n            // Create new group\n            attributes := map[string][]string{\n                \"externalID\":  {dept.ExternalID},\n                \"syncedAt\":    {time.Now().Format(time.RFC3339)},\n                \"description\": {dept.Description},\n            }\n            \n            _, err := client.Groups.Create(ctx, dept.Name, attributes)\n            if err != nil {\n                return fmt.Errorf(\"create group %s: %w\", dept.Name, err)\n            }\n            log.Printf(\"Created group: %s\", dept.Name)\n        } else if err != nil {\n            return fmt.Errorf(\"lookup group %s: %w\", dept.Name, err)\n        } else {\n            // Update existing group\n            (*group.Attributes)[\"syncedAt\"] = []string{time.Now().Format(time.RFC3339)}\n            (*group.Attributes)[\"description\"] = []string{dept.Description}\n            \n            if err := client.Groups.Update(ctx, *group); err != nil {\n                return fmt.Errorf(\"update group %s: %w\", dept.Name, err)\n            }\n            log.Printf(\"Updated group: %s\", dept.Name)\n        }\n    }\n    return nil\n}\n```\n\n\u003c/details\u003e\n\n#### Example 2: Bulk Operations with Proper Error Handling\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eView complete example\u003c/b\u003e - Create multiple groups with rollback on failure\u003c/summary\u003e\n\n```go\n// Create multiple groups with rollback on failure\nfunc createOrganizationStructure(ctx context.Context, client *keycloak.Client) error {\n    var createdGroups []string\n    defer func() {\n        if err := recover(); err != nil {\n            // Cleanup on panic\n            for _, groupID := range createdGroups {\n                _ = client.Groups.Delete(ctx, groupID)\n            }\n        }\n    }()\n    \n    // Create parent organization\n    orgID, err := client.Groups.Create(ctx, \"Acme Corp\", map[string][]string{\n        \"type\": {\"organization\"},\n    })\n    if err != nil {\n        return fmt.Errorf(\"create organization: %w\", err)\n    }\n    createdGroups = append(createdGroups, orgID)\n    \n    // Create departments\n    departments := []string{\"Engineering\", \"Sales\", \"Marketing\"}\n    for _, dept := range departments {\n        deptID, err := client.Groups.CreateSubGroup(ctx, orgID, dept, map[string][]string{\n            \"type\": {\"department\"},\n        })\n        if err != nil {\n            // Rollback all created groups\n            for _, id := range createdGroups {\n                _ = client.Groups.Delete(ctx, id)\n            }\n            return fmt.Errorf(\"create department %s: %w\", dept, err)\n        }\n        createdGroups = append(createdGroups, deptID)\n    }\n    \n    log.Printf(\"Successfully created organization with %d departments\", len(departments))\n    return nil\n}\n```\n\n\u003c/details\u003e\n\n#### Example 3: Context with Timeout and Cancellation\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eView complete example\u003c/b\u003e - Graceful shutdown with context cancellation\u003c/summary\u003e\n\n```go\n// Graceful shutdown with context cancellation\nfunc processGroupsWithCancellation(ctx context.Context, client *keycloak.Client) error {\n    // Create a context with timeout\n    ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)\n    defer cancel()\n    \n    // Listen for interrupt signal\n    sigChan := make(chan os.Signal, 1)\n    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)\n    \n    go func() {\n        \u003c-sigChan\n        log.Println(\"Interrupt received, cancelling operations...\")\n        cancel()\n    }()\n    \n    // Process groups page by page\n    first := 0\n    max := 100\n    \n    for {\n        select {\n        case \u003c-ctx.Done():\n            return ctx.Err()\n        default:\n            groups, err := client.Groups.ListPaginated(ctx, nil, false, first, max)\n            if err != nil {\n                return fmt.Errorf(\"list groups (page %d): %w\", first/max, err)\n            }\n            \n            if len(groups) == 0 {\n                break // No more groups\n            }\n            \n            // Process this batch\n            for _, group := range groups {\n                // Process each group - your custom logic here\n                log.Printf(\"Processing group: %s\", *group.Name)\n                \n                // Example: Update group attributes\n                if group.Attributes == nil {\n                    attrs := make(map[string][]string)\n                    group.Attributes = \u0026attrs\n                }\n                (*group.Attributes)[\"processed\"] = []string{time.Now().Format(time.RFC3339)}\n                \n                if err := client.Groups.Update(ctx, *group); err != nil {\n                    return fmt.Errorf(\"failed to update group %s: %w\", *group.ID, err)\n                }\n            }\n            \n            first += max\n        }\n    }\n    \n    return nil\n}\n```\n\n\u003c/details\u003e\n\n#### Example 4: Production Service with Dependency Injection\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eView complete example\u003c/b\u003e - Service pattern with dependency injection and structured logging\u003c/summary\u003e\n\n```go\n// Service struct for dependency injection\ntype KeycloakService struct {\n    client *keycloak.Client\n    logger *slog.Logger\n}\n\nfunc NewKeycloakService(ctx context.Context, cfg Config, logger *slog.Logger) (*KeycloakService, error) {\n    client, err := keycloak.New(ctx, keycloak.Config{\n        URL:          cfg.KeycloakURL,\n        Realm:        cfg.Realm,\n        ClientID:     cfg.ClientID,\n        ClientSecret: cfg.ClientSecret,\n    },\n        keycloak.WithTimeout(30*time.Second),\n        keycloak.WithRetry(3, 1*time.Second, 10*time.Second),\n        keycloak.WithUserAgent(fmt.Sprintf(\"myapp/%s\", cfg.Version)),\n    )\n    if err != nil {\n        return nil, fmt.Errorf(\"initialize keycloak client: %w\", err)\n    }\n    \n    return \u0026KeycloakService{\n        client: client,\n        logger: logger,\n    }, nil\n}\n\nfunc (s *KeycloakService) GetOrCreateGroup(ctx context.Context, name string) (*keycloak.Group, error) {\n    s.logger.InfoContext(ctx, \"looking up group\", \"name\", name)\n    \n    // Try to find by name\n    groups, err := s.client.Groups.List(ctx, \u0026name, false)\n    if err != nil {\n        s.logger.ErrorContext(ctx, \"failed to search groups\", \"error\", err)\n        return nil, fmt.Errorf(\"search groups: %w\", err)\n    }\n    \n    for _, group := range groups {\n        if *group.Name == name {\n            s.logger.InfoContext(ctx, \"found existing group\", \"id\", *group.ID)\n            return group, nil\n        }\n    }\n    \n    // Create if not found\n    groupID, err := s.client.Groups.Create(ctx, name, nil)\n    if err != nil {\n        s.logger.ErrorContext(ctx, \"failed to create group\", \"name\", name, \"error\", err)\n        return nil, fmt.Errorf(\"create group: %w\", err)\n    }\n    \n    s.logger.InfoContext(ctx, \"created new group\", \"id\", groupID)\n    return s.client.Groups.Get(ctx, groupID)\n}\n```\n\n\u003c/details\u003e\n\n## API Reference\n\n### Client Structure\n\nThe `Client` struct provides access to resource-specific clients:\n\n```go\ntype Client struct {\n    Groups GroupsClient  // Group management operations\n    // Future: Users, Roles, Organizations, etc.\n}\n```\n\n### GroupsClient Interface\n\nThe `GroupsClient` provides methods for managing Keycloak groups:\n\n#### Group Operations\n\n- `Create(ctx, name, attributes) (string, error)` - Create a new group\n- `Update(ctx, group) error` - Update an existing group\n- `Delete(ctx, groupID) error` - Delete a group\n- `Get(ctx, groupID) (*Group, error)` - Get group by ID\n- `List(ctx, search, briefRepresentation) ([]*Group, error)` - List all groups\n- `ListPaginated(ctx, search, briefRepresentation, first, max) ([]*Group, error)` - Get paginated groups\n- `ListWithSubGroups(ctx, searchQuery, briefRepresentation, first, max) ([]*Group, error)` - List groups with subgroups included\n- `ListWithParams(ctx, params) ([]*Group, error)` - List groups with full parameter control\n- `Count(ctx, search, top) (int, error)` - Get total count of groups\n- `GetByAttribute(ctx, attribute) (*Group, error)` - Find group by attribute\n\n#### Subgroup Operations\n\n- `CreateSubGroup(ctx, groupID, name, attributes) (string, error)` - Create a subgroup\n- `ListSubGroups(ctx, groupID) ([]*Group, error)` - Get all subgroups\n- `ListSubGroupsPaginated(ctx, groupID, params) ([]*Group, error)` - Get paginated subgroups with search\n- `GetSubGroupByID(group, subGroupID) (*Group, error)` - Find subgroup by ID\n- `GetSubGroupByAttribute(group, attribute) (*Group, error)` - Find subgroup by attribute\n\n#### Important: Working with Subgroups\n\n**Keycloak API Behavior**: Due to how Keycloak's REST API works, the `SubGroups` field is only populated in group responses when a `search` or `q` query parameter is provided. This is a limitation of Keycloak's API, not this library.\n\n**Two Approaches to Fetch Subgroups**:\n\n1. **Use `ListWithSubGroups()` (Recommended for hierarchies)**:\n\n   ```go\n   // Fetches groups with their subgroups included in the response\n   groups, err := client.Groups.ListWithSubGroups(ctx, \"search-term\", false, 0, 100)\n   for _, group := range groups {\n       if group.SubGroups != nil {\n           for _, subgroup := range *group.SubGroups {\n               fmt.Printf(\"Subgroup: %s\\n\", *subgroup.Name)\n           }\n       }\n   }\n   ```\n\n2. **Use `ListSubGroups()` (Explicit subgroup fetch)**:\n\n   ```go\n   // First get parent groups\n   groups, err := client.Groups.List(ctx, nil, false)\n   \n   // Then explicitly fetch subgroups for each parent\n   for _, group := range groups {\n       subgroups, err := client.Groups.ListSubGroups(ctx, *group.ID)\n       // Process subgroups...\n   }\n   ```\n\n**Note**: The `Get()` method does NOT populate the `SubGroups` field. Use `ListSubGroups()` if you need to fetch children of a specific group.\n\n## Models\n\n### Group\n\n```go\ntype Group struct {\n    ID          *string\n    Name        *string\n    Path        *string\n    SubGroups   *[]*Group\n    Attributes  *map[string][]string\n    Access      *map[string]bool\n    ClientRoles *map[string][]string\n    RealmRoles  *[]string\n}\n```\n\n### GroupAttribute\n\n```go\ntype GroupAttribute struct {\n    Key   string\n    Value string\n}\n```\n\n## Error Handling\n\nThe package uses standard Go error handling with sentinel errors for common cases:\n\n### Sentinel Errors\n\nThe library provides typed errors for common scenarios:\n\n- `keycloak.ErrGroupNotFound` - Group not found in search or lookup operations\n\n```go\nimport \"go.companyinfo.dev/keycloak\"\n\ngroup, err := client.Groups.GetByAttribute(ctx, attribute)\nif err == keycloak.ErrGroupNotFound {\n    log.Println(\"Group not found\")\n} else if err != nil {\n    log.Fatalf(\"Unexpected error: %v\", err)\n}\n```\n\n### HTTP Error Handling\n\n```go\n// Check for specific HTTP status codes\nif err != nil {\n    if strings.Contains(err.Error(), \"401\") {\n        // Authentication failed - check credentials\n        log.Fatal(\"Authentication failed. Check your client credentials.\")\n    } else if strings.Contains(err.Error(), \"403\") {\n        // Permission denied - check client roles\n        log.Fatal(\"Permission denied. Ensure client has required roles.\")\n    } else if strings.Contains(err.Error(), \"409\") {\n        // Conflict - resource already exists\n        log.Println(\"Resource already exists\")\n    }\n    return err\n}\n```\n\n### Best Error Handling Practices\n\n```go\n// Always wrap errors with context\nif err != nil {\n    return fmt.Errorf(\"failed to create group %s: %w\", groupName, err)\n}\n\n// Use context for timeout control\nctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\ndefer cancel()\n\ngroup, err := client.Groups.Get(ctx, groupID)\nif err != nil {\n    if ctx.Err() == context.DeadlineExceeded {\n        return fmt.Errorf(\"operation timed out: %w\", err)\n    }\n    return err\n}\n```\n\n## Best Practices\n\n### 1. Always Use Context with Timeout\n\n```go\n// ✅ Good: Prevents hanging requests\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\ndefer cancel()\n\n// ❌ Bad: Can hang indefinitely\nctx := context.Background()\n```\n\n### 2. Configure Retry Logic for Production\n\n```go\n// ✅ Good: Handles transient failures\nclient, err := keycloak.New(ctx, config,\n    keycloak.WithRetry(3, 1*time.Second, 10*time.Second),\n    keycloak.WithTimeout(30*time.Second),\n)\n\n// ❌ Bad: No retry, fails on first network hiccup\nclient, err := keycloak.New(ctx, config)\n```\n\n### 3. Use Pagination for Large Datasets\n\n\u003e **⚠️ Warning**: Loading all groups at once can cause memory issues and timeouts in large Keycloak installations.\n\n```go\n// ✅ Good: Memory efficient, handles any size\nfirst := 0\nmax := 100\nfor {\n    groups, err := client.Groups.ListPaginated(ctx, nil, false, first, max)\n    if err != nil || len(groups) == 0 {\n        break\n    }\n    processGroups(groups)\n    first += max\n}\n\n// ❌ Bad: Loads everything into memory\ngroups, err := client.Groups.List(ctx, nil, false)\n```\n\n### 4. Store Secrets Securely\n\n\u003e **🔒 Security**: Never commit credentials to version control. Use environment variables or secret management services.\n\n```go\n// ✅ Good: Load from environment or secret manager\nconfig := keycloak.Config{\n    URL:          os.Getenv(\"KEYCLOAK_URL\"),\n    Realm:        os.Getenv(\"KEYCLOAK_REALM\"),\n    ClientID:     os.Getenv(\"KEYCLOAK_CLIENT_ID\"),\n    ClientSecret: os.Getenv(\"KEYCLOAK_CLIENT_SECRET\"),\n}\n\n// ❌ Bad: Hardcoded credentials\nconfig := keycloak.Config{\n    ClientSecret: \"my-secret-12345\", // Never do this!\n}\n```\n\n### 5. Use Structured Logging\n\n\u003e **💡 Best Practice**: Use structured logging (like `slog`) for better observability and easier debugging.\n\n```go\nimport \"log/slog\"\n\n// ✅ Good: Structured logs with context\nlogger.InfoContext(ctx, \"creating group\",\n    slog.String(\"name\", groupName),\n    slog.String(\"operation\", \"group.create\"),\n    slog.Any(\"attributes\", attributes),\n)\n\nif err != nil {\n    logger.ErrorContext(ctx, \"failed to create group\",\n        slog.String(\"name\", groupName),\n        slog.String(\"error\", err.Error()),\n    )\n}\n\n// ❌ Bad: Unstructured logs (harder to parse and search)\nlog.Printf(\"Creating group %s with attributes %v\", groupName, attributes)\n```\n\n### 6. Handle Idempotency\n\n```go\n// ✅ Good: Check before create\nfunc ensureGroupExists(ctx context.Context, client *keycloak.Client, name string) (string, error) {\n    // Try to find existing\n    groups, err := client.Groups.List(ctx, \u0026name, false)\n    if err != nil {\n        return \"\", err\n    }\n    \n    for _, g := range groups {\n        if *g.Name == name {\n            return *g.ID, nil\n        }\n    }\n    \n    // Create if not found\n    return client.Groups.Create(ctx, name, nil)\n}\n```\n\n### 7. Add Request Tracing\n\n```go\n// ✅ Good: Add tracing headers\nclient, err := keycloak.New(ctx, config,\n    keycloak.WithHeaders(map[string]string{\n        \"X-Request-ID\": generateRequestID(),\n        \"X-Service\":    \"my-service\",\n    }),\n)\n```\n\n### 8. Test with Mocks\n\n```go\n// ✅ Good: Use interface for testing\ntype GroupManager interface {\n    Create(ctx context.Context, name string, attrs map[string][]string) (string, error)\n    Get(ctx context.Context, id string) (*keycloak.Group, error)\n}\n\n// Your service depends on interface, not concrete implementation\ntype MyService struct {\n    groups GroupManager\n}\n```\n\n## Troubleshooting\n\n### Common Issues and Solutions\n\n#### Authentication Failures (401 Unauthorized)\n\n**Problem**: Client cannot authenticate with Keycloak.\n\n**Solutions**:\n\n1. **Verify credentials**:\n\n   ```bash\n   # Test with curl\n   curl -X POST \"https://keycloak.example.com/realms/my-realm/protocol/openid-connect/token\" \\\n     -d \"client_id=admin-cli\" \\\n     -d \"client_secret=your-secret\" \\\n     -d \"grant_type=client_credentials\"\n   ```\n\n2. **Check client configuration**:\n\n   - Client authentication: Must be ON\n   - Service accounts enabled: Must be ON\n   - Access Type: confidential\n\n3. **Verify realm name**: Case-sensitive!\n\n#### Permission Denied (403 Forbidden)\n\n**Problem**: Client authenticated but lacks permissions.\n\n**Solutions**:\n\n1. **Check service account roles**:\n   - Go to Clients → your client → Service accounts roles\n   - Assign necessary roles: `manage-groups`, `view-users`, etc.\n   - For full admin: assign `realm-admin` role\n\n2. **Check fine-grained permissions**: Some operations require specific permissions\n\n#### Connection Timeouts\n\n**Problem**: Requests hang or timeout.\n\n**Solutions**:\n\n1. **Add timeout to context**:\n\n   ```go\n   ctx, cancel := context.WithTimeout(ctx, 30*time.Second)\n   defer cancel()\n   ```\n\n2. **Configure client timeout**:\n\n   ```go\n   client, err := keycloak.New(ctx, config,\n       keycloak.WithTimeout(30*time.Second),\n   )\n   ```\n\n3. **Check network connectivity**:\n\n   ```bash\n   curl -v https://keycloak.example.com\n   ```\n\n#### Certificate Errors (TLS/SSL)\n\n**Problem**: `x509: certificate signed by unknown authority`\n\n\u003e **🔒 Security Warning**: Never use `InsecureSkipVerify` in production. It disables certificate validation and makes you vulnerable to man-in-the-middle attacks.\n\n**Solutions**:\n\n1. **For development only** - Skip verification (NOT FOR PRODUCTION):\n\n   ```go\n   import (\n       \"crypto/tls\"\n       \"net/http\"\n   )\n   \n   httpClient := \u0026http.Client{\n       Transport: \u0026http.Transport{\n           TLSClientConfig: \u0026tls.Config{\n               InsecureSkipVerify: true, // ⚠️ NEVER in production!\n           },\n       },\n   }\n   client, err := keycloak.New(ctx, config,\n       keycloak.WithHTTPClient(httpClient),\n   )\n   ```\n\n2. **For production** - Add CA certificate:\n\n   ```go\n   import (\n       \"crypto/tls\"\n       \"crypto/x509\"\n       \"net/http\"\n       \"os\"\n   )\n   \n   caCert, err := os.ReadFile(\"/path/to/ca.crt\")\n   if err != nil {\n       return err\n   }\n   \n   caCertPool := x509.NewCertPool()\n   if !caCertPool.AppendCertsFromPEM(caCert) {\n       return fmt.Errorf(\"failed to parse CA certificate\")\n   }\n   \n   httpClient := \u0026http.Client{\n       Transport: \u0026http.Transport{\n           TLSClientConfig: \u0026tls.Config{\n               RootCAs:    caCertPool,\n               MinVersion: tls.VersionTLS12,\n           },\n       },\n       Timeout: 30 * time.Second,\n   }\n   \n   client, err := keycloak.New(ctx, config,\n       keycloak.WithHTTPClient(httpClient),\n   )\n   ```\n\n#### Group Not Found Errors\n\n**Problem**: Cannot find groups that exist in Keycloak.\n\n**Solutions**:\n\n1. **Check search is case-sensitive**: Use exact name\n2. **Use briefRepresentation=false**: Gets full group details including attributes\n3. **Check group path**: Groups might be subgroups\n\n#### Rate Limiting (429 Too Many Requests)\n\n**Problem**: Too many requests to Keycloak.\n\n**Solutions**:\n\n1. **Enable retry with backoff**:\n\n   ```go\n   client, err := keycloak.New(ctx, config,\n       keycloak.WithRetry(5, 2*time.Second, 30*time.Second),\n   )\n   ```\n\n2. **Implement request throttling**: Use `time.Ticker` or rate limiter\n3. **Batch operations**: Use pagination instead of individual requests\n\n#### Memory Issues with Large Datasets\n\n**Problem**: Application runs out of memory when fetching many groups.\n\n**Solution**: Always use pagination:\n\n```go\n// ✅ Good: Process in chunks\nfirst := 0\nmax := 100\nfor {\n    groups, err := client.Groups.ListPaginated(ctx, nil, false, first, max)\n    if err != nil || len(groups) == 0 {\n        break\n    }\n    processGroups(groups) // Process and discard\n    first += max\n}\n```\n\n### Debug Mode\n\nEnable debug logging to see all requests and responses:\n\n```go\nclient, err := keycloak.New(ctx, config,\n    keycloak.WithDebug(true),\n)\n```\n\n## FAQ\n\n### General Questions\n\n**Q: Is this library production-ready?**  \nA: Yes. It includes automatic token refresh, retry logic, comprehensive error handling, and has been battle-tested in production environments.\n\n**Q: What versions of Keycloak are supported?**  \nA: Keycloak 26.x and later. The library is tested against Keycloak 26.\n\n**Q: Does it support Keycloak 25 or earlier?**  \nA: It may work, but it's not officially tested.\n\n**Q: Can I use this with Red Hat SSO?**  \nA: Yes, Red Hat SSO is based on Keycloak, so this library should work.\n\n### Authentication\n\n**Q: What authentication methods are supported?**  \nA: Currently only OAuth2 client credentials flow (service accounts). This is the recommended method for server-to-server communication.\n\n**Q: Can I use username/password authentication?**  \nA: Not currently. Client credentials flow is more secure for automated processes.\n\n**Q: How often do tokens refresh?**  \nA: Tokens are automatically refreshed before expiration. You don't need to handle this.\n\n### Feature Support\n\n**Q: Does this support user management?**  \nA: Not yet. Currently only group management is implemented. User management is planned for a future release.\n\n**Q: Can I manage roles?**  \nA: Not yet. Role management is planned for a future release.\n\n**Q: What about realm management?**  \nA: Not currently. The library focuses on resource management within a realm.\n\n**Q: Can I create custom attributes?**  \nA: Yes! Groups support arbitrary attributes as `map[string][]string`.\n\n### Performance\n\n**Q: How many requests per second can it handle?**  \nA: This depends on your Keycloak instance. The library includes retry logic and supports concurrent requests.\n\n**Q: Should I create one client per request or reuse it?**  \nA: **Always reuse the client**. Create one client at application startup and reuse it throughout your application's lifetime. The client:\n\n- Maintains HTTP connection pools (expensive to recreate)\n- Caches OAuth2 access tokens (avoids repeated authentication)\n- Is safe for concurrent use across goroutines\n- Creating new clients for each request wastes resources and degrades performance\n\n```go\n// ✅ Correct: Single client, initialized once\nvar client *keycloak.Client\n\nfunc main() {\n    var err error\n    client, err = keycloak.New(context.Background(), config)\n    // ... use client throughout application lifecycle\n}\n```\n\n**Q: Does it support connection pooling?**  \nA: Yes, through the underlying `http.Client`. You can customize this with `WithHTTPClient()`.\n\n### Testing and Development\n\n**Q: How do I test code that uses this library?**  \nA: The library uses interfaces (`GroupsClient`, etc.) that you can mock. See the test files for examples.\n\n**Q: Can I run tests without a real Keycloak instance?**  \nA: Yes. Unit tests and mock suite tests don't require Keycloak. Only integration tests need a real instance.\n\n**Q: Should I test against production Keycloak?**  \nA: **Never!** Always use a dedicated test realm. Integration tests can create/delete resources.\n\n### Errors and Edge Cases\n\n**Q: What happens if Keycloak is down?**  \nA: Requests will fail after the configured timeout and retry attempts. Use appropriate error handling and monitoring.\n\n**Q: Are operations atomic?**  \nA: No. Keycloak API calls are independent. If you need transaction-like behavior, implement compensating operations (see Example 2 above).\n\n**Q: What if I create duplicate groups?**  \nA: Keycloak allows groups with the same name. Use attributes (like `externalID`) to enforce uniqueness in your application.\n\n### Configuration Options\n\n**Q: How do I use a proxy?**  \nA: Use `WithProxy(proxyURL)` option when creating the client.\n\n**Q: Can I customize HTTP headers?**  \nA: Yes, use `WithHeaders(map[string]string{...})` for headers on all requests.\n\n**Q: What's the default timeout?**  \nA: The client itself doesn't set a default timeout, so requests will use Go's standard HTTP client behavior (no timeout). **Always set an explicit timeout** using `WithTimeout()` when creating the client or use `context.WithTimeout()` for individual operations. This prevents operations from hanging indefinitely.\n\n```go\n// Recommended: Set timeout when creating client\nclient, err := keycloak.New(ctx, config,\n    keycloak.WithTimeout(30*time.Second),\n)\n\n// Or use context timeout for specific operations\nctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)\ndefer cancel()\ngroups, err := client.Groups.List(ctx, nil, false)\n```\n\n## Architecture\n\nThe package follows a resource-based client design pattern:\n\n- **Main Client**: Entry point that holds resource-specific clients\n- **Resource Clients**: Focused interfaces for each Keycloak resource (Groups, Users, Roles, etc.)\n- **Shared State**: Authentication and configuration shared across all resource clients\n\nThis design makes it easy to add new Keycloak resources without bloating a single interface, and allows for better organization and testability.\n\n### Performance Characteristics\n\nUnderstanding the performance profile helps you optimize your application:\n\n**Connection Management**:\n\n- HTTP connection pooling automatically managed by Go's `http.Client`\n- Keep-alive connections reused across requests\n- Default pool size: 100 idle connections, 10 per host\n\n**Authentication**:\n\n- OAuth2 access tokens cached automatically\n- Token refresh handled transparently before expiration\n- Minimal authentication overhead after initial setup\n\n**Memory Usage**:\n\n- Client baseline: ~2-5 MB (includes HTTP client, configuration, token cache)\n- Per group object: ~1-2 KB (varies with attributes and subgroups)\n- 10,000 groups in memory: ~10-20 MB\n- Use pagination to control memory footprint\n\n**Concurrency**:\n\n- **Thread-safe**: Safe for concurrent use across multiple goroutines\n- No locking in read paths (immutable after initialization)\n- Recommended: Create one client at startup, reuse across application\n\n**Best Practices for Performance**:\n\n```go\n// ✅ Good: Single client instance, reused everywhere\nvar keycloakClient *keycloak.Client\n\nfunc init() {\n    var err error\n    keycloakClient, err = keycloak.New(context.Background(), config)\n    // handle error\n}\n\n// ❌ Bad: Creating new client for each request (wastes resources)\nfunc handleRequest() {\n    client, _ := keycloak.New(context.Background(), config) // Don't do this!\n}\n```\n\n**Throughput Considerations**:\n\n- Bottleneck is typically Keycloak server, not this client\n- Client can handle hundreds of concurrent requests\n- Use context timeouts to prevent slow requests from blocking\n- Consider rate limiting for bulk operations\n\n## Configuration\n\n### Required Configuration\n\nThe `Config` struct requires the following fields:\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| `URL` | string | ✅ | Keycloak server URL (e.g., `https://keycloak.example.com`) |\n| `Realm` | string | ✅ | Keycloak realm name |\n| `ClientID` | string | ✅ | OAuth2 client ID |\n| `ClientSecret` | string | ✅ | OAuth2 client secret |\n\n**Example**:\n\n```go\nconfig := keycloak.Config{\n    URL:          \"https://keycloak.example.com\",\n    Realm:        \"production\",\n    ClientID:     \"backend-service\",\n    ClientSecret: os.Getenv(\"KEYCLOAK_CLIENT_SECRET\"),\n}\n```\n\n### Environment-Based Configuration\n\nBest practice: Load configuration from environment variables:\n\n```go\nimport (\n    \"os\"\n    \"log\"\n)\n\nfunc loadConfig() keycloak.Config {\n    config := keycloak.Config{\n        URL:          getEnv(\"KEYCLOAK_URL\", \"\"),\n        Realm:        getEnv(\"KEYCLOAK_REALM\", \"\"),\n        ClientID:     getEnv(\"KEYCLOAK_CLIENT_ID\", \"\"),\n        ClientSecret: getEnv(\"KEYCLOAK_CLIENT_SECRET\", \"\"),\n    }\n    \n    // Validate required fields\n    if config.URL == \"\" || config.Realm == \"\" || \n       config.ClientID == \"\" || config.ClientSecret == \"\" {\n        log.Fatal(\"Missing required Keycloak configuration\")\n    }\n    \n    return config\n}\n\nfunc getEnv(key, fallback string) string {\n    if value := os.Getenv(key); value != \"\" {\n        return value\n    }\n    return fallback\n}\n```\n\n**Environment Variables**:\n\n```bash\nexport KEYCLOAK_URL=\"https://keycloak.example.com\"\nexport KEYCLOAK_REALM=\"production\"\nexport KEYCLOAK_CLIENT_ID=\"backend-service\"\nexport KEYCLOAK_CLIENT_SECRET=\"your-secret-here\"\n```\n\n### Optional Configuration\n\nUse functional options to customize client behavior:\n\n| Option | Description | Default | Recommendation |\n|--------|-------------|---------|----------------|\n| `WithPageSize(size int)` | Default page size for pagination | 50 | Use 100-500 for batch operations |\n| `WithTimeout(duration)` | Request timeout for API calls | No timeout | **Always set** (e.g., 30s) |\n| `WithRetry(count, wait, maxWait)` | Retry behavior with exponential backoff | No retry | Use 3-5 retries for production |\n| `WithDebug(bool)` | Enable debug logging | false | Only in development |\n| `WithHeaders(map[string]string)` | Add custom headers | None | Use for tracing/correlation IDs |\n| `WithUserAgent(string)` | Set custom User-Agent | \"\" | Include app name/version |\n| `WithProxy(proxyURL)` | Configure HTTP proxy | None | As needed for your network |\n| `WithHTTPClient(*http.Client)` | Use custom HTTP client | Default | For advanced scenarios only |\n\n### Recommended Production Configuration\n\n```go\nclient, err := keycloak.New(ctx, config,\n    // Essential for production\n    keycloak.WithTimeout(30*time.Second),                    // Prevent hanging\n    keycloak.WithRetry(3, 1*time.Second, 10*time.Second),   // Handle transient failures\n    \n    // Recommended for operations and debugging\n    keycloak.WithUserAgent(fmt.Sprintf(\"myapp/%s\", version)), // Identify your app\n    keycloak.WithHeaders(map[string]string{\n        \"X-Service\": \"backend-api\",                             // For tracking\n    }),\n    \n    // Optional based on your needs\n    keycloak.WithPageSize(100),                              // For bulk operations\n)\n```\n\n### Advanced HTTP Client Configuration\n\nFor custom TLS, proxy, or connection pooling:\n\n```go\nimport (\n    \"crypto/tls\"\n    \"net/http\"\n    \"time\"\n)\n\n// Custom HTTP client with connection pooling\nhttpClient := \u0026http.Client{\n    Transport: \u0026http.Transport{\n        MaxIdleConns:        100,\n        MaxIdleConnsPerHost: 10,\n        IdleConnTimeout:     90 * time.Second,\n        TLSClientConfig: \u0026tls.Config{\n            MinVersion: tls.VersionTLS12,\n        },\n    },\n    Timeout: 30 * time.Second,\n}\n\nclient, err := keycloak.New(ctx, config,\n    keycloak.WithHTTPClient(httpClient),\n)\n```\n\nSee the [Advanced Configuration](#advanced-configuration-with-options) section for more usage examples.\n\n## License\n\nThis project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.\n\n### What This Means\n\n- ✅ **Commercial use** - Use in commercial products\n- ✅ **Modification** - Modify the source code\n- ✅ **Distribution** - Distribute the library\n- ✅ **Patent use** - Patent grant included\n- ⚠️ **Trademark** - No trademark rights granted\n- ⚠️ **Liability** - No warranty provided\n- ⚠️ **State changes** - Must document modifications\n\n## Testing\n\nThe project includes comprehensive test coverage with multiple test types:\n\n### Test Structure\n\n- **Unit Tests** (`*_test.go`) - Test individual components without external dependencies\n- **Mock Suite Tests** (`groups_mock_suite_test.go`) - Test API operations using HTTP mocks\n- **Integration Tests** (`groups_integration_suite_test.go`) - Test against a real Keycloak instance\n\n### Running Tests\n\n#### Run Unit Tests Only\n\n```bash\n# Fast unit tests with mocks\ngo test -v ./...\n\n# Or with short flag to skip integration tests\ngo test -v -short ./...\n```\n\n#### Run Mock Suite Tests\n\n```bash\n# Run just the mock suite\ngo test -v -run TestGroupsMockSuite ./...\n```\n\n#### Run Integration Tests\n\nIntegration tests require a running Keycloak instance. Set up your environment first:\n\n\u003e **⚠️ CRITICAL WARNING**: Integration tests create and delete resources. **NEVER** run them against production! Always use a dedicated test realm.\n\n```bash\n# Copy the example environment file\ncp .env.example .env\n\n# Edit .env with your Keycloak credentials\n# Then run integration tests\ngo test -v -tags=integration ./...\n```\n\n**Required environment variables for integration tests:**\n\n- `KEYCLOAK_URL` - Keycloak server URL (e.g., `http://localhost:8080`)\n- `KEYCLOAK_REALM` - Test realm name (use a dedicated test realm!)\n- `KEYCLOAK_CLIENT_ID` - Client ID with admin privileges\n- `KEYCLOAK_CLIENT_SECRET` - Client secret\n\n\u003e **💡 Tip**: Use Docker to run a local Keycloak instance for testing. See setup instructions below.\n\n#### Run All Tests\n\n```bash\n# Run all tests including integration tests\ngo test -v -tags=integration ./...\n```\n\n### Test Coverage\n\nGenerate and view test coverage:\n\n```bash\n# Generate coverage report\ngo test -v -cover -coverprofile=coverage.out ./...\n\n# View coverage in browser\ngo tool cover -html=coverage.out\n\n# Or view in terminal\ngo tool cover -func=coverage.out\n```\n\n### Setting Up Keycloak for Integration Tests\n\n1. **Run Keycloak locally** (using Docker):\n\n   ```bash\n   docker run -d \\\n     --name keycloak-test \\\n     -p 8080:8080 \\\n     -e KEYCLOAK_ADMIN=admin \\\n     -e KEYCLOAK_ADMIN_PASSWORD=admin \\\n     quay.io/keycloak/keycloak:latest \\\n     start-dev\n   ```\n\n2. **Create a test realm:**\n   - Access Keycloak Admin Console at \u003chttp://localhost:8080\u003e\n   - Create a new realm (e.g., `test-realm`)\n\n3. **Create a client for testing:**\n   - Go to Clients → Create\n   - Client ID: `admin-cli` (or custom name)\n   - Client authentication: ON\n   - Service accounts enabled: ON\n   - Save\n\n4. **Assign admin roles:**\n   - Go to Clients → your client → Service accounts roles\n   - Assign Client role: `realm-admin` (or at minimum `manage-groups`)\n\n5. **Get credentials:**\n   - Go to Clients → your client → Credentials\n   - Copy the client secret\n\n6. **Update .env file:**\n\n   ```env\n   KEYCLOAK_URL=http://localhost:8080\n   KEYCLOAK_REALM=test-realm\n   KEYCLOAK_CLIENT_ID=admin-cli\n   KEYCLOAK_CLIENT_SECRET=your-copied-secret\n   ```\n\n### Writing Tests\n\nThe project uses [testify/suite](https://github.com/stretchr/testify#suite-package) for organized test suites and [testify/assert](https://github.com/stretchr/testify#assert-package) for readable assertions.\n\n#### Example Unit Test\n\n```go\nfunc TestWithPageSize(t *testing.T) {\n    tests := []struct {\n        name      string\n        size      int\n        wantErr   bool\n        wantValue int\n    }{\n        {\n            name:      \"valid page size\",\n            size:      100,\n            wantErr:   false,\n            wantValue: 100,\n        },\n        {\n            name:    \"invalid page size\",\n            size:    -1,\n            wantErr: true,\n        },\n    }\n\n    for _, tt := range tests {\n        t.Run(tt.name, func(t *testing.T) {\n            client := \u0026Client{pageSize: defaultSize}\n            err := WithPageSize(tt.size)(client)\n\n            if tt.wantErr {\n                assert.Error(t, err)\n            } else {\n                assert.NoError(t, err)\n                assert.Equal(t, tt.wantValue, client.pageSize)\n            }\n        })\n    }\n}\n```\n\n#### Example Mock Suite Test\n\n```go\nfunc (s *GroupsMockSuite) TestGetGroupSuccess() {\n    groupID := \"test-group-id\"\n    expectedGroup := \u0026Group{\n        ID:   StringP(groupID),\n        Name: StringP(\"Test Group\"),\n    }\n\n    path := fmt.Sprintf(\"/admin/realms/%s/groups/%s\", s.mockRealm, groupID)\n    s.mockJSONResponse(http.MethodGet, path, http.StatusOK, expectedGroup)\n\n    group, err := s.client.Groups.Get(s.ctx, groupID)\n    \n    s.NoError(err)\n    s.NotNil(group)\n    s.Equal(*expectedGroup.ID, *group.ID)\n}\n```\n\n#### Example Integration Test\n\n```go\nfunc (s *GroupsIntegrationTestSuite) TestGroupLifecycle() {\n    // Create\n    groupID, err := s.client.Groups.Create(s.ctx, \"Test Group\", nil)\n    s.Require().NoError(err)\n    s.trackGroup(groupID) // Auto-cleanup\n\n    // Read\n    group, err := s.client.Groups.Get(s.ctx, groupID)\n    s.NoError(err)\n    s.Equal(\"Test Group\", *group.Name)\n\n    // Update\n    group.Description = keycloak.StringP(\"Updated\")\n    err = s.client.Groups.Update(s.ctx, *group)\n    s.NoError(err)\n\n    // Delete\n    err = s.client.Groups.Delete(s.ctx, groupID)\n    s.NoError(err)\n}\n```\n\n### Continuous Integration\n\nThe tests are designed to run in CI/CD pipelines:\n\n```yaml\n# Example GitHub Actions workflow\nname: Tests\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-go@v4\n        with:\n          go-version: '1.24'\n      \n      # Run unit and mock tests\n      - name: Unit Tests\n        run: go test -v -short -cover ./...\n      \n      # Optional: Run integration tests if Keycloak is available\n      - name: Integration Tests\n        env:\n          KEYCLOAK_URL: ${{ secrets.KEYCLOAK_URL }}\n          KEYCLOAK_REALM: ${{ secrets.KEYCLOAK_REALM }}\n          KEYCLOAK_CLIENT_ID: ${{ secrets.KEYCLOAK_CLIENT_ID }}\n          KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }}\n        run: go test -v -tags=integration ./...\n        if: env.KEYCLOAK_URL != ''\n```\n\n## Contributing\n\nContributions are welcome! We appreciate your help in making this library better.\n\n### How to Contribute\n\n1. **Fork the repository**\n2. **Create a feature branch**: `git checkout -b feature/your-feature-name`\n3. **Make your changes** with clear, focused commits\n4. **Add tests** for new functionality\n5. **Run tests**: `go test -v ./...`\n6. **Update documentation** if needed\n7. **Submit a pull request**\n\n### Code Standards\n\n- Follow [Effective Go](https://golang.org/doc/effective_go) guidelines\n- Use `gofmt` to format your code\n- Add meaningful comments for exported functions\n- Keep functions small and focused\n- Write tests for all new functionality\n- Maintain backward compatibility when possible\n\n### Testing Requirements\n\nAll contributions must include appropriate tests:\n\n- **Unit tests** for new functions\n- **Mock tests** for API interactions\n- **Integration tests** for critical paths (when applicable)\n\nRun all tests before submitting:\n\n```bash\n# Unit and mock tests\ngo test -v -short ./...\n\n# With coverage\ngo test -v -cover -coverprofile=coverage.out ./...\n\n# Integration tests (requires Keycloak)\ngo test -v -tags=integration ./...\n```\n\n### What We're Looking For\n\n**High Priority**:\n\n- Bug fixes with test cases\n- Performance improvements\n- Documentation improvements\n- Additional resource clients (Users, Roles, etc.)\n\n**Welcome**:\n\n- New features with clear use cases\n- Code quality improvements\n- Example applications\n\n**Please Discuss First**:\n\n- Breaking API changes\n- Major architectural changes\n- Large new features\n\n### Reporting Issues\n\nWhen reporting bugs, please include:\n\n1. **Go version**: `go version`\n2. **Keycloak version**\n3. **Library version**\n4. **Minimal reproduction code**\n5. **Expected vs actual behavior**\n6. **Error messages** (with stack traces if applicable)\n\n### Questions and Support\n\n- **Documentation issues**: Open an issue with the \"documentation\" label\n- **Usage questions**: Check the [FAQ](#faq) first, then open a discussion\n- **Bug reports**: Open an issue with reproduction steps\n- **Feature requests**: Open an issue describing the use case\n\n### Development Setup\n\n1. Clone the repository:\n\n   ```bash\n   git clone https://github.com/companyinfo/keycloak.git\n   cd keycloak\n   ```\n\n2. Install dependencies:\n\n   ```bash\n   go mod download\n   ```\n\n3. Run tests:\n\n   ```bash\n   go test -v ./...\n   ```\n\n4. (Optional) Set up Keycloak for integration tests:\n\n   ```bash\n   # See Testing section for detailed setup\n   cp .env.example .env\n   # Edit .env with your test Keycloak credentials\n   ```\n\n### Code Review Process\n\n1. All submissions require review\n2. We aim to review PRs within 3-5 business days\n3. Address feedback promptly\n4. Once approved, maintainers will merge\n\nThank you for contributing! 🙏\n\n## Acknowledgments\n\nBuilt with:\n\n- [go-resty](https://github.com/go-resty/resty) - HTTP client library\n- [testify](https://github.com/stretchr/testify) - Testing toolkit\n\nInspired by the Keycloak community and Go best practices.\n\n---\n\n**Maintained by** [CompanyInfo](https://github.com/companyinfo)\n\n**Found this useful?** Give it a star ⭐ to show your support!\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcompanyinfo%2Fkeycloak","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcompanyinfo%2Fkeycloak","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcompanyinfo%2Fkeycloak/lists"}