- Implements Apache APISIX packaging for Cloudron platform. - Includes Dockerfile, CloudronManifest.json, and start.sh. - Configured to use Cloudron's etcd addon. 🤖 Generated with Gemini CLI Co-Authored-By: Gemini <noreply@google.com>
443 lines
14 KiB
TypeScript
443 lines
14 KiB
TypeScript
/*
|
|
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
* contributor license agreements. See the NOTICE file distributed with
|
|
* this work for additional information regarding copyright ownership.
|
|
* The ASF licenses this file to You under the Apache License, Version 2.0
|
|
* (the "License"); you may not use this file except in compliance with
|
|
* the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import axios from "axios";
|
|
import YAML from "yaml";
|
|
|
|
const ENDPOINT = "/apisix/admin/configs";
|
|
const clientConfig = {
|
|
baseURL: "http://localhost:1984",
|
|
headers: {
|
|
"X-API-KEY": "edd1c9f034335f136f87ad84b625c8f1",
|
|
},
|
|
};
|
|
const config1 = {
|
|
routes: [
|
|
{
|
|
id: "r1",
|
|
uri: "/r1",
|
|
upstream: {
|
|
nodes: { "127.0.0.1:1980": 1 },
|
|
type: "roundrobin",
|
|
},
|
|
plugins: { "proxy-rewrite": { uri: "/hello" } },
|
|
},
|
|
],
|
|
};
|
|
const config2 = {
|
|
routes: [
|
|
{
|
|
id: "r2",
|
|
uri: "/r2",
|
|
upstream: {
|
|
nodes: { "127.0.0.1:1980": 1 },
|
|
type: "roundrobin",
|
|
},
|
|
plugins: { "proxy-rewrite": { uri: "/hello" } },
|
|
},
|
|
],
|
|
};
|
|
const invalidConfVersionConfig1 = {
|
|
routes_conf_version: -1,
|
|
};
|
|
const invalidConfVersionConfig2 = {
|
|
routes_conf_version: "adc",
|
|
};
|
|
const routeWithModifiedIndex = {
|
|
routes: [
|
|
{
|
|
id: "r1",
|
|
uri: "/r1",
|
|
modifiedIndex: 1,
|
|
upstream: {
|
|
nodes: { "127.0.0.1:1980": 1 },
|
|
type: "roundrobin",
|
|
},
|
|
plugins: { "proxy-rewrite": { uri: "/hello" } },
|
|
},
|
|
],
|
|
};
|
|
const routeWithKeyAuth = {
|
|
routes: [
|
|
{
|
|
id: "r1",
|
|
uri: "/r1",
|
|
upstream: {
|
|
nodes: { "127.0.0.1:1980": 1 },
|
|
type: "roundrobin",
|
|
},
|
|
plugins: {
|
|
"proxy-rewrite": { uri: "/hello" },
|
|
"key-auth": {},
|
|
},
|
|
},
|
|
]
|
|
}
|
|
const consumerWithModifiedIndex = {
|
|
routes: routeWithKeyAuth.routes,
|
|
consumers: [
|
|
{
|
|
modifiedIndex: 10,
|
|
username: "jack",
|
|
plugins: {
|
|
"key-auth": {
|
|
key: "jack-key",
|
|
}
|
|
},
|
|
},
|
|
],
|
|
}
|
|
const credential1 = {
|
|
routes: routeWithKeyAuth.routes,
|
|
consumers: [
|
|
{
|
|
"username": "john_1"
|
|
},
|
|
{
|
|
"id": "john_1/credentials/john-a",
|
|
"plugins": {
|
|
"key-auth": {
|
|
"key": "auth-a"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"id": "john_1/credentials/john-b",
|
|
"plugins": {
|
|
"key-auth": {
|
|
"key": "auth-b"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
|
|
describe("Admin - Standalone", () => {
|
|
const client = axios.create(clientConfig);
|
|
client.interceptors.response.use((response) => {
|
|
const contentType = response.headers["content-type"] || "";
|
|
if (
|
|
contentType.includes("application/yaml") &&
|
|
typeof response.data === "string" &&
|
|
response.config.responseType !== "text"
|
|
)
|
|
response.data = YAML.parse(response.data);
|
|
return response;
|
|
});
|
|
|
|
describe("Normal", () => {
|
|
it("dump empty config (default json format)", async () => {
|
|
const resp = await client.get(ENDPOINT);
|
|
expect(resp.status).toEqual(200);
|
|
expect(resp.data.routes_conf_version).toEqual(0);
|
|
expect(resp.data.ssls_conf_version).toEqual(0);
|
|
expect(resp.data.services_conf_version).toEqual(0);
|
|
expect(resp.data.upstreams_conf_version).toEqual(0);
|
|
expect(resp.data.consumers_conf_version).toEqual(0);
|
|
});
|
|
|
|
it("dump empty config (yaml format)", async () => {
|
|
const resp = await client.get(ENDPOINT, {
|
|
headers: { Accept: "application/yaml" },
|
|
});
|
|
expect(resp.status).toEqual(200);
|
|
expect(resp.headers["content-type"]).toEqual("application/yaml");
|
|
expect(resp.data.routes_conf_version).toEqual(0);
|
|
expect(resp.data.ssls_conf_version).toEqual(0);
|
|
expect(resp.data.services_conf_version).toEqual(0);
|
|
expect(resp.data.upstreams_conf_version).toEqual(0);
|
|
expect(resp.data.consumers_conf_version).toEqual(0);
|
|
});
|
|
|
|
it("update config (add routes, by json)", async () => {
|
|
const resp = await client.put(ENDPOINT, config1);
|
|
expect(resp.status).toEqual(202);
|
|
});
|
|
|
|
it("dump config (json format)", async () => {
|
|
const resp = await client.get(ENDPOINT);
|
|
expect(resp.status).toEqual(200);
|
|
expect(resp.data.routes_conf_version).toEqual(1);
|
|
expect(resp.data.ssls_conf_version).toEqual(1);
|
|
expect(resp.data.services_conf_version).toEqual(1);
|
|
expect(resp.data.upstreams_conf_version).toEqual(1);
|
|
expect(resp.data.consumers_conf_version).toEqual(1);
|
|
});
|
|
|
|
it("check default value", async () => {
|
|
const resp = await client.get(ENDPOINT);
|
|
expect(resp.status).toEqual(200);
|
|
expect(resp.data.routes).toEqual(config1.routes);
|
|
});
|
|
|
|
it("dump config (yaml format)", async () => {
|
|
const resp = await client.get(ENDPOINT, {
|
|
headers: { Accept: "application/yaml" },
|
|
responseType: 'text',
|
|
});
|
|
expect(resp.status).toEqual(200);
|
|
expect(resp.data).toContain("routes:")
|
|
expect(resp.data).toContain("id: r1")
|
|
expect(resp.data.startsWith('---')).toBe(false);
|
|
expect(resp.data.endsWith('...')).toBe(false);
|
|
});
|
|
|
|
it('check route "r1"', async () => {
|
|
const resp = await client.get("/r1");
|
|
expect(resp.status).toEqual(200);
|
|
expect(resp.data).toEqual("hello world\n");
|
|
});
|
|
|
|
it("update config (add routes, by yaml)", async () => {
|
|
const resp = await client.put(
|
|
ENDPOINT,
|
|
YAML.stringify(config2),
|
|
{
|
|
headers: { "Content-Type": "application/yaml" },
|
|
}
|
|
);
|
|
expect(resp.status).toEqual(202);
|
|
});
|
|
|
|
it("dump config (json format)", async () => {
|
|
const resp = await client.get(ENDPOINT);
|
|
expect(resp.status).toEqual(200);
|
|
expect(resp.data.routes_conf_version).toEqual(2);
|
|
expect(resp.data.ssls_conf_version).toEqual(2);
|
|
expect(resp.data.services_conf_version).toEqual(2);
|
|
expect(resp.data.upstreams_conf_version).toEqual(2);
|
|
expect(resp.data.consumers_conf_version).toEqual(2);
|
|
});
|
|
|
|
it('check route "r1"', () =>
|
|
expect(client.get("/r1")).rejects.toThrow(
|
|
"Request failed with status code 404"
|
|
));
|
|
|
|
it('check route "r2"', async () => {
|
|
const resp = await client.get("/r2");
|
|
expect(resp.status).toEqual(200);
|
|
expect(resp.data).toEqual("hello world\n");
|
|
});
|
|
|
|
it("update config (delete routes)", async () => {
|
|
const resp = await client.put(
|
|
ENDPOINT,
|
|
{},
|
|
{ params: { conf_version: 3 } }
|
|
);
|
|
expect(resp.status).toEqual(202);
|
|
});
|
|
|
|
it('check route "r2"', () =>
|
|
expect(client.get("/r2")).rejects.toThrow(
|
|
"Request failed with status code 404"
|
|
));
|
|
|
|
it("only set routes_conf_version", async () => {
|
|
const resp = await client.put(
|
|
ENDPOINT,
|
|
YAML.stringify({ routes_conf_version: 15 }),
|
|
{
|
|
headers: { "Content-Type": "application/yaml" },
|
|
});
|
|
expect(resp.status).toEqual(202);
|
|
|
|
const resp_1 = await client.get(ENDPOINT);
|
|
expect(resp_1.status).toEqual(200);
|
|
expect(resp_1.data.routes_conf_version).toEqual(15);
|
|
expect(resp_1.data.ssls_conf_version).toEqual(4);
|
|
expect(resp_1.data.services_conf_version).toEqual(4);
|
|
expect(resp_1.data.upstreams_conf_version).toEqual(4);
|
|
expect(resp_1.data.consumers_conf_version).toEqual(4);
|
|
|
|
const resp2 = await client.put(
|
|
ENDPOINT,
|
|
YAML.stringify({ routes_conf_version: 17 }),
|
|
{
|
|
headers: { "Content-Type": "application/yaml" },
|
|
});
|
|
expect(resp2.status).toEqual(202);
|
|
|
|
const resp2_1 = await client.get(ENDPOINT);
|
|
expect(resp2_1.status).toEqual(200);
|
|
expect(resp2_1.data.routes_conf_version).toEqual(17);
|
|
expect(resp2_1.data.ssls_conf_version).toEqual(5);
|
|
expect(resp2_1.data.services_conf_version).toEqual(5);
|
|
expect(resp2_1.data.upstreams_conf_version).toEqual(5);
|
|
expect(resp2_1.data.consumers_conf_version).toEqual(5);
|
|
});
|
|
|
|
it("control resource changes using modifiedIndex", async () => {
|
|
const c1 = structuredClone(routeWithModifiedIndex);
|
|
c1.routes[0].modifiedIndex = 1;
|
|
|
|
const c2 = structuredClone(c1);
|
|
c2.routes[0].uri = "/r2";
|
|
|
|
const c3 = structuredClone(c2);
|
|
c3.routes[0].modifiedIndex = 2;
|
|
|
|
// Update with c1
|
|
const resp = await client.put(ENDPOINT, c1);
|
|
expect(resp.status).toEqual(202);
|
|
|
|
// Check route /r1 exists
|
|
const resp_1 = await client.get("/r1");
|
|
expect(resp_1.status).toEqual(200);
|
|
|
|
// Update with c2
|
|
const resp2 = await client.put(ENDPOINT, c2);
|
|
expect(resp2.status).toEqual(202);
|
|
|
|
// Check route /r1 exists
|
|
// But it is not applied because the modifiedIndex is the same as the old value
|
|
const resp2_2 = await client.get("/r1");
|
|
expect(resp2_2.status).toEqual(200);
|
|
|
|
// Check route /r2 not exists
|
|
const resp2_1 = await client.get("/r2").catch((err) => err.response);
|
|
expect(resp2_1.status).toEqual(404);
|
|
|
|
// Update with c3
|
|
const resp3 = await client.put(ENDPOINT, c3);
|
|
expect(resp3.status).toEqual(202);
|
|
|
|
// Check route /r1 not exists
|
|
const resp3_1 = await client.get("/r1").catch((err) => err.response);
|
|
expect(resp3_1.status).toEqual(404);
|
|
|
|
// Check route /r2 exists
|
|
const resp3_2 = await client.get("/r2");
|
|
expect(resp3_2.status).toEqual(200);
|
|
});
|
|
|
|
it("apply consumer with modifiedIndex", async () => {
|
|
const resp = await client.put(ENDPOINT, consumerWithModifiedIndex);
|
|
expect(resp.status).toEqual(202);
|
|
|
|
const resp_1 = await client.get("/r1", { headers: { "apikey": "invalid-key" } }).catch((err) => err.response);
|
|
expect(resp_1.status).toEqual(401);
|
|
const resp_2 = await client.get("/r1", { headers: { "apikey": "jack-key" } });
|
|
expect(resp_2.status).toEqual(200);
|
|
|
|
const updatedConsumer = structuredClone(consumerWithModifiedIndex);
|
|
|
|
// update key of key-auth plugin, but modifiedIndex is not changed
|
|
updatedConsumer.consumers[0].plugins["key-auth"] = { "key": "jack-key-updated" };
|
|
const resp2 = await client.put(ENDPOINT, updatedConsumer);
|
|
expect(resp2.status).toEqual(202);
|
|
|
|
const resp2_1 = await client.get("/r1", { headers: { "apikey": "jack-key-updated" } }).catch((err) => err.response);
|
|
expect(resp2_1.status).toEqual(401);
|
|
const resp2_2 = await client.get("/r1", { headers: { "apikey": "jack-key" } });
|
|
expect(resp2_2.status).toEqual(200);
|
|
|
|
// update key of key-auth plugin, and modifiedIndex is changed
|
|
updatedConsumer.consumers[0].modifiedIndex++;
|
|
const resp3 = await client.put(ENDPOINT, updatedConsumer);
|
|
const resp3_1 = await client.get("/r1", { headers: { "apikey": "jack-key-updated" } });
|
|
expect(resp3_1.status).toEqual(200);
|
|
const resp3_2 = await client.get("/r1", { headers: { "apikey": "jack-key" } }).catch((err) => err.response);
|
|
expect(resp3_2.status).toEqual(401);
|
|
});
|
|
|
|
it("apply consumer with credentials", async () => {
|
|
const resp = await client.put(ENDPOINT, credential1);
|
|
expect(resp.status).toEqual(202);
|
|
|
|
const resp_1 = await client.get("/r1", { headers: { "apikey": "auth-a" } });
|
|
expect(resp_1.status).toEqual(200);
|
|
const resp_2 = await client.get("/r1", { headers: { "apikey": "auth-b" } });
|
|
expect(resp_2.status).toEqual(200);
|
|
const resp_3 = await client.get("/r1", { headers: { "apikey": "invalid-key" } }).catch((err) => err.response);
|
|
expect(resp_3.status).toEqual(401);
|
|
});
|
|
});
|
|
|
|
describe("Exceptions", () => {
|
|
const clientException = axios.create({
|
|
...clientConfig,
|
|
validateStatus: () => true,
|
|
});
|
|
|
|
it("update config (lower conf_version)", async () => {
|
|
const resp = await clientException.put(
|
|
ENDPOINT,
|
|
{ routes_conf_version: 100 },
|
|
{ headers: { "Content-Type": "application/yaml" } }
|
|
);
|
|
const resp2 = await clientException.put(
|
|
ENDPOINT,
|
|
YAML.stringify(invalidConfVersionConfig1),
|
|
{ headers: { "Content-Type": "application/yaml" } }
|
|
);
|
|
expect(resp2.status).toEqual(400);
|
|
expect(resp2.data).toEqual({
|
|
error_msg:
|
|
"routes_conf_version must be greater than or equal to (100)",
|
|
});
|
|
});
|
|
|
|
it("update config (invalid conf_version)", async () => {
|
|
const resp = await clientException.put(
|
|
ENDPOINT,
|
|
YAML.stringify(invalidConfVersionConfig2),
|
|
{
|
|
headers: {
|
|
"Content-Type": "application/yaml",
|
|
},
|
|
}
|
|
);
|
|
expect(resp.status).toEqual(400);
|
|
expect(resp.data).toEqual({
|
|
error_msg: "routes_conf_version must be a number",
|
|
});
|
|
});
|
|
|
|
it("update config (invalid json format)", async () => {
|
|
const resp = await clientException.put(ENDPOINT, "{abcd", {
|
|
params: { conf_version: 4 },
|
|
});
|
|
expect(resp.status).toEqual(400);
|
|
expect(resp.data).toEqual({
|
|
error_msg:
|
|
"invalid request body: Expected object key string but found invalid token at character 2",
|
|
});
|
|
});
|
|
|
|
it("update config (not compliant with jsonschema)", async () => {
|
|
const data = structuredClone(config1);
|
|
(data.routes[0].uri as unknown) = 123;
|
|
const resp = await clientException.put(ENDPOINT, data);
|
|
expect(resp.status).toEqual(400);
|
|
expect(resp.data).toMatchObject({
|
|
error_msg:
|
|
'invalid routes at index 0, err: property "uri" validation failed: wrong type: expected string, got number',
|
|
});
|
|
});
|
|
|
|
it("update config (empty request body)", async () => {
|
|
const resp = await clientException.put(ENDPOINT, "");
|
|
expect(resp.status).toEqual(400);
|
|
expect(resp.data).toEqual({
|
|
error_msg: "invalid request body: empty request body",
|
|
});
|
|
});
|
|
});
|
|
});
|