diff --git a/backend/azure/azure.go b/backend/azure/azure.go index 591dd04..c12e2ba 100644 --- a/backend/azure/azure.go +++ b/backend/azure/azure.go @@ -417,6 +417,19 @@ func (az *Azure) GetObject(ctx context.Context, input *s3.GetObjectInput) (*s3.G return nil, azureErrToS3Err(err) } + if resp.ETag != nil && resp.LastModified != nil { + err = backend.EvaluatePreconditions(convertAzureEtag(resp.ETag), *resp.LastModified, + backend.PreConditions{ + IfMatch: input.IfMatch, + IfNoneMatch: input.IfNoneMatch, + IfModSince: input.IfModifiedSince, + IfUnmodeSince: input.IfUnmodifiedSince, + }) + if err != nil { + return nil, err + } + } + var opts *azblob.DownloadStreamOptions if *input.Range != "" { offset, count, isValid, err := backend.ParseObjectRange(*resp.ContentLength, *input.Range) @@ -478,6 +491,19 @@ func (az *Azure) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3 return nil, azureErrToS3Err(err) } + if res.ETag != nil && res.LastModified != nil { + err = backend.EvaluatePreconditions(convertAzureEtag(res.ETag), *res.LastModified, + backend.PreConditions{ + IfMatch: input.IfMatch, + IfNoneMatch: input.IfNoneMatch, + IfModSince: input.IfModifiedSince, + IfUnmodeSince: input.IfUnmodifiedSince, + }) + if err != nil { + return nil, err + } + } + partsCount := int32(len(res.UncommittedBlocks)) for _, block := range res.UncommittedBlocks { @@ -508,6 +534,20 @@ func (az *Azure) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3 if err != nil { return nil, azureErrToS3Err(err) } + + if resp.ETag != nil && resp.LastModified != nil { + err = backend.EvaluatePreconditions(convertAzureEtag(resp.ETag), *resp.LastModified, + backend.PreConditions{ + IfMatch: input.IfMatch, + IfNoneMatch: input.IfNoneMatch, + IfModSince: input.IfModifiedSince, + IfUnmodeSince: input.IfUnmodifiedSince, + }) + if err != nil { + return nil, err + } + } + var size int64 if resp.ContentLength != nil { size = *resp.ContentLength diff --git a/backend/common.go b/backend/common.go index 8982f92..fd3a68c 100644 --- a/backend/common.go +++ b/backend/common.go @@ -88,6 +88,8 @@ func TrimEtag(etag *string) *string { var ( errInvalidRange = s3err.GetAPIError(s3err.ErrInvalidRange) errInvalidCopySourceRange = s3err.GetAPIError(s3err.ErrInvalidCopySourceRange) + errPreconditionFailed = s3err.GetAPIError(s3err.ErrPreconditionFailed) + errNotModified = s3err.GetAPIError(s3err.ErrNotModified) ) // ParseObjectRange parses input range header and returns startoffset, length, isValid @@ -422,3 +424,111 @@ func GenerateEtag(h hash.Hash) string { func AreEtagsSame(e1, e2 string) bool { return strings.Trim(e1, `"`) == strings.Trim(e2, `"`) } + +func getBoolPtr(b bool) *bool { + return &b +} + +type PreConditions struct { + IfMatch *string + IfNoneMatch *string + IfModSince *time.Time + IfUnmodeSince *time.Time +} + +// EvaluatePreconditions takes the object ETag, the last modified time and +// evaluates the read preconditions: +// - if-match, +// - if-none-match +// - if-modified-since +// - if-unmodified-since +// if-match and if-none-match are ETag comparisions +// if-modified-since and if-unmodified-since are last modifed time comparisons +func EvaluatePreconditions(etag string, modTime time.Time, preconditions PreConditions) error { + if preconditions.IfMatch == nil && preconditions.IfNoneMatch == nil && preconditions.IfModSince == nil && preconditions.IfUnmodeSince == nil { + return nil + } + + // convert all conditions to *bool to evaluate the conditions + var ifMatch, ifNoneMatch, ifModSince, ifUnmodeSince *bool + if preconditions.IfMatch != nil { + ifMatch = getBoolPtr(*preconditions.IfMatch == etag) + } + if preconditions.IfNoneMatch != nil { + ifNoneMatch = getBoolPtr(*preconditions.IfNoneMatch != etag) + } + if preconditions.IfModSince != nil { + ifModSince = getBoolPtr(preconditions.IfModSince.UTC().Before(modTime.UTC())) + } + if preconditions.IfUnmodeSince != nil { + ifUnmodeSince = getBoolPtr(preconditions.IfUnmodeSince.UTC().After(modTime.UTC())) + } + + if ifMatch != nil { + // if `if-match` doesn't matches, return PreconditionFailed + if !*ifMatch { + return errPreconditionFailed + } + + // if-match matches + if *ifMatch { + if ifNoneMatch != nil { + // if `if-none-match` doesn't match return NotModified + if !*ifNoneMatch { + return errNotModified + } + + // if both `if-match` and `if-none-match` match, return no error + return nil + } + + // if `if-match` matches but `if-modified-since` is false return NotModified + if ifModSince != nil && !*ifModSince { + return errNotModified + } + + // ignore `if-unmodified-since` as `if-match` is true + return nil + } + } + + if ifNoneMatch != nil { + if *ifNoneMatch { + // if `if-none-match` is true, but `if-unmodified-since` is false + // return PreconditionFailed + if ifUnmodeSince != nil && !*ifUnmodeSince { + return errPreconditionFailed + } + + // ignore `if-modified-since` as `if-none-match` is true + return nil + } else { + // if `if-none-match` is false and `if-unmodified-since` is false + // return PreconditionFailed + if ifUnmodeSince != nil && !*ifUnmodeSince { + return errPreconditionFailed + } + + // in all other cases when `if-none-match` is false return NotModified + return errNotModified + } + } + + if ifModSince != nil && !*ifModSince { + // if both `if-modified-since` and `if-unmodified-since` are false + // return PreconditionFailed + if ifUnmodeSince != nil && !*ifUnmodeSince { + return errPreconditionFailed + } + + // if only `if-modified-since` is false, return NotModified + return errNotModified + } + + // if `if-unmodified-since` is false return PreconditionFailed + if ifUnmodeSince != nil && !*ifUnmodeSince { + return errPreconditionFailed + } + + return nil +} diff --git a/backend/posix/posix.go b/backend/posix/posix.go index 0659c6d..5a0b753 100644 --- a/backend/posix/posix.go +++ b/backend/posix/posix.go @@ -3484,6 +3484,23 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput) (*s3.GetO } } + b, err := p.meta.RetrieveAttribute(nil, bucket, object, etagkey) + etag := string(b) + if err != nil { + etag = "" + } + + // evaluate preconditions + err = backend.EvaluatePreconditions(etag, fid.ModTime(), backend.PreConditions{ + IfMatch: input.IfMatch, + IfNoneMatch: input.IfNoneMatch, + IfModSince: input.IfModifiedSince, + IfUnmodeSince: input.IfUnmodifiedSince, + }) + if err != nil { + return nil, err + } + if fid.IsDir() { _, _, _, err := backend.ParseObjectRange(0, *input.Range) if err != nil { @@ -3493,11 +3510,6 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput) (*s3.GetO userMetaData := make(map[string]string) objMeta := p.loadObjectMetaData(nil, bucket, object, &fid, userMetaData) - b, err := p.meta.RetrieveAttribute(nil, bucket, object, etagkey) - etag := string(b) - if err != nil { - etag = "" - } var tagCount *int32 tags, err := p.getAttrTags(bucket, object) @@ -3579,12 +3591,6 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput) (*s3.GetO objMeta := p.loadObjectMetaData(f, bucket, object, &fi, userMetaData) - b, err := p.meta.RetrieveAttribute(f, bucket, object, etagkey) - etag := string(b) - if err != nil { - etag = "" - } - var tagCount *int32 tags, err := p.getAttrTags(bucket, object) if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrBucketTaggingNotFound)) { @@ -3689,17 +3695,30 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. return nil, err } + // retreive the part etag + b, err := p.meta.RetrieveAttribute(nil, bucket, partPath, etagkey) + etag := string(b) + if err != nil { + etag = "" + } + + // evaluate preconditions + err = backend.EvaluatePreconditions(etag, part.ModTime(), backend.PreConditions{ + IfMatch: input.IfMatch, + IfNoneMatch: input.IfNoneMatch, + IfModSince: input.IfModifiedSince, + IfUnmodeSince: input.IfUnmodifiedSince, + }) + if err != nil { + return nil, err + } + var contentRange string if isValid { contentRange = fmt.Sprintf("bytes %v-%v/%v", startOffset, startOffset+length-1, size) } - b, err := p.meta.RetrieveAttribute(nil, bucket, partPath, etagkey) - etag := string(b) - if err != nil { - etag = "" - } partsCount := int32(len(ents)) return &s3.HeadObjectOutput{ @@ -3799,6 +3818,17 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3. etag = "" } + // evaluate preconditions + err = backend.EvaluatePreconditions(etag, fi.ModTime(), backend.PreConditions{ + IfMatch: input.IfMatch, + IfNoneMatch: input.IfNoneMatch, + IfModSince: input.IfModifiedSince, + IfUnmodeSince: input.IfUnmodifiedSince, + }) + if err != nil { + return nil, err + } + size := fi.Size() if fi.IsDir() { size = 0 diff --git a/s3api/controllers/object-get.go b/s3api/controllers/object-get.go index e87f29d..6ed2d7e 100644 --- a/s3api/controllers/object-get.go +++ b/s3api/controllers/object-get.go @@ -450,12 +450,18 @@ func (c S3ApiController) GetObject(ctx *fiber.Ctx) (*Response, error) { }, s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode") } + conditionalHeaders := utils.ParsePreconditionHeaders(ctx) + res, err := c.be.GetObject(ctx.Context(), &s3.GetObjectInput{ - Bucket: &bucket, - Key: &key, - Range: &acceptRange, - VersionId: &versionId, - ChecksumMode: checksumMode, + Bucket: &bucket, + Key: &key, + Range: &acceptRange, + IfMatch: conditionalHeaders.IfMatch, + IfNoneMatch: conditionalHeaders.IfNoneMatch, + IfModifiedSince: conditionalHeaders.IfModSince, + IfUnmodifiedSince: conditionalHeaders.IfUnmodeSince, + VersionId: &versionId, + ChecksumMode: checksumMode, }) if err != nil { var headers map[string]*string diff --git a/s3api/controllers/object-head.go b/s3api/controllers/object-head.go index 1361603..80e648a 100644 --- a/s3api/controllers/object-head.go +++ b/s3api/controllers/object-head.go @@ -90,14 +90,20 @@ func (c S3ApiController) HeadObject(ctx *fiber.Ctx) (*Response, error) { }, s3err.GetInvalidChecksumHeaderErr("x-amz-checksum-mode") } + conditionalHeaders := utils.ParsePreconditionHeaders(ctx) + res, err := c.be.HeadObject(ctx.Context(), &s3.HeadObjectInput{ - Bucket: &bucket, - Key: &key, - PartNumber: partNumber, - VersionId: &versionId, - ChecksumMode: checksumMode, - Range: &objRange, + Bucket: &bucket, + Key: &key, + PartNumber: partNumber, + VersionId: &versionId, + ChecksumMode: checksumMode, + Range: &objRange, + IfMatch: conditionalHeaders.IfMatch, + IfNoneMatch: conditionalHeaders.IfNoneMatch, + IfModifiedSince: conditionalHeaders.IfModSince, + IfUnmodifiedSince: conditionalHeaders.IfUnmodeSince, }) if err != nil { var headers map[string]*string diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go index e327810..44aa23c 100644 --- a/s3api/utils/utils.go +++ b/s3api/utils/utils.go @@ -642,6 +642,59 @@ func ParseCreateMpChecksumHeaders(ctx *fiber.Ctx) (types.ChecksumAlgorithm, type return algo, chType, nil } +// ConditionalHeaders holds the conditional header values +type ConditionalHeaders struct { + IfMatch *string + IfNoneMatch *string + IfModSince *time.Time + IfUnmodeSince *time.Time +} + +// ParsePreconditionHeaders parses the precondition headers: +// - If-Match +// - If-None-Match +// - If-Modified-Since +// - If-Unmodified-Since +func ParsePreconditionHeaders(ctx *fiber.Ctx) ConditionalHeaders { + ifMatch, ifNoneMatch := ParsePreconditionMatchHeaders(ctx) + ifModSince, ifUnmodeSince := ParsePreconditionDateHeaders(ctx) + + return ConditionalHeaders{ + IfMatch: ifMatch, + IfNoneMatch: ifNoneMatch, + IfModSince: ifModSince, + IfUnmodeSince: ifUnmodeSince, + } +} + +// ParsePreconditionMatchHeaders extracts "If-Match" and "If-None-Match" headers from fiber Ctx +func ParsePreconditionMatchHeaders(ctx *fiber.Ctx) (*string, *string) { + return GetStringPtr(ctx.Get("If-Match")), GetStringPtr(ctx.Get("If-None-Match")) +} + +// ParsePreconditionDateHeaders parses the "If-Modified-Since" and "If-Unmodified-Since" +// headers from fiber context to *time.Time +func ParsePreconditionDateHeaders(ctx *fiber.Ctx) (*time.Time, *time.Time) { + ifModSince := ctx.Get("If-Modified-Since") + ifUnmodSince := ctx.Get("If-Unmodified-Since") + + var ifModSinceParsed, ifUnmodSinceParsed *time.Time + + // the time format should be a valid RFC1123 + // if parsing fails, ignore the error and leave the value as nil + modParsed, err := time.Parse(time.RFC1123, ifModSince) + if err == nil { + ifModSinceParsed = &modParsed + } + + unmodParsed, err := time.Parse(time.RFC1123, ifUnmodSince) + if err == nil { + ifUnmodSinceParsed = &unmodParsed + } + + return ifModSinceParsed, ifUnmodSinceParsed +} + // TagLimit specifies the allowed tag count in a tag set type TagLimit int diff --git a/s3err/s3err.go b/s3err/s3err.go index 7b4d499..ed65968 100644 --- a/s3err/s3err.go +++ b/s3err/s3err.go @@ -177,6 +177,7 @@ const ( ErrCORSForbidden ErrMissingCORSOrigin ErrCORSIsNotEnabled + ErrNotModified // Non-AWS errors ErrExistingObjectIsDirectory @@ -786,6 +787,11 @@ var errorCodeResponse = map[ErrorCode]APIError{ Description: "CORSResponse: CORS is not enabled for this bucket.", HTTPStatusCode: http.StatusForbidden, }, + ErrNotModified: { + Code: "NotModified", + Description: "Not Modified", + HTTPStatusCode: http.StatusNotModified, + }, // non aws errors ErrExistingObjectIsDirectory: { diff --git a/tests/integration/group-tests.go b/tests/integration/group-tests.go index 65332c1..4fd3481 100644 --- a/tests/integration/group-tests.go +++ b/tests/integration/group-tests.go @@ -179,6 +179,7 @@ func TestHeadObject(s *S3Conf) { HeadObject_with_range(s) HeadObject_zero_len_with_range(s) HeadObject_dir_with_range(s) + HeadObject_conditional_reads(s) //TODO: remove the condition after implementing checksums in azure if !s.azureTests { HeadObject_not_enabled_checksum_mode(s) @@ -209,6 +210,7 @@ func TestGetObject(s *S3Conf) { GetObject_dir_with_range(s) GetObject_invalid_parent(s) GetObject_large_object(s) + GetObject_conditional_reads(s) //TODO: remove the condition after implementing checksums in azure if !s.azureTests { GetObject_checksums(s) @@ -1124,6 +1126,7 @@ func GetIntTests() IntTests { "HeadObject_with_range": HeadObject_with_range, "HeadObject_zero_len_with_range": HeadObject_zero_len_with_range, "HeadObject_dir_with_range": HeadObject_dir_with_range, + "HeadObject_conditional_reads": HeadObject_conditional_reads, "HeadObject_not_enabled_checksum_mode": HeadObject_not_enabled_checksum_mode, "HeadObject_checksums": HeadObject_checksums, "HeadObject_success": HeadObject_success, @@ -1142,6 +1145,7 @@ func GetIntTests() IntTests { "GetObject_dir_with_range": GetObject_dir_with_range, "GetObject_invalid_parent": GetObject_invalid_parent, "GetObject_large_object": GetObject_large_object, + "GetObject_conditional_reads": GetObject_conditional_reads, "GetObject_checksums": GetObject_checksums, "GetObject_success": GetObject_success, "GetObject_directory_success": GetObject_directory_success, diff --git a/tests/integration/tests.go b/tests/integration/tests.go index 1ba381a..c110fd0 100644 --- a/tests/integration/tests.go +++ b/tests/integration/tests.go @@ -3945,6 +3945,151 @@ func HeadObject_dir_with_range(s *S3Conf) error { return headObject_zero_len_with_range_helper(testName, "my-dir/", s) } +func HeadObject_conditional_reads(s *S3Conf) error { + testName := "HeadObject_conditional_reads" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + key := "my-obj" + obj, err := putObjectWithData(10, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &key, + }, s3client) + if err != nil { + return err + } + + errMod := getPtr("NotModified") + errCond := getPtr("PreconditionFailed") + + // sleep one second to get dates before and after + // the object creation + time.Sleep(time.Second * 1) + + before := time.Now().AddDate(0, 0, -3) + after := time.Now() + etag := obj.res.ETag + + for i, test := range []struct { + ifmatch *string + ifnonematch *string + ifmodifiedsince *time.Time + ifunmodifiedsince *time.Time + err *string + }{ + // all the cases when preconditions are either empty, true or false + {getPtr("invalid_etag"), getPtr("invalid_etag"), &before, &before, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), &before, &after, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), &before, nil, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), &after, &before, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), &after, &after, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), &after, nil, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), nil, &before, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), nil, &after, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), nil, nil, errCond}, + + {getPtr("invalid_etag"), etag, &before, &before, errCond}, + {getPtr("invalid_etag"), etag, &before, &after, errCond}, + {getPtr("invalid_etag"), etag, &before, nil, errCond}, + {getPtr("invalid_etag"), etag, &after, &before, errCond}, + {getPtr("invalid_etag"), etag, &after, &after, errCond}, + {getPtr("invalid_etag"), etag, &after, nil, errCond}, + {getPtr("invalid_etag"), etag, nil, &before, errCond}, + {getPtr("invalid_etag"), etag, nil, &after, errCond}, + {getPtr("invalid_etag"), etag, nil, nil, errCond}, + + {getPtr("invalid_etag"), nil, &before, &before, errCond}, + {getPtr("invalid_etag"), nil, &before, &after, errCond}, + {getPtr("invalid_etag"), nil, &before, nil, errCond}, + {getPtr("invalid_etag"), nil, &after, &before, errCond}, + {getPtr("invalid_etag"), nil, &after, &after, errCond}, + {getPtr("invalid_etag"), nil, &after, nil, errCond}, + {getPtr("invalid_etag"), nil, nil, &before, errCond}, + {getPtr("invalid_etag"), nil, nil, &after, errCond}, + {getPtr("invalid_etag"), nil, nil, nil, errCond}, + + {etag, getPtr("invalid_etag"), &before, &before, nil}, + {etag, getPtr("invalid_etag"), &before, &after, nil}, + {etag, getPtr("invalid_etag"), &before, nil, nil}, + {etag, getPtr("invalid_etag"), &after, &before, nil}, + {etag, getPtr("invalid_etag"), &after, &after, nil}, + {etag, getPtr("invalid_etag"), &after, nil, nil}, + {etag, getPtr("invalid_etag"), nil, &before, nil}, + {etag, getPtr("invalid_etag"), nil, &after, nil}, + {etag, getPtr("invalid_etag"), nil, nil, nil}, + + {etag, etag, &before, &before, errMod}, + {etag, etag, &before, &after, errMod}, + {etag, etag, &before, nil, errMod}, + {etag, etag, &after, &before, errMod}, + {etag, etag, &after, &after, errMod}, + {etag, etag, &after, nil, errMod}, + {etag, etag, nil, &before, errMod}, + {etag, etag, nil, &after, errMod}, + {etag, etag, nil, nil, errMod}, + + {etag, nil, &before, &before, nil}, + {etag, nil, &before, &after, nil}, + {etag, nil, &before, nil, nil}, + {etag, nil, &after, &before, errMod}, + {etag, nil, &after, &after, errMod}, + {etag, nil, &after, nil, errMod}, + {etag, nil, nil, &before, nil}, + {etag, nil, nil, &after, nil}, + {etag, nil, nil, nil, nil}, + + {nil, getPtr("invalid_etag"), &before, &before, errCond}, + {nil, getPtr("invalid_etag"), &before, &after, nil}, + {nil, getPtr("invalid_etag"), &before, nil, nil}, + {nil, getPtr("invalid_etag"), &after, &before, errCond}, + {nil, getPtr("invalid_etag"), &after, &after, nil}, + {nil, getPtr("invalid_etag"), &after, nil, nil}, + {nil, getPtr("invalid_etag"), nil, &before, errCond}, + {nil, getPtr("invalid_etag"), nil, &after, nil}, + {nil, getPtr("invalid_etag"), nil, nil, nil}, + + {nil, etag, &before, &before, errCond}, + {nil, etag, &before, &after, errMod}, + {nil, etag, &before, nil, errMod}, + {nil, etag, &after, &before, errCond}, + {nil, etag, &after, &after, errMod}, + {nil, etag, &after, nil, errMod}, + {nil, etag, nil, &before, errCond}, + {nil, etag, nil, &after, errMod}, + {nil, etag, nil, nil, errMod}, + + {nil, nil, &before, &before, errCond}, + {nil, nil, &before, &after, nil}, + {nil, nil, &before, nil, nil}, + {nil, nil, &after, &before, errCond}, + {nil, nil, &after, &after, errMod}, + {nil, nil, &after, nil, errMod}, + {nil, nil, nil, &before, errCond}, + {nil, nil, nil, &after, nil}, + {nil, nil, nil, nil, nil}, + } { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucket, + Key: &key, + IfMatch: test.ifmatch, + IfNoneMatch: test.ifnonematch, + IfModifiedSince: test.ifmodifiedsince, + IfUnmodifiedSince: test.ifunmodifiedsince, + }) + cancel() + if test.err == nil && err != nil { + return fmt.Errorf("test case %d failed: expected no error, but got %v", i, err) + } + if test.err != nil { + if err := checkSdkApiErr(err, *test.err); err != nil { + return fmt.Errorf("test case %d failed: %w", i, err) + } + } + } + + return nil + }) +} + func HeadObject_success(s *S3Conf) error { testName := "HeadObject_success" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { @@ -4642,6 +4787,155 @@ func GetObject_large_object(s *S3Conf) error { }) } +func GetObject_conditional_reads(s *S3Conf) error { + testName := "GetObject_conditional_reads" + return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error { + key := "my-obj" + obj, err := putObjectWithData(10, &s3.PutObjectInput{ + Bucket: &bucket, + Key: &key, + }, s3client) + if err != nil { + return err + } + + errMod := s3err.GetAPIError(s3err.ErrNotModified) + errCond := s3err.GetAPIError(s3err.ErrPreconditionFailed) + + // sleep one second to get dates before and after + // the object creation + time.Sleep(time.Second * 1) + + before := time.Now().AddDate(0, 0, -3) + after := time.Now() + etag := obj.res.ETag + + for i, test := range []struct { + ifmatch *string + ifnonematch *string + ifmodifiedsince *time.Time + ifunmodifiedsince *time.Time + err error + }{ + // all the cases when preconditions are either empty, true or false + {getPtr("invalid_etag"), getPtr("invalid_etag"), &before, &before, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), &before, &after, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), &before, nil, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), &after, &before, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), &after, &after, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), &after, nil, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), nil, &before, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), nil, &after, errCond}, + {getPtr("invalid_etag"), getPtr("invalid_etag"), nil, nil, errCond}, + + {getPtr("invalid_etag"), etag, &before, &before, errCond}, + {getPtr("invalid_etag"), etag, &before, &after, errCond}, + {getPtr("invalid_etag"), etag, &before, nil, errCond}, + {getPtr("invalid_etag"), etag, &after, &before, errCond}, + {getPtr("invalid_etag"), etag, &after, &after, errCond}, + {getPtr("invalid_etag"), etag, &after, nil, errCond}, + {getPtr("invalid_etag"), etag, nil, &before, errCond}, + {getPtr("invalid_etag"), etag, nil, &after, errCond}, + {getPtr("invalid_etag"), etag, nil, nil, errCond}, + + {getPtr("invalid_etag"), nil, &before, &before, errCond}, + {getPtr("invalid_etag"), nil, &before, &after, errCond}, + {getPtr("invalid_etag"), nil, &before, nil, errCond}, + {getPtr("invalid_etag"), nil, &after, &before, errCond}, + {getPtr("invalid_etag"), nil, &after, &after, errCond}, + {getPtr("invalid_etag"), nil, &after, nil, errCond}, + {getPtr("invalid_etag"), nil, nil, &before, errCond}, + {getPtr("invalid_etag"), nil, nil, &after, errCond}, + {getPtr("invalid_etag"), nil, nil, nil, errCond}, + + {etag, getPtr("invalid_etag"), &before, &before, nil}, + {etag, getPtr("invalid_etag"), &before, &after, nil}, + {etag, getPtr("invalid_etag"), &before, nil, nil}, + {etag, getPtr("invalid_etag"), &after, &before, nil}, + {etag, getPtr("invalid_etag"), &after, &after, nil}, + {etag, getPtr("invalid_etag"), &after, nil, nil}, + {etag, getPtr("invalid_etag"), nil, &before, nil}, + {etag, getPtr("invalid_etag"), nil, &after, nil}, + {etag, getPtr("invalid_etag"), nil, nil, nil}, + + {etag, etag, &before, &before, errMod}, + {etag, etag, &before, &after, errMod}, + {etag, etag, &before, nil, errMod}, + {etag, etag, &after, &before, errMod}, + {etag, etag, &after, &after, errMod}, + {etag, etag, &after, nil, errMod}, + {etag, etag, nil, &before, errMod}, + {etag, etag, nil, &after, errMod}, + {etag, etag, nil, nil, errMod}, + + {etag, nil, &before, &before, nil}, + {etag, nil, &before, &after, nil}, + {etag, nil, &before, nil, nil}, + {etag, nil, &after, &before, errMod}, + {etag, nil, &after, &after, errMod}, + {etag, nil, &after, nil, errMod}, + {etag, nil, nil, &before, nil}, + {etag, nil, nil, &after, nil}, + {etag, nil, nil, nil, nil}, + + {nil, getPtr("invalid_etag"), &before, &before, errCond}, + {nil, getPtr("invalid_etag"), &before, &after, nil}, + {nil, getPtr("invalid_etag"), &before, nil, nil}, + {nil, getPtr("invalid_etag"), &after, &before, errCond}, + {nil, getPtr("invalid_etag"), &after, &after, nil}, + {nil, getPtr("invalid_etag"), &after, nil, nil}, + {nil, getPtr("invalid_etag"), nil, &before, errCond}, + {nil, getPtr("invalid_etag"), nil, &after, nil}, + {nil, getPtr("invalid_etag"), nil, nil, nil}, + + {nil, etag, &before, &before, errCond}, + {nil, etag, &before, &after, errMod}, + {nil, etag, &before, nil, errMod}, + {nil, etag, &after, &before, errCond}, + {nil, etag, &after, &after, errMod}, + {nil, etag, &after, nil, errMod}, + {nil, etag, nil, &before, errCond}, + {nil, etag, nil, &after, errMod}, + {nil, etag, nil, nil, errMod}, + + {nil, nil, &before, &before, errCond}, + {nil, nil, &before, &after, nil}, + {nil, nil, &before, nil, nil}, + {nil, nil, &after, &before, errCond}, + {nil, nil, &after, &after, errMod}, + {nil, nil, &after, nil, errMod}, + {nil, nil, nil, &before, errCond}, + {nil, nil, nil, &after, nil}, + {nil, nil, nil, nil, nil}, + } { + ctx, cancel := context.WithTimeout(context.Background(), shortTimeout) + _, err := s3client.GetObject(ctx, &s3.GetObjectInput{ + Bucket: &bucket, + Key: &key, + IfMatch: test.ifmatch, + IfNoneMatch: test.ifnonematch, + IfModifiedSince: test.ifmodifiedsince, + IfUnmodifiedSince: test.ifunmodifiedsince, + }) + cancel() + if test.err == nil && err != nil { + return fmt.Errorf("test case %d failed: expected no error, but got %v", i, err) + } + if test.err != nil { + apiErr, ok := test.err.(s3err.APIError) + if !ok { + return fmt.Errorf("invalid error type: expected s3err.APIError") + } + if err := checkApiErr(err, apiErr); err != nil { + return fmt.Errorf("test case %d failed: %w", i, err) + } + } + } + + return nil + }) +} + func GetObject_success(s *S3Conf) error { testName := "GetObject_success" return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {