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
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.1.0] - 2025-09-05

### Added

- **Organization Resource** - New resource for retrieving organization information including branding, payment method settings, and payout configurations
- **createTestOrganization()** utility function for testing with realistic organization data
- **Organization Types** - Complete TypeScript definitions for Organization, OrganizationBranding, PaymentMethodSettings, PaymentGateway, PaymentMethodRate, and PayoutSettings

### Changed

- **BREAKING: SourcesResource** - Removed `create()` method as sources should only be created client-side for security
- **SourcesResource Authentication** - Implemented lazy public key (PK) authentication that automatically switches from secret key to public key when retrieving sources
- **BaseClient** - Added `setApiKey()` and `getApiKey()` methods for dynamic API key switching
- **Enhanced Test Utilities** - Extended SpyableMagpie with `mockRequest()` and `mockNetworkError()` methods for better test coverage

### Fixed

- **Sources Security** - Sources now correctly use public key authentication as required by the API
- **Test Coverage** - Updated sources tests to remove create method tests and add PK authentication tests

### Technical Improvements

- Lazy authentication switching prevents unnecessary HTTP calls during SDK initialization
- Public key caching eliminates redundant organization API calls
- Enhanced mock testing infrastructure with request tracking and custom response handling

## [1.0.0] - 2025-09-04

### Added
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Magpie IM
Copyright (c) 2025 Magpie IM

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
60 changes: 0 additions & 60 deletions docs/plan.md

This file was deleted.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@magpieim/magpie-node",
"version": "1.0.0",
"version": "1.1.0",
"description": "The Magpie API Library for NodeJS enables you to work with Magpie APIs.",
"keywords": [
"magpie",
Expand Down
165 changes: 136 additions & 29 deletions src/__tests__/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ export interface LastRequest {
*/
interface SpyableMagpie extends Magpie {
LAST_REQUEST: LastRequest | null;
REQUESTS: LastRequest[];
mockRequest(method: string, url: string, responseData: any, status?: number, headers?: Record<string, string>): void;
mockNetworkError(method: string, url: string): void;
}

/**
Expand All @@ -39,6 +42,22 @@ export function getSpyableMagpie(

// Initialize the LAST_REQUEST tracker
magpie.LAST_REQUEST = null;
magpie.REQUESTS = [];

// Store mock responses and errors
const mockResponses: Map<string, { data: any; status: number; headers: Record<string, string> }> = new Map();
const mockErrors: Map<string, Error> = new Map();

// Add mock methods
magpie.mockRequest = (method: string, url: string, responseData: any, status = 200, headers = {}) => {
const key = `${method.toUpperCase()}:${url}`;
mockResponses.set(key, { data: responseData, status, headers });
};

magpie.mockNetworkError = (method: string, url: string) => {
const key = `${method.toUpperCase()}:${url}`;
mockErrors.set(key, new Error('Network Error'));
};

// Mock the HTTP client to capture requests
(magpie as any).http.request = jest.fn().mockImplementation((requestConfig: AxiosRequestConfig) => {
Expand Down Expand Up @@ -89,7 +108,7 @@ export function getSpyableMagpie(
};

// Capture request details
magpie.LAST_REQUEST = {
const requestDetails = {
method: requestConfig.method?.toUpperCase() ?? 'GET',
url: requestConfig.url ?? '',
data: requestConfig.data,
Expand All @@ -103,17 +122,52 @@ export function getSpyableMagpie(
}
};

// Return a mock successful response
const mockResponse: AxiosResponse = {
magpie.LAST_REQUEST = requestDetails;
magpie.REQUESTS.push(requestDetails);

// Check for mock responses and errors
const key = `${requestDetails.method}:${requestDetails.url}`;

// Check for network error first
if (mockErrors.has(key)) {
const error = mockErrors.get(key);
return Promise.reject(error ?? new Error('Network Error'));
}

// Check for custom mock response
if (mockResponses.has(key)) {
const mockConfig = mockResponses.get(key)!;
const mockResponse: AxiosResponse = {
data: mockConfig.data,
status: mockConfig.status,
statusText: mockConfig.status >= 400 ? 'Error' : 'OK',
headers: { 'request-id': 'req_test_123', ...mockConfig.headers },
config: requestConfig as any,
request: {}
};

// Throw error for 4xx and 5xx status codes
if (mockConfig.status >= 400) {
const error = new Error(`HTTP ${mockConfig.status} Error`) as any;
error.response = mockResponse;
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
return Promise.reject(error);
}

return Promise.resolve(mockResponse);
}

// Return default mock response
const defaultResponse: AxiosResponse = {
data: { id: 'mock_response', object: 'test' },
status: 200,
statusText: 'OK',
headers: {},
headers: { 'request-id': 'req_default_123' },
config: requestConfig as any,
request: {}
};

return Promise.resolve(mockResponse);
return Promise.resolve(defaultResponse);
});

return magpie;
Expand Down Expand Up @@ -259,29 +313,6 @@ export function createTestCharge(overrides: Record<string, any> = {}): Record<st
};
}

/**
* Utility to create test source data
*/
export function createTestSource(overrides: Record<string, any> = {}): Record<string, any> {
return {
type: 'card',
card: {
number: '4242424242424242',
exp_month: 12,
exp_year: 2025,
cvc: '123'
},
redirect: {
success: 'https://example.com/success',
fail: 'https://example.com/fail'
},
billing: {
name: 'Test User',
email: 'test@example.com'
},
...overrides
};
}

/**
* Utility to create test line item data
Expand Down Expand Up @@ -415,6 +446,82 @@ export function createTestWebhookHeaders(signature: string, timestamp?: number,
return headers;
}

/**
* Utility to create test organization data
*/
export function createTestOrganization(overrides: Record<string, any> = {}): Record<string, any> {
return {
object: 'organization',
id: `org_test_${getRandomString()}`,
title: 'Test Organization',
account_name: 'Test Org Account',
statement_descriptor: 'TEST_ORG',
pk_test_key: `pk_test_${getRandomString()}`,
sk_test_key: `sk_test_${getRandomString()}`,
pk_live_key: `pk_live_${getRandomString()}`,
sk_live_key: `sk_live_${getRandomString()}`,
branding: {
icon: 'https://example.com/icon.png',
logo: null,
use_logo: false,
brand_color: '#fffefd',
accent_color: '#1a3da6'
},
status: 'approved',
created_at: '2021-08-15T23:13:11.682944+08:00',
updated_at: '2025-09-05T11:35:51.957059+08:00',
business_address: null,
payment_method_settings: {
card: {
mid: null,
gateway: {
id: 'default',
name: 'Magpie Gateway'
},
rate: {
mdr: 0.029,
fixed_fee: 1000,
formula: 'mdr_plus_fixed'
},
status: 'approved'
},
gcash: {
mid: '217020000038029496672',
gateway: null,
rate: {
mdr: 0.022,
fixed_fee: 0,
formula: 'mdr_plus_fixed'
},
status: 'approved'
}
},
rates: {
card: {
mdr: 0.029,
fixed_fee: 1000
},
gcash: {
mdr: 0.022,
fixed_fee: 0
}
},
payout_settings: {
schedule: 'automatic',
delivery_type: 'standard',
bank_code: 'BPI/BPI Family Savings Bank',
account_number: '3259442965',
account_name: 'Test Account'
},
metadata: {
business_website: 'https://test.com',
support_phone: '917 513 4281',
support_email: 'support@test.com'
},
...overrides
};
}

/**
* Utility to create test webhook signature configuration
*/
Expand All @@ -427,4 +534,4 @@ export function createTestWebhookConfig(overrides: Record<string, any> = {}): Re
prefix: 'v1=',
...overrides
};
}
}
54 changes: 53 additions & 1 deletion src/base-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,59 @@ export class BaseClient {
this.config.debug = debug;
}

// Test the connection
/**
* Updates the API key used for authentication.
*
* This method allows switching between different API keys (e.g., from secret key
* to public key) for specific requests. The key validation ensures only valid
* Magpie API keys are accepted.
*
* @param apiKey - New API key (must start with 'sk_' or 'pk_')
*
* @throws {Error} When apiKey is missing, invalid, or malformed
*
* @example
* ```typescript
* // Switch to public key for sources operations
* client.setApiKey('pk_test_123');
*
* // Switch back to secret key
* client.setApiKey('sk_test_456');
* ```
*/
public setApiKey(apiKey: string): void {
if (!apiKey || typeof apiKey !== 'string') {
throw new Error('Missing or invalid API key');
}

if (!apiKey.startsWith('sk_') && !apiKey.startsWith('pk_')) {
throw new Error('Invalid API key - must start with sk_ or pk_');
}

this.secretKey = apiKey;

// Update the Axios instance with the new authentication
this.http.defaults.auth = {
username: apiKey,
password: '',
};
}

/**
* Gets the currently configured API key.
*
* @returns The current API key (secret or public)
*
* @example
* ```typescript
* const currentKey = client.getApiKey();
* console.log(currentKey.startsWith('sk_') ? 'Secret Key' : 'Public Key');
* ```
*/
public getApiKey(): string {
return this.secretKey;
}

/**
* Tests connectivity to the Magpie API.
*
Expand Down
Loading