mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-10 21:20:24 +02:00
202 lines
6.5 KiB
TypeScript
202 lines
6.5 KiB
TypeScript
import { INestApplication } from "@nestjs/common";
|
|
import { ConfigModule } from "@nestjs/config";
|
|
import { Test, TestingModule } from "@nestjs/testing";
|
|
import request from "supertest";
|
|
import { App } from "supertest/types";
|
|
import { AppController } from "./../src/app.controller.js";
|
|
import { AppService } from "./../src/app.service.js";
|
|
import { SyncModule } from "./../src/sync/sync.module.js";
|
|
import {
|
|
configureTestEnv,
|
|
TEST_S3_ENDPOINT,
|
|
TEST_SYNC_TOKEN,
|
|
waitForTestS3,
|
|
} from "./test-env.js";
|
|
|
|
// Presigning is offline, so this host never has to accept a connection — the
|
|
// assertions are about which host ends up in the signed URL.
|
|
const PUBLIC_ENDPOINT = "https://storage.example.com";
|
|
|
|
// Only needs to be present for the server to consider itself cloud-mode; no
|
|
// token is verified against it in these assertions.
|
|
const CLOUD_PUBLIC_KEY =
|
|
"-----BEGIN PUBLIC KEY-----\nnot-a-real-key\n-----END PUBLIC KEY-----";
|
|
|
|
interface PresignResponse {
|
|
url: string;
|
|
}
|
|
|
|
interface PresignBatchResponse {
|
|
items: Array<{ key: string; url: string }>;
|
|
}
|
|
|
|
interface ReadyResponse {
|
|
status: string;
|
|
s3: boolean;
|
|
storageEndpoint: string;
|
|
}
|
|
|
|
async function bootstrap(publicEndpoint: string | undefined) {
|
|
configureTestEnv();
|
|
if (publicEndpoint) {
|
|
process.env.S3_PUBLIC_ENDPOINT = publicEndpoint;
|
|
} else {
|
|
delete process.env.S3_PUBLIC_ENDPOINT;
|
|
}
|
|
await waitForTestS3();
|
|
|
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
|
imports: [ConfigModule.forRoot({ isGlobal: true }), SyncModule],
|
|
controllers: [AppController],
|
|
providers: [AppService],
|
|
}).compile();
|
|
|
|
const app = moduleFixture.createNestApplication<INestApplication<App>>();
|
|
await app.listen(0);
|
|
return app;
|
|
}
|
|
|
|
// A self-hosted server usually reaches its storage over a private address the
|
|
// desktop client has no route to. Signing client URLs against that address
|
|
// handed every client a URL it could not open, so uploads failed at connect
|
|
// while /health and /readyz stayed green.
|
|
describe("presigned URL host", () => {
|
|
describe("with S3_PUBLIC_ENDPOINT set", () => {
|
|
let app: INestApplication<App>;
|
|
|
|
beforeAll(async () => {
|
|
app = await bootstrap(PUBLIC_ENDPOINT);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
delete process.env.S3_PUBLIC_ENDPOINT;
|
|
await app.close();
|
|
});
|
|
|
|
it("signs single upload URLs against the public endpoint", async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.post("/v1/objects/presign-upload")
|
|
.set("Authorization", `Bearer ${TEST_SYNC_TOKEN}`)
|
|
.send({ key: "endpoint/single.txt" })
|
|
.expect(200);
|
|
|
|
const { url } = response.body as PresignResponse;
|
|
expect(url.startsWith(PUBLIC_ENDPOINT)).toBe(true);
|
|
expect(url).not.toContain(TEST_S3_ENDPOINT);
|
|
});
|
|
|
|
it("signs batch upload URLs against the public endpoint", async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.post("/v1/objects/presign-upload-batch")
|
|
.set("Authorization", `Bearer ${TEST_SYNC_TOKEN}`)
|
|
.send({ items: [{ key: "endpoint/a.txt" }, { key: "endpoint/b.txt" }] })
|
|
.expect(200);
|
|
|
|
const { items } = response.body as PresignBatchResponse;
|
|
expect(items).toHaveLength(2);
|
|
for (const item of items) {
|
|
expect(item.url.startsWith(PUBLIC_ENDPOINT)).toBe(true);
|
|
}
|
|
});
|
|
|
|
it("signs download URLs against the public endpoint", async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.post("/v1/objects/presign-download")
|
|
.set("Authorization", `Bearer ${TEST_SYNC_TOKEN}`)
|
|
.send({ key: "endpoint/single.txt" })
|
|
.expect(200);
|
|
|
|
const { url } = response.body as PresignResponse;
|
|
expect(url.startsWith(PUBLIC_ENDPOINT)).toBe(true);
|
|
});
|
|
|
|
// The server's own S3 calls must keep using the private endpoint, or
|
|
// pointing clients at a public address would break the server itself.
|
|
it("still reaches storage over the private endpoint", async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.post("/v1/objects/stat")
|
|
.set("Authorization", `Bearer ${TEST_SYNC_TOKEN}`)
|
|
.send({ key: "endpoint/does-not-exist" })
|
|
.expect(200);
|
|
|
|
expect(response.body).toEqual({ exists: false });
|
|
});
|
|
|
|
it("reports the client-facing endpoint from /readyz", async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get("/readyz")
|
|
.expect(200);
|
|
|
|
const body = response.body as ReadyResponse;
|
|
expect(body.s3).toBe(true);
|
|
expect(body.storageEndpoint).toBe(PUBLIC_ENDPOINT);
|
|
});
|
|
});
|
|
|
|
// /readyz has no auth, so a managed deployment must not publish its storage
|
|
// host to anyone who can reach the probe.
|
|
describe("in cloud mode", () => {
|
|
let app: INestApplication<App>;
|
|
const previousKey = process.env.SYNC_JWT_PUBLIC_KEY;
|
|
|
|
beforeAll(async () => {
|
|
process.env.SYNC_JWT_PUBLIC_KEY = CLOUD_PUBLIC_KEY;
|
|
app = await bootstrap(PUBLIC_ENDPOINT);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (previousKey === undefined) {
|
|
delete process.env.SYNC_JWT_PUBLIC_KEY;
|
|
} else {
|
|
process.env.SYNC_JWT_PUBLIC_KEY = previousKey;
|
|
}
|
|
delete process.env.S3_PUBLIC_ENDPOINT;
|
|
await app.close();
|
|
});
|
|
|
|
it("withholds the storage endpoint from /readyz", async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get("/readyz")
|
|
.expect(200);
|
|
|
|
const body = response.body as ReadyResponse;
|
|
expect(body.s3).toBe(true);
|
|
expect(body.storageEndpoint).toBeUndefined();
|
|
expect(JSON.stringify(body)).not.toContain("storage.example.com");
|
|
});
|
|
});
|
|
|
|
describe("without S3_PUBLIC_ENDPOINT", () => {
|
|
let app: INestApplication<App>;
|
|
|
|
beforeAll(async () => {
|
|
app = await bootstrap(undefined);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
it("falls back to S3_ENDPOINT", async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.post("/v1/objects/presign-upload")
|
|
.set("Authorization", `Bearer ${TEST_SYNC_TOKEN}`)
|
|
.send({ key: "endpoint/fallback.txt" })
|
|
.expect(200);
|
|
|
|
const { url } = response.body as PresignResponse;
|
|
expect(url.startsWith(TEST_S3_ENDPOINT)).toBe(true);
|
|
});
|
|
|
|
it("reports the fallback endpoint from /readyz", async () => {
|
|
const response = await request(app.getHttpServer())
|
|
.get("/readyz")
|
|
.expect(200);
|
|
|
|
expect((response.body as ReadyResponse).storageEndpoint).toBe(
|
|
TEST_S3_ENDPOINT,
|
|
);
|
|
});
|
|
});
|
|
});
|