From 381eb332d9063a3aa1409a3f5ff8b191fff01374 Mon Sep 17 00:00:00 2001 From: Patrick Omland Date: Tue, 9 Jun 2026 00:44:50 +0200 Subject: [PATCH] feat: add permission-templates commands (list, get, create, update, delete) --- README.md | 26 ++++ internal/cli/root.go | 2 + internal/cli/root_test.go | 4 +- internal/cmd/groups/create_test.go | 8 +- internal/cmd/groups/delete_test.go | 8 +- internal/cmd/groups/get_test.go | 8 +- internal/cmd/groups/list_test.go | 6 +- internal/cmd/groups/member_add_test.go | 6 +- internal/cmd/groups/member_remove_test.go | 6 +- internal/cmd/groups/members_test.go | 6 +- internal/cmd/groups/update_test.go | 8 +- internal/cmd/groups/zone_add_test.go | 6 +- internal/cmd/groups/zone_remove_test.go | 6 +- internal/cmd/groups/zones_test.go | 6 +- internal/cmd/permission_templates/create.go | 88 +++++++++++++ .../cmd/permission_templates/create_test.go | 67 ++++++++++ internal/cmd/permission_templates/delete.go | 89 +++++++++++++ .../cmd/permission_templates/delete_test.go | 73 +++++++++++ internal/cmd/permission_templates/get.go | 89 +++++++++++++ internal/cmd/permission_templates/get_test.go | 73 +++++++++++ internal/cmd/permission_templates/list.go | 78 ++++++++++++ .../cmd/permission_templates/list_test.go | 64 ++++++++++ .../permission_templates.go | 30 +++++ .../permission_templates_test.go | 19 +++ internal/cmd/permission_templates/update.go | 118 ++++++++++++++++++ .../cmd/permission_templates/update_test.go | 57 +++++++++ internal/cmd/records/get_test.go | 10 +- internal/cmd/records/update_test.go | 8 +- internal/cmd/users/create_test.go | 8 +- internal/cmd/users/delete_test.go | 8 +- internal/cmd/users/get_test.go | 8 +- internal/cmd/users/list_test.go | 6 +- .../cmd/users/set_permission_template_test.go | 8 +- internal/cmd/users/update_test.go | 8 +- internal/cmd/zones/export_test.go | 6 +- internal/cmd/zones/import_test.go | 12 +- internal/cmd/zones/list_test.go | 4 +- internal/testutil/fixture.go | 6 +- .../mock_permission_template_client.go | 63 ++++++++++ 39 files changed, 1022 insertions(+), 84 deletions(-) create mode 100644 internal/cmd/permission_templates/create.go create mode 100644 internal/cmd/permission_templates/create_test.go create mode 100644 internal/cmd/permission_templates/delete.go create mode 100644 internal/cmd/permission_templates/delete_test.go create mode 100644 internal/cmd/permission_templates/get.go create mode 100644 internal/cmd/permission_templates/get_test.go create mode 100644 internal/cmd/permission_templates/list.go create mode 100644 internal/cmd/permission_templates/list_test.go create mode 100644 internal/cmd/permission_templates/permission_templates.go create mode 100644 internal/cmd/permission_templates/permission_templates_test.go create mode 100644 internal/cmd/permission_templates/update.go create mode 100644 internal/cmd/permission_templates/update_test.go create mode 100644 internal/testutil/mock_permission_template_client.go diff --git a/README.md b/README.md index 15f6d5b..15edcf4 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,32 @@ poweradmin groups zone-add --group-id 1 --zone-id 78 poweradmin groups zone-remove --group-id 1 --zone-id 78 ``` +### Permission Templates + +```bash +# List all permission templates +poweradmin permission-templates list +poweradmin permission-templates list -o json + +# Get a template by name or ID (shows full permission list) +poweradmin permission-templates get --name Administrator +poweradmin permission-templates get --id 1 -o json + +# Create a template +poweradmin permission-templates create --name "Zone Editors" --description "Can edit zone records" +poweradmin permission-templates create --name "Zone Editors" --type group --permissions 1,2,3 + +# Create and capture the ID +PT_ID=$(poweradmin permission-templates create --name "Zone Editors" -q) + +# Update a template +poweradmin permission-templates update --name "Zone Editors" --new-name "DNS Editors" +poweradmin permission-templates update --name "Zone Editors" --permissions 1,2,3,4 + +# Delete a template +poweradmin permission-templates delete --name "Zone Editors" --yes +``` + ### Version ```bash diff --git a/internal/cli/root.go b/internal/cli/root.go index 8c126e8..67f689c 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -8,6 +8,7 @@ import ( "os" "github.com/contentways/poweradmin-cli/internal/cmd/groups" + "github.com/contentways/poweradmin-cli/internal/cmd/permission_templates" "github.com/contentways/poweradmin-cli/internal/cmd/records" "github.com/contentways/poweradmin-cli/internal/cmd/users" cmdversion "github.com/contentways/poweradmin-cli/internal/cmd/version" @@ -70,6 +71,7 @@ func NewRootCommand(s *state.State) *cobra.Command { root.AddCommand(users.NewUsersCommand(s)) root.AddCommand(groups.NewGroupsCommand(s)) root.AddCommand(cmdversion.NewVersionCmd()) + root.AddCommand(permission_templates.NewPermissionTemplatesCommand(s)) return root } diff --git a/internal/cli/root_test.go b/internal/cli/root_test.go index 84cc0dc..5abedea 100644 --- a/internal/cli/root_test.go +++ b/internal/cli/root_test.go @@ -19,7 +19,7 @@ func TestNewRootCommand(t *testing.T) { t.Fatalf("Use = %q", cmd.Use) } - if len(cmd.Commands()) != 5 { - t.Fatalf("got %d commands, want 5", len(cmd.Commands())) + if len(cmd.Commands()) != 6 { + t.Fatalf("got %d commands, want 6", len(cmd.Commands())) } } diff --git a/internal/cmd/groups/create_test.go b/internal/cmd/groups/create_test.go index fa05f4b..3162f53 100644 --- a/internal/cmd/groups/create_test.go +++ b/internal/cmd/groups/create_test.go @@ -19,7 +19,7 @@ func TestGroupsCreate(t *testing.T) { return 42, nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewCreateCmd(), []string{"--name", "TestGroup"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -36,7 +36,7 @@ func TestGroupsCreateJSON(t *testing.T) { return 42, nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewCreateCmd(), []string{"--name", "TestGroup", "-o", "json"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -48,7 +48,7 @@ func TestGroupsCreateJSON(t *testing.T) { } func TestGroupsCreateMissingName(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}, nil) err := fx.Run(groups.NewCreateCmd(), []string{}) if err == nil { t.Fatal("expected error when name is missing") @@ -61,7 +61,7 @@ func TestGroupsCreateError(t *testing.T) { return 0, nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewCreateCmd(), []string{"--name", "TestGroup"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/groups/delete_test.go b/internal/cmd/groups/delete_test.go index 7874108..68569e7 100644 --- a/internal/cmd/groups/delete_test.go +++ b/internal/cmd/groups/delete_test.go @@ -22,7 +22,7 @@ func TestGroupsDelete(t *testing.T) { return nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewDeleteCmd(nil), []string{"--name", "TestGroup", "--yes"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -42,7 +42,7 @@ func TestGroupsDeleteJSON(t *testing.T) { return nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewDeleteCmd(nil), []string{"--name", "TestGroup", "--yes", "-o", "json"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -54,7 +54,7 @@ func TestGroupsDeleteJSON(t *testing.T) { } func TestGroupsDeleteMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}, nil) err := fx.Run(groups.NewDeleteCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -70,7 +70,7 @@ func TestGroupsDeleteError(t *testing.T) { return nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewDeleteCmd(nil), []string{"--name", "TestGroup", "--yes"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/groups/get_test.go b/internal/cmd/groups/get_test.go index 4e3075d..4782590 100644 --- a/internal/cmd/groups/get_test.go +++ b/internal/cmd/groups/get_test.go @@ -19,7 +19,7 @@ func TestGroupsGetByName(t *testing.T) { return &poweradmin.Group{ID: 1, Name: name, Description: "Full access"}, nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewGetCmd(nil), []string{"--name", "Administrators"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -36,7 +36,7 @@ func TestGroupsGetJSON(t *testing.T) { return &poweradmin.Group{ID: 1, Name: name}, nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewGetCmd(nil), []string{"--name", "Administrators", "-o", "json"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -48,7 +48,7 @@ func TestGroupsGetJSON(t *testing.T) { } func TestGroupsGetMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}, nil) err := fx.Run(groups.NewGetCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -61,7 +61,7 @@ func TestGroupsGetError(t *testing.T) { return nil, nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewGetCmd(nil), []string{"--name", "Administrators"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/groups/list_test.go b/internal/cmd/groups/list_test.go index fabf112..bc63337 100644 --- a/internal/cmd/groups/list_test.go +++ b/internal/cmd/groups/list_test.go @@ -22,7 +22,7 @@ func TestGroupsList(t *testing.T) { }, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewListCmd(nil), []string{}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -41,7 +41,7 @@ func TestGroupsListJSON(t *testing.T) { }, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewListCmd(nil), []string{"-o", "json"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -58,7 +58,7 @@ func TestGroupsListError(t *testing.T) { return nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewListCmd(nil), []string{}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/groups/member_add_test.go b/internal/cmd/groups/member_add_test.go index d657d23..3a66065 100644 --- a/internal/cmd/groups/member_add_test.go +++ b/internal/cmd/groups/member_add_test.go @@ -19,7 +19,7 @@ func TestGroupsMemberAdd(t *testing.T) { return nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewMemberAddCmd(nil), []string{"--group-id", "1", "--user-id", "2"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -31,7 +31,7 @@ func TestGroupsMemberAdd(t *testing.T) { } func TestGroupsMemberAddMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}, nil) err := fx.Run(groups.NewMemberAddCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -44,7 +44,7 @@ func TestGroupsMemberAddError(t *testing.T) { return nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewMemberAddCmd(nil), []string{"--group-id", "1", "--user-id", "2"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/groups/member_remove_test.go b/internal/cmd/groups/member_remove_test.go index 83d2542..7b9105e 100644 --- a/internal/cmd/groups/member_remove_test.go +++ b/internal/cmd/groups/member_remove_test.go @@ -19,7 +19,7 @@ func TestGroupsMemberRemove(t *testing.T) { return nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewMemberRemoveCmd(nil), []string{"--group-id", "1", "--user-id", "2"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -31,7 +31,7 @@ func TestGroupsMemberRemove(t *testing.T) { } func TestGroupsMemberRemoveMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}, nil) err := fx.Run(groups.NewMemberRemoveCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -44,7 +44,7 @@ func TestGroupsMemberRemoveError(t *testing.T) { return nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewMemberRemoveCmd(nil), []string{"--group-id", "1", "--user-id", "2"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/groups/members_test.go b/internal/cmd/groups/members_test.go index c0de089..5afe169 100644 --- a/internal/cmd/groups/members_test.go +++ b/internal/cmd/groups/members_test.go @@ -24,7 +24,7 @@ func TestGroupsMembers(t *testing.T) { }, nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewMembersCmd(nil), []string{"--name", "Administrators"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -36,7 +36,7 @@ func TestGroupsMembers(t *testing.T) { } func TestGroupsMembersMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}, nil) err := fx.Run(groups.NewMembersCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -52,7 +52,7 @@ func TestGroupsMembersError(t *testing.T) { return nil, nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewMembersCmd(nil), []string{"--name", "Administrators"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/groups/update_test.go b/internal/cmd/groups/update_test.go index 6ca4193..537cdf6 100644 --- a/internal/cmd/groups/update_test.go +++ b/internal/cmd/groups/update_test.go @@ -22,7 +22,7 @@ func TestGroupsUpdate(t *testing.T) { return &poweradmin.Group{ID: 42, Name: "NewName"}, nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewUpdateCmd(nil), []string{"--name", "TestGroup", "--new-name", "NewName"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -42,7 +42,7 @@ func TestGroupsUpdateJSON(t *testing.T) { return &poweradmin.Group{ID: 42, Name: "NewName"}, nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewUpdateCmd(nil), []string{"--name", "TestGroup", "--new-name", "NewName", "-o", "json"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -54,7 +54,7 @@ func TestGroupsUpdateJSON(t *testing.T) { } func TestGroupsUpdateMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}, nil) err := fx.Run(groups.NewUpdateCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -70,7 +70,7 @@ func TestGroupsUpdateError(t *testing.T) { return nil, nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewUpdateCmd(nil), []string{"--name", "TestGroup", "--new-name", "NewName"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/groups/zone_add_test.go b/internal/cmd/groups/zone_add_test.go index 7fcca6e..979f223 100644 --- a/internal/cmd/groups/zone_add_test.go +++ b/internal/cmd/groups/zone_add_test.go @@ -19,7 +19,7 @@ func TestGroupsZoneAdd(t *testing.T) { return nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewZoneAddCmd(nil), []string{"--group-id", "1", "--zone-id", "78"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -31,7 +31,7 @@ func TestGroupsZoneAdd(t *testing.T) { } func TestGroupsZoneAddMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}, nil) err := fx.Run(groups.NewZoneAddCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -44,7 +44,7 @@ func TestGroupsZoneAddError(t *testing.T) { return nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewZoneAddCmd(nil), []string{"--group-id", "1", "--zone-id", "78"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/groups/zone_remove_test.go b/internal/cmd/groups/zone_remove_test.go index 0f3f571..b3c5980 100644 --- a/internal/cmd/groups/zone_remove_test.go +++ b/internal/cmd/groups/zone_remove_test.go @@ -19,7 +19,7 @@ func TestGroupsZoneRemove(t *testing.T) { return nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewZoneRemoveCmd(nil), []string{"--group-id", "1", "--zone-id", "78"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -31,7 +31,7 @@ func TestGroupsZoneRemove(t *testing.T) { } func TestGroupsZoneRemoveMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}, nil) err := fx.Run(groups.NewZoneRemoveCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -44,7 +44,7 @@ func TestGroupsZoneRemoveError(t *testing.T) { return nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewZoneRemoveCmd(nil), []string{"--group-id", "1", "--zone-id", "78"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/groups/zones_test.go b/internal/cmd/groups/zones_test.go index 20bcef4..aa53d9f 100644 --- a/internal/cmd/groups/zones_test.go +++ b/internal/cmd/groups/zones_test.go @@ -24,7 +24,7 @@ func TestGroupsZones(t *testing.T) { }, nil, nil }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewZonesCmd(nil), []string{"--name", "Administrators"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -36,7 +36,7 @@ func TestGroupsZones(t *testing.T) { } func TestGroupsZonesMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, &testutil.MockGroupClient{}, nil) err := fx.Run(groups.NewZonesCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -52,7 +52,7 @@ func TestGroupsZonesError(t *testing.T) { return nil, nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, mockGroup, nil) err := fx.Run(groups.NewZonesCmd(nil), []string{"--name", "Administrators"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/permission_templates/create.go b/internal/cmd/permission_templates/create.go new file mode 100644 index 0000000..01c2b8a --- /dev/null +++ b/internal/cmd/permission_templates/create.go @@ -0,0 +1,88 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates + +import ( + "fmt" + "strconv" + "strings" + + "contentways.dev/contentways/poweradmin-go/v2/poweradmin" + "github.com/contentways/poweradmin-cli/internal/cmd/base" + "github.com/contentways/poweradmin-cli/internal/output" + "github.com/contentways/poweradmin-cli/internal/state" + "github.com/spf13/cobra" +) + +// NewCreateCmd returns a new "permission-templates create" command instance. +func NewCreateCmd(s *state.State) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "Create a permission template", + Long: `Create a new Poweradmin permission template.`, + RunE: func(cmd *cobra.Command, args []string) error { + s := state.FromContext(cmd.Context()) + + name, _ := cmd.Flags().GetString("name") + descr, _ := cmd.Flags().GetString("description") + tmplType, _ := cmd.Flags().GetString("type") + permsStr, _ := cmd.Flags().GetStringSlice("permissions") + + if name == "" { + return fmt.Errorf("--name is required") + } + + // Parse permission IDs. + var permissions []int + for _, p := range permsStr { + p = strings.TrimSpace(p) + if p == "" { + continue + } + id, err := strconv.Atoi(p) + if err != nil { + return fmt.Errorf("invalid permission ID %q: %w", p, err) + } + permissions = append(permissions, id) + } + + client, err := s.Client() + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + tmpl, _, err := client.PermissionTemplate.Create(cmd.Context(), poweradmin.PermissionTemplateOpts{ + Name: name, + Descr: descr, + TemplateType: tmplType, + Permissions: permissions, + }) + if err != nil { + return fmt.Errorf("failed to create permission template: %w", err) + } + + outputStr, _ := cmd.Flags().GetString("output") + outputFmt := output.ParseFormat(outputStr) + + if outputFmt == output.FormatJSON { + return base.PrintJSON(cmd, tmpl) + } + + if base.IsQuiet(cmd) { + fmt.Fprintln(cmd.OutOrStdout(), tmpl.ID) + return nil + } + + fmt.Fprintf(cmd.OutOrStdout(), "created permission template %s (id %d)\n", tmpl.Name, tmpl.ID) + return nil + }, + } + + cmd.Flags().String("name", "", "Template name (required)") + cmd.Flags().String("description", "", "Template description") + cmd.Flags().String("type", "user", "Template type. One of: user|group") + cmd.Flags().StringSlice("permissions", []string{}, "Permission IDs to assign (comma-separated or multiple flags)") + cmd.Flags().StringP("output", "o", "table", "Output format. One of: table|json") + cmd.Flags().BoolP("quiet", "q", false, "Only print the ID of the created template") + return cmd +} diff --git a/internal/cmd/permission_templates/create_test.go b/internal/cmd/permission_templates/create_test.go new file mode 100644 index 0000000..7d6236d --- /dev/null +++ b/internal/cmd/permission_templates/create_test.go @@ -0,0 +1,67 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "contentways.dev/contentways/poweradmin-go/v2/poweradmin" + "github.com/contentways/poweradmin-cli/internal/cmd/permission_templates" + "github.com/contentways/poweradmin-cli/internal/testutil" +) + +func TestPermissionTemplatesCreate(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + CreateFn: func(ctx context.Context, opts poweradmin.PermissionTemplateOpts) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: 1, Name: opts.Name, TemplateType: opts.TemplateType}, nil, nil + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewCreateCmd(nil), []string{"--name", "MyTemplate"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(fx.Stdout.String(), "MyTemplate") { + t.Errorf("expected MyTemplate in output, got:\n%s", fx.Stdout.String()) + } +} + +func TestPermissionTemplatesCreateJSON(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + CreateFn: func(ctx context.Context, opts poweradmin.PermissionTemplateOpts) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: 1, Name: opts.Name, TemplateType: "user"}, nil, nil + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewCreateCmd(nil), []string{"--name", "MyTemplate", "-o", "json"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(fx.Stdout.String(), "MyTemplate") { + t.Errorf("expected JSON output") + } +} + +func TestPermissionTemplatesCreateMissingName(t *testing.T) { + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, &testutil.MockPermissionTemplateClient{}) + err := fx.Run(permission_templates.NewCreateCmd(nil), []string{}) + if err == nil { + t.Fatal("expected error when --name missing") + } +} + +func TestPermissionTemplatesCreateError(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + CreateFn: func(ctx context.Context, opts poweradmin.PermissionTemplateOpts) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return nil, nil, fmt.Errorf("api error") + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewCreateCmd(nil), []string{"--name", "MyTemplate"}) + if err == nil { + t.Fatal("expected error, got nil") + } +} diff --git a/internal/cmd/permission_templates/delete.go b/internal/cmd/permission_templates/delete.go new file mode 100644 index 0000000..30ff03f --- /dev/null +++ b/internal/cmd/permission_templates/delete.go @@ -0,0 +1,89 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates + +import ( + "fmt" + "strconv" + + "github.com/contentways/poweradmin-cli/internal/cmd/base" + "github.com/contentways/poweradmin-cli/internal/output" + "github.com/contentways/poweradmin-cli/internal/state" + "github.com/spf13/cobra" +) + +// NewDeleteCmd returns a new "permission-templates delete" command instance. +func NewDeleteCmd(s *state.State) *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a permission template", + Long: `Delete a Poweradmin permission template by name or numeric ID.`, + RunE: func(cmd *cobra.Command, args []string) error { + s := state.FromContext(cmd.Context()) + + name, _ := cmd.Flags().GetString("name") + idStr, _ := cmd.Flags().GetString("id") + + if name == "" && idStr == "" { + return fmt.Errorf("either --name or --id is required") + } + + client, err := s.Client() + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + var tmplID int + var tmplName string + + if idStr != "" { + var err error + tmplID, err = strconv.Atoi(idStr) + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + tmplName = idStr + } else { + t, _, err := client.PermissionTemplate.GetByName(cmd.Context(), name) + if err != nil { + return fmt.Errorf("failed to resolve permission template: %w", err) + } + tmplID = t.ID + tmplName = t.Name + } + + if !base.Confirm(cmd, fmt.Sprintf("Delete permission template %s (id %d)? [y/N] ", tmplName, tmplID)) { + return nil + } + + _, err = client.PermissionTemplate.Delete(cmd.Context(), tmplID) + if err != nil { + return fmt.Errorf("failed to delete permission template: %w", err) + } + + if base.IsQuiet(cmd) { + return nil + } + + outputStr, _ := cmd.Flags().GetString("output") + outputFmt := output.ParseFormat(outputStr) + + if outputFmt == output.FormatJSON { + return base.PrintJSON(cmd, map[string]any{ + "id": tmplID, + "name": tmplName, + }) + } + + fmt.Fprintf(cmd.OutOrStdout(), "deleted permission template %s (id %d)\n", tmplName, tmplID) + return nil + }, + } + + cmd.Flags().String("name", "", "Template name to identify the template") + cmd.Flags().String("id", "", "Template ID to identify the template") + cmd.Flags().StringP("output", "o", "table", "Output format. One of: table|json") + cmd.Flags().BoolP("yes", "y", false, "Skip confirmation prompt") + cmd.Flags().BoolP("quiet", "q", false, "Suppress output after deletion") + return cmd +} diff --git a/internal/cmd/permission_templates/delete_test.go b/internal/cmd/permission_templates/delete_test.go new file mode 100644 index 0000000..541824c --- /dev/null +++ b/internal/cmd/permission_templates/delete_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "contentways.dev/contentways/poweradmin-go/v2/poweradmin" + "github.com/contentways/poweradmin-cli/internal/cmd/permission_templates" + "github.com/contentways/poweradmin-cli/internal/testutil" +) + +func TestPermissionTemplatesDelete(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + GetByNameFn: func(ctx context.Context, name string) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: 1, Name: name}, nil, nil + }, + DeleteFn: func(ctx context.Context, id int) (*poweradmin.Response, error) { + return nil, nil + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewDeleteCmd(nil), []string{"--name", "MyTemplate", "--yes"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(fx.Stdout.String(), "deleted") { + t.Errorf("expected deleted in output, got:\n%s", fx.Stdout.String()) + } +} + +func TestPermissionTemplatesDeleteJSON(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + GetByNameFn: func(ctx context.Context, name string) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: 1, Name: name}, nil, nil + }, + DeleteFn: func(ctx context.Context, id int) (*poweradmin.Response, error) { + return nil, nil + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewDeleteCmd(nil), []string{"--name", "MyTemplate", "--yes", "-o", "json"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(fx.Stdout.String(), `"name"`) { + t.Errorf("expected JSON output") + } +} + +func TestPermissionTemplatesDeleteMissingFlags(t *testing.T) { + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, &testutil.MockPermissionTemplateClient{}) + err := fx.Run(permission_templates.NewDeleteCmd(nil), []string{}) + if err == nil { + t.Fatal("expected error when no flags provided") + } +} + +func TestPermissionTemplatesDeleteError(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + GetByNameFn: func(ctx context.Context, name string) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return nil, nil, fmt.Errorf("api error") + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewDeleteCmd(nil), []string{"--name", "MyTemplate", "--yes"}) + if err == nil { + t.Fatal("expected error, got nil") + } +} diff --git a/internal/cmd/permission_templates/get.go b/internal/cmd/permission_templates/get.go new file mode 100644 index 0000000..45bc10f --- /dev/null +++ b/internal/cmd/permission_templates/get.go @@ -0,0 +1,89 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates + +import ( + "fmt" + "strconv" + "strings" + + "contentways.dev/contentways/poweradmin-go/v2/poweradmin" + "github.com/contentways/poweradmin-cli/internal/cmd/base" + "github.com/contentways/poweradmin-cli/internal/output" + "github.com/contentways/poweradmin-cli/internal/state" + "github.com/spf13/cobra" +) + +// NewGetCmd returns a new "permission-templates get" command instance. +// Fetches the full permission list by calling GetByID after resolving by name. +func NewGetCmd(s *state.State) *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: "Get a permission template by name or ID", + Long: `Get a Poweradmin permission template by name or numeric ID.`, + RunE: func(cmd *cobra.Command, args []string) error { + s := state.FromContext(cmd.Context()) + + name, _ := cmd.Flags().GetString("name") + idStr, _ := cmd.Flags().GetString("id") + + if name == "" && idStr == "" { + return fmt.Errorf("either --name or --id is required") + } + + client, err := s.Client() + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + var tmpl *poweradmin.PermissionTemplate + + if idStr != "" { + id, err := strconv.Atoi(idStr) + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + tmpl, _, err = client.PermissionTemplate.GetByID(cmd.Context(), id) + if err != nil { + return fmt.Errorf("failed to get permission template: %w", err) + } + } else { + // GetByName returns a list result without full permissions. + // Call GetByID to get the full permission list. + t, _, err := client.PermissionTemplate.GetByName(cmd.Context(), name) + if err != nil { + return fmt.Errorf("failed to get permission template: %w", err) + } + tmpl, _, err = client.PermissionTemplate.GetByID(cmd.Context(), t.ID) + if err != nil { + return fmt.Errorf("failed to get permission template details: %w", err) + } + } + + outputStr, _ := cmd.Flags().GetString("output") + outputFmt := output.ParseFormat(outputStr) + + if outputFmt == output.FormatJSON { + return base.PrintJSON(cmd, tmpl) + } + + fmt.Fprintf(cmd.OutOrStdout(), "ID: %d\n", tmpl.ID) + fmt.Fprintf(cmd.OutOrStdout(), "Name: %s\n", tmpl.Name) + fmt.Fprintf(cmd.OutOrStdout(), "Description: %s\n", tmpl.Descr) + fmt.Fprintf(cmd.OutOrStdout(), "Type: %s\n", tmpl.TemplateType) + if len(tmpl.Permissions) > 0 { + perms := make([]string, len(tmpl.Permissions)) + for i, p := range tmpl.Permissions { + perms[i] = fmt.Sprintf("%d (%s)", p.ID, p.Name) + } + fmt.Fprintf(cmd.OutOrStdout(), "Permissions: %s\n", strings.Join(perms, ", ")) + } + return nil + }, + } + + cmd.Flags().String("name", "", "Permission template name") + cmd.Flags().String("id", "", "Permission template ID") + cmd.Flags().StringP("output", "o", "table", "Output format. One of: table|json") + return cmd +} diff --git a/internal/cmd/permission_templates/get_test.go b/internal/cmd/permission_templates/get_test.go new file mode 100644 index 0000000..c18968e --- /dev/null +++ b/internal/cmd/permission_templates/get_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "contentways.dev/contentways/poweradmin-go/v2/poweradmin" + "github.com/contentways/poweradmin-cli/internal/cmd/permission_templates" + "github.com/contentways/poweradmin-cli/internal/testutil" +) + +func TestPermissionTemplatesGet(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + GetByNameFn: func(ctx context.Context, name string) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: 1, Name: name, Descr: "Full rights", TemplateType: "user"}, nil, nil + }, + GetByIDFn: func(ctx context.Context, id int) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: id, Name: "Administrator", Descr: "Full rights", TemplateType: "user"}, nil, nil + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewGetCmd(nil), []string{"--name", "Administrator"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(fx.Stdout.String(), "Administrator") { + t.Errorf("expected Administrator in output, got:\n%s", fx.Stdout.String()) + } +} + +func TestPermissionTemplatesGetJSON(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + GetByNameFn: func(ctx context.Context, name string) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: 1, Name: name, TemplateType: "user"}, nil, nil + }, + GetByIDFn: func(ctx context.Context, id int) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: id, Name: "Administrator", TemplateType: "user"}, nil, nil + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewGetCmd(nil), []string{"--name", "Administrator", "-o", "json"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(fx.Stdout.String(), "Administrator") { + t.Errorf("expected JSON output") + } +} + +func TestPermissionTemplatesGetMissingFlags(t *testing.T) { + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, &testutil.MockPermissionTemplateClient{}) + err := fx.Run(permission_templates.NewGetCmd(nil), []string{}) + if err == nil { + t.Fatal("expected error when no flags provided") + } +} + +func TestPermissionTemplatesGetError(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + GetByNameFn: func(ctx context.Context, name string) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return nil, nil, fmt.Errorf("api error") + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewGetCmd(nil), []string{"--name", "Administrator"}) + if err == nil { + t.Fatal("expected error, got nil") + } +} diff --git a/internal/cmd/permission_templates/list.go b/internal/cmd/permission_templates/list.go new file mode 100644 index 0000000..e354847 --- /dev/null +++ b/internal/cmd/permission_templates/list.go @@ -0,0 +1,78 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates + +import ( + "fmt" + "strconv" + + "github.com/contentways/poweradmin-cli/internal/cmd/base" + "github.com/contentways/poweradmin-cli/internal/output" + "github.com/contentways/poweradmin-cli/internal/state" + "github.com/spf13/cobra" +) + +// NewListCmd returns a new "permission-templates list" command instance. +func NewListCmd(s *state.State) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List all permission templates", + Long: `List all permission templates in Poweradmin.`, + RunE: func(cmd *cobra.Command, args []string) error { + s := state.FromContext(cmd.Context()) + + client, err := s.Client() + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + templates, _, err := client.PermissionTemplate.List(cmd.Context()) + if err != nil { + return fmt.Errorf("failed to list permission templates: %w", err) + } + + outputStr, _ := cmd.Flags().GetString("output") + outputFmt := output.ParseFormat(outputStr) + + if outputFmt == output.FormatJSON { + type templateJSON struct { + ID int `json:"id"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + TemplateType string `json:"template_type"` + } + type listJSON struct { + Templates []templateJSON `json:"permission_templates"` + Count int `json:"count"` + } + list := listJSON{Count: len(templates)} + for _, t := range templates { + list.Templates = append(list.Templates, templateJSON{ + ID: t.ID, + Name: t.Name, + Description: t.Descr, + TemplateType: t.TemplateType, + }) + } + return base.PrintJSON(cmd, list) + } + + t := base.NewTable(cmd) + t.AddHeader("ID", "NAME", "DESCRIPTION", "TYPE") + for _, tmpl := range templates { + t.AddColoredRow( + output.PlainCell(strconv.Itoa(tmpl.ID)), + output.PlainCell(tmpl.Name), + output.PlainCell(tmpl.Descr), + output.Cell(tmpl.TemplateType, output.CyanCode()), + ) + } + t.Flush() + return nil + }, + } + + cmd.Flags().StringP("output", "o", "table", "Output format. One of: table|json") + cmd.Flags().Bool("no-header", false, "Suppress table header row") + return cmd +} diff --git a/internal/cmd/permission_templates/list_test.go b/internal/cmd/permission_templates/list_test.go new file mode 100644 index 0000000..ec5292e --- /dev/null +++ b/internal/cmd/permission_templates/list_test.go @@ -0,0 +1,64 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "contentways.dev/contentways/poweradmin-go/v2/poweradmin" + "github.com/contentways/poweradmin-cli/internal/cmd/permission_templates" + "github.com/contentways/poweradmin-cli/internal/testutil" +) + +func TestPermissionTemplatesList(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + ListFn: func(ctx context.Context) ([]*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return []*poweradmin.PermissionTemplate{ + {ID: 1, Name: "Administrator", Descr: "Full rights", TemplateType: "user"}, + {ID: 2, Name: "Editor", Descr: "Edit only", TemplateType: "user"}, + }, nil, nil + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewListCmd(nil), []string{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(fx.Stdout.String(), "Administrator") { + t.Errorf("expected Administrator in output, got:\n%s", fx.Stdout.String()) + } +} + +func TestPermissionTemplatesListJSON(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + ListFn: func(ctx context.Context) ([]*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return []*poweradmin.PermissionTemplate{ + {ID: 1, Name: "Administrator", Descr: "Full rights", TemplateType: "user"}, + }, nil, nil + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewListCmd(nil), []string{"-o", "json"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(fx.Stdout.String(), `"name": "Administrator"`) { + t.Errorf("expected JSON output with name") + } +} + +func TestPermissionTemplatesListError(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + ListFn: func(ctx context.Context) ([]*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return nil, nil, fmt.Errorf("api error") + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewListCmd(nil), []string{}) + if err == nil { + t.Fatal("expected error, got nil") + } +} diff --git a/internal/cmd/permission_templates/permission_templates.go b/internal/cmd/permission_templates/permission_templates.go new file mode 100644 index 0000000..9b0bb0c --- /dev/null +++ b/internal/cmd/permission_templates/permission_templates.go @@ -0,0 +1,30 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT + +// Package permission_templates provides CLI commands for managing Poweradmin +// permission templates via the REST API. +package permission_templates + +import ( + "github.com/contentways/poweradmin-cli/internal/state" + "github.com/spf13/cobra" +) + +// NewPermissionTemplatesCommand returns the "permission-templates" subcommand +// with all child commands registered. +func NewPermissionTemplatesCommand(s *state.State) *cobra.Command { + cmd := &cobra.Command{ + Use: "permission-templates", + Aliases: []string{"pt"}, + Short: "Manage Poweradmin permission templates", + Long: `Create, list, update and delete Poweradmin permission templates.`, + } + + cmd.AddCommand(NewListCmd(s)) + cmd.AddCommand(NewGetCmd(s)) + cmd.AddCommand(NewCreateCmd(s)) + cmd.AddCommand(NewUpdateCmd(s)) + cmd.AddCommand(NewDeleteCmd(s)) + + return cmd +} diff --git a/internal/cmd/permission_templates/permission_templates_test.go b/internal/cmd/permission_templates/permission_templates_test.go new file mode 100644 index 0000000..5cf3126 --- /dev/null +++ b/internal/cmd/permission_templates/permission_templates_test.go @@ -0,0 +1,19 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates_test + +import ( + "testing" + + "github.com/contentways/poweradmin-cli/internal/cmd/permission_templates" +) + +func TestNewPermissionTemplatesCommand(t *testing.T) { + cmd := permission_templates.NewPermissionTemplatesCommand(nil) + if cmd == nil { + t.Fatal("expected command, got nil") + } + if got := len(cmd.Commands()); got != 5 { + t.Errorf("got %d commands, want 5", got) + } +} diff --git a/internal/cmd/permission_templates/update.go b/internal/cmd/permission_templates/update.go new file mode 100644 index 0000000..d1ed026 --- /dev/null +++ b/internal/cmd/permission_templates/update.go @@ -0,0 +1,118 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates + +import ( + "fmt" + "strconv" + "strings" + + "contentways.dev/contentways/poweradmin-go/v2/poweradmin" + "github.com/contentways/poweradmin-cli/internal/cmd/base" + "github.com/contentways/poweradmin-cli/internal/output" + "github.com/contentways/poweradmin-cli/internal/state" + "github.com/spf13/cobra" +) + +// NewUpdateCmd returns a new "permission-templates update" command instance. +func NewUpdateCmd(s *state.State) *cobra.Command { + cmd := &cobra.Command{ + Use: "update", + Short: "Update a permission template", + Long: `Update an existing Poweradmin permission template by name or numeric ID.`, + RunE: func(cmd *cobra.Command, args []string) error { + s := state.FromContext(cmd.Context()) + + name, _ := cmd.Flags().GetString("name") + idStr, _ := cmd.Flags().GetString("id") + + if name == "" && idStr == "" { + return fmt.Errorf("either --name or --id is required") + } + + client, err := s.Client() + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + // Resolve ID. + var tmplID int + if idStr != "" { + tmplID, err = strconv.Atoi(idStr) + if err != nil { + return fmt.Errorf("invalid id: %w", err) + } + } else { + t, _, err := client.PermissionTemplate.GetByName(cmd.Context(), name) + if err != nil { + return fmt.Errorf("failed to resolve permission template: %w", err) + } + tmplID = t.ID + } + + // Fetch current state to preserve unchanged fields. + current, _, err := client.PermissionTemplate.GetByID(cmd.Context(), tmplID) + if err != nil { + return fmt.Errorf("failed to get permission template: %w", err) + } + + opts := poweradmin.PermissionTemplateOpts{ + Name: current.Name, + Descr: current.Descr, + TemplateType: current.TemplateType, + } + for _, p := range current.Permissions { + opts.Permissions = append(opts.Permissions, p.ID) + } + + if cmd.Flags().Changed("new-name") { + opts.Name, _ = cmd.Flags().GetString("new-name") + } + if cmd.Flags().Changed("description") { + opts.Descr, _ = cmd.Flags().GetString("description") + } + if cmd.Flags().Changed("type") { + opts.TemplateType, _ = cmd.Flags().GetString("type") + } + if cmd.Flags().Changed("permissions") { + permsStr, _ := cmd.Flags().GetStringSlice("permissions") + opts.Permissions = nil + for _, p := range permsStr { + p = strings.TrimSpace(p) + if p == "" { + continue + } + id, err := strconv.Atoi(p) + if err != nil { + return fmt.Errorf("invalid permission ID %q: %w", p, err) + } + opts.Permissions = append(opts.Permissions, id) + } + } + + updated, _, err := client.PermissionTemplate.Update(cmd.Context(), tmplID, opts) + if err != nil { + return fmt.Errorf("failed to update permission template: %w", err) + } + + outputStr, _ := cmd.Flags().GetString("output") + outputFmt := output.ParseFormat(outputStr) + + if outputFmt == output.FormatJSON { + return base.PrintJSON(cmd, updated) + } + + fmt.Fprintf(cmd.OutOrStdout(), "updated permission template %s (id %d)\n", updated.Name, updated.ID) + return nil + }, + } + + cmd.Flags().String("name", "", "Template name to identify the template") + cmd.Flags().String("id", "", "Template ID to identify the template") + cmd.Flags().String("new-name", "", "New template name") + cmd.Flags().String("description", "", "New description") + cmd.Flags().String("type", "", "New template type. One of: user|group") + cmd.Flags().StringSlice("permissions", []string{}, "New permission IDs (replaces existing)") + cmd.Flags().StringP("output", "o", "table", "Output format. One of: table|json") + return cmd +} diff --git a/internal/cmd/permission_templates/update_test.go b/internal/cmd/permission_templates/update_test.go new file mode 100644 index 0000000..7cd5379 --- /dev/null +++ b/internal/cmd/permission_templates/update_test.go @@ -0,0 +1,57 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package permission_templates_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "contentways.dev/contentways/poweradmin-go/v2/poweradmin" + "github.com/contentways/poweradmin-cli/internal/cmd/permission_templates" + "github.com/contentways/poweradmin-cli/internal/testutil" +) + +func TestPermissionTemplatesUpdate(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + GetByNameFn: func(ctx context.Context, name string) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: 1, Name: name, TemplateType: "user"}, nil, nil + }, + GetByIDFn: func(ctx context.Context, id int) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: id, Name: "Administrator", TemplateType: "user"}, nil, nil + }, + UpdateFn: func(ctx context.Context, id int, opts poweradmin.PermissionTemplateOpts) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return &poweradmin.PermissionTemplate{ID: id, Name: opts.Name, TemplateType: opts.TemplateType}, nil, nil + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewUpdateCmd(nil), []string{"--name", "Administrator", "--new-name", "SuperAdmin"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(fx.Stdout.String(), "updated") { + t.Errorf("expected updated in output, got:\n%s", fx.Stdout.String()) + } +} + +func TestPermissionTemplatesUpdateMissingFlags(t *testing.T) { + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, &testutil.MockPermissionTemplateClient{}) + err := fx.Run(permission_templates.NewUpdateCmd(nil), []string{}) + if err == nil { + t.Fatal("expected error when no flags provided") + } +} + +func TestPermissionTemplatesUpdateError(t *testing.T) { + mock := &testutil.MockPermissionTemplateClient{ + GetByNameFn: func(ctx context.Context, name string) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + return nil, nil, fmt.Errorf("api error") + }, + } + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, nil, nil, mock) + err := fx.Run(permission_templates.NewUpdateCmd(nil), []string{"--name", "Administrator", "--new-name", "SuperAdmin"}) + if err == nil { + t.Fatal("expected error, got nil") + } +} diff --git a/internal/cmd/records/get_test.go b/internal/cmd/records/get_test.go index 8929e06..b59463a 100644 --- a/internal/cmd/records/get_test.go +++ b/internal/cmd/records/get_test.go @@ -27,7 +27,7 @@ func TestRecordsGet(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil, nil) err := fx.Run(records.NewGetCmd(nil), []string{ "--zone-name", "example.com", @@ -57,7 +57,7 @@ func TestRecordsGetJSON(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil, nil) err := fx.Run(records.NewGetCmd(nil), []string{ "--zone-name", "example.com", @@ -86,7 +86,7 @@ func TestRecordsGetNotFound(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil, nil) err := fx.Run(records.NewGetCmd(nil), []string{ "--zone-name", "example.com", @@ -98,7 +98,7 @@ func TestRecordsGetNotFound(t *testing.T) { } func TestRecordsGetMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil, nil) err := fx.Run(records.NewGetCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -117,7 +117,7 @@ func TestRecordsGetError(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil, nil) err := fx.Run(records.NewGetCmd(nil), []string{ "--zone-name", "example.com", diff --git a/internal/cmd/records/update_test.go b/internal/cmd/records/update_test.go index 23116b3..6cd1150 100644 --- a/internal/cmd/records/update_test.go +++ b/internal/cmd/records/update_test.go @@ -25,7 +25,7 @@ func TestRecordsUpdate(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil, nil) err := fx.Run(records.NewUpdateCmd(nil), []string{ "--zone-name", "example.com", @@ -54,7 +54,7 @@ func TestRecordsUpdateJSON(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil, nil) err := fx.Run(records.NewUpdateCmd(nil), []string{ "--zone-name", "example.com", @@ -73,7 +73,7 @@ func TestRecordsUpdateJSON(t *testing.T) { } func TestRecordsUpdateMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil, nil) err := fx.Run(records.NewUpdateCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -92,7 +92,7 @@ func TestRecordsUpdateError(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil, nil) err := fx.Run(records.NewUpdateCmd(nil), []string{ "--zone-name", "example.com", diff --git a/internal/cmd/users/create_test.go b/internal/cmd/users/create_test.go index 5267044..8029615 100644 --- a/internal/cmd/users/create_test.go +++ b/internal/cmd/users/create_test.go @@ -20,7 +20,7 @@ func TestUsersCreate(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewCreateCmd(), []string{ "--username", "max", @@ -47,7 +47,7 @@ func TestUsersCreateJSON(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewCreateCmd(), []string{ "--username", "max", @@ -69,7 +69,7 @@ func TestUsersCreateJSON(t *testing.T) { } func TestUsersCreateMissingUsername(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, &testutil.MockUserClient{}, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, &testutil.MockUserClient{}, nil, nil) err := fx.Run(users.NewCreateCmd(), []string{ "--password", "secret123", @@ -87,7 +87,7 @@ func TestUsersCreateError(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewCreateCmd(), []string{ "--username", "max", diff --git a/internal/cmd/users/delete_test.go b/internal/cmd/users/delete_test.go index 1b9fa99..d8f7182 100644 --- a/internal/cmd/users/delete_test.go +++ b/internal/cmd/users/delete_test.go @@ -23,7 +23,7 @@ func TestUsersDelete(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewDeleteCmd(nil), []string{"--name", "max", "--yes"}) if err != nil { @@ -49,7 +49,7 @@ func TestUsersDeleteJSON(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewDeleteCmd(nil), []string{"--name", "max", "--yes", "--output", "json"}) if err != nil { @@ -63,7 +63,7 @@ func TestUsersDeleteJSON(t *testing.T) { } func TestUsersDeleteMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, &testutil.MockUserClient{}, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, &testutil.MockUserClient{}, nil, nil) err := fx.Run(users.NewDeleteCmd(nil), []string{}) if err == nil { @@ -81,7 +81,7 @@ func TestUsersDeleteError(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewDeleteCmd(nil), []string{"--name", "max", "--yes"}) if err == nil { diff --git a/internal/cmd/users/get_test.go b/internal/cmd/users/get_test.go index 1b8f2ee..fca20a2 100644 --- a/internal/cmd/users/get_test.go +++ b/internal/cmd/users/get_test.go @@ -20,7 +20,7 @@ func TestUsersGetByName(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewGetCmd(nil), []string{"--name", "max"}) if err != nil { @@ -40,7 +40,7 @@ func TestUsersGetJSON(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewGetCmd(nil), []string{"--name", "max", "--output", "json"}) if err != nil { @@ -54,7 +54,7 @@ func TestUsersGetJSON(t *testing.T) { } func TestUsersGetMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, &testutil.MockUserClient{}, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, &testutil.MockUserClient{}, nil, nil) err := fx.Run(users.NewGetCmd(nil), []string{}) if err == nil { @@ -69,7 +69,7 @@ func TestUsersGetError(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewGetCmd(nil), []string{"--name", "max"}) if err == nil { diff --git a/internal/cmd/users/list_test.go b/internal/cmd/users/list_test.go index 1cc37de..15fc297 100644 --- a/internal/cmd/users/list_test.go +++ b/internal/cmd/users/list_test.go @@ -23,7 +23,7 @@ func TestUsersList(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewListCmd(), []string{}) if err != nil { @@ -48,7 +48,7 @@ func TestUsersListJSON(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewListCmd(), []string{"--output", "json"}) if err != nil { @@ -68,7 +68,7 @@ func TestUsersListError(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewListCmd(), []string{}) if err == nil { diff --git a/internal/cmd/users/set_permission_template_test.go b/internal/cmd/users/set_permission_template_test.go index d3ec81f..ec8e497 100644 --- a/internal/cmd/users/set_permission_template_test.go +++ b/internal/cmd/users/set_permission_template_test.go @@ -23,7 +23,7 @@ func TestUsersSetPermissionTemplate(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewSetPermissionTemplateCmd(nil), []string{ "--name", "max", @@ -52,7 +52,7 @@ func TestUsersSetPermissionTemplateJSON(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewSetPermissionTemplateCmd(nil), []string{ "--name", "max", @@ -73,7 +73,7 @@ func TestUsersSetPermissionTemplateJSON(t *testing.T) { } func TestUsersSetPermissionTemplateMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, &testutil.MockUserClient{}, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, &testutil.MockUserClient{}, nil, nil) err := fx.Run(users.NewSetPermissionTemplateCmd(nil), []string{}) if err == nil { @@ -91,7 +91,7 @@ func TestUsersSetPermissionTemplateError(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewSetPermissionTemplateCmd(nil), []string{ "--name", "max", diff --git a/internal/cmd/users/update_test.go b/internal/cmd/users/update_test.go index f5449d9..63cef62 100644 --- a/internal/cmd/users/update_test.go +++ b/internal/cmd/users/update_test.go @@ -23,7 +23,7 @@ func TestUsersUpdate(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewUpdateCmd(nil), []string{ "--name", "max", @@ -49,7 +49,7 @@ func TestUsersUpdateJSON(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewUpdateCmd(nil), []string{ "--name", "max", @@ -67,7 +67,7 @@ func TestUsersUpdateJSON(t *testing.T) { } func TestUsersUpdateMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, &testutil.MockUserClient{}, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, &testutil.MockUserClient{}, nil, nil) err := fx.Run(users.NewUpdateCmd(nil), []string{}) if err == nil { @@ -85,7 +85,7 @@ func TestUsersUpdateError(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil) + fx := testutil.NewFixtureWithAllMocks(t, nil, nil, mockUser, nil, nil) err := fx.Run(users.NewUpdateCmd(nil), []string{ "--name", "max", diff --git a/internal/cmd/zones/export_test.go b/internal/cmd/zones/export_test.go index c77b787..99a0e14 100644 --- a/internal/cmd/zones/export_test.go +++ b/internal/cmd/zones/export_test.go @@ -47,7 +47,7 @@ func TestZonesExport(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil, nil) err := fx.Run(zones.NewExportCmd(nil), []string{"--name", "example.com"}) if err != nil { @@ -70,7 +70,7 @@ func TestZonesExport(t *testing.T) { } func TestZonesExportMissingFlags(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil, nil) err := fx.Run(zones.NewExportCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no flags provided") @@ -83,7 +83,7 @@ func TestZonesExportError(t *testing.T) { return nil, nil, fmt.Errorf("api error") }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, &testutil.MockRecordClient{}, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, &testutil.MockRecordClient{}, nil, nil, nil) err := fx.Run(zones.NewExportCmd(nil), []string{"--name", "example.com"}) if err == nil { t.Fatal("expected error, got nil") diff --git a/internal/cmd/zones/import_test.go b/internal/cmd/zones/import_test.go index 845409f..965cdab 100644 --- a/internal/cmd/zones/import_test.go +++ b/internal/cmd/zones/import_test.go @@ -36,7 +36,7 @@ www IN A 1.2.3.4 `) f.Close() - fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil, nil) err = fx.Run(zones.NewImportCmd(nil), []string{"--file", f.Name(), "--dry-run"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -76,7 +76,7 @@ _dmarc IN TXT v=DMARC1; p=reject; rua=mailto:postmaster@example.co `) f.Close() - fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil, nil) err = fx.Run(zones.NewImportCmd(nil), []string{"--file", f.Name(), "--dry-run"}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -89,7 +89,7 @@ _dmarc IN TXT v=DMARC1; p=reject; rua=mailto:postmaster@example.co } func TestZonesImportMissingFile(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil, nil) err := fx.Run(zones.NewImportCmd(nil), []string{}) if err == nil { t.Fatal("expected error when no --file provided") @@ -97,7 +97,7 @@ func TestZonesImportMissingFile(t *testing.T) { } func TestZonesImportFileNotFound(t *testing.T) { - fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, &testutil.MockZoneClient{}, &testutil.MockRecordClient{}, nil, nil, nil) err := fx.Run(zones.NewImportCmd(nil), []string{"--file", "/tmp/nonexistent.zone"}) if err == nil { t.Fatal("expected error for nonexistent file") @@ -123,7 +123,7 @@ $TTL 3600 }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, &testutil.MockRecordClient{}, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, &testutil.MockRecordClient{}, nil, nil, nil) err = fx.Run(zones.NewImportCmd(nil), []string{"--file", f.Name()}) if err == nil { t.Fatal("expected error when zone not found and --create-zone not set") @@ -158,7 +158,7 @@ $TTL 3600 }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, mockRecord, nil, nil, nil) err = fx.Run(zones.NewImportCmd(nil), []string{"--file", f.Name(), "--create-zone"}) if err != nil { t.Fatalf("unexpected error: %v", err) diff --git a/internal/cmd/zones/list_test.go b/internal/cmd/zones/list_test.go index 465e8ed..9fc34bd 100644 --- a/internal/cmd/zones/list_test.go +++ b/internal/cmd/zones/list_test.go @@ -87,7 +87,7 @@ func TestZonesListFilterByType(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, nil, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, nil, nil, nil, nil) err := fx.Run(zones.NewListCmd(nil), []string{"--type", "NATIVE"}) if err != nil { @@ -113,7 +113,7 @@ func TestZonesListFilterByName(t *testing.T) { }, } - fx := testutil.NewFixtureWithAllMocks(t, mockZone, nil, nil, nil) + fx := testutil.NewFixtureWithAllMocks(t, mockZone, nil, nil, nil, nil) err := fx.Run(zones.NewListCmd(nil), []string{"--name-filter", "contentways"}) if err != nil { diff --git a/internal/testutil/fixture.go b/internal/testutil/fixture.go index d5dab80..4b9830f 100644 --- a/internal/testutil/fixture.go +++ b/internal/testutil/fixture.go @@ -77,8 +77,9 @@ func (f *Fixture) Run(cmd *cobra.Command, args []string) error { return cmd.Execute() } -// NewFixtureWithAllMocks creates a Fixture with mock Zone, Record and User clients. -func NewFixtureWithAllMocks(t *testing.T, zone poweradmin.IZoneClient, record poweradmin.IRecordClient, user poweradmin.IUserClient, group poweradmin.IGroupClient) *Fixture { +// NewFixtureWithAllMocks creates a Fixture with mock Zone, Record, User, Group +// and PermissionTemplate clients. +func NewFixtureWithAllMocks(t *testing.T, zone poweradmin.IZoneClient, record poweradmin.IRecordClient, user poweradmin.IUserClient, group poweradmin.IGroupClient, permTemplate poweradmin.IPermissionTemplateClient) *Fixture { t.Helper() client, _ := poweradmin.NewClient( poweradmin.WithBaseURL("https://test.example.com"), @@ -88,6 +89,7 @@ func NewFixtureWithAllMocks(t *testing.T, zone poweradmin.IZoneClient, record po client.Record = record client.User = user client.Group = group + client.PermissionTemplate = permTemplate s := state.New("https://test.example.com", "test-key") s.MockClient = client return &Fixture{ diff --git a/internal/testutil/mock_permission_template_client.go b/internal/testutil/mock_permission_template_client.go new file mode 100644 index 0000000..7dfff8e --- /dev/null +++ b/internal/testutil/mock_permission_template_client.go @@ -0,0 +1,63 @@ +// Copyright (c) 2026 Contentways +// SPDX-License-Identifier: MIT +package testutil + +import ( + "context" + + "contentways.dev/contentways/poweradmin-go/v2/poweradmin" +) + +// MockPermissionTemplateClient implements poweradmin.IPermissionTemplateClient for testing. +// Each method can be overridden by setting the corresponding function field. +// Unset fields return zero values and no error by default. +type MockPermissionTemplateClient struct { + GetByNameFn func(ctx context.Context, name string) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) + GetByIDFn func(ctx context.Context, id int) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) + ListFn func(ctx context.Context) ([]*poweradmin.PermissionTemplate, *poweradmin.Response, error) + CreateFn func(ctx context.Context, opts poweradmin.PermissionTemplateOpts) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) + UpdateFn func(ctx context.Context, id int, opts poweradmin.PermissionTemplateOpts) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) + DeleteFn func(ctx context.Context, id int) (*poweradmin.Response, error) +} + +func (m *MockPermissionTemplateClient) GetByName(ctx context.Context, name string) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + if m.GetByNameFn != nil { + return m.GetByNameFn(ctx, name) + } + return nil, nil, nil +} + +func (m *MockPermissionTemplateClient) GetByID(ctx context.Context, id int) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + if m.GetByIDFn != nil { + return m.GetByIDFn(ctx, id) + } + return nil, nil, nil +} + +func (m *MockPermissionTemplateClient) List(ctx context.Context) ([]*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + if m.ListFn != nil { + return m.ListFn(ctx) + } + return nil, nil, nil +} + +func (m *MockPermissionTemplateClient) Create(ctx context.Context, opts poweradmin.PermissionTemplateOpts) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + if m.CreateFn != nil { + return m.CreateFn(ctx, opts) + } + return nil, nil, nil +} + +func (m *MockPermissionTemplateClient) Update(ctx context.Context, id int, opts poweradmin.PermissionTemplateOpts) (*poweradmin.PermissionTemplate, *poweradmin.Response, error) { + if m.UpdateFn != nil { + return m.UpdateFn(ctx, id, opts) + } + return nil, nil, nil +} + +func (m *MockPermissionTemplateClient) Delete(ctx context.Context, id int) (*poweradmin.Response, error) { + if m.DeleteFn != nil { + return m.DeleteFn(ctx, id) + } + return nil, nil +}