Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ bun install # installs once for all workspaces
bun dev # starts both client (:3000) and server (:8080)
```

Run the following command to run tests:
```sh
bun run test
```

| Directory | Purpose |
| ----------------- | -------------------------------------------------------------- |
| `apps/server` | Bun HTTP + WebSocket server |
Expand Down
10 changes: 10 additions & 0 deletions apps/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,14 @@ To run:
bun run dev
```

To test:
```sh
bun run test
```

To get test coverage:
```sh
bun run test:coverage
```

open http://localhost:3000
11 changes: 10 additions & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
"name": "server",
"scripts": {
"dev": "bun run --hot src/index.ts",
"start": "bun run src/index.ts"
"start": "bun run src/index.ts",
"test": "bun test",
"test:watch": "bun test --watch",
"test:coverage": "bun test --coverage"
},
"dependencies": {
"@beatsync/shared": "workspace:*",
Expand All @@ -13,5 +16,11 @@
},
"devDependencies": {
"@types/bun": "latest"
},
"module": "src/index.ts",
"type": "module",
"private": true,
"peerDependencies": {
"typescript": "^5"
}
}
10 changes: 10 additions & 0 deletions apps/server/src/roomManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
epochNow,
MoveClientType,
WSBroadcastType,
ClientActionEnum,
} from "@beatsync/shared";
import { GRID, PositionType } from "@beatsync/shared/types/basic";
import { Server, ServerWebSocket } from "bun";
Expand Down Expand Up @@ -324,6 +325,15 @@ class RoomManager {
client.position = position;
room.clients.set(clientId, client);

const roomUpdateMessage: WSBroadcastType = {
type: "ROOM_EVENT",
event: {
type: ClientActionEnum.Enum.CLIENT_CHANGE,
clients: this.getClients(roomId),
},
};
sendBroadcast({ server, roomId, message: roomUpdateMessage });

// Update spatial audio config
this._calculateGainsAndBroadcast({ room, server });
}
Expand Down
92 changes: 92 additions & 0 deletions apps/server/src/test/routes/audioRoutes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
import { handleGetAudio } from "../../routes/audio";
import { setupTestAudioDir, cleanupTestAudioDir, createTestAudioFile, createTestServer } from "../utils/testHelpers";

describe("Audio Routes", () => {
const server = createTestServer();

beforeAll(() => {
setupTestAudioDir();
});

afterAll(() => {
cleanupTestAudioDir();
server.stop();
});

test("GET request should return 405 Method Not Allowed", async () => {
const request = new Request("http://localhost/audio", {
method: "GET",
});

const response = await handleGetAudio(request, server);
expect(response.status).toBe(405);
const text = await response.text();
expect(text).toBe("Method not allowed");
});

test("POST request without content-type should return 400", async () => {
const request = new Request("http://localhost/audio", {
method: "POST",
body: JSON.stringify({ id: "test.mp3" }),
});

const response = await handleGetAudio(request, server);
expect(response.status).toBe(400);
const text = await response.text();
expect(text).toBe("Content-Type must be application/json");
});

test("POST request with invalid body should return 400", async () => {
const request = new Request("http://localhost/audio", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ invalid: "data" }),
});

const response = await handleGetAudio(request, server);
expect(response.status).toBe(400);
const text = await response.text();
expect(text).toContain("Invalid request data");
});

test("POST request for non-existent file should return 404", async () => {
const request = new Request("http://localhost/audio", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id: "nonexistent.mp3" }),
});

const response = await handleGetAudio(request, server);
expect(response.status).toBe(404);
const text = await response.text();
expect(text).toBe("Audio file not found");
});

test("POST request for existing file should return audio file", async () => {
const testFileName = "test-audio.mp3";
const testContent = "test audio content";
createTestAudioFile(testFileName, testContent);

const request = new Request("http://localhost/audio", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ id: testFileName }),
});

const response = await handleGetAudio(request, server);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("audio/mpeg");
expect(response.headers.get("Content-Length")).toBe(testContent.length.toString());
expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");

const responseContent = await response.text();
expect(responseContent).toBe(testContent);
});
});
146 changes: 146 additions & 0 deletions apps/server/src/test/routes/websocket.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { describe, expect, test, beforeAll, afterAll } from "bun:test";
import { Server } from "bun";
import { ClientActionEnum } from "@beatsync/shared";
import { createTestWSServer, createTestWSClient, TestWebSocketClient } from "../utils/wsTestHelpers";
import { roomManager } from "../../roomManager";

describe("WebSocket Routes", () => {
let server: Server;

beforeAll(() => {
server = createTestWSServer();
});

afterAll(() => {
server.stop();
});

test("should reject connection without required parameters", async () => {
// Try to connect without parameters
const response = await fetch(`http://localhost:${server.port}`);
expect(response.status).toBe(400);
const text = await response.text();
expect(text).toBe("roomId and userId are required");

// Try to connect with only roomId
const response2 = await fetch(`http://localhost:${server.port}?roomId=test`);
expect(response2.status).toBe(400);
const text2 = await response2.text();
expect(text2).toBe("roomId and userId are required");

// Try to connect with only username
const response3 = await fetch(`http://localhost:${server.port}?username=test`);
expect(response3.status).toBe(400);
const text3 = await response3.text();
expect(text3).toBe("roomId and userId are required");
});

test("should establish connection with valid parameters", async () => {
const client = await createTestWSClient(server, {
roomId: "test-room",
username: "test-user",
clientId: "test-client",
});

try {
const clientId = client.getClientId();
expect(clientId).toBeDefined();
} finally {
client.close();
}
});

test("should handle multiple clients in a room", async () => {
const client1 = await createTestWSClient(server, {
roomId: "multi-room",
username: "user1",
clientId: "client1",
});

const client2 = await createTestWSClient(server, {
roomId: "multi-room",
username: "user2",
clientId: "client2",
});

try {
// Both clients should have valid client IDs
const client1Id = client1.getClientId();
const client2Id = client2.getClientId();
expect(client1Id).toBeDefined();
expect(client2Id).toBeDefined();

// Both clients should receive room update with 2 clients
const roomEvent = await client1.waitForRoomEvent();
expect(roomEvent.event.clients.length).toBe(2);
expect(roomEvent.event.clients.map((c: any) => c.username).sort()).toEqual(["user1", "user2"]);
} finally {
client1.close();
client2.close();
}
});

test("should handle client disconnection", async () => {
const client1 = await createTestWSClient(server, {
roomId: "disconnect-room",
username: "user1",
clientId: "client1",
});

const client2 = await createTestWSClient(server, {
roomId: "disconnect-room",
username: "user2",
clientId: "client2",
});

try {
// Close client1
client1.close();

// Client2 should receive update about client1's disconnection
const disconnectUpdate = await client2.waitForMessage(msg =>
msg.type === "ROOM_EVENT" &&
msg.event?.type === ClientActionEnum.Enum.CLIENT_CHANGE &&
msg.event?.clients?.length === 1 &&
msg.event?.clients[0].username === "user2"
);

expect(disconnectUpdate.type).toBe("ROOM_EVENT");
expect(disconnectUpdate.event.type).toBe(ClientActionEnum.Enum.CLIENT_CHANGE);
expect(disconnectUpdate.event.clients.length).toBe(1);
expect(disconnectUpdate.event.clients[0].username).toBe("user2");
} finally {
client2.close();
}
});

test("should handle NTP request/response", async () => {
const client = await createTestWSClient(server, {
roomId: "ntp-room",
username: "ntp-user",
clientId: "ntp-client",
});

try {
// Send NTP request
const t0 = Date.now();
client.send({
type: ClientActionEnum.enum.NTP_REQUEST,
t0,
});

// Wait for NTP response
const ntpResponse = await client.waitForMessage(msg =>
msg.type === "NTP_RESPONSE" && msg.t0 === t0
);

expect(ntpResponse.type).toBe("NTP_RESPONSE");
expect(ntpResponse.t0).toBe(t0);
expect(ntpResponse.t1).toBeNumber();
expect(ntpResponse.t2).toBeNumber();
expect(ntpResponse.t1).toBeLessThanOrEqual(ntpResponse.t2);
} finally {
client.close();
}
});
});
Loading