kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 166 lines 5.3 kB view raw
1import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2import { 3 assertTaskImageKeyMatchesContext, 4 buildObjectKey, 5 buildObjectKeyPrefix, 6 createTaskImageUploadUrl, 7 getFileExtension, 8 isImageContentType, 9 parseBoolean, 10 parsePositiveInt, 11 sanitizePathSegment, 12 validateTaskAssetUploadInput, 13} from "../../../apps/api/src/storage/s3"; 14 15describe("S3 helpers", () => { 16 const originalMaxSize = process.env.S3_MAX_IMAGE_UPLOAD_BYTES; 17 const originalEndpoint = process.env.S3_ENDPOINT; 18 const originalBucket = process.env.S3_BUCKET; 19 const originalAccessKeyId = process.env.S3_ACCESS_KEY_ID; 20 const originalSecretAccessKey = process.env.S3_SECRET_ACCESS_KEY; 21 const originalRegion = process.env.S3_REGION; 22 const originalPathStyle = process.env.S3_FORCE_PATH_STYLE; 23 24 beforeEach(() => { 25 delete process.env.S3_MAX_IMAGE_UPLOAD_BYTES; 26 }); 27 28 afterEach(() => { 29 vi.restoreAllMocks(); 30 31 if (originalMaxSize === undefined) { 32 delete process.env.S3_MAX_IMAGE_UPLOAD_BYTES; 33 } else { 34 process.env.S3_MAX_IMAGE_UPLOAD_BYTES = originalMaxSize; 35 } 36 37 if (originalEndpoint === undefined) { 38 delete process.env.S3_ENDPOINT; 39 } else { 40 process.env.S3_ENDPOINT = originalEndpoint; 41 } 42 43 if (originalBucket === undefined) { 44 delete process.env.S3_BUCKET; 45 } else { 46 process.env.S3_BUCKET = originalBucket; 47 } 48 49 if (originalAccessKeyId === undefined) { 50 delete process.env.S3_ACCESS_KEY_ID; 51 } else { 52 process.env.S3_ACCESS_KEY_ID = originalAccessKeyId; 53 } 54 55 if (originalSecretAccessKey === undefined) { 56 delete process.env.S3_SECRET_ACCESS_KEY; 57 } else { 58 process.env.S3_SECRET_ACCESS_KEY = originalSecretAccessKey; 59 } 60 61 if (originalRegion === undefined) { 62 delete process.env.S3_REGION; 63 } else { 64 process.env.S3_REGION = originalRegion; 65 } 66 67 if (originalPathStyle === undefined) { 68 delete process.env.S3_FORCE_PATH_STYLE; 69 } else { 70 process.env.S3_FORCE_PATH_STYLE = originalPathStyle; 71 } 72 }); 73 74 it("recognizes allowed image content types case-insensitively", () => { 75 expect(isImageContentType("IMAGE/PNG")).toBe(true); 76 expect(isImageContentType("text/plain")).toBe(false); 77 }); 78 79 it("parses booleans and positive integers with fallbacks", () => { 80 expect(parseBoolean(undefined, true)).toBe(true); 81 expect(parseBoolean(" false ", true)).toBe(false); 82 expect(parsePositiveInt("42", 10)).toBe(42); 83 expect(parsePositiveInt("0", 10)).toBe(10); 84 expect(parsePositiveInt("nope", 10)).toBe(10); 85 }); 86 87 it("sanitizes path segments and extracts normalized extensions", () => { 88 expect(sanitizePathSegment(" Release Notes!!.PNG ")).toBe( 89 "release-notes-.png", 90 ); 91 expect(sanitizePathSegment("")).toBe("file"); 92 expect(getFileExtension("Screenshot.Final.PNG")).toBe("png"); 93 expect(getFileExtension("README")).toBe("file"); 94 }); 95 96 it("builds stable key prefixes and keys", () => { 97 vi.spyOn(Date, "now").mockReturnValue(1_717_171_717_000); 98 99 const key = buildObjectKey({ 100 workspaceId: "Workspace 1", 101 projectId: "Project 2", 102 taskId: "Task 3", 103 surface: "comment", 104 filename: "Sprint Plan Final!!.PNG", 105 contentType: "image/png", 106 }); 107 108 expect( 109 buildObjectKeyPrefix({ 110 workspaceId: "Workspace 1", 111 projectId: "Project 2", 112 taskId: "Task 3", 113 surface: "comment", 114 }), 115 ).toBe("workspace/workspace-1/project/project-2/task/task-3/comments"); 116 117 expect(key).toMatch( 118 /^workspace\/workspace-1\/project\/project-2\/task\/task-3\/comments\/sprint-plan-final-1717171717000-[a-z0-9]+\.png$/, 119 ); 120 expect( 121 assertTaskImageKeyMatchesContext(key, { 122 workspaceId: "Workspace 1", 123 projectId: "Project 2", 124 taskId: "Task 3", 125 surface: "comment", 126 }), 127 ).toBe(true); 128 }); 129 130 it("validates upload size against the configured maximum", () => { 131 process.env.S3_MAX_IMAGE_UPLOAD_BYTES = "1048576"; 132 133 expect(() => validateTaskAssetUploadInput("", 10)).toThrow( 134 "A valid content type is required.", 135 ); 136 expect(() => validateTaskAssetUploadInput("image/png", 0)).toThrow( 137 "Upload size must be greater than zero.", 138 ); 139 expect(() => 140 validateTaskAssetUploadInput("image/png", 2 * 1024 * 1024), 141 ).toThrow("Upload exceeds the maximum upload size of 1MB."); 142 expect(() => validateTaskAssetUploadInput("image/png", 512)).not.toThrow(); 143 }); 144 145 it("creates presigned upload URLs without hoisted checksum query params", async () => { 146 process.env.S3_ENDPOINT = "https://storage.example.test"; 147 process.env.S3_BUCKET = "kaneo"; 148 process.env.S3_ACCESS_KEY_ID = "test-access-key"; 149 process.env.S3_SECRET_ACCESS_KEY = "test-secret-key"; 150 process.env.S3_REGION = "us-east-1"; 151 process.env.S3_FORCE_PATH_STYLE = "true"; 152 153 const upload = await createTaskImageUploadUrl({ 154 workspaceId: "workspace-1", 155 projectId: "project-1", 156 taskId: "task-1", 157 surface: "description", 158 filename: "report.png", 159 contentType: "image/png", 160 }); 161 162 const searchParams = new URL(upload.uploadUrl).searchParams; 163 expect(searchParams.has("x-amz-checksum-crc32")).toBe(false); 164 expect(searchParams.has("x-amz-sdk-checksum-algorithm")).toBe(false); 165 }); 166});