Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdl): move validation from cloudmos to sdl (#133) #82

Merged
merged 9 commits into from
May 10, 2024
2 changes: 1 addition & 1 deletion .commitlintrc.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": ["@commitlint/config-conventional"],
"rules": {
"scope-enum": [2, "always", ["certificates", "network", "wallet", "api", "stargate"]]
"scope-enum": [2, "always", ["certificates", "network", "wallet", "api", "stargate", "sdl"]]
}
}
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@types/jest": "^29.5.12",
"@types/js-yaml": "^4.0.5",
"@types/json-stable-stringify": "^1.0.34",
"@types/lodash": "^4.17.1",
"@types/node-fetch": "2",
"@types/sinon": "^10.0.11",
"@types/tap": "^15.0.5",
Expand All @@ -49,6 +50,7 @@
"husky": "^9.0.11",
"jest": "^29.7.0",
"lint-staged": "^15.2.2",
"lodash": "^4.17.21",
"node-polyfill-webpack-plugin": "^1.1.4",
"prettier": "^3.2.5",
"rimraf": "^5.0.1",
Expand Down
13 changes: 13 additions & 0 deletions src/config/network.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MainnetNetworkId, NetworkId, SandboxNetworkId, TestnetNetworkId } from "../types/network";

export const MAINNET_ID: MainnetNetworkId = "mainnet";
export const SANDBOX_ID: SandboxNetworkId = "sandbox";
export const TESTNET_ID: TestnetNetworkId = "testnet";

export const USDC_IBC_DENOMS: Record<NetworkId, string> = {
[MAINNET_ID]: "ibc/170C677610AC31DF0904FFE09CD3B5C657492170E7E52372E48756B71E56F2F1",
[SANDBOX_ID]: "ibc/12C6A0C374171B595A0A9E18B83FA09D295FB1F2D8C6DAA3AC28683471752D84",
[TESTNET_ID]: ""
};

export const AKT_DENOM = "uakt";
14 changes: 14 additions & 0 deletions src/error/SdlValidationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ValidationError } from "./ValidationError";

export class SdlValidationError extends ValidationError {
static assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new SdlValidationError(message);
}
}

constructor(message: string) {
super(message);
this.name = "SdlValidationError";
}
}
12 changes: 12 additions & 0 deletions src/error/ValidationError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class ValidationError extends Error {
static assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new ValidationError(message);
}
}

constructor(message: string) {
super(message);
this.name = "SdlValidationError";
}
}
2 changes: 2 additions & 0 deletions src/error/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./SdlValidationError";
export * from "./ValidationError";
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * as rpc from "./rpc";

export * as protoclient from "./pbclient/pbclient";
export * as sdl from "./sdl";
export * from "./error";
207 changes: 207 additions & 0 deletions src/sdl/SDL/SDL.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { faker } from "@faker-js/faker";

import { readBasicSdl } from "../../../test/yml";
import { SdlValidationError } from "../../error";
import { SDL } from "./SDL";
import { v2ServiceImageCredentials } from "../types";
import { omit } from "lodash";
import { AKT_DENOM, SANDBOX_ID, USDC_IBC_DENOMS } from "../../config/network";

describe("SDL", () => {
describe("profiles placement pricing denomination", () => {
it.each([AKT_DENOM, USDC_IBC_DENOMS[SANDBOX_ID]])('should resolve a group with a valid "%s" denomination', denom => {
const yml = readBasicSdl({ denom });
const sdl = SDL.fromString(yml, "beta3", "sandbox");

expect(sdl.groups()).toMatchObject([
{
resources: [
{
price: {
denom: denom,
amount: "1000"
}
}
]
}
]);
});

it("should throw an error when denomination is invalid", () => {
const denom = "usdt";
const yml = readBasicSdl({ denom });

expect(() => SDL.fromString(yml, "beta3", "sandbox")).toThrow(
new SdlValidationError(`Invalid denom: "${denom}". Only uakt and ${USDC_IBC_DENOMS[SANDBOX_ID]} are supported.`)
);
});
});

describe("endpoints", () => {
it("should resolve with valid endpoints", () => {
const endpointName = faker.lorem.word();
const endpoint = {
[endpointName]: {
kind: "ip"
}
};
const yml = readBasicSdl({ endpoint });
const sdl = SDL.fromString(yml, "beta3", "sandbox");

expect(sdl.manifest()).toMatchObject([
{
services: [
{
resources: {
endpoints: {
1: {
kind: 2,
sequence_number: 1
}
}
},
expose: [
{
ip: endpointName,
endpointSequenceNumber: 1
}
]
}
]
}
]);
expect(sdl.groups()).toMatchObject([
{
resources: [
{
resource: {
endpoints: {
1: {
kind: 2,
sequence_number: 1
}
}
}
}
]
}
]);
});

it("should throw provided an invalid endpoint name", () => {
const endpointName = faker.number.int().toString();
const endpoint = {
[endpointName]: {
kind: "ip"
}
};
const yml = readBasicSdl({ endpoint });

expect(() => SDL.fromString(yml, "beta3", "sandbox")).toThrowError(new SdlValidationError(`Endpoint named "${endpointName}" is not a valid name.`));
});

it("should throw provided no endpoint kind", () => {
const endpointName = faker.lorem.word();
const endpoint = {
[endpointName]: {}
};
const yml = readBasicSdl({ endpoint });

expect(() => SDL.fromString(yml, "beta3", "sandbox")).toThrowError(new SdlValidationError(`Endpoint named "${endpointName}" has no kind.`));
});

it("should throw provided invalid endpoint kind", () => {
const endpointName = faker.lorem.word();
const endpointKind = faker.lorem.word();
const endpoint = {
[endpointName]: {
kind: endpointKind
}
};
const yml = readBasicSdl({ endpoint });

expect(() => SDL.fromString(yml, "beta3", "sandbox")).toThrowError(
new SdlValidationError(`Endpoint named "${endpointName}" has an unknown kind "${endpointKind}".`)
);
});

it("should throw when endpoint is unused", () => {
const endpointName = faker.lorem.word();
const endpoint = {
[endpointName]: {
kind: "ip"
}
};
const yml = readBasicSdl({ endpoint, endpointRef: undefined });

expect(() => SDL.fromString(yml, "beta3", "sandbox")).toThrowError(new SdlValidationError(`Endpoint ${endpointName} declared but never used.`));
});
});

describe("service image credentials", () => {
it("should resolve a service with valid credentials", () => {
const credentials = {
host: faker.internet.url(),
username: faker.internet.userName(),
password: faker.internet.password()
};
const sdl = SDL.fromString(readBasicSdl({ credentials }), "beta3", "sandbox");

expect(sdl.manifest()).toMatchObject([
{
services: [
{
credentials
}
]
}
]);
});

it("should resolve a service without credentials", () => {
const sdl = SDL.fromString(readBasicSdl(), "beta3", "sandbox");
const group = sdl.manifest()[0];

if (!("services" in group)) {
throw new Error("No services found in group");
}

expect(group.services[0].credentials).toBeNull();
});

describe("should throw an error when credentials are invalid", () => {
const fields: (keyof v2ServiceImageCredentials)[] = ["host", "username", "password"];
let credentials: v2ServiceImageCredentials;

beforeEach(() => {
credentials = {
host: faker.internet.url(),
username: faker.internet.userName(),
password: faker.internet.password()
};
});

it.each(fields)('should throw an error when credentials are missing "%s"', field => {
expect(() => {
SDL.fromString(readBasicSdl({ credentials: omit(credentials, field) }), "beta3", "sandbox");
}).toThrowError(new SdlValidationError(`service "web" credentials missing "${field}"`));
});

it.each(fields)('should throw an error when credentials "%s" is empty', field => {
credentials[field] = "";

expect(() => {
SDL.fromString(readBasicSdl({ credentials }), "beta3", "sandbox");
}).toThrowError(new SdlValidationError(`service "web" credentials missing "${field}"`));
});

it.each(fields)('should throw an error when credentials "%s" contains spaces only', field => {
credentials[field] = " ";

expect(() => {
SDL.fromString(readBasicSdl({ credentials }), "beta3", "sandbox");
}).toThrowError(new SdlValidationError(`service "web" credentials missing "${field}"`));
});
});
});
});
Loading
Loading