Skip to content

Commit afce8d4

Browse files
authored
Show expired provisioning profiles in list (#1608)
1 parent bf56bf3 commit afce8d4

6 files changed

Lines changed: 213 additions & 4 deletions

File tree

internal/asc/client_http_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7189,6 +7189,9 @@ func TestGetProfiles_WithFilter(t *testing.T) {
71897189
if values.Get("filter[profileType]") != "IOS_APP_DEVELOPMENT,IOS_APP_STORE" {
71907190
t.Fatalf("expected filter[profileType] to be set, got %q", values.Get("filter[profileType]"))
71917191
}
7192+
if values.Get("filter[profileState]") != "ACTIVE,INVALID" {
7193+
t.Fatalf("expected filter[profileState] to be set, got %q", values.Get("filter[profileState]"))
7194+
}
71927195
if values.Get("limit") != "5" {
71937196
t.Fatalf("expected limit=5, got %q", values.Get("limit"))
71947197
}
@@ -7198,6 +7201,7 @@ func TestGetProfiles_WithFilter(t *testing.T) {
71987201
if _, err := client.GetProfiles(
71997202
context.Background(),
72007203
WithProfilesTypes([]string{"IOS_APP_DEVELOPMENT", "IOS_APP_STORE"}),
7204+
WithProfilesStates([]string{"ACTIVE", "INVALID"}),
72017205
WithProfilesLimit(5),
72027206
); err != nil {
72037207
t.Fatalf("GetProfiles() error: %v", err)

internal/asc/client_options.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2552,6 +2552,13 @@ func WithProfilesTypes(types []string) ProfilesOption {
25522552
}
25532553
}
25542554

2555+
// WithProfilesStates filters profiles by profile state.
2556+
func WithProfilesStates(states []string) ProfilesOption {
2557+
return func(q *profilesQuery) {
2558+
q.profileStates = normalizeUpperList(states)
2559+
}
2560+
}
2561+
25552562
// WithProfilesInclude sets include for profile responses.
25562563
func WithProfilesInclude(include []string) ProfilesOption {
25572564
return func(q *profilesQuery) {

internal/asc/client_queries.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,9 +464,10 @@ type merchantIDCertificatesQuery struct {
464464

465465
type profilesQuery struct {
466466
listQuery
467-
bundleID string
468-
profileTypes []string
469-
include []string
467+
bundleID string
468+
profileTypes []string
469+
profileStates []string
470+
include []string
470471
}
471472

472473
type usersQuery struct {
@@ -974,6 +975,7 @@ func buildProfilesQuery(query *profilesQuery) string {
974975
values.Set("filter[bundleId]", strings.TrimSpace(query.bundleID))
975976
}
976977
addCSV(values, "filter[profileType]", query.profileTypes)
978+
addCSV(values, "filter[profileState]", query.profileStates)
977979
addCSV(values, "include", query.include)
978980
addLimit(values, query.limit)
979981
return values.Encode()

internal/asc/signing.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,8 @@ type CertificateResponse = SingleResponse[CertificateAttributes]
173173
type ProfileState string
174174

175175
const (
176-
ProfileStateActive ProfileState = "ACTIVE"
176+
ProfileStateActive ProfileState = "ACTIVE"
177+
ProfileStateInvalid ProfileState = "INVALID"
177178
)
178179

179180
// ProfileAttributes describes a profile resource.

internal/cli/cmdtest/profiles_next_validation_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package cmdtest
22

33
import (
44
"context"
5+
"encoding/json"
6+
"errors"
7+
"flag"
58
"io"
69
"net/http"
710
"path/filepath"
@@ -56,6 +59,166 @@ func TestProfilesListRejectsInvalidNextURL(t *testing.T) {
5659
}
5760
}
5861

62+
func TestProfilesListDefaultsToActiveAndInvalidStates(t *testing.T) {
63+
setupAuth(t)
64+
t.Setenv("ASC_CONFIG_PATH", filepath.Join(t.TempDir(), "nonexistent.json"))
65+
66+
originalTransport := http.DefaultTransport
67+
t.Cleanup(func() {
68+
http.DefaultTransport = originalTransport
69+
})
70+
71+
http.DefaultTransport = roundTripFunc(func(req *http.Request) (*http.Response, error) {
72+
if req.Method != http.MethodGet || req.URL.Path != "/v1/profiles" {
73+
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.String())
74+
}
75+
if got := req.URL.Query().Get("filter[profileState]"); got != "ACTIVE,INVALID" {
76+
t.Fatalf("expected default profileState filter ACTIVE,INVALID, got %q", got)
77+
}
78+
body := `{"data":[` +
79+
`{"type":"profiles","id":"profile-active","attributes":{"name":"Active","profileType":"IOS_APP_STORE","profileState":"ACTIVE"}},` +
80+
`{"type":"profiles","id":"profile-invalid","attributes":{"name":"Expired","profileType":"IOS_APP_ADHOC","profileState":"INVALID"}}` +
81+
`]}`
82+
return &http.Response{
83+
StatusCode: http.StatusOK,
84+
Body: io.NopCloser(strings.NewReader(body)),
85+
Header: http.Header{"Content-Type": []string{"application/json"}},
86+
}, nil
87+
})
88+
89+
root := RootCommand("1.2.3")
90+
root.FlagSet.SetOutput(io.Discard)
91+
92+
var runErr error
93+
stdout, stderr := captureOutput(t, func() {
94+
if err := root.Parse([]string{"profiles", "list", "--output", "json"}); err != nil {
95+
t.Fatalf("parse error: %v", err)
96+
}
97+
runErr = root.Run(context.Background())
98+
})
99+
100+
if runErr != nil {
101+
t.Fatalf("run error: %v", runErr)
102+
}
103+
if stderr != "" {
104+
t.Fatalf("expected empty stderr, got %q", stderr)
105+
}
106+
107+
var payload struct {
108+
Data []struct {
109+
ID string `json:"id"`
110+
Attributes struct {
111+
ProfileState string `json:"profileState"`
112+
} `json:"attributes"`
113+
} `json:"data"`
114+
}
115+
if err := json.Unmarshal([]byte(stdout), &payload); err != nil {
116+
t.Fatalf("unmarshal profiles output: %v\n%s", err, stdout)
117+
}
118+
if len(payload.Data) != 2 {
119+
t.Fatalf("expected active and invalid profiles, got %d", len(payload.Data))
120+
}
121+
if payload.Data[0].Attributes.ProfileState != "ACTIVE" || payload.Data[1].Attributes.ProfileState != "INVALID" {
122+
t.Fatalf("unexpected profile states: %+v", payload.Data)
123+
}
124+
}
125+
126+
func TestProfilesListProfileStateFilter(t *testing.T) {
127+
setupAuth(t)
128+
t.Setenv("ASC_CONFIG_PATH", filepath.Join(t.TempDir(), "nonexistent.json"))
129+
130+
originalTransport := http.DefaultTransport
131+
t.Cleanup(func() {
132+
http.DefaultTransport = originalTransport
133+
})
134+
135+
http.DefaultTransport = roundTripFunc(func(req *http.Request) (*http.Response, error) {
136+
if req.Method != http.MethodGet || req.URL.Path != "/v1/profiles" {
137+
t.Fatalf("unexpected request: %s %s", req.Method, req.URL.String())
138+
}
139+
if got := req.URL.Query().Get("filter[profileState]"); got != "INVALID" {
140+
t.Fatalf("expected profileState filter INVALID, got %q", got)
141+
}
142+
body := `{"data":[{"type":"profiles","id":"profile-invalid","attributes":{"name":"Expired","profileType":"IOS_APP_ADHOC","profileState":"INVALID"}}]}`
143+
return &http.Response{
144+
StatusCode: http.StatusOK,
145+
Body: io.NopCloser(strings.NewReader(body)),
146+
Header: http.Header{"Content-Type": []string{"application/json"}},
147+
}, nil
148+
})
149+
150+
root := RootCommand("1.2.3")
151+
root.FlagSet.SetOutput(io.Discard)
152+
153+
var runErr error
154+
stdout, stderr := captureOutput(t, func() {
155+
if err := root.Parse([]string{"profiles", "list", "--profile-state", "invalid", "--output", "json"}); err != nil {
156+
t.Fatalf("parse error: %v", err)
157+
}
158+
runErr = root.Run(context.Background())
159+
})
160+
161+
if runErr != nil {
162+
t.Fatalf("run error: %v", runErr)
163+
}
164+
if stderr != "" {
165+
t.Fatalf("expected empty stderr, got %q", stderr)
166+
}
167+
168+
var payload struct {
169+
Data []struct {
170+
ID string `json:"id"`
171+
Attributes struct {
172+
ProfileState string `json:"profileState"`
173+
} `json:"attributes"`
174+
} `json:"data"`
175+
}
176+
if err := json.Unmarshal([]byte(stdout), &payload); err != nil {
177+
t.Fatalf("unmarshal profiles output: %v\n%s", err, stdout)
178+
}
179+
if len(payload.Data) != 1 || payload.Data[0].ID != "profile-invalid" || payload.Data[0].Attributes.ProfileState != "INVALID" {
180+
t.Fatalf("unexpected profiles output: %+v", payload.Data)
181+
}
182+
}
183+
184+
func TestProfilesListProfileStateInvalidValueReturnsUsageError(t *testing.T) {
185+
originalTransport := http.DefaultTransport
186+
t.Cleanup(func() {
187+
http.DefaultTransport = originalTransport
188+
})
189+
190+
requestCount := 0
191+
http.DefaultTransport = roundTripFunc(func(req *http.Request) (*http.Response, error) {
192+
requestCount++
193+
t.Fatalf("unexpected HTTP request for invalid profile-state: %s %s", req.Method, req.URL.String())
194+
return nil, nil
195+
})
196+
197+
root := RootCommand("1.2.3")
198+
root.FlagSet.SetOutput(io.Discard)
199+
200+
var runErr error
201+
stdout, stderr := captureOutput(t, func() {
202+
if err := root.Parse([]string{"profiles", "list", "--profile-state", "EXPIRED"}); err != nil {
203+
t.Fatalf("parse error: %v", err)
204+
}
205+
runErr = root.Run(context.Background())
206+
})
207+
208+
if !errors.Is(runErr, flag.ErrHelp) {
209+
t.Fatalf("expected flag.ErrHelp usage error, got %v", runErr)
210+
}
211+
if stdout != "" {
212+
t.Fatalf("expected empty stdout, got %q", stdout)
213+
}
214+
if !strings.Contains(stderr, "--profile-state must be one of: ACTIVE, INVALID") {
215+
t.Fatalf("expected profile-state usage error, got %q", stderr)
216+
}
217+
if requestCount != 0 {
218+
t.Fatalf("expected 0 requests, got %d", requestCount)
219+
}
220+
}
221+
59222
func TestProfilesListPaginateFromNext(t *testing.T) {
60223
setupAuth(t)
61224
t.Setenv("ASC_CONFIG_PATH", filepath.Join(t.TempDir(), "nonexistent.json"))

internal/cli/profiles/profiles.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ func ProfilesListCommand() *ffcli.Command {
5959
fs := flag.NewFlagSet("list", flag.ExitOnError)
6060

6161
profileType := fs.String("profile-type", "", "Filter by profile type(s), comma-separated")
62+
profileState := fs.String("profile-state", "", "Filter by profile state(s): ACTIVE, INVALID (default: ACTIVE,INVALID)")
6263
limit := fs.Int("limit", 0, "Maximum results per page (1-200)")
6364
next := fs.String("next", "", "Fetch next page using a links.next URL")
6465
paginate := fs.Bool("paginate", false, "Automatically fetch all pages (aggregate results)")
@@ -73,6 +74,7 @@ func ProfilesListCommand() *ffcli.Command {
7374
Examples:
7475
asc profiles list
7576
asc profiles list --profile-type IOS_APP_DEVELOPMENT
77+
asc profiles list --profile-state INVALID
7678
asc profiles list --paginate`,
7779
FlagSet: fs,
7880
UsageFunc: shared.DefaultUsageFunc,
@@ -85,6 +87,10 @@ Examples:
8587
}
8688

8789
profileTypes := shared.SplitCSVUpper(*profileType)
90+
profileStates, err := normalizeProfileStates(*profileState)
91+
if err != nil {
92+
return shared.UsageError(err.Error())
93+
}
8894

8995
client, err := shared.GetASCClient()
9096
if err != nil {
@@ -97,6 +103,7 @@ Examples:
97103
opts := []asc.ProfilesOption{
98104
asc.WithProfilesLimit(*limit),
99105
asc.WithProfilesNextURL(*next),
106+
asc.WithProfilesStates(profileStates),
100107
}
101108
if len(profileTypes) > 0 {
102109
opts = append(opts, asc.WithProfilesTypes(profileTypes))
@@ -402,3 +409,28 @@ func normalizeProfileInclude(value string) ([]string, error) {
402409
func profileIncludeList() []string {
403410
return []string{"bundleId", "certificates", "devices"}
404411
}
412+
413+
func normalizeProfileStates(value string) ([]string, error) {
414+
states := shared.SplitCSVUpper(value)
415+
if len(states) == 0 {
416+
return defaultProfileStates(), nil
417+
}
418+
allowed := map[string]struct{}{}
419+
for _, item := range profileStateList() {
420+
allowed[item] = struct{}{}
421+
}
422+
for _, item := range states {
423+
if _, ok := allowed[item]; !ok {
424+
return nil, fmt.Errorf("--profile-state must be one of: %s", strings.Join(profileStateList(), ", "))
425+
}
426+
}
427+
return states, nil
428+
}
429+
430+
func defaultProfileStates() []string {
431+
return []string{string(asc.ProfileStateActive), string(asc.ProfileStateInvalid)}
432+
}
433+
434+
func profileStateList() []string {
435+
return []string{string(asc.ProfileStateActive), string(asc.ProfileStateInvalid)}
436+
}

0 commit comments

Comments
 (0)