Skip to content

Commit

Permalink
feat: add new subStatus command (#248)
Browse files Browse the repository at this point in the history
  • Loading branch information
wKovacs64 authored Nov 9, 2023
1 parent 0594717 commit f75f0aa
Show file tree
Hide file tree
Showing 8 changed files with 166 additions and 1 deletion.
5 changes: 5 additions & 0 deletions .changeset/fresh-foxes-turn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'pwned': minor
---

Add new `subStatus` command to get the current subscription status of your HIBP API key. See https://haveibeenpwned.com/API/v3#SubscriptionStatus for more information.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Commands:
pwned pw <password> securely check a password for public exposure
pwned search <account|email> search breaches and pastes for an account (username or email
address)
pwned subStatus get your subscription status
Options:
-h, --help Show help [boolean]
Expand Down
2 changes: 2 additions & 0 deletions bin/pwned.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as dc from '../lib/commands/dc.js';
import * as pa from '../lib/commands/pa.js';
import * as pw from '../lib/commands/pw.js';
import * as search from '../lib/commands/search.js';
import * as subStatus from '../lib/commands/sub-status.js';
/* eslint-enable */

sourceMapSupport.install();
Expand All @@ -32,6 +33,7 @@ yargs(hideBin(process.argv))
.command(pa)
.command(pw)
.command(search)
.command(subStatus)
.demandCommand()
.recommendCommands()
.strict()
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ Commands:
pwned pw <password> securely check a password for public exposure
pwned search <account|email> search breaches and pastes for an account (username or email
address)
pwned subStatus get your subscription status
Options:
-h, --help Show help [boolean]
Expand Down
75 changes: 75 additions & 0 deletions src/commands/__tests__/sub-status.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { vi, type SpyInstance } from 'vitest';
import { http } from 'msw';
import { server } from '../../../test/server.js';
import { spinnerFns, loggerFns, ERROR_MSG } from '../../../test/fixtures.js';
import { logger as mockLogger, type Logger } from '../../utils/logger.js';
import { spinner as mockSpinner } from '../../utils/spinner.js';
import { handler as subStatus } from '../sub-status.js';

vi.mock('../../utils/logger');
vi.mock('../../utils/spinner');

const logger = mockLogger as Logger & Record<string, SpyInstance>;
const spinner = mockSpinner as typeof mockSpinner & Record<string, SpyInstance>;

describe('command: subStatus', () => {
describe('normal output (default)', () => {
it('calls spinner.start', async () => {
expect(spinner.start).toHaveBeenCalledTimes(0);
await subStatus({ raw: false });
expect(spinner.start).toHaveBeenCalledTimes(1);
});

it('with data: calls spinner.stop and logger.log', async () => {
expect(spinner.stop).toHaveBeenCalledTimes(0);
expect(logger.log).toHaveBeenCalledTimes(0);
await subStatus({ raw: false });
expect(spinner.stop).toHaveBeenCalledTimes(1);
expect(logger.log).toHaveBeenCalledTimes(1);
});

it('on error: only calls spinner.fail', async () => {
server.use(
http.get('*', () => {
throw new Error(ERROR_MSG);
}),
);

expect(spinner.fail).toHaveBeenCalledTimes(0);
loggerFns.forEach((fn) => expect(logger[fn]).toHaveBeenCalledTimes(0));
await subStatus({ raw: false });
expect(spinner.fail).toHaveBeenCalledTimes(1);
loggerFns.forEach((fn) => expect(logger[fn]).toHaveBeenCalledTimes(0));
});
});

describe('raw mode', () => {
it('does not call spinner.start', async () => {
expect(spinner.start).toHaveBeenCalledTimes(0);
await subStatus({ raw: true });
expect(spinner.start).toHaveBeenCalledTimes(0);
});

it('with data: only calls logger.log', async () => {
spinnerFns.forEach((fn) => expect(spinner[fn]).toHaveBeenCalledTimes(0));
expect(logger.log).toHaveBeenCalledTimes(0);
await subStatus({ raw: true });
spinnerFns.forEach((fn) => expect(spinner[fn]).toHaveBeenCalledTimes(0));
expect(logger.log).toHaveBeenCalledTimes(1);
});

it('on error: only calls logger.error', async () => {
server.use(
http.get('*', () => {
throw new Error(ERROR_MSG);
}),
);

spinnerFns.forEach((fn) => expect(spinner[fn]).toHaveBeenCalledTimes(0));
expect(logger.error).toHaveBeenCalledTimes(0);
await subStatus({ raw: true });
spinnerFns.forEach((fn) => expect(spinner[fn]).toHaveBeenCalledTimes(0));
expect(logger.error).toHaveBeenCalledTimes(1);
});
});
});
69 changes: 69 additions & 0 deletions src/commands/sub-status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { Argv } from 'yargs';
import { subscriptionStatus } from 'hibp';
import prettyjson from 'prettyjson';
import { config } from '../config.js';
import { logger } from '../utils/logger.js';
import { spinner } from '../utils/spinner.js';
import { userAgent } from '../utils/user-agent.js';

export const command = 'subStatus';
export const describe = 'get your subscription status';

interface SubStatusArgvOptions {
r?: boolean;
}

interface SubStatusHandlerOptions {
raw?: boolean;
}

/* c8 ignore start */
export function builder(
yargs: Argv<SubStatusArgvOptions>,
): Argv<SubStatusHandlerOptions> {
return yargs
.option('r', {
describe: 'output the raw JSON data',
type: 'boolean',
default: false,
})
.alias('r', 'raw')
.group(['r'], 'Command Options:')
.group(['h', 'v'], 'Global Options:');
}
/* c8 ignore stop */

/**
* Fetches and outputs your subscription status (of your API key).
*
* @param {object} argv the parsed argv object
* @param {boolean} [argv.raw] output the raw JSON data (default: false)
* @returns {Promise<void>} the resulting Promise where output is rendered
*/
export async function handler({ raw }: SubStatusHandlerOptions): Promise<void> {
if (!raw) {
spinner.start();
}

try {
const subStatusData = await subscriptionStatus({
apiKey: config.get('apiKey'),
userAgent,
});
if (raw) {
logger.log(JSON.stringify(subStatusData));
} else {
spinner.stop();
logger.log(prettyjson.render(subStatusData));
}
} catch (err: unknown) {
/* c8 ignore else */
if (err instanceof Error) {
if (!raw) {
spinner.fail(err.message);
} else {
logger.error(err.message);
}
}
}
}
10 changes: 9 additions & 1 deletion test/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { stripIndents } from 'common-tags';
import type { Breach, Paste } from 'hibp';
import type { Breach, Paste, SubscriptionStatus } from 'hibp';

export const spinnerFns = ['start', 'stop', 'succeed', 'warn', 'fail'];
export const loggerFns = ['info', 'log', 'warn', 'error'];
Expand Down Expand Up @@ -40,6 +40,14 @@ export const PASSWORD_HASHES = stripIndents`
01330C689E5D64F660D6947A93AD634EF8F:1
`;

export const SUBSCRIPTION_STATUS: SubscriptionStatus = {
SubscriptionName: 'Pwned 42',
Description: 'A mock subscrpition',
SubscribedUntil: '2023-12-31T01:23:45',
Rpm: 69,
DomainSearchMaxBreachedAccounts: 0,
};

export const EMAIL = '[email protected]';
export const EMPTY_ARRAY = [];
export const ERROR = 'foo';
Expand Down
4 changes: 4 additions & 0 deletions test/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DATA_CLASSES,
PASTES,
PASSWORD_HASHES,
SUBSCRIPTION_STATUS,
EMPTY_ARRAY,
} from './fixtures.js';

Expand Down Expand Up @@ -45,4 +46,7 @@ export const handlers = [
http.get('*/range/:suffix', () => {
return new Response(PASSWORD_HASHES);
}),
http.get('*/subscription/status', () => {
return new Response(JSON.stringify(SUBSCRIPTION_STATUS));
}),
];

0 comments on commit f75f0aa

Please sign in to comment.