diff --git a/README.md b/README.md index 577d31f..db318af 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ func main() { ```go package main -import (\n "context" +import ( "context" "fmt" "os" @@ -59,17 +59,28 @@ func main() { } params := accessgrid.ProvisionParams{ - CardTemplateID: "0xd3adb00b5", + CardTemplateID: "0xd3adb00b5", EmployeeID: "123456789", - CardNumber: "14563", - SiteCode: "42", - FullName: "Employee name", - Email: "employee@yourwebsite.com", - PhoneNumber: "+19547212241", - Classification: "full_time", - StartDate: time.Now().UTC(), - ExpirationDate: time.Now().UTC().AddDate(1, 0, 0), - EmployeePhoto: "[image_in_base64_encoded_format]", + TagID: "DDEADB33FB00B5", + AllowOnMultipleDevices: true, + FullName: "Employee name", + Email: "employee@yourwebsite.com", + PhoneNumber: "+19547212241", + Classification: "full_time", + Department: "Engineering", + Location: "San Francisco", + SiteName: "HQ Building A", + Workstation: "4F-207", + MailStop: "MS-401", + CompanyAddress: "123 Main St, San Francisco, CA 94105", + StartDate: time.Now().UTC(), + ExpirationDate: time.Now().UTC().AddDate(0, 3, 0), + EmployeePhoto: "[image_in_base64_encoded_format]", + Title: "Engineering Manager", + Metadata: map[string]interface{}{ + "department": "engineering", + "badge_type": "contractor", + }, } ctx := context.Background() @@ -487,6 +498,110 @@ fmt.Printf("Completed registration for org: %s\n", result.Name) fmt.Printf("Status: %s\n", result.Status) ``` +### Landing Pages + +#### List landing pages + +```go +ctx := context.Background() +landingPages, err := client.Console.ListLandingPages(ctx) +if err != nil { + fmt.Printf("Error listing landing pages: %v\n", err) + return +} + +for _, page := range landingPages { + fmt.Printf("ID: %s, Name: %s, Kind: %s\n", page.ID, page.Name, page.Kind) + fmt.Printf(" Password Protected: %v\n", page.PasswordProtected) + if page.LogoURL != "" { + fmt.Printf(" Logo URL: %s\n", page.LogoURL) + } +} +``` + +#### Create a landing page + +```go +ctx := context.Background() +params := accessgrid.CreateLandingPageParams{ + Name: "Miami Office Access Pass", + Kind: "universal", + AdditionalText: "Welcome to the Miami Office", + BgColor: "#f1f5f9", + AllowImmediateDownload: true, +} + +landingPage, err := client.Console.CreateLandingPage(ctx, params) +if err != nil { + fmt.Printf("Error creating landing page: %v\n", err) + return +} + +fmt.Printf("Landing page created: %s\n", landingPage.ID) +fmt.Printf("Name: %s, Kind: %s\n", landingPage.Name, landingPage.Kind) +``` + +#### Update a landing page + +```go +ctx := context.Background() +params := accessgrid.UpdateLandingPageParams{ + LandingPageID: "0xlandingpage1d", + Name: "Updated Miami Office Access Pass", + AdditionalText: "Welcome! Tap below to get your access pass.", + BgColor: "#e2e8f0", +} + +landingPage, err := client.Console.UpdateLandingPage(ctx, params) +if err != nil { + fmt.Printf("Error updating landing page: %v\n", err) + return +} + +fmt.Printf("Landing page updated: %s\n", landingPage.ID) +fmt.Printf("Name: %s\n", landingPage.Name) +``` + +### Credential Profiles + +#### List credential profiles + +```go +ctx := context.Background() +profiles, err := client.Console.CredentialProfiles.List(ctx) +if err != nil { + fmt.Printf("Error listing credential profiles: %v\n", err) + return +} + +for _, profile := range profiles { + fmt.Printf("ID: %s, Name: %s, AID: %s\n", profile.ID, profile.Name, profile.AID) +} +``` + +#### Create a credential profile + +```go +ctx := context.Background() +params := accessgrid.CreateCredentialProfileParams{ + Name: "Main Office Profile", + AppName: "KEY-ID-main", + Keys: []accessgrid.KeyParam{ + {Value: "your_32_char_hex_master_key_here"}, + {Value: "your_32_char_hex__read_key__here"}, + }, +} + +profile, err := client.Console.CredentialProfiles.Create(ctx, params) +if err != nil { + fmt.Printf("Error creating credential profile: %v\n", err) + return +} + +fmt.Printf("Profile created: %s\n", profile.ID) +fmt.Printf("AID: %s\n", profile.AID) +``` + ## Configuration The SDK can be configured with custom options: @@ -557,6 +672,11 @@ Never expose your `secretKey` in source code. Always use environment variables o | GET /v1/console/webhooks | `Console.Webhooks.List()` | Y | | POST /v1/console/webhooks | `Console.Webhooks.Create()` | Y | | DELETE /v1/console/webhooks/{id} | `Console.Webhooks.Delete()` | Y | +| GET /v1/console/landing-pages | `Console.ListLandingPages()` | Y | +| POST /v1/console/landing-pages | `Console.CreateLandingPage()` | Y | +| PUT /v1/console/landing-pages/{id} | `Console.UpdateLandingPage()` | Y | +| GET /v1/console/credential-profiles | `Console.CredentialProfiles.List()` | Y | +| POST /v1/console/credential-profiles | `Console.CredentialProfiles.Create()` | Y | | POST /v1/console/hid/orgs | `Console.HID.Orgs.Create()` | Y | | POST /v1/console/hid/orgs/activate | `Console.HID.Orgs.Activate()` | Y | | GET /v1/console/hid/orgs | `Console.HID.Orgs.List()` | Y | diff --git a/accessgrid.go b/accessgrid.go index 9d0cc3b..b81c8b8 100644 --- a/accessgrid.go +++ b/accessgrid.go @@ -134,4 +134,22 @@ type ( // CompleteHIDOrgParams defines parameters for completing HID org registration CompleteHIDOrgParams = models.CompleteHIDOrgParams + + // LandingPage represents a landing page configuration + LandingPage = models.LandingPage + + // CreateLandingPageParams defines parameters for creating a landing page + CreateLandingPageParams = models.CreateLandingPageParams + + // UpdateLandingPageParams defines parameters for updating a landing page + UpdateLandingPageParams = models.UpdateLandingPageParams + + // CredentialProfile represents a credential profile + CredentialProfile = models.CredentialProfile + + // KeyParam represents a key parameter for credential profile creation + KeyParam = models.KeyParam + + // CreateCredentialProfileParams defines parameters for creating a credential profile + CreateCredentialProfileParams = models.CreateCredentialProfileParams ) diff --git a/client/client.go b/client/client.go index 1dfc145..6d4f7aa 100644 --- a/client/client.go +++ b/client/client.go @@ -136,13 +136,13 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac Error string `json:"error"` RequestID string `json:"request_id"` } - + apiError := &APIError{ StatusCode: resp.StatusCode, RawBody: string(respBody), RequestID: resp.Header.Get("X-Request-ID"), // Extract request ID from header if available } - + if err := json.Unmarshal(respBody, &apiErrorResp); err != nil { apiError.Message = string(respBody) } else { @@ -154,12 +154,12 @@ func (c *Client) Request(ctx context.Context, method, path string, body interfac } else { apiError.Message = string(respBody) } - + if apiErrorResp.RequestID != "" { apiError.RequestID = apiErrorResp.RequestID } } - + return apiError } @@ -193,4 +193,4 @@ func (c *Client) signRequest(payload []byte) (string, error) { } return fmt.Sprintf("%x", h.Sum(nil)), nil -} \ No newline at end of file +} diff --git a/client/client_test.go b/client/client_test.go index e2587b0..e255673 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -248,4 +248,4 @@ func TestClientRequest_ErrorResponses(t *testing.T) { } }) } -} \ No newline at end of file +} diff --git a/models/models.go b/models/models.go index 4171ff7..345d1dc 100644 --- a/models/models.go +++ b/models/models.go @@ -14,29 +14,31 @@ type Device struct { // Card represents an NFC key or access pass type Card struct { - ID string `json:"id"` - CardTemplateID string `json:"card_template_id"` - EmployeeID string `json:"employee_id"` - CardNumber string `json:"card_number"` - SiteCode string `json:"site_code,omitempty"` - FullName string `json:"full_name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - Classification string `json:"classification"` - StartDate time.Time `json:"start_date"` - ExpirationDate time.Time `json:"expiration_date"` - EmployeePhoto string `json:"employee_photo"` - State string `json:"state"` - URL string `json:"install_url"` - InstallURL string `json:"install_url"` - Details interface{} `json:"details,omitempty"` - FileData string `json:"file_data,omitempty"` - DirectInstallURL string `json:"direct_install_url,omitempty"` - Temporary bool `json:"temporary"` - Devices []Device `json:"devices,omitempty"` - Metadata map[string]interface{} `json:"metadata,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID string `json:"id"` + CardTemplateID string `json:"card_template_id"` + EmployeeID string `json:"employee_id"` + OrganizationName string `json:"organization_name,omitempty"` + CardNumber string `json:"card_number"` + SiteCode string `json:"site_code,omitempty"` + FullName string `json:"full_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + Classification string `json:"classification"` + Title string `json:"title,omitempty"` + StartDate time.Time `json:"start_date"` + ExpirationDate time.Time `json:"expiration_date"` + EmployeePhoto string `json:"employee_photo"` + State string `json:"state"` + URL string `json:"url,omitempty"` + InstallURL string `json:"install_url"` + Details interface{} `json:"details,omitempty"` + FileData string `json:"file_data,omitempty"` + DirectInstallURL string `json:"direct_install_url,omitempty"` + Temporary bool `json:"temporary"` + Devices []Device `json:"devices,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // CardProvisionResponse represents the response from provisioning a card @@ -64,19 +66,28 @@ type CardProvisionResponse struct { // ProvisionParams defines parameters for provisioning a new card type ProvisionParams struct { - CardTemplateID string `json:"card_template_id"` - EmployeeID string `json:"employee_id"` - CardNumber string `json:"card_number"` - SiteCode string `json:"site_code,omitempty"` - FullName string `json:"full_name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - Classification string `json:"classification"` - Title string `json:"title,omitempty"` - StartDate time.Time `json:"start_date"` - ExpirationDate time.Time `json:"expiration_date"` - EmployeePhoto string `json:"employee_photo"` - Temporary bool `json:"temporary,omitempty"` + CardTemplateID string `json:"card_template_id"` + EmployeeID string `json:"employee_id,omitempty"` + TagID string `json:"tag_id,omitempty"` + AllowOnMultipleDevices bool `json:"allow_on_multiple_devices,omitempty"` + CardNumber string `json:"card_number,omitempty"` + SiteCode string `json:"site_code,omitempty"` + FullName string `json:"full_name,omitempty"` + Email string `json:"email,omitempty"` + PhoneNumber string `json:"phone_number,omitempty"` + Classification string `json:"classification,omitempty"` + Title string `json:"title,omitempty"` + Department string `json:"department,omitempty"` + Location string `json:"location,omitempty"` + SiteName string `json:"site_name,omitempty"` + Workstation string `json:"workstation,omitempty"` + MailStop string `json:"mail_stop,omitempty"` + CompanyAddress string `json:"company_address,omitempty"` + StartDate time.Time `json:"start_date"` + ExpirationDate time.Time `json:"expiration_date"` + EmployeePhoto string `json:"employee_photo,omitempty"` + Temporary bool `json:"temporary,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty"` } // UpdateParams defines parameters for updating an existing card @@ -87,6 +98,13 @@ type UpdateParams struct { Email string `json:"email,omitempty"` PhoneNumber string `json:"phone_number,omitempty"` Classification string `json:"classification,omitempty"` + Title string `json:"title,omitempty"` + Department string `json:"department,omitempty"` + Location string `json:"location,omitempty"` + SiteName string `json:"site_name,omitempty"` + Workstation string `json:"workstation,omitempty"` + MailStop string `json:"mail_stop,omitempty"` + CompanyAddress string `json:"company_address,omitempty"` ExpirationDate *time.Time `json:"expiration_date,omitempty"` EmployeePhoto string `json:"employee_photo,omitempty"` } @@ -242,12 +260,12 @@ type LedgerItemPassTemplate struct { // LedgerItemAccessPass represents an access pass reference within a ledger item type LedgerItemAccessPass struct { - ID string `json:"id"` - FullName string `json:"full_name"` - State string `json:"state"` - Metadata map[string]interface{} `json:"metadata"` - UnifiedAccessPassExID string `json:"unified_access_pass_ex_id"` - PassTemplate *LedgerItemPassTemplate `json:"pass_template,omitempty"` + ID string `json:"id"` + FullName string `json:"full_name"` + State string `json:"state"` + Metadata map[string]interface{} `json:"metadata"` + UnifiedAccessPassExID string `json:"unified_access_pass_ex_id"` + PassTemplate *LedgerItemPassTemplate `json:"pass_template,omitempty"` } // LedgerItem represents a billing ledger item @@ -342,3 +360,62 @@ type CompleteHIDOrgParams struct { Email string `json:"email"` Password string `json:"password"` } + +// LandingPage represents a landing page configuration +type LandingPage struct { + ID string `json:"id"` + Name string `json:"name"` + Kind string `json:"kind"` + CreatedAt string `json:"created_at"` + PasswordProtected bool `json:"password_protected"` + LogoURL string `json:"logo_url,omitempty"` +} + +// CreateLandingPageParams defines parameters for creating a landing page +type CreateLandingPageParams struct { + Name string `json:"name"` + Kind string `json:"kind"` + AdditionalText string `json:"additional_text,omitempty"` + BgColor string `json:"bg_color,omitempty"` + AllowImmediateDownload bool `json:"allow_immediate_download,omitempty"` + Password string `json:"password,omitempty"` + Is2FAEnabled bool `json:"is_2fa_enabled,omitempty"` + Logo string `json:"logo,omitempty"` +} + +// UpdateLandingPageParams defines parameters for updating a landing page +type UpdateLandingPageParams struct { + LandingPageID string `json:"landing_page_id"` + Name string `json:"name,omitempty"` + AdditionalText string `json:"additional_text,omitempty"` + BgColor string `json:"bg_color,omitempty"` + AllowImmediateDownload *bool `json:"allow_immediate_download,omitempty"` + Password string `json:"password,omitempty"` + Is2FAEnabled *bool `json:"is_2fa_enabled,omitempty"` + Logo string `json:"logo,omitempty"` +} + +// CredentialProfile represents a credential profile +type CredentialProfile struct { + ID string `json:"id"` + AID string `json:"aid"` + Name string `json:"name"` + AppleID string `json:"apple_id,omitempty"` + CreatedAt string `json:"created_at"` + CardStorage map[string]interface{} `json:"card_storage,omitempty"` + Keys []interface{} `json:"keys,omitempty"` + Files []interface{} `json:"files,omitempty"` +} + +// KeyParam represents a key parameter for credential profile creation +type KeyParam struct { + Value string `json:"value"` +} + +// CreateCredentialProfileParams defines parameters for creating a credential profile +type CreateCredentialProfileParams struct { + Name string `json:"name"` + AppName string `json:"app_name"` + Keys []KeyParam `json:"keys,omitempty"` + FileID string `json:"file_id,omitempty"` +} diff --git a/services/access_cards_test.go b/services/access_cards_test.go index a934999..f01be82 100644 --- a/services/access_cards_test.go +++ b/services/access_cards_test.go @@ -272,6 +272,121 @@ func TestAccessCardsService_NonTemporaryCard(t *testing.T) { } } +func TestAccessCardsService_CardNewFields(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "id": "0xc4rd1d", + "card_template_id": "0xd3adb00b5", + "employee_id": "emp_123", + "organization_name": "Acme Corp", + "full_name": "Jane Doe", + "title": "Engineering Manager", + "state": "active", + "temporary": false, + "created_at": "2025-06-01T00:00:00Z" + }`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewAccessCardsService(c) + + ctx := context.Background() + card, err := service.Get(ctx, "0xc4rd1d") + if err != nil { + t.Fatalf("Get() error = %v", err) + } + + if card.EmployeeID != "emp_123" { + t.Errorf("card.EmployeeID = %v, want emp_123", card.EmployeeID) + } + if card.OrganizationName != "Acme Corp" { + t.Errorf("card.OrganizationName = %v, want Acme Corp", card.OrganizationName) + } + if card.Title != "Engineering Manager" { + t.Errorf("card.Title = %v, want Engineering Manager", card.Title) + } +} + +func TestAccessCardsService_ProvisionWithNewParams(t *testing.T) { + var capturedBody string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, _ := io.ReadAll(r.Body) + capturedBody = string(body) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "id": "0xc4rd1d", + "card_template_id": "0xd3adb00b5", + "full_name": "Employee name", + "state": "active", + "install_url": "https://accessgrid.com/install/0xc4rd1d" + }`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewAccessCardsService(c) + + startDate, _ := time.Parse(time.RFC3339, "2025-01-01T00:00:00Z") + expDate, _ := time.Parse(time.RFC3339, "2025-04-01T00:00:00Z") + + params := models.ProvisionParams{ + CardTemplateID: "0xd3adb00b5", + EmployeeID: "123456789", + TagID: "DDEADB33FB00B5", + FullName: "Employee name", + Email: "employee@example.com", + PhoneNumber: "+19547212241", + Classification: "full_time", + Department: "Engineering", + Location: "San Francisco", + SiteName: "HQ Building A", + Workstation: "4F-207", + MailStop: "MS-401", + CompanyAddress: "123 Main St, San Francisco, CA 94105", + Title: "Engineering Manager", + StartDate: startDate, + ExpirationDate: expDate, + Metadata: map[string]interface{}{ + "department": "engineering", + "badge_type": "contractor", + }, + } + + ctx := context.Background() + _, err := service.Provision(ctx, params) + if err != nil { + t.Fatalf("Provision() error = %v", err) + } + + // Verify new fields are sent in request body + if !strings.Contains(capturedBody, `"department":"Engineering"`) { + t.Errorf("expected department in request body, got %s", capturedBody) + } + if !strings.Contains(capturedBody, `"location":"San Francisco"`) { + t.Errorf("expected location in request body, got %s", capturedBody) + } + if !strings.Contains(capturedBody, `"site_name":"HQ Building A"`) { + t.Errorf("expected site_name in request body, got %s", capturedBody) + } + if !strings.Contains(capturedBody, `"workstation":"4F-207"`) { + t.Errorf("expected workstation in request body, got %s", capturedBody) + } + if !strings.Contains(capturedBody, `"mail_stop":"MS-401"`) { + t.Errorf("expected mail_stop in request body, got %s", capturedBody) + } + if !strings.Contains(capturedBody, `"company_address":"123 Main St, San Francisco, CA 94105"`) { + t.Errorf("expected company_address in request body, got %s", capturedBody) + } + if !strings.Contains(capturedBody, `"tag_id":"DDEADB33FB00B5"`) { + t.Errorf("expected tag_id in request body, got %s", capturedBody) + } + if !strings.Contains(capturedBody, `"metadata"`) { + t.Errorf("expected metadata in request body, got %s", capturedBody) + } +} + func TestAccessCardsService_ErrorPropagation(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") diff --git a/services/console.go b/services/console.go index 4b5265d..e51a102 100644 --- a/services/console.go +++ b/services/console.go @@ -13,17 +13,19 @@ import ( // ConsoleService handles operations related to the enterprise console type ConsoleService struct { - client *client.Client - HID *HIDService - Webhooks *WebhooksService + client *client.Client + HID *HIDService + Webhooks *WebhooksService + CredentialProfiles *CredentialProfilesService } // NewConsoleService creates a new ConsoleService func NewConsoleService(c *client.Client) *ConsoleService { return &ConsoleService{ - client: c, - HID: NewHIDService(c), - Webhooks: NewWebhooksService(c), + client: c, + HID: NewHIDService(c), + Webhooks: NewWebhooksService(c), + CredentialProfiles: NewCredentialProfilesService(c), } } @@ -240,6 +242,67 @@ func (s *ConsoleService) ListLedgerItems(ctx context.Context, params models.List return &response, nil } +// ListLandingPages retrieves all landing pages +func (s *ConsoleService) ListLandingPages(ctx context.Context) ([]models.LandingPage, error) { + var pages []models.LandingPage + err := s.client.Request(ctx, http.MethodGet, "/v1/console/landing-pages", nil, &pages) + if err != nil { + return nil, fmt.Errorf("error listing landing pages: %w", err) + } + return pages, nil +} + +// CreateLandingPage creates a new landing page +func (s *ConsoleService) CreateLandingPage(ctx context.Context, params models.CreateLandingPageParams) (*models.LandingPage, error) { + var page models.LandingPage + err := s.client.Request(ctx, http.MethodPost, "/v1/console/landing-pages", params, &page) + if err != nil { + return nil, fmt.Errorf("error creating landing page: %w", err) + } + return &page, nil +} + +// UpdateLandingPage updates an existing landing page +func (s *ConsoleService) UpdateLandingPage(ctx context.Context, params models.UpdateLandingPageParams) (*models.LandingPage, error) { + var page models.LandingPage + path := fmt.Sprintf("/v1/console/landing-pages/%s", url.PathEscape(params.LandingPageID)) + err := s.client.Request(ctx, http.MethodPut, path, params, &page) + if err != nil { + return nil, fmt.Errorf("error updating landing page: %w", err) + } + return &page, nil +} + +// CredentialProfilesService handles credential profile operations +type CredentialProfilesService struct { + client *client.Client +} + +// NewCredentialProfilesService creates a new CredentialProfilesService +func NewCredentialProfilesService(c *client.Client) *CredentialProfilesService { + return &CredentialProfilesService{client: c} +} + +// List retrieves all credential profiles +func (s *CredentialProfilesService) List(ctx context.Context) ([]models.CredentialProfile, error) { + var profiles []models.CredentialProfile + err := s.client.Request(ctx, http.MethodGet, "/v1/console/credential-profiles", nil, &profiles) + if err != nil { + return nil, fmt.Errorf("error listing credential profiles: %w", err) + } + return profiles, nil +} + +// Create creates a new credential profile +func (s *CredentialProfilesService) Create(ctx context.Context, params models.CreateCredentialProfileParams) (*models.CredentialProfile, error) { + var profile models.CredentialProfile + err := s.client.Request(ctx, http.MethodPost, "/v1/console/credential-profiles", params, &profile) + if err != nil { + return nil, fmt.Errorf("error creating credential profile: %w", err) + } + return &profile, nil +} + // EventLog retrieves event logs for a specific template func (s *ConsoleService) EventLog(ctx context.Context, templateID string, filters models.EventLogFilters) ([]models.Event, error) { var events []models.Event diff --git a/services/console_test.go b/services/console_test.go index 1cdd5ec..43ea726 100644 --- a/services/console_test.go +++ b/services/console_test.go @@ -873,6 +873,265 @@ func TestHIDOrgsService_Activate(t *testing.T) { } } +// --- Landing Pages --- + +func TestConsoleService_ListLandingPages(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/console/landing-pages" || r.Method != http.MethodGet { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`[ + { + "id": "lp_1", + "name": "Miami Office", + "kind": "universal", + "created_at": "2025-01-01T00:00:00Z", + "password_protected": false, + "logo_url": "https://example.com/logo.png" + }, + { + "id": "lp_2", + "name": "NYC Office", + "kind": "universal", + "created_at": "2025-01-02T00:00:00Z", + "password_protected": true + } + ]`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewConsoleService(c) + + ctx := context.Background() + pages, err := service.ListLandingPages(ctx) + if err != nil { + t.Fatalf("ListLandingPages() error = %v", err) + } + + if len(pages) != 2 { + t.Fatalf("got %d pages, want 2", len(pages)) + } + if pages[0].ID != "lp_1" { + t.Errorf("pages[0].ID = %v, want lp_1", pages[0].ID) + } + if pages[0].Name != "Miami Office" { + t.Errorf("pages[0].Name = %v, want Miami Office", pages[0].Name) + } + if pages[0].Kind != "universal" { + t.Errorf("pages[0].Kind = %v, want universal", pages[0].Kind) + } + if pages[0].LogoURL != "https://example.com/logo.png" { + t.Errorf("pages[0].LogoURL = %v, want https://example.com/logo.png", pages[0].LogoURL) + } + if pages[1].PasswordProtected != true { + t.Errorf("pages[1].PasswordProtected = %v, want true", pages[1].PasswordProtected) + } +} + +func TestConsoleService_ListLandingPages_Empty(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`[]`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewConsoleService(c) + + ctx := context.Background() + pages, err := service.ListLandingPages(ctx) + if err != nil { + t.Fatalf("ListLandingPages() error = %v", err) + } + if len(pages) != 0 { + t.Errorf("got %d pages, want 0", len(pages)) + } +} + +func TestConsoleService_CreateLandingPage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/console/landing-pages" || r.Method != http.MethodPost { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{ + "id": "lp_new", + "name": "Miami Office Access Pass", + "kind": "universal", + "created_at": "2025-06-01T00:00:00Z", + "password_protected": false + }`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewConsoleService(c) + + ctx := context.Background() + page, err := service.CreateLandingPage(ctx, models.CreateLandingPageParams{ + Name: "Miami Office Access Pass", + Kind: "universal", + AdditionalText: "Welcome to the Miami Office", + BgColor: "#f1f5f9", + AllowImmediateDownload: true, + }) + if err != nil { + t.Fatalf("CreateLandingPage() error = %v", err) + } + + if page.ID != "lp_new" { + t.Errorf("page.ID = %v, want lp_new", page.ID) + } + if page.Name != "Miami Office Access Pass" { + t.Errorf("page.Name = %v, want Miami Office Access Pass", page.Name) + } + if page.Kind != "universal" { + t.Errorf("page.Kind = %v, want universal", page.Kind) + } +} + +func TestConsoleService_UpdateLandingPage(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/console/landing-pages/lp_123" || r.Method != http.MethodPut { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{ + "id": "lp_123", + "name": "Updated Miami Office", + "kind": "universal", + "created_at": "2025-06-01T00:00:00Z", + "password_protected": false + }`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewConsoleService(c) + + ctx := context.Background() + page, err := service.UpdateLandingPage(ctx, models.UpdateLandingPageParams{ + LandingPageID: "lp_123", + Name: "Updated Miami Office", + AdditionalText: "Welcome! Tap below to get your access pass.", + BgColor: "#e2e8f0", + }) + if err != nil { + t.Fatalf("UpdateLandingPage() error = %v", err) + } + + if page.ID != "lp_123" { + t.Errorf("page.ID = %v, want lp_123", page.ID) + } + if page.Name != "Updated Miami Office" { + t.Errorf("page.Name = %v, want Updated Miami Office", page.Name) + } +} + +// --- Credential Profiles --- + +func TestCredentialProfilesService_List(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/console/credential-profiles" || r.Method != http.MethodGet { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`[ + {"id": "cp_1", "name": "Main Office", "aid": "AID001", "created_at": "2025-01-01T00:00:00Z"}, + {"id": "cp_2", "name": "Branch Office", "aid": "AID002", "created_at": "2025-01-02T00:00:00Z"} + ]`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewCredentialProfilesService(c) + + ctx := context.Background() + profiles, err := service.List(ctx) + if err != nil { + t.Fatalf("List() error = %v", err) + } + + if len(profiles) != 2 { + t.Fatalf("got %d profiles, want 2", len(profiles)) + } + if profiles[0].ID != "cp_1" { + t.Errorf("profiles[0].ID = %v, want cp_1", profiles[0].ID) + } + if profiles[0].Name != "Main Office" { + t.Errorf("profiles[0].Name = %v, want Main Office", profiles[0].Name) + } + if profiles[0].AID != "AID001" { + t.Errorf("profiles[0].AID = %v, want AID001", profiles[0].AID) + } +} + +func TestCredentialProfilesService_List_Empty(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`[]`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewCredentialProfilesService(c) + + ctx := context.Background() + profiles, err := service.List(ctx) + if err != nil { + t.Fatalf("List() error = %v", err) + } + if len(profiles) != 0 { + t.Errorf("got %d profiles, want 0", len(profiles)) + } +} + +func TestCredentialProfilesService_Create(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/console/credential-profiles" || r.Method != http.MethodPost { + t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + w.Write([]byte(`{ + "id": "cp_new", + "name": "Main Office Profile", + "aid": "AID_NEW", + "created_at": "2025-06-01T00:00:00Z" + }`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewCredentialProfilesService(c) + + ctx := context.Background() + profile, err := service.Create(ctx, models.CreateCredentialProfileParams{ + Name: "Main Office Profile", + AppName: "KEY-ID-main", + Keys: []models.KeyParam{ + {Value: "your_32_char_hex_master_key_here"}, + {Value: "your_32_char_hex__read_key__here"}, + }, + }) + if err != nil { + t.Fatalf("Create() error = %v", err) + } + + if profile.ID != "cp_new" { + t.Errorf("profile.ID = %v, want cp_new", profile.ID) + } + if profile.Name != "Main Office Profile" { + t.Errorf("profile.Name = %v, want Main Office Profile", profile.Name) + } + if profile.AID != "AID_NEW" { + t.Errorf("profile.AID = %v, want AID_NEW", profile.AID) + } +} + func TestConsoleService_ErrorPropagation(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json")