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
7 changes: 3 additions & 4 deletions src/services/file-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Features:
* - File upload via GraphQL fileUpload mutation
* - File download with automatic authentication
* - Signed URL detection (skips Bearer token for signed URLs)
* - Signed URL detection (skips authentication for signed URLs)
* - Directory creation and file existence checks
* - Comprehensive error handling and status reporting
*/
Expand Down Expand Up @@ -139,7 +139,7 @@ export class FileService {
* Downloads a file from Linear's private cloud storage.
*
* Automatically handles authentication for Linear URLs and creates directories
* as needed. Detects signed URLs to skip Bearer token authentication.
* as needed. Detects signed URLs to skip authentication.
*
* @param url - URL to Linear file (uploads.linear.app domain)
* @param options - Download options including output path and overwrite behavior
Expand Down Expand Up @@ -192,10 +192,9 @@ export class FileService {
const urlObj = new URL(url);
const isSignedUrl = urlObj.searchParams.has("signature");

// Make HTTP request (with Bearer token only if not a signed URL)
const headers: Record<string, string> = {};
if (!isSignedUrl) {
headers["Authorization"] = `Bearer ${this.apiToken}`;
headers["Authorization"] = this.apiToken;
}

const response = await fetch(url, {
Expand Down
16 changes: 9 additions & 7 deletions tests/unit/services/file-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

const TEST_TOKEN = "lin_api_test_token";
const TEST_TOKEN = "test_token";

beforeEach(() => {
vi.clearAllMocks();
Expand All @@ -50,7 +50,10 @@ describe("downloadFile", () => {
expect(mockFetch).not.toHaveBeenCalled();
});

it("downloads file successfully", async () => {
it.each([
Comment thread
iamfj marked this conversation as resolved.
["a personal API key", "", { Authorization: TEST_TOKEN }],
["no credentials on a signed URL", "?signature=test", {}],
])("downloads with %s", async (_name, query, expectedHeaders) => {
vi.mocked(isLinearUploadUrl).mockReturnValue(true);
vi.mocked(extractFilenameFromUrl).mockReturnValue("image.png");
vi.mocked(access).mockRejectedValue(new Error("ENOENT")); // file doesn't exist
Expand All @@ -65,19 +68,18 @@ describe("downloadFile", () => {
});

const service = new FileService(TEST_TOKEN);
const result = await service.downloadFile(
"https://uploads.linear.app/org/file.png",
);
const url = `https://uploads.linear.app/org/file.png${query}`;
const result = await service.downloadFile(url);

expect(result).toEqual({
success: true,
filePath: "image.png",
});
expect(mockFetch).toHaveBeenCalledWith(
"https://uploads.linear.app/org/file.png",
url,
expect.objectContaining({
method: "GET",
headers: { Authorization: `Bearer ${TEST_TOKEN}` },
headers: expectedHeaders,
}),
);
expect(writeFile).toHaveBeenCalled();
Expand Down
Loading