Skip to content

Commit

Permalink
Add SEP-31+38 tests where SEP-31 POST transactions depend on SEP-38 P…
Browse files Browse the repository at this point in the history
…OST quote (#90)

* Add sep31+38 tests to test transactions when quotes_required is true

* Add changelog entry and update package version.
  • Loading branch information
erika-sdf authored Jun 28, 2022
1 parent 11bf82c commit 4bdf48e
Show file tree
Hide file tree
Showing 8 changed files with 344 additions and 3 deletions.
2 changes: 1 addition & 1 deletion @stellar/anchor-tests/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stellar/anchor-tests",
"version": "0.5.0",
"version": "0.5.1",
"description": "stellar-anchor-tests is a library and command line interface for testing Stellar anchors.",
"main": "./lib/index.js",
"types": "./lib/index.d.ts",
Expand Down
15 changes: 15 additions & 0 deletions @stellar/anchor-tests/src/helpers/sep31.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export function getQuotesRequiredFromInfo(
sep31Info: undefined,
assetCode: string,
): boolean {
// @ts-ignore
return sep31Info.receive[assetCode].quotes_required;
}

export function getQuotesSupportedFromInfo(
sep31Info: undefined,
assetCode: string,
): boolean {
// @ts-ignore
return sep31Info.receive[assetCode].quotes_supported;
}
4 changes: 4 additions & 0 deletions @stellar/anchor-tests/src/helpers/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { default as sep10Tests } from "../tests/sep10/tests";
import { default as sep12Tests } from "../tests/sep12/tests";
import { default as sep24Tests } from "../tests/sep24/tests";
import { default as sep31Tests } from "../tests/sep31/tests";
import { default as sep31And38Tests } from "../tests/sep31And38/tests";
import { default as sep38Tests } from "../tests/sep38/tests";
import { makeFailure } from "./failure";
import { checkConfig } from "./config";
Expand Down Expand Up @@ -349,6 +350,9 @@ function getTopLevelTests(config: Config): Test[] {
if (config.seps.includes(38)) {
tests = tests.concat(sep38Tests);
}
if (config.seps.includes(31) && config.seps.includes(38)) {
tests = tests.concat(sep31And38Tests);
}
return filterBySearchStrings(tests, config.searchStrings as string[]);
}

Expand Down
36 changes: 35 additions & 1 deletion @stellar/anchor-tests/src/tests/sep31/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { hasDirectPaymentServer } from "./toml";
import { differentMemosSameAccount } from "../sep12/putCustomer";
import { genericFailures, makeFailure } from "../../helpers/failure";
import { makeRequest } from "../../helpers/request";
import { getQuotesRequiredFromInfo } from "../../helpers/sep31";
import {
postTransactionsSchema,
getTransactionSchema,
Expand Down Expand Up @@ -47,6 +48,8 @@ const requiresJwt: Test = {
};
tests.push(requiresJwt);

let quotesRequired: boolean;

const canCreateTransaction: Test = {
assertion: "can create a transaction",
sep: 31,
Expand All @@ -63,6 +66,7 @@ const canCreateTransaction: Test = {
sendingAnchorToken: undefined,
sendingCustomerId: undefined,
receivingCustomerId: undefined,
sep31InfoObj: undefined,
},
provides: {
transactionId: undefined,
Expand Down Expand Up @@ -117,7 +121,25 @@ const canCreateTransaction: Test = {
),
};
result.networkCalls.push(postTransactionsCall);
const responseBody = await makeRequest(

let assetCode = config.assetCode;
if (assetCode === undefined) {
throw "asset not configured";
}
quotesRequired = getQuotesRequiredFromInfo(
this.context.expects.sep31InfoObj,
assetCode,
);

// If quotes are required, ignore this test, this will be addressed in SEP 31+38 tests
if (quotesRequired) {
await makeRequest(postTransactionsCall, 400, result, "application/json");
this.context.provides.transactionId = null;
return result;
}

// if quotes are not required, test this as usual.
let responseBody = await makeRequest(
postTransactionsCall,
201,
result,
Expand Down Expand Up @@ -273,6 +295,13 @@ const canFetchTransaction: Test = {
failureModes: genericFailures,
async run(_config: Config): Promise<Result> {
const result: Result = { networkCalls: [] };

// If quotes are required, ignore this test, this will be addressed in SEP 31+38 tests
if (quotesRequired) {
this.context.provides.transactionJson = null;
return result;
}

const getTransactionCall: NetworkCall = {
request: new Request(
this.context.expects.directPaymentServerUrl +
Expand Down Expand Up @@ -337,6 +366,11 @@ const hasValidTransactionJson: Test = {
},
async run(_config: Config): Promise<Result> {
const result: Result = { networkCalls: [] };
// If quotes are required, ignore this test, this will be addressed in SEP 31+38 tests
if (quotesRequired) {
this.context.provides.transactionJson = null;
return result;
}
const validationResult = validate(
this.context.expects.transactionJson,
getTransactionSchema,
Expand Down
3 changes: 3 additions & 0 deletions @stellar/anchor-tests/src/tests/sep31And38/tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { default as transactionsTests } from "./transactions";

export default transactionsTests;
275 changes: 275 additions & 0 deletions @stellar/anchor-tests/src/tests/sep31And38/transactions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
import { Request } from "node-fetch";
import { validate } from "jsonschema";
import { Keypair, Memo } from "stellar-sdk";

import { Test, Result, Config, NetworkCall } from "../../types";
import { hasDirectPaymentServer } from "../sep31/toml";
import { differentMemosSameAccount } from "../sep12/putCustomer";
import { canCreateQuote } from "../sep38/postQuote";
import { genericFailures, makeFailure } from "../../helpers/failure";
import { makeRequest } from "../../helpers/request";
import {
getTransactionSchema,
postTransactionsSchema,
} from "../../schemas/sep31";
import { hasExpectedAssetEnabled } from "../sep31/info";
import { getQuotesSupportedFromInfo } from "../../helpers/sep31";

const postTransactionsGroup = "POST /transactions";
const getTransactionsGroup = "GET /transactions/:id";
const tests: Test[] = [];

let quotesSupported: boolean;

const canCreateTransaction: Test = {
assertion: "[with SEP-38, quotes_required] can create a transaction",
sep: 31,
group: postTransactionsGroup,
dependencies: [
hasDirectPaymentServer,
hasExpectedAssetEnabled,
differentMemosSameAccount,
canCreateQuote,
],
context: {
expects: {
directPaymentServerUrl: undefined,
sendingAnchorClientKeypair: undefined,
sendingAnchorToken: undefined,
sendingCustomerId: undefined,
sep38QuoteResponseObj: undefined,
receivingCustomerId: undefined,
sep31InfoObj: undefined,
},
provides: {
transactionId: undefined,
},
},
failureModes: {
INVALID_SCHEMA: {
name: "invalid schema",
text(args: any): string {
return (
"The response body from POST /transactions does not match " +
"the schema defined by the protocol. " +
"Errors:\n\n" +
`${args.errors}`
);
},
links: {
"POST Transaction Schema":
"https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md#post-transactions",
},
},
...genericFailures,
},
async run(config: Config): Promise<Result> {
const result: Result = { networkCalls: [] };
if (!config.sepConfig || !config.sepConfig["31"]) {
// this configuration is checked prior to running tests
// but to satisfy TypeScript we make these checks here.
throw "improperly configured";
}

let assetCode = config.assetCode;
if (assetCode === undefined) {
throw "asset not configured";
}
quotesSupported = getQuotesSupportedFromInfo(
this.context.expects.sep31InfoObj,
assetCode,
);

// If quotes are not supported, ignore this test, this will be addressed in SEP 31 tests
if (!quotesSupported) {
this.context.provides.transactionId = null;
return result;
}

const splitAsset =
this.context.expects.sep38QuoteResponseObj.sell_asset.split(":", 3);
const postTransactionsCall: NetworkCall = {
request: new Request(
this.context.expects.directPaymentServerUrl + "/transactions",
{
method: "POST",
headers: {
Authorization: `Bearer ${this.context.expects.sendingAnchorToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
sender_id: this.context.expects.sendingCustomerId,
receiver_id: this.context.expects.receivingCustomerId,
amount: 100,
asset_code: splitAsset[1],
quote_id: this.context.expects.sep38QuoteResponseObj.id,
fields: {
transaction: {
...config.sepConfig["31"].transactionFields,
},
},
}),
},
),
};
result.networkCalls.push(postTransactionsCall);
const responseBody = await makeRequest(
postTransactionsCall,
201,
result,
"application/json",
);
if (!responseBody) return result;
const validationResult = validate(responseBody, postTransactionsSchema);
if (validationResult.errors.length !== 0) {
result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, {
errors: validationResult.errors.join("\n"),
});
return result;
}
try {
Keypair.fromPublicKey(responseBody.stellar_account_id);
} catch {
result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, {
errors: "'stellar_acocunt_id' must be a valid Stellar public key",
});
return result;
}
let memoValue = responseBody.stellar_memo;
if (responseBody.stellar_memo_type === "hash") {
memoValue = Buffer.from(responseBody.stellar_memo, "base64");
}
try {
new Memo(responseBody.stellar_memo_type, memoValue);
} catch {
result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, {
errors: "invalid 'stellar_memo' for 'stellar_memo_type'",
});
return result;
}
this.context.provides.transactionId = responseBody.id;
return result;
},
};
tests.push(canCreateTransaction);

const canFetchTransaction: Test = {
assertion: "[with SEP-38, quotes_required] can fetch a created transaction",
sep: 31,
group: getTransactionsGroup,
dependencies: [hasDirectPaymentServer, canCreateTransaction],
context: {
expects: {
sendingAnchorToken: undefined,
directPaymentServerUrl: undefined,
transactionId: undefined,
},
provides: {
transactionJson: undefined,
},
},
failureModes: genericFailures,
async run(_config: Config): Promise<Result> {
const result: Result = { networkCalls: [] };

// If quotes are not supported, ignore this test, this will be addressed in SEP 31 tests
if (!quotesSupported) {
this.context.provides.transactionJson = null;
return result;
}

const getTransactionCall: NetworkCall = {
request: new Request(
this.context.expects.directPaymentServerUrl +
`/transactions/${this.context.expects.transactionId}`,
{
headers: {
Authorization: `Bearer ${this.context.expects.sendingAnchorToken}`,
},
},
),
};
result.networkCalls.push(getTransactionCall);
this.context.provides.transactionJson = await makeRequest(
getTransactionCall,
200,
result,
"application/json",
);
return result;
},
};
tests.push(canFetchTransaction);

const hasValidTransactionJson: Test = {
assertion:
"[with SEP-38, quotes_required] response body complies with protocol schema",
sep: 31,
group: getTransactionsGroup,
dependencies: [canFetchTransaction],
context: {
expects: {
transactionId: undefined,
transactionJson: undefined,
},
provides: {},
},
failureModes: {
INVALID_SCHEMA: {
name: "invalid schema",
text(args: any): string {
return (
"The response body from GET /transactions/:id does not match " +
"the schema defined by the protocol. " +
"Errors:\n\n" +
`${args.errors}`
);
},
links: {
"Transactions Schema":
"https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md#get-transaction",
},
},
INVALID_ID: {
name: "invalid 'id'",
text(_args: any): string {
return "The 'id' value returned in the response body does match the ID used to fetch the transaction";
},
links: {
"Transactions Schema":
"https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0031.md#get-transaction",
},
},
},
async run(_config: Config): Promise<Result> {
const result: Result = { networkCalls: [] };
// If quotes are not supported, ignore this test, this will be addressed in SEP 31 tests
if (!quotesSupported) {
return result;
}

const validationResult = validate(
this.context.expects.transactionJson,
getTransactionSchema,
);
if (validationResult.errors.length !== 0) {
result.failure = makeFailure(this.failureModes.INVALID_SCHEMA, {
errors: validationResult.errors.join("\n"),
});
return result;
}
if (
this.context.expects.transactionId !==
this.context.expects.transactionJson.transaction.id
) {
result.failure = makeFailure(this.failureModes.INVALID_ID);
result.expected = this.context.expects.transactionId;
result.actual = this.context.expects.transactionJson.transaction.id;
return result;
}
return result;
},
};
tests.push(hasValidTransactionJson);

export default tests;
Loading

0 comments on commit 4bdf48e

Please sign in to comment.