11import { describe , expect , test } from "bun:test" ;
22import * as v from "valibot" ;
3- import { getExtensionFromKey , S3_KEY_PATTERN , S3KeySchema , validateS3Key } from "./storage" ;
3+ import {
4+ extractS3KeyFromUrl ,
5+ getExtensionFromKey ,
6+ S3_KEY_PATTERN ,
7+ S3KeySchema ,
8+ validateS3Key ,
9+ } from "./storage" ;
410
511describe ( "storage" , ( ) => {
612 describe ( "validateS3Key" , ( ) => {
7- test ( "accepts valid UUID key" , ( ) => {
8- expect ( validateS3Key ( "a1b2c3d4-e5f6 /a1b2c3d4-e5f6.jpg" ) ) . toBe ( true ) ;
13+ test ( "accepts valid key with folder/uuid-filename format " , ( ) => {
14+ expect ( validateS3Key ( "articles /a1b2c3d4-e5f6-7890-abcd-ef1234567890-cover .jpg" ) ) . toBe ( true ) ;
915 } ) ;
1016
1117 test ( "accepts valid key with full UUID format" , ( ) => {
12- expect (
13- validateS3Key (
14- "550e8400-e29b-41d4-a716-446655440000/550e8400-e29b-41d4-a716-446655440000.png" ,
15- ) ,
16- ) . toBe ( true ) ;
18+ expect ( validateS3Key ( "projects/550e8400-e29b-41d4-a716-446655440000-thumbnail.png" ) ) . toBe (
19+ true ,
20+ ) ;
1721 } ) ;
1822
19- test ( "accepts various file extensions" , ( ) => {
20- expect ( validateS3Key ( "abc123/def456.jpg" ) ) . toBe ( true ) ;
21- expect ( validateS3Key ( "abc123/def456.png" ) ) . toBe ( true ) ;
22- expect ( validateS3Key ( "abc123/def456.webp" ) ) . toBe ( true ) ;
23- expect ( validateS3Key ( "abc123/def456.gif" ) ) . toBe ( true ) ;
23+ test ( "accepts various allowed folders" , ( ) => {
24+ expect ( validateS3Key ( "images/abc-123-file.jpg" ) ) . toBe ( true ) ;
25+ expect ( validateS3Key ( "uploads/abc-123-file.png" ) ) . toBe ( true ) ;
26+ expect ( validateS3Key ( "covers/abc-123-file.webp" ) ) . toBe ( true ) ;
27+ expect ( validateS3Key ( "avatars/abc-123-file.gif" ) ) . toBe ( true ) ;
28+ expect ( validateS3Key ( "members/abc-123-file.jpg" ) ) . toBe ( true ) ;
2429 } ) ;
2530
2631 test ( "rejects path traversal attempt" , ( ) => {
2732 expect ( validateS3Key ( "../../../etc/passwd" ) ) . toBe ( false ) ;
2833 } ) ;
2934
3035 test ( "rejects key without extension" , ( ) => {
31- expect ( validateS3Key ( "a1b2c3d4-e5f6 /a1b2c3d4-e5f6 " ) ) . toBe ( false ) ;
36+ expect ( validateS3Key ( "articles /a1b2c3d4-cover " ) ) . toBe ( false ) ;
3237 } ) ;
3338
3439 test ( "rejects key without slash" , ( ) => {
@@ -43,8 +48,12 @@ describe("storage", () => {
4348 expect ( validateS3Key ( "test/../admin/secret.jpg" ) ) . toBe ( false ) ;
4449 } ) ;
4550
46- test ( "rejects uppercase letters" , ( ) => {
47- expect ( validateS3Key ( "ABC123/DEF456.jpg" ) ) . toBe ( false ) ;
51+ test ( "rejects uppercase folder" , ( ) => {
52+ expect ( validateS3Key ( "ARTICLES/abc-123.jpg" ) ) . toBe ( false ) ;
53+ } ) ;
54+
55+ test ( "rejects invalid folder" , ( ) => {
56+ expect ( validateS3Key ( "invalid/abc-123-file.jpg" ) ) . toBe ( false ) ;
4857 } ) ;
4958
5059 test ( "rejects empty string" , ( ) => {
@@ -54,19 +63,19 @@ describe("storage", () => {
5463
5564 describe ( "getExtensionFromKey" , ( ) => {
5665 test ( "extracts jpg extension" , ( ) => {
57- expect ( getExtensionFromKey ( "abc123/def456 .jpg" ) ) . toBe ( "jpg" ) ;
66+ expect ( getExtensionFromKey ( "articles/abc-123-file .jpg" ) ) . toBe ( "jpg" ) ;
5867 } ) ;
5968
6069 test ( "extracts png extension" , ( ) => {
61- expect ( getExtensionFromKey ( "abc123/def456 .png" ) ) . toBe ( "png" ) ;
70+ expect ( getExtensionFromKey ( "projects/abc-123-file .png" ) ) . toBe ( "png" ) ;
6271 } ) ;
6372
6473 test ( "extracts webp extension" , ( ) => {
65- expect ( getExtensionFromKey ( "abc123/def456 .webp" ) ) . toBe ( "webp" ) ;
74+ expect ( getExtensionFromKey ( "images/abc-123-file .webp" ) ) . toBe ( "webp" ) ;
6675 } ) ;
6776
6877 test ( "returns null for key without extension" , ( ) => {
69- expect ( getExtensionFromKey ( "abc123/def456 " ) ) . toBe ( null ) ;
78+ expect ( getExtensionFromKey ( "articles/abc-123-file " ) ) . toBe ( null ) ;
7079 } ) ;
7180
7281 test ( "returns null for empty string" , ( ) => {
@@ -76,7 +85,7 @@ describe("storage", () => {
7685
7786 describe ( "S3KeySchema (Valibot)" , ( ) => {
7887 test ( "passes valid key" , ( ) => {
79- const result = v . safeParse ( S3KeySchema , "a1b2c3d4-e5f6 /a1b2c3d4-e5f6 .jpg" ) ;
88+ const result = v . safeParse ( S3KeySchema , "articles /a1b2c3d4-cover .jpg" ) ;
8089 expect ( result . success ) . toBe ( true ) ;
8190 } ) ;
8291
@@ -89,15 +98,58 @@ describe("storage", () => {
8998 } ) ;
9099
91100 test ( "fails path without extension" , ( ) => {
92- const result = v . safeParse ( S3KeySchema , "abc123/def456 " ) ;
101+ const result = v . safeParse ( S3KeySchema , "articles/abc-123-file " ) ;
93102 expect ( result . success ) . toBe ( false ) ;
94103 } ) ;
104+
105+ test ( "fails with invalid folder" , ( ) => {
106+ const result = v . safeParse ( S3KeySchema , "invalid/abc-123-file.jpg" ) ;
107+ expect ( result . success ) . toBe ( false ) ;
108+ if ( ! result . success ) {
109+ expect ( result . issues [ 0 ] . message ) . toBe ( "Invalid folder" ) ;
110+ }
111+ } ) ;
95112 } ) ;
96113
97114 describe ( "S3_KEY_PATTERN" , ( ) => {
98115 test ( "is exported and usable" , ( ) => {
99116 expect ( S3_KEY_PATTERN ) . toBeInstanceOf ( RegExp ) ;
100- expect ( S3_KEY_PATTERN . test ( "abc123/def456.jpg" ) ) . toBe ( true ) ;
117+ expect ( S3_KEY_PATTERN . test ( "articles/abc-123-file.jpg" ) ) . toBe ( true ) ;
118+ } ) ;
119+ } ) ;
120+
121+ describe ( "extractS3KeyFromUrl" , ( ) => {
122+ const baseUrl = "http://localhost:9000/dev" ;
123+
124+ test ( "extracts key from valid S3 URL" , ( ) => {
125+ expect ( extractS3KeyFromUrl ( `${ baseUrl } /articles/a1b2c3d4-cover.webp` , baseUrl ) ) . toBe (
126+ "articles/a1b2c3d4-cover.webp" ,
127+ ) ;
128+ } ) ;
129+
130+ test ( "returns null for external URL" , ( ) => {
131+ expect ( extractS3KeyFromUrl ( "https://example.com/image.jpg" , baseUrl ) ) . toBe ( null ) ;
132+ } ) ;
133+
134+ test ( "returns null for URL with different base" , ( ) => {
135+ expect ( extractS3KeyFromUrl ( "http://other.host/articles/a1b2c3d4-cover.webp" , baseUrl ) ) . toBe (
136+ null ,
137+ ) ;
138+ } ) ;
139+
140+ test ( "returns null for invalid key format" , ( ) => {
141+ expect ( extractS3KeyFromUrl ( `${ baseUrl } /invalid/key` , baseUrl ) ) . toBe ( null ) ;
142+ } ) ;
143+
144+ test ( "returns null for empty URL" , ( ) => {
145+ expect ( extractS3KeyFromUrl ( "" , baseUrl ) ) . toBe ( null ) ;
146+ } ) ;
147+
148+ test ( "works with production-like URLs" , ( ) => {
149+ const prodUrl = "https://s3.example.com/bucket" ;
150+ expect ( extractS3KeyFromUrl ( `${ prodUrl } /members/a1b2c3d4-avatar.png` , prodUrl ) ) . toBe (
151+ "members/a1b2c3d4-avatar.png" ,
152+ ) ;
101153 } ) ;
102154 } ) ;
103155} ) ;
0 commit comments