-
Notifications
You must be signed in to change notification settings - Fork 9
Merge test into main #551
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merge test into main #551
Changes from all commits
99d9cdd
82e6280
329c589
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import type { NextRequest } from "next/server"; | ||
| import { NextResponse } from "next/server"; | ||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||
| import { postSandboxesUploadTokensHandler } from "@/lib/sandbox/postSandboxesUploadTokensHandler"; | ||
|
|
||
| /** | ||
| * CORS preflight. | ||
| * | ||
| * @returns A NextResponse with CORS headers. | ||
| */ | ||
| export async function OPTIONS() { | ||
| return new NextResponse(null, { status: 200, headers: getCorsHeaders() }); | ||
| } | ||
|
|
||
| /** | ||
| * POST /api/sandboxes/staged-file β Vercel Blob client-upload token handshake. | ||
| * | ||
| * @param request - The request object. | ||
| * @returns A NextResponse with the handshake result or error. | ||
| */ | ||
| export async function POST(request: NextRequest): Promise<Response> { | ||
| return postSandboxesUploadTokensHandler(request); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,124 @@ | ||
| import { describe, it, expect, vi, beforeEach } from "vitest"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Custom agent: Enforce Clear Code Style and Maintainability Practices This new test file exceeds the repositoryβs 100-line maintainability limit. Prompt for AI agents |
||
| import { NextResponse } from "next/server"; | ||
| import type { NextRequest } from "next/server"; | ||
| import { handleUpload } from "@vercel/blob/client"; | ||
|
|
||
| import { postSandboxesUploadTokensHandler } from "../postSandboxesUploadTokensHandler"; | ||
| import { validateAuthContext } from "@/lib/auth/validateAuthContext"; | ||
|
|
||
| vi.mock("@vercel/blob/client", () => ({ | ||
| handleUpload: vi.fn(), | ||
| })); | ||
|
|
||
| vi.mock("@/lib/auth/validateAuthContext", () => ({ | ||
| validateAuthContext: vi.fn(), | ||
| })); | ||
|
|
||
| function createMockRequest(body: unknown, headers: Record<string, string> = {}): NextRequest { | ||
| return new Request("http://localhost:3000/api/sandboxes/staged-file", { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json", ...headers }, | ||
| body: JSON.stringify(body), | ||
| }) as unknown as NextRequest; | ||
| } | ||
|
|
||
| const handshakeBody = { type: "blob.generate-client-token", payload: {} }; | ||
| const callbackBody = { type: "blob.upload-completed", payload: {} }; | ||
|
|
||
| describe("postSandboxesUploadTokensHandler", () => { | ||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| vi.mocked(validateAuthContext).mockResolvedValue({ | ||
| accountId: "acc_123", | ||
| orgId: null, | ||
| authToken: "tkn", | ||
| }); | ||
| }); | ||
|
|
||
| it("returns 200 with the handleUpload result on a valid handshake", async () => { | ||
| const blobResponse = { type: "blob.generate-client-token", clientToken: "tkn_abc" }; | ||
| vi.mocked(handleUpload).mockResolvedValue(blobResponse as never); | ||
|
|
||
| const request = createMockRequest(handshakeBody, { Authorization: "Bearer xyz" }); | ||
| const response = await postSandboxesUploadTokensHandler(request); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(await response.json()).toEqual(blobResponse); | ||
| expect(validateAuthContext).toHaveBeenCalledOnce(); | ||
| expect(handleUpload).toHaveBeenCalledOnce(); | ||
| }); | ||
|
|
||
| it("returns 401 when handshake auth fails", async () => { | ||
| vi.mocked(validateAuthContext).mockResolvedValue( | ||
| NextResponse.json({ error: "Unauthorized" }, { status: 401 }), | ||
| ); | ||
|
|
||
| const request = createMockRequest(handshakeBody); | ||
| const response = await postSandboxesUploadTokensHandler(request); | ||
|
|
||
| expect(response.status).toBe(401); | ||
| expect(handleUpload).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it("skips auth on the upload-completed callback", async () => { | ||
| vi.mocked(handleUpload).mockResolvedValue({ type: "blob.upload-completed" } as never); | ||
|
|
||
| const request = createMockRequest(callbackBody); | ||
| const response = await postSandboxesUploadTokensHandler(request); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(validateAuthContext).not.toHaveBeenCalled(); | ||
| expect(handleUpload).toHaveBeenCalledOnce(); | ||
| }); | ||
|
|
||
| it("configures the upload constraints in onBeforeGenerateToken", async () => { | ||
| let constraints: unknown; | ||
| vi.mocked(handleUpload).mockImplementation(async ({ onBeforeGenerateToken }) => { | ||
| constraints = await onBeforeGenerateToken!("file.png", null, false); | ||
| return { type: "blob.generate-client-token" } as never; | ||
| }); | ||
|
|
||
| const request = createMockRequest(handshakeBody, { Authorization: "Bearer xyz" }); | ||
| const response = await postSandboxesUploadTokensHandler(request); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| expect(constraints).toEqual({ | ||
| maximumSizeInBytes: 100 * 1024 * 1024, | ||
| addRandomSuffix: true, | ||
| }); | ||
| }); | ||
|
|
||
| it("returns 500 with a generic message when handleUpload throws", async () => { | ||
| const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); | ||
| vi.mocked(handleUpload).mockRejectedValue(new Error("blob client failure")); | ||
|
|
||
| const request = createMockRequest(handshakeBody, { Authorization: "Bearer xyz" }); | ||
| const response = await postSandboxesUploadTokensHandler(request); | ||
|
|
||
| expect(response.status).toBe(500); | ||
| const body = await response.json(); | ||
| expect(body).toEqual({ status: "error", error: "Failed to issue upload token" }); | ||
| expect(consoleSpy).toHaveBeenCalledOnce(); | ||
| consoleSpy.mockRestore(); | ||
| }); | ||
|
|
||
| it("includes CORS headers on success", async () => { | ||
| vi.mocked(handleUpload).mockResolvedValue({ type: "blob.generate-client-token" } as never); | ||
|
|
||
| const request = createMockRequest(handshakeBody, { Authorization: "Bearer xyz" }); | ||
| const response = await postSandboxesUploadTokensHandler(request); | ||
|
|
||
| expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); | ||
| }); | ||
|
|
||
| it("includes CORS headers on error", async () => { | ||
| const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); | ||
| vi.mocked(handleUpload).mockRejectedValue(new Error("nope")); | ||
|
|
||
| const request = createMockRequest(handshakeBody, { Authorization: "Bearer xyz" }); | ||
| const response = await postSandboxesUploadTokensHandler(request); | ||
|
|
||
| expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*"); | ||
| consoleSpy.mockRestore(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,41 @@ | ||||||
| import { handleUpload, type HandleUploadBody } from "@vercel/blob/client"; | ||||||
| import type { NextRequest } from "next/server"; | ||||||
| import { NextResponse } from "next/server"; | ||||||
| import { getCorsHeaders } from "@/lib/networking/getCorsHeaders"; | ||||||
| import { validateAuthContext } from "@/lib/auth/validateAuthContext"; | ||||||
|
|
||||||
| const MAX_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024; // 100MB | ||||||
|
|
||||||
| // Auth applies only to the handshake β the upload-completed callback is signature-verified by handleUpload(). | ||||||
| export async function postSandboxesUploadTokensHandler( | ||||||
| request: NextRequest, | ||||||
| ): Promise<NextResponse> { | ||||||
| try { | ||||||
| const body = (await request.json()) as HandleUploadBody; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Invalid/malformed request bodies are handled as 500s because JSON parsing isnβt validated separately. Return a 400 for bad JSON before calling Prompt for AI agents |
||||||
|
|
||||||
| if (body.type === "blob.generate-client-token") { | ||||||
| const auth = await validateAuthContext(request); | ||||||
| if (auth instanceof NextResponse) { | ||||||
| return auth; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| const jsonResponse = await handleUpload({ | ||||||
| body, | ||||||
| request, | ||||||
| onBeforeGenerateToken: async () => ({ | ||||||
| maximumSizeInBytes: MAX_UPLOAD_SIZE_BYTES, | ||||||
| addRandomSuffix: true, | ||||||
| }), | ||||||
| onUploadCompleted: async () => {}, | ||||||
| }); | ||||||
|
|
||||||
| return NextResponse.json(jsonResponse, { headers: getCorsHeaders() }); | ||||||
| } catch (error) { | ||||||
| console.error("[postSandboxesUploadTokensHandler] handleUpload failed:", error); | ||||||
| return NextResponse.json( | ||||||
| { status: "error", error: "Failed to issue upload token" }, | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Use the standardized 500 error message (Based on your team's feedback about standardizing 500 responses to avoid leaking internal details.) Prompt for AI agents
Suggested change
|
||||||
| { status: 500, headers: getCorsHeaders() }, | ||||||
| ); | ||||||
| } | ||||||
| } | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Route path appears to be singular (
staged-file) while the migration target is documented as plural (staged-files), which can break the intended endpoint.Prompt for AI agents