Skip to content
Merged
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
9 changes: 7 additions & 2 deletions src/app/api/agent-runs/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,17 @@ describe("GET /api/agent-runs", () => {
mocks.agentRunFindMany.mockResolvedValue([]);
});

it("returns agent runs without authentication", async () => {
it("401s an unauthenticated request", async () => {
const res = await GET(getRequest("http://localhost/api/agent-runs", false));
expect(res.status).toBe(401);
});

it("returns agent runs to an authenticated caller", async () => {
mocks.agentRunFindMany.mockResolvedValue([
{ id: "run-1", agentName: "saffron", status: "completed" },
]);

const res = await GET(getRequest("http://localhost/api/agent-runs", false));
const res = await GET(getRequest("http://localhost/api/agent-runs"));

expect(res.status).toBe(200);
const body = await res.json();
Expand Down
3 changes: 3 additions & 0 deletions src/app/api/agent-runs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { isValidEscalatedOutcome, VALID_ESCALATED_OUTCOMES } from "@/types";
import { authorizeRequest } from "@/lib/auth";

export async function GET(request: Request) {
if (!(await authorizeRequest(request)).authorized) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const limit = parseInt(searchParams.get("limit") || "50");

Expand Down
16 changes: 10 additions & 6 deletions src/app/api/audit/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,24 @@ vi.mock("@/lib/prisma", () => ({

import { GET } from "./route";

function request(urlString: string) {
return new Request(urlString, { headers: {} });
function request(urlString: string, includeAuth = true) {
const headers: Record<string, string> = {};
if (includeAuth) headers.Authorization = `Bearer ${mockToken}`;
return new Request(urlString, { headers });
}

describe("GET /api/audit", () => {
// NOTE: This route is intentionally unauthenticated. It returns all AuditLog
// rows to any caller. In production deployments behind a firewall or auth
// gateway this is acceptable; in open deployments consider adding auth.
beforeEach(() => {
vi.clearAllMocks();
mocks.auditLogFindMany.mockResolvedValue([]);
});

it("returns audit logs without authentication", async () => {
it("401s an unauthenticated request", async () => {
const res = await GET(request("http://localhost/api/audit", false));
expect(res.status).toBe(401);
});

it("returns audit logs to an authenticated caller", async () => {
mocks.auditLogFindMany.mockResolvedValue([
{ id: "log-1", actor: "agent", action: "move_issue", createdAt: new Date() },
]);
Expand Down
4 changes: 4 additions & 0 deletions src/app/api/audit/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { authorizeRequest } from "@/lib/auth";

export async function GET(request: Request) {
if (!(await authorizeRequest(request)).authorized) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { searchParams } = new URL(request.url);
const limit = parseInt(searchParams.get("limit") || "50");
const repo = searchParams.get("repo");
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/automation/repos/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { isValidRepoName } from "@/lib/config";
import { auditTrackedRepoCreateFailure, createTrackedRepo } from "@/lib/tracked-repos";
import { authorizeRequest } from "@/lib/auth";

export async function GET() {
export async function GET(request: Request) {
if (!(await authorizeRequest(request)).authorized) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const repos = await prisma.automationRepo.findMany({
orderBy: { fullName: "asc" },
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/automation/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,10 @@ async function syncRepo(repoFullName: string) {
}
}

export async function GET() {
export async function GET(request: Request) {
if (!(await authorizeRequest(request)).authorized) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const repos = await prisma.automationRepo.findMany({
orderBy: { fullName: "asc" },
include: {
Expand Down
21 changes: 13 additions & 8 deletions src/app/api/repos/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,26 @@ vi.mock("@/lib/prisma", () => ({
import { GET } from "./route";

describe("GET /api/repos", () => {
// NOTE: This route is intentionally unauthenticated. It returns all tracked
// repositories to any caller. In production deployments behind a firewall or
// auth gateway this is acceptable; in open deployments consider adding auth.
const authed = () =>
new Request("http://localhost/api/repos", { headers: { Authorization: `Bearer ${mockToken}` } });

beforeEach(() => {
vi.clearAllMocks();
mocks.repositoryFindMany.mockResolvedValue([]);
});

it("returns repos without authentication", async () => {
it("401s an unauthenticated request", async () => {
const res = await GET(new Request("http://localhost/api/repos"));
expect(res.status).toBe(401);
});

it("returns repos to an authenticated caller", async () => {
mocks.repositoryFindMany.mockResolvedValue([
{ id: "r1", fullName: "org/repo1", enabled: true },
{ id: "r2", fullName: "org/repo2", enabled: true },
]);

const res = await GET();
const res = await GET(authed());

expect(res.status).toBe(200);
const body = await res.json();
Expand All @@ -51,15 +56,15 @@ describe("GET /api/repos", () => {
it("returns empty array when no repos exist", async () => {
mocks.repositoryFindMany.mockResolvedValue([]);

const res = await GET();
const res = await GET(authed());

expect(res.status).toBe(200);
const body = await res.json();
expect(body).toEqual([]);
});

it("orders by fullName ascending", async () => {
await GET();
await GET(authed());

const call = mocks.repositoryFindMany.mock.calls[0][0];
expect(call.orderBy).toEqual({ fullName: "asc" });
Expand All @@ -68,7 +73,7 @@ describe("GET /api/repos", () => {
it("returns 500 on database error", async () => {
mocks.repositoryFindMany.mockRejectedValue(new Error("db connection lost"));

const res = await GET();
const res = await GET(authed());

expect(res.status).toBe(500);
const body = await res.json();
Expand Down
5 changes: 4 additions & 1 deletion src/app/api/repos/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { isValidRepoName } from "@/lib/config";
import { auditTrackedRepoCreateFailure, createTrackedRepo } from "@/lib/tracked-repos";
import { authorizeRequest } from "@/lib/auth";

export async function GET() {
export async function GET(request: Request) {
if (!(await authorizeRequest(request)).authorized) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
try {
const repos = await prisma.repository.findMany({
orderBy: { fullName: "asc" },
Expand Down