Skip to content
Open
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
147 changes: 147 additions & 0 deletions src/api-client/entity-summary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import {
summarizeUserDetail,
summarizeSegment,
summarizeSegmentRow,
summarizeTagRow,
summarizeMetricCategoryRow,
summarizeNamedEntityRow,
summarizeWebhookRow,
} from './entity-summary.js';

describe('applyShowExclude', () => {
Expand Down Expand Up @@ -190,3 +194,146 @@ describe('summarizeSegmentRow', () => {
expect(result).toHaveProperty('attribute', 'device');
});
});

describe('summarizeGoal created_by field', () => {
it('returns full name when first and last are present', () => {
const result = summarizeGoal({
id: 1,
name: 'g',
created_by: { first_name: 'Joe', last_name: 'Bloggs' },
});
expect(result.created_by).toBe('Joe Bloggs');
});

it('returns email when name parts are missing', () => {
const result = summarizeGoal({
id: 1,
name: 'g',
created_by: { email: 'joe@example.com' },
});
expect(result.created_by).toBe('joe@example.com');
});

it('returns empty string for null created_by', () => {
const result = summarizeGoal({ id: 1, name: 'g', created_by: null });
expect(result.created_by).toBe('');
});
});

describe('summarizeTagRow', () => {
it('curates the simple tag columns and summarizes user fields', () => {
expect(
summarizeTagRow({
id: 7,
tag: 'top-priority',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: { first_name: 'Joe', last_name: 'Bloggs' },
updated_at: null,
updated_by: null,
})
).toEqual({
id: 7,
tag: 'top-priority',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: 'Joe Bloggs',
updated_at: '',
updated_by: '',
});
});

it('defaults archived to false when missing', () => {
expect(summarizeTagRow({ id: 1, tag: 't' }).archived).toBe(false);
});
});

describe('summarizeMetricCategoryRow', () => {
it('picks the useful columns and summarizes user fields', () => {
expect(
summarizeMetricCategoryRow({
id: 3,
name: 'Revenue',
description: 'all revenue metrics',
color: '#ff5733',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: { first_name: 'Joe', last_name: 'Bloggs' },
updated_at: null,
updated_by: null,
})
).toEqual({
id: 3,
name: 'Revenue',
description: 'all revenue metrics',
color: '#ff5733',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: 'Joe Bloggs',
updated_at: '',
updated_by: '',
});
});
});

describe('summarizeNamedEntityRow', () => {
it('curates id/name/description/archived plus user/time audit fields', () => {
expect(
summarizeNamedEntityRow({
id: 4,
name: 'production',
description: 'production env',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: { first_name: 'Joe', last_name: 'Bloggs' },
updated_at: '2024-05-23T10:29:34.375Z',
updated_by: { email: 'admin@x' },
})
).toEqual({
id: 4,
name: 'production',
description: 'production env',
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: 'Joe Bloggs',
updated_at: '2024-05-23T10:29:34.375Z',
updated_by: 'admin@x',
});
});

it('keeps description empty if missing', () => {
expect(summarizeNamedEntityRow({ id: 1, name: 'x' }).description).toBe('');
});
});

describe('summarizeWebhookRow', () => {
it('curates webhook-specific columns and summarizes user fields', () => {
expect(
summarizeWebhookRow({
id: 2,
name: 'metrics-sync',
url: 'https://example.com/hooks/metrics',
enabled: true,
ordered: false,
max_retries: 5,
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: { first_name: 'Joe', last_name: 'Bloggs' },
updated_at: null,
updated_by: null,
})
).toEqual({
id: 2,
name: 'metrics-sync',
url: 'https://example.com/hooks/metrics',
enabled: true,
ordered: false,
max_retries: 5,
archived: false,
created_at: '2024-05-22T10:29:34.375Z',
created_by: 'Joe Bloggs',
updated_at: '',
updated_by: '',
});
});
});
60 changes: 57 additions & 3 deletions src/api-client/entity-summary.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { formatDate } from './format-helpers.js';
import { formatUserSummary } from '../lib/output/formatter.js';

export function applyShowExclude(
summary: Record<string, unknown>,
Expand All @@ -19,9 +20,7 @@ export function applyShowExclude(

function formatOwner(obj: Record<string, unknown> | undefined): string {
if (!obj) return '';
const first = (obj.first_name as string) ?? '';
const last = (obj.last_name as string) ?? '';
return [first, last].filter(Boolean).join(' ') || (obj.email as string) || '';
return formatUserSummary(obj) ?? '';
}

export function summarizeMetric(m: Record<string, unknown>): Record<string, unknown> {
Expand Down Expand Up @@ -204,3 +203,58 @@ export function summarizeCustomField(f: Record<string, unknown>): Record<string,
created_at: formatDate(f.created_at),
};
}

export function summarizeTagRow(t: Record<string, unknown>): Record<string, unknown> {
return {
id: t.id,
tag: t.tag ?? '',
archived: t.archived ?? false,
created_at: t.created_at ?? '',
created_by: formatOwner(t.created_by as Record<string, unknown> | undefined),
updated_at: t.updated_at ?? '',
updated_by: formatOwner(t.updated_by as Record<string, unknown> | undefined),
};
}

export function summarizeMetricCategoryRow(c: Record<string, unknown>): Record<string, unknown> {
return {
id: c.id,
name: c.name ?? '',
description: c.description ?? '',
color: c.color ?? '',
archived: c.archived ?? false,
created_at: c.created_at ?? '',
created_by: formatOwner(c.created_by as Record<string, unknown> | undefined),
updated_at: c.updated_at ?? '',
updated_by: formatOwner(c.updated_by as Record<string, unknown> | undefined),
};
}

export function summarizeNamedEntityRow(e: Record<string, unknown>): Record<string, unknown> {
return {
id: e.id,
name: e.name ?? '',
description: e.description ?? '',
archived: e.archived ?? false,
created_at: e.created_at ?? '',
created_by: formatOwner(e.created_by as Record<string, unknown> | undefined),
updated_at: e.updated_at ?? '',
updated_by: formatOwner(e.updated_by as Record<string, unknown> | undefined),
};
}

export function summarizeWebhookRow(w: Record<string, unknown>): Record<string, unknown> {
return {
id: w.id,
name: w.name ?? '',
url: w.url ?? '',
enabled: w.enabled ?? false,
ordered: w.ordered ?? false,
max_retries: w.max_retries ?? 0,
archived: w.archived ?? false,
created_at: w.created_at ?? '',
created_by: formatOwner(w.created_by as Record<string, unknown> | undefined),
updated_at: w.updated_at ?? '',
updated_by: formatOwner(w.updated_by as Record<string, unknown> | undefined),
};
}
48 changes: 47 additions & 1 deletion src/commands/apikeys/apikeys.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { apiKeysCommand } from './index.js';
import { apiKeysCommand, summarizeApiKeyRow } from './index.js';
import {
getAPIClientFromOptions,
getGlobalOptions,
Expand Down Expand Up @@ -105,3 +105,49 @@ describe('apikeys command', () => {
expect(mockClient.deleteApiKey).toHaveBeenCalledWith(5);
});
});

describe('summarizeApiKeyRow', () => {
it('picks curated columns and flattens created_by / updated_by', () => {
const raw = {
id: 36,
name: 'test new key',
description: 'new key',
hashed_key: 'X99upHMkKKM2XUpRuZ53YWCDx5G93SgB043EXx3LG5k=',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace the secret-like test literal with a clearly fake placeholder.

This value is patterned like a real credential and can trigger leak scanners in CI/history. Use an obviously synthetic constant instead.

Suggested change
-      hashed_key: 'X99upHMkKKM2XUpRuZ53YWCDx5G93SgB043EXx3LG5k=',
+      hashed_key: 'TEST_HASHED_KEY_PLACEHOLDER',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
hashed_key: 'X99upHMkKKM2XUpRuZ53YWCDx5G93SgB043EXx3LG5k=',
hashed_key: 'TEST_HASHED_KEY_PLACEHOLDER',
🧰 Tools
🪛 Betterleaks (1.2.0)

[high] 115-115: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/apikeys/apikeys.test.ts` at line 115, The test contains a
secret-like literal assigned to hashed_key which can trigger leak scanners;
replace the real-looking value in the test (the hashed_key string in
src/commands/apikeys/apikeys.test.ts) with an obviously synthetic placeholder
(e.g., "FAKE_HASHED_KEY_PLACEHOLDER" or similar) so it no longer resembles a
real credential; update any assertions or fixtures that reference this exact
value to use the new placeholder constant to keep tests consistent.

key_ending: 'ouNy',
permissions: '',
used_at: null,
created_at: '2024-05-22T10:29:34.375Z',
updated_at: null,
created_by: { id: 4, first_name: 'Márcio', last_name: 'Martins', email: 'm@x' },
updated_by: null,
};
expect(summarizeApiKeyRow(raw)).toEqual({
id: 36,
name: 'test new key',
description: 'new key',
key_ending: 'ouNy',
permissions: '',
used_at: '',
created_at: '2024-05-22T10:29:34.375Z',
created_by: 'Márcio Martins',
updated_at: '',
updated_by: '',
});
});

it('returns empty strings for missing optional fields', () => {
const raw = { id: 1, name: 'k', key_ending: 'xxxx' };
expect(summarizeApiKeyRow(raw)).toEqual({
id: 1,
name: 'k',
description: '',
key_ending: 'xxxx',
permissions: '',
used_at: '',
created_at: '',
created_by: '',
updated_at: '',
updated_by: '',
});
});
});
18 changes: 18 additions & 0 deletions src/commands/apikeys/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,30 @@ import {
} from '../../lib/utils/api-helper.js';
import { parseApiKeyId } from '../../lib/utils/validators.js';
import { createListCommand } from '../../lib/utils/list-command.js';
import { formatUserSummary } from '../../lib/output/formatter.js';
import type { ApiKeyId } from '../../lib/api/branded-types.js';
import { getApiKey, createApiKey, updateApiKey, deleteApiKey } from '../../core/apikeys/index.js';

export const apiKeysCommand = new Command('api-keys')
.aliases(['apikeys', 'apikey', 'api-key'])
.description('API key commands');

export function summarizeApiKeyRow(item: Record<string, unknown>): Record<string, unknown> {
const summarizeUser = (v: unknown) => formatUserSummary(v) ?? '';
return {
id: item.id,
name: item.name ?? '',
description: item.description ?? '',
key_ending: item.key_ending ?? '',
permissions: item.permissions ?? '',
used_at: item.used_at ?? '',
created_at: item.created_at ?? '',
created_by: summarizeUser(item.created_by),
updated_at: item.updated_at ?? '',
updated_by: summarizeUser(item.updated_by),
};
}

const listCommand = createListCommand({
description: 'List all API keys',
fetch: (client, options) =>
Expand All @@ -28,6 +45,7 @@ const listCommand = createListCommand({
archived: options.archived as boolean,
ids: options.ids as string | undefined,
}),
summarizeRow: summarizeApiKeyRow,
});

const getCommand = new Command('get')
Expand Down
5 changes: 4 additions & 1 deletion src/commands/apps/apps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ describe('apps command', () => {
await appsCommand.parseAsync(['node', 'test', 'list']);

expect(mockClient.listApplications).toHaveBeenCalled();
expect(printFormatted).toHaveBeenCalledWith([{ id: 1, name: 'web' }], expect.anything());
expect(printFormatted).toHaveBeenCalledWith(
[expect.objectContaining({ id: 1, name: 'web' })],
expect.anything()
);
});

it('should get application by id', async () => {
Expand Down
11 changes: 2 additions & 9 deletions src/commands/apps/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '../../lib/utils/api-helper.js';
import { parseApplicationId } from '../../lib/utils/validators.js';
import { createListCommand } from '../../lib/utils/list-command.js';
import { formatUserSummary } from '../../lib/output/formatter.js';
import type { ApplicationId } from '../../lib/api/branded-types.js';
import { getApp, createApp, updateApp, archiveApp } from '../../core/apps/index.js';

Expand Down Expand Up @@ -35,15 +36,7 @@ const listCommand = createListCommand({
description: item.description,
archived: item.archived,
created_at: item.created_at,
created_by:
item.created_by && typeof item.created_by === 'object'
? [
(item.created_by as Record<string, unknown>).first_name ?? '',
(item.created_by as Record<string, unknown>).last_name ?? '',
]
.filter(Boolean)
.join(' ')
: item.created_by,
created_by: formatUserSummary(item.created_by) ?? '',
}),
});

Expand Down
16 changes: 15 additions & 1 deletion src/commands/envs/envs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,21 @@ describe('envs command', () => {
await envsCommand.parseAsync(['node', 'test', 'list']);

expect(mockClient.listEnvironments).toHaveBeenCalled();
expect(printFormatted).toHaveBeenCalledWith([{ id: 1, name: 'production' }], expect.anything());
expect(printFormatted).toHaveBeenCalledWith(
[
{
id: 1,
name: 'production',
description: '',
archived: false,
created_at: '',
created_by: '',
updated_at: '',
updated_by: '',
},
],
expect.anything()
);
});

it('should get environment by id', async () => {
Expand Down
Loading
Loading