diff --git a/accessgrid.go b/accessgrid.go index 2f5d858..cc8a71f 100644 --- a/accessgrid.go +++ b/accessgrid.go @@ -80,4 +80,34 @@ type ( // Event represents an event in the event log Event = models.Event + + // Pagination represents pagination metadata in list responses + Pagination = models.Pagination + + // TemplateInfo represents minimal template info within a PassTemplatePair + TemplateInfo = models.TemplateInfo + + // PassTemplatePair represents a paired iOS and Android template configuration + PassTemplatePair = models.PassTemplatePair + + // PassTemplatePairsResponse represents the response from listing pass template pairs + PassTemplatePairsResponse = models.PassTemplatePairsResponse + + // ListPassTemplatePairsParams defines parameters for listing pass template pairs + ListPassTemplatePairsParams = models.ListPassTemplatePairsParams + + // LedgerItemPassTemplate represents a pass template reference within a ledger item's access pass + LedgerItemPassTemplate = models.LedgerItemPassTemplate + + // LedgerItemAccessPass represents an access pass reference within a ledger item + LedgerItemAccessPass = models.LedgerItemAccessPass + + // LedgerItem represents a billing ledger item + LedgerItem = models.LedgerItem + + // LedgerItemsResponse represents the response from listing ledger items + LedgerItemsResponse = models.LedgerItemsResponse + + // ListLedgerItemsParams defines parameters for listing ledger items + ListLedgerItemsParams = models.ListLedgerItemsParams ) diff --git a/models/models.go b/models/models.go index 82c5814..d278a66 100644 --- a/models/models.go +++ b/models/models.go @@ -32,6 +32,7 @@ type Card struct { 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"` @@ -56,6 +57,7 @@ type CardProvisionResponse struct { URL string `json:"install_url"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` + Temporary bool `json:"temporary"` DirectInstallUrl string `json:"direct_install_url"` Details []Card `json:"details"` } @@ -74,6 +76,7 @@ type ProvisionParams struct { StartDate time.Time `json:"start_date"` ExpirationDate time.Time `json:"expiration_date"` EmployeePhoto string `json:"employee_photo"` + Temporary bool `json:"temporary,omitempty"` } // UpdateParams defines parameters for updating an existing card @@ -172,3 +175,82 @@ type Event struct { Timestamp time.Time `json:"timestamp"` Details string `json:"details"` } + +// Pagination represents pagination metadata in list responses +type Pagination struct { + CurrentPage int `json:"current_page"` + PerPage int `json:"per_page,omitempty"` + TotalPages int `json:"total_pages"` + TotalCount int `json:"total_count,omitempty"` +} + +// TemplateInfo represents minimal template info within a PassTemplatePair +type TemplateInfo struct { + ID string `json:"id"` + Name string `json:"name"` + Platform string `json:"platform"` +} + +// PassTemplatePair represents a paired iOS and Android template configuration +type PassTemplatePair struct { + ID string `json:"id"` + Name string `json:"name"` + CreatedAt time.Time `json:"created_at"` + IOSTemplate *TemplateInfo `json:"ios_template"` + AndroidTemplate *TemplateInfo `json:"android_template"` +} + +// PassTemplatePairsResponse represents the response from listing pass template pairs +type PassTemplatePairsResponse struct { + PassTemplatePairs []PassTemplatePair `json:"pass_template_pairs"` + Pagination Pagination `json:"pagination"` +} + +// ListPassTemplatePairsParams defines parameters for listing pass template pairs +type ListPassTemplatePairsParams struct { + Page int `json:"page,omitempty"` + PerPage int `json:"per_page,omitempty"` +} + +// LedgerItemPassTemplate represents a pass template reference within a ledger item's access pass +type LedgerItemPassTemplate struct { + ID string `json:"id"` + Name string `json:"name"` + Protocol string `json:"protocol"` + Platform string `json:"platform"` + UseCase string `json:"use_case"` +} + +// 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"` +} + +// LedgerItem represents a billing ledger item +type LedgerItem struct { + CreatedAt time.Time `json:"created_at"` + Amount float64 `json:"amount"` + ID string `json:"id"` + Kind string `json:"kind"` + Metadata map[string]interface{} `json:"metadata"` + AccessPass *LedgerItemAccessPass `json:"access_pass"` +} + +// LedgerItemsResponse represents the response from listing ledger items +type LedgerItemsResponse struct { + LedgerItems []LedgerItem `json:"ledger_items"` + Pagination Pagination `json:"pagination"` +} + +// ListLedgerItemsParams defines parameters for listing ledger items +type ListLedgerItemsParams struct { + Page int `json:"page,omitempty"` + PerPage int `json:"per_page,omitempty"` + StartDate *time.Time `json:"start_date,omitempty"` + EndDate *time.Time `json:"end_date,omitempty"` +} diff --git a/services/access_cards_test.go b/services/access_cards_test.go index 37260a1..a934999 100644 --- a/services/access_cards_test.go +++ b/services/access_cards_test.go @@ -3,6 +3,7 @@ package services import ( "context" "errors" + "io" "net/http" "net/http/httptest" "strings" @@ -191,6 +192,86 @@ func TestAccessCardsService_CardStateOperations(t *testing.T) { } } +func TestAccessCardsService_ProvisionTemporary(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": "0xtemp1d", + "card_template_id": "0xd3adb00b5", + "full_name": "Temp Worker", + "state": "active", + "temporary": true, + "install_url": "https://accessgrid.com/install/0xtemp1d" + }`)) + })) + 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-01-02T00:00:00Z") + + params := models.ProvisionParams{ + CardTemplateID: "0xd3adb00b5", + EmployeeID: "tmp_001", + CardNumber: "99999", + FullName: "Temp Worker", + Email: "temp@example.com", + PhoneNumber: "+15551234567", + Classification: "contractor", + StartDate: startDate, + ExpirationDate: expDate, + Temporary: true, + } + + ctx := context.Background() + card, err := service.Provision(ctx, params) + if err != nil { + t.Fatalf("Provision() error = %v", err) + } + + // Verify temporary is sent in request body + if !strings.Contains(capturedBody, `"temporary":true`) { + t.Errorf("expected temporary:true in request body, got %s", capturedBody) + } + + // Verify temporary is deserialized on response + if !card.Temporary { + t.Errorf("card.Temporary = %v, want true", card.Temporary) + } +} + +func TestAccessCardsService_NonTemporaryCard(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", + "full_name": "Regular Worker", + "state": "active", + "temporary": false + }`)) + })) + 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.Temporary { + t.Errorf("card.Temporary = %v, want false", card.Temporary) + } +} + 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 68462d5..387610c 100644 --- a/services/console.go +++ b/services/console.go @@ -73,6 +73,60 @@ func (s *ConsoleService) DeleteTemplate(ctx context.Context, templateID string) return nil } +// ListPassTemplatePairs retrieves pass template pairs +func (s *ConsoleService) ListPassTemplatePairs(ctx context.Context, params models.ListPassTemplatePairsParams) (*models.PassTemplatePairsResponse, error) { + var response models.PassTemplatePairsResponse + + query := url.Values{} + if params.Page > 0 { + query.Add("page", fmt.Sprintf("%d", params.Page)) + } + if params.PerPage > 0 { + query.Add("per_page", fmt.Sprintf("%d", params.PerPage)) + } + + u := url.URL{Path: "/v1/console/pass-template-pairs"} + if len(query) > 0 { + u.RawQuery = query.Encode() + } + + err := s.client.Request(ctx, http.MethodGet, u.String(), nil, &response) + if err != nil { + return nil, fmt.Errorf("error listing pass template pairs: %w", err) + } + return &response, nil +} + +// ListLedgerItems retrieves billing ledger items +func (s *ConsoleService) ListLedgerItems(ctx context.Context, params models.ListLedgerItemsParams) (*models.LedgerItemsResponse, error) { + var response models.LedgerItemsResponse + + query := url.Values{} + if params.Page > 0 { + query.Add("page", fmt.Sprintf("%d", params.Page)) + } + if params.PerPage > 0 { + query.Add("per_page", fmt.Sprintf("%d", params.PerPage)) + } + if params.StartDate != nil { + query.Add("start_date", params.StartDate.Format(time.RFC3339)) + } + if params.EndDate != nil { + query.Add("end_date", params.EndDate.Format(time.RFC3339)) + } + + u := url.URL{Path: "/v1/console/ledger-items"} + if len(query) > 0 { + u.RawQuery = query.Encode() + } + + err := s.client.Request(ctx, http.MethodGet, u.String(), nil, &response) + if err != nil { + return nil, fmt.Errorf("error listing ledger items: %w", err) + } + return &response, 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 7d4c829..cc6eacc 100644 --- a/services/console_test.go +++ b/services/console_test.go @@ -78,6 +78,72 @@ func setupConsoleTestServer() (*httptest.Server, *ConsoleService) { "timestamp": "2023-01-01T12:00:00Z" } ]`)) + case "/v1/console/pass-template-pairs": + w.Write([]byte(`{ + "pass_template_pairs": [ + { + "id": "pair_1", + "name": "Employee Badge Pair", + "created_at": "2025-01-01T00:00:00Z", + "ios_template": {"id": "tmpl_ios_1", "name": "iOS Badge", "platform": "apple"}, + "android_template": {"id": "tmpl_android_1", "name": "Android Badge", "platform": "android"} + }, + { + "id": "pair_2", + "name": "Contractor Badge Pair", + "created_at": "2025-01-02T00:00:00Z", + "ios_template": {"id": "tmpl_ios_2", "name": "iOS Contractor", "platform": "apple"}, + "android_template": null + } + ], + "pagination": { + "current_page": 1, + "total_pages": 1 + } + }`)) + case "/v1/console/ledger-items": + w.Write([]byte(`{ + "ledger_items": [ + { + "created_at": "2025-06-15T14:30:00Z", + "amount": -1.50, + "id": "li_abc123", + "kind": "access_pass_debit", + "metadata": { + "access_pass_ex_id": "ap_xyz", + "pass_template_ex_id": "pt_456" + }, + "access_pass": { + "id": "ap_xyz", + "full_name": "Jane Doe", + "state": "active", + "metadata": {"department": "Engineering"}, + "unified_access_pass_ex_id": "uap_789", + "pass_template": { + "id": "pt_456", + "name": "Employee Badge", + "protocol": "desfire", + "platform": "apple", + "use_case": "employee_badge" + } + } + }, + { + "created_at": "2025-06-14T08:15:00Z", + "amount": 500.00, + "id": "li_def456", + "kind": "credit", + "metadata": {}, + "access_pass": null + } + ], + "pagination": { + "current_page": 1, + "per_page": 50, + "total_pages": 3, + "total_count": 125 + } + }`)) } })) @@ -248,6 +314,307 @@ func TestConsoleService_EventLog(t *testing.T) { } } +// --- Pass Template Pairs --- + +func TestConsoleService_ListPassTemplatePairs(t *testing.T) { + server, service := setupConsoleTestServer() + defer server.Close() + + ctx := context.Background() + response, err := service.ListPassTemplatePairs(ctx, models.ListPassTemplatePairsParams{}) + if err != nil { + t.Fatalf("ListPassTemplatePairs() error = %v", err) + } + + if len(response.PassTemplatePairs) != 2 { + t.Fatalf("got %d pairs, want 2", len(response.PassTemplatePairs)) + } + + // First pair: both templates present + pair := response.PassTemplatePairs[0] + if pair.ID != "pair_1" { + t.Errorf("pair.ID = %v, want pair_1", pair.ID) + } + if pair.Name != "Employee Badge Pair" { + t.Errorf("pair.Name = %v, want Employee Badge Pair", pair.Name) + } + expectedTime, _ := time.Parse(time.RFC3339, "2025-01-01T00:00:00Z") + if !pair.CreatedAt.Equal(expectedTime) { + t.Errorf("pair.CreatedAt = %v, want %v", pair.CreatedAt, expectedTime) + } + if pair.IOSTemplate == nil { + t.Fatal("pair.IOSTemplate is nil, want non-nil") + } + if pair.IOSTemplate.ID != "tmpl_ios_1" { + t.Errorf("IOSTemplate.ID = %v, want tmpl_ios_1", pair.IOSTemplate.ID) + } + if pair.IOSTemplate.Name != "iOS Badge" { + t.Errorf("IOSTemplate.Name = %v, want iOS Badge", pair.IOSTemplate.Name) + } + if pair.IOSTemplate.Platform != "apple" { + t.Errorf("IOSTemplate.Platform = %v, want apple", pair.IOSTemplate.Platform) + } + if pair.AndroidTemplate == nil { + t.Fatal("pair.AndroidTemplate is nil, want non-nil") + } + if pair.AndroidTemplate.ID != "tmpl_android_1" { + t.Errorf("AndroidTemplate.ID = %v, want tmpl_android_1", pair.AndroidTemplate.ID) + } + + // Second pair: null android_template + pair2 := response.PassTemplatePairs[1] + if pair2.AndroidTemplate != nil { + t.Errorf("pair2.AndroidTemplate = %v, want nil", pair2.AndroidTemplate) + } + if pair2.IOSTemplate == nil { + t.Fatal("pair2.IOSTemplate is nil, want non-nil") + } + + // Pagination + if response.Pagination.CurrentPage != 1 { + t.Errorf("Pagination.CurrentPage = %v, want 1", response.Pagination.CurrentPage) + } + if response.Pagination.TotalPages != 1 { + t.Errorf("Pagination.TotalPages = %v, want 1", response.Pagination.TotalPages) + } +} + +func TestConsoleService_ListPassTemplatePairs_WithPagination(t *testing.T) { + var capturedURL string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedURL = r.URL.String() + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"pass_template_pairs": [], "pagination": {"current_page": 2, "total_pages": 5}}`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewConsoleService(c) + + ctx := context.Background() + response, err := service.ListPassTemplatePairs(ctx, models.ListPassTemplatePairsParams{ + Page: 2, + PerPage: 10, + }) + if err != nil { + t.Fatalf("ListPassTemplatePairs() error = %v", err) + } + + if len(response.PassTemplatePairs) != 0 { + t.Errorf("got %d pairs, want 0", len(response.PassTemplatePairs)) + } + if response.Pagination.CurrentPage != 2 { + t.Errorf("Pagination.CurrentPage = %v, want 2", response.Pagination.CurrentPage) + } + if response.Pagination.TotalPages != 5 { + t.Errorf("Pagination.TotalPages = %v, want 5", response.Pagination.TotalPages) + } + + if !strings.Contains(capturedURL, "page=2") { + t.Errorf("expected page=2 in URL, got %s", capturedURL) + } + if !strings.Contains(capturedURL, "per_page=10") { + t.Errorf("expected per_page=10 in URL, got %s", capturedURL) + } +} + +// --- Ledger Items --- + +func TestConsoleService_ListLedgerItems(t *testing.T) { + server, service := setupConsoleTestServer() + defer server.Close() + + ctx := context.Background() + response, err := service.ListLedgerItems(ctx, models.ListLedgerItemsParams{}) + if err != nil { + t.Fatalf("ListLedgerItems() error = %v", err) + } + + if len(response.LedgerItems) != 2 { + t.Fatalf("got %d items, want 2", len(response.LedgerItems)) + } + + // First item: full nested structure + item := response.LedgerItems[0] + expectedTime, _ := time.Parse(time.RFC3339, "2025-06-15T14:30:00Z") + if !item.CreatedAt.Equal(expectedTime) { + t.Errorf("item.CreatedAt = %v, want %v", item.CreatedAt, expectedTime) + } + if item.Amount != -1.50 { + t.Errorf("item.Amount = %v, want -1.50", item.Amount) + } + if item.ID != "li_abc123" { + t.Errorf("item.ID = %v, want li_abc123", item.ID) + } + if item.Kind != "access_pass_debit" { + t.Errorf("item.Kind = %v, want access_pass_debit", item.Kind) + } + if item.Metadata["access_pass_ex_id"] != "ap_xyz" { + t.Errorf("item.Metadata[access_pass_ex_id] = %v, want ap_xyz", item.Metadata["access_pass_ex_id"]) + } + + // Nested access_pass + ap := item.AccessPass + if ap == nil { + t.Fatal("item.AccessPass is nil, want non-nil") + } + if ap.ID != "ap_xyz" { + t.Errorf("AccessPass.ID = %v, want ap_xyz", ap.ID) + } + if ap.FullName != "Jane Doe" { + t.Errorf("AccessPass.FullName = %v, want Jane Doe", ap.FullName) + } + if ap.State != "active" { + t.Errorf("AccessPass.State = %v, want active", ap.State) + } + if ap.Metadata["department"] != "Engineering" { + t.Errorf("AccessPass.Metadata[department] = %v, want Engineering", ap.Metadata["department"]) + } + if ap.UnifiedAccessPassExID != "uap_789" { + t.Errorf("AccessPass.UnifiedAccessPassExID = %v, want uap_789", ap.UnifiedAccessPassExID) + } + + // Nested pass_template + pt := ap.PassTemplate + if pt == nil { + t.Fatal("AccessPass.PassTemplate is nil, want non-nil") + } + if pt.ID != "pt_456" { + t.Errorf("PassTemplate.ID = %v, want pt_456", pt.ID) + } + if pt.Name != "Employee Badge" { + t.Errorf("PassTemplate.Name = %v, want Employee Badge", pt.Name) + } + if pt.Protocol != "desfire" { + t.Errorf("PassTemplate.Protocol = %v, want desfire", pt.Protocol) + } + if pt.Platform != "apple" { + t.Errorf("PassTemplate.Platform = %v, want apple", pt.Platform) + } + if pt.UseCase != "employee_badge" { + t.Errorf("PassTemplate.UseCase = %v, want employee_badge", pt.UseCase) + } + + // Second item: null access_pass + item2 := response.LedgerItems[1] + if item2.Kind != "credit" { + t.Errorf("item2.Kind = %v, want credit", item2.Kind) + } + if item2.AccessPass != nil { + t.Errorf("item2.AccessPass = %v, want nil", item2.AccessPass) + } + + // Pagination + if response.Pagination.CurrentPage != 1 { + t.Errorf("Pagination.CurrentPage = %v, want 1", response.Pagination.CurrentPage) + } + if response.Pagination.PerPage != 50 { + t.Errorf("Pagination.PerPage = %v, want 50", response.Pagination.PerPage) + } + if response.Pagination.TotalPages != 3 { + t.Errorf("Pagination.TotalPages = %v, want 3", response.Pagination.TotalPages) + } + if response.Pagination.TotalCount != 125 { + t.Errorf("Pagination.TotalCount = %v, want 125", response.Pagination.TotalCount) + } +} + +func TestConsoleService_ListLedgerItems_MissingPassTemplate(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(`{ + "ledger_items": [ + { + "created_at": "2025-06-15T14:30:00Z", + "amount": -1.50, + "id": "li_no_pt", + "kind": "access_pass_debit", + "metadata": {}, + "access_pass": { + "id": "ap_orphan", + "full_name": "John Smith", + "state": "suspended", + "metadata": {}, + "unified_access_pass_ex_id": null + } + } + ], + "pagination": {"current_page": 1, "per_page": 50, "total_pages": 1, "total_count": 1} + }`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewConsoleService(c) + + ctx := context.Background() + response, err := service.ListLedgerItems(ctx, models.ListLedgerItemsParams{}) + if err != nil { + t.Fatalf("ListLedgerItems() error = %v", err) + } + + ap := response.LedgerItems[0].AccessPass + if ap == nil { + t.Fatal("AccessPass is nil, want non-nil") + } + if ap.FullName != "John Smith" { + t.Errorf("AccessPass.FullName = %v, want John Smith", ap.FullName) + } + if ap.UnifiedAccessPassExID != "" { + t.Errorf("AccessPass.UnifiedAccessPassExID = %v, want empty", ap.UnifiedAccessPassExID) + } + if ap.PassTemplate != nil { + t.Errorf("AccessPass.PassTemplate = %v, want nil", ap.PassTemplate) + } +} + +func TestConsoleService_ListLedgerItems_WithFilters(t *testing.T) { + var capturedURL string + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedURL = r.URL.String() + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"ledger_items": [], "pagination": {"current_page": 2, "per_page": 20, "total_pages": 1, "total_count": 0}}`)) + })) + defer server.Close() + + c, _ := client.NewClient("test-account", "test-secret", client.WithBaseURL(server.URL)) + service := NewConsoleService(c) + + startDate, _ := time.Parse(time.RFC3339, "2025-01-01T00:00:00Z") + endDate, _ := time.Parse(time.RFC3339, "2025-06-30T23:59:59Z") + + ctx := context.Background() + response, err := service.ListLedgerItems(ctx, models.ListLedgerItemsParams{ + Page: 2, + PerPage: 20, + StartDate: &startDate, + EndDate: &endDate, + }) + if err != nil { + t.Fatalf("ListLedgerItems() error = %v", err) + } + + if len(response.LedgerItems) != 0 { + t.Errorf("got %d items, want 0", len(response.LedgerItems)) + } + if response.Pagination.CurrentPage != 2 { + t.Errorf("Pagination.CurrentPage = %v, want 2", response.Pagination.CurrentPage) + } + + if !strings.Contains(capturedURL, "page=2") { + t.Errorf("expected page=2 in URL, got %s", capturedURL) + } + if !strings.Contains(capturedURL, "per_page=20") { + t.Errorf("expected per_page=20 in URL, got %s", capturedURL) + } + if !strings.Contains(capturedURL, "start_date=") { + t.Errorf("expected start_date in URL, got %s", capturedURL) + } + if !strings.Contains(capturedURL, "end_date=") { + t.Errorf("expected end_date in URL, got %s", capturedURL) + } +} + 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")