parent
df5bfe4e1a
commit
fd3783cc8d
@ -0,0 +1,43 @@
|
||||
import { beforeEach, describe, expect, it } from "bun:test";
|
||||
import { ApplicationUpdateStarted } from "./events/ApplicationUpdateStarted";
|
||||
import { TestApp } from "./testing/TestApp";
|
||||
|
||||
describe("AppQueries", () => {
|
||||
describe("pendingApplicationUpdates", () => {
|
||||
let app: TestApp;
|
||||
beforeEach(() => {
|
||||
app = new TestApp([
|
||||
new ApplicationUpdateStarted(
|
||||
{ id: "mail", newVersion: "1.0.0" },
|
||||
new Date()
|
||||
),
|
||||
new ApplicationUpdateStarted(
|
||||
{ id: "blog", newVersion: "10.0.1" },
|
||||
new Date()
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return an empty array when there are no pending updates", () => {
|
||||
const app = new TestApp();
|
||||
expect(app.queries.pendingApplicationUpdates()).toEqual([]);
|
||||
});
|
||||
|
||||
it("should return all pending updates when no appName is provided", () => {
|
||||
expect(app.queries.pendingApplicationUpdates()).toEqual([
|
||||
{ id: "mail", newVersion: "1.0.0" },
|
||||
{ id: "blog", newVersion: "10.0.1" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return all pending updates for the provided application", () => {
|
||||
expect(app.queries.pendingApplicationUpdates("mail")).toEqual([
|
||||
{ id: "mail", newVersion: "1.0.0" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should return an empty array when there are no pending updates for the provided appName", () => {
|
||||
expect(app.queries.pendingApplicationUpdates("unknown")).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,44 @@
|
||||
import AppProjections from "../AppProjections";
|
||||
import AppQueries from "../AppQueries";
|
||||
import EventStore from "../EventStore";
|
||||
|
||||
export class TestApp {
|
||||
readonly eventStore: InMemoryEventStore = new InMemoryEventStore();
|
||||
readonly projections: AppProjections = new AppProjections();
|
||||
readonly queries: AppQueries = new AppQueries(this.projections);
|
||||
|
||||
constructor(history: DomainEvent<any>[] = []) {
|
||||
this.projections.getAll().forEach((projection) => {
|
||||
this.eventStore.subscribe(projection);
|
||||
});
|
||||
|
||||
for (const event of history) {
|
||||
this.eventStore.append(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
class InMemoryEventStore implements EventStore {
|
||||
private handlers: DomainProjection[] = [];
|
||||
private readonly events: DomainEvent<any>[] = [];
|
||||
|
||||
async append(event: DomainEvent<any>): Promise<void> {
|
||||
this.events.push(event);
|
||||
for (const handler of this.handlers) {
|
||||
handler.handle(event);
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(projection: DomainProjection): void {
|
||||
this.handlers.push(projection);
|
||||
}
|
||||
|
||||
async replay(): Promise<void> {
|
||||
throw new Error(
|
||||
"Replay is not relevant for InMemoryEventStore. Use append instead."
|
||||
);
|
||||
}
|
||||
|
||||
getAllEvents(): DomainEvent<any>[] {
|
||||
return this.events;
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"bun-types": "^1.0.25"
|
||||
"bun-types": "^1.0.25",
|
||||
"msw": "latest"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "bun:test";
|
||||
import DockerHub from "./DockerHub";
|
||||
import server from "./mocks/dockerHubServer";
|
||||
|
||||
describe("DockerHub", () => {
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
|
||||
describe("getLatestVersions", () => {
|
||||
it("should return the latest versions of an image", async () => {
|
||||
const dockerHub = new DockerHub();
|
||||
const versions = await dockerHub.getLatestVersions("vaultwarden/server");
|
||||
expect(versions).toEqual([
|
||||
"vaultwarden/server:1.30.5",
|
||||
"vaultwarden/server:1.30.5-alpine",
|
||||
"vaultwarden/server:latest",
|
||||
"vaultwarden/server:latest-alpine",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,145 @@
|
||||
[
|
||||
{
|
||||
"hasPersistentData": false,
|
||||
"description": "",
|
||||
"instanceCount": 1,
|
||||
"captainDefinitionRelativeFilePath": "./captain-definition",
|
||||
"networks": ["captain-overlay-network"],
|
||||
"envVars": [
|
||||
{
|
||||
"key": "ADMINER_PLUGINS",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"key": "ADMINER_DESIGN",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"volumes": [],
|
||||
"ports": [],
|
||||
"versions": [
|
||||
{
|
||||
"version": 0,
|
||||
"timeStamp": "2020-08-02T01:25:07.232Z",
|
||||
"deployedImageName": "img-captain-adminer:0",
|
||||
"gitHash": ""
|
||||
},
|
||||
{
|
||||
"version": 1,
|
||||
"timeStamp": "2021-03-19T10:04:54.823Z",
|
||||
"deployedImageName": "adminer:4.8.0",
|
||||
"gitHash": ""
|
||||
},
|
||||
{
|
||||
"version": 2,
|
||||
"timeStamp": "2021-12-04T10:24:48.757Z",
|
||||
"deployedImageName": "adminer:4.8.1",
|
||||
"gitHash": ""
|
||||
}
|
||||
],
|
||||
"deployedVersion": 2,
|
||||
"notExposeAsWebApp": false,
|
||||
"customDomain": [],
|
||||
"hasDefaultSubDomainSsl": true,
|
||||
"forceSsl": true,
|
||||
"websocketSupport": false,
|
||||
"containerHttpPort": 8080,
|
||||
"preDeployFunction": "",
|
||||
"serviceUpdateOverride": "",
|
||||
"appName": "adminer",
|
||||
"isAppBuilding": false
|
||||
},
|
||||
{
|
||||
"hasPersistentData": false,
|
||||
"description": "",
|
||||
"instanceCount": 1,
|
||||
"captainDefinitionRelativeFilePath": "./captain-definition",
|
||||
"networks": ["captain-overlay-network"],
|
||||
"envVars": [
|
||||
{
|
||||
"key": "MYSQL_ROOT_PASSWORD",
|
||||
"value": "root"
|
||||
},
|
||||
{
|
||||
"key": "MYSQL_DATABASE",
|
||||
"value": "test"
|
||||
},
|
||||
{
|
||||
"key": "MYSQL_USER",
|
||||
"value": "test"
|
||||
},
|
||||
{
|
||||
"key": "MYSQL_PASSWORD",
|
||||
"value": "test"
|
||||
}
|
||||
],
|
||||
"volumes": [],
|
||||
"ports": [],
|
||||
"versions": [
|
||||
{
|
||||
"version": 0,
|
||||
"timeStamp": "2020-08-02T01:25:07.232Z",
|
||||
"deployedImageName": "img-captain-mysql:0",
|
||||
"gitHash": ""
|
||||
},
|
||||
{
|
||||
"version": 1,
|
||||
"timeStamp": "2021-03-19T10:04:54.823Z",
|
||||
"deployedImageName": "mysql:5.7",
|
||||
"gitHash": ""
|
||||
},
|
||||
{
|
||||
"version": 2,
|
||||
"timeStamp": "2021-12-04T10:24:48.757Z",
|
||||
"deployedImageName": "mysql:8.0",
|
||||
"gitHash": ""
|
||||
}
|
||||
],
|
||||
"deployedVersion": 2,
|
||||
"notExposeAsWebApp": false,
|
||||
"customDomain": [],
|
||||
"hasDefaultSubDomainSsl": true,
|
||||
"forceSsl": true,
|
||||
"websocketSupport": false,
|
||||
"containerHttpPort": 3306,
|
||||
"preDeployFunction": "",
|
||||
"serviceUpdateOverride": "",
|
||||
"appName": "mysql",
|
||||
"isAppBuilding": false
|
||||
},
|
||||
{
|
||||
"hasPersistentData": false,
|
||||
"description": "",
|
||||
"instanceCount": 1,
|
||||
"captainDefinitionRelativeFilePath": "./captain-definition",
|
||||
"networks": ["captain-overlay-network"],
|
||||
"envVars": [],
|
||||
"volumes": [],
|
||||
"ports": [],
|
||||
"versions": [
|
||||
{
|
||||
"version": 0,
|
||||
"timeStamp": "2020-08-02T01:25:07.232Z",
|
||||
"deployedImageName": "img-captain-redis:0",
|
||||
"gitHash": ""
|
||||
},
|
||||
{
|
||||
"version": 1,
|
||||
"timeStamp": "2021-03-19T10:04:54.823Z",
|
||||
"deployedImageName": "redis:6.2.3",
|
||||
"gitHash": ""
|
||||
}
|
||||
],
|
||||
"deployedVersion": 1,
|
||||
"notExposeAsWebApp": false,
|
||||
"customDomain": [],
|
||||
"hasDefaultSubDomainSsl": true,
|
||||
"forceSsl": true,
|
||||
"websocketSupport": false,
|
||||
"containerHttpPort": 6379,
|
||||
"preDeployFunction": "",
|
||||
"serviceUpdateOverride": "",
|
||||
"appName": "redis",
|
||||
"isAppBuilding": false
|
||||
}
|
||||
]
|
@ -0,0 +1,99 @@
|
||||
import { setupServer } from "msw/node";
|
||||
import { http, HttpResponse, HttpResponseResolver, PathParams } from "msw";
|
||||
import appsFixtures from "./apps.fixtures.json";
|
||||
|
||||
export const CAPROVER_TEST_DOMAIN = "caprover.test";
|
||||
export const CAPROVER_TEST_PASSWORD = "password";
|
||||
const TEST_TOKEN = "123";
|
||||
const BASE_URI = `https://${CAPROVER_TEST_DOMAIN}/api/v2`;
|
||||
|
||||
const withAuth = (resolver: HttpResponseResolver): HttpResponseResolver => {
|
||||
return (input) => {
|
||||
const headers = input.request.headers;
|
||||
if (
|
||||
headers.get("x-namespace") !== "captain" ||
|
||||
headers.get("x-captain-auth") !== TEST_TOKEN
|
||||
) {
|
||||
return new HttpResponse("", { status: 401 });
|
||||
}
|
||||
return resolver(input);
|
||||
};
|
||||
};
|
||||
|
||||
// @see https://github.com/caprover/caprover-cli/blob/master/src/api/ApiManager.ts
|
||||
// @see https://github.com/caprover/caprover/tree/master/src/routes
|
||||
const handlers = [
|
||||
http.post<PathParams, { password: string }>(
|
||||
`${BASE_URI}/login`,
|
||||
async ({ request }) => {
|
||||
const credentials = await request.json();
|
||||
if (
|
||||
credentials.password !== CAPROVER_TEST_PASSWORD ||
|
||||
request.headers.get("x-namespace") !== "captain"
|
||||
) {
|
||||
return HttpResponse.json({
|
||||
status: 1106,
|
||||
description: "Auth token corrupted",
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
return HttpResponse.json({ data: { token: TEST_TOKEN } });
|
||||
}
|
||||
),
|
||||
http.get(
|
||||
`${BASE_URI}/user/apps/appDefinitions`,
|
||||
withAuth(() => {
|
||||
return HttpResponse.json({
|
||||
data: {
|
||||
appDefinitions: appsFixtures,
|
||||
},
|
||||
});
|
||||
})
|
||||
),
|
||||
http.post<
|
||||
{ name: string },
|
||||
{
|
||||
captainDefinitionContent?: string;
|
||||
tarballFile?: string;
|
||||
gitHash?: string;
|
||||
}
|
||||
>(`${BASE_URI}/user/apps/appData/:name`, async ({ request, params }) => {
|
||||
const body = await request.json();
|
||||
if (/* !body.tarballFile && */ !body.captainDefinitionContent) {
|
||||
return HttpResponse.json({
|
||||
status: 1100,
|
||||
description:
|
||||
"Either tarballfile or captainDefinitionContent should be present.",
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
||||
const app = appsFixtures.find((app) => app.appName === params.name);
|
||||
if (!app) {
|
||||
return HttpResponse.json({
|
||||
status: 1000,
|
||||
description: `App (${params.name}) could not be found. Make sure that you have created the app.`,
|
||||
data: {},
|
||||
});
|
||||
}
|
||||
|
||||
const newVersion = JSON.parse(body.captainDefinitionContent).imageName;
|
||||
app.deployedVersion = app.versions.length;
|
||||
app.versions.push({
|
||||
version: app.versions.length,
|
||||
deployedImageName: newVersion,
|
||||
gitHash: body.gitHash ?? "",
|
||||
timeStamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return HttpResponse.json({
|
||||
status: 100,
|
||||
description: "Deploy is done",
|
||||
data: {},
|
||||
});
|
||||
}),
|
||||
];
|
||||
|
||||
const server = setupServer(...handlers);
|
||||
|
||||
export default server;
|
@ -0,0 +1,114 @@
|
||||
import { setupServer } from "msw/node";
|
||||
import { http, HttpResponse, HttpResponseResolver, PathParams } from "msw";
|
||||
|
||||
const BASE_URI = "https://hub.docker.com/v2";
|
||||
|
||||
const handlers = [
|
||||
http.get<{ namespace: string; repository: string }>(
|
||||
`${BASE_URI}/repositories/:namespace/:repository/tags`,
|
||||
({ params }) => {
|
||||
const { namespace, repository } = params;
|
||||
if (namespace !== "vaultwarden" || repository !== "server") {
|
||||
return HttpResponse.json(
|
||||
{
|
||||
message: "httperror 404: object not found",
|
||||
errinfo: {
|
||||
namespace,
|
||||
repository,
|
||||
},
|
||||
},
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
return HttpResponse.json({
|
||||
count: 66,
|
||||
next: "https://hub.docker.com/v2/repositories/vaultwarden/server/tags?ordering=last_updated&page=2&page_size=10",
|
||||
previous: null,
|
||||
results: [
|
||||
createImageDescription(
|
||||
"1.30.5-alpine",
|
||||
"sha256:6f6ec220ed300e1a11475a91d270985915083512f9fb60c1c25783faaa66eef5"
|
||||
),
|
||||
createImageDescription(
|
||||
"latest-alpine",
|
||||
"sha256:6f6ec220ed300e1a11475a91d270985915083512f9fb60c1c25783faaa66eef5"
|
||||
),
|
||||
createImageDescription(
|
||||
"alpine",
|
||||
"sha256:6f6ec220ed300e1a11475a91d270985915083512f9fb60c1c25783faaa66eef5"
|
||||
),
|
||||
createImageDescription(
|
||||
"1.30.5",
|
||||
"sha256:edb8e2bab9cbca22e555638294db9b3657ffbb6e5d149a29d7ccdb243e3c71e0"
|
||||
),
|
||||
createImageDescription(
|
||||
"latest",
|
||||
"sha256:edb8e2bab9cbca22e555638294db9b3657ffbb6e5d149a29d7ccdb243e3c71e0"
|
||||
),
|
||||
createImageDescription(
|
||||
"testing-alpine",
|
||||
"sha256:af5021d1a4e5debd1dc16a2bef15993c07f93a0e3c6c4acfd1ffcdaaab71bd0d"
|
||||
),
|
||||
createImageDescription(
|
||||
"testing",
|
||||
"sha256:293f0127bc2fe0c59b26fea0ec0b990049a65b4f6f0c9f961e345276aadca3fd"
|
||||
),
|
||||
createImageDescription(
|
||||
"1.30.4-alpine",
|
||||
"sha256:743209ed6169e595f9fff2412619d6791002057e211f8725b779777e05066df4"
|
||||
),
|
||||
createImageDescription(
|
||||
"1.30.4",
|
||||
"sha256:b906f840f02ea481861cd90a4eafb92752f45afa1927a29406a26256f56271ed"
|
||||
),
|
||||
createImageDescription(
|
||||
"1.30.3-alpine",
|
||||
"sha256:153defd78a3ede850445d64d6fca283701d0c25978e513c61688cf63bd47a14a"
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
function createImageDescription(name: string, digest: string) {
|
||||
return {
|
||||
name,
|
||||
digest,
|
||||
|
||||
// fake data irrelevant for our use case
|
||||
creator: 14287463,
|
||||
id: 613523756,
|
||||
images: [
|
||||
{
|
||||
architecture: "amd64",
|
||||
features: "",
|
||||
variant: null,
|
||||
digest:
|
||||
"sha256:9f4c1ea3601e398656992f738af9f84faf7a6d68299b2deaf049580e5da0d37f",
|
||||
os: "linux",
|
||||
os_features: "",
|
||||
os_version: null,
|
||||
size: 31945148,
|
||||
status: "active",
|
||||
last_pulled: "2024-03-10T08:07:35.84822Z",
|
||||
last_pushed: "2024-03-02T18:59:17.437196Z",
|
||||
},
|
||||
],
|
||||
last_updated: "2024-03-02T18:59:28.977584Z",
|
||||
last_updater: 14287463,
|
||||
last_updater_username: "vaultwardenbot",
|
||||
repository: 12325169,
|
||||
full_size: 31945148,
|
||||
v2: true,
|
||||
tag_status: "active",
|
||||
tag_last_pulled: "2024-03-10T08:07:35.84822Z",
|
||||
tag_last_pushed: "2024-03-02T18:59:28.977584Z",
|
||||
media_type: "application/vnd.oci.image.index.v1+json",
|
||||
content_type: "image",
|
||||
};
|
||||
}
|
||||
|
||||
const server = setupServer(...handlers);
|
||||
|
||||
export default server;
|
@ -1,9 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"types": ["bun-types"]
|
||||
"types": ["bun-types"],
|
||||
"moduleResolution": "NodeNext",
|
||||
"resolveJsonModule": true,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue