Skip to content

Commit

Permalink
chore(ci): adds unit tests + coverage for deploy-web (#800)
Browse files Browse the repository at this point in the history
  • Loading branch information
stalniy authored Feb 8, 2025
1 parent f3e4991 commit dbf1944
Show file tree
Hide file tree
Showing 16 changed files with 990 additions and 27 deletions.
49 changes: 47 additions & 2 deletions .github/workflows/docker-build-deploy-web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name: Deploy Web CI
on:
pull_request:
branches: ["main"]
push:
branches: ["main"]

jobs:
build-deploy-web:
Expand All @@ -14,12 +16,55 @@ jobs:

# Check for changes in deploy-web folder
- uses: dorny/paths-filter@v2
id: filter
id: has_changes
with:
filters: |
deploy-web:
- 'apps/deploy-web/**'
- 'packages/*/**'
- name: Decide whether to validate
id: validation
run: echo "enabled=${{ github.event_name == 'push' || steps.has_changes.outputs.deploy-web == 'true' }}" >> $GITHUB_OUTPUT

- name: Setup Node.js
if: steps.validation.outputs.enabled == 'true'
uses: actions/setup-node@v4
with:
node-version: 20.14.0

- name: Restore root node_modules cache
if: steps.validation.outputs.enabled == 'true'
uses: actions/cache@v4
id: cache
with:
path: |
node_modules
apps/deploy-web/node_modules
packages/*/node_modules
key: deploy-web-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

- name: Install dependencies
if: steps.validation.outputs.enabled == 'true' && steps.cache.outputs.cache-hit != 'true'
run: npm ci

- name: Run static code analysis
if: steps.validation.outputs.enabled == 'true'
run: npm run lint -w apps/deploy-web -- --quiet

- name: Run tests
if: steps.validation.outputs.enabled == 'true'
run: npm run test:cov --workspace=apps/deploy-web

- name: Upload Test Coverage
if: steps.validation.outputs.enabled == 'true'
uses: codecov/codecov-action@v5
with:
directory: ./apps/deploy-web/coverage
flags: deploy-web
token: ${{ secrets.CODECOV_TOKEN }}
base_sha: ${{ github.event.pull_request.base.sha }}

- name: Build the Docker image
if: steps.filter.outputs.deploy-web == 'true'
if: steps.validation.outputs.enabled == 'true' && github.event_name == 'pull_request'
run: packages/docker/script/dc.sh build deploy-web --build-arg DEPLOYMENT_ENV=production
9 changes: 9 additions & 0 deletions apps/deploy-web/__mocks__/next/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import packageJSON from "../../package.json";

export default function getConfig() {
return {
publicRuntimeConfig: {
version: packageJSON.version
}
};
}
24 changes: 24 additions & 0 deletions apps/deploy-web/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import nextJest from "next/jest.js";

const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./"
});

const styleMockPath = "<rootDir>/../../node_modules/next/dist/build/jest/__mocks__/styleMock.js";
const getConfig = createJestConfig({
testEnvironment: "jsdom",
collectCoverageFrom: ["<rootDir>/src/**/*.{js,ts,tsx}"],
testMatch: ["<rootDir>/tests/unit/**/*.spec.{tsx,ts}"],
transform: {
"\\.tsx?$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.json" }]
},
moduleNameMapper: {
"^@src(.*)$": "<rootDir>/src/$1",
"@interchain-ui\\/react\\/styles$": styleMockPath,
"@interchain-ui\\/react\\/globalStyles$": styleMockPath
},
setupFilesAfterEnv: ["<rootDir>/tests/unit/setup.ts"]
});

export default getConfig;
8 changes: 7 additions & 1 deletion apps/deploy-web/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ try {
}
}

const transpilePackages = ["geist", "@akashnetwork/ui"];

if (process.env.NODE_ENV === "test") {
transpilePackages.push("nanoid", "uint8arrays", "multiformats");
}

/**
* @type {import('next').NextConfig}
*/
Expand All @@ -39,7 +45,7 @@ const moduleExports = {
eslint: {
ignoreDuringBuilds: true
},
transpilePackages: ["geist", "@akashnetwork/ui"],
transpilePackages,
experimental: {
instrumentationHook: true
},
Expand Down
8 changes: 8 additions & 0 deletions apps/deploy-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"lint": "eslint .",
"release": "release-it",
"start": "next start",
"test:cov": "DEPLOYMENT_ENV=staging jest --coverage",
"test:unit": "DEPLOYMENT_ENV=staging jest",
"type-check": "tsc"
},
"dependencies": {
Expand Down Expand Up @@ -119,10 +121,14 @@
"devDependencies": {
"@akashnetwork/dev-config": "*",
"@chain-registry/types": "^0.50.12",
"@faker-js/faker": "^9.4.0",
"@keplr-wallet/types": "^0.12.111",
"@next/bundle-analyzer": "^14.0.1",
"@octokit/openapi-types": "^22.2.0",
"@playwright/test": "^1.45.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@types/auth0": "^2.35.3",
"@types/file-saver": "^2.0.5",
"@types/gtag.js": "^0.0.20",
Expand All @@ -139,6 +145,8 @@
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.3",
"eslint-plugin-simple-import-sort": "^12.1.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"postcss": "^8.4.31",
"postcss-nesting": "^12.0.2",
"prettier": "^3.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type Props = {

export const GranteeRow: React.FunctionComponent<Props> = ({ grant }) => {
const limit = grant?.authorization?.spend_limit;
const denomData = limit ? useDenomData(limit.denom) : null;
const denomData = useDenomData(limit?.denom);

return (
<TableRow className="[&>td]:px-2 [&>td]:py-1">
Expand Down
22 changes: 15 additions & 7 deletions apps/deploy-web/src/components/providers/ProviderRawData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,19 @@ import Layout from "../layout/Layout";
import { CustomNextSeo } from "../shared/CustomNextSeo";
import ProviderDetailLayout, { ProviderDetailTabs } from "./ProviderDetailLayout";

export const COMPONENTS = {
Layout,
CustomNextSeo,
ProviderDetailLayout,
DynamicReactJson
};

type Props = {
owner: string;
components?: typeof COMPONENTS;
};

export const ProviderRawData: React.FunctionComponent<Props> = ({ owner }) => {
export const ProviderRawData: React.FunctionComponent<Props> = ({ owner, components: c = COMPONENTS }) => {
const [provider, setProvider] = useState<Partial<ClientProviderDetailWithStatus> | null>(null);
const { isLoading: isLoadingProvider, refetch: getProviderDetail } = useProviderDetail(owner, {
enabled: false,
Expand Down Expand Up @@ -59,12 +67,12 @@ export const ProviderRawData: React.FunctionComponent<Props> = ({ owner }) => {
};

return (
<Layout isLoading={isLoadingLeases || isLoadingProvider || isLoadingStatus}>
<CustomNextSeo title={`Provider raw data for ${owner}`} url={`${domainName}${UrlService.providerDetailRaw(owner)}`} />
<c.Layout isLoading={isLoadingLeases || isLoadingProvider || isLoadingStatus}>
<c.CustomNextSeo title={`Provider raw data for ${owner}`} url={`${domainName}${UrlService.providerDetailRaw(owner)}`} />

<ProviderDetailLayout address={owner} page={ProviderDetailTabs.RAW} refresh={refresh} provider={provider as ClientProviderDetailWithStatus}>
{provider && <DynamicReactJson src={JSON.parse(JSON.stringify(provider))} collapsed={1} />}
</ProviderDetailLayout>
</Layout>
<c.ProviderDetailLayout address={owner} page={ProviderDetailTabs.RAW} refresh={refresh} provider={provider as ClientProviderDetailWithStatus}>
{provider && <c.DynamicReactJson src={JSON.parse(JSON.stringify(provider))} collapsed={1} />}
</c.ProviderDetailLayout>
</c.Layout>
);
};
2 changes: 1 addition & 1 deletion apps/deploy-web/src/hooks/useWalletBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ type DenomData = {
balance: number;
};

export const useDenomData = (denom: string) => {
export const useDenomData = (denom?: string) => {
const { isLoaded, price, aktToUSD } = usePricing();
const { balance: walletBalance } = useWalletBalance();
const [depositData, setDepositData] = useState<DenomData | null>(null);
Expand Down
9 changes: 6 additions & 3 deletions apps/deploy-web/src/queries/useLeaseQuery.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { useQuery } from "react-query";
import { AxiosStatic } from "axios";

import { useServices } from "@src/context/ServicesProvider";
import { useScopedFetchProviderUrl } from "@src/hooks/useScopedFetchProviderUrl";
import { LeaseDto, RpcLease } from "@src/types/deployment";
import { ApiProviderList } from "@src/types/provider";
Expand Down Expand Up @@ -28,12 +30,12 @@ export function useDeploymentLeaseList(address: string, deployment, options) {
return useQuery(QueryKeys.getLeasesKey(address, deployment?.dseq), () => getDeploymentLeases(settings.apiEndpoint, address, deployment), options);
}

async function getAllLeases(apiEndpoint: string, address: string, deployment?) {
async function getAllLeases(apiEndpoint: string, address: string, deployment?: any, httpClient?: AxiosStatic) {
if (!address) {
return null;
}

const response = await loadWithPagination<RpcLease[]>(ApiUrlService.leaseList(apiEndpoint, address, deployment?.dseq), "leases", 1000);
const response = await loadWithPagination<RpcLease[]>(ApiUrlService.leaseList(apiEndpoint, address, deployment?.dseq), "leases", 1000, httpClient);

const leases = response.map(l => leaseToDto(l, deployment));

Expand All @@ -42,7 +44,8 @@ async function getAllLeases(apiEndpoint: string, address: string, deployment?) {

export function useAllLeases(address: string, options = {}) {
const { settings } = useSettings();
return useQuery(QueryKeys.getAllLeasesKey(address), () => getAllLeases(settings.apiEndpoint, address), options);
const { axios } = useServices();
return useQuery(QueryKeys.getAllLeasesKey(address), () => getAllLeases(settings.apiEndpoint, address, undefined, axios), options);
}

export function useLeaseStatus(provider: ApiProviderList | undefined, lease: LeaseDto, options) {
Expand Down
4 changes: 2 additions & 2 deletions apps/deploy-web/src/utils/apiUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export class ApiUrlService {

// TODO: implement proper pagination on clients
// Issue: https://github.com/akash-network/console/milestone/7
export async function loadWithPagination<T>(baseUrl: string, dataKey: string, limit: number) {
export async function loadWithPagination<T>(baseUrl: string, dataKey: string, limit: number, httpClient = axios) {
let items = [];
let nextKey = null;
// let callCount = 1;
Expand All @@ -140,7 +140,7 @@ export async function loadWithPagination<T>(baseUrl: string, dataKey: string, li
queryUrl += "&pagination.key=" + encodeURIComponent(nextKey);
}
// console.log(`Querying ${dataKey} [${callCount}] from : ${queryUrl}`);
const response = await axios.get(queryUrl);
const response = await httpClient.get(queryUrl);
const data = response.data;

// if (!nextKey) {
Expand Down
105 changes: 105 additions & 0 deletions apps/deploy-web/tests/seeders/provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { faker } from "@faker-js/faker";

import { ApiProviderDetail } from "@src/types/provider";

export function buildProvider(overrides?: Partial<ApiProviderDetail>): ApiProviderDetail {
return {
owner: `akash${faker.string.alphanumeric({ length: 39 })}`,
name: `${faker.internet.domainWord()}.${faker.internet.domainWord()}.${faker.internet.domainName()}`,
hostUri: `https://${faker.internet.domainName()}:8443`,
createdHeight: faker.number.int({ min: 100000, max: 500000 }),
email: faker.internet.email(),
website: faker.internet.url(),
lastCheckDate: faker.date.recent(),
deploymentCount: faker.number.int({ min: 0, max: 100 }),
leaseCount: faker.number.int({ min: 0, max: 100 }),
cosmosSdkVersion: `0.${faker.number.int({ min: 40, max: 50 })}.${faker.number.int({ min: 0, max: 20 })}`,
akashVersion: `0.${faker.number.int({ min: 5, max: 10 })}.${faker.number.int({ min: 0, max: 10 })}`,
ipRegion: faker.location.state(),
ipRegionCode: faker.location.state({ abbreviated: true }),
ipCountry: faker.location.country(),
ipCountryCode: faker.location.countryCode(),
ipLat: faker.location.latitude().toString(),
ipLon: faker.location.longitude().toString(),
stats: {
cpu: {
active: faker.number.int({ min: 0, max: 10 }),
available: faker.number.int({ min: 5000, max: 20000 }),
pending: faker.number.int({ min: 0, max: 5 })
},
gpu: {
active: faker.number.int({ min: 0, max: 2 }),
available: faker.number.int({ min: 1, max: 4 }),
pending: faker.number.int({ min: 0, max: 2 })
},
memory: {
active: faker.number.int({ min: 0, max: 10000000000 }),
available: faker.number.int({ min: 8000000000, max: 64000000000 }),
pending: faker.number.int({ min: 0, max: 1000000000 })
},
storage: {
ephemeral: {
active: faker.number.int({ min: 0, max: 10000000000 }),
available: faker.number.int({ min: 50000000000, max: 200000000000 }),
pending: faker.number.int({ min: 0, max: 5000000000 })
},
persistent: {
active: faker.number.int({ min: 0, max: 5000000000 }),
available: faker.number.int({ min: 10000000000, max: 50000000000 }),
pending: faker.number.int({ min: 0, max: 2000000000 })
}
}
},
gpuModels: [
{
vendor: "nvidia",
model: "rtx4000",
ram: "8Gi",
interface: "PCIe"
}
],
uptime1d: faker.number.float({ min: 0.9, max: 1, fractionDigits: 4 }),
uptime7d: faker.number.float({ min: 0.9, max: 1, fractionDigits: 4 }),
uptime30d: faker.number.float({ min: 0.9, max: 1, fractionDigits: 4 }),
isValidVersion: faker.datatype.boolean(),
isOnline: faker.datatype.boolean(),
lastOnlineDate: faker.date.recent().toISOString(),
isAudited: faker.datatype.boolean(),
attributes: [
{ key: "region", value: "us-east", auditedBy: [faker.string.alphanumeric(42)] },
{ key: "host", value: faker.internet.domainWord(), auditedBy: [faker.string.alphanumeric(42)] },
{ key: "tier", value: "community", auditedBy: [faker.string.alphanumeric(42)] },
{ key: "organization", value: "overclock", auditedBy: [faker.string.alphanumeric(42)] },
{ key: "capabilities/storage/1/class", value: "beta3", auditedBy: [faker.string.alphanumeric(42)] },
{ key: "capabilities/storage/1/persistent", value: "true", auditedBy: [faker.string.alphanumeric(42)] },
{ key: "capabilities/gpu/vendor/nvidia/model/rtx4000", value: "true", auditedBy: [faker.string.alphanumeric(42)] }
],
host: faker.internet.domainWord(),
organization: faker.company.name(),
statusPage: null,
locationRegion: faker.location.state(),
country: faker.location.countryCode(),
city: faker.location.city(),
timezone: faker.location.timeZone(),
locationType: "",
hostingProvider: "",
hardwareCpu: "",
hardwareCpuArch: "",
hardwareGpuVendor: "",
hardwareGpuModels: ["Nvidia Quadro RTX 4000"],
hardwareDisk: ["hdd"],
featPersistentStorage: faker.datatype.boolean(),
featPersistentStorageType: ["hdd"],
hardwareMemory: "",
networkProvider: "",
networkSpeedDown: faker.number.int({ min: 0, max: 1000 }),
networkSpeedUp: faker.number.int({ min: 0, max: 1000 }),
tier: "Community hosted provider",
featEndpointCustomDomain: faker.datatype.boolean(),
workloadSupportChia: faker.datatype.boolean(),
workloadSupportChiaCapabilities: [],
featEndpointIp: faker.datatype.boolean(),
uptime: [],
...overrides
} as ApiProviderDetail;
}
Loading

0 comments on commit dbf1944

Please sign in to comment.