// Copyright (c) 2015-2021 MinIO, Inc. // // This file is part of MinIO Object Storage stack // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package lock import ( "encoding/xml" "errors" "fmt" "net/http" "reflect" "strings" "testing" "time" xhttp "github.com/minio/minio/internal/http" ) func TestParseMode(t *testing.T) { testCases := []struct { value string expectedMode RetMode }{ { value: "governance", expectedMode: RetGovernance, }, { value: "complIAnce", expectedMode: RetCompliance, }, { value: "gce", expectedMode: "", }, } for _, tc := range testCases { if parseRetMode(tc.value) != tc.expectedMode { t.Errorf("Expected Mode %s, got %s", tc.expectedMode, parseRetMode(tc.value)) } } } func TestParseLegalHoldStatus(t *testing.T) { tests := []struct { value string expectedStatus LegalHoldStatus }{ { value: "ON", expectedStatus: LegalHoldOn, }, { value: "Off", expectedStatus: LegalHoldOff, }, { value: "x", expectedStatus: "", }, } for _, tt := range tests { actualStatus := parseLegalHoldStatus(tt.value) if actualStatus != tt.expectedStatus { t.Errorf("Expected legal hold status %s, got %s", tt.expectedStatus, actualStatus) } } } // TestUnmarshalDefaultRetention checks if default retention // marshaling and unmarshalling work as expected func TestUnmarshalDefaultRetention(t *testing.T) { days := uint64(4) years := uint64(1) zerodays := uint64(0) invalidDays := uint64(maximumRetentionDays + 1) tests := []struct { value DefaultRetention expectedErr error expectErr bool }{ { value: DefaultRetention{Mode: "retain"}, expectedErr: fmt.Errorf("unknown retention mode retain"), expectErr: true, }, { value: DefaultRetention{Mode: RetGovernance}, expectedErr: fmt.Errorf("either Days or Years must be specified"), expectErr: true, }, { value: DefaultRetention{Mode: RetGovernance, Days: &days}, expectedErr: nil, expectErr: false, }, { value: DefaultRetention{Mode: RetGovernance, Years: &years}, expectedErr: nil, expectErr: false, }, { value: DefaultRetention{Mode: RetGovernance, Days: &days, Years: &years}, expectedErr: fmt.Errorf("either Days or Years must be specified, not both"), expectErr: true, }, { value: DefaultRetention{Mode: RetGovernance, Days: &zerodays}, expectedErr: fmt.Errorf("Default retention period must be a positive integer value for 'Days'"), expectErr: true, }, { value: DefaultRetention{Mode: RetGovernance, Days: &invalidDays}, expectedErr: fmt.Errorf("Default retention period too large for 'Days' %d", invalidDays), expectErr: true, }, } for _, tt := range tests { d, err := xml.MarshalIndent(&tt.value, "", "\t") if err != nil { t.Fatal(err) } var dr DefaultRetention err = xml.Unmarshal(d, &dr) //nolint:gocritic if tt.expectedErr == nil { if err != nil { t.Fatalf("error: expected = , got = %v", err) } } else if err == nil { t.Fatalf("error: expected = %v, got = ", tt.expectedErr) } else if tt.expectedErr.Error() != err.Error() { t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err) } } } func TestParseObjectLockConfig(t *testing.T) { tests := []struct { value string expectedErr error expectErr bool }{ { value: `yes`, expectedErr: fmt.Errorf("only 'Enabled' value is allowed to ObjectLockEnabled element"), expectErr: true, }, { value: `EnabledCOMPLIANCE0`, expectedErr: fmt.Errorf("Default retention period must be a positive integer value for 'Days'"), expectErr: true, }, { value: `EnabledCOMPLIANCE30`, expectedErr: nil, expectErr: false, }, } for _, tt := range tests { tt := tt t.Run("", func(t *testing.T) { _, err := ParseObjectLockConfig(strings.NewReader(tt.value)) //nolint:gocritic if tt.expectedErr == nil { if err != nil { t.Fatalf("error: expected = , got = %v", err) } } else if err == nil { t.Fatalf("error: expected = %v, got = ", tt.expectedErr) } else if tt.expectedErr.Error() != err.Error() { t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err) } }) } } func TestParseObjectRetention(t *testing.T) { tests := []struct { value string expectedErr error expectErr bool }{ { value: `string2020-01-02T15:04:05Z`, expectedErr: ErrUnknownWORMModeDirective, expectErr: true, }, { value: `COMPLIANCE2017-01-02T15:04:05Z`, expectedErr: ErrPastObjectLockRetainDate, expectErr: true, }, { value: `GOVERNANCE2057-01-02T15:04:05Z`, expectedErr: nil, expectErr: false, }, { value: `GOVERNANCE2057-01-02T15:04:05.000Z`, expectedErr: nil, expectErr: false, }, } for _, tt := range tests { tt := tt t.Run("", func(t *testing.T) { _, err := ParseObjectRetention(strings.NewReader(tt.value)) //nolint:gocritic if tt.expectedErr == nil { if err != nil { t.Fatalf("error: expected = , got = %v", err) } } else if err == nil { t.Fatalf("error: expected = %v, got = ", tt.expectedErr) } else if tt.expectedErr.Error() != err.Error() { t.Fatalf("error: expected = %v, got = %v", tt.expectedErr, err) } }) } } func TestIsObjectLockRequested(t *testing.T) { tests := []struct { header http.Header expectedVal bool }{ { header: http.Header{ "Authorization": []string{"AWS4-HMAC-SHA256 "}, "X-Amz-Content-Sha256": []string{""}, "Content-Encoding": []string{""}, }, expectedVal: false, }, { header: http.Header{ AmzObjectLockLegalHold: []string{""}, }, expectedVal: true, }, { header: http.Header{ AmzObjectLockRetainUntilDate: []string{""}, AmzObjectLockMode: []string{""}, }, expectedVal: true, }, { header: http.Header{ AmzObjectLockBypassRetGovernance: []string{""}, }, expectedVal: false, }, } for _, tt := range tests { actualVal := IsObjectLockRequested(tt.header) if actualVal != tt.expectedVal { t.Fatalf("error: expected %v, actual %v", tt.expectedVal, actualVal) } } } func TestIsObjectLockGovernanceBypassSet(t *testing.T) { tests := []struct { header http.Header expectedVal bool }{ { header: http.Header{ "Authorization": []string{"AWS4-HMAC-SHA256 "}, "X-Amz-Content-Sha256": []string{""}, "Content-Encoding": []string{""}, }, expectedVal: false, }, { header: http.Header{ AmzObjectLockLegalHold: []string{""}, }, expectedVal: false, }, { header: http.Header{ AmzObjectLockRetainUntilDate: []string{""}, AmzObjectLockMode: []string{""}, }, expectedVal: false, }, { header: http.Header{ AmzObjectLockBypassRetGovernance: []string{""}, }, expectedVal: false, }, { header: http.Header{ AmzObjectLockBypassRetGovernance: []string{"true"}, }, expectedVal: true, }, } for _, tt := range tests { actualVal := IsObjectLockGovernanceBypassSet(tt.header) if actualVal != tt.expectedVal { t.Fatalf("error: expected %v, actual %v", tt.expectedVal, actualVal) } } } func TestParseObjectLockRetentionHeaders(t *testing.T) { tests := []struct { header http.Header expectedErr error }{ { header: http.Header{ "Authorization": []string{"AWS4-HMAC-SHA256 "}, "X-Amz-Content-Sha256": []string{""}, "Content-Encoding": []string{""}, }, expectedErr: ErrObjectLockInvalidHeaders, }, { header: http.Header{ xhttp.AmzObjectLockMode: []string{"lock"}, xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02"}, }, expectedErr: ErrUnknownWORMModeDirective, }, { header: http.Header{ xhttp.AmzObjectLockMode: []string{"governance"}, }, expectedErr: ErrObjectLockInvalidHeaders, }, { header: http.Header{ xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02"}, xhttp.AmzObjectLockMode: []string{"governance"}, }, expectedErr: ErrInvalidRetentionDate, }, { header: http.Header{ xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02T15:04:05Z"}, xhttp.AmzObjectLockMode: []string{"governance"}, }, expectedErr: ErrPastObjectLockRetainDate, }, { header: http.Header{ xhttp.AmzObjectLockMode: []string{"governance"}, xhttp.AmzObjectLockRetainUntilDate: []string{"2017-01-02T15:04:05Z"}, }, expectedErr: ErrPastObjectLockRetainDate, }, { header: http.Header{ xhttp.AmzObjectLockMode: []string{"governance"}, xhttp.AmzObjectLockRetainUntilDate: []string{"2087-01-02T15:04:05Z"}, }, expectedErr: nil, }, { header: http.Header{ xhttp.AmzObjectLockMode: []string{"governance"}, xhttp.AmzObjectLockRetainUntilDate: []string{"2087-01-02T15:04:05.000Z"}, }, expectedErr: nil, }, } for i, tt := range tests { _, _, err := ParseObjectLockRetentionHeaders(tt.header) //nolint:gocritic if tt.expectedErr == nil { if err != nil { t.Fatalf("Case %d error: expected = , got = %v", i, err) } } else if err == nil { t.Fatalf("Case %d error: expected = %v, got = ", i, tt.expectedErr) } else if tt.expectedErr.Error() != err.Error() { t.Fatalf("Case %d error: expected = %v, got = %v", i, tt.expectedErr, err) } } } func TestGetObjectRetentionMeta(t *testing.T) { tests := []struct { metadata map[string]string expected ObjectRetention }{ { metadata: map[string]string{ "Authorization": "AWS4-HMAC-SHA256 ", "X-Amz-Content-Sha256": "", "Content-Encoding": "", }, expected: ObjectRetention{}, }, { metadata: map[string]string{ "x-amz-object-lock-mode": "governance", }, expected: ObjectRetention{Mode: RetGovernance}, }, { metadata: map[string]string{ "x-amz-object-lock-retain-until-date": "2020-02-01", }, expected: ObjectRetention{RetainUntilDate: RetentionDate{time.Date(2020, 2, 1, 12, 0, 0, 0, time.UTC)}}, }, } for i, tt := range tests { o := GetObjectRetentionMeta(tt.metadata) if o.Mode != tt.expected.Mode { t.Fatalf("Case %d expected %v, got %v", i, tt.expected.Mode, o.Mode) } } } func TestGetObjectLegalHoldMeta(t *testing.T) { tests := []struct { metadata map[string]string expected ObjectLegalHold }{ { metadata: map[string]string{ "x-amz-object-lock-mode": "governance", }, expected: ObjectLegalHold{}, }, { metadata: map[string]string{ "x-amz-object-lock-legal-hold": "on", }, expected: ObjectLegalHold{Status: LegalHoldOn}, }, { metadata: map[string]string{ "x-amz-object-lock-legal-hold": "off", }, expected: ObjectLegalHold{Status: LegalHoldOff}, }, { metadata: map[string]string{ "x-amz-object-lock-legal-hold": "X", }, expected: ObjectLegalHold{Status: ""}, }, } for i, tt := range tests { o := GetObjectLegalHoldMeta(tt.metadata) if o.Status != tt.expected.Status { t.Fatalf("Case %d expected %v, got %v", i, tt.expected.Status, o.Status) } } } func TestParseObjectLegalHold(t *testing.T) { tests := []struct { value string expectedErr error expectErr bool }{ { value: `string`, expectedErr: ErrMalformedXML, expectErr: true, }, { value: `ON`, expectedErr: nil, expectErr: false, }, { value: `ON`, expectedErr: nil, expectErr: false, }, // invalid Status key { value: `ON`, expectedErr: errors.New("expected element type but have "), expectErr: true, }, // invalid XML attr { value: `ON`, expectedErr: errors.New("expected element type / but have "), expectErr: true, }, { value: `On`, expectedErr: ErrMalformedXML, expectErr: true, }, } for i, tt := range tests { _, err := ParseObjectLegalHold(strings.NewReader(tt.value)) //nolint:gocritic if tt.expectedErr == nil { if err != nil { t.Fatalf("Case %d error: expected = , got = %v", i, err) } } else if err == nil { t.Fatalf("Case %d error: expected = %v, got = ", i, tt.expectedErr) } else if tt.expectedErr.Error() != err.Error() { t.Fatalf("Case %d error: expected = %v, got = %v", i, tt.expectedErr, err) } } } func TestFilterObjectLockMetadata(t *testing.T) { tests := []struct { metadata map[string]string filterRetention bool filterLegalHold bool expected map[string]string }{ { metadata: map[string]string{ "Authorization": "AWS4-HMAC-SHA256 ", "X-Amz-Content-Sha256": "", "Content-Encoding": "", }, expected: map[string]string{ "Authorization": "AWS4-HMAC-SHA256 ", "X-Amz-Content-Sha256": "", "Content-Encoding": "", }, }, { metadata: map[string]string{ "x-amz-object-lock-mode": "governance", }, expected: map[string]string{ "x-amz-object-lock-mode": "governance", }, filterRetention: false, }, { metadata: map[string]string{ "x-amz-object-lock-mode": "governance", "x-amz-object-lock-retain-until-date": "2020-02-01", }, expected: map[string]string{}, filterRetention: true, }, { metadata: map[string]string{ "x-amz-object-lock-legal-hold": "off", }, expected: map[string]string{}, filterLegalHold: true, }, { metadata: map[string]string{ "x-amz-object-lock-legal-hold": "on", }, expected: map[string]string{"x-amz-object-lock-legal-hold": "on"}, filterLegalHold: false, }, { metadata: map[string]string{ "x-amz-object-lock-legal-hold": "on", "x-amz-object-lock-mode": "governance", "x-amz-object-lock-retain-until-date": "2020-02-01", }, expected: map[string]string{}, filterRetention: true, filterLegalHold: true, }, { metadata: map[string]string{ "x-amz-object-lock-legal-hold": "on", "x-amz-object-lock-mode": "governance", "x-amz-object-lock-retain-until-date": "2020-02-01", }, expected: map[string]string{ "x-amz-object-lock-legal-hold": "on", "x-amz-object-lock-mode": "governance", "x-amz-object-lock-retain-until-date": "2020-02-01", }, }, } for i, tt := range tests { o := FilterObjectLockMetadata(tt.metadata, tt.filterRetention, tt.filterLegalHold) if !reflect.DeepEqual(o, tt.expected) { t.Fatalf("Case %d expected %v, got %v", i, tt.metadata, o) } } } func TestToString(t *testing.T) { days := uint64(30) daysPtr := &days years := uint64(2) yearsPtr := &years tests := []struct { name string c Config want string }{ { name: "happy case", c: Config{ ObjectLockEnabled: "Enabled", }, want: "Enabled: true", }, { name: "with default retention days", c: Config{ ObjectLockEnabled: "Enabled", Rule: &struct { DefaultRetention DefaultRetention `xml:"DefaultRetention"` }{ DefaultRetention: DefaultRetention{ Mode: RetGovernance, Days: daysPtr, }, }, }, want: "Enabled: true, Mode: GOVERNANCE, Days: 30", }, { name: "with default retention years", c: Config{ ObjectLockEnabled: "Enabled", Rule: &struct { DefaultRetention DefaultRetention `xml:"DefaultRetention"` }{ DefaultRetention: DefaultRetention{ Mode: RetCompliance, Years: yearsPtr, }, }, }, want: "Enabled: true, Mode: COMPLIANCE, Years: 2", }, { name: "disabled case", c: Config{ ObjectLockEnabled: "Disabled", }, want: "Enabled: false", }, { name: "empty case", c: Config{}, want: "Enabled: false", }, } for _, tt := range tests { got := tt.c.String() if got != tt.want { t.Errorf("test: %s, got: '%v', want: '%v'", tt.name, got, tt.want) } } }