feat(apisix): add Cloudron package
- 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>
This commit is contained in:
442
CloudronPackages/APISIX/apisix-source/t/admin/standalone.spec.ts
Normal file
442
CloudronPackages/APISIX/apisix-source/t/admin/standalone.spec.ts
Normal file
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
* 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",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user