Initial import

This commit is contained in:
Andrew Mayorov 2022-09-15 20:44:34 +03:00
commit 80f11d1aca
No known key found for this signature in database
GPG Key ID: 2837C62ACFBFED5D
138 changed files with 10943 additions and 0 deletions

9
.env Normal file
View File

@ -0,0 +1,9 @@
#AUTH_ENDPOINT="http://auth.rbk.test:8080"
#CAPI_ENDPOINT="http://api.rbk.test:8080"
#ADMIN_ENDPOINT="http://iddqd.rbk.test:8080"
#PROXY_ENDPOINT="http://wrapper.rbk.test:8080"
#URL_SHORTENER_ENDPOINT="http://short.rbk.test:8080"
#EXTERNAL_LOGIN="demo_merchant"
#EXTERNAL_PASSWORD="test"
#INTERNAL_LOGIN="manager"
#INTERNAL_PASSWORD="manager"

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/build/
/node_modules/
/api/**/codegen/
mocha-tests

6
.prettierignore Normal file
View File

@ -0,0 +1,6 @@
package.json
package-lock.json
/node_modules/
/lib/
/api/**/codegen/
/build/

8
.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"printWidth": 100,
"singleQuote": true,
"useTabs": false,
"tabWidth": 4,
"semi": true,
"bracketSpacing": true
}

120
Makefile Normal file
View File

@ -0,0 +1,120 @@
.SUFFIXES:
NPM = $(shell which npm 2>/dev/null || exit 1)
TSC = ./node_modules/.bin/tsc
PKG = ./node_modules/.bin/pkg
PKG_CACHE_PATH = $(HOME)/.cache/pkg
export PKG_CACHE_PATH
TESTS_ARGS = \
--external-login '$(TT_LOGIN)' \
--external-password '$(TT_PASSWORD)' \
--internal-login '$(TT_INTERNAL_LOGIN)' \
--internal-password '$(TT_INTERNAL_PASSWORD)' \
--capi-endpoint $(TT_CAPI_ENDPOINT) \
--url-shortener-endpoint $(TT_URLSHORT_ENDPOINT) \
--test-webhook-receiver-endpoint $(TT_TEST_WEBHOOK_RECEIVER_ENDPOINT) \
--auth-endpoint $(TT_AUTH_ENDPOINT) \
--admin-endpoint $(TT_ADMIN_ENDPOINT) \
--proxy-endpoint $(TT_PROXY_ENDPOINT)
SWAG_V2 = ../../schemes/swag/v2/swagger.json
SWAG_ANALYTICS = ../../schemes/swag-analytics/swagger.json
SWAG_BINBASE = ../../schemes/swag-binbase/swagger.json
SWAG_URL_SHORTENER_V1 = ../../schemes/swag-url-shortener/v1/swagger.json
SWAG_WAPI_PAYRES_V0 = ../../schemes/swag-wallets/v0/api/payres/swagger.json
SWAG_WAPI_PRIVDOC_V0 = ../../schemes/swag-wallets/v0/api/privdoc/swagger.json
SWAG_WAPI_WALLET_V0 = ../../schemes/swag-wallets/v0/api/wallet/swagger.json
GEN_SWAG_V2 = ./api/capi-v2/codegen
GEN_SWAG_ANAL = ./api/anapi/codegen
GEN_SWAG_BINBASE = ./api/binapi/codegen
GEN_SWAG_URL_SHORTENER_V1 = ./api/url-shortener-v1/codegen
GEN_SWAG_WAPI_PAYRES_V0 = ./api/wapi-v0/payres/codegen
GEN_SWAG_WAPI_PRIVDOC_V0 = ./api/wapi-v0/privdoc/codegen
GEN_SWAG_WAPI_WALLET_V0 = ./api/wapi-v0/wallet/codegen
GEN_SWAG = \
$(GEN_SWAG_V2) \
$(GEN_SWAG_ANAL) \
$(GEN_SWAG_BINBASE) \
$(GEN_SWAG_URL_SHORTENER_V1) \
$(GEN_SWAG_WAPI_PAYRES_V0) \
$(GEN_SWAG_WAPI_PRIVDOC_V0) \
$(GEN_SWAG_WAPI_WALLET_V0)
.PHONY: mocha-tests mocha-tests.tar.gz distclean
# XXX node_modules dir is here to fix import errors in test files
mocha-tests.tar.gz: mocha-tests
tar zcf mocha-tests.tar.gz -C build . -C .. $^ node_modules
# XXX not installing pkg globally to avoid issues with permissions and bin path
mocha-tests: $(TSC) $(PKG) $(GEN_SWAG)
$(TSC)
$(PKG) -t node8-linux-x64 mocha-tests.js -o $@
$(GEN_SWAG_V2): $(SWAG_V2)
$(call swagger-codegen,$<,$@)
$(GEN_SWAG_ANAL): $(SWAG_ANALYTICS)
$(call swagger-codegen,$<,$@)
$(GEN_SWAG_BINBASE): $(SWAG_BINBASE)
$(call swagger-codegen,$<,$@)
$(GEN_SWAG_URL_SHORTENER_V1): $(SWAG_URL_SHORTENER_V1)
$(call swagger-codegen,$<,$@)
$(GEN_SWAG_WAPI_PAYRES_V0): $(SWAG_WAPI_PAYRES_V0)
$(call swagger-codegen,$<,$@)
$(GEN_SWAG_WAPI_PRIVDOC_V0): $(SWAG_WAPI_PRIVDOC_V0)
$(call swagger-codegen,$<,$@)
$(GEN_SWAG_WAPI_WALLET_V0): $(SWAG_WAPI_WALLET_V0)
$(call swagger-codegen,$<,$@)
$(TSC):
$(NPM) i
$(PKG):
$(NPM) i pkg
distclean:
rm -rf ./node_modules ./build ./mocha-tests ./api/**/codegen
# tests
.PHONY: test test.suite test.transaction
test: test.suite test.transaction
test.suite: mocha-tests
./mocha-tests $(TESTS_ARGS)
test.transaction: mocha-tests
./mocha-tests --tt --timeout $(TT_TIMEOUT) \
$(TESTS_ARGS) \
--test-shop-id $(TT_SHOP_ID) --create-test-shop \
--login-warn $(TT_LOGIN_WARN) \
--get-my-party-warn $(TT_GET_MY_PARTY_WARN) \
--create-invoice-warn $(TT_CREATE_INVOICE_WARN) \
--tokenize-card-warn $(TT_TOKENIZE_CARD_WARN) \
--create-payment-warn $(TT_CREATE_PAYMENT_WARN) \
--fulfill-invoice-warn $(TT_FULFILL_INVOICE_WARN)
# swagger-codegen
SWAGGER_CODEGEN = swagger-codegen-cli-2.3.1.jar
SWAGGER_CODEGEN_PREFIX = https://oss.sonatype.org/content/repositories/releases
SWAGGER_CODEGEN_URL := $(SWAGGER_CODEGEN_PREFIX)/io/swagger/swagger-codegen-cli/2.3.1/$(SWAGGER_CODEGEN)
define swagger-codegen
$(MAKE) $(SWAGGER_CODEGEN)
java -jar $(SWAGGER_CODEGEN) generate -l typescript-fetch -i $(1) -o $(2)
touch $(2)
endef
$(SWAGGER_CODEGEN):
wget $(SWAGGER_CODEGEN_URL)

79
README.md Normal file
View File

@ -0,0 +1,79 @@
# Тесты на Mocha
## Запуск
```
make test
```
## Локальная разработка
### Установка зависимостей и генерация swagger клиента
1. Установка зависимостей: `npm i`
1. Генерация swagger typescript. Необходим swagger codegen версии **2.3.1**:
- CAPI:
- v2: `swagger-codegen generate -i ../../schemes/swag/v2/swagger.json -l typescript-fetch -o api/capi-v2/codegen`
- Url shortener: `swagger-codegen generate -i ../../schemes/swag-url-shortener/v1/swagger.json -l typescript-fetch -o api/url-shortener-v1/codegen`
- WAPI:
- payres: `swagger-codegen generate -i ../../schemes/swag-wallets/v0/api/payres/swagger.json -l typescript-fetch -o api/wapi-v0/payres/codegen`
- privdoc `swagger-codegen generate -i ../../schemes/swag-wallets/v0/api/privdoc/swagger.json -l typescript-fetch -o api/wapi-v0/privdoc/codegen`
- wallet `swagger-codegen generate -i ../../schemes/swag-wallets/v0/api/wallet/swagger.json -l typescript-fetch -o api/wapi-v0/wallet/codegen`
### Вариант 1. Запуск использованием командной строки
Компиляция typescript после изменений в коде тестов: `./node_modules/.bin/tsc`
Запуск тестов: `node mocha-tests.js [options]`
`-h --help` - описание параметров запуска.
Пример:
```
node mocha-tests.js --auth-endpoint http://auth.rbk.test:8080 --external-login demo_merchant --external-password test --capi-endpoint http://api.rbk.test:8080 --admin-endpoint http://iddqd.rbk.test:8080 --internal-login manager --internal-password manager --url-shortener-endpoint http://short.rbk.test:8080 --proxy-endpoint http://wrapper.rbk.test:8080 --test-webhook-receiver-endpoint http://test-webhook-receiver.rbk.test:8080
```
### Вариант 2. Запуск и отладка с использованием Intellij IDEA, WebStorm
Настройка параметров тестов производится в [.env](.env)
В Run/Debug Configuration, необходимо настроить "Defaults" конфигурацию Mocha:
- Extra Mocha options: `--require ts-node/register --timeout 25000`
- Выбрать File patterns, со значением: `test/**/*.spec.ts`
- Working directory: `<...>/wetkitty/test/mocha`
Далее можно добавлять новую Mocha конфигурацию и запускать / отлаживать тесты
## Тестовая транзакция
### Сборка
`make mocha-tests`
В ходе этого собирается отдельный исполняемый файл пригодный для использования без nodejs на хостах, с которых идут тестовые транзакции на продакшн.
### Запуск
`--tt` - признак проведения тестовой транзакции
Если указан `--tt`, необходимо задать warning тайминги:
`--auth-warn <ms>`
`--create-invoice-warn <ms>`
`--create-payment-resource-warn <ms>`
`--create-payment-warn <ms>`
`--polling-warn <ms>`
`--fulfill-invoice-warn <ms>`
Пример:
```
node mocha-tests.js --tt --create-invoice-warn 200 --create-payment-resource-warn 200 --create-payment-warn 200 --polling-warn 5000 --fullfill-invoice-warn 100 --auth-endpoint http://auth.rbk.test:8080 --external-login demo_merchant --external-password test --capi-endpoint http://api.rbk.test:8080 --internal-login manager --internal-password manager --admin-endpoint http://iddqd.rbk.test:8080 --proxy-endpoint http://wrapper.rbk.test:8080 --url-shortener-endpoint http://short.rbk.test:8080 --test-webhook-receiver-endpoint http://test-webhook-receiver.rbk.test:8080
```

1
actions/anapi/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './search-actions';

View File

@ -0,0 +1,231 @@
import * as chai from 'chai';
import { Moment } from 'moment';
import {
SearchApiFp,
InlineResponse2001,
InlineResponse200,
InlineResponse2003,
InlineResponse2002
} from '../../api/anapi/codegen';
import guid from '../../utils/guid';
import { anapiEndpoint } from '../../settings';
import { handleResponseError } from '../../utils';
import { AuthActions } from '../auth-actions';
chai.should();
const anapi_version = 'v1';
export class AnapiSearchActions {
private api;
private static instance: AnapiSearchActions;
static async getInstance(): Promise<AnapiSearchActions> {
if (this.instance) {
return this.instance;
}
const token = await AuthActions.getInstance().getExternalAccessToken();
this.instance = new AnapiSearchActions(token);
return this.instance;
}
private constructor(exToken: string) {
this.api = SearchApiFp({
apiKey: `Bearer ${exToken}`
});
}
searchPayments(
partyID: string,
fromTime: Moment,
toTime: Moment,
limit: number,
shopID?: string,
paymentStatus?: string,
paymentFlow?: string,
paymentMethod?: string,
paymentTerminalProvider?: string,
invoiceID?: string,
paymentID?: string,
payerEmail?: string,
payerIP?: string,
payerFingerprint?: string,
customerID?: string,
first6?: string,
last4?: string,
rrn?: string,
approvalCode?: string,
bankCardTokenProvider?: string,
bankCardPaymentSystem?: string,
paymentAmountTo?: number,
paymentAmountFrom?: number,
excludedShops?: Array<string>,
continuationToken?: string
): Promise<InlineResponse2001> {
const xRequestID = guid();
const xRequestDeadline = undefined;
const shopIDs = undefined;
const paymentInstitutionRealm = undefined;
const invoiceIDs = undefined;
const externalID = undefined;
return this.api
.searchPayments(
xRequestID,
partyID,
fromTime,
toTime,
limit,
xRequestDeadline,
shopID,
shopIDs,
paymentInstitutionRealm,
invoiceIDs,
paymentStatus,
paymentFlow,
paymentMethod,
paymentTerminalProvider,
invoiceID,
paymentID,
externalID,
payerEmail,
payerIP,
payerFingerprint,
customerID,
first6,
last4,
rrn,
approvalCode,
bankCardTokenProvider,
bankCardPaymentSystem,
paymentAmountFrom,
paymentAmountTo,
excludedShops,
continuationToken
)(undefined, `${anapiEndpoint}/${anapi_version}`)
.catch(ex => handleResponseError(ex, xRequestID));
}
searchInvoices(
partyID: string,
fromTime: Moment,
toTime: Moment,
limit: number,
shopID?: string,
invoiceStatus?: string,
invoiceID?: string,
invoiceAmountFrom?: number,
invoiceAmountTo?: number,
excludedShops?: Array<string>,
continuationToken?: string
): Promise<InlineResponse200> {
const xRequestID = guid();
const xRequestDeadline = undefined;
const shopIDs = undefined;
const paymentInstitutionRealm = undefined;
const invoiceIDs = undefined;
const externalID = undefined;
return this.api
.searchInvoices(
xRequestID,
partyID,
fromTime,
toTime,
limit,
xRequestDeadline,
shopID,
shopIDs,
paymentInstitutionRealm,
invoiceIDs,
invoiceStatus,
invoiceID,
externalID,
invoiceAmountFrom,
invoiceAmountTo,
excludedShops,
continuationToken
)(undefined, `${anapiEndpoint}/${anapi_version}`)
.catch(ex => handleResponseError(ex, xRequestID));
}
searchRefunds(
partyID: string,
fromTime: Moment,
toTime: Moment,
limit: number,
shopID?: string,
offset?: number,
invoiceID?: string,
paymentID?: string,
refundID?: string,
refundStatus?: string,
excludedShops?: Array<string>,
continuationToken?: string
): Promise<InlineResponse2003> {
const xRequestID = guid();
const xRequestDeadline = undefined;
const shopIDs = undefined;
const paymentInstitutionRealm = undefined;
const invoiceIDs = undefined;
const externalID = undefined;
return this.api
.searchRefunds(
xRequestID,
partyID,
fromTime,
toTime,
limit,
xRequestDeadline,
shopID,
shopIDs,
paymentInstitutionRealm,
offset,
invoiceIDs,
invoiceID,
paymentID,
refundID,
externalID,
refundStatus,
excludedShops,
continuationToken
)(undefined, `${anapiEndpoint}/${anapi_version}`)
.catch(ex => handleResponseError(ex, xRequestID));
}
searchPayouts(
partyID: string,
fromTime: Moment,
toTime: Moment,
limit: number,
shopID?: string,
offset?: number,
payoutID?: string,
payoutToolType?: string,
excludedShops?: Array<string>,
continuationToken?: string
): Promise<InlineResponse2002> {
const xRequestID = guid();
const xRequestDeadline = undefined;
const shopIDs = undefined;
const paymentInstitutionRealm = undefined;
const invoiceIDs = undefined;
const externalID = undefined;
return this.api
.searchPayouts(
xRequestID,
partyID,
fromTime,
toTime,
limit,
xRequestDeadline,
shopID,
shopIDs,
paymentInstitutionRealm,
offset,
payoutID,
payoutToolType,
excludedShops,
continuationToken
)(undefined, `${anapiEndpoint}/${anapi_version}`)
.catch(ex => handleResponseError(ex, xRequestID));
}
}

61
actions/auth-actions.ts Normal file
View File

@ -0,0 +1,61 @@
import { getAccessToken } from '../api';
import { externalLogin, externalPassword, internalLogin, internalPassword } from '../settings';
export class AuthActions {
private externalAccessToken: string;
private internalAccessToken: string;
private static instance: AuthActions;
getExternalAccessToken(): Promise<string> {
return this.externalAccessToken
? Promise.resolve(this.externalAccessToken)
: this.createExternalAccessToken();
}
getInternalAccessToken(): Promise<string> {
return this.internalAccessToken
? Promise.resolve(this.internalAccessToken)
: this.createInternalAccessToken();
}
private createExternalAccessToken(): Promise<string> {
return AuthActions.authExternal().then(externalAccessToken => {
this.externalAccessToken = externalAccessToken;
return externalAccessToken;
});
}
private createInternalAccessToken(): Promise<string> {
return AuthActions.authInternal().then(internalAccessToken => {
this.internalAccessToken = internalAccessToken;
return internalAccessToken;
});
}
static authExternal(clientID?: string): Promise<string> {
return getAccessToken(
'external',
externalLogin,
externalPassword,
clientID ? clientID : 'common-api'
);
}
static authInternal(clientID?: string): Promise<string> {
return getAccessToken(
'internal',
internalLogin,
internalPassword,
clientID ? clientID : 'private-api'
);
}
static getInstance() {
if (this.instance) {
return this.instance;
}
this.instance = new AuthActions();
return this.instance;
}
}

1
actions/binapi/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './lookup-actions';

View File

@ -0,0 +1,44 @@
import * as chai from 'chai';
import { CardInfo, CardNumber, SearchApiFp } from '../../api/binapi/codegen';
import guid from '../../utils/guid';
import { binapiEndpoint } from '../../settings';
import { handleResponseError } from '../../utils';
import { AuthActions } from '../auth-actions';
chai.should();
const binapi_version = 'v1';
export class BinapiLookupActions {
private api;
private static instance: BinapiLookupActions;
static async getInstance(): Promise<BinapiLookupActions> {
if (this.instance) {
return this.instance;
}
const token = await AuthActions.getInstance().getExternalAccessToken();
this.instance = new BinapiLookupActions(token);
return this.instance;
}
constructor(exToken: string) {
this.api = SearchApiFp({
apiKey: `Bearer ${exToken}`
});
}
lookupCardInfo(cardNumber?: string): Promise<CardInfo> {
const xRequestID = guid();
const xRequestDeadline = undefined;
const cardData: CardNumber = {
cardNumber
};
return this.api
.lookupCardInfo(cardData, xRequestDeadline)(
undefined,
`${binapiEndpoint}/${binapi_version}`
)
.catch(ex => handleResponseError(ex, xRequestID));
}
}

View File

@ -0,0 +1,126 @@
import * as chai from 'chai';
import {
createWalletPayoutToolClaimChangeset,
createPayoutToolClaimChangeset,
liveShopClaimChangeset,
testShopClaimChangeset
} from '../../api/capi-v2';
import { shopPayoutScheduleChange } from '../../api/capi-v2/params/claim-params/default-claim-params';
import { Claim, ClaimsApiFp } from '../../api/capi-v2/codegen';
import { CAPIDispatcher } from '../../utils/codegen-utils';
import { AuthActions } from '../auth-actions';
chai.should();
export class ClaimsActions {
private api;
private dispatcher: CAPIDispatcher;
private static instance: ClaimsActions;
static async getInstance() {
if (this.instance) {
return this.instance;
}
const token = await AuthActions.getInstance().getExternalAccessToken();
this.instance = new ClaimsActions(token);
return this.instance;
}
constructor(accessToken: string) {
this.dispatcher = new CAPIDispatcher({});
this.api = ClaimsApiFp({
apiKey: `Bearer ${accessToken}`
});
}
createClaimForTestShop(testShopID?: string): Promise<Claim> {
return this.dispatcher
.callMethod(this.api.createClaim, testShopClaimChangeset(testShopID))
.then(claim => {
claim.should.to.deep.include({
revision: 2,
status: 'ClaimAccepted'
});
claim.should.to.have.property('id').to.be.a('number');
return claim;
});
}
createClaimForLiveShop(liveShopID?: string): Promise<Claim> {
return this.dispatcher
.callMethod(this.api.createClaim, liveShopClaimChangeset(liveShopID))
.then(claim => {
claim.should.to.deep.include({
revision: 1,
status: 'ClaimPending'
});
claim.should.to.have.property('id').to.be.a('number');
return claim;
});
}
createWalletPayoutToolClaimForLiveShop(
liveContractID: string,
walletID: string
): Promise<Claim> {
return this.dispatcher
.callMethod(
this.api.createClaim,
createWalletPayoutToolClaimChangeset(liveContractID, walletID)
)
.then(claim => {
claim.should.to.deep.include({
revision: 1,
status: 'ClaimPending'
});
claim.should.to.have.property('id').to.be.a('number');
return claim;
});
}
createPayoutToolClaimForLiveShop(liveContractID: string): Promise<Claim> {
return this.dispatcher
.callMethod(this.api.createClaim, createPayoutToolClaimChangeset(liveContractID))
.then(claim => {
claim.should.to.deep.include({
revision: 1,
status: 'ClaimPending'
});
claim.should.to.have.property('id').to.be.a('number');
return claim;
});
}
createClaimForLiveShopWithSchedule(liveShopID?: string, scheduleID?: number): Promise<Claim> {
return this.dispatcher
.callMethod(this.api.createClaim, [shopPayoutScheduleChange(liveShopID, scheduleID)])
.then(claim => {
claim.should.to.deep.include({
revision: 1,
status: 'ClaimPending'
});
claim.should.to.have.property('id').to.be.a('number');
return claim;
});
}
getPendingClaimByID(claimID: number): Promise<Claim> {
return this.dispatcher.callMethod(this.api.getClaimByID, claimID).then(claim => {
claim.should.to.deep.include({
id: claimID,
status: 'ClaimPending'
});
return claim;
});
}
getAcceptedClaimByID(claimID: number): Promise<Claim> {
return this.dispatcher.callMethod(this.api.getClaimByID, claimID).then(claim => {
claim.should.to.deep.include({
id: claimID,
status: 'ClaimAccepted'
});
return claim;
});
}
}

7
actions/capi-v2/index.ts Normal file
View File

@ -0,0 +1,7 @@
export * from './invoices-actions';
export * from './tokens-actions';
export * from './payments-actions';
export * from './parties-actions';
export * from './claims-actions';
export * from './shops-actions';
export * from './invoice-event-actions';

View File

@ -0,0 +1,75 @@
import {
InvoiceChange,
InvoiceStatus,
InvoiceStatusChanged,
PaymentStatus,
PaymentStatusChanged,
RefundStatus,
RefundStatusChanged
} from '../../../api/capi-v2/codegen';
import InvoiceChangeType = InvoiceChange.ChangeTypeEnum;
import InvoiceStatusType = InvoiceStatus.StatusEnum;
import PaymentStatusType = PaymentStatus.StatusEnum;
import RefundStatusType = RefundStatus.StatusEnum;
export type ChangeInvoiceCondition = (change: InvoiceChange) => boolean;
export function isInvoicePaid(): ChangeInvoiceCondition {
return (change: InvoiceChange) =>
change.changeType === InvoiceChangeType.InvoiceStatusChanged &&
(change as InvoiceStatusChanged).status === InvoiceStatusType.Paid;
}
export function isInvoiceUnpaid(): ChangeInvoiceCondition {
return (change: InvoiceChange) =>
change.changeType === InvoiceChangeType.InvoiceStatusChanged &&
(change as InvoiceStatusChanged).status === InvoiceStatusType.Unpaid;
}
export function isInvoiceInteracted(): ChangeInvoiceCondition {
return (change: InvoiceChange) =>
change.changeType === InvoiceChangeType.PaymentInteractionRequested;
}
export function isPaymentCaptured(paymentID: string): ChangeInvoiceCondition {
return (change: InvoiceChange) =>
change.changeType === InvoiceChangeType.PaymentStatusChanged &&
(change as PaymentStatusChanged).paymentID === paymentID &&
(change as PaymentStatusChanged).status === PaymentStatusType.Captured;
}
export function isPaymentFailed(paymentID: string): ChangeInvoiceCondition {
return (change: InvoiceChange) =>
change.changeType === InvoiceChangeType.PaymentStatusChanged &&
(change as PaymentStatusChanged).paymentID === paymentID &&
(change as PaymentStatusChanged).status === PaymentStatusType.Failed;
}
export function isPaymentPending(paymentID: string): ChangeInvoiceCondition {
return (change: InvoiceChange) =>
change.changeType === InvoiceChangeType.PaymentStatusChanged &&
(change as PaymentStatusChanged).paymentID === paymentID &&
(change as PaymentStatusChanged).status === PaymentStatusType.Pending;
}
export function isPaymentProcessed(paymentID: string): ChangeInvoiceCondition {
return (change: InvoiceChange) =>
change.changeType === InvoiceChangeType.PaymentStatusChanged &&
(change as PaymentStatusChanged).paymentID === paymentID &&
(change as PaymentStatusChanged).status === PaymentStatusType.Processed;
}
export function isPaymentRefunded(paymentID: string): ChangeInvoiceCondition {
return (change: InvoiceChange) =>
change.changeType === InvoiceChangeType.PaymentStatusChanged &&
(change as PaymentStatusChanged).paymentID === paymentID &&
(change as PaymentStatusChanged).status === PaymentStatusType.Refunded;
}
export function isRefundSucceeded(paymentID: string, refundID: string): ChangeInvoiceCondition {
return (change: InvoiceChange) =>
change.changeType === InvoiceChangeType.RefundStatusChanged &&
(change as RefundStatusChanged).paymentID === paymentID &&
(change as RefundStatusChanged).refundID === refundID &&
(change as RefundStatusChanged).status === RefundStatusType.Succeeded;
}

View File

@ -0,0 +1,2 @@
export * from './invoice-event-actions';
export * from './conditions';

View File

@ -0,0 +1,35 @@
import { InvoiceEvent, InvoicesApiFp } from '../../../api/capi-v2/codegen';
import { CAPIDispatcher } from '../../../utils/codegen-utils';
import { EventActions } from '../../event-actions';
import { AuthActions } from '../../auth-actions';
export class InvoicesEventActions extends EventActions {
private static instance: InvoicesEventActions;
private dispatcher: CAPIDispatcher;
static async getInstance(): Promise<InvoicesEventActions> {
if (this.instance) {
return this.instance;
}
const token = await AuthActions.getInstance().getExternalAccessToken();
this.instance = new InvoicesEventActions(token);
return this.instance;
}
constructor(accessToken: string) {
super();
this.dispatcher = new CAPIDispatcher({});
this.api = InvoicesApiFp({
apiKey: `Bearer ${accessToken}`
});
}
async getEvents(invoiceID: string): Promise<InvoiceEvent[]> {
return await this.dispatcher.callMethod(
this.api.getInvoiceEvents,
invoiceID,
1000,
undefined
);
}
}

View File

@ -0,0 +1,99 @@
import {
InvoiceAndToken,
InvoiceTemplate,
InvoiceTemplateAndToken,
InvoiceTemplateCreateParams,
InvoiceTemplatesApiFp,
PaymentMethod
} from '../../api/capi-v2/codegen';
import {
assertSimpleInvoiceTemplate,
assertSimpleInvoiceWithTemplate,
simpleInvoiceTemplateParams
} from '../../api/capi-v2/params/invoice-template-params/invoice-template-params';
import { invoiceParamsWithTemplate } from '../../api/capi-v2/params';
import { CAPIDispatcher } from '../../utils/codegen-utils';
export class InvoiceTemplatesActions {
private api;
private dispatcher: CAPIDispatcher;
constructor(accessToken: string) {
this.dispatcher = new CAPIDispatcher({});
this.api = InvoiceTemplatesApiFp({
apiKey: `Bearer ${accessToken}`
});
}
createSimpleInvoiceTemplate(shopID: string): Promise<InvoiceTemplateAndToken> {
let templateParams: InvoiceTemplateCreateParams = simpleInvoiceTemplateParams(shopID);
return this.dispatcher
.callMethod(this.api.createInvoiceTemplate, templateParams)
.then(invoiceTemplateAndToken => {
invoiceTemplateAndToken.should.to.have.property('invoiceTemplate');
invoiceTemplateAndToken.should.to.have.property('invoiceTemplateAccessToken');
const invoiceTemplate = invoiceTemplateAndToken.invoiceTemplate;
assertSimpleInvoiceTemplate(invoiceTemplate, shopID);
return invoiceTemplateAndToken;
});
}
createInvoiceWithTemplate(invoiceTemplateID: string, shopID: string): Promise<InvoiceAndToken> {
return this.dispatcher
.callMethod(
this.api.createInvoiceWithTemplate,
invoiceTemplateID,
invoiceParamsWithTemplate()
)
.then(invoiceAndToken => {
invoiceAndToken.should.to.have.property('invoice');
invoiceAndToken.should.to.have.property('invoiceAccessToken');
const invoice = invoiceAndToken.invoice;
assertSimpleInvoiceWithTemplate(
invoice,
invoiceParamsWithTemplate().amount,
shopID
);
return invoiceAndToken;
});
}
getInvoiceTemplateById(invoiceTemplateID: string, shopID: string): Promise<InvoiceTemplate> {
return this.dispatcher
.callMethod(this.api.getInvoiceTemplateByID, invoiceTemplateID)
.then(invoiceTemplate => {
assertSimpleInvoiceTemplate(invoiceTemplate, shopID);
return invoiceTemplate;
});
}
updateInvoiceTemplate(
invoiceTemplateCreateParams: InvoiceTemplateCreateParams,
invoiceTemplateID: string,
shopID: string,
assertParams?: {}
): Promise<InvoiceTemplate> {
return this.dispatcher
.callMethod(
this.api.updateInvoiceTemplate,
invoiceTemplateID,
invoiceTemplateCreateParams
)
.then(invoiceTemplate => {
assertSimpleInvoiceTemplate(invoiceTemplate, shopID, assertParams);
return invoiceTemplate;
});
}
deleteInvoiceTemplate(invoiceTemplateID: string): Promise<void> {
return this.dispatcher.callMethod(this.api.deleteInvoiceTemplate, invoiceTemplateID);
}
getInvoicePaymentMethodsByTemplateID(invoiceTemplateID: string): Promise<PaymentMethod[]> {
return this.dispatcher
.callMethod(this.api.getInvoicePaymentMethodsByTemplateID, invoiceTemplateID)
.then(paymentMethods => {
return paymentMethods;
});
}
}

View File

@ -0,0 +1,137 @@
import * as chai from 'chai';
import * as moment from 'moment';
import {
AccessToken,
Invoice,
InvoiceAndToken,
InvoiceParams,
InvoicesApiFp,
LogicError,
PaymentMethod,
Reason
} from '../../api/capi-v2/codegen';
import { assertSimpleInvoice, simpleInvoiceParams } from '../../api/capi-v2/params';
import { CAPIDispatcher } from '../../utils/codegen-utils';
chai.should();
export class InvoicesActions {
private api;
private dispatcher: CAPIDispatcher;
constructor(accessToken: string) {
this.dispatcher = new CAPIDispatcher({});
this.api = InvoicesApiFp({
apiKey: `Bearer ${accessToken}`
});
}
createSimpleInvoice(
shopID: string = 'TEST',
amount: number = 10000,
params?: {}
): Promise<InvoiceAndToken> {
let invoiceParams = simpleInvoiceParams(shopID, params);
invoiceParams = {
...invoiceParams,
amount,
cart: [
{
...invoiceParams.cart[0],
price: amount
}
]
};
return this.dispatcher
.callMethod(this.api.createInvoice, invoiceParams)
.then(invoiceAndToken => {
invoiceAndToken.should.to.have.property('invoice');
const invoice = invoiceAndToken.invoice;
assertSimpleInvoice(invoice, amount, shopID);
invoice.should.to.have.property('dueDate').to.eq(invoiceParams.dueDate);
invoiceAndToken.should.to.have.nested
.property('invoiceAccessToken.payload')
.to.be.a('string');
return invoiceAndToken;
});
}
getCreateInvoiceError(shopID: string, params?: {}): Promise<LogicError> {
let invoiceParams: InvoiceParams = simpleInvoiceParams(shopID, params);
return this.dispatcher.callMethod(this.api.createInvoice, invoiceParams).catch(error => {
return error;
});
}
createInvoiceWithoutCart(shopID: string = 'TEST'): Promise<LogicError> {
let invoiceParams: InvoiceParams = simpleInvoiceParams(shopID, { cart: [] });
return this.dispatcher.callMethod(this.api.createInvoice, invoiceParams).catch(error => {
error.message.should.to.include({
code: 'invalidInvoiceCart',
message: 'Wrong size. Path to item: cart'
});
return error;
});
}
createInvoiceWithWrongShopID(shopID: string = 'TEST'): Promise<LogicError> {
let invoiceParams: InvoiceParams = simpleInvoiceParams(shopID);
return this.dispatcher.callMethod(this.api.createInvoice, invoiceParams).catch(error => {
error.message.should.to.include({
code: 'invalidShopID',
message: 'Shop not found'
});
return error;
});
}
createInvoiceWithWrongDueDate(shopID: string = 'TEST'): Promise<LogicError> {
let invoiceParams: InvoiceParams = simpleInvoiceParams(shopID, {
dueDate: moment()
.subtract(15, 'days')
.utc()
.format() as any
});
return this.dispatcher.callMethod(this.api.createInvoice, invoiceParams).catch(error => {
return error;
});
}
createInvoiceAccessToken(invoiceID: string): Promise<AccessToken> {
return this.dispatcher
.callMethod(this.api.createInvoiceAccessToken, invoiceID)
.then(response => {
response.should.to.have.property('payload').to.be.a('string');
return response;
});
}
fulfillInvoice(invoiceID: string): Promise<void> {
return this.dispatcher.callMethod(this.api.fulfillInvoice, invoiceID, {
reason: 'test reason'
});
}
rescindInvoice(invoiceID: string, reason: Reason): Promise<Response> {
return this.dispatcher.callMethod(this.api.rescindInvoice, invoiceID, reason);
}
getInvoiceById(invoiceID: string): Promise<Invoice> {
return this.dispatcher
.callMethod(this.api.getInvoiceByID, invoiceID)
.then(invoice => invoice);
}
getInvoicePaymentMethods(invoiceID: string): Promise<PaymentMethod[]> {
return this.dispatcher
.callMethod(this.api.getInvoicePaymentMethods, invoiceID)
.then(methods => {
methods.should.to.deep.members([
{ method: 'BankCard', paymentSystems: ['mastercard', 'nspkmir', 'visa'] },
{ method: 'DigitalWallet', providers: ['qiwi'] },
{ method: 'PaymentTerminal', providers: ['euroset'] }
]);
return methods;
});
}
}

View File

@ -0,0 +1,12 @@
import { PayoutParams } from '../../../api/capi-v2/codegen';
import guid from '../../../utils/guid';
export function getPayoutParams(shopID: string, payoutToolID: string): PayoutParams {
return {
id: guid(),
shopID,
payoutToolID,
amount: 100,
currency: 'RUB'
};
}

View File

@ -0,0 +1,43 @@
import * as chai from 'chai';
import { PartiesApiFp, Party } from '../../api/capi-v2/codegen';
import { CAPIDispatcher } from '../../utils/codegen-utils';
import { AuthActions } from '../auth-actions';
chai.should();
export class PartiesActions {
private api;
private dispatcher: CAPIDispatcher;
private static instance: PartiesActions;
static async getInstance(): Promise<PartiesActions> {
if (this.instance) {
return this.instance;
}
const token = await AuthActions.getInstance().getExternalAccessToken();
this.instance = new PartiesActions(token);
return this.instance;
}
constructor(accessToken: string) {
this.dispatcher = new CAPIDispatcher({
headers: {
origin: 'https://dashboard.rbk.money'
}
});
this.api = PartiesApiFp({
apiKey: `Bearer ${accessToken}`
});
}
getActiveParty(): Promise<Party> {
return this.dispatcher.callMethod(this.api.getMyParty).then(party => {
party.should.to.deep.include({
isBlocked: false,
isSuspended: false
});
party.should.to.have.property('id').to.be.a('string');
return party;
});
}
}

View File

@ -0,0 +1,211 @@
import * as chai from 'chai';
import {
captureParams,
Payment,
PaymentFlow,
paymentParams,
PaymentRecurrentParent,
PaymentResource,
paymentResourcePayer,
PaymentsApiFp,
recurrentPayer,
LogicError,
Refund,
RefundParams,
SearchApiFp
} from '../../api/capi-v2';
import { assertPayment } from '../../api/capi-v2/params';
import { CAPIDispatcher } from '../../utils/codegen-utils';
import delay from '../../utils/delay';
import moment = require('moment');
chai.should();
export class PaymentsActions {
private api;
private searchApi;
private dispatcher: CAPIDispatcher;
constructor(accessToken: string) {
this.dispatcher = new CAPIDispatcher({});
this.api = PaymentsApiFp({
apiKey: `Bearer ${accessToken}`
});
this.searchApi = SearchApiFp({
apiKey: `Bearer ${accessToken}`
});
}
createInstantPayment(
invoiceID: string,
paymentResource: PaymentResource,
amount = 10000,
externalID?: string,
metadata?: object
): Promise<Payment> {
const payer = paymentResourcePayer(paymentResource);
let params = paymentParams(
payer,
PaymentFlow.TypeEnum.PaymentFlowInstant,
false,
undefined,
externalID,
metadata
);
return this.dispatcher
.callMethod(this.api.createPayment, invoiceID, params)
.then(payment => {
assertPayment(
payment,
amount,
PaymentFlow.TypeEnum.PaymentFlowInstant,
undefined,
metadata
);
return payment;
});
}
createHoldPayment(
invoiceID: string,
paymentResource: PaymentResource,
holdType?: string,
amount = 10000
): Promise<Payment> {
const payer = paymentResourcePayer(paymentResource);
const params = paymentParams(payer, PaymentFlow.TypeEnum.PaymentFlowHold, false, holdType);
return this.dispatcher
.callMethod(this.api.createPayment, invoiceID, params)
.then(payment => {
assertPayment(payment, amount, PaymentFlow.TypeEnum.PaymentFlowHold, holdType);
return payment;
});
}
createFirstRecurrentPayment(
invoiceID: string,
paymentResource: PaymentResource
): Promise<Payment> {
const payer = paymentResourcePayer(paymentResource);
const params = paymentParams(payer, PaymentFlow.TypeEnum.PaymentFlowInstant, true);
return this.dispatcher
.callMethod(this.api.createPayment, invoiceID, params)
.then(payment => {
assertPayment(payment, 10000, PaymentFlow.TypeEnum.PaymentFlowInstant);
return payment;
});
}
createRecurrentPayment(invoiceID: string, parent: PaymentRecurrentParent): Promise<Payment> {
const payer = recurrentPayer(parent);
const params = paymentParams(payer, PaymentFlow.TypeEnum.PaymentFlowInstant, true);
return this.dispatcher
.callMethod(this.api.createPayment, invoiceID, params)
.then(payment => {
assertPayment(payment, 10000, PaymentFlow.TypeEnum.PaymentFlowInstant);
return payment;
});
}
createRefund(
invoiceID: string,
paymentID: string,
refundParams: RefundParams
): Promise<Refund> {
return this.dispatcher
.callMethod(this.api.createRefund, invoiceID, paymentID, refundParams)
.then(refund => {
refund.should.to.have.property('id').to.be.a('string');
refund.should.to.have.property('createdAt').to.be.a('string');
refund.should.to.have.property('amount').to.be.a('number');
refund.should.to.have.property('currency').to.be.a('string');
refund.should.to.have.property('reason').to.be.a('string');
refund.should.to.have.property('status').to.equal('pending');
return refund;
});
}
createRefundError(
invoiceID: string,
paymentID: string,
refundParams: RefundParams
): Promise<LogicError> {
return this.dispatcher
.callMethod(this.api.createRefund, invoiceID, paymentID, refundParams)
.catch(error => {
return error;
});
}
getPaymentByID(
invoiceID: string,
paymentID: string,
paymentType: PaymentFlow.TypeEnum
): Promise<Payment> {
return this.dispatcher
.callMethod(this.api.getPaymentByID, invoiceID, paymentID)
.then(payment => {
assertPayment(payment, 10000, paymentType);
return payment;
});
}
capturePayment(invoiceID: string, paymentID: string, amount?: number): Promise<Response> {
const params = captureParams(amount);
return this.dispatcher
.callMethod(this.api.capturePayment, invoiceID, paymentID, params)
.then(resp => {
return resp;
});
}
async searchPayments(shopID: string) {
return await this.dispatcher.callMethod(
this.searchApi.searchPayments,
shopID,
moment().add(-1, 'minutes'),
moment(),
1000,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined
);
}
async waitPayment(paymentID: string, shopID: string) {
const result = await Promise.race([this.pollPayment(paymentID, shopID), delay(25000)]);
if (result) {
return result;
}
throw new Error(`payments event polling timeout`);
}
private async pollPayment(paymentID: string, shopID: string) {
let paymentFound = false;
while (!paymentFound) {
await delay(2000);
const payments = (await this.searchPayments(shopID)).result;
const foundPayments = payments.filter(payment => payment.id === paymentID);
paymentFound =
payments.length > 0 &&
foundPayments.length > 0 &&
foundPayments[0].status === 'captured';
}
return paymentFound;
}
}

View File

@ -0,0 +1,22 @@
import { CAPIDispatcher } from '../../utils/codegen-utils';
import { Payout, PayoutsApiFp } from '../../api/capi-v2/codegen';
import { getPayoutParams } from './params/payout-params';
export class PayoutActions {
private api;
private dispatcher: CAPIDispatcher;
constructor(accessToken: string) {
this.dispatcher = new CAPIDispatcher({});
this.api = PayoutsApiFp({
apiKey: `Bearer ${accessToken}`
});
}
createPayout(shopID: string, payoutToolID: string): Promise<Payout> {
const params = getPayoutParams(shopID, payoutToolID);
return this.dispatcher.callMethod(this.api.createPayout, params).then(payout => {
return payout;
});
}
}

View File

@ -0,0 +1,36 @@
import * as chai from 'chai';
import { Shop, ShopsApiFp } from '../../api/capi-v2/codegen';
import { CAPIDispatcher } from '../../utils/codegen-utils';
chai.should();
export class ShopsActions {
private api;
private dispatcher: CAPIDispatcher;
constructor(accessToken: string) {
this.dispatcher = new CAPIDispatcher({});
this.api = ShopsApiFp({
apiKey: `Bearer ${accessToken}`
});
}
getFirstShop(): Promise<Shop> {
return this.dispatcher.callMethod(this.api.getShops).then(shops => {
shops.should.to.be.an('array').that.is.not.empty;
const shop = shops[0];
shop.should.to.deep.include({
isBlocked: false,
isSuspended: false
});
return shop;
});
}
getShopByID(shopID: string = 'TEST'): Promise<Shop> {
return this.dispatcher.callMethod(this.api.getShopByID, shopID).then(shop => {
shop.should.to.have.property('id').to.be.a('string');
return shop;
});
}
}

View File

@ -0,0 +1,149 @@
import * as chai from 'chai';
import {
insufficientFundsVisaTool,
cryptoPaymentTool,
qiwiPaymentTool,
saneVisaPaymentTool,
CryptoWalletData
} from '../../api/capi-v2';
import {
ClientInfo,
PaymentResource,
PaymentTerminalDetails,
PaymentToolDetails,
PaymentToolDetailsBankCard,
PaymentToolDetailsDigitalWallet,
TokensApiFp,
LogicError,
PaymentResourceParams
} from '../../api/capi-v2/codegen';
import { secureVisaPaymentTool } from '../../api/capi-v2/params';
import { secureEmptyCVVVisaPaymentTool } from '../../api/capi-v2/params';
import { CAPIDispatcher } from '../../utils/codegen-utils';
import DigitalWalletDetailsType = PaymentToolDetailsDigitalWallet.DigitalWalletDetailsTypeEnum;
chai.should();
export class TokensActions {
private api;
private dispatcher: CAPIDispatcher;
constructor(accessToken: string) {
this.dispatcher = new CAPIDispatcher({});
this.api = TokensApiFp({
apiKey: `Bearer ${accessToken}`
});
}
createPaymentResource(paymentTool: PaymentResourceParams): Promise<PaymentResource> {
return this.dispatcher.callMethod(this.api.createPaymentResource, paymentTool);
}
createPaymentResourceError(paymentTool: PaymentResourceParams): Promise<LogicError> {
return this.dispatcher
.callMethod(this.api.createPaymentResource, paymentTool)
.catch(error => {
return error;
});
}
createSaneVisaPaymentResource(): Promise<PaymentResource> {
return this.dispatcher
.callMethod(this.api.createPaymentResource, saneVisaPaymentTool)
.then(resource => {
this.assertPaymentResource(resource, {
cardNumberMask: '424242******4242',
last4: '4242',
first6: '424242',
detailsType: 'PaymentToolDetailsBankCard',
paymentSystem: 'visa'
} as PaymentToolDetails);
return resource;
});
}
createInsufficientFundsVisaPaymentResource(): Promise<PaymentResource> {
return this.dispatcher
.callMethod(this.api.createPaymentResource, insufficientFundsVisaTool)
.then(resource => {
this.assertPaymentResource(resource, {
cardNumberMask: '400000******0002',
last4: '0002',
first6: '400000',
detailsType: 'PaymentToolDetailsBankCard',
paymentSystem: 'visa'
} as PaymentToolDetails);
return resource;
});
}
createSecureVisaPaymentResource(): Promise<PaymentResource> {
return this.dispatcher
.callMethod(this.api.createPaymentResource, secureVisaPaymentTool)
.then(resource => {
this.assertPaymentResource(resource, {
cardNumberMask: '401288******1881',
last4: '1881',
first6: '401288',
detailsType: 'PaymentToolDetailsBankCard',
paymentSystem: 'visa'
} as PaymentToolDetails);
return resource;
});
}
createSecureEmptyCVVVisaPaymentResource(): Promise<PaymentResource> {
return this.dispatcher
.callMethod(this.api.createPaymentResource, secureEmptyCVVVisaPaymentTool)
.then(resource => {
this.assertPaymentResource(resource, {
cardNumberMask: '401288******1881',
last4: '1881',
first6: '401288',
detailsType: 'PaymentToolDetailsBankCard',
paymentSystem: 'visa'
} as PaymentToolDetails);
return resource;
});
}
createQIWIPaymentResource(): Promise<PaymentResource> {
return this.dispatcher
.callMethod(this.api.createPaymentResource, qiwiPaymentTool)
.then(resource => {
this.assertPaymentResource(resource, {
detailsType: 'PaymentToolDetailsDigitalWallet',
digitalWalletDetailsType: DigitalWalletDetailsType.DigitalWalletDetailsQIWI,
phoneNumberMask: '+7*****1111'
} as PaymentToolDetails);
return resource;
});
}
createCryptoPaymentResource(): Promise<PaymentResource> {
return this.dispatcher
.callMethod(this.api.createPaymentResource, cryptoPaymentTool)
.then(resource => {
this.assertPaymentResource(resource, {
detailsType: 'PaymentToolDetailsCryptoWallet',
cryptoCurrency: 'bitcoinCash'
} as PaymentToolDetails);
return resource;
});
}
private assertPaymentResource(
resource: PaymentResource,
assertionPaymentToolDetails: PaymentToolDetails
) {
resource.should.to.have.property('paymentToolToken').to.be.a('string');
resource.should.to.have.property('paymentSession').to.be.a('string');
resource.should.to.have.property('clientInfo').to.be.a('object');
resource.should.to.have.property('paymentToolDetails').to.be.a('object');
const clientInfo = resource['clientInfo'] as ClientInfo; // TODO swagger multi inheritance bug
clientInfo.should.to.have.property('fingerprint').to.be.a('string');
clientInfo.should.to.have.property('ip').to.be.a('string');
const paymentToolDetails = resource.paymentToolDetails;
paymentToolDetails.should.to.deep.eq(assertionPaymentToolDetails);
}
}

View File

@ -0,0 +1,60 @@
import * as chai from 'chai';
import { InvoicesTopic, Webhook, WebhooksApiFp, WebhookScope } from '../../api/capi-v2/codegen';
import { AuthActions } from '../auth-actions';
import { countEvents, getEvents, webhookParams } from '../../api/capi-v2/params';
import { CAPIDispatcher } from '../../utils/codegen-utils';
chai.should();
export class WebhooksActions {
private api;
private dispatcher: CAPIDispatcher;
private static instance: WebhooksActions;
static async getInstance(): Promise<WebhooksActions> {
if (this.instance) {
return this.instance;
}
const token = await AuthActions.getInstance().getExternalAccessToken();
this.instance = new WebhooksActions(token);
return this.instance;
}
private constructor(exToken: string) {
this.dispatcher = new CAPIDispatcher({});
this.api = WebhooksApiFp({
apiKey: `Bearer ${exToken}`
});
}
async createWebhook(
shopID: string = 'TEST',
testId: string,
eventTypes: Array<InvoicesTopic.EventTypesEnum>
): Promise<Webhook> {
return this.dispatcher
.callMethod(
this.api.createWebhook,
webhookParams(
{
topic: WebhookScope.TopicEnum.InvoicesTopic,
shopID: shopID,
eventTypes: eventTypes
} as InvoicesTopic,
testId
)
)
.then(webhook => {
webhook.should.to.have.property('active').to.eq(true);
return webhook;
});
}
countEvents(testId: string): Promise<number> {
return countEvents(testId);
}
getEvents(testId: string): Promise<string> {
return getEvents(testId);
}
}

60
actions/event-actions.ts Normal file
View File

@ -0,0 +1,60 @@
import { ChangeIdentityCondition } from './wapi-v0/wallet/identities-event-actions';
import {
IdentityChallengeEvent,
IdentityChallengeStatusChanged,
WithdrawalEvent,
WithdrawalStatus
} from '../api/wapi-v0/wallet/codegen';
import { InvoiceChange, InvoiceEvent } from '../api/capi-v2/codegen';
import { ChangeInvoiceCondition } from './capi-v2/invoice-event-actions';
import { ChangeWithdrawalCondition } from './wapi-v0/wallet/withdrawals-event-actions';
import delay from '../utils/delay';
type ChangeCondition = ChangeIdentityCondition | ChangeInvoiceCondition | ChangeWithdrawalCondition;
type EventStatusChanged = IdentityChallengeStatusChanged | InvoiceChange | WithdrawalStatus;
type Event = IdentityChallengeEvent | InvoiceEvent | WithdrawalEvent;
export abstract class EventActions {
protected api;
protected constructor() {}
async waitConditions(
conditions: ChangeCondition[],
...args: any[]
): Promise<EventStatusChanged[]> {
const result = await Promise.race([this.pollEvents(conditions, ...args), delay(20000)]);
if (result) {
return result;
}
throw new Error(`event polling timeout`);
}
private async pollEvents(
conditions: ChangeCondition[],
...args: any[]
): Promise<EventStatusChanged[]> {
let events = [];
let foundChanges;
while (!foundChanges || foundChanges.length !== conditions.length) {
await delay(1000);
events = await this.getEvents(...args);
foundChanges = this.findChanges(events, conditions);
}
return foundChanges;
}
private findChanges(events: Event[], conditions: ChangeCondition[]): EventStatusChanged[] {
const result = [];
for (const { changes } of events) {
for (const condition of conditions) {
// @ts-ignore
const found = changes.find(change => condition(change));
found !== undefined && result.push(found);
}
}
return result;
}
abstract async getEvents(...args: any[]): Promise<Event[]>;
}

6
actions/index.ts Normal file
View File

@ -0,0 +1,6 @@
export * from './capi-v2';
export * from './papi-v1';
export * from './auth-actions';
export * from './wapi-v0';
export * from './anapi';
export * from './binapi';

View File

@ -0,0 +1,27 @@
import * as chai from 'chai';
import { ClaimsApiForTests, PapiFactory } from '../../api/papi-v1';
import { AuthActions } from '../auth-actions';
chai.should();
export class PapiClaimsActions {
private api;
private static instance: PapiClaimsActions;
static async getInstance() {
if (this.instance) {
return this.instance;
}
const token = await AuthActions.getInstance().getInternalAccessToken();
this.instance = new PapiClaimsActions(token);
return this.instance;
}
constructor(accessToken: string) {
this.api = PapiFactory.getInstance(ClaimsApiForTests.name, accessToken);
}
acceptClaimByID(partyID: string, claimID: number, claimRevision: number): Promise<void> {
return this.api.acceptClaimByIDForTests(partyID, claimID, claimRevision);
}
}

2
actions/papi-v1/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './claims-actions';
export * from './payouts-actions';

View File

@ -0,0 +1,52 @@
import * as chai from 'chai';
import { PapiFactory, Payout, PayoutsApiForTests } from '../../api/papi-v1';
import { AuthActions } from '../auth-actions';
import delay from '../../utils/delay';
chai.should();
export class PapiPayoutsActions {
private api: PayoutsApiForTests;
private static instance: PapiPayoutsActions;
static async getInstance(): Promise<PapiPayoutsActions> {
if (this.instance) {
return this.instance;
}
const token = await AuthActions.getInstance().getInternalAccessToken();
this.instance = new PapiPayoutsActions(token);
return this.instance;
}
constructor(accessToken: string) {
this.api = PapiFactory.getInstance(PayoutsApiForTests.name, accessToken);
}
async getPayoutWhenPresent(partyID: string, shopID: string): Promise<Payout> {
const payout = await Promise.race([delay(20000), this.pollSearchPayouts(partyID, shopID)]);
if (!payout) {
throw new Error('Wait searchPayouts result timeout');
}
return payout;
}
pay(payoutIds: string[]) {
return this.api.pay(payoutIds);
}
confirmPayouts(payoutIds: string[]) {
return this.api.confirmPayouts(payoutIds);
}
private async pollSearchPayouts(partyID: string, shopID: string): Promise<Payout> {
let result;
while (!result) {
const payouts = await this.api.searchPayoutsForTests();
result = payouts.find(
({ partyId, shopId }) => partyId === partyID && shopId === shopID
);
await delay(1000);
}
return result;
}
}

3
actions/wapi-v0/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './wallet';
export * from './payres/payres-actions';
export * from './privdoc/privdoc-actions';

View File

@ -0,0 +1,25 @@
import * as chai from 'chai';
import { PaymentResourcesApiFp } from '../../../api/wapi-v0/payres/codegen';
import { getBankCardParams } from '../../../api/wapi-v0/payres/params/payres-params';
import { WapiPayresDispatcher } from '../../../utils/codegen-utils';
chai.should();
export class PayresActions {
private api;
private dispatcher: WapiPayresDispatcher;
constructor(accessToken: string) {
this.dispatcher = new WapiPayresDispatcher({});
this.api = PaymentResourcesApiFp({
apiKey: `Bearer ${accessToken}`
});
}
async storeBankCard() {
const cardParams = getBankCardParams();
const card = await this.dispatcher.callMethod(this.api.storeBankCard, cardParams);
card.should.contain.keys('token', 'validUntil');
return card;
}
}

View File

@ -0,0 +1,39 @@
import * as chai from 'chai';
import { WapiPrivdocDispatcher } from '../../../utils/codegen-utils';
import { PrivateDocumentsApiFp } from '../../../api/wapi-v0/privdoc/codegen';
import {
getPassportParams,
getRICParams
} from '../../../api/wapi-v0/privdoc/params/privdoc-params';
chai.should();
export class PrivdocActions {
private api;
private dispatcher: WapiPrivdocDispatcher;
constructor(accessToken: string) {
this.dispatcher = new WapiPrivdocDispatcher({});
this.api = PrivateDocumentsApiFp({
apiKey: `Bearer ${accessToken}`
});
}
async savePassport() {
const passport = getPassportParams();
const storedPassport = await this.dispatcher.callMethod(
this.api.storePrivateDocument,
passport
);
storedPassport.should.contain.key('token');
return storedPassport;
}
async saveRIC() {
const ric = getRICParams();
const createdRIC = await this.dispatcher.callMethod(this.api.storePrivateDocument, ric);
createdRIC.should.contain.key('token');
return createdRIC;
}
}

View File

@ -0,0 +1,116 @@
import * as chai from 'chai';
import { IdentitiesApiFp, IdentityChallenge } from '../../../api/wapi-v0/wallet/codegen';
import { getSimpleIdentityParams } from '../../../api/wapi-v0/wallet/params/identities-params/simple-identity-params';
import { WAPIDispatcher } from '../../../utils/codegen-utils';
chai.should();
export class IdentitiesActions {
private api;
private dispatcher: WAPIDispatcher;
constructor(accessToken: string) {
this.dispatcher = new WAPIDispatcher({});
this.api = IdentitiesApiFp({
apiKey: `Bearer ${accessToken}`
});
}
async createIdentity() {
const simpleIdentity = getSimpleIdentityParams();
const createdIdentity = await this.dispatcher.callMethod(
this.api.createIdentity,
simpleIdentity,
undefined
);
createdIdentity.should.contain.keys('class', 'name', 'provider');
return createdIdentity;
}
async getIdentity(identityID: string) {
const identity = await this.dispatcher.callMethod(
this.api.getIdentity,
identityID,
undefined
);
identity.should.contain.keys('name', 'provider', 'class');
return identity;
}
async listIdentityChallenges(identityID: string) {
const challenges = await this.dispatcher.callMethod(
this.api.listIdentityChallenges,
identityID,
undefined,
undefined
);
challenges.should.be.a('array');
return challenges;
}
async startIdentityChallenge(identityID: string, challenge: IdentityChallenge) {
return await this.dispatcher.callMethod(
this.api.startIdentityChallenge,
identityID,
challenge,
undefined
);
}
async getIdentityChallenge(identityID: string, challengeID: string) {
const identityChallenge = await this.dispatcher.callMethod(
this.api.getIdentityChallenge,
identityID,
challengeID,
undefined
);
identityChallenge.should.contain.keys('type', 'proofs');
return identityChallenge;
}
async listIdentities(
limit: number,
providerID?: string,
classID?: string,
levelID?: string,
continuationToken?: string
) {
const identities = await this.dispatcher.callMethod(
this.api.listIdentities,
limit,
providerID,
classID,
levelID,
continuationToken,
undefined
);
identities.should.contain.keys('result');
return identities;
}
async pollIdentityChallengeEvents(identityID: string, challengeID: string) {
const events = await this.dispatcher.callMethod(
this.api.pollIdentityChallengeEvents,
identityID,
challengeID,
1000,
undefined,
undefined
);
events.should.property('length').not.equal(0);
events[0].should.contain.keys('eventID', 'occuredAt', 'changes');
return events;
}
async getIdentityChallengeEvent(identityID: string, challengeID: string, eventID: string) {
const event = await this.dispatcher.callMethod(
this.api.getIdentityChallengeEvent,
identityID,
challengeID,
eventID,
undefined
);
event.should.contain.keys('changes', 'eventID', 'occuredAt');
return event;
}
}

View File

@ -0,0 +1,13 @@
import {
IdentityChallengeStatus,
IdentityChallengeStatusChanged
} from '../../../../api/wapi-v0/wallet/codegen';
import IdentityChallengeStatusType = IdentityChallengeStatus.StatusEnum;
export type ChangeIdentityCondition = (change: IdentityChallengeStatusChanged) => boolean;
export function isIdentityChallengeCompleted(): ChangeIdentityCondition {
return (change: IdentityChallengeStatusChanged) =>
change.status === IdentityChallengeStatusType.Completed;
}

View File

@ -0,0 +1,25 @@
import { WAPIDispatcher } from '../../../../utils/codegen-utils';
import { IdentitiesApiFp, IdentityChallengeEvent } from '../../../../api/wapi-v0/wallet/codegen';
import { EventActions } from '../../../event-actions';
export class IdentitiesEventActions extends EventActions {
private dispatcher: WAPIDispatcher;
constructor(accessToken: string) {
super();
this.dispatcher = new WAPIDispatcher({});
this.api = IdentitiesApiFp({
apiKey: `Bearer ${accessToken}`
});
}
async getEvents(...args: any[]): Promise<IdentityChallengeEvent[]> {
return await this.dispatcher.callMethod(
this.api.pollIdentityChallengeEvents,
...args,
1000,
undefined,
undefined
);
}
}

View File

@ -0,0 +1,2 @@
export * from './identities-event-actions';
export * from './conditions';

View File

@ -0,0 +1,4 @@
export * from './identities-event-actions';
export * from './identities-actions';
export * from './providers-actions';
export * from './wallets-actions';

View File

@ -0,0 +1,130 @@
import * as chai from 'chai';
import {
CurrenciesApiFp,
ProvidersApiFp,
ResidencesApiFp
} from '../../../api/wapi-v0/wallet/codegen';
import { WAPIDispatcher } from '../../../utils/codegen-utils';
chai.should();
export class ProvidersActions {
private api;
private residenceAPI;
private currenciesAPI;
private dispatcher: WAPIDispatcher;
constructor(accessToken: string) {
this.dispatcher = new WAPIDispatcher({});
this.api = ProvidersApiFp({
apiKey: `Bearer ${accessToken}`
});
this.residenceAPI = ResidencesApiFp({
apiKey: `Bearer ${accessToken}`
});
this.currenciesAPI = CurrenciesApiFp({
apiKey: `Bearer ${accessToken}`
});
}
async listProviders(residence?: string) {
const providers = await this.dispatcher.callMethod(
this.api.listProviders,
undefined,
residence
);
providers.should.property('length').not.equal(0);
providers[0].should.contain.keys('id', 'name', 'residences');
return providers;
}
async getProvider(providerID: string) {
const provider = await this.dispatcher.callMethod(
this.api.getProvider,
providerID,
undefined
);
provider.should.contain.keys('id', 'name', 'residences');
return provider;
}
async listProviderIdentityClasses(providerID: string) {
const classes = await this.dispatcher.callMethod(
this.api.listProviderIdentityClasses,
providerID,
undefined
);
classes.should.property('length').not.equal(0);
classes[0].should.contain.keys('id', 'name');
return classes;
}
async getProviderIdentityClass(providerID: string, identityClassID: string) {
const identityClass = await this.dispatcher.callMethod(
this.api.getProviderIdentityClass,
providerID,
identityClassID,
undefined
);
identityClass.should.contain.keys('id', 'name');
return identityClass;
}
async listProviderIdentityLevels(providerID: string, identityClassID: string) {
try {
const levels = await this.dispatcher.callMethod(
this.api.listProviderIdentityLevels,
providerID,
identityClassID,
undefined
);
levels.should.property('length').not.equal(0);
levels[0].should.contain.keys('id', 'name', 'challenges');
return levels;
} catch (e) {
e.status.should.equal(501);
e.statusText.should.equal('Not Implemented');
}
}
async getProviderIdentityLevel(
providerID: string,
identityClassID: string,
identityLevelID: string
) {
try {
const level = await this.dispatcher.callMethod(
this.api.getProviderIdentityLevel,
providerID,
identityClassID,
identityLevelID,
undefined
);
level.should.contain.keys('id', 'name', 'challenges');
return level;
} catch (e) {
e.status.should.equal(501);
e.statusText.should.equal('Not Implemented');
}
}
async getResidence(residenceID: string) {
const residence = await this.dispatcher.callMethod(
this.residenceAPI.getResidence,
residenceID,
undefined
);
residence.should.contain.keys('id', 'name');
return residence;
}
async getCurrency(currencyID: string) {
const currency = await this.dispatcher.callMethod(
this.currenciesAPI.getCurrency,
currencyID,
undefined
);
currency.should.contain.keys('id', 'numericCode', 'name', 'exponent');
return currency;
}
}

View File

@ -0,0 +1,87 @@
import * as chai from 'chai';
import { Wallet, WalletsApiFp } from '../../../api/wapi-v0/wallet/codegen';
import { WAPIDispatcher } from '../../../utils/codegen-utils';
import { getSimpleWalletParams } from '../../../api/wapi-v0/wallet/params/wallets-params/simple-wallet-params';
import { getWalletGrantParams } from '../../../api/wapi-v0/wallet/params/wallets-params/grant-params';
chai.should();
export class WalletsActions {
private api;
private dispatcher: WAPIDispatcher;
constructor(accessToken: string) {
this.dispatcher = new WAPIDispatcher({});
this.api = WalletsApiFp({
apiKey: `Bearer ${accessToken}`
});
}
async createNewWallet(
identity: string,
name: string = 'Test wallet name',
currency: string = 'RUB'
): Promise<Wallet> {
const walletParams = getSimpleWalletParams(identity, name, currency);
const createdWallet = await this.dispatcher.callMethod(
this.api.createWallet,
walletParams,
undefined
);
createdWallet.should.contain.keys('name', 'currency', 'identity');
return createdWallet;
}
getWallet(walletID: string): Promise<Wallet> {
return this.dispatcher.callMethod(this.api.getWallet, walletID, undefined);
}
async listWallets(identityID?: string) {
const wallets = await Promise.race([
this.pollWallets(identityID),
new Promise(res => setTimeout(res, 20000))
]);
if (wallets) {
return wallets;
}
throw new Error('list wallets polling timeout');
}
async getWalletAccount(walletID: string) {
const walletAccount = await this.dispatcher.callMethod(
this.api.getWalletAccount,
walletID,
undefined
);
walletAccount.should.contain.keys('available', 'own');
return walletAccount;
}
async issueWalletGrant(walletID: string) {
const grantParams = getWalletGrantParams();
const grant = await this.dispatcher.callMethod(
this.api.issueWalletGrant,
walletID,
grantParams,
undefined
);
grant.should.contain.keys('token', 'validUntil', 'asset');
return grant;
}
async pollWallets(identityID?: string) {
let result;
while (!result || result.length === 0) {
result = (await this.dispatcher.callMethod(
this.api.listWallets,
1000,
undefined,
identityID,
undefined,
undefined
)).result;
await new Promise(res => setTimeout(res, 3000));
}
return result;
}
}

View File

@ -0,0 +1,187 @@
import * as chai from 'chai';
import {
Destination,
WithdrawalParameters,
WithdrawalsApiFp
} from '../../../api/wapi-v0/wallet/codegen';
import { WAPIDispatcher } from '../../../utils/codegen-utils';
import { getWithdrawalGrantParams } from '../../../api/wapi-v0/wallet/params/wallets-params/grant-params';
import delay from '../../../utils/delay';
chai.should();
export class WithdrawalsActions {
private api;
private dispatcher: WAPIDispatcher;
constructor(accessToken: string) {
this.dispatcher = new WAPIDispatcher({});
this.api = WithdrawalsApiFp({
apiKey: `Bearer ${accessToken}`
});
}
async listDestinations(
limit: number,
identityID?: string,
currencyID?: string,
continuationToken?: string
) {
const destination = await this.dispatcher.callMethod(
this.api.listDestinations,
limit,
identityID,
currencyID,
continuationToken,
undefined
);
destination.should.contain.keys('result');
return destination;
}
async createDestination(destination: Destination) {
const dest = await this.dispatcher.callMethod(
this.api.createDestination,
destination,
undefined
);
dest.should.contain.keys('name', 'identity', 'currency', 'resource');
return dest;
}
async createDestinationError(destination: Destination) {
return this.dispatcher
.callMethod(this.api.createDestination, destination, undefined)
.catch(error => {
return error;
});
}
async listWithdrawals(
limit: number,
walletID?: string,
identityID?: string,
withdrawalID?: string,
destinationID?: string,
status?: string,
createdAtFrom?: Date,
createdAtTo?: Date,
amountFrom?: number,
amountTo?: number,
currencyID?: string,
continuationToken?: string
) {
const withdrawals = await this.dispatcher.callMethod(
this.api.listWithdrawals,
limit,
undefined,
walletID,
identityID,
withdrawalID,
destinationID,
status,
createdAtFrom,
createdAtTo,
amountFrom,
amountTo,
currencyID,
continuationToken
);
withdrawals.should.contain.keys('result');
return withdrawals;
}
async createWithdrawal(withdrawalParams: WithdrawalParameters) {
return await this.dispatcher.callMethod(
this.api.createWithdrawal,
withdrawalParams,
undefined
);
}
async getDestination(destinationID: string) {
const destination = await this.dispatcher.callMethod(
this.api.getDestination,
destinationID,
undefined
);
destination.should.contain.keys('name', 'identity', 'currency', 'resource');
return destination;
}
async issueDestinationGrant(destinationID: string) {
const grantParams = getWithdrawalGrantParams();
const grant = await this.dispatcher.callMethod(
this.api.issueDestinationGrant,
destinationID,
grantParams,
undefined
);
grant.should.contain.keys('token', 'validUntil');
return grant;
}
async getWithdrawal(withdrawalID: string) {
const withdrawal = await this.dispatcher.callMethod(
this.api.getWithdrawal,
withdrawalID,
undefined
);
withdrawal.should.contain.keys('wallet', 'destination', 'body');
return withdrawal;
}
async getWithdrawalByExternal(externalID: string) {
const withdrawal = await this.dispatcher.callMethod(
this.api.getWithdrawalByExternalID,
externalID,
undefined
);
withdrawal.should.contain.keys('wallet', 'destination', 'body', 'externalID');
return withdrawal;
}
async pollWithdrawalEvents(withdrawalID: string) {
const events = await this.dispatcher.callMethod(
this.api.pollWithdrawalEvents,
withdrawalID,
1000,
undefined,
undefined
);
events.should.property('length').not.equal(0);
events[0].should.contain.keys('eventID', 'occuredAt', 'changes');
return events;
}
async getWithdrawalEvents(withdrawalID: string, eventID: string) {
const event = await this.dispatcher.callMethod(
this.api.getWithdrawalEvents,
withdrawalID,
eventID,
undefined
);
event.should.contain.keys('changes', 'eventID', 'occuredAt');
return event;
}
async waitDestinationCreate(destinationID: string) {
let result = await Promise.race([
new Promise(res => setTimeout(res, 10000)),
this.pollDestinationCreation(destinationID)
]);
if (result) {
return result;
}
throw 'poll destination create timeout';
}
async pollDestinationCreation(destinationID: string) {
let isCreated;
while (!isCreated) {
isCreated = (await this.getDestination(destinationID)).id;
await delay(1500);
}
return isCreated;
}
}

View File

@ -0,0 +1,10 @@
import { WithdrawalStatus } from '../../../../api/wapi-v0/wallet/codegen';
import WithdrawalStatusType = WithdrawalStatus.StatusEnum;
export type ChangeWithdrawalCondition = (change: WithdrawalStatus) => boolean;
// TODO: fix failed withdrawal
export function isWithdrawalSucceeded(): ChangeWithdrawalCondition {
return (change: WithdrawalStatus) => change.status === WithdrawalStatusType.Failed;
}

View File

@ -0,0 +1,2 @@
export * from './withdrawals-event-actions';
export * from './conditions';

View File

@ -0,0 +1,25 @@
import { WAPIDispatcher } from '../../../../utils/codegen-utils';
import { WithdrawalEvent, WithdrawalsApiFp } from '../../../../api/wapi-v0/wallet/codegen';
import { EventActions } from '../../../event-actions';
export class WithdrawalsEventActions extends EventActions {
private dispatcher: WAPIDispatcher;
constructor(accessToken: string) {
super();
this.dispatcher = new WAPIDispatcher({});
this.api = WithdrawalsApiFp({
apiKey: `Bearer ${accessToken}`
});
}
async getEvents(...args: any[]): Promise<WithdrawalEvent[]> {
return await this.dispatcher.callMethod(
this.api.pollWithdrawalEvents,
...args,
1000,
undefined,
undefined
);
}
}

View File

@ -0,0 +1,33 @@
import * as chai from 'chai';
import { authEndpoint } from '../../settings';
import request = require('request');
chai.should();
export function getAccessToken(
realm: string,
login: string,
password: string,
clientID: string
): Promise<string> {
return new Promise((resolve, reject) => {
request.post(
{
url: `${authEndpoint}/auth/realms/${realm}/protocol/openid-connect/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Host: process.env.AUTH_HOST
},
body: `username=${login}&password=${password}&client_id=${clientID}&grant_type=password`
},
(err, response, body) => {
if (err) {
reject(err);
} else {
response.statusCode.should.eq(200);
resolve(JSON.parse(body).access_token);
}
}
);
});
}

2
api/capi-v2/index.ts Normal file
View File

@ -0,0 +1,2 @@
export * from './codegen/api';
export * from './params';

View File

@ -0,0 +1,184 @@
import * as moment from 'moment';
import {
PartyModification,
ContractModification,
ContractCreation,
Contractor,
LegalEntity,
BankAccount,
ContractPayoutToolCreation,
PayoutToolDetails,
PayoutToolDetailsBankAccount,
ContractLegalAgreementBinding,
LegalAgreement,
ShopModification,
ShopCreation,
ShopCategoryChange,
ShopPayoutScheduleChange,
ShopAccountCreation,
ShopLocationUrl,
ShopDetails,
PayoutToolDetailsWalletInfo
} from '../../codegen';
import PartyModificationType = PartyModification.PartyModificationTypeEnum;
import ContractModificationType = ContractModification.ContractModificationTypeEnum;
import ShopModificationType = ShopModification.ShopModificationTypeEnum;
/* Contract changes and defaults */
export const defaultContractor = {
contractorType: Contractor.ContractorTypeEnum.LegalEntity,
entityType: LegalEntity.EntityTypeEnum.RussianLegalEntity,
registeredName: 'ООО Иванов Иван Иванович',
registeredNumber: '1117800008336',
inn: '7840290139',
actualAddress:
'191040, г Санкт-Петербург, Центральный р-н, Лиговский пр-кт, д 87 стр а, оф 15Н',
postAddress: '191040, г Санкт-Петербург, Центральный р-н, Лиговский пр-кт, д 87 стр а, оф 509',
representativePosition: 'Директор',
representativeFullName: 'Кочетков Игорь Викторович',
representativeDocument:
'паспорт 4012688115, 28.02.2013, ТП №71 отдела УФМС России по Санкт-Петербургу и Ленинградской обл. в Пушкинском р-не гор. Санкт-Петербурга',
bankAccount: {
account: '40703810432060000034',
bankName: 'ФИЛИАЛ "САНКТ-ПЕТЕРБУРГСКИЙ" АО "АЛЬФА-БАНК"',
bankPostAccount: '30101810600000000786',
bankBik: '044030786'
} as BankAccount
} as Contractor;
export function contractCreationChange(
contractID: string,
paymentInstitutionID: number,
contractor?: Contractor
): ContractCreation {
return {
partyModificationType: PartyModificationType.ContractModification,
contractID: contractID,
contractModificationType: ContractModificationType.ContractCreation,
paymentInstitutionID: paymentInstitutionID,
contractor: contractor || defaultContractor
} as ContractCreation;
}
export const defaultPayoutToolDetails = {
detailsType: 'PayoutToolDetailsBankAccount',
account: '40703810432060000034',
bankName: 'ФИЛИАЛ "САНКТ-ПЕТЕРБУРГСКИЙ" АО "АЛЬФА-БАНК"',
bankPostAccount: '30101810600000000786',
bankBik: '044030786'
} as PayoutToolDetailsBankAccount;
export function contractPayoutToolCreationChange(
contractID: string,
payoutToolID: string,
currency: string,
details?: PayoutToolDetails
): ContractPayoutToolCreation {
return {
partyModificationType: PartyModificationType.ContractModification,
contractID: contractID,
contractModificationType: ContractModificationType.ContractPayoutToolCreation,
payoutToolID: payoutToolID,
currency: currency,
details: details || defaultPayoutToolDetails
} as ContractPayoutToolCreation;
}
export function getDefaultWalletInfo(walletID?: string): PayoutToolDetailsWalletInfo {
return {
detailsType: 'PayoutToolDetailsWalletInfo',
walletID: walletID || '1'
};
}
export function contractPayoutToolWalletCreationChange(
contractID: string,
payoutToolID: string,
currency: string,
walletID?: string
): ContractPayoutToolCreation {
return {
partyModificationType: PartyModificationType.ContractModification,
contractID: contractID,
contractModificationType: ContractModificationType.ContractPayoutToolCreation,
payoutToolID: payoutToolID,
currency: currency,
details: getDefaultWalletInfo(walletID)
} as ContractPayoutToolCreation;
}
export const defaultLegalAgreement = {
id: '006815/07',
signedAt: moment()
.subtract(1, 'days')
.utc()
.format() as any
} as LegalAgreement;
export function contractLegalAgreementBindingChange(
contractID: string,
legalAgreement?: LegalAgreement
): ContractLegalAgreementBinding {
return {
partyModificationType: PartyModificationType.ContractModification,
contractID: contractID,
contractModificationType: ContractModificationType.ContractLegalAgreementBinding,
legalAgreement: legalAgreement || defaultLegalAgreement
} as ContractLegalAgreementBinding;
}
/* Shop changes and defaults */
export function shopCreationChange(
shopID: string,
contractID: string,
payoutToolID: string
): ShopCreation {
return {
partyModificationType: PartyModificationType.ShopModification,
shopID: shopID,
shopModificationType: ShopModificationType.ShopCreation,
location: {
locationType: 'ShopLocationUrl',
url: 'http://test.url'
} as ShopLocationUrl,
details: {
name: 'Test shop',
description: 'Shop for test integration'
} as ShopDetails,
contractID: contractID,
payoutToolID: payoutToolID
} as ShopCreation;
}
export function shopCategoryChange(shopID: string, categoryID: number): ShopCategoryChange {
return {
partyModificationType: PartyModificationType.ShopModification,
shopID: shopID,
shopModificationType: ShopModificationType.ShopCategoryChange,
categoryID: categoryID
} as ShopCategoryChange;
}
export function shopAccountCreationChange(shopID: string, currency: string): ShopAccountCreation {
return {
partyModificationType: PartyModificationType.ShopModification,
shopID: shopID,
shopModificationType: ShopModificationType.ShopAccountCreation,
currency: currency
} as ShopAccountCreation;
}
export function shopPayoutScheduleChange(
shopID: string,
scheduleID: number | undefined
): ShopPayoutScheduleChange {
return {
partyModificationType: PartyModificationType.ShopModification,
shopModificationType: ShopModificationType.ShopPayoutScheduleChange,
shopID,
scheduleID
};
}

View File

@ -0,0 +1,39 @@
import guid from '../../../../utils/guid';
import {
contractCreationChange,
contractPayoutToolCreationChange,
contractLegalAgreementBindingChange,
shopCreationChange,
shopCategoryChange,
shopAccountCreationChange,
contractPayoutToolWalletCreationChange
} from './default-claim-params';
import { ClaimChangeset } from '../../codegen';
export function liveShopClaimChangeset(id?: string): ClaimChangeset {
const liveShopID = id || guid();
const liveContractID = id || guid();
const livePayoutToolID = id || guid();
return [
contractCreationChange(liveContractID, 2),
contractPayoutToolCreationChange(liveContractID, livePayoutToolID, 'RUB'),
contractLegalAgreementBindingChange(liveContractID),
shopCreationChange(liveShopID, liveContractID, livePayoutToolID),
shopCategoryChange(liveShopID, 2),
shopAccountCreationChange(liveShopID, 'RUB')
] as ClaimChangeset;
}
export function createPayoutToolClaimChangeset(liveContractID: string): ClaimChangeset {
return [contractPayoutToolCreationChange(liveContractID, guid(), 'RUB')] as ClaimChangeset;
}
export function createWalletPayoutToolClaimChangeset(
liveContractID: string,
walletID: string
): ClaimChangeset {
return [
contractPayoutToolWalletCreationChange(liveContractID, guid(), 'RUB', walletID)
] as ClaimChangeset;
}

View File

@ -0,0 +1,25 @@
import guid from '../../../../utils/guid';
import {
contractCreationChange,
contractPayoutToolCreationChange,
contractLegalAgreementBindingChange,
shopCreationChange,
shopCategoryChange,
shopAccountCreationChange
} from './default-claim-params';
import { ClaimChangeset } from '../../codegen';
export function testShopClaimChangeset(id?: string): ClaimChangeset {
const testShopID = id || guid();
const testContractID = id || guid();
const testPayoutToolID = id || guid();
return [
contractCreationChange(testContractID, 1),
contractPayoutToolCreationChange(testContractID, testPayoutToolID, 'RUB'),
contractLegalAgreementBindingChange(testContractID),
shopCreationChange(testShopID, testContractID, testPayoutToolID),
shopCategoryChange(testShopID, 1),
shopAccountCreationChange(testShopID, 'RUB')
] as ClaimChangeset;
}

View File

@ -0,0 +1,13 @@
export * from './invoice-params/simple-invoice-params';
export * from './payment-params/payment-params';
export * from './payment-params/payer-params';
export * from './payment-params/refund-params';
export * from './payment-tools/qiwi-payment-tool';
export * from './payment-tools/crypto-payment-tool';
export * from './payment-tools/sane-visa-payment-tool';
export * from './payment-tools/insufficient-funds-visa-tool';
export * from './payment-tools/secure-visa-payment-tool';
export * from './payment-tools/secure-visa-empty-cvv-payment-tool';
export * from './claim-params/test-shop-claim-params';
export * from './claim-params/live-shop-claim-params';
export * from './webhook/params';

View File

@ -0,0 +1,67 @@
import * as moment from 'moment';
import { InvoiceLineTaxMode, InvoiceLineTaxVAT, InvoiceParams } from '../../codegen';
import { Invoice, InvoiceParamsWithTemplate } from '../../index';
export function simpleInvoiceParams(shopID: string, params?: {}): InvoiceParams {
return {
shopID: shopID,
dueDate: moment()
.add(1, 'days')
.utc()
.format() as any,
amount: 10000,
currency: 'RUB',
product: 'Test product',
description: 'Test product description',
cart: [
{
product: 'Product 1',
quantity: 1,
price: 10000,
taxMode: {
type: InvoiceLineTaxMode.TypeEnum.InvoiceLineTaxVAT,
rate: InvoiceLineTaxVAT.RateEnum._10
} as InvoiceLineTaxVAT
}
],
metadata: {
test: 1
},
...params
} as InvoiceParams;
}
export function invoiceParamsWithTemplate(params?: {}): InvoiceParamsWithTemplate {
return {
amount: 10000,
currency: 'RUB',
metadata: { test: 1 },
...params
};
}
export function assertSimpleInvoice(invoice: Invoice, amount: number, shopID: string) {
invoice.should.to.include({
amount,
currency: 'RUB',
description: 'Test product description',
product: 'Test product',
shopID: shopID,
status: 'unpaid'
});
invoice.cart.should.to.deep.equal([
{
cost: amount,
price: amount,
product: 'Product 1',
quantity: 1,
taxMode: {
type: 'InvoiceLineTaxVAT',
rate: '10%'
}
}
]);
invoice.metadata.should.to.deep.equal({ test: 1 });
invoice.should.to.have.property('createdAt').to.be.a('string');
invoice.should.to.have.property('id').to.be.a('string');
}

View File

@ -0,0 +1,83 @@
import { InvoiceLineTaxVAT } from '../../codegen';
import { Invoice, InvoiceTemplate, InvoiceTemplateCreateParams } from '../../index';
export function simpleInvoiceTemplateParams(
shopID: string,
params?: {}
): InvoiceTemplateCreateParams {
return {
shopID,
product: 'Test product',
description: 'Test product description',
lifetime: {
days: 3,
months: 0,
years: 0
},
details: {
templateType: 'InvoiceTemplateMultiLine',
cart: [
{
product: 'Product 1',
quantity: 1,
price: 10000,
taxMode: {
type: 'InvoiceLineTaxVAT',
rate: '10%'
}
}
],
currency: 'RUB'
},
metadata: { test: 1 },
...params
} as InvoiceTemplateCreateParams;
}
export function assertSimpleInvoiceTemplate(
invoiceTemplate: InvoiceTemplate,
shopID: string,
assertParams?: {}
) {
invoiceTemplate.should.to.include({
shopID: shopID,
description: 'Test product description',
...assertParams
});
invoiceTemplate.lifetime.should.to.deep.equal({
days: 3,
months: 0,
years: 0
});
invoiceTemplate.details.should.to.include({
templateType: 'InvoiceTemplateMultiLine'
});
invoiceTemplate.should.to.have.property('id').to.be.a('string');
invoiceTemplate.metadata.should.to.deep.equal({ test: 1 });
}
export function assertSimpleInvoiceWithTemplate(invoice: Invoice, amount: number, shopID: string) {
invoice.should.to.include({
amount,
currency: 'RUB',
description: 'Test product description',
product: 'Product 1',
shopID: shopID,
status: 'unpaid'
});
invoice.cart.should.to.deep.equal([
{
cost: amount,
price: amount,
product: 'Product 1',
quantity: 1,
taxMode: {
type: 'InvoiceLineTaxVAT',
rate: '10%'
}
}
]);
invoice.metadata.should.to.deep.equal({ test: 1 });
invoice.should.to.have.property('createdAt').to.be.a('string');
invoice.should.to.have.property('id').to.be.a('string');
}

View File

@ -0,0 +1,69 @@
import {
PaymentResource,
PaymentResourcePayer,
PaymentRecurrentParent,
RecurrentPayer,
CustomerPayer,
Payer
} from '../../codegen';
export function paymentResourcePayer(payload: PaymentResource): PaymentResourcePayer {
return {
payerType: 'PaymentResourcePayer',
paymentToolToken: payload.paymentToolToken,
paymentSession: payload.paymentSession,
contactInfo: {
email: 'user@example.com'
}
} as PaymentResourcePayer;
}
export function recurrentPayer(payload: PaymentRecurrentParent): RecurrentPayer {
return {
payerType: 'RecurrentPayer',
recurrentParentPayment: payload,
contactInfo: {
email: 'user@example.com'
}
} as RecurrentPayer;
}
export function assertPayer(payer: Payer) {
switch (payer.payerType) {
case 'PaymentResourcePayer':
payer.should.to.deep.include({
contactInfo: {
email: 'user@example.com'
}
});
payer.should.to.have.property('paymentSession').to.be.a('string');
payer.should.to.have.property('paymentToolToken').to.be.a('string');
payer.should.to.have.property('paymentToolDetails').to.be.a('object');
payer.should.to.have.property('contactInfo').to.be.a('object');
payer.should.to.have.property('clientInfo').to.be.a('object');
return;
case 'RecurrentPayer':
payer.should.to.deep.include({
contactInfo: {
email: 'user@example.com'
}
});
payer.should.to.have.property('contactInfo').to.be.a('object');
payer.should.to.have.property('paymentToolToken').to.be.a('string');
payer.should.to.have.property('paymentToolDetails').to.be.a('object');
const parent = (payer as RecurrentPayer).recurrentParentPayment;
parent.should.to.have.property('invoiceID').to.be.a('string');
parent.should.to.have.property('paymentID').to.be.a('string');
return;
case 'CustomerPayer':
payer.should.to.deep.include({
contactInfo: {
email: 'user@example.com'
}
});
payer.should.to.have.property('customerID').to.be.a('string');
payer.should.to.have.property('paymentToolToken').to.be.a('string');
payer.should.to.have.property('paymentToolDetails').to.be.a('object');
return;
}
}

View File

@ -0,0 +1,72 @@
import { PaymentFlow, PaymentParams, CaptureParams, Payer } from '../../codegen';
import TypeEnum = PaymentFlow.TypeEnum;
import { Payment } from '../../';
import { assertPayer } from './payer-params';
function getFlow(paymentType: TypeEnum, holdType?: string) {
switch (paymentType) {
case TypeEnum.PaymentFlowHold:
return {
type: paymentType,
onHoldExpiration: holdType === undefined ? 'capture' : holdType
};
case TypeEnum.PaymentFlowInstant:
default:
return {
type: paymentType
};
}
}
export function paymentParams(
payer: Payer,
paymentType: TypeEnum,
makeRecurrent: boolean,
holdType?: string,
externalID?: string,
metadata?: object
): PaymentParams {
return {
externalID,
flow: getFlow(paymentType, holdType),
payer: payer,
makeRecurrent: makeRecurrent,
metadata: metadata
};
}
export function captureParams(amount?: number): CaptureParams {
return amount === undefined
? {
reason: 'testCapture'
}
: {
reason: 'testCapture',
amount: amount,
currency: 'RUB'
};
}
export function assertPayment(
payment: Payment,
amount: number,
paymentType: TypeEnum,
holdType?: string,
metadata?: object
) {
payment.should.to.include({
amount: amount,
currency: 'RUB'
});
if (metadata != undefined) {
payment.should.have.deep.property('metadata', metadata);
}
payment.flow.should.to.include(getFlow(paymentType, holdType));
payment.should.to.have.property('status').to.be.a('string');
payment.should.to.have.property('id').to.be.a('string');
payment.should.to.have.property('createdAt').to.be.a('string');
payment.should.to.have.property('invoiceID').to.be.a('string');
payment.should.to.have.property('payer').to.be.a('object');
payment.should.to.have.property('makeRecurrent').to.be.a('boolean');
assertPayer(payment.payer);
}

View File

@ -0,0 +1,14 @@
import { RefundParams } from '../../codegen';
export function refundParams(
amount?: number,
currency?: string,
externalID?: string
): RefundParams {
return {
amount: amount,
currency: currency,
reason: 'Some reason',
externalID: externalID
};
}

View File

@ -0,0 +1,14 @@
import { CardData, PaymentTool, PaymentResourceParams } from '../../codegen';
export const badCardholderPaymentTool = {
paymentTool: {
paymentToolType: PaymentTool.PaymentToolTypeEnum.CardData,
cardNumber: '4242424242424242',
expDate: '12/20',
cvv: '123',
cardHolder: 'ЛЕХА СВОТИН 123'
} as CardData,
clientInfo: {
fingerprint: '316a2eee53ea181b3deecb70691021ce'
}
} as PaymentResourceParams;

View File

@ -0,0 +1,11 @@
import { CryptoWalletData, PaymentTool, PaymentResourceParams } from '../../codegen';
export const cryptoPaymentTool = {
paymentTool: {
paymentToolType: PaymentTool.PaymentToolTypeEnum.CryptoWalletData,
cryptoCurrency: 'bitcoinCash'
} as CryptoWalletData,
clientInfo: {
fingerprint: 'dc8280558372bd072521cff0f178aa1c'
}
} as PaymentResourceParams;

View File

@ -0,0 +1,14 @@
import { CardData, PaymentTool, PaymentResourceParams } from '../../codegen';
export const insufficientFundsVisaTool = {
paymentTool: {
paymentToolType: PaymentTool.PaymentToolTypeEnum.CardData,
cardNumber: '4000000000000002',
expDate: '12/20',
cvv: '123',
cardHolder: 'LEXA SVOTIN'
} as CardData,
clientInfo: {
fingerprint: '316a2eee53ea181b3deecb70691021ce'
}
} as PaymentResourceParams;

View File

@ -0,0 +1,17 @@
import {
DigitalWalletData,
DigitalWalletQIWI,
PaymentTool,
PaymentResourceParams
} from '../../codegen';
export const qiwiPaymentTool = {
paymentTool: {
paymentToolType: PaymentTool.PaymentToolTypeEnum.DigitalWalletData,
digitalWalletType: DigitalWalletData.DigitalWalletTypeEnum.DigitalWalletQIWI,
phoneNumber: '+7911111111'
} as DigitalWalletQIWI,
clientInfo: {
fingerprint: '71aadcee86e140f794924855f5e48aa9'
}
} as PaymentResourceParams;

View File

@ -0,0 +1,14 @@
import { CardData, PaymentTool, PaymentResourceParams } from '../../codegen';
export const saneVisaPaymentTool = {
paymentTool: {
paymentToolType: PaymentTool.PaymentToolTypeEnum.CardData,
cardNumber: '4242424242424242',
expDate: '12/20',
cvv: '123',
cardHolder: 'LEXA SVOTIN'
} as CardData,
clientInfo: {
fingerprint: '316a2eee53ea181b3deecb70691021ce'
}
} as PaymentResourceParams;

View File

@ -0,0 +1,13 @@
import { CardData, PaymentTool, PaymentResourceParams } from '../../codegen';
export const secureEmptyCVVVisaPaymentTool = {
paymentTool: {
paymentToolType: PaymentTool.PaymentToolTypeEnum.CardData,
cardNumber: '4012888888881881',
expDate: '12/20',
cardHolder: 'LEXA SVOTIN'
} as CardData,
clientInfo: {
fingerprint: '316a2eee53ea181b3deecb70691021ce'
}
} as PaymentResourceParams;

View File

@ -0,0 +1,14 @@
import { CardData, PaymentTool, PaymentResourceParams } from '../../codegen';
export const secureVisaPaymentTool = {
paymentTool: {
paymentToolType: PaymentTool.PaymentToolTypeEnum.CardData,
cardNumber: '4012888888881881',
expDate: '12/20',
cvv: '123',
cardHolder: 'LEXA SVOTIN'
} as CardData,
clientInfo: {
fingerprint: '316a2eee53ea181b3deecb70691021ce'
}
} as PaymentResourceParams;

View File

@ -0,0 +1,13 @@
import { PaymentTerminalData, PaymentTool, PaymentResourceParams } from '../../codegen';
import { PaymentTerminalDetails } from '../../';
import ProviderEnum = PaymentTerminalDetails.ProviderEnum;
export const terminalPaymentTool = {
paymentTool: {
paymentToolType: PaymentTool.PaymentToolTypeEnum.PaymentTerminalData,
provider: ProviderEnum.Euroset
} as PaymentTerminalData,
clientInfo: {
fingerprint: '316a2eee53ea181b3deecb70691021ce'
}
} as PaymentResourceParams;

View File

@ -0,0 +1,46 @@
import { Webhook, WebhookScope } from '../../codegen';
import * as request from 'request';
import { testWebhookReceiverEndpoint, testWebhookReceiverInternal } from '../../../../settings';
export function webhookParams(scope: WebhookScope, testId: string): Webhook {
return {
scope: scope,
url: `${testWebhookReceiverInternal}/hooker/${testId}`
} as Webhook;
}
export function getEvents(testId: string): Promise<string> {
return new Promise((resolve, reject) => {
request.get(
{
url: `${testWebhookReceiverEndpoint}/search/get/${testId}`
},
(err, response) => {
if (err) {
reject(err);
} else {
response.statusCode.should.eq(200);
resolve(response.body);
}
}
);
});
}
export function countEvents(testId: string): Promise<number> {
return new Promise((resolve, reject) => {
request.get(
{
url: `${testWebhookReceiverEndpoint}/search/count/${testId}`
},
(err, response) => {
if (err) {
reject(err);
} else {
response.statusCode.should.eq(200);
resolve(JSON.parse(response.body).count);
}
}
);
});
}

5
api/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './auth/get-access-token';
export * from './capi-v2';
// @ts-ignore
export * from './papi-v1';
export * from './url-shortener-v1';

View File

@ -0,0 +1,16 @@
import request = require('request');
import { Authentication } from './authentication';
export class ApiKeyAuth implements Authentication {
public apiKey: string;
constructor(private location: string, private paramName: string) {}
applyToRequest(requestOptions: request.Options): void {
if (this.location == 'query') {
(<any>requestOptions.qs)[this.paramName] = this.apiKey;
} else if (this.location == 'header' && requestOptions && requestOptions.headers) {
requestOptions.headers[this.paramName] = this.apiKey;
}
}
}

View File

@ -0,0 +1,8 @@
import request = require('request');
export interface Authentication {
/**
* Apply authentication settings to header and query params.
*/
applyToRequest(requestOptions: request.Options): void;
}

View File

@ -0,0 +1,3 @@
export enum ClaimsApiApiKeys {
bearer
}

132
api/papi-v1/claims-api.ts Normal file
View File

@ -0,0 +1,132 @@
import request = require('request');
import http = require('http');
import * as chai from 'chai';
import guid from '../../utils/guid';
import { PapiForTests } from './papi-for-tests';
import { Authentication } from './authentication';
import { VoidAuth } from './void-auth';
import { ApiKeyAuth } from './api-key-auth';
import { ClaimsApiApiKeys } from './claims-api-api-keys';
import { handleResponseError } from '../../utils';
chai.should();
export class ClaimsApiForTests implements PapiForTests {
public defaultHeaders: any = {};
public basePath: string = '';
public useQuerystring: boolean = false;
protected authentications = {
default: <Authentication>new VoidAuth(),
bearer: new ApiKeyAuth('header', 'Authorization')
};
constructor(basePath: string) {
this.basePath = basePath;
}
public setDefaultAuthentication(auth: Authentication) {
this.authentications.default = auth;
}
public setApiKey(key: ClaimsApiApiKeys, value: string) {
this.authentications[ClaimsApiApiKeys[key]].apiKey = value;
}
acceptClaimByIDForTests(
partyID: string,
claimID: number,
claimRevision: number
): Promise<void> {
return this.acceptClaimByID(guid(), partyID, claimID, claimRevision)
.then(result => {
const response = result.response;
response.statusCode.should.eq(200);
return;
})
.catch(ex => handleResponseError(ex));
}
private acceptClaimByID(
xRequestID: string,
partyID: string,
claimID: number,
claimRevision: number
): Promise<{ response: http.ClientResponse; body?: any }> {
const localVarPath = this.basePath + '/walk/claim/accept';
let queryParameters: any = {};
let headerParams: any = (<any>Object).assign({}, this.defaultHeaders);
let formParams: any = {};
// verify required parameter 'xRequestID' is not null or undefined
if (xRequestID === null || xRequestID === undefined) {
throw new Error(
'Required parameter xRequestID was null or undefined when calling acceptClaimByID.'
);
}
// verify required parameter 'partyID' is not null or undefined
if (partyID === null || partyID === undefined) {
throw new Error(
'Required parameter partyID was null or undefined when calling acceptClaimByID.'
);
}
// verify required parameter 'claimID' is not null or undefined
if (claimID === null || claimID === undefined) {
throw new Error(
'Required parameter claimID was null or undefined when calling acceptClaimByID.'
);
}
// verify required parameter 'claimRevision' is not null or undefined
if (claimRevision === null || claimRevision === undefined) {
throw new Error(
'Required parameter claimRevision was null or undefined when calling acceptClaimByID.'
);
}
headerParams['X-Request-ID'] = xRequestID;
let useFormData = false;
let requestOptions: request.Options = {
method: 'POST',
qs: queryParameters,
headers: headerParams,
uri: localVarPath,
useQuerystring: this.useQuerystring,
json: true,
body: {
claimId: claimID,
partyId: partyID,
revision: claimRevision
}
};
this.authentications.bearer.applyToRequest(requestOptions);
this.authentications.default.applyToRequest(requestOptions);
if (Object.keys(formParams).length) {
if (useFormData) {
(<any>requestOptions).formData = formParams;
} else {
requestOptions.form = formParams;
}
}
return new Promise<{ response: http.ClientResponse; body?: any }>((resolve, reject) => {
request(requestOptions, (error, response, body) => {
if (error) {
reject(error);
} else {
if (response.statusCode >= 200 && response.statusCode <= 299) {
resolve({ response: response, body: body });
} else {
reject({ response: response, body: body });
}
}
});
});
}
}

5
api/papi-v1/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './claims-api';
export * from './payouts-api';
export * from './payout';
export * from './papi-factory';
export * from './papi-for-tests';

View File

@ -0,0 +1,21 @@
import { ClaimsApiForTests, PapiForTests, PayoutsApiForTests } from '.';
import { adminEndpoint } from '../../settings';
const classes = {
ClaimsApiForTests,
PayoutsApiForTests
};
export class PapiFactory {
public static getInstance<T extends PapiForTests>(className: string, accessToken: string): T {
let instance = classes[className] && new classes[className]();
if (instance) {
instance.setApiKey(0, `Bearer ${accessToken}`);
instance.defaultHeaders = {
'Content-Type': 'application/json; charset=utf-8'
};
instance.basePath = `${adminEndpoint}/papi/v1`;
}
return instance;
}
}

View File

@ -0,0 +1,3 @@
export interface PapiForTests {
defaultHeaders: any;
}

9
api/papi-v1/payout.ts Normal file
View File

@ -0,0 +1,9 @@
export class Payout {
id: string;
partyId: string;
shopId: string;
payoutType: string;
createdAt: Date;
amount: number;
currency: string;
}

140
api/papi-v1/payouts-api.ts Normal file
View File

@ -0,0 +1,140 @@
import request = require('request');
import http = require('http');
import { PapiForTests } from './papi-for-tests';
import { Payout } from './payout';
import * as chai from 'chai';
import { Authentication } from './authentication';
import { VoidAuth } from './void-auth';
import { ApiKeyAuth } from './api-key-auth';
import { ClaimsApiApiKeys } from './claims-api-api-keys';
import { handleResponseError } from '../../utils';
chai.should();
export class PayoutsApiForTests implements PapiForTests {
public defaultHeaders: any = {};
public basePath: string = '';
public useQuerystring: boolean = false;
protected authentications = {
default: <Authentication>new VoidAuth(),
bearer: new ApiKeyAuth('header', 'Authorization')
};
constructor(basePath: string) {
this.basePath = basePath;
}
private getDefaultHeaders() {
const headers = new Headers();
headers.append('Content-type', 'application/json; charset=UTF-8');
headers.append('Authorization', this.authentications.bearer.apiKey);
return headers;
}
public setDefaultAuthentication(auth: Authentication) {
this.authentications.default = auth;
}
public setApiKey(key: ClaimsApiApiKeys, value: string) {
this.authentications[ClaimsApiApiKeys[key]].apiKey = value;
}
public pay(payoutIds: string[]) {
const params = {
method: 'POST',
headers: this.getDefaultHeaders(),
body: JSON.stringify({ payoutIds })
};
return fetch(`${this.basePath}/payouts/pay`, params)
.then(response => {
if (response.status === 200) {
return response;
} else {
return handleResponseError(response);
}
})
.catch(ex => handleResponseError(ex));
}
public confirmPayouts(payoutIds: string[]) {
const params = {
method: 'POST',
headers: this.getDefaultHeaders(),
body: JSON.stringify({ payoutIds })
};
return fetch(`${this.basePath}/payouts/confirm`, params)
.then(response => {
if (response.status === 200) {
return response;
} else {
return handleResponseError(response);
}
})
.catch(ex => handleResponseError(ex));
}
public searchPayoutsForTests(fromTime?: Date, toTime?: Date): Promise<Payout[]> {
return this.searchPayouts(fromTime, toTime)
.then(http => {
const response = http.response;
response.statusCode.should.eq(200);
return http.body.payouts;
})
.catch(ex => handleResponseError(ex));
}
public searchPayouts(
fromTime?: Date,
toTime?: Date
): Promise<{ response: http.ClientResponse; body: any }> {
const localVarPath = this.basePath + '/payouts';
let queryParameters: any = {};
let headerParams: any = (<any>Object).assign({}, this.defaultHeaders);
let formParams: any = {};
if (fromTime !== undefined) {
queryParameters['fromTime'] = fromTime;
}
if (toTime !== undefined) {
queryParameters['toTime'] = toTime;
}
let useFormData = false;
let requestOptions: request.Options = {
method: 'GET',
qs: queryParameters,
headers: headerParams,
uri: localVarPath,
useQuerystring: this.useQuerystring,
json: true
};
this.authentications.bearer.applyToRequest(requestOptions);
this.authentications.default.applyToRequest(requestOptions);
if (Object.keys(formParams).length) {
if (useFormData) {
(<any>requestOptions).formData = formParams;
} else {
requestOptions.form = formParams;
}
}
return new Promise<{ response: http.ClientResponse; body: any }>((resolve, reject) => {
request(requestOptions, (error, response, body) => {
if (error) {
reject(error);
} else {
if (response.statusCode >= 200 && response.statusCode <= 299) {
resolve({ response: response, body: body });
} else {
reject({ response: response, body: body });
}
}
});
});
}
}

11
api/papi-v1/void-auth.ts Normal file
View File

@ -0,0 +1,11 @@
import request = require('request');
import { Authentication } from './authentication';
export class VoidAuth implements Authentication {
public username: string;
public password: string;
applyToRequest(_: request.Options): void {
// Do nothing
}
}

29
api/proxy-api.ts Normal file
View File

@ -0,0 +1,29 @@
import guid from '../utils/guid';
import * as request from 'request';
import { proxyEndpoint } from '../settings';
export class ProxyApiForTests {
constructor(private accessToken: string) {}
public payTerminalPayment(spid: string, amount: number): Promise<void> {
return new Promise((resolve, reject) => {
const params = this.defaultParams(spid, amount);
request.get(params, (err, response) =>
response.body.indexOf('ERR_CD=0') !== -1 ? resolve() : reject('ERR_CD !== 0')
);
});
}
private defaultParams(spid: string, amount: number) {
return {
url: `${proxyEndpoint}/agent/api/payment?VERSION=2.01&DSTACNT_NR=${spid}&ACT_CD=1&TR_AMT=${amount /
100}.00&CUR_CD=643&TR_NR=123`,
json: true,
headers: {
'Content-Type': 'application/json;charset=utf-8',
Authorization: `Bearer ${this.accessToken}`,
'X-Request-ID': guid()
}
};
}
}

View File

@ -0,0 +1 @@
export * from './short-urls';

View File

@ -0,0 +1,38 @@
import * as chai from 'chai';
import guid from '../../utils/guid';
import { urlShortenerEndpoint } from '../../settings';
import { ShortenerApiFp, ShortenedUrl } from './codegen';
chai.should();
export class ShortUrls {
private defaultOptions = {
headers: {
'Content-Type': 'application/json; charset=utf-8'
}
};
private basePath = `${urlShortenerEndpoint}/v1`;
private api;
constructor(accessToken: string) {
this.api = ShortenerApiFp({
apiKey: `Bearer ${accessToken}`
});
}
shortenUrl(sourceUrl: string, expiresAt: any): Promise<ShortenedUrl> {
return this.api.shortenUrl(guid(), { sourceUrl, expiresAt }, this.defaultOptions)(
undefined,
this.basePath
);
}
deleteShortenedUrl(id: string): Promise<void> {
return this.api.deleteShortenedUrl(guid(), id, this.defaultOptions)(
undefined,
this.basePath
);
}
}

View File

@ -0,0 +1,35 @@
import { BankCard } from '../codegen';
import {
BankCardDestinationResource,
Destination,
DestinationResource
} from '../../wallet/codegen';
import BankCardType = BankCard.TypeEnum;
import DestinationResourceType = DestinationResource.TypeEnum;
export function getBankCardParams(): BankCard {
return {
type: BankCardType.BankCard,
cardNumber: '4242424242424242',
expDate: '12/21',
cardHolder: 'LEXA SVOTIN',
cvv: '123'
};
}
export function getDestinationParams(
identity: string,
destinationResource: BankCardDestinationResource
): Destination {
const resource = {
type: DestinationResourceType.BankCardDestinationResource,
token: destinationResource.token
};
return {
name: 'Test destination',
identity,
currency: 'RUB',
resource
};
}

View File

@ -0,0 +1,39 @@
import {
PrivateDocument,
RUSDomesticPassportData,
RUSRetireeInsuranceCertificateData,
SecuredPrivateDocument
} from '../codegen';
import PrivateDocumentType = PrivateDocument.TypeEnum;
import { IdentityChallenge } from '../../wallet/codegen';
import ric from '../../../../utils/ric-generator';
export function getPassportParams(): RUSDomesticPassportData {
return {
type: PrivateDocumentType.RUSDomesticPassportData,
series: '4567',
number: '123456',
issuer: 'Отделение УФМС России по Кемеровской области в Юргинском районе',
issuerCode: '666-777',
issuedAt: '2018-11-28',
familyName: 'Иванов',
firstName: 'Иван',
patronymic: 'Иванович',
birthDate: '2018-11-28',
birthPlace: 'дер. Белянино'
};
}
export function getRICParams(): RUSRetireeInsuranceCertificateData {
return {
type: PrivateDocumentType.RUSRetireeInsuranceCertificateData,
number: ric()
};
}
export function getIdentityChallengeParams(proofs: SecuredPrivateDocument[]): IdentityChallenge {
return {
type: 'esia',
proofs
};
}

View File

@ -0,0 +1,13 @@
import { Identity } from '../../codegen';
export function getSimpleIdentityParams(
name: string = 'Test Name',
provider: string = 'test',
_class: string = 'person'
): Identity {
return {
name,
provider,
class: _class
} as any;
}

View File

@ -0,0 +1,18 @@
import { DestinationGrantRequest, WalletGrantRequest } from '../../codegen';
import moment = require('moment');
export function getWalletGrantParams(): WalletGrantRequest {
return {
asset: {
amount: 1000,
currency: 'RUB'
},
validUntil: new Date(moment.now() + 24 * 60 * 60 * 1000)
};
}
export function getWithdrawalGrantParams(): DestinationGrantRequest {
return {
validUntil: new Date(moment.now() + 24 * 60 * 60 * 1000)
};
}

View File

@ -0,0 +1,9 @@
import { Wallet } from '../../codegen';
export function getSimpleWalletParams(identity: string, name: string, currency: string): Wallet {
return {
name,
identity,
currency
};
}

View File

@ -0,0 +1,15 @@
import { WithdrawalBody, WithdrawalParameters } from '../../codegen';
export function getWithdrawalParams(
wallet: string,
destination: string,
body: WithdrawalBody,
externalID?: string
): WithdrawalParameters {
return {
wallet,
destination,
body,
externalID
};
}

3
conditions/index.ts Normal file
View File

@ -0,0 +1,3 @@
export * from './payment-conditions';
export * from './shop-conditions';
export * from './refund-conditions';

View File

@ -0,0 +1,71 @@
import { TokensActions, PaymentsActions, InvoicesActions } from '../actions/capi-v2';
import {
InvoicesEventActions,
isInvoicePaid,
isPaymentCaptured
} from '../actions/capi-v2/invoice-event-actions';
import { AuthActions } from '../actions';
import { InvoiceAndToken, PaymentResource } from '../api/capi-v2/codegen/api';
export interface InstantPaymentProceedResult {
invoiceID: string;
paymentID: string;
}
export class PaymentConditions {
private invoiceActions: InvoicesActions;
private paymentsActions: PaymentsActions;
private invoiceEventActions: InvoicesEventActions;
private static instance: PaymentConditions;
static async getInstance(): Promise<PaymentConditions> {
if (this.instance) {
return this.instance;
}
const token = await AuthActions.getInstance().getExternalAccessToken();
this.instance = new PaymentConditions(token);
return this.instance;
}
private constructor(token: string) {
this.invoiceActions = new InvoicesActions(token);
this.paymentsActions = new PaymentsActions(token);
this.invoiceEventActions = new InvoicesEventActions(token);
}
async proceedInstantPayment(
shopID: string,
amount: number = 10000
): Promise<InstantPaymentProceedResult> {
const invoiceAndToken = await this.invoiceActions.createSimpleInvoice(shopID, amount);
const invoiceAccessToken = invoiceAndToken.invoiceAccessToken.payload;
const tokensActions = new TokensActions(invoiceAccessToken);
const paymentResource = await tokensActions.createSaneVisaPaymentResource();
return this.proceedInstantPaymentExtend(amount, paymentResource, invoiceAndToken);
}
async proceedInstantPaymentExtend(
amount: number,
paymentResource: PaymentResource,
invoiceAndToken: InvoiceAndToken,
externalID?: string,
metadata?: object
): Promise<InstantPaymentProceedResult> {
const invoiceID = invoiceAndToken.invoice.id;
const { id } = await this.paymentsActions.createInstantPayment(
invoiceID,
paymentResource,
amount,
externalID,
metadata
);
await this.invoiceEventActions.waitConditions(
[isInvoicePaid(), isPaymentCaptured(id)],
invoiceID
);
return {
invoiceID,
paymentID: id
};
}
}

View File

@ -0,0 +1,35 @@
import * as chai from 'chai';
import { LogicError, Refund, RefundParams } from '../api/capi-v2/codegen';
import { InvoicesEventActions, isRefundSucceeded, PaymentsActions } from '../actions/capi-v2';
import { AuthActions } from '../actions';
chai.should();
export class RefundConditions {
private paymentsActions: PaymentsActions;
private invoiceEventActions: InvoicesEventActions;
private static instance: RefundConditions;
static async getInstance(): Promise<RefundConditions> {
if (this.instance) {
return this.instance;
}
const token = await AuthActions.getInstance().getExternalAccessToken();
this.instance = new RefundConditions(token);
return this.instance;
}
private constructor(token: string) {
this.paymentsActions = new PaymentsActions(token);
this.invoiceEventActions = new InvoicesEventActions(token);
}
async provideRefund(invoiceID: string, paymentID: string, params: RefundParams) {
const refund = await this.paymentsActions.createRefund(invoiceID, paymentID, params);
refund.should.have.property('amount').to.equal(params.amount);
await this.invoiceEventActions.waitConditions(
[isRefundSucceeded(paymentID, refund.id)],
invoiceID
);
}
}

View File

@ -0,0 +1,64 @@
import { ClaimsActions, PartiesActions, ShopsActions } from '../actions/capi-v2';
import { Shop } from '../api/capi-v2/codegen';
import guid from '../utils/guid';
import { PapiClaimsActions } from '../actions/papi-v1';
import { AuthActions } from '../actions';
export class ShopConditions {
private static instance: ShopConditions;
private claimsActions: ClaimsActions;
private partiesActions: PartiesActions;
private shopsActions: ShopsActions;
private papiClaimsActions: PapiClaimsActions;
static async getInstance(): Promise<ShopConditions> {
if (this.instance) {
return this.instance;
}
const authActions = AuthActions.getInstance();
const exToken = await authActions.getExternalAccessToken();
const inToken = await authActions.getInternalAccessToken();
this.instance = new ShopConditions(exToken, inToken);
return this.instance;
}
constructor(externalToken: string, internalToken: string) {
this.claimsActions = new ClaimsActions(externalToken);
this.partiesActions = new PartiesActions(externalToken);
this.shopsActions = new ShopsActions(externalToken);
this.papiClaimsActions = new PapiClaimsActions(internalToken);
}
async createShops(count: number): Promise<Shop[]> {
const container = [];
for (let i = 0; i < count; i++) {
container.push(this.createShop());
}
return await Promise.all(container);
}
async createShop(): Promise<Shop> {
const party = await this.partiesActions.getActiveParty();
const shopID = guid();
const claim = await this.claimsActions.createClaimForLiveShop(shopID);
await this.papiClaimsActions.acceptClaimByID(party.id, claim.id, 1);
return await this.shopsActions.getShopByID(shopID);
}
async createShopWithPayoutSchedule(scheduleID = 1): Promise<Shop> {
const shop = await this.createShop();
const claim = await this.claimsActions.createClaimForLiveShopWithSchedule(
shop.id,
scheduleID
);
const party = await this.partiesActions.getActiveParty();
await this.papiClaimsActions.acceptClaimByID(party.id, claim.id, 1);
return shop;
}
async turnOffPayoutSchedule(shopID: string) {
const claim = await this.claimsActions.createClaimForLiveShopWithSchedule(shopID);
const party = await this.partiesActions.getActiveParty();
await this.papiClaimsActions.acceptClaimByID(party.id, claim.id, 1);
}
}

2
lib/chai-moment-js/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules/
.idea/

View File

@ -0,0 +1 @@
.idea/

View File

@ -0,0 +1,11 @@
Copyright (c) 2017 Phil Gates-Shannon
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,169 @@
# Chai MomentJS
**Chai MomentJS** is a plugin for the [Chai][1] assertion library that provides date/time comparisons. It is a wrapper for some of the query functions in the [MomentJS][2] date library. Use **Chai MomentJS** to write fluent BDD/TDD tests and get useful error messages.
In other words, _don't_ do this:
```javascript
expect( moment( '2016-12-31' ).isSame( '2017-01-01' ) ).to.be.true;
// => "expected false to be true"
```
Do this instead:
```javascript
expect( moment('2016-12-31') ).is.same.moment( '2017-01-01' );
// => "expected 2016-12-31 00:00:00 to be 2017-01-01 00:00:00"
```
[1]: http://chaijs.com/
[2]: https://momentjs.com/
## Usage
Include the plugin as normal:
```javascript
let chai = require( 'chai' );
let chaiMoment = require( 'chai-moment-js' );
chai.use( chaiMoment );
let expect = chai.expect;
```
### Test Methods
**Chai MomentJS** provides two methods that can used to compare dates: `moment` and `betweenMoments`.
#### moment( date, [accuracy] )
In the default usage, `moment` is a wrapper for the MomentJS's [`isSame`][3] query function. (See "Flags" below for how to change behavior from the default.) It has one required argument, `date`, which can be either a a native JavaScript Date object or a Moment object from MomentJS. The optional argument, `accuracy`, specifies the accuracy of the comparison. You can use any of the vales recognized by MomentJS's [`startOf`][4] function, but the most common ones are:
- second
- minute
- hour
- day
- month
- year
You can use them like this:
```javascript
let m0 = moment( 1487070166000 );
let m1 = moment( 1487070166773 );
expect( m1 ).is.same.moment( m0 ); // => false
expect( m1 ).is.same.moment( m0, 'second' ); // => true
```
[3]: https://momentjs.com/docs/#/query/is-same/
[4]: https://momentjs.com/docs/#/manipulating/start-of/
#### betweenMoments( start, end, [accuracy], [inclusivity] )
This is a wrapper for MomentJS's [`isBetween`][5] query function. It requires `start` and `end` arguments, which may be either a Date or a Moment. The `accuracy` parameters functions as in the `moment` function; it will accept `null` for millisecond accuracy.
Finally, the `inclusivity` parameter determines whether to return true or false if the object-under-test matches the `start` or `end` argument. Basically, a parenthesis excludes an exact match (returns false) while a square bracket includes an exact match (returns true). The default is to exclude on exact matches.
The following table explains inclusivity in more concrete terms:
| argument | result of exact match on `start` | result of exact match on `end` |
| --- | --- | --- |
| '()' | `false` | `false` |
| '[]' | `true` | `true` |
| '(]' | `false` | `true` |
| '[)' | `true` | `false` |
The meaning of "exact match" is determined by the `accuracy` parameter.
Some examples:
```javascript
let m0 = moment( 1487070166000 );
let m1 = moment( 1487070166500 );
let m2 = moment( 1487070166773 );
expect( m1 ).is.betweenMoments( m0, m2 ); // => true
expect( m1 ).is.betweenMoments( m0, m2, 'second' ); // => false
expect( m1 ).is.betweenMoments( m0, m2, 'second', '[]' ); // => true
expect( m0 ).is.betweenMoments( m0, m2 ); // => false
expect( m0 ).is.betweenMoments( m0, m2, null, '[)' ); // => true
expect( m0 ).is.betweenMoments( m0, m2, null, '(]' ); // => false
expect( m2 ).is.betweenMoments( m0, m2, null, '[)' ); // => false
expect( m2 ).is.betweenMoments( m0, m2, null, '(]' ); // => true
```
[5]: https://momentjs.com/docs/#/query/is-between/
### Flags
These flags change the behavior of the `moment` comparison function. This allows you to write fluent TDD/BDD statements like `expect( fileDate ).is.before.moment( myDate )`.
Don't combine flags. That's bad, like crossing-the-streams bad.
#### before
The **before** flag tells **Chai MomentJS** to use MomentJS's [`isBefore`][6] query function.
```javascript
let m0 = moment( 1487070166000 );
let m1 = moment( 1487070166773 );
expect( m0 ).is.before.moment( m1 ); // => true
expect( m0 ).is.before.moment( m1, 'second' ); // => false
```
[6]: https://momentjs.com/docs/#/query/is-before/
#### after
The **after** flag tells **Chai MomentJS** to use MomentJS's [`isAfter`][7] query function.
```javascript
let m0 = moment( 1487070166000 );
let m1 = moment( 1487070166773 );
expect( m1 ).is.after.moment( m0 ); // => true
expect( m1 ).is.after.moment( m0, 'second' ); // => false
```
[7]: https://momentjs.com/docs/#/query/is-after/
#### sameOrBefore
The **sameOrBefore** flag tells **Chai MomentJS** to use MomentJS's [`isSameOrBefore`][8] query function.
```javascript
let m0 = moment( 1487070166000 );
let m1 = moment( 1487070166773 );
expect( m0 ).is.sameOrBefore.moment( m1 ); // => true
expect( m0 ).is.sameOrBefore.moment( m1, 'second' ); // => true
```
[8]: https://momentjs.com/docs/#/query/is-same-or-before/
#### sameOrAfter
The **sameOrAfter** flag tells **Chai MomentJS** to use MomentJS's [`isSameOrAfter`][9] query function.
```javascript
let m0 = moment( 1487070166000 );
let m1 = moment( 1487070166773 );
expect( m1 ).is.sameOrAfter.moment( m0 ); // => true
expect( m1 ).is.sameOrAfter.moment( m0, 'second' ); // => true
```
[9]: https://momentjs.com/docs/#/query/is-same-or-after/
## Thanks
Thanks to:
- @mguterl for [chai-datetime][3], which inspired this plugin.
- @fastfrwrd for [chai-moment][10], which I didn't know about until I got a name collision upon running `npm publish`!
[3]: https://github.com/mguterl/chai-datetime
[10]: https://www.npmjs.com/package/chai-moment
## License
The content of this repository is licensed under the [3-Clause BSD license][4]. Please see the enclosed [license file][5] for specific terms.
[4]: https://opensource.org/licenses/BSD-3-Clause
[5]: https://github.com/philgs/chai-moment/blob/release/LICENSE.md

20
lib/chai-moment-js/index.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
/// <reference types="chai" />
declare global {
export namespace Chai {
interface Assertion extends LanguageChains, NumericComparison, TypeComparison {
after:Assertion;
before:Assertion;
sameOrAfter:Assertion;
sameOrBefore:Assertion;
betweenMoments( start:any, end:any, specificity?:string, inclusivity?:string ):Assertion;
moment( timestamp:any, specificity?:string ):Assertion;
}
}
}
declare function chaiMoment( chai: any, utils: any ): void;
declare namespace chaiMoment { }
export = chaiMoment;

173
lib/chai-moment-js/index.js Normal file
View File

@ -0,0 +1,173 @@
let moment = require( 'moment' );
let _ = require( 'lodash' ); // TODO Only require necessary modules
const AFTER = 'after';
const BEFORE = 'before';
const BETWEEN = 'betweenMoments';
const MOMENT = 'moment';
const SAME_OR_AFTER = 'sameOrAfter';
const SAME_OR_BEFORE = 'sameOrBefore';
let ensureMoment = function( date ) {
return moment.isMoment( date ) ? date : moment( date )
};
let errorMessages = {
getBadDate: function( value ) {
return `AssertionError: expected ${value} to be a Date or Moment, but `
+ `it is a ${typeof value}: expected false to be true`
},
getBetweenError: function( actual, start, end ) {
let format = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
actual = ensureMoment( actual ).format( format );
start = ensureMoment( start ).format( format );
end = ensureMoment( end ).format( format );
return [
`expected ${actual} to be between ${start} and ${end}`,
`expected ${actual} to not be between ${start} and ${end}`,
actual,
start + ' <---> ' + end
];
},
getChainableError: function( name ) {
return 'Chainable property "' + name + '" can only be used in a chain, '
+ 'NOT to check a value';
},
getComparisonError: function( actual, expected, comparisonPhrase ) {
let format = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
let act = ensureMoment( actual ).format( format );
let exp = ensureMoment( expected ).format( format );
return [
`expected ${act} to be ${comparisonPhrase} ${exp}`,
`expected ${act} to not be ${comparisonPhrase} ${exp}`,
act,
exp
];
},
noFlagsForBetween: 'No flags can be used with the `betweenMoments` comparison.'
};
let namespace = function( name ) {
const ns = 'moment';
return ns + '.' + name;
};
let chainableError = function( name ) {
return function() {
throw new Error( errorMessages.getChainableError( name ) );
}
};
module.exports = function( chai, utils ) {
let Assertion = chai.Assertion;
Assertion.addChainableMethod( AFTER, chainableError( AFTER ), function() {
utils.flag( this, namespace( AFTER ), true );
} );
Assertion.addChainableMethod( BEFORE, chainableError( BEFORE ), function() {
utils.flag( this, namespace( BEFORE ), true );
} );
Assertion.addChainableMethod( SAME_OR_AFTER, chainableError( SAME_OR_AFTER ), function() {
utils.flag( this, namespace( SAME_OR_AFTER ), true );
} );
Assertion.addChainableMethod( SAME_OR_BEFORE, chainableError( SAME_OR_BEFORE ), function() {
utils.flag( this, namespace( SAME_OR_BEFORE ), true );
} );
Assertion.addMethod( BETWEEN, function( start, end, accuracy, inclusivity ) {
// I said, NO FLAGS ALLOWED!
if ( utils.flag( this, namespace( AFTER ) )
|| utils.flag( this, namespace( BEFORE ) )
|| utils.flag( this, namespace( SAME_OR_AFTER ) )
|| utils.flag( this, namespace( SAME_OR_BEFORE ) ) ) {
throw new Error( errorMessages.noFlagsForBetween );
}
// Do this check independent of `this` so it is not affect by flags
new Assertion(
moment.isDate( start ) || moment.isMoment( start ),
errorMessages.getBadDate( start )
).is.true;
new Assertion(
moment.isDate( end ) || moment.isMoment( end ),
errorMessages.getBadDate( end )
).is.true;
// Make sure that we have Moment objects
let obj = ensureMoment( this._obj );
start = ensureMoment( start );
end = ensureMoment( end );
// Use a curried function, so we don't type the args to `this.assert` every time
let compareThisTo = null;
if ( inclusivity ) {
compareThisTo = _.curry( obj.isBetween.bind( obj ), 4 )( _, _, accuracy, inclusivity );
} else if ( accuracy ) {
compareThisTo = _.curry( obj.isBetween.bind( obj ), 3 )( _, _, accuracy );
} else {
compareThisTo = _.curry( obj.isBetween.bind( obj ), 2 )( _, _ );
}
// Do the comparison
let [ positive, negative, actual, expected ] = errorMessages
.getBetweenError( obj, start, end );
this.assert( compareThisTo( start, end )
, positive, negative, expected, actual, true );
} );
Assertion.addMethod( MOMENT, function( timestamp, accuracy ) {
// Do this check independent of `this` so it is not affect by flags
new Assertion(
moment.isDate( timestamp ) || moment.isMoment( timestamp ),
errorMessages.getBadDate( timestamp )
).is.true;
// Make sure that we have Moment objects
let obj = ensureMoment( this._obj );
timestamp = ensureMoment( timestamp );
// Determine the comparator function and message based on flags
let comparatorFn = obj.isSame.bind( obj ); // default comparator
let comparatorMsg = 'the same as';
if ( utils.flag( this, namespace( AFTER ) ) ) {
comparatorFn = obj.isAfter.bind( obj );
comparatorMsg = 'after';
}
if ( utils.flag( this, namespace( BEFORE ) ) ) {
comparatorFn = obj.isBefore.bind( obj );
comparatorMsg = 'before';
}
if ( utils.flag( this, namespace( SAME_OR_AFTER ) ) ) {
comparatorFn = obj.isSameOrAfter.bind( obj );
comparatorMsg = 'same or after';
}
if ( utils.flag( this, namespace( SAME_OR_BEFORE ) ) ) {
comparatorFn = obj.isSameOrBefore.bind( obj );
comparatorMsg = 'same or before';
}
// Create a curried comparison function, to reduce redundancy
let compareThisTo = null;
if ( accuracy ) {
compareThisTo = _.curry( comparatorFn, 2 )( _, accuracy );
} else {
compareThisTo = _.curry( comparatorFn, 1 );
}
// Do the comparison
let [ positive, negative, actual, expected ] = errorMessages
.getComparisonError( obj, timestamp, comparatorMsg );
this.assert( compareThisTo( timestamp ), positive, negative, expected, actual, true );
} );
};
module.exports.messages = errorMessages;

View File

@ -0,0 +1,32 @@
{
"name": "chai-moment-js",
"version": "1.0.0",
"description": "Date/time comparisons in Chai with MomentJS",
"main": "index.js",
"types": "index.d.ts",
"scripts": {
"test": "mocha --reporter spec"
},
"repository": {
"type": "git",
"url": "git://github.com/philgs/chai-moment-js.git"
},
"author": {
"name": "Phil Gates-Shannon",
"email": "phil.gs@gmail.com",
"url": "http://philgs.me"
},
"license": "BSD-3-Clause",
"dependencies": {
"lodash": "^4.17.4",
"moment": "^2.17.1"
},
"peerDependencies": {
"chai": ">1.10.0 <4"
},
"devDependencies": {
"chai": "^3.5.0",
"dirty-chai": "^1.2.2",
"mocha": "^4.0.1"
}
}

30
lib/chai-url/index.d.ts vendored Normal file
View File

@ -0,0 +1,30 @@
// Type definitions for chai-url
/// <reference types="chai" />
declare function chaiUrl(chai: any, utils: any): void;
export = chaiUrl;
declare global {
namespace Chai {
interface Assertion {
path(path: string): Assertion;
pathname(pathname: string): Assertion;
port(port: string): Assertion;
hostname(hostname: string): Assertion;
protocol(protocol: string): Assertion;
auth(auth: string): Assertion;
hash(hash: string): Assertion;
}
interface Assert {
path(val: string, exp: string, msg?: string): void;
pathname(val: string, exp: string, msg?: string): void;
port(val: string, exp: string, msg?: string): void;
hostname(val: string, exp: string, msg?: string): void;
protocol(val: string, exp: string, msg?: string): void;
auth(val: string, exp: string, msg?: string): void;
hash(val: string, exp: string, msg?: string): void;
}
}
}

74
mocha-tests.js Normal file
View File

@ -0,0 +1,74 @@
const Mocha = require('mocha');
const path = require('path');
const glob = require('glob');
const argv = require('yargs')
.usage('Usage: $0 [options]')
.describe('auth-endpoint', 'Auth endpoint')
.describe('external-login', 'Auth external realm login')
.describe('external-password', 'Auth external realm password')
.describe('internal-login', 'Auth internal realm login')
.describe('internal-password', 'Auth internal realm password')
.describe('capi-endpoint', 'Common API endpoint')
.describe('admin-endpoint', 'IDDQD endpoint')
.describe('proxy-endpoint', 'Proxy endpoint')
.describe('url-shortener-endpoint', 'URL Shortener API endpoint')
.describe('test-webhook-receiver-endpoint', 'Test webhook receiver endpoint')
.describe('file', 'include a file to be ran during the suite [filepath]')
.describe('slow', '"slow" test threshold in ms')
.describe('timeout', 'Set test-case timeout in ms')
.describe('tt', 'Test transaction flag')
.describe('test-dir', 'Path to directory containng tests')
.describe('auth-warn', 'Auth flow warn threshold in ms')
.describe('create-invoice-warn', 'Create invoice warn threshold in ms')
.describe('create-payment-resource-warn', 'Create payment resource warn threshold in ms')
.describe('create-payment-warn', 'Create payment warn threshold in ms')
.describe('polling-warn', 'Polling payment and invoice events warn threshold in ms')
.describe('fulfill-invoice-warn', 'Fulfill invoice warn threshold in ms')
.describe('test-shop-id', 'Test shopID for test transaction')
.describe('create-test-shop', 'Create test shop if not found')
.default({
timeout: 30000,
slow: 150,
'auth-warn': 200,
'create-invoice-warn': 200,
'create-payment-resource-warn': 200,
'create-payment-warn': 200,
'polling-warn': 5000,
'fulfill-invoice-warn': 100,
'test-shop-id': 'TEST',
'create-test-shop': true,
'test-dir': './build/test'
})
.demandOption([
'auth-endpoint',
'external-login',
'external-password',
'internal-login',
'internal-password',
'capi-endpoint',
'admin-endpoint',
'proxy-endpoint',
'url-shortener-endpoint',
'test-webhook-receiver-endpoint'
]).argv;
const mocha = new Mocha({
timeout: argv.timeout,
slow: argv.slow
});
const testDir = argv['test-dir'];
const integrationTestFile = 'transaction.spec.js';
if (argv.file) {
mocha.addFile(path.resolve(argv.file));
} else {
glob.sync('**/*.js', { cwd: testDir })
.filter(file => (argv.tt ? file === integrationTestFile : file !== integrationTestFile))
.forEach(file => mocha.addFile(path.resolve(testDir, file)));
}
mocha
.reporter(argv.tt ? 'min' : 'spec')
.run(code => (argv.tt ? process.exit(code === 1 ? 3 : code) : process.exit(code)));

4227
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

43
package.json Normal file
View File

@ -0,0 +1,43 @@
{
"name": "wetkitty-integration-tests",
"version": "1.0.0",
"description": "",
"private": true,
"repository": {
"type": "git",
"url": "git+https://github.com/rbkmoney/wetkitty.git"
},
"scripts": {
"prettier": "prettier \"**/*.{js,ts,md,json,prettierrc}\" --write",
"check": "prettier \"**/*.{js,ts,md,json,prettierrc}\" --list-different"
},
"author": "",
"dependencies": {
"chai": "4.1.2",
"chai-moment-js": "file:lib/chai-moment-js",
"chai-url": "1.0.4",
"glob": "7.1.2",
"http": "0.0.0",
"lodash": "^4.17.15",
"memory-cache": "0.2.0",
"mocha": "4.0.1",
"moment": "^2.24.0",
"pkg": "4.3.0-beta.5",
"portable-fetch": "3.0.0",
"request": "2.83.0",
"url": "0.11.0",
"yargs": "10.0.3"
},
"devDependencies": {
"@types/chai": "4.0.5",
"@types/memory-cache": "0.2.0",
"@types/mocha": "2.2.44",
"@types/node": "8.0.53",
"@types/request": "^2.48.3",
"dotenv": "5.0.1",
"prettier": "1.16.4",
"rimraf": "2.6.2",
"ts-node": "5.0.1",
"typescript": "2.6.1"
}
}

21
settings.ts Normal file
View File

@ -0,0 +1,21 @@
import * as yargs from 'yargs';
require('dotenv').config();
export const authEndpoint = yargs.argv['auth-endpoint'] || process.env.AUTH_ENDPOINT;
export const capiEndpoint = yargs.argv['capi-endpoint'] || process.env.CAPI_ENDPOINT;
export const adminEndpoint = yargs.argv['admin-endpoint'] || process.env.ADMIN_ENDPOINT;
export const proxyEndpoint = yargs.argv['proxy-endpoint'] || process.env.PROXY_ENDPOINT;
export const urlShortenerEndpoint =
yargs.argv['url-shortener-endpoint'] || process.env.URL_SHORTENER_ENDPOINT;
export const externalLogin = yargs.argv['external-login'] || process.env.EXTERNAL_LOGIN;
export const externalPassword = yargs.argv['external-password'] || process.env.EXTERNAL_PASSWORD;
export const internalLogin = yargs.argv['internal-login'] || process.env.INTERNAL_LOGIN;
export const internalPassword = yargs.argv['internal-password'] || process.env.INTERNAL_PASSWORD;
export const wapiEndpoint = capiEndpoint + '/wallet';
export const privdocEndpoint = capiEndpoint + '/privdoc';
export const payresEndpoint = capiEndpoint + '/payres';
export const anapiEndpoint = capiEndpoint + '/lk';
export const binapiEndpoint = capiEndpoint + '/binbase';
export const testWebhookReceiverInternal = 'http://test-webhook-receiver:8080';
export const testWebhookReceiverEndpoint =
yargs.argv['test-webhook-receiver-endpoint'] || process.env.TEST_WEBHOOK_RECEIVER_ENDPOINT;

Some files were not shown because too many files have changed in this diff Show More