s3api_conditional_headers_test.go 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849
  1. package s3api
  2. import (
  3. "bytes"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "testing"
  8. "time"
  9. "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
  10. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  11. "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
  12. )
  13. // TestConditionalHeadersWithExistingObjects tests conditional headers against existing objects
  14. // This addresses the PR feedback about missing test coverage for object existence scenarios
  15. func TestConditionalHeadersWithExistingObjects(t *testing.T) {
  16. bucket := "test-bucket"
  17. object := "/test-object"
  18. // Mock object with known ETag and modification time
  19. testObject := &filer_pb.Entry{
  20. Name: "test-object",
  21. Extended: map[string][]byte{
  22. s3_constants.ExtETagKey: []byte("\"abc123\""),
  23. },
  24. Attributes: &filer_pb.FuseAttributes{
  25. Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(), // June 15, 2024
  26. FileSize: 1024, // Add file size
  27. },
  28. Chunks: []*filer_pb.FileChunk{
  29. // Add a mock chunk to make calculateETagFromChunks work
  30. {
  31. FileId: "test-file-id",
  32. Offset: 0,
  33. Size: 1024,
  34. },
  35. },
  36. }
  37. // Test If-None-Match with existing object
  38. t.Run("IfNoneMatch_ObjectExists", func(t *testing.T) {
  39. // Test case 1: If-None-Match=* when object exists (should fail)
  40. t.Run("Asterisk_ShouldFail", func(t *testing.T) {
  41. getter := createMockEntryGetter(testObject)
  42. req := createTestPutRequest(bucket, object, "test content")
  43. req.Header.Set(s3_constants.IfNoneMatch, "*")
  44. s3a := NewS3ApiServerForTest()
  45. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  46. if errCode != s3err.ErrPreconditionFailed {
  47. t.Errorf("Expected ErrPreconditionFailed when object exists with If-None-Match=*, got %v", errCode)
  48. }
  49. })
  50. // Test case 2: If-None-Match with matching ETag (should fail)
  51. t.Run("MatchingETag_ShouldFail", func(t *testing.T) {
  52. getter := createMockEntryGetter(testObject)
  53. req := createTestPutRequest(bucket, object, "test content")
  54. req.Header.Set(s3_constants.IfNoneMatch, "\"abc123\"")
  55. s3a := NewS3ApiServerForTest()
  56. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  57. if errCode != s3err.ErrPreconditionFailed {
  58. t.Errorf("Expected ErrPreconditionFailed when ETag matches, got %v", errCode)
  59. }
  60. })
  61. // Test case 3: If-None-Match with non-matching ETag (should succeed)
  62. t.Run("NonMatchingETag_ShouldSucceed", func(t *testing.T) {
  63. getter := createMockEntryGetter(testObject)
  64. req := createTestPutRequest(bucket, object, "test content")
  65. req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\"")
  66. s3a := NewS3ApiServerForTest()
  67. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  68. if errCode != s3err.ErrNone {
  69. t.Errorf("Expected ErrNone when ETag doesn't match, got %v", errCode)
  70. }
  71. })
  72. // Test case 4: If-None-Match with multiple ETags, one matching (should fail)
  73. t.Run("MultipleETags_OneMatches_ShouldFail", func(t *testing.T) {
  74. getter := createMockEntryGetter(testObject)
  75. req := createTestPutRequest(bucket, object, "test content")
  76. req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\", \"abc123\", \"def456\"")
  77. s3a := NewS3ApiServerForTest()
  78. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  79. if errCode != s3err.ErrPreconditionFailed {
  80. t.Errorf("Expected ErrPreconditionFailed when one ETag matches, got %v", errCode)
  81. }
  82. })
  83. // Test case 5: If-None-Match with multiple ETags, none matching (should succeed)
  84. t.Run("MultipleETags_NoneMatch_ShouldSucceed", func(t *testing.T) {
  85. getter := createMockEntryGetter(testObject)
  86. req := createTestPutRequest(bucket, object, "test content")
  87. req.Header.Set(s3_constants.IfNoneMatch, "\"xyz789\", \"def456\", \"ghi123\"")
  88. s3a := NewS3ApiServerForTest()
  89. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  90. if errCode != s3err.ErrNone {
  91. t.Errorf("Expected ErrNone when no ETags match, got %v", errCode)
  92. }
  93. })
  94. })
  95. // Test If-Match with existing object
  96. t.Run("IfMatch_ObjectExists", func(t *testing.T) {
  97. // Test case 1: If-Match with matching ETag (should succeed)
  98. t.Run("MatchingETag_ShouldSucceed", func(t *testing.T) {
  99. getter := createMockEntryGetter(testObject)
  100. req := createTestPutRequest(bucket, object, "test content")
  101. req.Header.Set(s3_constants.IfMatch, "\"abc123\"")
  102. s3a := NewS3ApiServerForTest()
  103. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  104. if errCode != s3err.ErrNone {
  105. t.Errorf("Expected ErrNone when ETag matches, got %v", errCode)
  106. }
  107. })
  108. // Test case 2: If-Match with non-matching ETag (should fail)
  109. t.Run("NonMatchingETag_ShouldFail", func(t *testing.T) {
  110. getter := createMockEntryGetter(testObject)
  111. req := createTestPutRequest(bucket, object, "test content")
  112. req.Header.Set(s3_constants.IfMatch, "\"xyz789\"")
  113. s3a := NewS3ApiServerForTest()
  114. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  115. if errCode != s3err.ErrPreconditionFailed {
  116. t.Errorf("Expected ErrPreconditionFailed when ETag doesn't match, got %v", errCode)
  117. }
  118. })
  119. // Test case 3: If-Match with multiple ETags, one matching (should succeed)
  120. t.Run("MultipleETags_OneMatches_ShouldSucceed", func(t *testing.T) {
  121. getter := createMockEntryGetter(testObject)
  122. req := createTestPutRequest(bucket, object, "test content")
  123. req.Header.Set(s3_constants.IfMatch, "\"xyz789\", \"abc123\"")
  124. s3a := NewS3ApiServerForTest()
  125. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  126. if errCode != s3err.ErrNone {
  127. t.Errorf("Expected ErrNone when one ETag matches, got %v", errCode)
  128. }
  129. })
  130. // Test case 4: If-Match with wildcard * (should succeed if object exists)
  131. t.Run("Wildcard_ShouldSucceed", func(t *testing.T) {
  132. getter := createMockEntryGetter(testObject)
  133. req := createTestPutRequest(bucket, object, "test content")
  134. req.Header.Set(s3_constants.IfMatch, "*")
  135. s3a := NewS3ApiServerForTest()
  136. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  137. if errCode != s3err.ErrNone {
  138. t.Errorf("Expected ErrNone when If-Match=* and object exists, got %v", errCode)
  139. }
  140. })
  141. })
  142. // Test If-Modified-Since with existing object
  143. t.Run("IfModifiedSince_ObjectExists", func(t *testing.T) {
  144. // Test case 1: If-Modified-Since with date before object modification (should succeed)
  145. t.Run("DateBefore_ShouldSucceed", func(t *testing.T) {
  146. getter := createMockEntryGetter(testObject)
  147. req := createTestPutRequest(bucket, object, "test content")
  148. dateBeforeModification := time.Date(2024, 6, 14, 12, 0, 0, 0, time.UTC)
  149. req.Header.Set(s3_constants.IfModifiedSince, dateBeforeModification.Format(time.RFC1123))
  150. s3a := NewS3ApiServerForTest()
  151. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  152. if errCode != s3err.ErrNone {
  153. t.Errorf("Expected ErrNone when object was modified after date, got %v", errCode)
  154. }
  155. })
  156. // Test case 2: If-Modified-Since with date after object modification (should fail)
  157. t.Run("DateAfter_ShouldFail", func(t *testing.T) {
  158. getter := createMockEntryGetter(testObject)
  159. req := createTestPutRequest(bucket, object, "test content")
  160. dateAfterModification := time.Date(2024, 6, 16, 12, 0, 0, 0, time.UTC)
  161. req.Header.Set(s3_constants.IfModifiedSince, dateAfterModification.Format(time.RFC1123))
  162. s3a := NewS3ApiServerForTest()
  163. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  164. if errCode != s3err.ErrPreconditionFailed {
  165. t.Errorf("Expected ErrPreconditionFailed when object wasn't modified since date, got %v", errCode)
  166. }
  167. })
  168. // Test case 3: If-Modified-Since with exact modification date (should fail - not after)
  169. t.Run("ExactDate_ShouldFail", func(t *testing.T) {
  170. getter := createMockEntryGetter(testObject)
  171. req := createTestPutRequest(bucket, object, "test content")
  172. exactDate := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)
  173. req.Header.Set(s3_constants.IfModifiedSince, exactDate.Format(time.RFC1123))
  174. s3a := NewS3ApiServerForTest()
  175. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  176. if errCode != s3err.ErrPreconditionFailed {
  177. t.Errorf("Expected ErrPreconditionFailed when object modification time equals header date, got %v", errCode)
  178. }
  179. })
  180. })
  181. // Test If-Unmodified-Since with existing object
  182. t.Run("IfUnmodifiedSince_ObjectExists", func(t *testing.T) {
  183. // Test case 1: If-Unmodified-Since with date after object modification (should succeed)
  184. t.Run("DateAfter_ShouldSucceed", func(t *testing.T) {
  185. getter := createMockEntryGetter(testObject)
  186. req := createTestPutRequest(bucket, object, "test content")
  187. dateAfterModification := time.Date(2024, 6, 16, 12, 0, 0, 0, time.UTC)
  188. req.Header.Set(s3_constants.IfUnmodifiedSince, dateAfterModification.Format(time.RFC1123))
  189. s3a := NewS3ApiServerForTest()
  190. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  191. if errCode != s3err.ErrNone {
  192. t.Errorf("Expected ErrNone when object wasn't modified after date, got %v", errCode)
  193. }
  194. })
  195. // Test case 2: If-Unmodified-Since with date before object modification (should fail)
  196. t.Run("DateBefore_ShouldFail", func(t *testing.T) {
  197. getter := createMockEntryGetter(testObject)
  198. req := createTestPutRequest(bucket, object, "test content")
  199. dateBeforeModification := time.Date(2024, 6, 14, 12, 0, 0, 0, time.UTC)
  200. req.Header.Set(s3_constants.IfUnmodifiedSince, dateBeforeModification.Format(time.RFC1123))
  201. s3a := NewS3ApiServerForTest()
  202. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  203. if errCode != s3err.ErrPreconditionFailed {
  204. t.Errorf("Expected ErrPreconditionFailed when object was modified after date, got %v", errCode)
  205. }
  206. })
  207. })
  208. }
  209. // TestConditionalHeadersForReads tests conditional headers for read operations (GET, HEAD)
  210. // This implements AWS S3 conditional reads behavior where different conditions return different status codes
  211. // See: https://docs.aws.amazon.com/AmazonS3/latest/userguide/conditional-reads.html
  212. func TestConditionalHeadersForReads(t *testing.T) {
  213. bucket := "test-bucket"
  214. object := "/test-read-object"
  215. // Mock existing object to test conditional headers against
  216. existingObject := &filer_pb.Entry{
  217. Name: "test-read-object",
  218. Extended: map[string][]byte{
  219. s3_constants.ExtETagKey: []byte("\"read123\""),
  220. },
  221. Attributes: &filer_pb.FuseAttributes{
  222. Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(),
  223. FileSize: 1024,
  224. },
  225. Chunks: []*filer_pb.FileChunk{
  226. {
  227. FileId: "read-file-id",
  228. Offset: 0,
  229. Size: 1024,
  230. },
  231. },
  232. }
  233. // Test conditional reads with existing object
  234. t.Run("ConditionalReads_ObjectExists", func(t *testing.T) {
  235. // Test If-None-Match with existing object (should return 304 Not Modified)
  236. t.Run("IfNoneMatch_ObjectExists_ShouldReturn304", func(t *testing.T) {
  237. getter := createMockEntryGetter(existingObject)
  238. req := createTestGetRequest(bucket, object)
  239. req.Header.Set(s3_constants.IfNoneMatch, "\"read123\"")
  240. s3a := NewS3ApiServerForTest()
  241. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  242. if errCode.ErrorCode != s3err.ErrNotModified {
  243. t.Errorf("Expected ErrNotModified when If-None-Match matches, got %v", errCode)
  244. }
  245. })
  246. // Test If-None-Match=* with existing object (should return 304 Not Modified)
  247. t.Run("IfNoneMatchAsterisk_ObjectExists_ShouldReturn304", func(t *testing.T) {
  248. getter := createMockEntryGetter(existingObject)
  249. req := createTestGetRequest(bucket, object)
  250. req.Header.Set(s3_constants.IfNoneMatch, "*")
  251. s3a := NewS3ApiServerForTest()
  252. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  253. if errCode.ErrorCode != s3err.ErrNotModified {
  254. t.Errorf("Expected ErrNotModified when If-None-Match=* with existing object, got %v", errCode)
  255. }
  256. })
  257. // Test If-None-Match with non-matching ETag (should succeed)
  258. t.Run("IfNoneMatch_NonMatchingETag_ShouldSucceed", func(t *testing.T) {
  259. getter := createMockEntryGetter(existingObject)
  260. req := createTestGetRequest(bucket, object)
  261. req.Header.Set(s3_constants.IfNoneMatch, "\"different-etag\"")
  262. s3a := NewS3ApiServerForTest()
  263. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  264. if errCode.ErrorCode != s3err.ErrNone {
  265. t.Errorf("Expected ErrNone when If-None-Match doesn't match, got %v", errCode)
  266. }
  267. })
  268. // Test If-Match with matching ETag (should succeed)
  269. t.Run("IfMatch_MatchingETag_ShouldSucceed", func(t *testing.T) {
  270. getter := createMockEntryGetter(existingObject)
  271. req := createTestGetRequest(bucket, object)
  272. req.Header.Set(s3_constants.IfMatch, "\"read123\"")
  273. s3a := NewS3ApiServerForTest()
  274. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  275. if errCode.ErrorCode != s3err.ErrNone {
  276. t.Errorf("Expected ErrNone when If-Match matches, got %v", errCode)
  277. }
  278. })
  279. // Test If-Match with non-matching ETag (should return 412 Precondition Failed)
  280. t.Run("IfMatch_NonMatchingETag_ShouldReturn412", func(t *testing.T) {
  281. getter := createMockEntryGetter(existingObject)
  282. req := createTestGetRequest(bucket, object)
  283. req.Header.Set(s3_constants.IfMatch, "\"different-etag\"")
  284. s3a := NewS3ApiServerForTest()
  285. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  286. if errCode.ErrorCode != s3err.ErrPreconditionFailed {
  287. t.Errorf("Expected ErrPreconditionFailed when If-Match doesn't match, got %v", errCode)
  288. }
  289. })
  290. // Test If-Match=* with existing object (should succeed)
  291. t.Run("IfMatchAsterisk_ObjectExists_ShouldSucceed", func(t *testing.T) {
  292. getter := createMockEntryGetter(existingObject)
  293. req := createTestGetRequest(bucket, object)
  294. req.Header.Set(s3_constants.IfMatch, "*")
  295. s3a := NewS3ApiServerForTest()
  296. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  297. if errCode.ErrorCode != s3err.ErrNone {
  298. t.Errorf("Expected ErrNone when If-Match=* with existing object, got %v", errCode)
  299. }
  300. })
  301. // Test If-Modified-Since (object modified after date - should succeed)
  302. t.Run("IfModifiedSince_ObjectModifiedAfter_ShouldSucceed", func(t *testing.T) {
  303. getter := createMockEntryGetter(existingObject)
  304. req := createTestGetRequest(bucket, object)
  305. req.Header.Set(s3_constants.IfModifiedSince, "Sat, 14 Jun 2024 12:00:00 GMT") // Before object mtime
  306. s3a := NewS3ApiServerForTest()
  307. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  308. if errCode.ErrorCode != s3err.ErrNone {
  309. t.Errorf("Expected ErrNone when object modified after If-Modified-Since date, got %v", errCode)
  310. }
  311. })
  312. // Test If-Modified-Since (object not modified since date - should return 304)
  313. t.Run("IfModifiedSince_ObjectNotModified_ShouldReturn304", func(t *testing.T) {
  314. getter := createMockEntryGetter(existingObject)
  315. req := createTestGetRequest(bucket, object)
  316. req.Header.Set(s3_constants.IfModifiedSince, "Sun, 16 Jun 2024 12:00:00 GMT") // After object mtime
  317. s3a := NewS3ApiServerForTest()
  318. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  319. if errCode.ErrorCode != s3err.ErrNotModified {
  320. t.Errorf("Expected ErrNotModified when object not modified since If-Modified-Since date, got %v", errCode)
  321. }
  322. })
  323. // Test If-Unmodified-Since (object not modified since date - should succeed)
  324. t.Run("IfUnmodifiedSince_ObjectNotModified_ShouldSucceed", func(t *testing.T) {
  325. getter := createMockEntryGetter(existingObject)
  326. req := createTestGetRequest(bucket, object)
  327. req.Header.Set(s3_constants.IfUnmodifiedSince, "Sun, 16 Jun 2024 12:00:00 GMT") // After object mtime
  328. s3a := NewS3ApiServerForTest()
  329. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  330. if errCode.ErrorCode != s3err.ErrNone {
  331. t.Errorf("Expected ErrNone when object not modified since If-Unmodified-Since date, got %v", errCode)
  332. }
  333. })
  334. // Test If-Unmodified-Since (object modified since date - should return 412)
  335. t.Run("IfUnmodifiedSince_ObjectModified_ShouldReturn412", func(t *testing.T) {
  336. getter := createMockEntryGetter(existingObject)
  337. req := createTestGetRequest(bucket, object)
  338. req.Header.Set(s3_constants.IfUnmodifiedSince, "Fri, 14 Jun 2024 12:00:00 GMT") // Before object mtime
  339. s3a := NewS3ApiServerForTest()
  340. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  341. if errCode.ErrorCode != s3err.ErrPreconditionFailed {
  342. t.Errorf("Expected ErrPreconditionFailed when object modified since If-Unmodified-Since date, got %v", errCode)
  343. }
  344. })
  345. })
  346. // Test conditional reads with non-existent object
  347. t.Run("ConditionalReads_ObjectNotExists", func(t *testing.T) {
  348. // Test If-None-Match with non-existent object (should succeed)
  349. t.Run("IfNoneMatch_ObjectNotExists_ShouldSucceed", func(t *testing.T) {
  350. getter := createMockEntryGetter(nil) // No object
  351. req := createTestGetRequest(bucket, object)
  352. req.Header.Set(s3_constants.IfNoneMatch, "\"any-etag\"")
  353. s3a := NewS3ApiServerForTest()
  354. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  355. if errCode.ErrorCode != s3err.ErrNone {
  356. t.Errorf("Expected ErrNone when object doesn't exist with If-None-Match, got %v", errCode)
  357. }
  358. })
  359. // Test If-Match with non-existent object (should return 412)
  360. t.Run("IfMatch_ObjectNotExists_ShouldReturn412", func(t *testing.T) {
  361. getter := createMockEntryGetter(nil) // No object
  362. req := createTestGetRequest(bucket, object)
  363. req.Header.Set(s3_constants.IfMatch, "\"any-etag\"")
  364. s3a := NewS3ApiServerForTest()
  365. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  366. if errCode.ErrorCode != s3err.ErrPreconditionFailed {
  367. t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match, got %v", errCode)
  368. }
  369. })
  370. // Test If-Modified-Since with non-existent object (should succeed)
  371. t.Run("IfModifiedSince_ObjectNotExists_ShouldSucceed", func(t *testing.T) {
  372. getter := createMockEntryGetter(nil) // No object
  373. req := createTestGetRequest(bucket, object)
  374. req.Header.Set(s3_constants.IfModifiedSince, "Sat, 15 Jun 2024 12:00:00 GMT")
  375. s3a := NewS3ApiServerForTest()
  376. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  377. if errCode.ErrorCode != s3err.ErrNone {
  378. t.Errorf("Expected ErrNone when object doesn't exist with If-Modified-Since, got %v", errCode)
  379. }
  380. })
  381. // Test If-Unmodified-Since with non-existent object (should return 412)
  382. t.Run("IfUnmodifiedSince_ObjectNotExists_ShouldReturn412", func(t *testing.T) {
  383. getter := createMockEntryGetter(nil) // No object
  384. req := createTestGetRequest(bucket, object)
  385. req.Header.Set(s3_constants.IfUnmodifiedSince, "Sat, 15 Jun 2024 12:00:00 GMT")
  386. s3a := NewS3ApiServerForTest()
  387. errCode := s3a.checkConditionalHeadersForReadsWithGetter(getter, req, bucket, object)
  388. if errCode.ErrorCode != s3err.ErrPreconditionFailed {
  389. t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Unmodified-Since, got %v", errCode)
  390. }
  391. })
  392. })
  393. }
  394. // Helper function to create a GET request for testing
  395. func createTestGetRequest(bucket, object string) *http.Request {
  396. return &http.Request{
  397. Method: "GET",
  398. Header: make(http.Header),
  399. URL: &url.URL{
  400. Path: fmt.Sprintf("/%s%s", bucket, object),
  401. },
  402. }
  403. }
  404. // TestConditionalHeadersWithNonExistentObjects tests the original scenarios (object doesn't exist)
  405. func TestConditionalHeadersWithNonExistentObjects(t *testing.T) {
  406. s3a := NewS3ApiServerForTest()
  407. if s3a == nil {
  408. t.Skip("S3ApiServer not available for testing")
  409. }
  410. bucket := "test-bucket"
  411. object := "/test-object"
  412. // Test If-None-Match header when object doesn't exist
  413. t.Run("IfNoneMatch_ObjectDoesNotExist", func(t *testing.T) {
  414. // Test case 1: If-None-Match=* when object doesn't exist (should return ErrNone)
  415. t.Run("Asterisk_ShouldSucceed", func(t *testing.T) {
  416. getter := createMockEntryGetter(nil) // No object exists
  417. req := createTestPutRequest(bucket, object, "test content")
  418. req.Header.Set(s3_constants.IfNoneMatch, "*")
  419. s3a := NewS3ApiServerForTest()
  420. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  421. if errCode != s3err.ErrNone {
  422. t.Errorf("Expected ErrNone when object doesn't exist, got %v", errCode)
  423. }
  424. })
  425. // Test case 2: If-None-Match with specific ETag when object doesn't exist
  426. t.Run("SpecificETag_ShouldSucceed", func(t *testing.T) {
  427. getter := createMockEntryGetter(nil) // No object exists
  428. req := createTestPutRequest(bucket, object, "test content")
  429. req.Header.Set(s3_constants.IfNoneMatch, "\"some-etag\"")
  430. s3a := NewS3ApiServerForTest()
  431. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  432. if errCode != s3err.ErrNone {
  433. t.Errorf("Expected ErrNone when object doesn't exist, got %v", errCode)
  434. }
  435. })
  436. })
  437. // Test If-Match header when object doesn't exist
  438. t.Run("IfMatch_ObjectDoesNotExist", func(t *testing.T) {
  439. // Test case 1: If-Match with specific ETag when object doesn't exist (should fail - critical bug fix)
  440. t.Run("SpecificETag_ShouldFail", func(t *testing.T) {
  441. getter := createMockEntryGetter(nil) // No object exists
  442. req := createTestPutRequest(bucket, object, "test content")
  443. req.Header.Set(s3_constants.IfMatch, "\"some-etag\"")
  444. s3a := NewS3ApiServerForTest()
  445. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  446. if errCode != s3err.ErrPreconditionFailed {
  447. t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match header, got %v", errCode)
  448. }
  449. })
  450. // Test case 2: If-Match with wildcard * when object doesn't exist (should fail)
  451. t.Run("Wildcard_ShouldFail", func(t *testing.T) {
  452. getter := createMockEntryGetter(nil) // No object exists
  453. req := createTestPutRequest(bucket, object, "test content")
  454. req.Header.Set(s3_constants.IfMatch, "*")
  455. s3a := NewS3ApiServerForTest()
  456. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  457. if errCode != s3err.ErrPreconditionFailed {
  458. t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match=*, got %v", errCode)
  459. }
  460. })
  461. })
  462. // Test date format validation (works regardless of object existence)
  463. t.Run("DateFormatValidation", func(t *testing.T) {
  464. // Test case 1: Valid If-Modified-Since date format
  465. t.Run("IfModifiedSince_ValidFormat", func(t *testing.T) {
  466. getter := createMockEntryGetter(nil) // No object exists
  467. req := createTestPutRequest(bucket, object, "test content")
  468. req.Header.Set(s3_constants.IfModifiedSince, time.Now().Format(time.RFC1123))
  469. s3a := NewS3ApiServerForTest()
  470. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  471. if errCode != s3err.ErrNone {
  472. t.Errorf("Expected ErrNone with valid date format, got %v", errCode)
  473. }
  474. })
  475. // Test case 2: Invalid If-Modified-Since date format
  476. t.Run("IfModifiedSince_InvalidFormat", func(t *testing.T) {
  477. getter := createMockEntryGetter(nil) // No object exists
  478. req := createTestPutRequest(bucket, object, "test content")
  479. req.Header.Set(s3_constants.IfModifiedSince, "invalid-date")
  480. s3a := NewS3ApiServerForTest()
  481. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  482. if errCode != s3err.ErrInvalidRequest {
  483. t.Errorf("Expected ErrInvalidRequest for invalid date format, got %v", errCode)
  484. }
  485. })
  486. // Test case 3: Invalid If-Unmodified-Since date format
  487. t.Run("IfUnmodifiedSince_InvalidFormat", func(t *testing.T) {
  488. getter := createMockEntryGetter(nil) // No object exists
  489. req := createTestPutRequest(bucket, object, "test content")
  490. req.Header.Set(s3_constants.IfUnmodifiedSince, "invalid-date")
  491. s3a := NewS3ApiServerForTest()
  492. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  493. if errCode != s3err.ErrInvalidRequest {
  494. t.Errorf("Expected ErrInvalidRequest for invalid date format, got %v", errCode)
  495. }
  496. })
  497. })
  498. // Test no conditional headers
  499. t.Run("NoConditionalHeaders", func(t *testing.T) {
  500. getter := createMockEntryGetter(nil) // No object exists
  501. req := createTestPutRequest(bucket, object, "test content")
  502. // Don't set any conditional headers
  503. s3a := NewS3ApiServerForTest()
  504. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  505. if errCode != s3err.ErrNone {
  506. t.Errorf("Expected ErrNone when no conditional headers, got %v", errCode)
  507. }
  508. })
  509. }
  510. // TestETagMatching tests the etagMatches helper function
  511. func TestETagMatching(t *testing.T) {
  512. s3a := NewS3ApiServerForTest()
  513. if s3a == nil {
  514. t.Skip("S3ApiServer not available for testing")
  515. }
  516. testCases := []struct {
  517. name string
  518. headerValue string
  519. objectETag string
  520. expected bool
  521. }{
  522. {
  523. name: "ExactMatch",
  524. headerValue: "\"abc123\"",
  525. objectETag: "abc123",
  526. expected: true,
  527. },
  528. {
  529. name: "ExactMatchWithQuotes",
  530. headerValue: "\"abc123\"",
  531. objectETag: "\"abc123\"",
  532. expected: true,
  533. },
  534. {
  535. name: "NoMatch",
  536. headerValue: "\"abc123\"",
  537. objectETag: "def456",
  538. expected: false,
  539. },
  540. {
  541. name: "MultipleETags_FirstMatch",
  542. headerValue: "\"abc123\", \"def456\"",
  543. objectETag: "abc123",
  544. expected: true,
  545. },
  546. {
  547. name: "MultipleETags_SecondMatch",
  548. headerValue: "\"abc123\", \"def456\"",
  549. objectETag: "def456",
  550. expected: true,
  551. },
  552. {
  553. name: "MultipleETags_NoMatch",
  554. headerValue: "\"abc123\", \"def456\"",
  555. objectETag: "ghi789",
  556. expected: false,
  557. },
  558. {
  559. name: "WithSpaces",
  560. headerValue: " \"abc123\" , \"def456\" ",
  561. objectETag: "def456",
  562. expected: true,
  563. },
  564. }
  565. for _, tc := range testCases {
  566. t.Run(tc.name, func(t *testing.T) {
  567. result := s3a.etagMatches(tc.headerValue, tc.objectETag)
  568. if result != tc.expected {
  569. t.Errorf("Expected %v, got %v for headerValue='%s', objectETag='%s'",
  570. tc.expected, result, tc.headerValue, tc.objectETag)
  571. }
  572. })
  573. }
  574. }
  575. // TestConditionalHeadersIntegration tests conditional headers with full integration
  576. func TestConditionalHeadersIntegration(t *testing.T) {
  577. // This would be a full integration test that requires a running SeaweedFS instance
  578. t.Skip("Integration test - requires running SeaweedFS instance")
  579. }
  580. // createTestPutRequest creates a test HTTP PUT request
  581. func createTestPutRequest(bucket, object, content string) *http.Request {
  582. req, _ := http.NewRequest("PUT", "/"+bucket+object, bytes.NewReader([]byte(content)))
  583. req.Header.Set("Content-Type", "application/octet-stream")
  584. // Set up mux vars to simulate the bucket and object extraction
  585. // In real tests, this would be handled by the gorilla mux router
  586. return req
  587. }
  588. // NewS3ApiServerForTest creates a minimal S3ApiServer for testing
  589. // Note: This is a simplified version for unit testing conditional logic
  590. func NewS3ApiServerForTest() *S3ApiServer {
  591. // In a real test environment, this would set up a proper S3ApiServer
  592. // with filer connection, etc. For unit testing conditional header logic,
  593. // we create a minimal instance
  594. return &S3ApiServer{
  595. option: &S3ApiServerOption{
  596. BucketsPath: "/buckets",
  597. },
  598. }
  599. }
  600. // MockEntryGetter implements the simplified EntryGetter interface for testing
  601. // Only mocks the data access dependency - tests use production getObjectETag and etagMatches
  602. type MockEntryGetter struct {
  603. mockEntry *filer_pb.Entry
  604. }
  605. // Implement only the simplified EntryGetter interface
  606. func (m *MockEntryGetter) getEntry(parentDirectoryPath, entryName string) (*filer_pb.Entry, error) {
  607. if m.mockEntry != nil {
  608. return m.mockEntry, nil
  609. }
  610. return nil, filer_pb.ErrNotFound
  611. }
  612. // createMockEntryGetter creates a mock EntryGetter for testing
  613. func createMockEntryGetter(mockEntry *filer_pb.Entry) *MockEntryGetter {
  614. return &MockEntryGetter{
  615. mockEntry: mockEntry,
  616. }
  617. }
  618. // TestConditionalHeadersMultipartUpload tests conditional headers with multipart uploads
  619. // This verifies AWS S3 compatibility where conditional headers only apply to CompleteMultipartUpload
  620. func TestConditionalHeadersMultipartUpload(t *testing.T) {
  621. bucket := "test-bucket"
  622. object := "/test-multipart-object"
  623. // Mock existing object to test conditional headers against
  624. existingObject := &filer_pb.Entry{
  625. Name: "test-multipart-object",
  626. Extended: map[string][]byte{
  627. s3_constants.ExtETagKey: []byte("\"existing123\""),
  628. },
  629. Attributes: &filer_pb.FuseAttributes{
  630. Mtime: time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC).Unix(),
  631. FileSize: 2048,
  632. },
  633. Chunks: []*filer_pb.FileChunk{
  634. {
  635. FileId: "existing-file-id",
  636. Offset: 0,
  637. Size: 2048,
  638. },
  639. },
  640. }
  641. // Test CompleteMultipartUpload with If-None-Match: * (should fail when object exists)
  642. t.Run("CompleteMultipartUpload_IfNoneMatchAsterisk_ObjectExists_ShouldFail", func(t *testing.T) {
  643. getter := createMockEntryGetter(existingObject)
  644. // Create a mock CompleteMultipartUpload request with If-None-Match: *
  645. req := &http.Request{
  646. Method: "POST",
  647. Header: make(http.Header),
  648. URL: &url.URL{
  649. RawQuery: "uploadId=test-upload-id",
  650. },
  651. }
  652. req.Header.Set(s3_constants.IfNoneMatch, "*")
  653. s3a := NewS3ApiServerForTest()
  654. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  655. if errCode != s3err.ErrPreconditionFailed {
  656. t.Errorf("Expected ErrPreconditionFailed when object exists with If-None-Match=*, got %v", errCode)
  657. }
  658. })
  659. // Test CompleteMultipartUpload with If-None-Match: * (should succeed when object doesn't exist)
  660. t.Run("CompleteMultipartUpload_IfNoneMatchAsterisk_ObjectNotExists_ShouldSucceed", func(t *testing.T) {
  661. getter := createMockEntryGetter(nil) // No existing object
  662. req := &http.Request{
  663. Method: "POST",
  664. Header: make(http.Header),
  665. URL: &url.URL{
  666. RawQuery: "uploadId=test-upload-id",
  667. },
  668. }
  669. req.Header.Set(s3_constants.IfNoneMatch, "*")
  670. s3a := NewS3ApiServerForTest()
  671. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  672. if errCode != s3err.ErrNone {
  673. t.Errorf("Expected ErrNone when object doesn't exist with If-None-Match=*, got %v", errCode)
  674. }
  675. })
  676. // Test CompleteMultipartUpload with If-Match (should succeed when ETag matches)
  677. t.Run("CompleteMultipartUpload_IfMatch_ETagMatches_ShouldSucceed", func(t *testing.T) {
  678. getter := createMockEntryGetter(existingObject)
  679. req := &http.Request{
  680. Method: "POST",
  681. Header: make(http.Header),
  682. URL: &url.URL{
  683. RawQuery: "uploadId=test-upload-id",
  684. },
  685. }
  686. req.Header.Set(s3_constants.IfMatch, "\"existing123\"")
  687. s3a := NewS3ApiServerForTest()
  688. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  689. if errCode != s3err.ErrNone {
  690. t.Errorf("Expected ErrNone when ETag matches, got %v", errCode)
  691. }
  692. })
  693. // Test CompleteMultipartUpload with If-Match (should fail when object doesn't exist)
  694. t.Run("CompleteMultipartUpload_IfMatch_ObjectNotExists_ShouldFail", func(t *testing.T) {
  695. getter := createMockEntryGetter(nil) // No existing object
  696. req := &http.Request{
  697. Method: "POST",
  698. Header: make(http.Header),
  699. URL: &url.URL{
  700. RawQuery: "uploadId=test-upload-id",
  701. },
  702. }
  703. req.Header.Set(s3_constants.IfMatch, "\"any-etag\"")
  704. s3a := NewS3ApiServerForTest()
  705. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  706. if errCode != s3err.ErrPreconditionFailed {
  707. t.Errorf("Expected ErrPreconditionFailed when object doesn't exist with If-Match, got %v", errCode)
  708. }
  709. })
  710. // Test CompleteMultipartUpload with If-Match wildcard (should succeed when object exists)
  711. t.Run("CompleteMultipartUpload_IfMatchWildcard_ObjectExists_ShouldSucceed", func(t *testing.T) {
  712. getter := createMockEntryGetter(existingObject)
  713. req := &http.Request{
  714. Method: "POST",
  715. Header: make(http.Header),
  716. URL: &url.URL{
  717. RawQuery: "uploadId=test-upload-id",
  718. },
  719. }
  720. req.Header.Set(s3_constants.IfMatch, "*")
  721. s3a := NewS3ApiServerForTest()
  722. errCode := s3a.checkConditionalHeadersWithGetter(getter, req, bucket, object)
  723. if errCode != s3err.ErrNone {
  724. t.Errorf("Expected ErrNone when object exists with If-Match=*, got %v", errCode)
  725. }
  726. })
  727. }