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
8 changes: 6 additions & 2 deletions src/__tests__/integration/api/admin/pr-stats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ describe("GET /api/admin/workspaces/[id]/pr-stats (integration)", () => {
repositoryUrl: "https://github.com/testorg/testrepo",
});

// Use a counter for deterministic, unique PR numbers (avoids random collision/deduplication)
let prCounter = 1;

// Helper: create a task → message → PR artifact
async function seedPRArtifact(status: string, ageHours: number) {
const task = await createTestTask({
Expand All @@ -104,7 +107,7 @@ describe("GET /api/admin/workspaces/[id]/pr-stats (integration)", () => {
messageId: message.id,
type: "PULL_REQUEST",
content: {
url: `https://github.com/testorg/testrepo/pull/${Math.floor(Math.random() * 9999)}`,
url: `https://github.com/testorg/testrepo/pull/${prCounter++}`,
repo: "testorg/testrepo",
status,
title: `Test PR (${status})`,
Expand Down Expand Up @@ -144,6 +147,7 @@ describe("GET /api/admin/workspaces/[id]/pr-stats (integration)", () => {
repositoryUrl: "https://github.com/testorg/bucketrepo",
});

let bucketPrCounter = 1;
async function seedDoneArtifact(ageHours: number) {
const task = await createTestTask({ workspaceId: workspace.id, createdById: regularUser.id });
const message = await createTestChatMessage({ taskId: task.id, message: "test" });
Expand All @@ -153,7 +157,7 @@ describe("GET /api/admin/workspaces/[id]/pr-stats (integration)", () => {
messageId: message.id,
type: "PULL_REQUEST",
content: {
url: `https://github.com/testorg/bucketrepo/pull/${Math.floor(Math.random() * 9999)}`,
url: `https://github.com/testorg/bucketrepo/pull/${bucketPrCounter++}`,
repo: "testorg/bucketrepo",
status: "DONE",
title: "Test PR",
Expand Down
109 changes: 109 additions & 0 deletions src/__tests__/unit/lib/mock/swarm-state-vanity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { describe, test, expect, beforeEach } from "vitest";
import { mockSwarmState } from "@/lib/mock/swarm-state";

describe("MockSwarmStateManager.updateVanityAddress", () => {
beforeEach(() => {
mockSwarmState.reset();
});

test("returns success: false when swarm not found by address", () => {
const result = mockSwarmState.updateVanityAddress(
"nonexistent.sphinx.chat",
"newname.sphinx.chat"
);

expect(result).toEqual({ success: false, message: "Swarm not found" });
});

test("updates swarm address on success", () => {
// Create a swarm so we have something to look up
const created = mockSwarmState.createSwarm({ instance_type: "t3.small" });
const swarms = mockSwarmState.getAllSwarms();
const swarm = swarms.find((s) => s.swarm_id === created.swarm_id)!;
const originalAddress = swarm.address;

const result = mockSwarmState.updateVanityAddress(
originalAddress,
"machinelearning.sphinx.chat"
);

expect(result).toEqual({ success: true, message: "Vanity address updated" });

const updatedSwarms = mockSwarmState.getAllSwarms();
const updatedSwarm = updatedSwarms.find((s) => s.swarm_id === created.swarm_id)!;
expect(updatedSwarm.address).toBe("machinelearning.sphinx.chat");
});

test("removes old subdomain from domain registry and adds new one", () => {
const created = mockSwarmState.createSwarm({ instance_type: "t3.small" });
const swarms = mockSwarmState.getAllSwarms();
const swarm = swarms.find((s) => s.swarm_id === created.swarm_id)!;
const originalAddress = swarm.address;

// Old subdomain should be registered (check domain exists)
const oldSubdomain = originalAddress.replace(/\.sphinx\.chat$/, "");
const beforeCheck = mockSwarmState.checkDomain(oldSubdomain);
// The mock creates addresses like "mock-swarm-000001.test.local" without .sphinx.chat
// so let's use the actual subdomain stripping logic
// The domain registry uses the subdomain directly from createSwarm which adds swarmId
// Let's verify via updateVanityAddress result and subsequent checkDomain calls

mockSwarmState.updateVanityAddress(originalAddress, "machinelearning.sphinx.chat");

// New subdomain should now exist in domain registry
const newCheck = mockSwarmState.checkDomain("machinelearning");
expect(newCheck.domain_exists).toBe(true);
expect(newCheck.swarm_name_exist).toBe(true);
});

test("removing old subdomain makes it available in domain registry", () => {
const created = mockSwarmState.createSwarm({ instance_type: "t3.small" });
const swarms = mockSwarmState.getAllSwarms();
const swarm = swarms.find((s) => s.swarm_id === created.swarm_id)!;

// Manually set a sphinx.chat style address so we can verify removal
// We'll call updateVanityAddress to set it first
const firstAddress = `${created.swarm_id}.sphinx.chat`;
// Simulate the swarm having a sphinx.chat address by updating from current
mockSwarmState.updateVanityAddress(swarm.address, firstAddress);

// Now update to new address
const result = mockSwarmState.updateVanityAddress(firstAddress, "newvanity.sphinx.chat");
expect(result.success).toBe(true);

// Old subdomain should no longer exist
const oldCheck = mockSwarmState.checkDomain(created.swarm_id);
expect(oldCheck.domain_exists).toBe(false);

// New subdomain should exist
const newCheck = mockSwarmState.checkDomain("newvanity");
expect(newCheck.domain_exists).toBe(true);
});

test("exact address match required (not partial)", () => {
mockSwarmState.createSwarm({ instance_type: "t3.small" });

// Using a partial match that is not exact should fail
const result = mockSwarmState.updateVanityAddress(
"sphinx.chat",
"newname.sphinx.chat"
);

expect(result).toEqual({ success: false, message: "Swarm not found" });
});

test("updatedAt is refreshed after update", () => {
const created = mockSwarmState.createSwarm({ instance_type: "t3.small" });
const swarms = mockSwarmState.getAllSwarms();
const swarm = swarms.find((s) => s.swarm_id === created.swarm_id)!;
const originalUpdatedAt = swarm.updatedAt;

// Small delay to ensure timestamp differs
const before = Date.now();
mockSwarmState.updateVanityAddress(swarm.address, "updated.sphinx.chat");

const updatedSwarms = mockSwarmState.getAllSwarms();
const updatedSwarm = updatedSwarms.find((s) => s.swarm_id === created.swarm_id)!;
expect(updatedSwarm.updatedAt.getTime()).toBeGreaterThanOrEqual(before);
});
});
143 changes: 143 additions & 0 deletions src/__tests__/unit/services/swarm/api/updateVanityAddressApi.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { describe, test, expect, beforeEach, vi } from "vitest";

// Mock config and env before importing the module under test
vi.mock("@/config/env", () => ({
env: {
SWARM_SUPERADMIN_API_KEY: "test-super-token",
},
config: {
SWARM_SUPER_ADMIN_URL: "https://swarm-admin.example.com",
},
}));

vi.mock("@/lib/encryption", () => ({
EncryptionService: {
getInstance: vi.fn(() => ({
decryptField: vi.fn((_, v) => v),
})),
},
}));

const mockFetch = vi.fn();
global.fetch = mockFetch;

const { updateVanityAddressApi } = await import("@/services/swarm/api/swarm");

describe("updateVanityAddressApi", () => {
beforeEach(() => {
vi.clearAllMocks();
});

test("POSTs to the correct URL", async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ success: true, message: "Vanity address updated" }),
});

await updateVanityAddressApi("myswarm.sphinx.chat", "newname.sphinx.chat");

const [url] = mockFetch.mock.calls[0];
expect(url).toBe(
"https://swarm-admin.example.com/api/super/update_swarm_vanity_address"
);
});

test("uses POST method", async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ success: true, message: "Vanity address updated" }),
});

await updateVanityAddressApi("myswarm.sphinx.chat", "newname.sphinx.chat");

const [, init] = mockFetch.mock.calls[0];
expect(init.method).toBe("POST");
});

test("sends x-super-token header with the API key", async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ success: true, message: "Vanity address updated" }),
});

await updateVanityAddressApi("myswarm.sphinx.chat", "newname.sphinx.chat");

const [, init] = mockFetch.mock.calls[0];
expect(init.headers["x-super-token"]).toBe("test-super-token");
});

test("sends correct body with host and vanity_address", async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ success: true, message: "Vanity address updated" }),
});

await updateVanityAddressApi("myswarm.sphinx.chat", "newname.sphinx.chat");

const [, init] = mockFetch.mock.calls[0];
const body = JSON.parse(init.body);
expect(body.host).toBe("myswarm.sphinx.chat");
expect(body.vanity_address).toBe("newname.sphinx.chat");
});

test("sends Content-Type application/json header", async () => {
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({ success: true, message: "Vanity address updated" }),
});

await updateVanityAddressApi("myswarm.sphinx.chat", "newname.sphinx.chat");

const [, init] = mockFetch.mock.calls[0];
expect(init.headers["Content-Type"]).toBe("application/json");
});

test("returns parsed JSON response on success", async () => {
const expected = { success: true, message: "Vanity address updated" };
mockFetch.mockResolvedValue({
ok: true,
status: 200,
json: async () => expected,
});

const result = await updateVanityAddressApi("myswarm.sphinx.chat", "newname.sphinx.chat");
expect(result).toEqual(expected);
});

test("throws when response is not ok", async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 502,
json: async () => ({ success: false, message: "Bad gateway" }),
});

await expect(
updateVanityAddressApi("myswarm.sphinx.chat", "newname.sphinx.chat")
).rejects.toThrow("Bad gateway");
});

test("throws with fallback message when error response has no message", async () => {
mockFetch.mockResolvedValue({
ok: false,
status: 500,
json: async () => ({}),
});

await expect(
updateVanityAddressApi("myswarm.sphinx.chat", "newname.sphinx.chat")
).rejects.toThrow("Request failed with status 500");
});

test("throws when fetch throws a network error", async () => {
mockFetch.mockRejectedValue(new Error("Network error"));

await expect(
updateVanityAddressApi("myswarm.sphinx.chat", "newname.sphinx.chat")
).rejects.toThrow("Network error");
});
});
Loading
Loading