Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions packages/cli/src/commands/ci/fetch-default-org-slug.mts
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,17 @@ export async function getDefaultOrgSlug(): Promise<CResult<string>> {
}

const { organizations } = orgsCResult.data
const keys = Object.keys(organizations)
if (!keys.length) {
if (!organizations.length) {
return {
ok: false,
message: 'Failed to establish identity',
data: 'No organization associated with the Socket API token. Unable to continue.',
}
}

const [firstKey] = keys
const slug = firstKey
? ((organizations as any)[firstKey]?.name ?? undefined)
: undefined
// Use `.slug` (URL-safe) — `.name` is the display label and may
// contain spaces ("Example Org Ltd") that break API URLs.
const slug = organizations[0]?.slug
if (!slug) {
return {
ok: false,
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/src/commands/scan/suggest-org-slug.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ export async function suggestOrgSlug(): Promise<string | undefined> {
return undefined
}

// Ignore a failed request here. It was not the primary goal of
// running this command and reporting it only leads to end-user confusion.
const { organizations } = orgsCResult.data
const proceed = await select({
message:
'Missing org name; do you want to use any of these orgs for this scan?',
choices: [
...organizations.map(o => {
const name = o.name ?? o.slug
// Display the human-readable name but route with the slug —
// display names may contain spaces that break API URLs.
const display = o.name ?? o.slug
return {
name: `Yes [${name}]`,
value: name,
description: `Use "${name}" as the organization`,
name: `Yes [${display}]`,
value: o.slug,
description: `Use "${display}" as the organization`,
}
}),
{
Expand Down
47 changes: 34 additions & 13 deletions packages/cli/test/unit/commands/ci/fetch-default-org-slug.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -97,13 +97,15 @@ describe('getDefaultOrgSlug', () => {
mockFetchFn.mockResolvedValue({
ok: true,
data: {
organizations: {
'org-1': {
// fetchOrganization converts the SDK dict into an array of org
// objects before returning, so mock the array shape directly.
organizations: [
{
id: 'org-1',
name: 'Test Organization',
slug: 'test-org',
},
},
],
},
})

Expand All @@ -112,7 +114,31 @@ describe('getDefaultOrgSlug', () => {
expect(result).toEqual({
ok: true,
message: 'Retrieved default org from server',
data: 'Test Organization',
data: 'test-org',
})
})

it('returns slug (not display name) for orgs with spaces', async () => {
// Regression guard: orgs whose display name has spaces produce
// URLs like `/v0/orgs/Example%20Org%20Ltd/...` that 404.
mockFn.mockReturnValue(undefined)
mockOrgSlug.value = undefined

mockFetchFn.mockResolvedValue({
ok: true,
data: {
organizations: [
{ id: 'org-1', name: 'Example Org Ltd', slug: 'example-org-ltd' },
],
},
})

const result = await getDefaultOrgSlug()

expect(result).toEqual({
ok: true,
message: 'Retrieved default org from server',
data: 'example-org-ltd',
})
})

Expand All @@ -139,7 +165,7 @@ describe('getDefaultOrgSlug', () => {
mockFetchFn.mockResolvedValue({
ok: true,
data: {
organizations: {},
organizations: [],
},
})

Expand All @@ -152,20 +178,15 @@ describe('getDefaultOrgSlug', () => {
})
})

it('returns error when organization has no name', async () => {
it('returns error when organization has no slug', async () => {
mockFn.mockReturnValue(undefined)
mockOrgSlug.value = undefined

mockFetchFn.mockResolvedValue({
ok: true,
data: {
organizations: {
'org-1': {
id: 'org-1',
slug: 'org-slug',
// Missing name field.
},
},
// Missing slug — defensive check in case the API ever omits it.
organizations: [{ id: 'org-1', name: 'Test Org' }],
},
})

Expand Down
27 changes: 27 additions & 0 deletions packages/cli/test/unit/commands/scan/suggest-org-slug.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -136,5 +136,32 @@ describe('suggest-org-slug', () => {
expect(noChoice).toBeDefined()
expect(noChoice!.value).toBe('')
})

it('returns the slug (not display name) for orgs where they differ', async () => {
// Regression guard: passing the display name through to the API
// produced 404s for orgs with spaces, e.g.
// `/v0/orgs/Example%20Org%20Ltd/...` instead of
// `/v0/orgs/example-org-ltd/...`.
mockFetchOrganization.mockResolvedValue({
ok: true,
data: {
organizations: [
{ name: 'Example Org Ltd', slug: 'example-org-ltd' },
],
},
})
mockSelect.mockResolvedValue('example-org-ltd')

await suggestOrgSlug()

const callArg = mockSelect.mock.calls[0]![0] as {
choices: Array<{ name: string; value: string; description: string }>
}
// The choice value must be the slug. The visible label/description
// still use the friendlier display name.
expect(callArg.choices[0]!.value).toBe('example-org-ltd')
expect(callArg.choices[0]!.name).toContain('Example Org Ltd')
expect(callArg.choices[0]!.description).toContain('Example Org Ltd')
})
})
})