Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,24 @@ on:
- main
types: [opened, synchronize, reopened, labeled]
jobs:
cli:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# Build and test ADC CLI
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
- uses: pnpm/action-setup@v2
with:
version: latest
- name: Install dependencies
run: pnpm install

# Run E2E tests
- name: Run E2E tests
run: npx nx run cli:e2e
apisix:
runs-on: ubuntu-latest
strategy:
Expand Down
18 changes: 0 additions & 18 deletions apps/cli-e2e/.eslintrc.json

This file was deleted.

5 changes: 0 additions & 5 deletions apps/cli-e2e/README.md

This file was deleted.

26 changes: 0 additions & 26 deletions apps/cli-e2e/project.json

This file was deleted.

3 changes: 0 additions & 3 deletions apps/cli-e2e/src/support/global-setup.ts

This file was deleted.

13 changes: 0 additions & 13 deletions apps/cli-e2e/tsconfig.json

This file was deleted.

9 changes: 0 additions & 9 deletions apps/cli-e2e/tsconfig.spec.json

This file was deleted.

80 changes: 80 additions & 0 deletions apps/cli/e2e/server/basic.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import * as ADCSDK from '@api7/adc-sdk';
import { lastValueFrom } from 'rxjs';
import request from 'supertest';

import { ADCServer } from '../../src/server';
import { jestMockBackend } from '../support/utils';

describe('Server - Basic', () => {
let mockedBackend: ADCSDK.Backend;
let server: ADCServer;

beforeAll(async () => {
mockedBackend = jestMockBackend();
server = new ADCServer();
});

it('test mocked load backend', async () => {
const { status, body } = await request(server.TEST_ONLY_getExpress())
.put('/sync')
.send({
task: {
opts: {
backend: 'mock',
server: 'http://1.1.1.1:3000',
token: 'mock',
},
config: {},
},
});

expect(status).toBe(202);
expect(body.status).toBe('success');
await expect(lastValueFrom(mockedBackend.dump())).resolves.toEqual({});
});

it('test real apisix backend (expect connect refused)', async () => {
const { status, body } = await request(server.TEST_ONLY_getExpress())
.put('/sync')
.send({
task: {
opts: {
backend: 'apisix',
server: 'http://127.0.0.1:50000',
token: 'mock',
},
config: {},
},
});

expect(status).toBe(500);
expect(body.message).toBe('Error: connect ECONNREFUSED 127.0.0.1:50000');
});

it('test sync', async () => {
const config = {
consumers: [
{
username: 'test-consumer',
plugins: { 'limit-count': { count: 10, time_window: 60 } },
},
],
} as ADCSDK.Configuration;
const { status, body } = await request(server.TEST_ONLY_getExpress())
.put('/sync')
.send({
task: {
opts: {
backend: 'mock',
server: 'http://1.1.1.1:3000',
token: 'mock',
},
config: config,
},
});

expect(status).toBe(202);
expect(body.status).toBe('success');
await expect(lastValueFrom(mockedBackend.dump())).resolves.toEqual(config);
});
});
5 changes: 5 additions & 0 deletions apps/cli/e2e/support/global-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { configurePluralize } from '../../src/command/utils';

export default () => {
configurePluralize();
};
67 changes: 67 additions & 0 deletions apps/cli/e2e/support/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as ADCSDK from '@api7/adc-sdk';
import pluralize from 'pluralize';
import { Subscription, from, of, switchMap, tap, toArray } from 'rxjs';
import { SemVer } from 'semver';

import * as commandUtils from '../../src/command/utils';

export const mockBackend = (): ADCSDK.Backend => {
class MockBackend implements ADCSDK.Backend {
private readonly cache: { config?: ADCSDK.Configuration } = {};

public metadata() {
return { logScope: ['mock'] };
}
public ping() {
return Promise.resolve();
}
public version() {
return Promise.resolve(new SemVer('0.0.0-mock'));
}
public defaultValue() {
return Promise.resolve({});
}
public dump() {
return of(this.cache.config);
}
public sync(events: Array<ADCSDK.Event>) {
const config: ADCSDK.Configuration = {};
const resourceTypeToPluralKey = (type: ADCSDK.ResourceType) =>
pluralize.plural(type);
return from(events).pipe(
tap((event) => {
if (event.type === ADCSDK.EventType.DELETE) return;
const key = resourceTypeToPluralKey(event.resourceType);
if (!config[key]) config[key] = [];
config[key].push(event.newValue);
}),
toArray(),
switchMap(
() => (
(this.cache.config = config),
of({
success: true,
event: {} as ADCSDK.Event, // keep empty
} as ADCSDK.BackendSyncResult)
),
),
);
}
on: () => Subscription;
supportValidate?: () => Promise<boolean>;
supportStreamRoute?: () => Promise<boolean>;
}
return new MockBackend();
};

export const jestMockBackend = (mockedBackend?: ADCSDK.Backend) => {
if (!mockedBackend) mockedBackend = mockBackend();
const originalLoadBackend = commandUtils.loadBackend;
jest
.spyOn(commandUtils, 'loadBackend')
.mockImplementation((backend, opts) => {
if (backend === 'mock') return mockedBackend;
return originalLoadBackend(backend, opts);
});
return mockedBackend;
};
9 changes: 5 additions & 4 deletions apps/cli-e2e/jest.config.ts → apps/cli/jest.config.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* eslint-disable */
export default {
displayName: 'cli-e2e',
displayName: 'cli',
preset: '../../jest.preset.js',
globalSetup: '<rootDir>/src/support/global-setup.ts',
globalTeardown: '<rootDir>/src/support/global-teardown.ts',
globalSetup: '<rootDir>/e2e/support/global-setup.ts',
globalTeardown: '<rootDir>/e2e/support/global-teardown.ts',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': [
Expand All @@ -14,5 +14,6 @@ export default {
],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/cli-e2e',
coverageDirectory: '../../coverage/libs/backend-api7/e2e',
testMatch: ['**/?(*.)+(e2e-spec).[jt]s?(x)'],
};
11 changes: 9 additions & 2 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
{
"private": true,
"devDependencies": {
"@api7/adc-sdk": "workspace:*",
"@api7/adc-backend-api7": "workspace:*",
"@api7/adc-backend-apisix": "workspace:*",
"@api7/adc-backend-apisix-standalone": "workspace:*",
"@api7/adc-converter-openapi": "workspace:*"
"@api7/adc-converter-openapi": "workspace:*",
"@api7/adc-sdk": "workspace:*",
"@types/express": "^5.0.3",
"@types/supertest": "^6.0.3",
"supertest": "^7.1.4"
},
"dependencies": {
"body-parser": "^2.2.0",
"express": "^5.1.0"
}
}
11 changes: 11 additions & 0 deletions apps/cli/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@
"jestConfig": "apps/cli/jest.config.ts"
}
},
"e2e": {
"executor": "@nx/jest:jest",
"outputs": [
"{workspaceRoot}/coverage/{e2eProjectRoot}"
],
"options": {
"jestConfig": "apps/cli/jest.config.e2e.ts",
"passWithNoTests": true,
"runInBand": true
}
},
"export-schema": {
"executor": "nx:run-commands",
"options": {
Expand Down
43 changes: 24 additions & 19 deletions apps/cli/src/command/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import { resolve } from 'node:path';
import parseDuration from 'parse-duration';
import qs from 'qs';

export class BaseCommand extends Command {
export interface BaseOptions {
verbose: boolean;
}
export class BaseCommand<
OPTS extends BaseOptions = BaseOptions,
> extends Command {
private examples: Array<{ title: string; command: string }> = [];

constructor(name: string, summary?: string, description?: string) {
Expand Down Expand Up @@ -49,34 +54,34 @@ export class BaseCommand extends Command {
this.description(`${currDescription}\n\n${exampleHeader}\n${exampleText}`);
return this;
}

public handle(cb: (opts: OPTS, command: Command) => void | Promise<void>) {
this.action((_, command: Command) => cb(command.opts<OPTS>(), command));
return this;
}
}

export class BackendCommand<OPTS extends object = object> extends BaseCommand {
export class BackendCommand<
OPTS extends BaseOptions = BaseOptions,
> extends BaseCommand<OPTS> {
constructor(name: string, summary?: string, description?: string) {
super(name, summary, description);

this.addBackendOptions();
}

public handle(cb: (opts: OPTS, command: Command) => void | Promise<void>) {
this.action(async (_, command: Command) => {
const opts = command.opts<OPTS>();

if (
(has(opts, 'tlsClientCertFile') && !has(opts, 'tlsClientKeyFile')) ||
(has(opts, 'tlsClientKeyFile') && !has(opts, 'tlsClientCertFile'))
) {
console.log(
chalk.red(
'TLS client certificate and key must be provided at the same time',
),
);
return;
}
const opts = this.opts<OPTS>();

await cb(opts, command);
});
return this;
if (!has(opts, 'tlsClientCertFile') || !has(opts, 'tlsClientKeyFile')) {
console.log(
chalk.red(
'TLS client certificate and key must be provided at the same time',
),
);
return this;
}
return super.handle(cb);
}

private addBackendOptions() {
Expand Down
Loading