Skip to content

Commit 5e36845

Browse files
committed
fix: secure media server and invite decline
1 parent 99f6b3d commit 5e36845

15 files changed

Lines changed: 551 additions & 251 deletions

File tree

apps/media-server/src/__tests__/routes/audio-memory.test.ts

Lines changed: 29 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ import { getActiveProcessCount } from "../../lib/ffmpeg";
44

55
const FIXTURES_DIR = join(import.meta.dir, "..", "fixtures");
66
const TEST_VIDEO_WITH_AUDIO = `file://${join(FIXTURES_DIR, "test-with-audio.mp4")}`;
7+
const MEDIA_SERVER_SECRET = "test-secret";
8+
const AUTH_HEADERS = {
9+
"Content-Type": "application/json",
10+
"x-media-server-secret": MEDIA_SERVER_SECRET,
11+
};
12+
13+
process.env.MEDIA_SERVER_WEBHOOK_SECRET = MEDIA_SERVER_SECRET;
14+
15+
function audioPostRequest(path: string, body: unknown): Request {
16+
return new Request(`http://localhost${path}`, {
17+
method: "POST",
18+
headers: AUTH_HEADERS,
19+
body: JSON.stringify(body),
20+
});
21+
}
722

823
async function waitForProcessCleanup(
924
expectedCount: number,
@@ -41,11 +56,7 @@ describe("audio routes memory management", () => {
4156
test("cleans up after successful check", async () => {
4257
const app = await getFreshApp();
4358
const response = await app.fetch(
44-
new Request("http://localhost/audio/check", {
45-
method: "POST",
46-
headers: { "Content-Type": "application/json" },
47-
body: JSON.stringify({ videoUrl: TEST_VIDEO_WITH_AUDIO }),
48-
}),
59+
audioPostRequest("/audio/check", { videoUrl: TEST_VIDEO_WITH_AUDIO }),
4960
);
5061

5162
expect(response.status).toBe(200);
@@ -59,11 +70,7 @@ describe("audio routes memory management", () => {
5970
const app = await getFreshApp();
6071
const requests = Array.from({ length: 5 }, () =>
6172
app.fetch(
62-
new Request("http://localhost/audio/check", {
63-
method: "POST",
64-
headers: { "Content-Type": "application/json" },
65-
body: JSON.stringify({ videoUrl: TEST_VIDEO_WITH_AUDIO }),
66-
}),
73+
audioPostRequest("/audio/check", { videoUrl: TEST_VIDEO_WITH_AUDIO }),
6774
),
6875
);
6976

@@ -82,13 +89,9 @@ describe("audio routes memory management", () => {
8289
test("cleans up after stream is fully consumed", async () => {
8390
const app = await getFreshApp();
8491
const response = await app.fetch(
85-
new Request("http://localhost/audio/extract", {
86-
method: "POST",
87-
headers: { "Content-Type": "application/json" },
88-
body: JSON.stringify({
89-
videoUrl: TEST_VIDEO_WITH_AUDIO,
90-
stream: true,
91-
}),
92+
audioPostRequest("/audio/extract", {
93+
videoUrl: TEST_VIDEO_WITH_AUDIO,
94+
stream: true,
9295
}),
9396
);
9497

@@ -107,13 +110,9 @@ describe("audio routes memory management", () => {
107110
test("cleans up when stream is cancelled early", async () => {
108111
const app = await getFreshApp();
109112
const response = await app.fetch(
110-
new Request("http://localhost/audio/extract", {
111-
method: "POST",
112-
headers: { "Content-Type": "application/json" },
113-
body: JSON.stringify({
114-
videoUrl: TEST_VIDEO_WITH_AUDIO,
115-
stream: true,
116-
}),
113+
audioPostRequest("/audio/extract", {
114+
videoUrl: TEST_VIDEO_WITH_AUDIO,
115+
stream: true,
117116
}),
118117
);
119118

@@ -129,13 +128,9 @@ describe("audio routes memory management", () => {
129128
test("cleans up when response body is not read at all", async () => {
130129
const app = await getFreshApp();
131130
const response = await app.fetch(
132-
new Request("http://localhost/audio/extract", {
133-
method: "POST",
134-
headers: { "Content-Type": "application/json" },
135-
body: JSON.stringify({
136-
videoUrl: TEST_VIDEO_WITH_AUDIO,
137-
stream: true,
138-
}),
131+
audioPostRequest("/audio/extract", {
132+
videoUrl: TEST_VIDEO_WITH_AUDIO,
133+
stream: true,
139134
}),
140135
);
141136

@@ -150,13 +145,9 @@ describe("audio routes memory management", () => {
150145
const app = await getFreshApp();
151146
const requests = Array.from({ length: 3 }, () =>
152147
app.fetch(
153-
new Request("http://localhost/audio/extract", {
154-
method: "POST",
155-
headers: { "Content-Type": "application/json" },
156-
body: JSON.stringify({
157-
videoUrl: TEST_VIDEO_WITH_AUDIO,
158-
stream: true,
159-
}),
148+
audioPostRequest("/audio/extract", {
149+
videoUrl: TEST_VIDEO_WITH_AUDIO,
150+
stream: true,
160151
}),
161152
),
162153
);

apps/media-server/src/__tests__/routes/audio.test.ts

Lines changed: 114 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,56 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
22
import app from "../../app";
33
import * as ffmpeg from "../../lib/ffmpeg";
44

5+
const MEDIA_SERVER_SECRET = "test-secret";
6+
const AUTH_HEADERS = {
7+
"Content-Type": "application/json",
8+
"x-media-server-secret": MEDIA_SERVER_SECRET,
9+
};
10+
11+
process.env.MEDIA_SERVER_WEBHOOK_SECRET = MEDIA_SERVER_SECRET;
12+
13+
function audioPostRequest(path: string, body: unknown): Request {
14+
return new Request(`http://localhost${path}`, {
15+
method: "POST",
16+
headers: AUTH_HEADERS,
17+
body: JSON.stringify(body),
18+
});
19+
}
20+
21+
function unauthenticatedAudioPostRequest(path: string, body: unknown): Request {
22+
return new Request(`http://localhost${path}`, {
23+
method: "POST",
24+
headers: { "Content-Type": "application/json" },
25+
body: JSON.stringify(body),
26+
});
27+
}
28+
529
describe("POST /audio/check", () => {
630
beforeEach(() => {
731
mock.restore();
832
});
933

10-
test("returns 400 for missing videoUrl", async () => {
34+
test("returns 401 without media server secret", async () => {
1135
const response = await app.fetch(
12-
new Request("http://localhost/audio/check", {
13-
method: "POST",
14-
headers: { "Content-Type": "application/json" },
15-
body: JSON.stringify({}),
36+
unauthenticatedAudioPostRequest("/audio/check", {
37+
videoUrl: "https://example.com/video.mp4",
1638
}),
1739
);
1840

41+
expect(response.status).toBe(401);
42+
});
43+
44+
test("returns 400 for missing videoUrl", async () => {
45+
const response = await app.fetch(audioPostRequest("/audio/check", {}));
46+
1947
expect(response.status).toBe(400);
2048
const data = await response.json();
2149
expect(data.code).toBe("INVALID_REQUEST");
2250
});
2351

2452
test("returns 400 for invalid URL format", async () => {
2553
const response = await app.fetch(
26-
new Request("http://localhost/audio/check", {
27-
method: "POST",
28-
headers: { "Content-Type": "application/json" },
29-
body: JSON.stringify({ videoUrl: "not-a-valid-url" }),
30-
}),
54+
audioPostRequest("/audio/check", { videoUrl: "not-a-valid-url" }),
3155
);
3256

3357
expect(response.status).toBe(400);
@@ -44,10 +68,8 @@ describe("POST /audio/check", () => {
4468
const { default: appWithMock } = await import("../../app");
4569

4670
const response = await appWithMock.fetch(
47-
new Request("http://localhost/audio/check", {
48-
method: "POST",
49-
headers: { "Content-Type": "application/json" },
50-
body: JSON.stringify({ videoUrl: "https://example.com/video.mp4" }),
71+
audioPostRequest("/audio/check", {
72+
videoUrl: "https://example.com/video.mp4",
5173
}),
5274
);
5375

@@ -65,10 +87,8 @@ describe("POST /audio/check", () => {
6587
const { default: appWithMock } = await import("../../app");
6688

6789
const response = await appWithMock.fetch(
68-
new Request("http://localhost/audio/check", {
69-
method: "POST",
70-
headers: { "Content-Type": "application/json" },
71-
body: JSON.stringify({ videoUrl: "https://example.com/video.mp4" }),
90+
audioPostRequest("/audio/check", {
91+
videoUrl: "https://example.com/video.mp4",
7292
}),
7393
);
7494

@@ -83,27 +103,27 @@ describe("POST /audio/extract", () => {
83103
mock.restore();
84104
});
85105

86-
test("returns 400 for missing videoUrl", async () => {
106+
test("returns 401 without media server secret", async () => {
87107
const response = await app.fetch(
88-
new Request("http://localhost/audio/extract", {
89-
method: "POST",
90-
headers: { "Content-Type": "application/json" },
91-
body: JSON.stringify({}),
108+
unauthenticatedAudioPostRequest("/audio/extract", {
109+
videoUrl: "https://example.com/video.mp4",
92110
}),
93111
);
94112

113+
expect(response.status).toBe(401);
114+
});
115+
116+
test("returns 400 for missing videoUrl", async () => {
117+
const response = await app.fetch(audioPostRequest("/audio/extract", {}));
118+
95119
expect(response.status).toBe(400);
96120
const data = await response.json();
97121
expect(data.code).toBe("INVALID_REQUEST");
98122
});
99123

100124
test("returns 400 for invalid URL format", async () => {
101125
const response = await app.fetch(
102-
new Request("http://localhost/audio/extract", {
103-
method: "POST",
104-
headers: { "Content-Type": "application/json" },
105-
body: JSON.stringify({ videoUrl: "invalid-url" }),
106-
}),
126+
audioPostRequest("/audio/extract", { videoUrl: "invalid-url" }),
107127
);
108128

109129
expect(response.status).toBe(400);
@@ -120,10 +140,8 @@ describe("POST /audio/extract", () => {
120140
const { default: appWithMock } = await import("../../app");
121141

122142
const response = await appWithMock.fetch(
123-
new Request("http://localhost/audio/extract", {
124-
method: "POST",
125-
headers: { "Content-Type": "application/json" },
126-
body: JSON.stringify({ videoUrl: "https://example.com/video.mp4" }),
143+
audioPostRequest("/audio/extract", {
144+
videoUrl: "https://example.com/video.mp4",
127145
}),
128146
);
129147

@@ -143,13 +161,9 @@ describe("POST /audio/extract", () => {
143161
const { default: appWithMock } = await import("../../app");
144162

145163
const response = await appWithMock.fetch(
146-
new Request("http://localhost/audio/extract", {
147-
method: "POST",
148-
headers: { "Content-Type": "application/json" },
149-
body: JSON.stringify({
150-
videoUrl: "https://example.com/video.mp4",
151-
stream: false,
152-
}),
164+
audioPostRequest("/audio/extract", {
165+
videoUrl: "https://example.com/video.mp4",
166+
stream: false,
153167
}),
154168
);
155169

@@ -174,13 +188,9 @@ describe("POST /audio/extract", () => {
174188
const { default: appWithMock } = await import("../../app");
175189

176190
const response = await appWithMock.fetch(
177-
new Request("http://localhost/audio/extract", {
178-
method: "POST",
179-
headers: { "Content-Type": "application/json" },
180-
body: JSON.stringify({
181-
videoUrl: "https://example.com/video.mp4",
182-
stream: false,
183-
}),
191+
audioPostRequest("/audio/extract", {
192+
videoUrl: "https://example.com/video.mp4",
193+
stream: false,
184194
}),
185195
);
186196

@@ -190,3 +200,61 @@ describe("POST /audio/extract", () => {
190200
expect(data.details).toContain("FFmpeg failed");
191201
});
192202
});
203+
204+
describe("POST /audio/convert", () => {
205+
beforeEach(() => {
206+
mock.restore();
207+
});
208+
209+
test("returns 401 without media server secret", async () => {
210+
const response = await app.fetch(
211+
unauthenticatedAudioPostRequest("/audio/convert", {
212+
audioUrl: "https://example.com/audio.wav",
213+
}),
214+
);
215+
216+
expect(response.status).toBe(401);
217+
});
218+
219+
test("returns 400 for missing audioUrl", async () => {
220+
const response = await app.fetch(audioPostRequest("/audio/convert", {}));
221+
222+
expect(response.status).toBe(400);
223+
const data = await response.json();
224+
expect(data.code).toBe("INVALID_REQUEST");
225+
});
226+
227+
test("returns audio stream when conversion succeeds", async () => {
228+
const mockAudioData = new Uint8Array([0x49, 0x44, 0x33]);
229+
230+
mock.module("../../lib/ffmpeg", () => ({
231+
canAcceptNewProcess: ffmpeg.canAcceptNewProcess,
232+
checkHasAudioTrack: ffmpeg.checkHasAudioTrack,
233+
extractAudio: ffmpeg.extractAudio,
234+
extractAudioStream: () => ({
235+
stream: new ReadableStream<Uint8Array>({
236+
start(controller) {
237+
controller.enqueue(mockAudioData);
238+
controller.close();
239+
},
240+
}),
241+
cleanup: () => {},
242+
}),
243+
getActiveProcessCount: ffmpeg.getActiveProcessCount,
244+
}));
245+
246+
const { default: appWithMock } = await import("../../app");
247+
248+
const response = await appWithMock.fetch(
249+
audioPostRequest("/audio/convert", {
250+
audioUrl: "https://example.com/audio.wav",
251+
}),
252+
);
253+
254+
expect(response.status).toBe(200);
255+
expect(response.headers.get("Content-Type")).toBe("audio/mpeg");
256+
257+
const buffer = await response.arrayBuffer();
258+
expect(new Uint8Array(buffer)).toEqual(mockAudioData);
259+
});
260+
});

0 commit comments

Comments
 (0)