diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 2e79e5a..30dd616 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -1,30 +1,37 @@ name: Version Bump - on: workflow_dispatch: inputs: - version: - description: 'Version to bump to (major.minor.patch)' + bump_type: + description: 'Version bump type' required: true - type: string - + type: choice + options: + - patch + - minor + - major jobs: bump-version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: - node-version: '18.x' + node-version: '20.x' + - name: Configure Git run: | git config --global user.name 'GitHub Actions' git config --global user.email 'github-actions[bot]@users.noreply.github.com' - - name: Update version - run: npm version ${{ github.event.inputs.version }} -m "Bump version to %s" + + - name: Bump version + run: npm version ${{ github.event.inputs.bump_type }} -m "Bump version to %s" + - name: Push changes run: | git push diff --git a/README.md b/README.md index 454034e..a09bd6f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Usagey Node.js SDK [![Test](https://github.com/8thwanda/usagey-node-sdk/actions/workflows/test.yml/badge.svg)](https://github.com/8thwanda/usagey-node-sdk/actions/workflows/test.yml) -[![npm version](https://badge.fury.io/js/usagey.svg)](https://badge.fury.io/js/usagey) +[![npm version](https://img.shields.io/npm/v/usagey.svg)](https://www.npmjs.com/package/usagey) [![codecov](https://codecov.io/gh/8thwanda/usagey-node-sdk/branch/main/graph/badge.svg)](https://codecov.io/gh/8thwanda/usagey-node-sdk) The official Node.js SDK for [Usagey](https://usagey.com) - the complete toolkit for implementing usage-based pricing. diff --git a/src/_tests_/client.test.ts b/src/_tests_/client.test.ts index 9564172..7ccd62e 100644 --- a/src/_tests_/client.test.ts +++ b/src/_tests_/client.test.ts @@ -16,68 +16,172 @@ describe('UsageyClient', () => { nock.restore(); }); - describe('trackEvent', () => { - it('should successfully track an event', async () => { - const mockResponse = { - success: true, - event_id: 'evt_123', - timestamp: '2023-01-01T00:00:00Z' - }; + describe('constructor', () => { + it('should use provided baseUrl when options.baseUrl is provided', () => { + const customBaseUrl = 'https://custom.api.com'; + const customClient = new UsageyClient(API_KEY, { baseUrl: customBaseUrl }); + + // We can't directly access private properties, but we can test the behavior + expect(customClient).toBeInstanceOf(UsageyClient); + }); - nock(BASE_URL) - .post('/api/usage', { - event_type: 'api_call', - quantity: 1 - }) - .reply(200, mockResponse); + it('should use default baseUrl when options.baseUrl is not provided', () => { + const defaultClient = new UsageyClient(API_KEY); + expect(defaultClient).toBeInstanceOf(UsageyClient); + }); - const result = await client.trackEvent('api_call'); - expect(result).toEqual(mockResponse); + it('should use default baseUrl when options.baseUrl is empty string', () => { + const emptyBaseUrlClient = new UsageyClient(API_KEY, { baseUrl: '' }); + expect(emptyBaseUrlClient).toBeInstanceOf(UsageyClient); }); - it('should include quantity and metadata when provided', async () => { - const mockResponse = { - success: true, - event_id: 'evt_123', - timestamp: '2023-01-01T00:00:00Z' - }; + it('should use default baseUrl when options.baseUrl is null', () => { + const nullBaseUrlClient = new UsageyClient(API_KEY, { baseUrl: null as any }); + expect(nullBaseUrlClient).toBeInstanceOf(UsageyClient); + }); - const metadata = { endpoint: '/users', method: 'GET' }; + it('should use default baseUrl when options.baseUrl is undefined', () => { + const undefinedBaseUrlClient = new UsageyClient(API_KEY, { baseUrl: undefined }); + expect(undefinedBaseUrlClient).toBeInstanceOf(UsageyClient); + }); - nock(BASE_URL) - .post('/api/usage', { + it('should use default baseUrl when options is empty object', () => { + const emptyOptionsClient = new UsageyClient(API_KEY, {}); + expect(emptyOptionsClient).toBeInstanceOf(UsageyClient); + }); + }); + + describe('trackEvent', () => { + it('should successfully track an event with default quantity and no metadata', async () => { + const mockResponse = { + success: true, + event_id: 'evt_123', + timestamp: '2023-01-01T00:00:00Z' + }; + + nock(BASE_URL) + .post('/api/usage', (body) => { + expect(body).toEqual({ + event_type: 'api_call', + quantity: 1, + metadata: undefined + }); + return true; + }) + .reply(200, mockResponse); + + const result = await client.trackEvent('api_call'); + expect(result).toEqual(mockResponse); + }); + + it('should successfully track an event with explicit quantity and no metadata', async () => { + const mockResponse = { + success: true, + event_id: 'evt_123', + timestamp: '2023-01-01T00:00:00Z' + }; + + nock(BASE_URL) + .post('/api/usage', (body) => { + expect(body).toEqual({ event_type: 'api_call', quantity: 5, - metadata - }) - .reply(200, mockResponse); + metadata: undefined + }); + return true; + }) + .reply(200, mockResponse); - const result = await client.trackEvent('api_call', 5, metadata); - expect(result).toEqual(mockResponse); - }); + const result = await client.trackEvent('api_call', 5); + expect(result).toEqual(mockResponse); + }); - it('should throw AuthenticationError on 401', async () => { - nock(BASE_URL) - .post('/api/usage') - .reply(401, { error: 'Invalid API key' }); + it('should include quantity and metadata when provided', async () => { + const mockResponse = { + success: true, + event_id: 'evt_123', + timestamp: '2023-01-01T00:00:00Z' + }; + + const metadata = { endpoint: '/users', method: 'GET' }; + + nock(BASE_URL) + .post('/api/usage', { + event_type: 'api_call', + quantity: 5, + metadata + }) + .reply(200, mockResponse); + + const result = await client.trackEvent('api_call', 5, metadata); + expect(result).toEqual(mockResponse); + }); - await expect(client.trackEvent('api_call')).rejects.toThrow(AuthenticationError); - }); + it('should handle explicitly passed undefined metadata', async () => { + const mockResponse = { + success: true, + event_id: 'evt_123', + timestamp: '2023-01-01T00:00:00Z' + }; - it('should throw RateLimitError on 429', async () => { - nock(BASE_URL) - .post('/api/usage') - .reply(429, { - error: 'Rate limit exceeded', - retry_after: 60, - limit: 100, - remaining: 0 + nock(BASE_URL) + .post('/api/usage', (body) => { + expect(body).toEqual({ + event_type: 'api_call', + quantity: 1, + metadata: undefined }); + return true; + }) + .reply(200, mockResponse); - await expect(client.trackEvent('api_call')).rejects.toThrow(RateLimitError); - }); + const result = await client.trackEvent('api_call', 1, undefined); + expect(result).toEqual(mockResponse); }); + it('should handle empty metadata object', async () => { + const mockResponse = { + success: true, + event_id: 'evt_123', + timestamp: '2023-01-01T00:00:00Z' + }; + + const metadata = {}; + + nock(BASE_URL) + .post('/api/usage', { + event_type: 'api_call', + quantity: 1, + metadata + }) + .reply(200, mockResponse); + + const result = await client.trackEvent('api_call', 1, metadata); + expect(result).toEqual(mockResponse); + }); + + it('should throw AuthenticationError on 401', async () => { + nock(BASE_URL) + .post('/api/usage') + .reply(401, { error: 'Invalid API key' }); + + await expect(client.trackEvent('api_call')).rejects.toThrow(AuthenticationError); + }); + + it('should throw RateLimitError on 429', async () => { + nock(BASE_URL) + .post('/api/usage') + .reply(429, { + error: 'Rate limit exceeded', + retry_after: 60, + limit: 100, + remaining: 0 + }); + + await expect(client.trackEvent('api_call')).rejects.toThrow(RateLimitError); + }); +}); + describe('createApiKey', () => { it('should successfully create an API key', async () => { const mockResponse = { @@ -120,6 +224,29 @@ describe('UsageyClient', () => { const result = await client.createApiKey('Test Key', 'org_123', expiresAt); expect(result).toEqual(mockResponse); }); + + it('should handle createApiKey without expiresAt (undefined branch)', async () => { + const mockResponse = { + id: 'key_123', + name: 'Test Key', + key: 'usgy_test123', + createdAt: '2023-01-01T00:00:00Z' + }; + + nock(BASE_URL) + .post('/api/api-keys', (body) => { + expect(body).toEqual({ + name: 'Test Key', + organizationId: 'org_123', + expiresAt: undefined + }); + return true; + }) + .reply(200, mockResponse); + + const result = await client.createApiKey('Test Key', 'org_123'); + expect(result).toEqual(mockResponse); + }); }); describe('regenerateApiKey', () => { @@ -218,5 +345,153 @@ describe('UsageyClient', () => { expect(result).toEqual(mockResponse); }); + + it('should handle getUsageEvents with string dates (string branch)', async () => { + const mockResponse = { + success: true, + data: [ + { id: 'evt_1', event_type: 'api_call', quantity: 1, timestamp: '2023-01-01T00:00:00Z' } + ] + }; + + nock(BASE_URL) + .get('/api/usage') + .query({ + start_date: '2023-01-01T00:00:00.000Z', + end_date: '2023-01-31T23:59:59.999Z' + }) + .reply(200, mockResponse); + + const result = await client.getUsageEvents({ + startDate: '2023-01-01T00:00:00.000Z', + endDate: '2023-01-31T23:59:59.999Z' + }); + + expect(result).toEqual(mockResponse); + }); + + it('should handle getUsageEvents with only startDate as Date', async () => { + const mockResponse = { + success: true, + data: [] + }; + + const startDate = new Date('2023-01-01'); + + nock(BASE_URL) + .get('/api/usage') + .query({ + start_date: startDate.toISOString() + }) + .reply(200, mockResponse); + + const result = await client.getUsageEvents({ + startDate + }); + + expect(result).toEqual(mockResponse); + }); + + it('should handle getUsageEvents with only endDate as Date', async () => { + const mockResponse = { + success: true, + data: [] + }; + + const endDate = new Date('2023-01-31T23:59:59Z'); + + nock(BASE_URL) + .get('/api/usage') + .query({ + end_date: endDate.toISOString() + }) + .reply(200, mockResponse); + + const result = await client.getUsageEvents({ + endDate + }); + + expect(result).toEqual(mockResponse); + }); + + it('should handle getUsageEvents with only startDate as string', async () => { + const mockResponse = { + success: true, + data: [] + }; + + nock(BASE_URL) + .get('/api/usage') + .query({ + start_date: '2023-01-01T00:00:00.000Z' + }) + .reply(200, mockResponse); + + const result = await client.getUsageEvents({ + startDate: '2023-01-01T00:00:00.000Z' + }); + + expect(result).toEqual(mockResponse); + }); + + it('should handle getUsageEvents with only endDate as string', async () => { + const mockResponse = { + success: true, + data: [] + }; + + nock(BASE_URL) + .get('/api/usage') + .query({ + end_date: '2023-01-31T23:59:59.999Z' + }) + .reply(200, mockResponse); + + const result = await client.getUsageEvents({ + endDate: '2023-01-31T23:59:59.999Z' + }); + + expect(result).toEqual(mockResponse); + }); + + it('should handle getUsageEvents with only eventType', async () => { + const mockResponse = { + success: true, + data: [] + }; + + nock(BASE_URL) + .get('/api/usage') + .query({ + event_type: 'api_call' + }) + .reply(200, mockResponse); + + const result = await client.getUsageEvents({ + eventType: 'api_call' + }); + + expect(result).toEqual(mockResponse); + }); + + it('should handle getUsageEvents with only limit', async () => { + const mockResponse = { + success: true, + data: [] + }; + + nock(BASE_URL) + .get('/api/usage') + .query({ + limit: 5 + }) + .reply(200, mockResponse); + + const result = await client.getUsageEvents({ + limit: 5 + }); + + expect(result).toEqual(mockResponse); + }); }); }); \ No newline at end of file