-
Notifications
You must be signed in to change notification settings - Fork 0
Review dp function #62
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
078dcb2
feat: add Brevo Function commands with smoke tests
satyamdev10 6322f3c
feat: gate Brevo Function behind __BREVO_PREVIEW__ and add fn alias
satyamdev10 255350a
feat: add brevo function activate, deactivate and delete commands
satyamdev10 0500fe4
Merge pull request #60 from getbrevo/features_fn-manage-commands
satyamdev10 a88d524
Merge branch 'features_set-dp-function' into review_dp_function
satyamdev10 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| /* | ||
| * Brevo Function commands: list -> list --draft -> get -> get (404). | ||
| * | ||
| * Read-only — no apps are created or deleted. The suite validates response shape | ||
| * and error handling, not data content (the account may have zero functions). | ||
| */ | ||
|
|
||
| import { | ||
| State, | ||
| Suite, | ||
| brevoCmd, | ||
| exec, | ||
| execOrThrow, | ||
| must, | ||
| parseJson, | ||
| } from './core'; | ||
|
|
||
| function stepFunctionList(state: State): string { | ||
| const r = execOrThrow(brevoCmd(state), ['function', 'list', '--json'], state); | ||
| const parsed = parseJson<Record<string, unknown>>(r.stdout); | ||
|
|
||
| // The response must carry the list shape regardless of how many functions exist. | ||
| must( | ||
| Array.isArray(parsed.functions), | ||
| `function list --json: "functions" is not an array: ${JSON.stringify(parsed).slice(0, 200)}`, | ||
| ); | ||
| must( | ||
| typeof parsed.total === 'number', | ||
| `function list --json: "total" is not a number: ${JSON.stringify(parsed).slice(0, 200)}`, | ||
| ); | ||
| must( | ||
| typeof parsed.max === 'number', | ||
| `function list --json: "max" is not a number: ${JSON.stringify(parsed).slice(0, 200)}`, | ||
| ); | ||
|
|
||
| const functions = parsed.functions as Array<Record<string, unknown>>; | ||
|
|
||
| // If there are functions, spot-check the first one's shape. | ||
| const first = functions[0]; | ||
| if (first) { | ||
| must(typeof first.id === 'string', 'first function missing "id"'); | ||
| must(typeof first.name === 'string', 'first function missing "name"'); | ||
| must(typeof first.formula === 'string', 'first function missing "formula"'); | ||
| } | ||
|
|
||
| // Store the first function ID for the get step. | ||
| state._functionId = first ? String(first.id) : null; | ||
|
|
||
| return `${functions.length} function(s), total ${parsed.total} / max ${parsed.max}`; | ||
| } | ||
|
|
||
| function stepFunctionListDraft(state: State): string { | ||
| const r = execOrThrow(brevoCmd(state), ['function', 'list', '--draft', '--json'], state); | ||
| const parsed = parseJson<Record<string, unknown>>(r.stdout); | ||
|
|
||
| must( | ||
| Array.isArray(parsed.drafts), | ||
| `function list --draft --json: "drafts" is not an array: ${JSON.stringify(parsed).slice(0, 200)}`, | ||
| ); | ||
| must( | ||
| typeof parsed.total === 'number', | ||
| `function list --draft --json: "total" is not a number`, | ||
| ); | ||
|
|
||
| const drafts = parsed.drafts as Array<Record<string, unknown>>; | ||
|
|
||
| const firstDraft = drafts[0]; | ||
| if (firstDraft) { | ||
| must(typeof firstDraft.id === 'string', 'first draft missing "id"'); | ||
| must(typeof firstDraft.formula === 'string', 'first draft missing "formula"'); | ||
| } | ||
|
|
||
| return `${drafts.length} draft(s), total ${parsed.total}`; | ||
| } | ||
|
|
||
| function stepFunctionGet(state: State): string { | ||
| const id = state._functionId; | ||
| if (!id) { | ||
| return 'skipped — no functions on this account to get'; | ||
| } | ||
|
|
||
| const r = execOrThrow(brevoCmd(state), ['function', 'get', id, '--json'], state); | ||
| const parsed = parseJson<Record<string, unknown>>(r.stdout); | ||
|
|
||
| must(parsed.id === id, `function get: returned id "${parsed.id}", expected "${id}"`); | ||
| must(typeof parsed.name === 'string', 'function get: missing "name"'); | ||
| must(typeof parsed.formula === 'string', 'function get: missing "formula"'); | ||
| must(typeof parsed.version === 'number', 'function get: missing "version"'); | ||
| must(typeof parsed.is_active === 'boolean', 'function get: missing "is_active"'); | ||
|
|
||
| return `got function "${parsed.name}" (${id})`; | ||
| } | ||
|
|
||
| function stepFunctionGetNotFound(state: State): string { | ||
| // A nonexistent ID should exit 0 with a JSON error body — the command handles | ||
| // 404 gracefully rather than throwing. | ||
| const fakeId = 'brevo-cli-smoke-nonexistent-fn'; | ||
| const r = exec(brevoCmd(state), ['function', 'get', fakeId, '--json'], state); | ||
|
|
||
| must( | ||
| r.exitCode === 0, | ||
| `function get (404) exited ${r.exitCode}, expected 0: ${(r.stderr || r.stdout).slice(0, 200)}`, | ||
| ); | ||
|
|
||
| const parsed = parseJson<Record<string, unknown>>(r.stdout); | ||
| must( | ||
| parsed.error === 'not_found', | ||
| `function get (404): expected error "not_found", got ${JSON.stringify(parsed.error)}`, | ||
| ); | ||
|
|
||
| return `404 handled gracefully for "${fakeId}"`; | ||
| } | ||
|
|
||
| // Extend State with a temporary field for the function ID found during list. | ||
| // This avoids changing the shared State interface — the field is set and read | ||
| // only within this suite. | ||
| declare module './core' { | ||
| interface State { | ||
| _functionId?: string | null; | ||
| } | ||
| } | ||
|
|
||
| export const functionSuite: Suite = { | ||
| name: 'function', | ||
| description: 'Brevo Function commands — list, list --draft, get, get (404)', | ||
| steps: [ | ||
| ['Function list', stepFunctionList], | ||
| ['Function list (draft)', stepFunctionListDraft], | ||
| ['Function get', stepFunctionGet], | ||
| ['Function get (not found)', stepFunctionGetNotFound], | ||
| ], | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { activateFunctionCommand } from '../../../commands/function/activate'; | ||
| import { ApiError } from '../../../lib/errors'; | ||
|
|
||
| jest.mock('../../../container', () => ({ | ||
| functionService: { | ||
| activateFunction: jest.fn(), | ||
| }, | ||
| })); | ||
|
|
||
| import { functionService } from '../../../container'; | ||
|
|
||
| describe('function/activate', () => { | ||
| let stdoutSpy: jest.SpyInstance; | ||
|
|
||
| beforeEach(() => { | ||
| stdoutSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true); | ||
| jest.clearAllMocks(); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| stdoutSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it('should show success message on activation', async () => { | ||
| (functionService.activateFunction as jest.Mock).mockResolvedValue(undefined); | ||
|
|
||
| await activateFunctionCommand({ id: 'fn-001', json: false }); | ||
|
|
||
| expect(functionService.activateFunction).toHaveBeenCalledWith('fn-001'); | ||
| const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); | ||
| expect(output).toContain('Brevo Function "fn-001" activated'); | ||
| }); | ||
|
|
||
| it('should output JSON on success with --json', async () => { | ||
| (functionService.activateFunction as jest.Mock).mockResolvedValue(undefined); | ||
|
|
||
| await activateFunctionCommand({ id: 'fn-001', json: true }); | ||
|
|
||
| const output = stdoutSpy.mock.calls[0][0]; | ||
| const parsed = JSON.parse(output); | ||
| expect(parsed.activated).toBe(true); | ||
| expect(parsed.id).toBe('fn-001'); | ||
| }); | ||
|
|
||
| it('should show not-found message on 404', async () => { | ||
| (functionService.activateFunction as jest.Mock).mockRejectedValue( | ||
| new ApiError('Not found', 404), | ||
| ); | ||
|
|
||
| await activateFunctionCommand({ id: 'fn-999', json: false }); | ||
|
|
||
| const output = stdoutSpy.mock.calls.map((c: [string]) => c[0]).join(''); | ||
| expect(output).toContain('Brevo Function "fn-999" not found'); | ||
| }); | ||
|
|
||
| it('should output JSON error on 404 with --json', async () => { | ||
| (functionService.activateFunction as jest.Mock).mockRejectedValue( | ||
| new ApiError('Not found', 404), | ||
| ); | ||
|
|
||
| await activateFunctionCommand({ id: 'fn-999', json: true }); | ||
|
|
||
| const output = stdoutSpy.mock.calls[0][0]; | ||
| const parsed = JSON.parse(output); | ||
| expect(parsed.error).toBe('not_found'); | ||
| expect(parsed.message).toContain('fn-999'); | ||
| }); | ||
|
|
||
| it('should propagate non-404 API errors', async () => { | ||
| (functionService.activateFunction as jest.Mock).mockRejectedValue( | ||
| new ApiError('Server error', 500), | ||
| ); | ||
|
|
||
| await expect(activateFunctionCommand({ id: 'fn-001', json: false })).rejects.toThrow( | ||
| 'Server error', | ||
| ); | ||
| }); | ||
|
|
||
| it('should propagate generic errors', async () => { | ||
| (functionService.activateFunction as jest.Mock).mockRejectedValue(new Error('Network error')); | ||
|
|
||
| await expect(activateFunctionCommand({ id: 'fn-001', json: false })).rejects.toThrow( | ||
| 'Network error', | ||
| ); | ||
| }); | ||
| }); |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.