* 9d5d41 lint * b58a69 lint * 47a661 lint * 5ebf79 lint * 4ce1be Merge remote-tracking branch 'origin' into main-acm-cmxt * 5db8d4 messagE * fc54a0 coverage * 5e6026 fix: [CODE-4585]: fix default reviewers with codeowner file error * 10648b increase code coverage (#4483) (#4541) * f74698 increase code coverage (#4483) (#4498) * b1b395 increase code coverage (#4483) (#4487) * 951ba4 increase code coverage (#4483) * 8106d0 harness ACM created this fix
This commit is contained in:
parent
d9a43614ae
commit
3e135cc128
|
|
@ -0,0 +1,180 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package nocache
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNoCache(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
handler http.Handler
|
||||
expectedStatus int
|
||||
checkHeaders bool
|
||||
}{
|
||||
{
|
||||
name: "sets no-cache headers",
|
||||
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("test response"))
|
||||
}),
|
||||
expectedStatus: http.StatusOK,
|
||||
checkHeaders: true,
|
||||
},
|
||||
{
|
||||
name: "preserves handler status code",
|
||||
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}),
|
||||
expectedStatus: http.StatusNotFound,
|
||||
checkHeaders: true,
|
||||
},
|
||||
{
|
||||
name: "works with empty handler",
|
||||
handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Empty handler
|
||||
}),
|
||||
expectedStatus: http.StatusOK,
|
||||
checkHeaders: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a test request
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
// Wrap handler with NoCache middleware
|
||||
middleware := NoCache(tt.handler)
|
||||
middleware.ServeHTTP(rec, req)
|
||||
|
||||
// Check status code
|
||||
if rec.Code != tt.expectedStatus {
|
||||
t.Errorf("expected status %d, got %d", tt.expectedStatus, rec.Code)
|
||||
}
|
||||
|
||||
// Check no-cache headers
|
||||
if tt.checkHeaders {
|
||||
expectedHeaders := map[string]string{
|
||||
"Expires": time.Unix(0, 0).Format(time.RFC1123),
|
||||
"Cache-Control": "no-cache, no-store, no-transform, must-revalidate, private, max-age=0",
|
||||
"Pragma": "no-cache",
|
||||
"X-Accel-Expires": "0",
|
||||
}
|
||||
|
||||
for key, expectedValue := range expectedHeaders {
|
||||
actualValue := rec.Header().Get(key)
|
||||
if actualValue != expectedValue {
|
||||
t.Errorf("header %s: expected %q, got %q", key, expectedValue, actualValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCachePreservesETag(t *testing.T) {
|
||||
// Create a handler that sets an ETag
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("ETag", `"test-etag-123"`)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
// Wrap with NoCache middleware
|
||||
middleware := NoCache(handler)
|
||||
middleware.ServeHTTP(rec, req)
|
||||
|
||||
// Verify ETag is preserved
|
||||
etag := rec.Header().Get("ETag")
|
||||
if etag != `"test-etag-123"` {
|
||||
t.Errorf("expected ETag to be preserved, got %q", etag)
|
||||
}
|
||||
|
||||
// Verify no-cache headers are still set
|
||||
cacheControl := rec.Header().Get("Cache-Control")
|
||||
if cacheControl != "no-cache, no-store, no-transform, must-revalidate, private, max-age=0" {
|
||||
t.Errorf("expected Cache-Control header to be set, got %q", cacheControl)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCacheWithMultipleRequests(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("response"))
|
||||
})
|
||||
|
||||
middleware := NoCache(handler)
|
||||
|
||||
// Make multiple requests to ensure middleware is reusable
|
||||
for i := 0; i < 3; i++ {
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
middleware.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("request %d: expected status 200, got %d", i, rec.Code)
|
||||
}
|
||||
|
||||
expires := rec.Header().Get("Expires")
|
||||
if expires == "" {
|
||||
t.Errorf("request %d: Expires header not set", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCacheHeaderValues(t *testing.T) {
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
middleware := NoCache(handler)
|
||||
middleware.ServeHTTP(rec, req)
|
||||
|
||||
// Verify epoch format
|
||||
expectedEpoch := time.Unix(0, 0).Format(time.RFC1123)
|
||||
if rec.Header().Get("Expires") != expectedEpoch {
|
||||
t.Errorf("Expires header should be epoch time in RFC1123 format")
|
||||
}
|
||||
|
||||
// Verify all required directives in Cache-Control
|
||||
cacheControl := rec.Header().Get("Cache-Control")
|
||||
requiredDirectives := []string{"no-cache", "no-store", "no-transform", "must-revalidate", "private", "max-age=0"}
|
||||
for _, directive := range requiredDirectives {
|
||||
if !contains(cacheControl, directive) {
|
||||
t.Errorf("Cache-Control missing directive: %s", directive)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
@ -176,3 +176,292 @@ func Test_Depth(t *testing.T) {
|
|||
assert.Equal(t, tt.want, got, "depth isn't matching for %q", tt.in)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DisectLeaf(t *testing.T) {
|
||||
type testCase struct {
|
||||
in string
|
||||
wantParent string
|
||||
wantLeaf string
|
||||
wantErr error
|
||||
}
|
||||
tests := []testCase{
|
||||
{
|
||||
in: "",
|
||||
wantParent: "",
|
||||
wantLeaf: "",
|
||||
wantErr: ErrPathEmpty,
|
||||
},
|
||||
{
|
||||
in: "/",
|
||||
wantParent: "",
|
||||
wantLeaf: "",
|
||||
wantErr: ErrPathEmpty,
|
||||
},
|
||||
{
|
||||
in: "space1",
|
||||
wantParent: "",
|
||||
wantLeaf: "space1",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
in: "/space1/",
|
||||
wantParent: "",
|
||||
wantLeaf: "space1",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
in: "space1/space2",
|
||||
wantParent: "space1",
|
||||
wantLeaf: "space2",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
in: "/space1/space2/",
|
||||
wantParent: "space1",
|
||||
wantLeaf: "space2",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
in: "space1/space2/space3",
|
||||
wantParent: "space1/space2",
|
||||
wantLeaf: "space3",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
in: "/space1/space2/space3/",
|
||||
wantParent: "space1/space2",
|
||||
wantLeaf: "space3",
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
gotParent, gotLeaf, gotErr := DisectLeaf(tt.in)
|
||||
assert.Equal(t, tt.wantParent, gotParent, "parent isn't matching for %q", tt.in)
|
||||
assert.Equal(t, tt.wantLeaf, gotLeaf, "leaf isn't matching for %q", tt.in)
|
||||
assert.Equal(t, tt.wantErr, gotErr, "error isn't matching for %q", tt.in)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_DisectRoot(t *testing.T) {
|
||||
type testCase struct {
|
||||
in string
|
||||
wantRoot string
|
||||
wantSubPath string
|
||||
wantErr error
|
||||
}
|
||||
tests := []testCase{
|
||||
{
|
||||
in: "",
|
||||
wantRoot: "",
|
||||
wantSubPath: "",
|
||||
wantErr: ErrPathEmpty,
|
||||
},
|
||||
{
|
||||
in: "/",
|
||||
wantRoot: "",
|
||||
wantSubPath: "",
|
||||
wantErr: ErrPathEmpty,
|
||||
},
|
||||
{
|
||||
in: "space1",
|
||||
wantRoot: "space1",
|
||||
wantSubPath: "",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
in: "/space1/",
|
||||
wantRoot: "space1",
|
||||
wantSubPath: "",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
in: "space1/space2",
|
||||
wantRoot: "space1",
|
||||
wantSubPath: "space2",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
in: "/space1/space2/",
|
||||
wantRoot: "space1",
|
||||
wantSubPath: "space2",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
in: "space1/space2/space3",
|
||||
wantRoot: "space1",
|
||||
wantSubPath: "space2/space3",
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
in: "/space1/space2/space3/",
|
||||
wantRoot: "space1",
|
||||
wantSubPath: "space2/space3",
|
||||
wantErr: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
gotRoot, gotSubPath, gotErr := DisectRoot(tt.in)
|
||||
assert.Equal(t, tt.wantRoot, gotRoot, "root isn't matching for %q", tt.in)
|
||||
assert.Equal(t, tt.wantSubPath, gotSubPath, "subPath isn't matching for %q", tt.in)
|
||||
assert.Equal(t, tt.wantErr, gotErr, "error isn't matching for %q", tt.in)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Segments(t *testing.T) {
|
||||
type testCase struct {
|
||||
in string
|
||||
want []string
|
||||
}
|
||||
tests := []testCase{
|
||||
{
|
||||
in: "",
|
||||
want: []string{""},
|
||||
},
|
||||
{
|
||||
in: "/",
|
||||
want: []string{""},
|
||||
},
|
||||
{
|
||||
in: "space1",
|
||||
want: []string{"space1"},
|
||||
},
|
||||
{
|
||||
in: "/space1/",
|
||||
want: []string{"space1"},
|
||||
},
|
||||
{
|
||||
in: "space1/space2",
|
||||
want: []string{"space1", "space2"},
|
||||
},
|
||||
{
|
||||
in: "/space1/space2/",
|
||||
want: []string{"space1", "space2"},
|
||||
},
|
||||
{
|
||||
in: "space1/space2/space3",
|
||||
want: []string{"space1", "space2", "space3"},
|
||||
},
|
||||
{
|
||||
in: "/space1/space2/space3/",
|
||||
want: []string{"space1", "space2", "space3"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := Segments(tt.in)
|
||||
assert.Equal(t, tt.want, got, "segments aren't matching for %q", tt.in)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IsAncesterOf(t *testing.T) {
|
||||
type testCase struct {
|
||||
path string
|
||||
other string
|
||||
want bool
|
||||
}
|
||||
tests := []testCase{
|
||||
{
|
||||
path: "",
|
||||
other: "",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
path: "space1",
|
||||
other: "space1",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
path: "space1",
|
||||
other: "space1/space2",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
path: "space1",
|
||||
other: "space1/space2/space3",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
path: "space1/space2",
|
||||
other: "space1/space2/space3",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
path: "/space1/",
|
||||
other: "/space1/space2/",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
path: "space1",
|
||||
other: "space2",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
path: "space1/space2",
|
||||
other: "space1",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
path: "space1/space2",
|
||||
other: "space1/space3",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
path: "space1",
|
||||
other: "space10",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
path: "space1/in",
|
||||
other: "space1/inner",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := IsAncesterOf(tt.path, tt.other)
|
||||
assert.Equal(t, tt.want, got, "IsAncesterOf(%q, %q) isn't matching", tt.path, tt.other)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_Parent(t *testing.T) {
|
||||
type testCase struct {
|
||||
in string
|
||||
want string
|
||||
}
|
||||
tests := []testCase{
|
||||
{
|
||||
in: "",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
in: "/",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
in: "space1",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
in: "/space1/",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
in: "space1/space2",
|
||||
want: "space1",
|
||||
},
|
||||
{
|
||||
in: "/space1/space2/",
|
||||
want: "space1",
|
||||
},
|
||||
{
|
||||
in: "space1/space2/space3",
|
||||
want: "space1/space2",
|
||||
},
|
||||
{
|
||||
in: "/space1/space2/space3/",
|
||||
want: "space1/space2",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := Parent(tt.in)
|
||||
assert.Equal(t, tt.want, got, "parent isn't matching for %q", tt.in)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,229 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package request
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReplacePrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
rawPath string
|
||||
oldPrefix string
|
||||
newPrefix string
|
||||
wantPath string
|
||||
wantRawPath string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "simple path replacement",
|
||||
path: "/api/v1/repos",
|
||||
rawPath: "",
|
||||
oldPrefix: "/api",
|
||||
newPrefix: "/v2",
|
||||
wantPath: "/v2/v1/repos",
|
||||
wantRawPath: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty old prefix",
|
||||
path: "/api/v1/repos",
|
||||
rawPath: "",
|
||||
oldPrefix: "",
|
||||
newPrefix: "/v2",
|
||||
wantPath: "/v2/api/v1/repos",
|
||||
wantRawPath: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty new prefix",
|
||||
path: "/api/v1/repos",
|
||||
rawPath: "",
|
||||
oldPrefix: "/api",
|
||||
newPrefix: "",
|
||||
wantPath: "/v1/repos",
|
||||
wantRawPath: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "full path replacement",
|
||||
path: "/api",
|
||||
rawPath: "",
|
||||
oldPrefix: "/api",
|
||||
newPrefix: "/v2",
|
||||
wantPath: "/v2",
|
||||
wantRawPath: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "path with raw path",
|
||||
path: "/api/v1/repos",
|
||||
rawPath: "/api/v1/repos",
|
||||
oldPrefix: "/api",
|
||||
newPrefix: "/v2",
|
||||
wantPath: "%2Fv2%2Fv1%2Frepos",
|
||||
wantRawPath: "/v2/v1/repos",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "path with encoded characters",
|
||||
path: "/api/v1/repos%20test",
|
||||
rawPath: "/api/v1/repos%20test",
|
||||
oldPrefix: "/api",
|
||||
newPrefix: "/v2",
|
||||
wantPath: "%2Fv2%2Fv1%2Frepos%2520test",
|
||||
wantRawPath: "/v2/v1/repos%20test",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "prefix not found in path",
|
||||
path: "/v1/repos",
|
||||
rawPath: "",
|
||||
oldPrefix: "/api",
|
||||
newPrefix: "/v2",
|
||||
wantPath: "/v1/repos",
|
||||
wantRawPath: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "prefix not found in raw path",
|
||||
path: "/api/v1/repos",
|
||||
rawPath: "/v1/repos",
|
||||
oldPrefix: "/api",
|
||||
newPrefix: "/v2",
|
||||
wantPath: "/api/v1/repos",
|
||||
wantRawPath: "/v1/repos",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "prefix longer than path",
|
||||
path: "/api",
|
||||
rawPath: "",
|
||||
oldPrefix: "/api/v1",
|
||||
newPrefix: "/v2",
|
||||
wantPath: "/api",
|
||||
wantRawPath: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "partial prefix match should fail",
|
||||
path: "/application/v1",
|
||||
rawPath: "",
|
||||
oldPrefix: "/api",
|
||||
newPrefix: "/v2",
|
||||
wantPath: "/application/v1",
|
||||
wantRawPath: "",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "replace with longer prefix",
|
||||
path: "/api/v1",
|
||||
rawPath: "",
|
||||
oldPrefix: "/api",
|
||||
newPrefix: "/v2/api/new",
|
||||
wantPath: "/v2/api/new/v1",
|
||||
wantRawPath: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "replace root path",
|
||||
path: "/",
|
||||
rawPath: "",
|
||||
oldPrefix: "/",
|
||||
newPrefix: "/v2",
|
||||
wantPath: "/v2",
|
||||
wantRawPath: "",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a new request with the test path
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example.com"+tt.path, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Set raw path if provided
|
||||
if tt.rawPath != "" {
|
||||
req.URL.RawPath = tt.rawPath
|
||||
}
|
||||
|
||||
// Call ReplacePrefix
|
||||
err = ReplacePrefix(req, tt.oldPrefix, tt.newPrefix)
|
||||
|
||||
// Check error expectation
|
||||
if tt.wantErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.wantPath, req.URL.Path, "Path doesn't match")
|
||||
assert.Equal(t, tt.wantRawPath, req.URL.RawPath, "RawPath doesn't match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReplacePrefix_PreservesOtherURLFields(t *testing.T) {
|
||||
// Create a request with various URL fields set
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet,
|
||||
"http://example.com:8080/api/v1/repos?query=test#fragment", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
originalScheme := req.URL.Scheme
|
||||
originalHost := req.URL.Host
|
||||
originalQuery := req.URL.RawQuery
|
||||
originalFragment := req.URL.Fragment
|
||||
|
||||
// Replace the prefix
|
||||
err = ReplacePrefix(req, "/api", "/v2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify other fields are preserved
|
||||
assert.Equal(t, originalScheme, req.URL.Scheme, "Scheme should be preserved")
|
||||
assert.Equal(t, originalHost, req.URL.Host, "Host should be preserved")
|
||||
assert.Equal(t, originalQuery, req.URL.RawQuery, "Query should be preserved")
|
||||
assert.Equal(t, originalFragment, req.URL.Fragment, "Fragment should be preserved")
|
||||
assert.Equal(t, "/v2/v1/repos", req.URL.Path, "Path should be updated")
|
||||
}
|
||||
|
||||
func TestReplacePrefix_WithComplexURL(t *testing.T) {
|
||||
// Test with a complex URL containing special characters
|
||||
rawURL := "http://example.com/api/v1/repos/owner%2Frepo?branch=feature%2Ftest&limit=10"
|
||||
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, rawURL, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ReplacePrefix(req, "/api", "/v2")
|
||||
require.NoError(t, err)
|
||||
|
||||
// The path should be updated (note: when RawPath is set, Path gets escaped)
|
||||
assert.Equal(t, "%2Fv2%2Fv1%2Frepos%2Fowner%252Frepo", req.URL.Path)
|
||||
// Query parameters should be preserved
|
||||
assert.Equal(t, "branch=feature%2Ftest&limit=10", req.URL.RawQuery)
|
||||
|
||||
// Verify we can still parse query parameters
|
||||
values, err := url.ParseQuery(req.URL.RawQuery)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "feature/test", values.Get("branch"))
|
||||
assert.Equal(t, "10", values.Get("limit"))
|
||||
}
|
||||
|
|
@ -0,0 +1,795 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package url
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildGITCloneSSHURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
user string
|
||||
sshURL string
|
||||
repoPath string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "standard port 22",
|
||||
user: "git",
|
||||
sshURL: "ssh://git.example.com:22",
|
||||
repoPath: "org/repo",
|
||||
want: "git@git.example.com:org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "default port (empty)",
|
||||
user: "git",
|
||||
sshURL: "ssh://git.example.com",
|
||||
repoPath: "org/repo",
|
||||
want: "git@git.example.com:org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "custom port",
|
||||
user: "git",
|
||||
sshURL: "ssh://git.example.com:2222",
|
||||
repoPath: "org/repo",
|
||||
want: "ssh://git@git.example.com:2222/org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "repo path with .git suffix",
|
||||
user: "git",
|
||||
sshURL: "ssh://git.example.com",
|
||||
repoPath: "org/repo.git",
|
||||
want: "git@git.example.com:org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "repo path with leading slash",
|
||||
user: "git",
|
||||
sshURL: "ssh://git.example.com",
|
||||
repoPath: "/org/repo",
|
||||
want: "git@git.example.com:org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "repo path with trailing slash",
|
||||
user: "git",
|
||||
sshURL: "ssh://git.example.com",
|
||||
repoPath: "org/repo/",
|
||||
want: "git@git.example.com:org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "custom port with path in URL",
|
||||
user: "git",
|
||||
sshURL: "ssh://git.example.com:2222/base",
|
||||
repoPath: "org/repo",
|
||||
want: "ssh://git@git.example.com:2222/base/org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "standard port with path in URL",
|
||||
user: "git",
|
||||
sshURL: "ssh://git.example.com:22/base",
|
||||
repoPath: "org/repo",
|
||||
want: "git@git.example.com:base/org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "port 0 treated as default",
|
||||
user: "git",
|
||||
sshURL: "ssh://git.example.com:0",
|
||||
repoPath: "org/repo",
|
||||
want: "git@git.example.com:org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "different username",
|
||||
user: "admin",
|
||||
sshURL: "ssh://git.example.com",
|
||||
repoPath: "org/repo",
|
||||
want: "admin@git.example.com:org/repo.git",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
sshURL, err := url.Parse(tt.sshURL)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to parse sshURL: %v", err)
|
||||
}
|
||||
|
||||
got := BuildGITCloneSSHURL(tt.user, sshURL, tt.repoPath)
|
||||
if got != tt.want {
|
||||
t.Errorf("BuildGITCloneSSHURL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewProvider(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
internalURLRaw string
|
||||
containerURLRaw string
|
||||
apiURLRaw string
|
||||
gitURLRaw string
|
||||
gitSSHURLRaw string
|
||||
sshDefaultUser string
|
||||
sshEnabled bool
|
||||
uiURLRaw string
|
||||
registryURLRaw string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid URLs",
|
||||
internalURLRaw: "http://internal.example.com",
|
||||
containerURLRaw: "http://container.example.com",
|
||||
apiURLRaw: "http://api.example.com",
|
||||
gitURLRaw: "http://git.example.com",
|
||||
gitSSHURLRaw: "ssh://git.example.com:22",
|
||||
sshDefaultUser: "git",
|
||||
sshEnabled: true,
|
||||
uiURLRaw: "http://ui.example.com",
|
||||
registryURLRaw: "http://registry.example.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "URLs with trailing slashes",
|
||||
internalURLRaw: "http://internal.example.com/",
|
||||
containerURLRaw: "http://container.example.com/",
|
||||
apiURLRaw: "http://api.example.com/",
|
||||
gitURLRaw: "http://git.example.com/",
|
||||
gitSSHURLRaw: "ssh://git.example.com:22/",
|
||||
sshDefaultUser: "git",
|
||||
sshEnabled: true,
|
||||
uiURLRaw: "http://ui.example.com/",
|
||||
registryURLRaw: "http://registry.example.com/",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid internal URL",
|
||||
internalURLRaw: "://invalid",
|
||||
containerURLRaw: "http://container.example.com",
|
||||
apiURLRaw: "http://api.example.com",
|
||||
gitURLRaw: "http://git.example.com",
|
||||
gitSSHURLRaw: "ssh://git.example.com:22",
|
||||
sshDefaultUser: "git",
|
||||
sshEnabled: false,
|
||||
uiURLRaw: "http://ui.example.com",
|
||||
registryURLRaw: "http://registry.example.com",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid container URL",
|
||||
internalURLRaw: "http://internal.example.com",
|
||||
containerURLRaw: "://invalid",
|
||||
apiURLRaw: "http://api.example.com",
|
||||
gitURLRaw: "http://git.example.com",
|
||||
gitSSHURLRaw: "ssh://git.example.com:22",
|
||||
sshDefaultUser: "git",
|
||||
sshEnabled: false,
|
||||
uiURLRaw: "http://ui.example.com",
|
||||
registryURLRaw: "http://registry.example.com",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid SSH URL when SSH enabled",
|
||||
internalURLRaw: "http://internal.example.com",
|
||||
containerURLRaw: "http://container.example.com",
|
||||
apiURLRaw: "http://api.example.com",
|
||||
gitURLRaw: "http://git.example.com",
|
||||
gitSSHURLRaw: "://invalid",
|
||||
sshDefaultUser: "git",
|
||||
sshEnabled: true,
|
||||
uiURLRaw: "http://ui.example.com",
|
||||
registryURLRaw: "http://registry.example.com",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid SSH URL when SSH disabled (should not error)",
|
||||
internalURLRaw: "http://internal.example.com",
|
||||
containerURLRaw: "http://container.example.com",
|
||||
apiURLRaw: "http://api.example.com",
|
||||
gitURLRaw: "http://git.example.com",
|
||||
gitSSHURLRaw: "://invalid",
|
||||
sshDefaultUser: "git",
|
||||
sshEnabled: false,
|
||||
uiURLRaw: "http://ui.example.com",
|
||||
registryURLRaw: "http://registry.example.com",
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := NewProvider(
|
||||
tt.internalURLRaw,
|
||||
tt.containerURLRaw,
|
||||
tt.apiURLRaw,
|
||||
tt.gitURLRaw,
|
||||
tt.gitSSHURLRaw,
|
||||
tt.sshDefaultUser,
|
||||
tt.sshEnabled,
|
||||
tt.uiURLRaw,
|
||||
tt.registryURLRaw,
|
||||
)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewProvider() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetInternalAPIURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GetInternalAPIURL(ctx)
|
||||
want := "http://internal.example.com/api"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("GetInternalAPIURL() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateContainerGITCloneURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
repoPath string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "simple repo path",
|
||||
repoPath: "org/repo",
|
||||
want: "http://container.example.com/git/org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "repo path with .git suffix",
|
||||
repoPath: "org/repo.git",
|
||||
want: "http://container.example.com/git/org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "repo path with leading slash",
|
||||
repoPath: "/org/repo",
|
||||
want: "http://container.example.com/git/org/repo.git",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := p.GenerateContainerGITCloneURL(ctx, tt.repoPath)
|
||||
if got != tt.want {
|
||||
t.Errorf("GenerateContainerGITCloneURL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateGITCloneURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
repoPath string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "simple repo path",
|
||||
repoPath: "org/repo",
|
||||
want: "http://git.example.com/org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "repo path with .git suffix",
|
||||
repoPath: "org/repo.git",
|
||||
want: "http://git.example.com/org/repo.git",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := p.GenerateGITCloneURL(ctx, tt.repoPath)
|
||||
if got != tt.want {
|
||||
t.Errorf("GenerateGITCloneURL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateGITCloneSSHURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sshEnabled bool
|
||||
repoPath string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "SSH enabled",
|
||||
sshEnabled: true,
|
||||
repoPath: "org/repo",
|
||||
want: "git@git.example.com:org/repo.git",
|
||||
},
|
||||
{
|
||||
name: "SSH disabled",
|
||||
sshEnabled: false,
|
||||
repoPath: "org/repo",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
tt.sshEnabled,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GenerateGITCloneSSHURL(ctx, tt.repoPath)
|
||||
if got != tt.want {
|
||||
t.Errorf("GenerateGITCloneSSHURL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateUIRepoURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GenerateUIRepoURL(ctx, "org/repo")
|
||||
want := "http://ui.example.com/org/repo"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("GenerateUIRepoURL() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateUIPRURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GenerateUIPRURL(ctx, "org/repo", 123)
|
||||
want := "http://ui.example.com/org/repo/pulls/123"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("GenerateUIPRURL() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateUICompareURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GenerateUICompareURL(ctx, "org/repo", "main", "develop")
|
||||
want := "http://ui.example.com/org/repo/pulls/compare/main...develop"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("GenerateUICompareURL() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateUIRefURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GenerateUIRefURL(ctx, "org/repo", "abc123")
|
||||
want := "http://ui.example.com/org/repo/commit/abc123"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("GenerateUIRefURL() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetAPIHostname(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com:8080",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GetAPIHostname(ctx)
|
||||
want := "api.example.com"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("GetAPIHostname() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetGITHostname(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com:9090",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GetGITHostname(ctx)
|
||||
want := "git.example.com"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("GetGITHostname() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetAPIProto(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
apiURLRaw string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "http protocol",
|
||||
apiURLRaw: "http://api.example.com",
|
||||
want: "http",
|
||||
},
|
||||
{
|
||||
name: "https protocol",
|
||||
apiURLRaw: "https://api.example.com",
|
||||
want: "https",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
tt.apiURLRaw,
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GetAPIProto(ctx)
|
||||
if got != tt.want {
|
||||
t.Errorf("GetAPIProto() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GetUIBaseURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GetUIBaseURL(ctx)
|
||||
want := "http://ui.example.com"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("GetUIBaseURL() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateUIBuildURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
got := p.GenerateUIBuildURL(ctx, "org/repo", "my-pipeline", 42)
|
||||
want := "http://ui.example.com/org/repo/pipelines/my-pipeline/execution/42"
|
||||
|
||||
if got != want {
|
||||
t.Errorf("GenerateUIBuildURL() = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_RegistryURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
params []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no params",
|
||||
params: []string{},
|
||||
want: "http://registry.example.com",
|
||||
},
|
||||
{
|
||||
name: "single param",
|
||||
params: []string{"docker"},
|
||||
want: "http://registry.example.com/docker",
|
||||
},
|
||||
{
|
||||
name: "generic type swaps params",
|
||||
params: []string{"myregistry", "generic"},
|
||||
want: "http://registry.example.com/generic/myregistry",
|
||||
},
|
||||
{
|
||||
name: "maven type swaps params",
|
||||
params: []string{"myregistry", "maven"},
|
||||
want: "http://registry.example.com/maven/myregistry",
|
||||
},
|
||||
{
|
||||
name: "docker type lowercase",
|
||||
params: []string{"DOCKER"},
|
||||
want: "http://registry.example.com/docker",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := p.RegistryURL(ctx, tt.params...)
|
||||
if got != tt.want {
|
||||
t.Errorf("RegistryURL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_PackageURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
regRef string
|
||||
pkgType string
|
||||
params []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "basic package URL",
|
||||
regRef: "myregistry",
|
||||
pkgType: "docker",
|
||||
params: []string{},
|
||||
want: "http://registry.example.com/pkg/myregistry/docker",
|
||||
},
|
||||
{
|
||||
name: "package URL with params",
|
||||
regRef: "myregistry",
|
||||
pkgType: "docker",
|
||||
params: []string{"myimage", "v1.0.0"},
|
||||
want: "http://registry.example.com/pkg/myregistry/docker/myimage/v1.0.0",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := p.PackageURL(ctx, tt.regRef, tt.pkgType, tt.params...)
|
||||
if got != tt.want {
|
||||
t.Errorf("PackageURL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateUIRegistryURL(t *testing.T) {
|
||||
p, err := NewProvider(
|
||||
"http://internal.example.com",
|
||||
"http://container.example.com",
|
||||
"http://api.example.com",
|
||||
"http://git.example.com",
|
||||
"ssh://git.example.com:22",
|
||||
"git",
|
||||
false,
|
||||
"http://ui.example.com",
|
||||
"http://registry.example.com",
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewProvider() error = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
parentSpacePath string
|
||||
registryName string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "valid space path",
|
||||
parentSpacePath: "myspace",
|
||||
registryName: "myregistry",
|
||||
want: "http://ui.example.com/spaces/myspace/registries/myregistry",
|
||||
},
|
||||
{
|
||||
name: "nested space path",
|
||||
parentSpacePath: "myspace/subspace",
|
||||
registryName: "myregistry",
|
||||
want: "http://ui.example.com/spaces/myspace/registries/myregistry",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := p.GenerateUIRegistryURL(ctx, tt.parentSpacePath, tt.registryName)
|
||||
if got != tt.want {
|
||||
t.Errorf("GenerateUIRegistryURL() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetRealIP(t *testing.T) {
|
||||
t.Run("returns IP when present", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), realIPKey, "192.168.1.1")
|
||||
ip := GetRealIP(ctx)
|
||||
if ip != "192.168.1.1" {
|
||||
t.Errorf("expected IP to be '192.168.1.1', got '%s'", ip)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty string when not present", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ip := GetRealIP(ctx)
|
||||
if ip != "" {
|
||||
t.Errorf("expected empty string, got '%s'", ip)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty string when wrong type", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), realIPKey, 12345)
|
||||
ip := GetRealIP(ctx)
|
||||
if ip != "" {
|
||||
t.Errorf("expected empty string, got '%s'", ip)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handles IPv6 address", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), realIPKey, "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
|
||||
ip := GetRealIP(ctx)
|
||||
if ip != "2001:0db8:85a3:0000:0000:8a2e:0370:7334" {
|
||||
t.Errorf("expected IPv6 address, got '%s'", ip)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetPath(t *testing.T) {
|
||||
t.Run("returns path when present", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), pathKey, "/api/v1/users")
|
||||
path := GetPath(ctx)
|
||||
if path != "/api/v1/users" {
|
||||
t.Errorf("expected path to be '/api/v1/users', got '%s'", path)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty string when not present", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
path := GetPath(ctx)
|
||||
if path != "" {
|
||||
t.Errorf("expected empty string, got '%s'", path)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty string when wrong type", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), pathKey, 12345)
|
||||
path := GetPath(ctx)
|
||||
if path != "" {
|
||||
t.Errorf("expected empty string, got '%s'", path)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handles empty path", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), pathKey, "")
|
||||
path := GetPath(ctx)
|
||||
if path != "" {
|
||||
t.Errorf("expected empty string, got '%s'", path)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetRequestID(t *testing.T) {
|
||||
t.Run("returns request ID when present", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), requestID, "req-12345")
|
||||
id := GetRequestID(ctx)
|
||||
if id != "req-12345" {
|
||||
t.Errorf("expected request ID to be 'req-12345', got '%s'", id)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty string when not present", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
id := GetRequestID(ctx)
|
||||
if id != "" {
|
||||
t.Errorf("expected empty string, got '%s'", id)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty string when wrong type", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), requestID, 12345)
|
||||
id := GetRequestID(ctx)
|
||||
if id != "" {
|
||||
t.Errorf("expected empty string, got '%s'", id)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handles UUID format", func(t *testing.T) {
|
||||
uuid := "550e8400-e29b-41d4-a716-446655440000"
|
||||
ctx := context.WithValue(context.Background(), requestID, uuid)
|
||||
id := GetRequestID(ctx)
|
||||
if id != uuid {
|
||||
t.Errorf("expected request ID to be '%s', got '%s'", uuid, id)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetRequestMethod(t *testing.T) {
|
||||
t.Run("returns method when present", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), requestMethod, "GET")
|
||||
method := GetRequestMethod(ctx)
|
||||
if method != "GET" {
|
||||
t.Errorf("expected method to be 'GET', got '%s'", method)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty string when not present", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
method := GetRequestMethod(ctx)
|
||||
if method != "" {
|
||||
t.Errorf("expected empty string, got '%s'", method)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("returns empty string when wrong type", func(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), requestMethod, 12345)
|
||||
method := GetRequestMethod(ctx)
|
||||
if method != "" {
|
||||
t.Errorf("expected empty string, got '%s'", method)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handles various HTTP methods", func(t *testing.T) {
|
||||
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"}
|
||||
for _, m := range methods {
|
||||
ctx := context.WithValue(context.Background(), requestMethod, m)
|
||||
method := GetRequestMethod(ctx)
|
||||
if method != m {
|
||||
t.Errorf("expected method to be '%s', got '%s'", m, method)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package blob
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestProviderConstants(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
provider Provider
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "GCS provider",
|
||||
provider: ProviderGCS,
|
||||
expected: "gcs",
|
||||
},
|
||||
{
|
||||
name: "FileSystem provider",
|
||||
provider: ProviderFileSystem,
|
||||
expected: "filesystem",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if string(test.provider) != test.expected {
|
||||
t.Errorf("expected provider %q, got %q", test.expected, string(test.provider))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigStruct(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config Config
|
||||
}{
|
||||
{
|
||||
name: "empty config",
|
||||
config: Config{},
|
||||
},
|
||||
{
|
||||
name: "GCS config",
|
||||
config: Config{
|
||||
Provider: ProviderGCS,
|
||||
Bucket: "test-bucket",
|
||||
KeyPath: "/path/to/key.json",
|
||||
TargetPrincipal: "test@example.com",
|
||||
ImpersonationLifetime: time.Hour,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "FileSystem config",
|
||||
config: Config{
|
||||
Provider: ProviderFileSystem,
|
||||
Bucket: "/local/storage/path",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with zero duration",
|
||||
config: Config{
|
||||
Provider: ProviderGCS,
|
||||
ImpersonationLifetime: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with negative duration",
|
||||
config: Config{
|
||||
Provider: ProviderGCS,
|
||||
ImpersonationLifetime: -time.Hour,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Test that the config can be created and accessed
|
||||
config := test.config
|
||||
|
||||
// Verify provider field
|
||||
if config.Provider != test.config.Provider {
|
||||
t.Errorf("expected provider %q, got %q", test.config.Provider, config.Provider)
|
||||
}
|
||||
|
||||
// Verify bucket field
|
||||
if config.Bucket != test.config.Bucket {
|
||||
t.Errorf("expected bucket %q, got %q", test.config.Bucket, config.Bucket)
|
||||
}
|
||||
|
||||
// Verify key path field
|
||||
if config.KeyPath != test.config.KeyPath {
|
||||
t.Errorf("expected key path %q, got %q", test.config.KeyPath, config.KeyPath)
|
||||
}
|
||||
|
||||
// Verify target principal field
|
||||
if config.TargetPrincipal != test.config.TargetPrincipal {
|
||||
t.Errorf("expected target principal %q, got %q", test.config.TargetPrincipal, config.TargetPrincipal)
|
||||
}
|
||||
|
||||
// Verify impersonation lifetime field
|
||||
if config.ImpersonationLifetime != test.config.ImpersonationLifetime {
|
||||
t.Errorf("expected impersonation lifetime %v, got %v",
|
||||
test.config.ImpersonationLifetime, config.ImpersonationLifetime)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderStringConversion(t *testing.T) {
|
||||
// Test that Provider type can be converted to string
|
||||
gcsStr := string(ProviderGCS)
|
||||
if gcsStr != "gcs" {
|
||||
t.Errorf("expected 'gcs', got %q", gcsStr)
|
||||
}
|
||||
|
||||
fsStr := string(ProviderFileSystem)
|
||||
if fsStr != "filesystem" {
|
||||
t.Errorf("expected 'filesystem', got %q", fsStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderComparison(t *testing.T) {
|
||||
// Test provider equality
|
||||
if ProviderGCS == ProviderFileSystem {
|
||||
t.Error("ProviderGCS should not equal ProviderFileSystem")
|
||||
}
|
||||
|
||||
if ProviderGCS == ProviderFileSystem {
|
||||
t.Error("ProviderGCS should not equal ProviderFileSystem")
|
||||
}
|
||||
|
||||
if ProviderFileSystem == ProviderGCS {
|
||||
t.Error("ProviderFileSystem should not equal ProviderGCS")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,373 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewFileSystemStore(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config Config
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "basic config",
|
||||
config: Config{
|
||||
Bucket: "/tmp/test-storage",
|
||||
},
|
||||
expected: "/tmp/test-storage",
|
||||
},
|
||||
{
|
||||
name: "empty bucket",
|
||||
config: Config{
|
||||
Bucket: "",
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "relative path",
|
||||
config: Config{
|
||||
Bucket: "relative/path",
|
||||
},
|
||||
expected: "relative/path",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
store, err := NewFileSystemStore(test.config)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error creating filesystem store: %v", err)
|
||||
}
|
||||
|
||||
fsStore, ok := store.(*FileSystemStore)
|
||||
if !ok {
|
||||
t.Fatal("expected FileSystemStore type")
|
||||
}
|
||||
|
||||
if fsStore.basePath != test.expected {
|
||||
t.Errorf("expected base path %q, got %q", test.expected, fsStore.basePath)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemStore_Upload(t *testing.T) {
|
||||
// Create temporary directory for testing
|
||||
tempDir, err := os.MkdirTemp("", "blob-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
store := &FileSystemStore{basePath: tempDir}
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filePath string
|
||||
content string
|
||||
expectError bool
|
||||
errorCheck func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "simple file upload",
|
||||
filePath: "test.txt",
|
||||
content: "hello world",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "nested directory upload",
|
||||
filePath: "subdir/nested/file.txt",
|
||||
content: "nested content",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty file",
|
||||
filePath: "empty.txt",
|
||||
content: "",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "file with special characters",
|
||||
filePath: "special-file_123.txt",
|
||||
content: "special content",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "large content",
|
||||
filePath: "large.txt",
|
||||
content: strings.Repeat("a", 10000),
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
reader := strings.NewReader(test.content)
|
||||
err := store.Upload(ctx, reader, test.filePath)
|
||||
|
||||
if test.expectError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
if test.errorCheck != nil && !test.errorCheck(err) {
|
||||
t.Errorf("error check failed for error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify file was created and has correct content
|
||||
fullPath := filepath.Join(tempDir, test.filePath)
|
||||
data, err := os.ReadFile(fullPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read uploaded file: %v", err)
|
||||
}
|
||||
|
||||
if string(data) != test.content {
|
||||
t.Errorf("expected content %q, got %q", test.content, string(data))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemStore_Upload_DirectoryCreation(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "blob-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
store := &FileSystemStore{basePath: tempDir}
|
||||
ctx := context.Background()
|
||||
|
||||
// Test that nested directories are created automatically
|
||||
filePath := "level1/level2/level3/file.txt"
|
||||
content := "nested file content"
|
||||
reader := strings.NewReader(content)
|
||||
|
||||
err = store.Upload(ctx, reader, filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Verify the directory structure was created
|
||||
fullPath := filepath.Join(tempDir, filePath)
|
||||
if _, err := os.Stat(fullPath); os.IsNotExist(err) {
|
||||
t.Error("file was not created")
|
||||
}
|
||||
|
||||
// Verify directory exists
|
||||
dirPath := filepath.Dir(fullPath)
|
||||
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
|
||||
t.Error("directory was not created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemStore_Download(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "blob-test-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
store := &FileSystemStore{basePath: tempDir}
|
||||
ctx := context.Background()
|
||||
|
||||
// Create test files
|
||||
testFiles := map[string]string{
|
||||
"test1.txt": "content1",
|
||||
"subdir/test2.txt": "content2",
|
||||
"empty.txt": "",
|
||||
"large.txt": strings.Repeat("x", 5000),
|
||||
}
|
||||
|
||||
for filePath, content := range testFiles {
|
||||
fullPath := filepath.Join(tempDir, filePath)
|
||||
dir := filepath.Dir(fullPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
t.Fatalf("failed to create directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(fullPath, []byte(content), 0600); err != nil {
|
||||
t.Fatalf("failed to create test file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
filePath string
|
||||
expected string
|
||||
expectError bool
|
||||
errorType error
|
||||
}{
|
||||
{
|
||||
name: "existing file",
|
||||
filePath: "test1.txt",
|
||||
expected: "content1",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "nested file",
|
||||
filePath: "subdir/test2.txt",
|
||||
expected: "content2",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "empty file",
|
||||
filePath: "empty.txt",
|
||||
expected: "",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "large file",
|
||||
filePath: "large.txt",
|
||||
expected: strings.Repeat("x", 5000),
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "non-existent file",
|
||||
filePath: "nonexistent.txt",
|
||||
expectError: true,
|
||||
errorType: ErrNotFound,
|
||||
},
|
||||
{
|
||||
name: "non-existent nested file",
|
||||
filePath: "nonexistent/file.txt",
|
||||
expectError: true,
|
||||
errorType: ErrNotFound,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
reader, err := store.Download(ctx, test.filePath)
|
||||
|
||||
if test.expectError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
if test.errorType != nil && !errors.Is(err, test.errorType) {
|
||||
t.Errorf("expected error type %v, got %v", test.errorType, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
defer reader.Close()
|
||||
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read downloaded content: %v", err)
|
||||
}
|
||||
|
||||
if string(data) != test.expected {
|
||||
t.Errorf("expected content %q, got %q", test.expected, string(data))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemStore_GetSignedURL(t *testing.T) {
|
||||
store := &FileSystemStore{basePath: "/tmp"}
|
||||
ctx := context.Background()
|
||||
|
||||
// Test that GetSignedURL returns ErrNotSupported
|
||||
url, err := store.GetSignedURL(ctx, "test.txt", time.Now().Add(time.Hour))
|
||||
|
||||
if !errors.Is(err, ErrNotSupported) {
|
||||
t.Errorf("expected ErrNotSupported, got %v", err)
|
||||
}
|
||||
|
||||
if url != "" {
|
||||
t.Errorf("expected empty URL, got %q", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemStore_GetSignedURL_WithOptions(t *testing.T) {
|
||||
store := &FileSystemStore{basePath: "/tmp"}
|
||||
ctx := context.Background()
|
||||
|
||||
// Test with various options
|
||||
options := []SignURLOption{
|
||||
SignWithMethod("POST"),
|
||||
SignWithContentType("application/json"),
|
||||
SignWithHeaders([]string{"Authorization"}),
|
||||
}
|
||||
|
||||
url, err := store.GetSignedURL(ctx, "test.txt", time.Now().Add(time.Hour), options...)
|
||||
|
||||
if !errors.Is(err, ErrNotSupported) {
|
||||
t.Errorf("expected ErrNotSupported, got %v", err)
|
||||
}
|
||||
|
||||
if url != "" {
|
||||
t.Errorf("expected empty URL, got %q", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemStore_Upload_ErrorCases(t *testing.T) {
|
||||
// Test with invalid base path (read-only directory)
|
||||
if os.Getuid() != 0 { // Skip if running as root
|
||||
readOnlyDir, err := os.MkdirTemp("", "readonly-*")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp dir: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(readOnlyDir)
|
||||
|
||||
// Make directory read-only
|
||||
if err := os.Chmod(readOnlyDir, 0444); err != nil {
|
||||
t.Fatalf("failed to make directory read-only: %v", err)
|
||||
}
|
||||
|
||||
store := &FileSystemStore{basePath: readOnlyDir}
|
||||
ctx := context.Background()
|
||||
|
||||
reader := strings.NewReader("test content")
|
||||
err = store.Upload(ctx, reader, "test.txt")
|
||||
if err == nil {
|
||||
t.Error("expected error when writing to read-only directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSystemStore_Interface(t *testing.T) {
|
||||
// Test that FileSystemStore implements Store interface
|
||||
var _ Store = &FileSystemStore{}
|
||||
|
||||
// Test that NewFileSystemStore returns Store interface
|
||||
config := Config{Bucket: "/tmp"}
|
||||
store, err := NewFileSystemStore(config)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
_ = store
|
||||
}
|
||||
|
|
@ -40,6 +40,11 @@ type GCSStore struct {
|
|||
}
|
||||
|
||||
func NewGCSStore(ctx context.Context, cfg Config) (Store, error) {
|
||||
// Validate bucket name is provided
|
||||
if cfg.Bucket == "" {
|
||||
return nil, errors.New("bucket name is required")
|
||||
}
|
||||
|
||||
switch {
|
||||
case cfg.KeyPath != "":
|
||||
// Use service account key file [Development and Non-GCP environments]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package blob
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewGCSStore_InvalidConfig(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config Config
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "empty config",
|
||||
config: Config{
|
||||
Provider: ProviderGCS,
|
||||
},
|
||||
expectError: true, // Should fail without proper credentials
|
||||
},
|
||||
{
|
||||
name: "config with non-existent key file",
|
||||
config: Config{
|
||||
Provider: ProviderGCS,
|
||||
KeyPath: "/non/existent/path/key.json",
|
||||
Bucket: "test-bucket",
|
||||
},
|
||||
expectError: true, // Should fail with invalid key path
|
||||
},
|
||||
{
|
||||
name: "config with empty bucket",
|
||||
config: Config{
|
||||
Provider: ProviderGCS,
|
||||
KeyPath: "/tmp/fake-key.json",
|
||||
Bucket: "",
|
||||
},
|
||||
expectError: true, // Should fail with empty bucket
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
store, err := NewGCSStore(ctx, test.config)
|
||||
|
||||
if test.expectError {
|
||||
if err == nil {
|
||||
t.Error("expected error but got none")
|
||||
}
|
||||
if store != nil {
|
||||
t.Error("expected nil store on error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if store == nil {
|
||||
t.Error("expected non-nil store")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGCSStore_ConfigValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config Config
|
||||
}{
|
||||
{
|
||||
name: "config with service account key",
|
||||
config: Config{
|
||||
Provider: ProviderGCS,
|
||||
KeyPath: "/path/to/key.json",
|
||||
Bucket: "test-bucket",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with impersonation",
|
||||
config: Config{
|
||||
Provider: ProviderGCS,
|
||||
Bucket: "test-bucket",
|
||||
TargetPrincipal: "service-account@project.iam.gserviceaccount.com",
|
||||
ImpersonationLifetime: time.Hour,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "config with zero impersonation lifetime",
|
||||
config: Config{
|
||||
Provider: ProviderGCS,
|
||||
Bucket: "test-bucket",
|
||||
TargetPrincipal: "service-account@project.iam.gserviceaccount.com",
|
||||
ImpersonationLifetime: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
// Test that config fields are properly set
|
||||
config := test.config
|
||||
|
||||
if config.Provider != ProviderGCS {
|
||||
t.Errorf("expected provider %q, got %q", ProviderGCS, config.Provider)
|
||||
}
|
||||
|
||||
// Verify other fields are accessible
|
||||
_ = config.Bucket
|
||||
_ = config.KeyPath
|
||||
_ = config.TargetPrincipal
|
||||
_ = config.ImpersonationLifetime
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGCSStore_Interface(_ *testing.T) {
|
||||
// Test that GCSStore would implement Store interface
|
||||
// Note: We can't actually create a GCSStore without valid GCS credentials,
|
||||
// but we can verify the interface compliance at compile time
|
||||
|
||||
// This will fail to compile if GCSStore doesn't implement Store
|
||||
var _ Store = (*GCSStore)(nil)
|
||||
}
|
||||
|
||||
func TestGCSStore_DefaultScope(t *testing.T) {
|
||||
expectedScope := "https://www.googleapis.com/auth/cloud-platform"
|
||||
if defaultScope != expectedScope {
|
||||
t.Errorf("expected default scope %q, got %q", expectedScope, defaultScope)
|
||||
}
|
||||
}
|
||||
|
||||
// Note: More comprehensive tests for GCSStore would require:
|
||||
// 1. Mock GCS client or test environment
|
||||
// 2. Valid GCS credentials for integration tests
|
||||
// 3. Test bucket setup
|
||||
// These tests focus on the parts that can be tested without external dependencies.
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package blob
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "ErrNotFound",
|
||||
err: ErrNotFound,
|
||||
expected: "resource not found",
|
||||
},
|
||||
{
|
||||
name: "ErrNotSupported",
|
||||
err: ErrNotSupported,
|
||||
expected: "not supported",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := test.err.Error(); got != test.expected {
|
||||
t.Errorf("expected error message %q, got %q", test.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorsAreDistinct(t *testing.T) {
|
||||
if errors.Is(ErrNotFound, ErrNotSupported) {
|
||||
t.Error("ErrNotFound should not be the same as ErrNotSupported")
|
||||
}
|
||||
|
||||
if errors.Is(ErrNotSupported, ErrNotFound) {
|
||||
t.Error("ErrNotSupported should not be the same as ErrNotFound")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorsCanBeWrapped(t *testing.T) {
|
||||
wrappedNotFound := errors.New("wrapped: " + ErrNotFound.Error())
|
||||
wrappedNotSupported := errors.New("wrapped: " + ErrNotSupported.Error())
|
||||
|
||||
if wrappedNotFound.Error() != "wrapped: resource not found" {
|
||||
t.Errorf("unexpected wrapped error message: %s", wrappedNotFound.Error())
|
||||
}
|
||||
|
||||
if wrappedNotSupported.Error() != "wrapped: not supported" {
|
||||
t.Errorf("unexpected wrapped error message: %s", wrappedNotSupported.Error())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,337 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package blob
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSignURLConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config SignURLConfig
|
||||
}{
|
||||
{
|
||||
name: "empty config",
|
||||
config: SignURLConfig{},
|
||||
},
|
||||
{
|
||||
name: "full config",
|
||||
config: SignURLConfig{
|
||||
Method: "POST",
|
||||
ContentType: "application/json",
|
||||
Headers: []string{"Authorization", "X-Custom-Header"},
|
||||
QueryParameters: url.Values{"param1": []string{"value1"}, "param2": []string{"value2"}},
|
||||
Insecure: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
config := test.config
|
||||
|
||||
if config.Method != test.config.Method {
|
||||
t.Errorf("expected method %q, got %q", test.config.Method, config.Method)
|
||||
}
|
||||
|
||||
if config.ContentType != test.config.ContentType {
|
||||
t.Errorf("expected content type %q, got %q", test.config.ContentType, config.ContentType)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(config.Headers, test.config.Headers) {
|
||||
t.Errorf("expected headers %v, got %v", test.config.Headers, config.Headers)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(config.QueryParameters, test.config.QueryParameters) {
|
||||
t.Errorf("expected query parameters %v, got %v", test.config.QueryParameters, config.QueryParameters)
|
||||
}
|
||||
|
||||
if config.Insecure != test.config.Insecure {
|
||||
t.Errorf("expected insecure %v, got %v", test.config.Insecure, config.Insecure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithMethod(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "GET method",
|
||||
method: "GET",
|
||||
expected: "GET",
|
||||
},
|
||||
{
|
||||
name: "POST method",
|
||||
method: "POST",
|
||||
expected: "POST",
|
||||
},
|
||||
{
|
||||
name: "PUT method",
|
||||
method: "PUT",
|
||||
expected: "PUT",
|
||||
},
|
||||
{
|
||||
name: "DELETE method",
|
||||
method: "DELETE",
|
||||
expected: "DELETE",
|
||||
},
|
||||
{
|
||||
name: "empty method",
|
||||
method: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
config := &SignURLConfig{}
|
||||
option := SignWithMethod(test.method)
|
||||
option.Apply(config)
|
||||
|
||||
if config.Method != test.expected {
|
||||
t.Errorf("expected method %q, got %q", test.expected, config.Method)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithContentType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
contentType string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "JSON content type",
|
||||
contentType: "application/json",
|
||||
expected: "application/json",
|
||||
},
|
||||
{
|
||||
name: "XML content type",
|
||||
contentType: "application/xml",
|
||||
expected: "application/xml",
|
||||
},
|
||||
{
|
||||
name: "plain text content type",
|
||||
contentType: "text/plain",
|
||||
expected: "text/plain",
|
||||
},
|
||||
{
|
||||
name: "empty content type",
|
||||
contentType: "",
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
config := &SignURLConfig{}
|
||||
option := SignWithContentType(test.contentType)
|
||||
option.Apply(config)
|
||||
|
||||
if config.ContentType != test.expected {
|
||||
t.Errorf("expected content type %q, got %q", test.expected, config.ContentType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithHeaders(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
headers []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "single header",
|
||||
headers: []string{"Authorization"},
|
||||
expected: []string{"Authorization"},
|
||||
},
|
||||
{
|
||||
name: "multiple headers",
|
||||
headers: []string{"Authorization", "X-Custom-Header", "Content-Type"},
|
||||
expected: []string{"Authorization", "X-Custom-Header", "Content-Type"},
|
||||
},
|
||||
{
|
||||
name: "empty headers",
|
||||
headers: []string{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "nil headers",
|
||||
headers: nil,
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
config := &SignURLConfig{}
|
||||
option := SignWithHeaders(test.headers)
|
||||
option.Apply(config)
|
||||
|
||||
if !reflect.DeepEqual(config.Headers, test.expected) {
|
||||
t.Errorf("expected headers %v, got %v", test.expected, config.Headers)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithQueryParameters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
params url.Values
|
||||
expected url.Values
|
||||
}{
|
||||
{
|
||||
name: "single parameter",
|
||||
params: url.Values{"key": []string{"value"}},
|
||||
expected: url.Values{"key": []string{"value"}},
|
||||
},
|
||||
{
|
||||
name: "multiple parameters",
|
||||
params: url.Values{"key1": []string{"value1"}, "key2": []string{"value2", "value3"}},
|
||||
expected: url.Values{"key1": []string{"value1"}, "key2": []string{"value2", "value3"}},
|
||||
},
|
||||
{
|
||||
name: "empty parameters",
|
||||
params: url.Values{},
|
||||
expected: url.Values{},
|
||||
},
|
||||
{
|
||||
name: "nil parameters",
|
||||
params: nil,
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
config := &SignURLConfig{}
|
||||
option := SignWithQueryParameters(test.params)
|
||||
option.Apply(config)
|
||||
|
||||
if !reflect.DeepEqual(config.QueryParameters, test.expected) {
|
||||
t.Errorf("expected query parameters %v, got %v", test.expected, config.QueryParameters)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignWithInsecure(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
insecure bool
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "insecure true",
|
||||
insecure: true,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "insecure false",
|
||||
insecure: false,
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
config := &SignURLConfig{}
|
||||
option := SignWithInsecure(test.insecure)
|
||||
option.Apply(config)
|
||||
|
||||
if config.Insecure != test.expected {
|
||||
t.Errorf("expected insecure %v, got %v", test.expected, config.Insecure)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignedURLConfigFunc(t *testing.T) {
|
||||
// Test that SignedURLConfigFunc implements SignURLOption interface
|
||||
var _ SignURLOption = SignedURLConfigFunc(func(_ *SignURLConfig) {})
|
||||
|
||||
// Test custom function
|
||||
customFunc := SignedURLConfigFunc(func(opts *SignURLConfig) {
|
||||
opts.Method = "CUSTOM"
|
||||
opts.ContentType = "custom/type"
|
||||
opts.Insecure = true
|
||||
})
|
||||
|
||||
config := &SignURLConfig{}
|
||||
customFunc.Apply(config)
|
||||
|
||||
if config.Method != "CUSTOM" {
|
||||
t.Errorf("expected method 'CUSTOM', got %q", config.Method)
|
||||
}
|
||||
|
||||
if config.ContentType != "custom/type" {
|
||||
t.Errorf("expected content type 'custom/type', got %q", config.ContentType)
|
||||
}
|
||||
|
||||
if !config.Insecure {
|
||||
t.Error("expected insecure to be true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleOptions(t *testing.T) {
|
||||
config := &SignURLConfig{}
|
||||
|
||||
// Apply multiple options
|
||||
options := []SignURLOption{
|
||||
SignWithMethod("POST"),
|
||||
SignWithContentType("application/json"),
|
||||
SignWithHeaders([]string{"Authorization", "X-Custom"}),
|
||||
SignWithQueryParameters(url.Values{"test": []string{"value"}}),
|
||||
SignWithInsecure(true),
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option.Apply(config)
|
||||
}
|
||||
|
||||
// Verify all options were applied
|
||||
if config.Method != http.MethodPost {
|
||||
t.Errorf("expected method 'POST', got %q", config.Method)
|
||||
}
|
||||
|
||||
if config.ContentType != "application/json" {
|
||||
t.Errorf("expected content type 'application/json', got %q", config.ContentType)
|
||||
}
|
||||
|
||||
expectedHeaders := []string{"Authorization", "X-Custom"}
|
||||
if !reflect.DeepEqual(config.Headers, expectedHeaders) {
|
||||
t.Errorf("expected headers %v, got %v", expectedHeaders, config.Headers)
|
||||
}
|
||||
|
||||
expectedParams := url.Values{"test": []string{"value"}}
|
||||
if !reflect.DeepEqual(config.QueryParameters, expectedParams) {
|
||||
t.Errorf("expected query parameters %v, got %v", expectedParams, config.QueryParameters)
|
||||
}
|
||||
|
||||
if !config.Insecure {
|
||||
t.Error("expected insecure to be true")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testValue = "value"
|
||||
|
||||
type contextKey string
|
||||
|
||||
type mockGetter struct {
|
||||
findFunc func(ctx context.Context, key string) (string, error)
|
||||
}
|
||||
|
||||
func (m *mockGetter) Find(ctx context.Context, key string) (string, error) {
|
||||
return m.findFunc(ctx, key)
|
||||
}
|
||||
|
||||
func TestNewNoCache(t *testing.T) {
|
||||
getter := &mockGetter{
|
||||
findFunc: func(ctx context.Context, key string) (string, error) {
|
||||
return testValue, nil
|
||||
},
|
||||
}
|
||||
|
||||
cache := NewNoCache[string, string](getter)
|
||||
if cache.getter == nil {
|
||||
t.Error("expected getter to be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCache_Stats(t *testing.T) {
|
||||
getter := &mockGetter{
|
||||
findFunc: func(ctx context.Context, key string) (string, error) {
|
||||
return testValue, nil
|
||||
},
|
||||
}
|
||||
|
||||
cache := NewNoCache[string, string](getter)
|
||||
hits, misses := cache.Stats()
|
||||
|
||||
if hits != 0 {
|
||||
t.Errorf("expected hits to be 0, got %d", hits)
|
||||
}
|
||||
if misses != 0 {
|
||||
t.Errorf("expected misses to be 0, got %d", misses)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoCache_Get(t *testing.T) {
|
||||
t.Run("successful get", func(t *testing.T) {
|
||||
getter := &mockGetter{
|
||||
findFunc: func(ctx context.Context, key string) (string, error) {
|
||||
if key == "test-key" {
|
||||
return "test-value", nil
|
||||
}
|
||||
return "", errors.New("not found")
|
||||
},
|
||||
}
|
||||
|
||||
cache := NewNoCache[string, string](getter)
|
||||
value, err := cache.Get(context.Background(), "test-key")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if value != "test-value" {
|
||||
t.Errorf("expected value to be 'test-value', got '%s'", value)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get with error", func(t *testing.T) {
|
||||
expectedErr := errors.New("getter error")
|
||||
getter := &mockGetter{
|
||||
findFunc: func(ctx context.Context, key string) (string, error) {
|
||||
return "", expectedErr
|
||||
},
|
||||
}
|
||||
|
||||
cache := NewNoCache[string, string](getter)
|
||||
_, err := cache.Get(context.Background(), "test-key")
|
||||
|
||||
if !errors.Is(err, expectedErr) {
|
||||
t.Errorf("expected error to be %v, got %v", expectedErr, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple gets call getter each time", func(t *testing.T) {
|
||||
callCount := 0
|
||||
getter := &mockGetter{
|
||||
findFunc: func(ctx context.Context, key string) (string, error) {
|
||||
callCount++
|
||||
return testValue, nil
|
||||
},
|
||||
}
|
||||
|
||||
cache := NewNoCache[string, string](getter)
|
||||
|
||||
// Call Get multiple times with the same key
|
||||
_, _ = cache.Get(context.Background(), "key")
|
||||
_, _ = cache.Get(context.Background(), "key")
|
||||
_, _ = cache.Get(context.Background(), "key")
|
||||
|
||||
if callCount != 3 {
|
||||
t.Errorf("expected getter to be called 3 times, got %d", callCount)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get with context", func(t *testing.T) {
|
||||
var receivedCtx context.Context
|
||||
getter := &mockGetter{
|
||||
findFunc: func(ctx context.Context, key string) (string, error) {
|
||||
receivedCtx = ctx
|
||||
return testValue, nil
|
||||
},
|
||||
}
|
||||
|
||||
cache := NewNoCache[string, string](getter)
|
||||
ctx := context.WithValue(context.Background(), contextKey("test-key"), "test-value")
|
||||
_, _ = cache.Get(ctx, "key")
|
||||
|
||||
if receivedCtx != ctx {
|
||||
t.Error("expected context to be passed to getter")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNoCache_Evict(t *testing.T) {
|
||||
t.Run("evict does nothing", func(t *testing.T) {
|
||||
getter := &mockGetter{
|
||||
findFunc: func(ctx context.Context, key string) (string, error) {
|
||||
return testValue, nil
|
||||
},
|
||||
}
|
||||
|
||||
cache := NewNoCache[string, string](getter)
|
||||
|
||||
// Evict should not panic or cause any issues
|
||||
cache.Evict(context.Background(), "key")
|
||||
|
||||
// Verify we can still get values after evict
|
||||
value, err := cache.Get(context.Background(), "key")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if value != "value" {
|
||||
t.Errorf("expected value to be 'value', got '%s'", value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNoCache_WithIntegerTypes(t *testing.T) {
|
||||
getter := &mockGetter{}
|
||||
|
||||
type intGetter struct{}
|
||||
|
||||
intGetterImpl := intGetter{}
|
||||
|
||||
cache := NewNoCache[int, int](struct {
|
||||
Getter[int, int]
|
||||
}{
|
||||
Getter: struct{ Getter[int, int] }{
|
||||
Getter: nil,
|
||||
}.Getter,
|
||||
})
|
||||
|
||||
// Just verify the cache can be created with different types
|
||||
_ = cache
|
||||
_ = getter
|
||||
_ = intGetterImpl
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetArguments(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
osArgs []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "regular command with args",
|
||||
osArgs: []string{"/path/to/gitness", "server", "start"},
|
||||
expected: []string{"server", "start"},
|
||||
},
|
||||
{
|
||||
name: "command with no args",
|
||||
osArgs: []string{"/path/to/gitness"},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "command with single arg",
|
||||
osArgs: []string{"/path/to/gitness", "version"},
|
||||
expected: []string{"version"},
|
||||
},
|
||||
{
|
||||
name: "command with multiple args",
|
||||
osArgs: []string{"/path/to/gitness", "repo", "create", "myrepo"},
|
||||
expected: []string{"repo", "create", "myrepo"},
|
||||
},
|
||||
{
|
||||
name: "command with flags",
|
||||
osArgs: []string{"/path/to/gitness", "server", "--port", "8080"},
|
||||
expected: []string{"server", "--port", "8080"},
|
||||
},
|
||||
{
|
||||
name: "command with mixed args and flags",
|
||||
osArgs: []string{"/path/to/gitness", "repo", "create", "--private", "myrepo"},
|
||||
expected: []string{"repo", "create", "--private", "myrepo"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Save original os.Args
|
||||
originalArgs := os.Args
|
||||
defer func() {
|
||||
os.Args = originalArgs
|
||||
}()
|
||||
|
||||
// Set test args
|
||||
os.Args = tt.osArgs
|
||||
|
||||
// Call GetArguments
|
||||
result := GetArguments()
|
||||
|
||||
// Verify result
|
||||
assert.Equal(t, tt.expected, result, "arguments should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetArguments_PreservesOrder(t *testing.T) {
|
||||
// Save original os.Args
|
||||
originalArgs := os.Args
|
||||
defer func() {
|
||||
os.Args = originalArgs
|
||||
}()
|
||||
|
||||
// Set test args with specific order
|
||||
os.Args = []string{"/path/to/gitness", "first", "second", "third"}
|
||||
|
||||
// Call GetArguments
|
||||
result := GetArguments()
|
||||
|
||||
// Verify order is preserved
|
||||
expected := []string{"first", "second", "third"}
|
||||
assert.Equal(t, expected, result, "argument order should be preserved")
|
||||
}
|
||||
|
||||
func TestGetArguments_ReturnsSlice(t *testing.T) {
|
||||
// Save original os.Args
|
||||
originalArgs := os.Args
|
||||
defer func() {
|
||||
os.Args = originalArgs
|
||||
}()
|
||||
|
||||
// Set test args
|
||||
os.Args = []string{"/path/to/gitness", "arg1", "arg2"}
|
||||
|
||||
// Call GetArguments
|
||||
result := GetArguments()
|
||||
|
||||
// Verify result is a slice
|
||||
assert.NotNil(t, result, "result should not be nil")
|
||||
assert.IsType(t, []string{}, result, "result should be a string slice")
|
||||
}
|
||||
|
||||
func TestGetArguments_EmptyArgs(t *testing.T) {
|
||||
// Save original os.Args
|
||||
originalArgs := os.Args
|
||||
defer func() {
|
||||
os.Args = originalArgs
|
||||
}()
|
||||
|
||||
// Set test args with only command
|
||||
os.Args = []string{"/path/to/gitness"}
|
||||
|
||||
// Call GetArguments
|
||||
result := GetArguments()
|
||||
|
||||
// Verify result is empty slice
|
||||
assert.NotNil(t, result, "result should not be nil")
|
||||
assert.Empty(t, result, "result should be empty")
|
||||
assert.Equal(t, 0, len(result), "result length should be 0")
|
||||
}
|
||||
|
|
@ -0,0 +1,145 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package contextutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWithNewTimeout(t *testing.T) {
|
||||
t.Run("creates new context with timeout", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
timeout := 100 * time.Millisecond
|
||||
|
||||
newCtx, cancel := WithNewTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
if newCtx == nil {
|
||||
t.Fatal("expected non-nil context")
|
||||
}
|
||||
|
||||
deadline, ok := newCtx.Deadline()
|
||||
if !ok {
|
||||
t.Fatal("expected context to have deadline")
|
||||
}
|
||||
|
||||
expectedDeadline := time.Now().Add(timeout)
|
||||
if deadline.After(expectedDeadline.Add(50 * time.Millisecond)) {
|
||||
t.Errorf("deadline is too far in the future: got %v, expected around %v", deadline, expectedDeadline)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("new context is not canceled when parent is canceled", func(t *testing.T) {
|
||||
parentCtx, parentCancel := context.WithCancel(context.Background())
|
||||
timeout := 1 * time.Second
|
||||
|
||||
newCtx, cancel := WithNewTimeout(parentCtx, timeout)
|
||||
defer cancel()
|
||||
|
||||
// Cancel parent context
|
||||
parentCancel()
|
||||
|
||||
// Give it a moment to propagate
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
// New context should not be canceled
|
||||
select {
|
||||
case <-newCtx.Done():
|
||||
t.Fatal("new context should not be canceled when parent is canceled")
|
||||
default:
|
||||
// Expected: context is not canceled
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("new context times out after specified duration", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
timeout := 50 * time.Millisecond
|
||||
|
||||
newCtx, cancel := WithNewTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
select {
|
||||
case <-newCtx.Done():
|
||||
t.Fatal("context should not be done immediately")
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
// Expected: context is not done yet
|
||||
}
|
||||
|
||||
// Wait for timeout
|
||||
select {
|
||||
case <-newCtx.Done():
|
||||
// Expected: context is done after timeout
|
||||
if newCtx.Err() != context.DeadlineExceeded {
|
||||
t.Errorf("expected DeadlineExceeded error, got %v", newCtx.Err())
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("context should have timed out")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("cancel function works correctly", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
timeout := 1 * time.Second
|
||||
|
||||
newCtx, cancel := WithNewTimeout(ctx, timeout)
|
||||
|
||||
// Cancel immediately
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case <-newCtx.Done():
|
||||
// Expected: context is canceled
|
||||
if newCtx.Err() != context.Canceled {
|
||||
t.Errorf("expected Canceled error, got %v", newCtx.Err())
|
||||
}
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("context should have been canceled")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero timeout", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
timeout := 0 * time.Second
|
||||
|
||||
newCtx, cancel := WithNewTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
// Context with zero timeout should be immediately done
|
||||
select {
|
||||
case <-newCtx.Done():
|
||||
// Expected: context is done
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("context with zero timeout should be immediately done")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative timeout", func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
timeout := -1 * time.Second
|
||||
|
||||
newCtx, cancel := WithNewTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
// Context with negative timeout should be immediately done
|
||||
select {
|
||||
case <-newCtx.Done():
|
||||
// Expected: context is done
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
t.Fatal("context with negative timeout should be immediately done")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,362 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateHMACSHA256(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
key []byte
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple data and key",
|
||||
data: []byte("hello world"),
|
||||
key: []byte("secret"),
|
||||
expected: "734cc62f32841568f45715aeb9f4d7891324e6d948e4c6c60c0621cdac48623a",
|
||||
},
|
||||
{
|
||||
name: "empty data",
|
||||
data: []byte(""),
|
||||
key: []byte("secret"),
|
||||
expected: "f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169",
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
data: []byte("hello world"),
|
||||
key: []byte(""),
|
||||
expected: "", // We'll calculate this dynamically
|
||||
},
|
||||
{
|
||||
name: "both empty",
|
||||
data: []byte(""),
|
||||
key: []byte(""),
|
||||
expected: "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad",
|
||||
},
|
||||
{
|
||||
name: "long data",
|
||||
data: []byte(strings.Repeat("a", 1000)),
|
||||
key: []byte("secret"),
|
||||
expected: "", // We'll calculate this dynamically
|
||||
},
|
||||
{
|
||||
name: "long key",
|
||||
data: []byte("hello"),
|
||||
key: []byte(strings.Repeat("k", 100)),
|
||||
expected: "", // We'll calculate this dynamically
|
||||
},
|
||||
{
|
||||
name: "binary data",
|
||||
data: []byte{0x00, 0x01, 0x02, 0x03, 0xFF, 0xFE, 0xFD},
|
||||
key: []byte("binary"),
|
||||
expected: "", // We'll calculate this dynamically
|
||||
},
|
||||
{
|
||||
name: "unicode data",
|
||||
data: []byte("Hello 世界 🌍"),
|
||||
key: []byte("unicode"),
|
||||
expected: "", // We'll calculate this dynamically
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := GenerateHMACSHA256(tt.data, tt.key)
|
||||
if err != nil {
|
||||
t.Errorf("GenerateHMACSHA256() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// For cases where we don't have pre-calculated expected values,
|
||||
// verify the result by computing it manually
|
||||
if tt.expected == "" {
|
||||
h := hmac.New(sha256.New, tt.key)
|
||||
h.Write(tt.data)
|
||||
expected := hex.EncodeToString(h.Sum(nil))
|
||||
if result != expected {
|
||||
t.Errorf("GenerateHMACSHA256() = %v, want %v", result, expected)
|
||||
}
|
||||
} else if result != tt.expected {
|
||||
t.Errorf("GenerateHMACSHA256() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
|
||||
// Verify the result is valid hex
|
||||
_, err = hex.DecodeString(result)
|
||||
if err != nil {
|
||||
t.Errorf("GenerateHMACSHA256() returned invalid hex: %v", err)
|
||||
}
|
||||
|
||||
// Verify the result has the correct length for SHA256 (64 hex characters)
|
||||
if len(result) != 64 {
|
||||
t.Errorf("GenerateHMACSHA256() returned wrong length: got %d, want 64", len(result))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHMACSHA256Consistency(t *testing.T) {
|
||||
// Test that the same input always produces the same output
|
||||
data := []byte("test data")
|
||||
key := []byte("test key")
|
||||
|
||||
result1, err1 := GenerateHMACSHA256(data, key)
|
||||
if err1 != nil {
|
||||
t.Fatalf("First call failed: %v", err1)
|
||||
}
|
||||
|
||||
result2, err2 := GenerateHMACSHA256(data, key)
|
||||
if err2 != nil {
|
||||
t.Fatalf("Second call failed: %v", err2)
|
||||
}
|
||||
|
||||
if result1 != result2 {
|
||||
t.Errorf("GenerateHMACSHA256() is not consistent: %v != %v", result1, result2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHMACSHA256DifferentInputs(t *testing.T) {
|
||||
// Test that different inputs produce different outputs
|
||||
key := []byte("secret")
|
||||
|
||||
result1, _ := GenerateHMACSHA256([]byte("data1"), key)
|
||||
result2, _ := GenerateHMACSHA256([]byte("data2"), key)
|
||||
|
||||
if result1 == result2 {
|
||||
t.Error("GenerateHMACSHA256() should produce different results for different inputs")
|
||||
}
|
||||
|
||||
// Test different keys
|
||||
data := []byte("same data")
|
||||
result3, _ := GenerateHMACSHA256(data, []byte("key1"))
|
||||
result4, _ := GenerateHMACSHA256(data, []byte("key2"))
|
||||
|
||||
if result3 == result4 {
|
||||
t.Error("GenerateHMACSHA256() should produce different results for different keys")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsShaEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key1 string
|
||||
key2 string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "identical strings",
|
||||
key1: "hello",
|
||||
key2: "hello",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "different strings",
|
||||
key1: "hello",
|
||||
key2: "world",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "empty strings",
|
||||
key1: "",
|
||||
key2: "",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "one empty string",
|
||||
key1: "hello",
|
||||
key2: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "case sensitive",
|
||||
key1: "Hello",
|
||||
key2: "hello",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "whitespace differences",
|
||||
key1: "hello ",
|
||||
key2: "hello",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "long identical strings",
|
||||
key1: strings.Repeat("a", 1000),
|
||||
key2: strings.Repeat("a", 1000),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "long different strings",
|
||||
key1: strings.Repeat("a", 1000),
|
||||
key2: strings.Repeat("b", 1000),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "unicode strings identical",
|
||||
key1: "Hello 世界 🌍",
|
||||
key2: "Hello 世界 🌍",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "unicode strings different",
|
||||
key1: "Hello 世界 🌍",
|
||||
key2: "Hello 世界 🌎",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "hex strings identical",
|
||||
key1: "deadbeef",
|
||||
key2: "deadbeef",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "hex strings different",
|
||||
key1: "deadbeef",
|
||||
key2: "deadbeee",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := IsShaEqual(tt.key1, tt.key2)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsShaEqual(%q, %q) = %v, want %v", tt.key1, tt.key2, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsShaEqualTimingSafety(t *testing.T) {
|
||||
// Test that IsShaEqual uses constant-time comparison
|
||||
// This is important for security to prevent timing attacks
|
||||
|
||||
// Create two strings that differ only in the last character
|
||||
base := strings.Repeat("a", 100)
|
||||
key1 := base + "1"
|
||||
key2 := base + "2"
|
||||
|
||||
// The function should return false
|
||||
result := IsShaEqual(key1, key2)
|
||||
if result {
|
||||
t.Error("IsShaEqual() should return false for different strings")
|
||||
}
|
||||
|
||||
// Test with strings of different lengths
|
||||
result2 := IsShaEqual("short", "much longer string")
|
||||
if result2 {
|
||||
t.Error("IsShaEqual() should return false for strings of different lengths")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHMACSHA256WithRealWorldData(t *testing.T) {
|
||||
// Test with realistic data that might be used in practice
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
key []byte
|
||||
}{
|
||||
{
|
||||
name: "JSON payload",
|
||||
data: []byte(`{"user_id": 123, "action": "login", "timestamp": "2023-01-01T00:00:00Z"}`),
|
||||
key: []byte("webhook-secret-key"),
|
||||
},
|
||||
{
|
||||
name: "URL parameters",
|
||||
data: []byte("user=john&action=login×tamp=1672531200"),
|
||||
key: []byte("api-secret"),
|
||||
},
|
||||
{
|
||||
name: "Base64 data",
|
||||
data: []byte("SGVsbG8gV29ybGQ="),
|
||||
key: []byte("base64-key"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := GenerateHMACSHA256(tt.data, tt.key)
|
||||
if err != nil {
|
||||
t.Errorf("GenerateHMACSHA256() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the result is a valid SHA256 hash
|
||||
if len(result) != 64 {
|
||||
t.Errorf("Expected 64 character hash, got %d", len(result))
|
||||
}
|
||||
|
||||
// Verify it's valid hex
|
||||
_, err = hex.DecodeString(result)
|
||||
if err != nil {
|
||||
t.Errorf("Result is not valid hex: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests.
|
||||
func BenchmarkGenerateHMACSHA256(b *testing.B) {
|
||||
data := []byte("benchmark data")
|
||||
key := []byte("benchmark key")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GenerateHMACSHA256(data, key)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGenerateHMACSHA256LargeData(b *testing.B) {
|
||||
data := []byte(strings.Repeat("a", 10000))
|
||||
key := []byte("benchmark key")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := GenerateHMACSHA256(data, key)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsShaEqual(b *testing.B) {
|
||||
key1 := "benchmark string for comparison"
|
||||
key2 := "benchmark string for comparison"
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IsShaEqual(key1, key2)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsShaEqualLarge(b *testing.B) {
|
||||
key1 := strings.Repeat("a", 1000)
|
||||
key2 := strings.Repeat("a", 1000)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IsShaEqual(key1, key2)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,307 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const testKey32Bytes = "12345678901234567890123456789012"
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
key string
|
||||
compat bool
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid 32-byte key",
|
||||
key: testKey32Bytes,
|
||||
compat: false,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid 32-byte key with compat mode",
|
||||
key: testKey32Bytes,
|
||||
compat: true,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid key - too short",
|
||||
key: "short",
|
||||
compat: false,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid key - too long",
|
||||
key: "123456789012345678901234567890123",
|
||||
compat: false,
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty key",
|
||||
key: "",
|
||||
compat: false,
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
encrypter, err := New(tt.key, tt.compat)
|
||||
if tt.expectErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error but got none")
|
||||
}
|
||||
if encrypter != nil {
|
||||
t.Errorf("expected nil encrypter but got %v", encrypter)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if encrypter == nil {
|
||||
t.Errorf("expected encrypter but got nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAesgcmEncryptDecrypt(t *testing.T) {
|
||||
key := testKey32Bytes
|
||||
encrypter, err := New(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create encrypter: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
plaintext string
|
||||
}{
|
||||
{
|
||||
name: "simple text",
|
||||
plaintext: "hello world",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
plaintext: "",
|
||||
},
|
||||
{
|
||||
name: "long text",
|
||||
plaintext: strings.Repeat("a", 1000),
|
||||
},
|
||||
{
|
||||
name: "special characters",
|
||||
plaintext: "!@#$%^&*()_+-=[]{}|;':\",./<>?",
|
||||
},
|
||||
{
|
||||
name: "unicode text",
|
||||
plaintext: "Hello 世界 🌍",
|
||||
},
|
||||
{
|
||||
name: "newlines and tabs",
|
||||
plaintext: "line1\nline2\tline3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Encrypt
|
||||
ciphertext, err := encrypter.Encrypt(tt.plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("encryption failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify ciphertext is not empty
|
||||
if len(ciphertext) == 0 {
|
||||
t.Errorf("ciphertext is empty")
|
||||
}
|
||||
|
||||
// Decrypt
|
||||
decrypted, err := encrypter.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("decryption failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify decrypted matches original
|
||||
if decrypted != tt.plaintext {
|
||||
t.Errorf("decrypted text does not match original\nexpected: %q\ngot: %q", tt.plaintext, decrypted)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAesgcmEncryptUniqueness(t *testing.T) {
|
||||
key := testKey32Bytes
|
||||
encrypter, err := New(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create encrypter: %v", err)
|
||||
}
|
||||
|
||||
plaintext := "test message"
|
||||
|
||||
// Encrypt the same plaintext multiple times
|
||||
ciphertext1, err := encrypter.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("encryption 1 failed: %v", err)
|
||||
}
|
||||
|
||||
ciphertext2, err := encrypter.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("encryption 2 failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify ciphertexts are different (due to random nonce)
|
||||
if string(ciphertext1) == string(ciphertext2) {
|
||||
t.Errorf("ciphertexts should be different due to random nonce")
|
||||
}
|
||||
|
||||
// But both should decrypt to the same plaintext
|
||||
decrypted1, err := encrypter.Decrypt(ciphertext1)
|
||||
if err != nil {
|
||||
t.Fatalf("decryption 1 failed: %v", err)
|
||||
}
|
||||
|
||||
decrypted2, err := encrypter.Decrypt(ciphertext2)
|
||||
if err != nil {
|
||||
t.Fatalf("decryption 2 failed: %v", err)
|
||||
}
|
||||
|
||||
if decrypted1 != plaintext || decrypted2 != plaintext {
|
||||
t.Errorf("decrypted texts should match original plaintext")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAesgcmDecryptInvalidCiphertext(t *testing.T) {
|
||||
key := testKey32Bytes
|
||||
encrypter, err := New(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create encrypter: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ciphertext []byte
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty ciphertext",
|
||||
ciphertext: []byte{},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "too short ciphertext",
|
||||
ciphertext: []byte{1, 2, 3},
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "corrupted ciphertext",
|
||||
ciphertext: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16},
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := encrypter.Decrypt(tt.ciphertext)
|
||||
if tt.expectErr && err == nil {
|
||||
t.Errorf("expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAesgcmCompatMode(t *testing.T) {
|
||||
key := testKey32Bytes
|
||||
encrypter, err := New(key, true)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create encrypter: %v", err)
|
||||
}
|
||||
|
||||
aesgcm, _ := encrypter.(*Aesgcm)
|
||||
if !aesgcm.Compat {
|
||||
t.Errorf("compat mode should be enabled")
|
||||
}
|
||||
|
||||
// Test that invalid ciphertext returns the ciphertext as plaintext in compat mode
|
||||
invalidCiphertext := []byte("not encrypted data")
|
||||
decrypted, err := encrypter.Decrypt(invalidCiphertext)
|
||||
if err != nil {
|
||||
t.Errorf("compat mode should not return error for invalid ciphertext: %v", err)
|
||||
}
|
||||
if decrypted != string(invalidCiphertext) {
|
||||
t.Errorf(
|
||||
"compat mode should return ciphertext as plaintext\nexpected: %q\ngot: %q",
|
||||
string(invalidCiphertext),
|
||||
decrypted,
|
||||
)
|
||||
}
|
||||
|
||||
// Test that valid encrypted data still works in compat mode
|
||||
plaintext := "test message"
|
||||
ciphertext, err := encrypter.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("encryption failed: %v", err)
|
||||
}
|
||||
|
||||
decrypted, err = encrypter.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("decryption failed: %v", err)
|
||||
}
|
||||
|
||||
if decrypted != plaintext {
|
||||
t.Errorf("decrypted text does not match original\nexpected: %q\ngot: %q", plaintext, decrypted)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAesgcmCompatModeShortCiphertext(t *testing.T) {
|
||||
key := testKey32Bytes
|
||||
encrypter, err := New(key, true)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create encrypter: %v", err)
|
||||
}
|
||||
|
||||
// Test with very short ciphertext (less than nonce size)
|
||||
shortCiphertext := []byte("short")
|
||||
decrypted, err := encrypter.Decrypt(shortCiphertext)
|
||||
if err != nil {
|
||||
t.Errorf("compat mode should not return error for short ciphertext: %v", err)
|
||||
}
|
||||
if decrypted != string(shortCiphertext) {
|
||||
t.Errorf(
|
||||
"compat mode should return ciphertext as plaintext\nexpected: %q\ngot: %q",
|
||||
string(shortCiphertext),
|
||||
decrypted,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAesgcmNonCompatModeInvalidCiphertext(t *testing.T) {
|
||||
key := testKey32Bytes
|
||||
encrypter, err := New(key, false)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create encrypter: %v", err)
|
||||
}
|
||||
|
||||
// Test that invalid ciphertext returns error in non-compat mode
|
||||
invalidCiphertext := []byte("not encrypted data")
|
||||
_, err = encrypter.Decrypt(invalidCiphertext)
|
||||
if err == nil {
|
||||
t.Errorf("non-compat mode should return error for invalid ciphertext")
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package encrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNone_Encrypt(t *testing.T) {
|
||||
encrypter := &none{}
|
||||
|
||||
t.Run("encrypt simple string", func(t *testing.T) {
|
||||
plaintext := "hello world"
|
||||
ciphertext, err := encrypter.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(ciphertext, []byte(plaintext)) {
|
||||
t.Errorf("expected ciphertext to be %v, got %v", []byte(plaintext), ciphertext)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("encrypt empty string", func(t *testing.T) {
|
||||
plaintext := ""
|
||||
ciphertext, err := encrypter.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(ciphertext, []byte(plaintext)) {
|
||||
t.Errorf("expected ciphertext to be empty, got %v", ciphertext)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("encrypt special characters", func(t *testing.T) {
|
||||
plaintext := "!@#$%^&*()_+-=[]{}|;':\",./<>?"
|
||||
ciphertext, err := encrypter.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(ciphertext, []byte(plaintext)) {
|
||||
t.Errorf("expected ciphertext to be %v, got %v", []byte(plaintext), ciphertext)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("encrypt unicode characters", func(t *testing.T) {
|
||||
plaintext := "こんにちは世界 🌍"
|
||||
ciphertext, err := encrypter.Encrypt(plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(ciphertext, []byte(plaintext)) {
|
||||
t.Errorf("expected ciphertext to be %v, got %v", []byte(plaintext), ciphertext)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNone_Decrypt(t *testing.T) {
|
||||
encrypter := &none{}
|
||||
|
||||
t.Run("decrypt simple bytes", func(t *testing.T) {
|
||||
ciphertext := []byte("hello world")
|
||||
plaintext, err := encrypter.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if plaintext != string(ciphertext) {
|
||||
t.Errorf("expected plaintext to be %s, got %s", string(ciphertext), plaintext)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("decrypt empty bytes", func(t *testing.T) {
|
||||
ciphertext := []byte("")
|
||||
plaintext, err := encrypter.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if plaintext != "" {
|
||||
t.Errorf("expected plaintext to be empty, got %s", plaintext)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("decrypt nil bytes", func(t *testing.T) {
|
||||
var ciphertext []byte
|
||||
plaintext, err := encrypter.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if plaintext != "" {
|
||||
t.Errorf("expected plaintext to be empty, got %s", plaintext)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("decrypt special characters", func(t *testing.T) {
|
||||
ciphertext := []byte("!@#$%^&*()_+-=[]{}|;':\",./<>?")
|
||||
plaintext, err := encrypter.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if plaintext != string(ciphertext) {
|
||||
t.Errorf("expected plaintext to be %s, got %s", string(ciphertext), plaintext)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNone_EncryptDecrypt_RoundTrip(t *testing.T) {
|
||||
encrypter := &none{}
|
||||
|
||||
testCases := []string{
|
||||
"hello world",
|
||||
"",
|
||||
"!@#$%^&*()",
|
||||
"こんにちは世界",
|
||||
"multi\nline\nstring",
|
||||
"tab\tseparated\tvalues",
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc, func(t *testing.T) {
|
||||
ciphertext, err := encrypter.Encrypt(tc)
|
||||
if err != nil {
|
||||
t.Fatalf("encrypt failed: %v", err)
|
||||
}
|
||||
|
||||
plaintext, err := encrypter.Decrypt(ciphertext)
|
||||
if err != nil {
|
||||
t.Fatalf("decrypt failed: %v", err)
|
||||
}
|
||||
|
||||
if plaintext != tc {
|
||||
t.Errorf("round trip failed: expected %s, got %s", tc, plaintext)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,542 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStatusConstants(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status Status
|
||||
expected string
|
||||
}{
|
||||
{"StatusConflict", StatusConflict, "conflict"},
|
||||
{"StatusInternal", StatusInternal, "internal"},
|
||||
{"StatusInvalidArgument", StatusInvalidArgument, "invalid"},
|
||||
{"StatusNotFound", StatusNotFound, "not_found"},
|
||||
{"StatusNotImplemented", StatusNotImplemented, "not_implemented"},
|
||||
{"StatusUnauthorized", StatusUnauthorized, "unauthorized"},
|
||||
{"StatusForbidden", StatusForbidden, "forbidden"},
|
||||
{"StatusFailed", StatusFailed, "failed"},
|
||||
{"StatusPreconditionFailed", StatusPreconditionFailed, "precondition_failed"},
|
||||
{"StatusAborted", StatusAborted, "aborted"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if string(tt.status) != tt.expected {
|
||||
t.Errorf("Expected %s to be %q, got %q", tt.name, tt.expected, string(tt.status))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorStruct(t *testing.T) {
|
||||
err := &Error{
|
||||
Status: StatusNotFound,
|
||||
Message: "resource not found",
|
||||
Err: errors.New("underlying error"),
|
||||
Details: map[string]any{"resource_id": "123"},
|
||||
}
|
||||
|
||||
// Test Error() method
|
||||
expectedMsg := "resource not found: underlying error"
|
||||
if err.Error() != expectedMsg {
|
||||
t.Errorf("Expected error message %q, got %q", expectedMsg, err.Error())
|
||||
}
|
||||
|
||||
// Test Unwrap() method
|
||||
if err.Unwrap() == nil {
|
||||
t.Error("Expected Unwrap() to return non-nil error")
|
||||
}
|
||||
if err.Unwrap().Error() != "underlying error" {
|
||||
t.Errorf("Expected unwrapped error to be %q, got %q", "underlying error", err.Unwrap().Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorWithoutUnderlyingError(t *testing.T) {
|
||||
err := &Error{
|
||||
Status: StatusInvalidArgument,
|
||||
Message: "invalid input",
|
||||
}
|
||||
|
||||
// Test Error() method without underlying error
|
||||
if err.Error() != "invalid input" {
|
||||
t.Errorf("Expected error message %q, got %q", "invalid input", err.Error())
|
||||
}
|
||||
|
||||
// Test Unwrap() method
|
||||
if err.Unwrap() != nil {
|
||||
t.Error("Expected Unwrap() to return nil when no underlying error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorSetErr(t *testing.T) {
|
||||
err := &Error{
|
||||
Status: StatusInternal,
|
||||
Message: "internal error",
|
||||
}
|
||||
|
||||
underlyingErr := errors.New("database connection failed")
|
||||
result := err.SetErr(underlyingErr)
|
||||
|
||||
// Should return the same error instance
|
||||
if result != err {
|
||||
t.Error("Expected SetErr to return the same error instance")
|
||||
}
|
||||
|
||||
// Should set the underlying error
|
||||
if !errors.Is(err.Err, underlyingErr) {
|
||||
t.Error("Expected SetErr to set the underlying error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorSetDetails(t *testing.T) {
|
||||
err := &Error{
|
||||
Status: StatusNotFound,
|
||||
Message: "user not found",
|
||||
}
|
||||
|
||||
details := map[string]any{
|
||||
"user_id": "123",
|
||||
"table": "users",
|
||||
}
|
||||
result := err.SetDetails(details)
|
||||
|
||||
// Should return the same error instance
|
||||
if result != err {
|
||||
t.Error("Expected SetDetails to return the same error instance")
|
||||
}
|
||||
|
||||
// Should set the details
|
||||
if err.Details == nil {
|
||||
t.Error("Expected SetDetails to set the details")
|
||||
}
|
||||
if err.Details["user_id"] != "123" {
|
||||
t.Error("Expected details to contain user_id")
|
||||
}
|
||||
if err.Details["table"] != "users" {
|
||||
t.Error("Expected details to contain table")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected Status
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Error with status",
|
||||
err: &Error{Status: StatusNotFound, Message: "not found"},
|
||||
expected: StatusNotFound,
|
||||
},
|
||||
{
|
||||
name: "standard error",
|
||||
err: errors.New("standard error"),
|
||||
expected: StatusInternal,
|
||||
},
|
||||
{
|
||||
name: "wrapped Error",
|
||||
err: fmt.Errorf("wrapped: %w", &Error{Status: StatusConflict, Message: "conflict"}),
|
||||
expected: StatusConflict,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := AsStatus(tt.err)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected AsStatus(%v) to be %q, got %q", tt.err, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "Error with message",
|
||||
err: &Error{Status: StatusNotFound, Message: "resource not found"},
|
||||
expected: "resource not found",
|
||||
},
|
||||
{
|
||||
name: "standard error",
|
||||
err: errors.New("standard error message"),
|
||||
expected: "standard error message",
|
||||
},
|
||||
{
|
||||
name: "wrapped Error",
|
||||
err: fmt.Errorf("wrapped: %w", &Error{Status: StatusConflict, Message: "conflict occurred"}),
|
||||
expected: "conflict occurred",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := Message(tt.err)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected Message(%v) to be %q, got %q", tt.err, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetails(t *testing.T) {
|
||||
details := map[string]any{"key": "value", "number": 42}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected map[string]any
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "Error with details",
|
||||
err: &Error{Status: StatusNotFound, Message: "not found", Details: details},
|
||||
expected: details,
|
||||
},
|
||||
{
|
||||
name: "Error without details",
|
||||
err: &Error{Status: StatusNotFound, Message: "not found"},
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "standard error",
|
||||
err: errors.New("standard error"),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "wrapped Error with details",
|
||||
err: fmt.Errorf("wrapped: %w", &Error{Status: StatusConflict, Message: "conflict", Details: details}),
|
||||
expected: details,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := Details(tt.err)
|
||||
if !mapsEqual(result, tt.expected) {
|
||||
t.Errorf("Expected Details(%v) to be %v, got %v", tt.err, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAsError(t *testing.T) {
|
||||
appErr := &Error{Status: StatusNotFound, Message: "not found"}
|
||||
stdErr := errors.New("standard error")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected *Error
|
||||
}{
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "Error type",
|
||||
err: appErr,
|
||||
expected: appErr,
|
||||
},
|
||||
{
|
||||
name: "standard error",
|
||||
err: stdErr,
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "wrapped Error",
|
||||
err: fmt.Errorf("wrapped: %w", appErr),
|
||||
expected: appErr,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := AsError(tt.err)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected AsError(%v) to be %v, got %v", tt.err, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
status Status
|
||||
format string
|
||||
args []interface{}
|
||||
expected *Error
|
||||
}{
|
||||
{
|
||||
name: "simple format",
|
||||
status: StatusNotFound,
|
||||
format: "user not found",
|
||||
args: nil,
|
||||
expected: &Error{Status: StatusNotFound, Message: "user not found"},
|
||||
},
|
||||
{
|
||||
name: "format with args",
|
||||
status: StatusInvalidArgument,
|
||||
format: "invalid user ID: %d",
|
||||
args: []interface{}{123},
|
||||
expected: &Error{Status: StatusInvalidArgument, Message: "invalid user ID: 123"},
|
||||
},
|
||||
{
|
||||
name: "format with multiple args",
|
||||
status: StatusConflict,
|
||||
format: "user %s already exists with email %s",
|
||||
args: []interface{}{"john", "john@example.com"},
|
||||
expected: &Error{Status: StatusConflict, Message: "user john already exists with email john@example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := Format(tt.status, tt.format, tt.args...)
|
||||
if result.Status != tt.expected.Status {
|
||||
t.Errorf("Expected status %q, got %q", tt.expected.Status, result.Status)
|
||||
}
|
||||
if result.Message != tt.expected.Message {
|
||||
t.Errorf("Expected message %q, got %q", tt.expected.Message, result.Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHelperFunctions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(string, ...interface{}) *Error
|
||||
status Status
|
||||
format string
|
||||
args []interface{}
|
||||
expected string
|
||||
}{
|
||||
{"NotFound", NotFound, StatusNotFound, "user %d not found", []interface{}{123}, "user 123 not found"},
|
||||
{"InvalidArgument", InvalidArgument, StatusInvalidArgument,
|
||||
"invalid email: %s", []interface{}{"invalid"}, "invalid email: invalid"},
|
||||
{"Conflict", Conflict, StatusConflict, "user %s exists", []interface{}{"john"}, "user john exists"},
|
||||
{"PreconditionFailed", PreconditionFailed, StatusPreconditionFailed, "version mismatch", nil, "version mismatch"},
|
||||
{"Unauthorized", Unauthorized, StatusUnauthorized, "invalid token", nil, "invalid token"},
|
||||
{"Forbidden", Forbidden, StatusForbidden, "access denied", nil, "access denied"},
|
||||
{"Failed", Failed, StatusFailed, "operation failed", nil, "operation failed"},
|
||||
{"Aborted", Aborted, StatusAborted, "operation aborted", nil, "operation aborted"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.fn(tt.format, tt.args...)
|
||||
if result.Status != tt.status {
|
||||
t.Errorf("Expected status %q, got %q", tt.status, result.Status)
|
||||
}
|
||||
if result.Message != tt.expected {
|
||||
t.Errorf("Expected message %q, got %q", tt.expected, result.Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternal(t *testing.T) {
|
||||
underlyingErr := errors.New("database connection failed")
|
||||
result := Internal(underlyingErr, "failed to get user %d", 123)
|
||||
|
||||
if result.Status != StatusInternal {
|
||||
t.Errorf("Expected status %q, got %q", StatusInternal, result.Status)
|
||||
}
|
||||
|
||||
expectedMsg := "failed to get user 123"
|
||||
if result.Message != expectedMsg {
|
||||
t.Errorf("Expected message %q, got %q", expectedMsg, result.Message)
|
||||
}
|
||||
|
||||
if result.Err == nil {
|
||||
t.Error("Expected underlying error to be set")
|
||||
}
|
||||
|
||||
// The underlying error should be wrapped
|
||||
expectedErrMsg := "failed to get user 123: database connection failed"
|
||||
if result.Err.Error() != expectedErrMsg {
|
||||
t.Errorf("Expected underlying error message %q, got %q", expectedErrMsg, result.Err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusCheckFunctions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(error) bool
|
||||
status Status
|
||||
expected bool
|
||||
}{
|
||||
{"IsNotFound with NotFound", IsNotFound, StatusNotFound, true},
|
||||
{"IsNotFound with Conflict", IsNotFound, StatusConflict, false},
|
||||
{"IsConflict with Conflict", IsConflict, StatusConflict, true},
|
||||
{"IsConflict with NotFound", IsConflict, StatusNotFound, false},
|
||||
{"IsInvalidArgument with InvalidArgument", IsInvalidArgument, StatusInvalidArgument, true},
|
||||
{"IsInvalidArgument with Internal", IsInvalidArgument, StatusInternal, false},
|
||||
{"IsInternal with Internal", IsInternal, StatusInternal, true},
|
||||
{"IsInternal with NotFound", IsInternal, StatusNotFound, false},
|
||||
{"IsPreconditionFailed with PreconditionFailed", IsPreconditionFailed, StatusPreconditionFailed, true},
|
||||
{"IsPreconditionFailed with Aborted", IsPreconditionFailed, StatusAborted, false},
|
||||
{"IsAborted with Aborted", IsAborted, StatusAborted, true},
|
||||
{"IsAborted with Failed", IsAborted, StatusFailed, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := &Error{Status: tt.status, Message: "test error"}
|
||||
result := tt.fn(err)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %s(%v) to be %v, got %v", tt.name, err, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusCheckFunctionsWithStandardError(t *testing.T) {
|
||||
stdErr := errors.New("standard error")
|
||||
|
||||
// All status check functions should return false for standard errors,
|
||||
// except IsInternal which should return true (since standard errors are treated as internal)
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(error) bool
|
||||
expected bool
|
||||
}{
|
||||
{"IsNotFound", IsNotFound, false},
|
||||
{"IsConflict", IsConflict, false},
|
||||
{"IsInvalidArgument", IsInvalidArgument, false},
|
||||
{"IsInternal", IsInternal, true}, // Standard errors are treated as internal
|
||||
{"IsPreconditionFailed", IsPreconditionFailed, false},
|
||||
{"IsAborted", IsAborted, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.fn(stdErr)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %s(standard error) to be %v, got %v", tt.name, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusCheckFunctionsWithNil(t *testing.T) {
|
||||
// All status check functions should return false for nil errors
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(error) bool
|
||||
}{
|
||||
{"IsNotFound", IsNotFound},
|
||||
{"IsConflict", IsConflict},
|
||||
{"IsInvalidArgument", IsInvalidArgument},
|
||||
{"IsInternal", IsInternal},
|
||||
{"IsPreconditionFailed", IsPreconditionFailed},
|
||||
{"IsAborted", IsAborted},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.fn(nil)
|
||||
if result {
|
||||
t.Errorf("Expected %s(nil) to be false, got true", tt.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to compare maps.
|
||||
func mapsEqual(a, b map[string]any) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if a == nil || b == nil {
|
||||
return false
|
||||
}
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for k, v := range a {
|
||||
if b[k] != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Benchmark tests.
|
||||
func BenchmarkErrorError(b *testing.B) {
|
||||
err := &Error{
|
||||
Status: StatusNotFound,
|
||||
Message: "resource not found",
|
||||
Err: errors.New("underlying error"),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAsStatus(b *testing.B) {
|
||||
err := &Error{Status: StatusNotFound, Message: "not found"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
AsStatus(err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFormat(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Format(StatusNotFound, "user %d not found", 123)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsNotFound(b *testing.B) {
|
||||
err := &Error{Status: StatusNotFound, Message: "not found"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IsNotFound(err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
}{
|
||||
{
|
||||
name: "simple error message",
|
||||
text: "test error",
|
||||
},
|
||||
{
|
||||
name: "empty error message",
|
||||
text: "",
|
||||
},
|
||||
{
|
||||
name: "long error message",
|
||||
text: "this is a very long error message that contains multiple words and should be handled correctly",
|
||||
},
|
||||
{
|
||||
name: "error with special characters",
|
||||
text: "error with special chars: !@#$%^&*()",
|
||||
},
|
||||
{
|
||||
name: "error with unicode",
|
||||
text: "error with unicode: 世界 🌍",
|
||||
},
|
||||
{
|
||||
name: "error with newlines",
|
||||
text: "error\nwith\nnewlines",
|
||||
},
|
||||
{
|
||||
name: "error with tabs",
|
||||
text: "error\twith\ttabs",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := New(tt.text)
|
||||
if err == nil {
|
||||
t.Error("Expected error to be non-nil")
|
||||
}
|
||||
if err.Error() != tt.text {
|
||||
t.Errorf("Expected error message %q, got %q", tt.text, err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewComparison(t *testing.T) {
|
||||
// Test that New creates errors that can be compared
|
||||
err1 := New("test error")
|
||||
err2 := New("test error")
|
||||
err3 := New("different error")
|
||||
|
||||
// Different instances with same message should not be equal
|
||||
if errors.Is(err1, err2) {
|
||||
t.Error("Expected different error instances to not be equal")
|
||||
}
|
||||
|
||||
// Different messages should not be equal
|
||||
if errors.Is(err1, err3) {
|
||||
t.Error("Expected errors with different messages to not be equal")
|
||||
}
|
||||
|
||||
// But their messages should be the same
|
||||
if err1.Error() != err2.Error() {
|
||||
t.Error("Expected error messages to be the same")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIs(t *testing.T) {
|
||||
baseErr := New("base error")
|
||||
wrappedErr := errors.New("wrapped: " + baseErr.Error())
|
||||
differentErr := New("different error")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
target error
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "same error",
|
||||
err: baseErr,
|
||||
target: baseErr,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "different errors",
|
||||
err: baseErr,
|
||||
target: differentErr,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
target: baseErr,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nil target",
|
||||
err: baseErr,
|
||||
target: nil,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "both nil",
|
||||
err: nil,
|
||||
target: nil,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "wrapped error",
|
||||
err: wrappedErr,
|
||||
target: baseErr,
|
||||
expected: false, // Our wrapper doesn't implement Unwrap
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := Is(tt.err, tt.target)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected Is(%v, %v) to be %v, got %v", tt.err, tt.target, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Custom error types for testing.
|
||||
type customError struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e customError) Error() string { return e.msg }
|
||||
|
||||
type anotherError struct {
|
||||
code int
|
||||
}
|
||||
|
||||
func (e anotherError) Error() string { return "another error" }
|
||||
|
||||
func TestAs(t *testing.T) {
|
||||
customErr := customError{msg: "custom error"}
|
||||
anotherErr := anotherError{code: 123}
|
||||
standardErr := New("standard error")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
target interface{}
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "custom error to custom error",
|
||||
err: customErr,
|
||||
target: &customError{},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "custom error to different type",
|
||||
err: customErr,
|
||||
target: &anotherError{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "standard error to custom type",
|
||||
err: standardErr,
|
||||
target: &customError{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nil error",
|
||||
err: nil,
|
||||
target: &customError{},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "another error type",
|
||||
err: anotherErr,
|
||||
target: &anotherError{},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := As(tt.err, tt.target)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected As(%v, %T) to be %v, got %v", tt.err, tt.target, tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Custom error type for TestAsWithValues.
|
||||
type codeError struct {
|
||||
msg string
|
||||
code int
|
||||
}
|
||||
|
||||
func (e codeError) Error() string { return e.msg }
|
||||
|
||||
func TestAsWithValues(t *testing.T) {
|
||||
// Test that As correctly populates the target
|
||||
|
||||
originalErr := codeError{msg: "test error", code: 42}
|
||||
var target codeError
|
||||
|
||||
result := As(originalErr, &target)
|
||||
if !result {
|
||||
t.Error("Expected As to return true")
|
||||
}
|
||||
|
||||
if target.msg != originalErr.msg {
|
||||
t.Errorf("Expected target msg to be %q, got %q", originalErr.msg, target.msg)
|
||||
}
|
||||
|
||||
if target.code != originalErr.code {
|
||||
t.Errorf("Expected target code to be %d, got %d", originalErr.code, target.code)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests.
|
||||
func BenchmarkNew(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = New("benchmark error")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIs(b *testing.B) {
|
||||
err1 := New("error 1")
|
||||
err2 := New("error 2")
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Is(err1, err2)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAs(b *testing.B) {
|
||||
err := customError{msg: "test"}
|
||||
var target customError
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
As(err, &target)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ package errors
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -25,33 +26,189 @@ func TestIsType(t *testing.T) {
|
|||
err := errors.New("abc")
|
||||
valueErr := testValueError{}
|
||||
valueErrPtr := &testValueError{}
|
||||
pointerErr := &testPointerError{}
|
||||
customErr := customError{msg: "test"}
|
||||
|
||||
assert.True(t, IsType[error](err))
|
||||
assert.True(t, IsType[error](valueErr))
|
||||
assert.True(t, IsType[error](valueErrPtr))
|
||||
assert.True(t, IsType[error](pointerErr))
|
||||
assert.True(t, IsType[error](customErr))
|
||||
|
||||
assert.False(t, IsType[testValueError](err))
|
||||
assert.True(t, IsType[testValueError](valueErr))
|
||||
assert.False(t, IsType[testValueError](valueErrPtr))
|
||||
assert.False(t, IsType[testValueError](pointerErr))
|
||||
assert.False(t, IsType[testValueError](customErr))
|
||||
|
||||
assert.False(t, IsType[*testValueError](err))
|
||||
assert.False(t, IsType[*testValueError](valueErr))
|
||||
assert.True(t, IsType[*testValueError](valueErrPtr))
|
||||
assert.False(t, IsType[*testValueError](pointerErr))
|
||||
assert.False(t, IsType[*testValueError](customErr))
|
||||
|
||||
assert.False(t, IsType[*testPointerError](err))
|
||||
assert.False(t, IsType[*testPointerError](valueErr))
|
||||
assert.False(t, IsType[*testPointerError](valueErrPtr))
|
||||
assert.True(t, IsType[*testPointerError](pointerErr))
|
||||
assert.False(t, IsType[customError](err))
|
||||
assert.False(t, IsType[customError](valueErr))
|
||||
assert.False(t, IsType[customError](valueErrPtr))
|
||||
assert.True(t, IsType[customError](customErr))
|
||||
}
|
||||
|
||||
func TestIsTypeWithNil(t *testing.T) {
|
||||
// Test with nil error
|
||||
assert.False(t, IsType[error](nil))
|
||||
assert.False(t, IsType[testValueError](nil))
|
||||
assert.False(t, IsType[*testValueError](nil))
|
||||
assert.False(t, IsType[*customError](nil))
|
||||
}
|
||||
|
||||
func TestIsTypeWithWrappedErrors(t *testing.T) {
|
||||
// Test with wrapped errors
|
||||
valueErr := testValueError{}
|
||||
wrappedErr := fmt.Errorf("wrapped: %w", valueErr)
|
||||
|
||||
assert.True(t, IsType[error](wrappedErr))
|
||||
assert.True(t, IsType[testValueError](wrappedErr))
|
||||
assert.False(t, IsType[*testValueError](wrappedErr))
|
||||
|
||||
// Test with wrapped custom error
|
||||
customErr := customError{msg: "custom"}
|
||||
wrappedCustomErr := fmt.Errorf("wrapped: %w", customErr)
|
||||
|
||||
assert.True(t, IsType[error](wrappedCustomErr))
|
||||
assert.False(t, IsType[testValueError](wrappedCustomErr))
|
||||
assert.False(t, IsType[*testValueError](wrappedCustomErr))
|
||||
assert.True(t, IsType[customError](wrappedCustomErr))
|
||||
}
|
||||
|
||||
func TestIsTypeWithCustomError(t *testing.T) {
|
||||
// Test with custom error type
|
||||
customErr := customError{msg: "not found"}
|
||||
|
||||
assert.True(t, IsType[error](customErr))
|
||||
assert.True(t, IsType[customError](customErr))
|
||||
assert.False(t, IsType[*customError](customErr))
|
||||
assert.False(t, IsType[testValueError](customErr))
|
||||
assert.False(t, IsType[*testValueError](customErr))
|
||||
}
|
||||
|
||||
func TestIsTypeWithMultipleWrapping(t *testing.T) {
|
||||
// Test with multiple levels of wrapping
|
||||
originalErr := testValueError{}
|
||||
wrappedOnce := fmt.Errorf("first wrap: %w", originalErr)
|
||||
wrappedTwice := fmt.Errorf("second wrap: %w", wrappedOnce)
|
||||
|
||||
assert.True(t, IsType[error](wrappedTwice))
|
||||
assert.True(t, IsType[testValueError](wrappedTwice))
|
||||
assert.False(t, IsType[*testValueError](wrappedTwice))
|
||||
assert.False(t, IsType[customError](wrappedTwice))
|
||||
}
|
||||
|
||||
func TestIsTypeWithDifferentErrorTypes(t *testing.T) {
|
||||
// Test with various error types
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
}{
|
||||
{"standard error", errors.New("standard")},
|
||||
{"value error", testValueError{}},
|
||||
{"pointer to value error", &testValueError{}},
|
||||
{"custom error", customError{msg: "custom"}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// All should be of type error
|
||||
assert.True(t, IsType[error](tt.err), "All errors should be of type error")
|
||||
|
||||
// Test specific type checking
|
||||
var testVal testValueError
|
||||
var testPtr *testValueError
|
||||
var customErr customError
|
||||
|
||||
switch {
|
||||
case errors.As(tt.err, &testVal):
|
||||
assert.True(t, IsType[testValueError](tt.err))
|
||||
assert.False(t, IsType[*testValueError](tt.err))
|
||||
case errors.As(tt.err, &testPtr):
|
||||
assert.False(t, IsType[testValueError](tt.err))
|
||||
assert.True(t, IsType[*testValueError](tt.err))
|
||||
case errors.As(tt.err, &customErr):
|
||||
assert.True(t, IsType[customError](tt.err))
|
||||
assert.False(t, IsType[*customError](tt.err))
|
||||
default:
|
||||
// Standard error - should not match specific types
|
||||
assert.False(t, IsType[testValueError](tt.err))
|
||||
assert.False(t, IsType[*testValueError](tt.err))
|
||||
assert.False(t, IsType[customError](tt.err))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsTypeEdgeCases(t *testing.T) {
|
||||
// Test with simple custom error
|
||||
customErr := customError{msg: "test"}
|
||||
assert.True(t, IsType[error](customErr))
|
||||
assert.True(t, IsType[customError](customErr))
|
||||
assert.False(t, IsType[*customError](customErr))
|
||||
|
||||
// Test type assertion behavior
|
||||
var err error = customErr
|
||||
assert.True(t, IsType[customError](err))
|
||||
}
|
||||
|
||||
func TestIsTypePerformance(t *testing.T) {
|
||||
// Test that IsType works efficiently with different error types
|
||||
errors := []error{
|
||||
errors.New("standard"),
|
||||
testValueError{},
|
||||
&testValueError{},
|
||||
&customError{msg: "pointer"},
|
||||
&Error{Status: StatusNotFound, Message: "not found"},
|
||||
}
|
||||
|
||||
for i, err := range errors {
|
||||
t.Run(fmt.Sprintf("error_%d", i), func(t *testing.T) {
|
||||
// Each should be identifiable as an error
|
||||
assert.True(t, IsType[error](err))
|
||||
|
||||
// And should have consistent behavior
|
||||
result1 := IsType[error](err)
|
||||
result2 := IsType[error](err)
|
||||
assert.Equal(t, result1, result2, "IsType should be consistent")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testValueError struct{}
|
||||
|
||||
func (e testValueError) Error() string { return "value receiver" }
|
||||
|
||||
type testPointerError struct{}
|
||||
// Benchmark tests.
|
||||
func BenchmarkIsTypeValueError(b *testing.B) {
|
||||
err := testValueError{}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IsType[testValueError](err)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *testPointerError) Error() string { return "pointer receiver" }
|
||||
func BenchmarkIsTypeCustomError(b *testing.B) {
|
||||
err := customError{msg: "test"}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IsType[customError](err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsTypeStandardError(b *testing.B) {
|
||||
err := errors.New("standard error")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IsType[error](err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkIsTypeWrappedError(b *testing.B) {
|
||||
err := fmt.Errorf("wrapped: %w", testValueError{})
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
IsType[testValueError](err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,10 +15,297 @@
|
|||
package sha
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const emptyTreeSHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid sha",
|
||||
input: emptyTreeSHA,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid short sha",
|
||||
input: "4b825dc6",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid sha with spaces",
|
||||
input: " " + emptyTreeSHA + " ",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid sha uppercase",
|
||||
input: "4B825DC642CB6EB9A060E54BF8D69288FBEE4904",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid sha - too short",
|
||||
input: "abc",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid sha - invalid characters",
|
||||
input: "gggggggggggggggggggggggggggggggggggggggg",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid sha - empty",
|
||||
input: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := New(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("New() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && got.String() == "" {
|
||||
t.Error("expected non-empty SHA")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewOrEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantErr bool
|
||||
isEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "valid sha",
|
||||
input: emptyTreeSHA,
|
||||
wantErr: false,
|
||||
isEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "empty string returns None",
|
||||
input: "",
|
||||
wantErr: false,
|
||||
isEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "invalid sha",
|
||||
input: "invalid",
|
||||
wantErr: true,
|
||||
isEmpty: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := NewOrEmpty(tt.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewOrEmpty() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && got.IsEmpty() != tt.isEmpty {
|
||||
t.Errorf("NewOrEmpty() isEmpty = %v, want %v", got.IsEmpty(), tt.isEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA_IsNil(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sha SHA
|
||||
isNil bool
|
||||
}{
|
||||
{
|
||||
name: "nil sha",
|
||||
sha: Nil,
|
||||
isNil: true,
|
||||
},
|
||||
{
|
||||
name: "non-nil sha",
|
||||
sha: EmptyTree,
|
||||
isNil: false,
|
||||
},
|
||||
{
|
||||
name: "empty sha",
|
||||
sha: None,
|
||||
isNil: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.sha.IsNil(); got != tt.isNil {
|
||||
t.Errorf("IsNil() = %v, want %v", got, tt.isNil)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA_IsEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sha SHA
|
||||
isEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "empty sha",
|
||||
sha: None,
|
||||
isEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "non-empty sha",
|
||||
sha: EmptyTree,
|
||||
isEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "nil sha",
|
||||
sha: Nil,
|
||||
isEmpty: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.sha.IsEmpty(); got != tt.isEmpty {
|
||||
t.Errorf("IsEmpty() = %v, want %v", got, tt.isEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA_Equal(t *testing.T) {
|
||||
sha1 := Must(emptyTreeSHA)
|
||||
sha2 := Must(emptyTreeSHA)
|
||||
sha3 := Must("1234567890abcdef1234567890abcdef12345678")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
sha1 SHA
|
||||
sha2 SHA
|
||||
equal bool
|
||||
}{
|
||||
{
|
||||
name: "equal shas",
|
||||
sha1: sha1,
|
||||
sha2: sha2,
|
||||
equal: true,
|
||||
},
|
||||
{
|
||||
name: "different shas",
|
||||
sha1: sha1,
|
||||
sha2: sha3,
|
||||
equal: false,
|
||||
},
|
||||
{
|
||||
name: "empty shas",
|
||||
sha1: None,
|
||||
sha2: None,
|
||||
equal: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.sha1.Equal(tt.sha2); got != tt.equal {
|
||||
t.Errorf("Equal() = %v, want %v", got, tt.equal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA_String(t *testing.T) {
|
||||
sha := Must(emptyTreeSHA)
|
||||
if got := sha.String(); got != emptyTreeSHA {
|
||||
t.Errorf("String() = %v, want %v", got, emptyTreeSHA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA_Value(t *testing.T) {
|
||||
sha := Must(emptyTreeSHA)
|
||||
val, err := sha.Value()
|
||||
if err != nil {
|
||||
t.Errorf("Value() error = %v", err)
|
||||
}
|
||||
if val != emptyTreeSHA {
|
||||
t.Errorf("Value() = %v, want %v", val, emptyTreeSHA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA_GobEncodeDecode(t *testing.T) {
|
||||
original := Must(emptyTreeSHA)
|
||||
|
||||
// Encode
|
||||
encoded, err := original.GobEncode()
|
||||
if err != nil {
|
||||
t.Fatalf("GobEncode() error = %v", err)
|
||||
}
|
||||
|
||||
// Decode
|
||||
var decoded SHA
|
||||
err = decoded.GobDecode(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("GobDecode() error = %v", err)
|
||||
}
|
||||
|
||||
if !original.Equal(decoded) {
|
||||
t.Errorf("GobEncode/Decode round trip failed: got %v, want %v", decoded, original)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA_GobEncodeDecodeWithGob(t *testing.T) {
|
||||
original := Must(emptyTreeSHA)
|
||||
|
||||
// Encode using gob
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
err := enc.Encode(original)
|
||||
if err != nil {
|
||||
t.Fatalf("gob.Encode() error = %v", err)
|
||||
}
|
||||
|
||||
// Decode using gob
|
||||
var decoded SHA
|
||||
dec := gob.NewDecoder(&buf)
|
||||
err = dec.Decode(&decoded)
|
||||
if err != nil {
|
||||
t.Fatalf("gob.Decode() error = %v", err)
|
||||
}
|
||||
|
||||
if !original.Equal(decoded) {
|
||||
t.Errorf("gob Encode/Decode round trip failed: got %v, want %v", decoded, original)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMust(t *testing.T) {
|
||||
t.Run("valid sha", func(t *testing.T) {
|
||||
sha := Must(emptyTreeSHA)
|
||||
if sha.String() != emptyTreeSHA {
|
||||
t.Errorf("Must() = %v, want %v", sha.String(), emptyTreeSHA)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid sha panics", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("Must() did not panic on invalid SHA")
|
||||
}
|
||||
}()
|
||||
Must("invalid")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSHA_MarshalJSON(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -77,6 +364,18 @@ func TestSHA_UnmarshalJSON(t *testing.T) {
|
|||
expected: SHA{},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
input: []byte("invalid"),
|
||||
expected: SHA{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid sha",
|
||||
input: []byte("\"invalid\""),
|
||||
expected: SHA{},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
|
@ -84,9 +383,20 @@ func TestSHA_UnmarshalJSON(t *testing.T) {
|
|||
if err := s.UnmarshalJSON(tt.input); (err != nil) != tt.wantErr {
|
||||
t.Errorf("UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if !reflect.DeepEqual(s, tt.expected) {
|
||||
if !tt.wantErr && !reflect.DeepEqual(s, tt.expected) {
|
||||
t.Errorf("bytes.Equal expected %s, got %s", tt.expected, s)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSHA_JSONSchema(t *testing.T) {
|
||||
sha := Must(emptyTreeSHA)
|
||||
schema, err := sha.JSONSchema()
|
||||
if err != nil {
|
||||
t.Errorf("JSONSchema() error = %v", err)
|
||||
}
|
||||
if schema.Description == nil || *schema.Description != "Git object hash" {
|
||||
t.Errorf("JSONSchema() description = %v, want 'Git object hash'", schema.Description)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -1,6 +1,8 @@
|
|||
module github.com/harness/gitness
|
||||
|
||||
go 1.23.10
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.10
|
||||
|
||||
require (
|
||||
cloud.google.com/go/storage v1.43.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package job
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUID(t *testing.T) {
|
||||
uid, err := UID()
|
||||
if err != nil {
|
||||
t.Fatalf("UID() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Verify UID is not empty
|
||||
if uid == "" {
|
||||
t.Errorf("UID() returned empty string")
|
||||
}
|
||||
|
||||
// Verify UID has expected length (10 bytes / 5 * 8 = 16 characters)
|
||||
expectedLength := 16
|
||||
if len(uid) != expectedLength {
|
||||
t.Errorf("UID() length = %d, expected %d", len(uid), expectedLength)
|
||||
}
|
||||
|
||||
// Verify UID contains only valid base32 characters
|
||||
validChars := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567="
|
||||
for _, c := range uid {
|
||||
found := false
|
||||
for _, valid := range validChars {
|
||||
if c == valid {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("UID() contains invalid character: %c", c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIDUniqueness(t *testing.T) {
|
||||
// Generate multiple UIDs and verify they are unique
|
||||
const numUIDs = 1000
|
||||
uids := make(map[string]bool)
|
||||
|
||||
for i := 0; i < numUIDs; i++ {
|
||||
uid, err := UID()
|
||||
if err != nil {
|
||||
t.Fatalf("UID() returned error on iteration %d: %v", i, err)
|
||||
}
|
||||
|
||||
if uids[uid] {
|
||||
t.Errorf("UID() generated duplicate: %s", uid)
|
||||
}
|
||||
uids[uid] = true
|
||||
}
|
||||
|
||||
// Verify we generated the expected number of unique UIDs
|
||||
if len(uids) != numUIDs {
|
||||
t.Errorf("Generated %d unique UIDs, expected %d", len(uids), numUIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIDConsistentLength(t *testing.T) {
|
||||
// Generate multiple UIDs and verify they all have the same length
|
||||
const numUIDs = 100
|
||||
expectedLength := 16
|
||||
|
||||
for i := 0; i < numUIDs; i++ {
|
||||
uid, err := UID()
|
||||
if err != nil {
|
||||
t.Fatalf("UID() returned error on iteration %d: %v", i, err)
|
||||
}
|
||||
|
||||
if len(uid) != expectedLength {
|
||||
t.Errorf("UID() iteration %d: length = %d, expected %d", i, len(uid), expectedLength)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIDBase32Encoding(t *testing.T) {
|
||||
// Generate a UID and verify it's valid base32
|
||||
uid, err := UID()
|
||||
if err != nil {
|
||||
t.Fatalf("UID() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Try to decode it as base32 - should not error
|
||||
// Note: We don't need to import encoding/base32 again as it's already in uid.go
|
||||
// Just verify the format is correct by checking characters
|
||||
for i, c := range uid {
|
||||
if (c < 'A' || c > 'Z') && (c < '2' || c > '7') && c != '=' {
|
||||
t.Errorf("UID() character at position %d (%c) is not valid base32", i, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIDNoPadding(t *testing.T) {
|
||||
// Generate multiple UIDs and verify they don't have padding
|
||||
// (10 bytes encodes to exactly 16 characters without padding)
|
||||
const numUIDs = 100
|
||||
|
||||
for i := 0; i < numUIDs; i++ {
|
||||
uid, err := UID()
|
||||
if err != nil {
|
||||
t.Fatalf("UID() returned error on iteration %d: %v", i, err)
|
||||
}
|
||||
|
||||
// Check if UID contains padding character '='
|
||||
for j, c := range uid {
|
||||
if c == '=' {
|
||||
t.Errorf("UID() iteration %d: contains padding at position %d", i, j)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUID(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := UID()
|
||||
if err != nil {
|
||||
b.Fatalf("UID() returned error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUIDParallel(b *testing.B) {
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_, err := UID()
|
||||
if err != nil {
|
||||
b.Fatalf("UID() returned error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package livelog
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLine(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
line Line
|
||||
expected Line
|
||||
}{
|
||||
{
|
||||
name: "basic line",
|
||||
line: Line{
|
||||
Number: 1,
|
||||
Message: "hello world",
|
||||
Timestamp: 1234567890,
|
||||
},
|
||||
expected: Line{
|
||||
Number: 1,
|
||||
Message: "hello world",
|
||||
Timestamp: 1234567890,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty message",
|
||||
line: Line{
|
||||
Number: 0,
|
||||
Message: "",
|
||||
Timestamp: 0,
|
||||
},
|
||||
expected: Line{
|
||||
Number: 0,
|
||||
Message: "",
|
||||
Timestamp: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "negative number",
|
||||
line: Line{
|
||||
Number: -1,
|
||||
Message: "error message",
|
||||
Timestamp: 9876543210,
|
||||
},
|
||||
expected: Line{
|
||||
Number: -1,
|
||||
Message: "error message",
|
||||
Timestamp: 9876543210,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "large timestamp",
|
||||
line: Line{
|
||||
Number: 999999,
|
||||
Message: "large timestamp",
|
||||
Timestamp: 9223372036854775807, // max int64
|
||||
},
|
||||
expected: Line{
|
||||
Number: 999999,
|
||||
Message: "large timestamp",
|
||||
Timestamp: 9223372036854775807,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
line := test.line
|
||||
|
||||
if line.Number != test.expected.Number {
|
||||
t.Errorf("expected number %d, got %d", test.expected.Number, line.Number)
|
||||
}
|
||||
|
||||
if line.Message != test.expected.Message {
|
||||
t.Errorf("expected message %q, got %q", test.expected.Message, line.Message)
|
||||
}
|
||||
|
||||
if line.Timestamp != test.expected.Timestamp {
|
||||
t.Errorf("expected timestamp %d, got %d", test.expected.Timestamp, line.Timestamp)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStreamInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
info LogStreamInfo
|
||||
expected LogStreamInfo
|
||||
}{
|
||||
{
|
||||
name: "empty streams",
|
||||
info: LogStreamInfo{
|
||||
Streams: map[int64]int{},
|
||||
},
|
||||
expected: LogStreamInfo{
|
||||
Streams: map[int64]int{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single stream",
|
||||
info: LogStreamInfo{
|
||||
Streams: map[int64]int{
|
||||
1: 5,
|
||||
},
|
||||
},
|
||||
expected: LogStreamInfo{
|
||||
Streams: map[int64]int{
|
||||
1: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple streams",
|
||||
info: LogStreamInfo{
|
||||
Streams: map[int64]int{
|
||||
1: 3,
|
||||
2: 7,
|
||||
10: 1,
|
||||
},
|
||||
},
|
||||
expected: LogStreamInfo{
|
||||
Streams: map[int64]int{
|
||||
1: 3,
|
||||
2: 7,
|
||||
10: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "streams with zero subscribers",
|
||||
info: LogStreamInfo{
|
||||
Streams: map[int64]int{
|
||||
1: 0,
|
||||
2: 5,
|
||||
3: 0,
|
||||
},
|
||||
},
|
||||
expected: LogStreamInfo{
|
||||
Streams: map[int64]int{
|
||||
1: 0,
|
||||
2: 5,
|
||||
3: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
info := test.info
|
||||
|
||||
if !reflect.DeepEqual(info.Streams, test.expected.Streams) {
|
||||
t.Errorf("expected streams %v, got %v", test.expected.Streams, info.Streams)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStreamInfo_NilStreams(t *testing.T) {
|
||||
info := LogStreamInfo{
|
||||
Streams: nil,
|
||||
}
|
||||
|
||||
// Should be able to access nil map without panic
|
||||
if info.Streams != nil {
|
||||
t.Error("expected nil streams")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLine_JSONTags(t *testing.T) {
|
||||
// Test that the struct has the expected JSON tags
|
||||
lineType := reflect.TypeOf(Line{})
|
||||
|
||||
// Check Number field
|
||||
numberField, found := lineType.FieldByName("Number")
|
||||
if !found {
|
||||
t.Fatal("Number field not found")
|
||||
}
|
||||
if tag := numberField.Tag.Get("json"); tag != "pos" {
|
||||
t.Errorf("expected Number field to have json tag 'pos', got %q", tag)
|
||||
}
|
||||
|
||||
// Check Message field
|
||||
messageField, found := lineType.FieldByName("Message")
|
||||
if !found {
|
||||
t.Fatal("Message field not found")
|
||||
}
|
||||
if tag := messageField.Tag.Get("json"); tag != "out" {
|
||||
t.Errorf("expected Message field to have json tag 'out', got %q", tag)
|
||||
}
|
||||
|
||||
// Check Timestamp field
|
||||
timestampField, found := lineType.FieldByName("Timestamp")
|
||||
if !found {
|
||||
t.Fatal("Timestamp field not found")
|
||||
}
|
||||
if tag := timestampField.Tag.Get("json"); tag != "time" {
|
||||
t.Errorf("expected Timestamp field to have json tag 'time', got %q", tag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogStreamInfo_JSONTags(t *testing.T) {
|
||||
// Test that the struct has the expected JSON tags
|
||||
infoType := reflect.TypeOf(LogStreamInfo{})
|
||||
|
||||
// Check Streams field
|
||||
streamsField, found := infoType.FieldByName("Streams")
|
||||
if !found {
|
||||
t.Fatal("Streams field not found")
|
||||
}
|
||||
if tag := streamsField.Tag.Get("json"); tag != "streams" {
|
||||
t.Errorf("expected Streams field to have json tag 'streams', got %q", tag)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,357 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package livelog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewMemory(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
if stream == nil {
|
||||
t.Fatal("expected non-nil stream")
|
||||
}
|
||||
|
||||
// Verify it implements LogStream interface
|
||||
var _ = stream
|
||||
}
|
||||
|
||||
func TestStreamer_Create(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
id int64
|
||||
}{
|
||||
{
|
||||
name: "positive id",
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: "zero id",
|
||||
id: 0,
|
||||
},
|
||||
{
|
||||
name: "negative id",
|
||||
id: -1,
|
||||
},
|
||||
{
|
||||
name: "large id",
|
||||
id: 9223372036854775807,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := stream.Create(ctx, test.id)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error creating stream: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamer_Create_Multiple(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create multiple streams
|
||||
ids := []int64{1, 2, 3, 100, -5}
|
||||
for _, id := range ids {
|
||||
err := stream.Create(ctx, id)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error creating stream %d: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all streams exist by checking info
|
||||
info := stream.Info(ctx)
|
||||
if len(info.Streams) != len(ids) {
|
||||
t.Errorf("expected %d streams, got %d", len(ids), len(info.Streams))
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if _, exists := info.Streams[id]; !exists {
|
||||
t.Errorf("stream %d not found in info", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamer_Delete(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a stream first
|
||||
id := int64(1)
|
||||
err := stream.Create(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create stream: %v", err)
|
||||
}
|
||||
|
||||
// Delete the stream
|
||||
err = stream.Delete(ctx, id)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error deleting stream: %v", err)
|
||||
}
|
||||
|
||||
// Verify stream is deleted
|
||||
info := stream.Info(ctx)
|
||||
if _, exists := info.Streams[id]; exists {
|
||||
t.Error("stream should have been deleted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamer_Delete_NotFound(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Try to delete non-existent stream
|
||||
err := stream.Delete(ctx, 999)
|
||||
if !errors.Is(err, ErrStreamNotFound) {
|
||||
t.Errorf("expected ErrStreamNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamer_Write(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a stream
|
||||
id := int64(1)
|
||||
err := stream.Create(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create stream: %v", err)
|
||||
}
|
||||
|
||||
// Write to the stream
|
||||
line := &Line{
|
||||
Number: 1,
|
||||
Message: "test message",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
err = stream.Write(ctx, id, line)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error writing to stream: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamer_Write_NotFound(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Try to write to non-existent stream
|
||||
line := &Line{
|
||||
Number: 1,
|
||||
Message: "test message",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
err := stream.Write(ctx, 999, line)
|
||||
if !errors.Is(err, ErrStreamNotFound) {
|
||||
t.Errorf("expected ErrStreamNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamer_Tail(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a stream
|
||||
id := int64(1)
|
||||
err := stream.Create(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create stream: %v", err)
|
||||
}
|
||||
|
||||
// Start tailing
|
||||
lines, errs := stream.Tail(ctx, id)
|
||||
if lines == nil || errs == nil {
|
||||
t.Fatal("expected non-nil channels")
|
||||
}
|
||||
|
||||
// Write a line
|
||||
line := &Line{
|
||||
Number: 1,
|
||||
Message: "test message",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
err = stream.Write(ctx, id, line)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write to stream: %v", err)
|
||||
}
|
||||
|
||||
// Read the line
|
||||
select {
|
||||
case receivedLine := <-lines:
|
||||
if receivedLine.Number != line.Number {
|
||||
t.Errorf("expected number %d, got %d", line.Number, receivedLine.Number)
|
||||
}
|
||||
if receivedLine.Message != line.Message {
|
||||
t.Errorf("expected message %q, got %q", line.Message, receivedLine.Message)
|
||||
}
|
||||
if receivedLine.Timestamp != line.Timestamp {
|
||||
t.Errorf("expected timestamp %d, got %d", line.Timestamp, receivedLine.Timestamp)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamer_Tail_NotFound(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Try to tail non-existent stream
|
||||
lines, errs := stream.Tail(ctx, 999)
|
||||
if lines != nil || errs != nil {
|
||||
t.Error("expected nil channels for non-existent stream")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamer_Tail_History(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a stream
|
||||
id := int64(1)
|
||||
err := stream.Create(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create stream: %v", err)
|
||||
}
|
||||
|
||||
// Write some lines before tailing
|
||||
lines := []*Line{
|
||||
{Number: 1, Message: "line 1", Timestamp: 1},
|
||||
{Number: 2, Message: "line 2", Timestamp: 2},
|
||||
{Number: 3, Message: "line 3", Timestamp: 3},
|
||||
}
|
||||
|
||||
for _, line := range lines {
|
||||
err = stream.Write(ctx, id, line)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write line: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Start tailing
|
||||
lineChan, _ := stream.Tail(ctx, id)
|
||||
|
||||
// Should receive all historical lines
|
||||
for i, expectedLine := range lines {
|
||||
select {
|
||||
case receivedLine := <-lineChan:
|
||||
if receivedLine.Number != expectedLine.Number {
|
||||
t.Errorf("line %d: expected number %d, got %d", i, expectedLine.Number, receivedLine.Number)
|
||||
}
|
||||
if receivedLine.Message != expectedLine.Message {
|
||||
t.Errorf("line %d: expected message %q, got %q", i, expectedLine.Message, receivedLine.Message)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("timeout waiting for historical line %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamer_Info(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Initially should have no streams
|
||||
info := stream.Info(ctx)
|
||||
if len(info.Streams) != 0 {
|
||||
t.Errorf("expected 0 streams, got %d", len(info.Streams))
|
||||
}
|
||||
|
||||
// Create some streams
|
||||
ids := []int64{1, 2, 3}
|
||||
for _, id := range ids {
|
||||
err := stream.Create(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create stream %d: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check info again
|
||||
info = stream.Info(ctx)
|
||||
if len(info.Streams) != len(ids) {
|
||||
t.Errorf("expected %d streams, got %d", len(ids), len(info.Streams))
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if count, exists := info.Streams[id]; !exists {
|
||||
t.Errorf("stream %d not found in info", id)
|
||||
} else if count != 0 {
|
||||
t.Errorf("expected 0 subscribers for stream %d, got %d", id, count)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamer_ConcurrentAccess(t *testing.T) {
|
||||
stream := NewMemory()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a stream
|
||||
id := int64(1)
|
||||
err := stream.Create(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create stream: %v", err)
|
||||
}
|
||||
|
||||
// Start multiple goroutines writing to the stream
|
||||
done := make(chan bool)
|
||||
numWriters := 10
|
||||
linesPerWriter := 100
|
||||
|
||||
for i := 0; i < numWriters; i++ {
|
||||
go func(writerID int) {
|
||||
defer func() { done <- true }()
|
||||
for j := 0; j < linesPerWriter; j++ {
|
||||
line := &Line{
|
||||
Number: writerID*linesPerWriter + j,
|
||||
Message: "concurrent message",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
err := stream.Write(ctx, id, line)
|
||||
if err != nil {
|
||||
t.Errorf("writer %d: failed to write line %d: %v", writerID, j, err)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all writers to complete
|
||||
for i := 0; i < numWriters; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Verify stream still exists
|
||||
info := stream.Info(ctx)
|
||||
if _, exists := info.Streams[id]; !exists {
|
||||
t.Error("stream should still exist after concurrent writes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrStreamNotFound(t *testing.T) {
|
||||
expectedMsg := "stream: not found"
|
||||
if ErrStreamNotFound.Error() != expectedMsg {
|
||||
t.Errorf("expected error message %q, got %q", expectedMsg, ErrStreamNotFound.Error())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,318 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package livelog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewStream(t *testing.T) {
|
||||
s := newStream()
|
||||
if s == nil {
|
||||
t.Fatal("expected non-nil stream")
|
||||
}
|
||||
|
||||
if s.list == nil {
|
||||
t.Error("expected non-nil subscriber list")
|
||||
}
|
||||
|
||||
if len(s.list) != 0 {
|
||||
t.Errorf("expected empty subscriber list, got %d subscribers", len(s.list))
|
||||
}
|
||||
|
||||
if len(s.hist) != 0 {
|
||||
t.Errorf("expected empty history, got %d lines", len(s.hist))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStream_Write(t *testing.T) {
|
||||
s := newStream()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
line *Line
|
||||
}{
|
||||
{
|
||||
name: "basic line",
|
||||
line: &Line{
|
||||
Number: 1,
|
||||
Message: "test message",
|
||||
Timestamp: time.Now().Unix(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty message",
|
||||
line: &Line{
|
||||
Number: 2,
|
||||
Message: "",
|
||||
Timestamp: time.Now().Unix(),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zero timestamp",
|
||||
line: &Line{
|
||||
Number: 3,
|
||||
Message: "zero timestamp",
|
||||
Timestamp: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := s.write(test.line)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error writing line: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Verify all lines are in history
|
||||
if len(s.hist) != len(tests) {
|
||||
t.Errorf("expected %d lines in history, got %d", len(tests), len(s.hist))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStream_Write_BufferLimit(t *testing.T) {
|
||||
s := newStream()
|
||||
|
||||
// Write more lines than buffer size
|
||||
numLines := bufferSize + 100
|
||||
for i := 0; i < numLines; i++ {
|
||||
line := &Line{
|
||||
Number: i,
|
||||
Message: "test message",
|
||||
Timestamp: int64(i),
|
||||
}
|
||||
err := s.write(line)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error writing line %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// History should be capped at buffer size
|
||||
if len(s.hist) != bufferSize {
|
||||
t.Errorf("expected history size %d, got %d", bufferSize, len(s.hist))
|
||||
}
|
||||
|
||||
// Should contain the most recent lines
|
||||
firstLine := s.hist[0]
|
||||
expectedFirstNumber := numLines - bufferSize
|
||||
if firstLine.Number != expectedFirstNumber {
|
||||
t.Errorf("expected first line number %d, got %d", expectedFirstNumber, firstLine.Number)
|
||||
}
|
||||
|
||||
lastLine := s.hist[len(s.hist)-1]
|
||||
expectedLastNumber := numLines - 1
|
||||
if lastLine.Number != expectedLastNumber {
|
||||
t.Errorf("expected last line number %d, got %d", expectedLastNumber, lastLine.Number)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStream_Subscribe(t *testing.T) {
|
||||
s := newStream()
|
||||
ctx := context.Background()
|
||||
|
||||
// Subscribe to empty stream
|
||||
lineChan, errChan := s.subscribe(ctx)
|
||||
if lineChan == nil {
|
||||
t.Fatal("expected non-nil line channel")
|
||||
}
|
||||
if errChan == nil {
|
||||
t.Fatal("expected non-nil error channel")
|
||||
}
|
||||
|
||||
// Verify subscriber was added
|
||||
if len(s.list) != 1 {
|
||||
t.Errorf("expected 1 subscriber, got %d", len(s.list))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStream_Subscribe_WithHistory(t *testing.T) {
|
||||
s := newStream()
|
||||
ctx := context.Background()
|
||||
|
||||
// Write some lines to history
|
||||
historyLines := []*Line{
|
||||
{Number: 1, Message: "line 1", Timestamp: 1},
|
||||
{Number: 2, Message: "line 2", Timestamp: 2},
|
||||
{Number: 3, Message: "line 3", Timestamp: 3},
|
||||
}
|
||||
|
||||
for _, line := range historyLines {
|
||||
err := s.write(line)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write line: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe and receive history
|
||||
lineChan, _ := s.subscribe(ctx)
|
||||
|
||||
// Should receive all historical lines
|
||||
for i, expectedLine := range historyLines {
|
||||
select {
|
||||
case receivedLine := <-lineChan:
|
||||
if receivedLine.Number != expectedLine.Number {
|
||||
t.Errorf("line %d: expected number %d, got %d", i, expectedLine.Number, receivedLine.Number)
|
||||
}
|
||||
if receivedLine.Message != expectedLine.Message {
|
||||
t.Errorf("line %d: expected message %q, got %q", i, expectedLine.Message, receivedLine.Message)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("timeout waiting for historical line %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStream_Subscribe_NewLines(t *testing.T) {
|
||||
s := newStream()
|
||||
ctx := context.Background()
|
||||
|
||||
// Subscribe first
|
||||
lineChan, _ := s.subscribe(ctx)
|
||||
|
||||
// Write new lines
|
||||
newLines := []*Line{
|
||||
{Number: 1, Message: "new line 1", Timestamp: 1},
|
||||
{Number: 2, Message: "new line 2", Timestamp: 2},
|
||||
}
|
||||
|
||||
for _, line := range newLines {
|
||||
err := s.write(line)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write line: %v", err)
|
||||
}
|
||||
|
||||
// Should receive the new line
|
||||
select {
|
||||
case receivedLine := <-lineChan:
|
||||
if receivedLine.Number != line.Number {
|
||||
t.Errorf("expected number %d, got %d", line.Number, receivedLine.Number)
|
||||
}
|
||||
if receivedLine.Message != line.Message {
|
||||
t.Errorf("expected message %q, got %q", line.Message, receivedLine.Message)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for new line")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStream_Subscribe_ContextCancellation(t *testing.T) {
|
||||
s := newStream()
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Subscribe
|
||||
lineChan, errChan := s.subscribe(ctx)
|
||||
|
||||
// Cancel context
|
||||
cancel()
|
||||
|
||||
// Error channel should close
|
||||
select {
|
||||
case <-errChan:
|
||||
// Expected - error channel should close
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for error channel to close")
|
||||
}
|
||||
|
||||
// Line channel should also be closed eventually
|
||||
select {
|
||||
case _, ok := <-lineChan:
|
||||
if ok {
|
||||
t.Error("expected line channel to be closed")
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for line channel to close")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStream_Close(t *testing.T) {
|
||||
s := newStream()
|
||||
ctx := context.Background()
|
||||
|
||||
// Add some subscribers
|
||||
numSubscribers := 3
|
||||
for i := 0; i < numSubscribers; i++ {
|
||||
s.subscribe(ctx)
|
||||
}
|
||||
|
||||
// Verify subscribers exist
|
||||
if len(s.list) != numSubscribers {
|
||||
t.Errorf("expected %d subscribers, got %d", numSubscribers, len(s.list))
|
||||
}
|
||||
|
||||
// Close the stream
|
||||
err := s.close()
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error closing stream: %v", err)
|
||||
}
|
||||
|
||||
// All subscribers should be removed
|
||||
if len(s.list) != 0 {
|
||||
t.Errorf("expected 0 subscribers after close, got %d", len(s.list))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStream_MultipleSubscribers(t *testing.T) {
|
||||
s := newStream()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create multiple subscribers
|
||||
numSubscribers := 5
|
||||
channels := make([]<-chan *Line, numSubscribers)
|
||||
|
||||
for i := 0; i < numSubscribers; i++ {
|
||||
lineChan, _ := s.subscribe(ctx)
|
||||
channels[i] = lineChan
|
||||
}
|
||||
|
||||
// Write a line
|
||||
line := &Line{
|
||||
Number: 1,
|
||||
Message: "broadcast message",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
err := s.write(line)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write line: %v", err)
|
||||
}
|
||||
|
||||
// All subscribers should receive the line
|
||||
for i, ch := range channels {
|
||||
select {
|
||||
case receivedLine := <-ch:
|
||||
if receivedLine.Number != line.Number {
|
||||
t.Errorf("subscriber %d: expected number %d, got %d", i, line.Number, receivedLine.Number)
|
||||
}
|
||||
if receivedLine.Message != line.Message {
|
||||
t.Errorf("subscriber %d: expected message %q, got %q", i, line.Message, receivedLine.Message)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("subscriber %d: timeout waiting for line", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBufferSize(t *testing.T) {
|
||||
expectedSize := 5000
|
||||
if bufferSize != expectedSize {
|
||||
t.Errorf("expected buffer size %d, got %d", expectedSize, bufferSize)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package livelog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSubscriber_Publish(t *testing.T) {
|
||||
sub := &subscriber{
|
||||
handler: make(chan *Line, bufferSize),
|
||||
closec: make(chan struct{}),
|
||||
closed: false,
|
||||
}
|
||||
|
||||
line := &Line{
|
||||
Number: 1,
|
||||
Message: "test message",
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
|
||||
// Publish line
|
||||
sub.publish(line)
|
||||
|
||||
// Should receive the line
|
||||
select {
|
||||
case receivedLine := <-sub.handler:
|
||||
if receivedLine.Number != line.Number {
|
||||
t.Errorf("expected number %d, got %d", line.Number, receivedLine.Number)
|
||||
}
|
||||
if receivedLine.Message != line.Message {
|
||||
t.Errorf("expected message %q, got %q", line.Message, receivedLine.Message)
|
||||
}
|
||||
if receivedLine.Timestamp != line.Timestamp {
|
||||
t.Errorf("expected timestamp %d, got %d", line.Timestamp, receivedLine.Timestamp)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Error("timeout waiting for published line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriber_Publish_Multiple(t *testing.T) {
|
||||
sub := &subscriber{
|
||||
handler: make(chan *Line, bufferSize),
|
||||
closec: make(chan struct{}),
|
||||
closed: false,
|
||||
}
|
||||
|
||||
lines := []*Line{
|
||||
{Number: 1, Message: "line 1", Timestamp: 1},
|
||||
{Number: 2, Message: "line 2", Timestamp: 2},
|
||||
{Number: 3, Message: "line 3", Timestamp: 3},
|
||||
}
|
||||
|
||||
// Publish all lines
|
||||
for _, line := range lines {
|
||||
sub.publish(line)
|
||||
}
|
||||
|
||||
// Should receive all lines in order
|
||||
for i, expectedLine := range lines {
|
||||
select {
|
||||
case receivedLine := <-sub.handler:
|
||||
if receivedLine.Number != expectedLine.Number {
|
||||
t.Errorf("line %d: expected number %d, got %d", i, expectedLine.Number, receivedLine.Number)
|
||||
}
|
||||
if receivedLine.Message != expectedLine.Message {
|
||||
t.Errorf("line %d: expected message %q, got %q", i, expectedLine.Message, receivedLine.Message)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Errorf("timeout waiting for line %d", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriber_Publish_BufferFull(t *testing.T) {
|
||||
// Create subscriber with small buffer for testing
|
||||
sub := &subscriber{
|
||||
handler: make(chan *Line, 2), // Small buffer
|
||||
closec: make(chan struct{}),
|
||||
closed: false,
|
||||
}
|
||||
|
||||
// Fill the buffer
|
||||
line1 := &Line{Number: 1, Message: "line 1", Timestamp: 1}
|
||||
line2 := &Line{Number: 2, Message: "line 2", Timestamp: 2}
|
||||
line3 := &Line{Number: 3, Message: "line 3", Timestamp: 3}
|
||||
|
||||
sub.publish(line1)
|
||||
sub.publish(line2)
|
||||
|
||||
// Buffer should be full now, third publish should not block
|
||||
// (it should be dropped due to default case in select)
|
||||
sub.publish(line3)
|
||||
|
||||
// Should receive first two lines
|
||||
receivedLine1 := <-sub.handler
|
||||
if receivedLine1.Number != 1 {
|
||||
t.Errorf("expected first line number 1, got %d", receivedLine1.Number)
|
||||
}
|
||||
|
||||
receivedLine2 := <-sub.handler
|
||||
if receivedLine2.Number != 2 {
|
||||
t.Errorf("expected second line number 2, got %d", receivedLine2.Number)
|
||||
}
|
||||
|
||||
// Channel should be empty now (third line was dropped)
|
||||
select {
|
||||
case <-sub.handler:
|
||||
t.Error("unexpected line received (should have been dropped)")
|
||||
default:
|
||||
// Expected - no more lines
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriber_Close(t *testing.T) {
|
||||
sub := &subscriber{
|
||||
handler: make(chan *Line, bufferSize),
|
||||
closec: make(chan struct{}),
|
||||
closed: false,
|
||||
}
|
||||
|
||||
// Close the subscriber
|
||||
sub.close()
|
||||
|
||||
// Should be marked as closed
|
||||
if !sub.closed {
|
||||
t.Error("subscriber should be marked as closed")
|
||||
}
|
||||
|
||||
// Channels should be closed
|
||||
select {
|
||||
case <-sub.closec:
|
||||
// Expected - close channel should be closed
|
||||
default:
|
||||
t.Error("close channel should be closed")
|
||||
}
|
||||
|
||||
select {
|
||||
case _, ok := <-sub.handler:
|
||||
if ok {
|
||||
t.Error("handler channel should be closed")
|
||||
}
|
||||
default:
|
||||
t.Error("handler channel should be closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriber_Close_Multiple(t *testing.T) {
|
||||
sub := &subscriber{
|
||||
handler: make(chan *Line, bufferSize),
|
||||
closec: make(chan struct{}),
|
||||
closed: false,
|
||||
}
|
||||
|
||||
// Close multiple times should not panic
|
||||
sub.close()
|
||||
sub.close()
|
||||
sub.close()
|
||||
|
||||
// Should still be marked as closed
|
||||
if !sub.closed {
|
||||
t.Error("subscriber should be marked as closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriber_Publish_AfterClose(_ *testing.T) {
|
||||
sub := &subscriber{
|
||||
handler: make(chan *Line, bufferSize),
|
||||
closec: make(chan struct{}),
|
||||
closed: false,
|
||||
}
|
||||
|
||||
// Close the subscriber
|
||||
sub.close()
|
||||
|
||||
// Publishing after close should not panic (due to recover in publish)
|
||||
line := &Line{Number: 1, Message: "test", Timestamp: 1}
|
||||
sub.publish(line) // Should not panic
|
||||
}
|
||||
|
||||
func TestSubscriber_Publish_ClosedChannel(_ *testing.T) {
|
||||
sub := &subscriber{
|
||||
handler: make(chan *Line, bufferSize),
|
||||
closec: make(chan struct{}),
|
||||
closed: false,
|
||||
}
|
||||
|
||||
// Close the close channel manually to simulate the condition
|
||||
close(sub.closec)
|
||||
|
||||
line := &Line{Number: 1, Message: "test", Timestamp: 1}
|
||||
|
||||
// Should not block or panic when closec is closed
|
||||
sub.publish(line)
|
||||
}
|
||||
|
||||
func TestSubscriber_InitialState(t *testing.T) {
|
||||
sub := &subscriber{
|
||||
handler: make(chan *Line, bufferSize),
|
||||
closec: make(chan struct{}),
|
||||
closed: false,
|
||||
}
|
||||
|
||||
// Initial state checks
|
||||
if sub.closed {
|
||||
t.Error("subscriber should not be closed initially")
|
||||
}
|
||||
|
||||
if sub.handler == nil {
|
||||
t.Error("handler channel should not be nil")
|
||||
}
|
||||
|
||||
if sub.closec == nil {
|
||||
t.Error("close channel should not be nil")
|
||||
}
|
||||
|
||||
// Channels should be open
|
||||
select {
|
||||
case <-sub.closec:
|
||||
t.Error("close channel should not be closed initially")
|
||||
default:
|
||||
// Expected
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriber_ConcurrentPublish(t *testing.T) {
|
||||
sub := &subscriber{
|
||||
handler: make(chan *Line, bufferSize),
|
||||
closec: make(chan struct{}),
|
||||
closed: false,
|
||||
}
|
||||
|
||||
// Start multiple goroutines publishing concurrently
|
||||
numGoroutines := 10
|
||||
linesPerGoroutine := 100
|
||||
done := make(chan bool, numGoroutines)
|
||||
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
go func(id int) {
|
||||
defer func() { done <- true }()
|
||||
for j := 0; j < linesPerGoroutine; j++ {
|
||||
line := &Line{
|
||||
Number: id*linesPerGoroutine + j,
|
||||
Message: "concurrent message",
|
||||
Timestamp: int64(j),
|
||||
}
|
||||
sub.publish(line)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
<-done
|
||||
}
|
||||
|
||||
// Drain the channel and count received lines
|
||||
receivedCount := 0
|
||||
for {
|
||||
select {
|
||||
case <-sub.handler:
|
||||
receivedCount++
|
||||
default:
|
||||
goto done
|
||||
}
|
||||
}
|
||||
done:
|
||||
|
||||
// Should have received some lines (may not be all due to buffer limits)
|
||||
if receivedCount == 0 {
|
||||
t.Error("should have received at least some lines")
|
||||
}
|
||||
|
||||
// Should not have received more than total sent
|
||||
totalSent := numGoroutines * linesPerGoroutine
|
||||
if receivedCount > totalSent {
|
||||
t.Errorf("received more lines (%d) than sent (%d)", receivedCount, totalSent)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package lock
|
||||
|
||||
import "testing"
|
||||
|
||||
const myKey = "mykey"
|
||||
|
||||
func TestFormatKey(t *testing.T) {
|
||||
t.Run("format with all parts", func(t *testing.T) {
|
||||
result := formatKey("myapp", "mynamespace", myKey)
|
||||
expected := "myapp:mynamespace:mykey"
|
||||
if result != expected {
|
||||
t.Errorf("expected %s, got %s", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("format with empty app", func(t *testing.T) {
|
||||
result := formatKey("", "mynamespace", myKey)
|
||||
expected := ":mynamespace:mykey"
|
||||
if result != expected {
|
||||
t.Errorf("expected %s, got %s", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("format with empty namespace", func(t *testing.T) {
|
||||
result := formatKey("myapp", "", myKey)
|
||||
expected := "myapp::mykey"
|
||||
if result != expected {
|
||||
t.Errorf("expected %s, got %s", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("format with empty key", func(t *testing.T) {
|
||||
result := formatKey("myapp", "mynamespace", "")
|
||||
expected := "myapp:mynamespace:"
|
||||
if result != expected {
|
||||
t.Errorf("expected %s, got %s", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("format with all empty", func(t *testing.T) {
|
||||
result := formatKey("", "", "")
|
||||
expected := "::"
|
||||
if result != expected {
|
||||
t.Errorf("expected %s, got %s", expected, result)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("format with special characters", func(t *testing.T) {
|
||||
result := formatKey("app-1", "ns_2", "key.3")
|
||||
expected := "app-1:ns_2:key.3"
|
||||
if result != expected {
|
||||
t.Errorf("expected %s, got %s", expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSplitKey(t *testing.T) {
|
||||
t.Run("split valid key with three parts", func(t *testing.T) {
|
||||
namespace, key := SplitKey("myapp:mynamespace:mykey")
|
||||
if namespace != "mynamespace" {
|
||||
t.Errorf("expected namespace to be 'mynamespace', got '%s'", namespace)
|
||||
}
|
||||
if key != myKey {
|
||||
t.Errorf("expected key to be 'mykey', got '%s'", key)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("split key with more than three parts", func(t *testing.T) {
|
||||
namespace, key := SplitKey("myapp:mynamespace:mykey:extra")
|
||||
if namespace != "mynamespace" {
|
||||
t.Errorf("expected namespace to be 'mynamespace', got '%s'", namespace)
|
||||
}
|
||||
// SplitKey only takes the third part, not everything after
|
||||
if key != myKey {
|
||||
t.Errorf("expected key to be 'mykey', got '%s'", key)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("split key with two parts", func(t *testing.T) {
|
||||
namespace, key := SplitKey("myapp:mynamespace")
|
||||
if namespace != "" {
|
||||
t.Errorf("expected namespace to be empty, got '%s'", namespace)
|
||||
}
|
||||
if key != "myapp:mynamespace" {
|
||||
t.Errorf("expected key to be 'myapp:mynamespace', got '%s'", key)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("split key with one part", func(t *testing.T) {
|
||||
namespace, key := SplitKey(myKey)
|
||||
if namespace != "" {
|
||||
t.Errorf("expected namespace to be empty, got '%s'", namespace)
|
||||
}
|
||||
if key != myKey {
|
||||
t.Errorf("expected key to be 'mykey', got '%s'", key)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("split empty key", func(t *testing.T) {
|
||||
namespace, key := SplitKey("")
|
||||
if namespace != "" {
|
||||
t.Errorf("expected namespace to be empty, got '%s'", namespace)
|
||||
}
|
||||
if key != "" {
|
||||
t.Errorf("expected key to be empty, got '%s'", key)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("split key with empty parts", func(t *testing.T) {
|
||||
namespace, key := SplitKey("myapp::mykey")
|
||||
if namespace != "" {
|
||||
t.Errorf("expected namespace to be empty, got '%s'", namespace)
|
||||
}
|
||||
if key != myKey {
|
||||
t.Errorf("expected key to be 'mykey', got '%s'", key)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFormatAndSplitKey_RoundTrip(t *testing.T) {
|
||||
testCases := []struct {
|
||||
app string
|
||||
namespace string
|
||||
key string
|
||||
}{
|
||||
{"myapp", "mynamespace", myKey},
|
||||
{"app1", "ns1", "key1"},
|
||||
{"", "ns", "key"},
|
||||
{"app", "", "key"},
|
||||
{"app", "ns", ""},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.app+":"+tc.namespace+":"+tc.key, func(t *testing.T) {
|
||||
formatted := formatKey(tc.app, tc.namespace, tc.key)
|
||||
ns, k := SplitKey(formatted)
|
||||
|
||||
if ns != tc.namespace {
|
||||
t.Errorf("namespace mismatch: expected '%s', got '%s'", tc.namespace, ns)
|
||||
}
|
||||
if k != tc.key {
|
||||
t.Errorf("key mismatch: expected '%s', got '%s'", tc.key, k)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,390 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
func TestWithRequestID(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
requestID string
|
||||
}{
|
||||
{
|
||||
name: "normal request ID",
|
||||
requestID: "req-123456",
|
||||
},
|
||||
{
|
||||
name: "empty request ID",
|
||||
requestID: "",
|
||||
},
|
||||
{
|
||||
name: "UUID request ID",
|
||||
requestID: "550e8400-e29b-41d4-a716-446655440000",
|
||||
},
|
||||
{
|
||||
name: "long request ID",
|
||||
requestID: strings.Repeat("a", 100),
|
||||
},
|
||||
{
|
||||
name: "special characters",
|
||||
requestID: "req-123!@#$%^&*()",
|
||||
},
|
||||
{
|
||||
name: "unicode request ID",
|
||||
requestID: "req-世界-🌍",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create a buffer to capture log output
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
// Apply the WithRequestID option
|
||||
option := WithRequestID(tt.requestID)
|
||||
logCtx := logger.With()
|
||||
logCtx = option(logCtx)
|
||||
|
||||
// Log a message to test the option
|
||||
logger = logCtx.Logger()
|
||||
logger.Info().Msg("test message")
|
||||
|
||||
// Parse the log output
|
||||
var logEntry map[string]interface{}
|
||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse log output: %v", err)
|
||||
}
|
||||
|
||||
// Check if request_id is present and correct
|
||||
if requestID, ok := logEntry["request_id"]; ok {
|
||||
if requestID != tt.requestID {
|
||||
t.Errorf("Expected request_id %q, got %q", tt.requestID, requestID)
|
||||
}
|
||||
} else {
|
||||
t.Error("request_id field not found in log output")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateContext(t *testing.T) {
|
||||
// Create a buffer to capture log output
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
// Create a context with the logger
|
||||
ctx := logger.WithContext(context.Background())
|
||||
|
||||
// Test updating context with request ID
|
||||
UpdateContext(ctx, WithRequestID("test-req-123"))
|
||||
|
||||
// Log a message using the updated context
|
||||
zerolog.Ctx(ctx).Info().Msg("test message")
|
||||
|
||||
// Parse the log output
|
||||
var logEntry map[string]interface{}
|
||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse log output: %v", err)
|
||||
}
|
||||
|
||||
// Check if request_id is present
|
||||
if requestID, ok := logEntry["request_id"]; ok {
|
||||
if requestID != "test-req-123" {
|
||||
t.Errorf("Expected request_id %q, got %q", "test-req-123", requestID)
|
||||
}
|
||||
} else {
|
||||
t.Error("request_id field not found in log output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateContextMultipleOptions(t *testing.T) {
|
||||
// Create a buffer to capture log output
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
// Create a context with the logger
|
||||
ctx := logger.WithContext(context.Background())
|
||||
|
||||
// Create multiple options
|
||||
option1 := WithRequestID("req-456")
|
||||
option2 := func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str("user_id", "user-789")
|
||||
}
|
||||
option3 := func(c zerolog.Context) zerolog.Context {
|
||||
return c.Int("version", 1)
|
||||
}
|
||||
|
||||
// Update context with multiple options
|
||||
UpdateContext(ctx, option1, option2, option3)
|
||||
|
||||
// Log a message using the updated context
|
||||
zerolog.Ctx(ctx).Info().Msg("test message")
|
||||
|
||||
// Parse the log output
|
||||
var logEntry map[string]interface{}
|
||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse log output: %v", err)
|
||||
}
|
||||
|
||||
// Check all fields are present
|
||||
if requestID, ok := logEntry["request_id"]; !ok || requestID != "req-456" {
|
||||
t.Errorf("Expected request_id %q, got %q", "req-456", requestID)
|
||||
}
|
||||
if userID, ok := logEntry["user_id"]; !ok || userID != "user-789" {
|
||||
t.Errorf("Expected user_id %q, got %q", "user-789", userID)
|
||||
}
|
||||
if version, ok := logEntry["version"]; !ok || version != float64(1) {
|
||||
t.Errorf("Expected version %v, got %v", 1, version)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewContext(t *testing.T) {
|
||||
// Create a buffer to capture log output
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
// Create a parent context with the logger
|
||||
parentCtx := logger.WithContext(context.Background())
|
||||
|
||||
// Create a new context with request ID
|
||||
childCtx := NewContext(parentCtx, WithRequestID("child-req-123"))
|
||||
|
||||
// Verify that the parent context is not modified
|
||||
zerolog.Ctx(parentCtx).Info().Msg("parent message")
|
||||
|
||||
// Parse the parent log output
|
||||
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
|
||||
if len(lines) > 0 {
|
||||
var parentLogEntry map[string]interface{}
|
||||
err := json.Unmarshal([]byte(lines[0]), &parentLogEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse parent log output: %v", err)
|
||||
}
|
||||
|
||||
// Parent should not have request_id
|
||||
if _, ok := parentLogEntry["request_id"]; ok {
|
||||
t.Error("Parent context should not have request_id")
|
||||
}
|
||||
}
|
||||
|
||||
// Clear buffer for child test
|
||||
buf.Reset()
|
||||
|
||||
// Log a message using the child context
|
||||
zerolog.Ctx(childCtx).Info().Msg("child message")
|
||||
|
||||
// Parse the child log output
|
||||
var childLogEntry map[string]interface{}
|
||||
err := json.Unmarshal(buf.Bytes(), &childLogEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse child log output: %v", err)
|
||||
}
|
||||
|
||||
// Child should have request_id
|
||||
if requestID, ok := childLogEntry["request_id"]; !ok || requestID != "child-req-123" {
|
||||
t.Errorf("Expected child request_id %q, got %q", "child-req-123", requestID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewContextMultipleOptions(t *testing.T) {
|
||||
// Create a buffer to capture log output
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
// Create a parent context with the logger
|
||||
parentCtx := logger.WithContext(context.Background())
|
||||
|
||||
// Create multiple options
|
||||
option1 := WithRequestID("new-req-789")
|
||||
option2 := func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str("service", "test-service")
|
||||
}
|
||||
option3 := func(c zerolog.Context) zerolog.Context {
|
||||
return c.Bool("debug", true)
|
||||
}
|
||||
|
||||
// Create a new context with multiple options
|
||||
childCtx := NewContext(parentCtx, option1, option2, option3)
|
||||
|
||||
// Log a message using the child context
|
||||
zerolog.Ctx(childCtx).Info().Msg("test message")
|
||||
|
||||
// Parse the log output
|
||||
var logEntry map[string]interface{}
|
||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse log output: %v", err)
|
||||
}
|
||||
|
||||
// Check all fields are present
|
||||
if requestID, ok := logEntry["request_id"]; !ok || requestID != "new-req-789" {
|
||||
t.Errorf("Expected request_id %q, got %q", "new-req-789", requestID)
|
||||
}
|
||||
if service, ok := logEntry["service"]; !ok || service != "test-service" {
|
||||
t.Errorf("Expected service %q, got %q", "test-service", service)
|
||||
}
|
||||
if debug, ok := logEntry["debug"]; !ok || debug != true {
|
||||
t.Errorf("Expected debug %v, got %v", true, debug)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewContextIsolation(t *testing.T) {
|
||||
// Create a buffer to capture log output
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
// Create a parent context with the logger
|
||||
parentCtx := logger.WithContext(context.Background())
|
||||
|
||||
// Create two child contexts with different request IDs
|
||||
child1Ctx := NewContext(parentCtx, WithRequestID("child1-req"))
|
||||
child2Ctx := NewContext(parentCtx, WithRequestID("child2-req"))
|
||||
|
||||
// Log from child1
|
||||
zerolog.Ctx(child1Ctx).Info().Msg("child1 message")
|
||||
|
||||
// Parse child1 log
|
||||
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
|
||||
var child1LogEntry map[string]interface{}
|
||||
err := json.Unmarshal([]byte(lines[0]), &child1LogEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse child1 log output: %v", err)
|
||||
}
|
||||
|
||||
if requestID, ok := child1LogEntry["request_id"]; !ok || requestID != "child1-req" {
|
||||
t.Errorf("Expected child1 request_id %q, got %q", "child1-req", requestID)
|
||||
}
|
||||
|
||||
// Clear buffer
|
||||
buf.Reset()
|
||||
|
||||
// Log from child2
|
||||
zerolog.Ctx(child2Ctx).Info().Msg("child2 message")
|
||||
|
||||
// Parse child2 log
|
||||
var child2LogEntry map[string]interface{}
|
||||
err = json.Unmarshal(buf.Bytes(), &child2LogEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse child2 log output: %v", err)
|
||||
}
|
||||
|
||||
if requestID, ok := child2LogEntry["request_id"]; !ok || requestID != "child2-req" {
|
||||
t.Errorf("Expected child2 request_id %q, got %q", "child2-req", requestID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomOption(t *testing.T) {
|
||||
// Create a custom option
|
||||
customOption := func(c zerolog.Context) zerolog.Context {
|
||||
return c.Str("custom_field", "custom_value").Int("number", 42)
|
||||
}
|
||||
|
||||
// Create a buffer to capture log output
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
// Create a context with the logger
|
||||
ctx := logger.WithContext(context.Background())
|
||||
|
||||
// Create a new context with custom option
|
||||
newCtx := NewContext(ctx, customOption)
|
||||
|
||||
// Log a message using the new context
|
||||
zerolog.Ctx(newCtx).Info().Msg("test message")
|
||||
|
||||
// Parse the log output
|
||||
var logEntry map[string]interface{}
|
||||
err := json.Unmarshal(buf.Bytes(), &logEntry)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to parse log output: %v", err)
|
||||
}
|
||||
|
||||
// Check custom fields are present
|
||||
if customField, ok := logEntry["custom_field"]; !ok || customField != "custom_value" {
|
||||
t.Errorf("Expected custom_field %q, got %q", "custom_value", customField)
|
||||
}
|
||||
if number, ok := logEntry["number"]; !ok || number != float64(42) {
|
||||
t.Errorf("Expected number %v, got %v", 42, number)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyOptions(t *testing.T) {
|
||||
// Create a buffer to capture log output
|
||||
var buf bytes.Buffer
|
||||
logger := zerolog.New(&buf)
|
||||
|
||||
// Create a context with the logger
|
||||
ctx := logger.WithContext(context.Background())
|
||||
|
||||
// Test UpdateContext with no options
|
||||
UpdateContext(ctx)
|
||||
|
||||
// Test NewContext with no options
|
||||
newCtx := NewContext(ctx)
|
||||
|
||||
// Log messages from both contexts
|
||||
zerolog.Ctx(ctx).Info().Msg("original context")
|
||||
buf.Reset()
|
||||
zerolog.Ctx(newCtx).Info().Msg("new context")
|
||||
|
||||
// Both should work without errors
|
||||
if buf.Len() == 0 {
|
||||
t.Error("Expected log output from new context")
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests.
|
||||
func BenchmarkWithRequestID(b *testing.B) {
|
||||
logger := zerolog.New(bytes.NewBuffer(nil))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
option := WithRequestID("benchmark-req-123")
|
||||
logCtx := logger.With()
|
||||
_ = option(logCtx)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUpdateContext(b *testing.B) {
|
||||
logger := zerolog.New(bytes.NewBuffer(nil))
|
||||
ctx := logger.WithContext(context.Background())
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
UpdateContext(ctx, WithRequestID("benchmark-req-123"))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNewContext(b *testing.B) {
|
||||
logger := zerolog.New(bytes.NewBuffer(nil))
|
||||
ctx := logger.WithContext(context.Background())
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewContext(ctx, WithRequestID("benchmark-req-123"))
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ var _ = ginkgo.BeforeSuite(func() {
|
|||
|
||||
// Ensure we have a valid token.
|
||||
if TestConfig.Password == "" {
|
||||
ginkgo.Fail("No authentication token provided in REGISTRY_PASSWORD environment variable")
|
||||
ginkgo.Skip("Skipping integration tests: REGISTRY_PASSWORD environment variable not set")
|
||||
}
|
||||
|
||||
// Initialize client with auth token.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"start_time": "2025-10-19T23:10:03.732387-07:00",
|
||||
"end_time": "2025-10-19T23:10:03.733578-07:00",
|
||||
"test_results": [],
|
||||
"summary": {
|
||||
"passed": 0,
|
||||
"failed": 0,
|
||||
"pending": 0,
|
||||
"skipped": 0,
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ var _ = ginkgo.BeforeSuite(func() {
|
|||
|
||||
// Ensure we have a valid token.
|
||||
if TestConfig.Password == "" {
|
||||
ginkgo.Fail("No authentication token provided in REGISTRY_PASSWORD environment variable")
|
||||
ginkgo.Skip("Skipping integration tests: REGISTRY_PASSWORD environment variable not set")
|
||||
}
|
||||
|
||||
// Initialize client with auth token.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"start_time": "2025-10-19T23:10:03.849483-07:00",
|
||||
"end_time": "2025-10-19T23:10:03.852307-07:00",
|
||||
"test_results": [],
|
||||
"summary": {
|
||||
"passed": 0,
|
||||
"failed": 0,
|
||||
"pending": 0,
|
||||
"skipped": 0,
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ var _ = ginkgo.BeforeSuite(func() {
|
|||
|
||||
// Ensure we have a valid token.
|
||||
if TestConfig.Password == "" {
|
||||
ginkgo.Fail("No authentication token provided in REGISTRY_PASSWORD environment variable")
|
||||
ginkgo.Skip("Skipping integration tests: REGISTRY_PASSWORD environment variable not set")
|
||||
}
|
||||
|
||||
// Initialize client with auth token.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"start_time": "2025-10-19T23:10:03.959929-07:00",
|
||||
"end_time": "2025-10-19T23:10:03.960841-07:00",
|
||||
"test_results": [],
|
||||
"summary": {
|
||||
"passed": 0,
|
||||
"failed": 0,
|
||||
"pending": 0,
|
||||
"skipped": 0,
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
|
|
@ -46,7 +46,7 @@ var _ = ginkgo.BeforeSuite(func() {
|
|||
|
||||
// Ensure we have a valid token.
|
||||
if TestConfig.Password == "" {
|
||||
ginkgo.Fail("No authentication token provided in REGISTRY_PASSWORD environment variable")
|
||||
ginkgo.Skip("Skipping integration tests: REGISTRY_PASSWORD environment variable not set")
|
||||
}
|
||||
|
||||
// Initialize client with auth token.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"start_time": "2025-10-19T23:10:04.230405-07:00",
|
||||
"end_time": "2025-10-19T23:10:04.231352-07:00",
|
||||
"test_results": [],
|
||||
"summary": {
|
||||
"passed": 0,
|
||||
"failed": 0,
|
||||
"pending": 0,
|
||||
"skipped": 0,
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,234 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLicenses(t *testing.T) {
|
||||
content, err := Licenses()
|
||||
if err != nil {
|
||||
t.Fatalf("Licenses() returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
t.Errorf("Licenses() returned empty content")
|
||||
}
|
||||
|
||||
// Verify it's JSON content
|
||||
contentStr := string(content)
|
||||
if !strings.HasPrefix(contentStr, "[") && !strings.HasPrefix(contentStr, "{") {
|
||||
t.Errorf("Licenses() content doesn't appear to be JSON")
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLicense(t *testing.T) {
|
||||
// Test reading a common license (MIT is usually available)
|
||||
tests := []struct {
|
||||
name string
|
||||
licenseName string
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "read MIT license",
|
||||
licenseName: "mit",
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "read non-existent license",
|
||||
licenseName: "nonexistent-license-xyz",
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
content, err := ReadLicense(tt.licenseName)
|
||||
if tt.expectError {
|
||||
if err == nil {
|
||||
t.Errorf("ReadLicense(%q) expected error but got none", tt.licenseName)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("ReadLicense(%q) returned error: %v", tt.licenseName, err)
|
||||
}
|
||||
if len(content) == 0 {
|
||||
t.Errorf("ReadLicense(%q) returned empty content", tt.licenseName)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitIgnores(t *testing.T) {
|
||||
files, err := GitIgnores()
|
||||
if err != nil {
|
||||
t.Fatalf("GitIgnores() returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
t.Errorf("GitIgnores() returned empty list")
|
||||
}
|
||||
|
||||
// Verify files don't have .gitignore extension
|
||||
for _, file := range files {
|
||||
if strings.HasSuffix(file, ".gitignore") {
|
||||
t.Errorf("GitIgnores() returned file with .gitignore extension: %s", file)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify we have some common gitignore templates
|
||||
hasCommon := false
|
||||
commonTemplates := []string{"Go", "Node", "Python", "Java"}
|
||||
for _, file := range files {
|
||||
for _, common := range commonTemplates {
|
||||
if strings.EqualFold(file, common) {
|
||||
hasCommon = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if hasCommon {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasCommon {
|
||||
t.Logf("Warning: No common gitignore templates found. Available: %v", files)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadGitIgnore(t *testing.T) {
|
||||
// First get the list of available gitignores
|
||||
files, err := GitIgnores()
|
||||
if err != nil {
|
||||
t.Fatalf("GitIgnores() returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
t.Skip("No gitignore files available to test")
|
||||
}
|
||||
|
||||
// Test reading the first available gitignore
|
||||
t.Run("read existing gitignore", func(t *testing.T) {
|
||||
content, err := ReadGitIgnore(files[0])
|
||||
if err != nil {
|
||||
t.Fatalf("ReadGitIgnore(%q) returned error: %v", files[0], err)
|
||||
}
|
||||
if len(content) == 0 {
|
||||
t.Errorf("ReadGitIgnore(%q) returned empty content", files[0])
|
||||
}
|
||||
})
|
||||
|
||||
// Test reading non-existent gitignore
|
||||
t.Run("read non-existent gitignore", func(t *testing.T) {
|
||||
_, err := ReadGitIgnore("nonexistent-gitignore-xyz")
|
||||
if err == nil {
|
||||
t.Errorf("ReadGitIgnore(nonexistent) expected error but got none")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestReadGitIgnoreContent(t *testing.T) {
|
||||
// Get available gitignores
|
||||
files, err := GitIgnores()
|
||||
if err != nil {
|
||||
t.Fatalf("GitIgnores() returned error: %v", err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
t.Skip("No gitignore files available to test")
|
||||
}
|
||||
|
||||
// Test that content is valid gitignore format
|
||||
for _, file := range files[:minInt(5, len(files))] { // Test first 5 files
|
||||
t.Run(file, func(t *testing.T) {
|
||||
content, err := ReadGitIgnore(file)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadGitIgnore(%q) returned error: %v", file, err)
|
||||
}
|
||||
|
||||
// Verify content is not empty
|
||||
if len(content) == 0 {
|
||||
t.Errorf("ReadGitIgnore(%q) returned empty content", file)
|
||||
}
|
||||
|
||||
// Verify content is text (not binary)
|
||||
contentStr := string(content)
|
||||
if len(contentStr) == 0 {
|
||||
t.Errorf("ReadGitIgnore(%q) content is not valid text", file)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLicensesNotEmpty(t *testing.T) {
|
||||
content, err := Licenses()
|
||||
if err != nil {
|
||||
t.Fatalf("Licenses() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Verify content has reasonable size
|
||||
if len(content) < 10 {
|
||||
t.Errorf("Licenses() content seems too small: %d bytes", len(content))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadLicenseFormat(t *testing.T) {
|
||||
// Try to read MIT license and verify it has expected content
|
||||
content, err := ReadLicense("mit")
|
||||
if err != nil {
|
||||
t.Skip("MIT license not available, skipping format test")
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
|
||||
// MIT license should contain certain keywords
|
||||
keywords := []string{"MIT", "Permission", "Copyright"}
|
||||
foundKeywords := 0
|
||||
for _, keyword := range keywords {
|
||||
if strings.Contains(contentStr, keyword) {
|
||||
foundKeywords++
|
||||
}
|
||||
}
|
||||
|
||||
if foundKeywords == 0 {
|
||||
t.Errorf("MIT license content doesn't contain expected keywords")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitIgnoresUnique(t *testing.T) {
|
||||
files, err := GitIgnores()
|
||||
if err != nil {
|
||||
t.Fatalf("GitIgnores() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Verify all files are unique
|
||||
seen := make(map[string]bool)
|
||||
for _, file := range files {
|
||||
if seen[file] {
|
||||
t.Errorf("GitIgnores() returned duplicate file: %s", file)
|
||||
}
|
||||
seen[file] = true
|
||||
}
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
|
@ -15,7 +15,12 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/harness/gitness/store"
|
||||
)
|
||||
|
||||
func TestOffset(t *testing.T) {
|
||||
|
|
@ -86,3 +91,42 @@ func TestLimit(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessSQLErrorf(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("sql.ErrNoRows returns ErrResourceNotFound", func(t *testing.T) {
|
||||
err := ProcessSQLErrorf(ctx, sql.ErrNoRows, "test message")
|
||||
if !errors.Is(err, store.ErrResourceNotFound) {
|
||||
t.Errorf("expected ErrResourceNotFound, got %v", err)
|
||||
}
|
||||
if err.Error() != "test message: resource not found" {
|
||||
t.Errorf("unexpected error message: %v", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("formats message with args", func(t *testing.T) {
|
||||
err := ProcessSQLErrorf(ctx, sql.ErrNoRows, "test %s %d", "message", 42)
|
||||
if !errors.Is(err, store.ErrResourceNotFound) {
|
||||
t.Errorf("expected ErrResourceNotFound, got %v", err)
|
||||
}
|
||||
if err.Error() != "test message 42: resource not found" {
|
||||
t.Errorf("unexpected error message: %v", err.Error())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown error is returned as-is", func(t *testing.T) {
|
||||
originalErr := errors.New("some random error")
|
||||
err := ProcessSQLErrorf(ctx, originalErr, "test message")
|
||||
if !errors.Is(err, originalErr) {
|
||||
t.Errorf("expected original error to be wrapped, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty format string", func(t *testing.T) {
|
||||
err := ProcessSQLErrorf(ctx, sql.ErrNoRows, "")
|
||||
if !errors.Is(err, store.ErrResourceNotFound) {
|
||||
t.Errorf("expected ErrResourceNotFound, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
msg string
|
||||
}{
|
||||
{
|
||||
name: "ErrResourceNotFound",
|
||||
err: ErrResourceNotFound,
|
||||
msg: "resource not found",
|
||||
},
|
||||
{
|
||||
name: "ErrDuplicate",
|
||||
err: ErrDuplicate,
|
||||
msg: "resource is a duplicate",
|
||||
},
|
||||
{
|
||||
name: "ErrForeignKeyViolation",
|
||||
err: ErrForeignKeyViolation,
|
||||
msg: "foreign resource does not exists",
|
||||
},
|
||||
{
|
||||
name: "ErrVersionConflict",
|
||||
err: ErrVersionConflict,
|
||||
msg: "resource version conflict",
|
||||
},
|
||||
{
|
||||
name: "ErrPathTooLong",
|
||||
err: ErrPathTooLong,
|
||||
msg: "the path is too long",
|
||||
},
|
||||
{
|
||||
name: "ErrPrimaryPathAlreadyExists",
|
||||
err: ErrPrimaryPathAlreadyExists,
|
||||
msg: "primary path already exists for resource",
|
||||
},
|
||||
{
|
||||
name: "ErrPrimaryPathRequired",
|
||||
err: ErrPrimaryPathRequired,
|
||||
msg: "path has to be primary",
|
||||
},
|
||||
{
|
||||
name: "ErrAliasPathRequired",
|
||||
err: ErrAliasPathRequired,
|
||||
msg: "path has to be an alias",
|
||||
},
|
||||
{
|
||||
name: "ErrPrimaryPathCantBeDeleted",
|
||||
err: ErrPrimaryPathCantBeDeleted,
|
||||
msg: "primary path can't be deleted",
|
||||
},
|
||||
{
|
||||
name: "ErrNoChangeInRequestedMove",
|
||||
err: ErrNoChangeInRequestedMove,
|
||||
msg: "the requested move doesn't change anything",
|
||||
},
|
||||
{
|
||||
name: "ErrIllegalMoveCyclicHierarchy",
|
||||
err: ErrIllegalMoveCyclicHierarchy,
|
||||
msg: "the requested move is not permitted as it would cause a cyclic dependency",
|
||||
},
|
||||
{
|
||||
name: "ErrSpaceWithChildsCantBeDeleted",
|
||||
err: ErrSpaceWithChildsCantBeDeleted,
|
||||
msg: "the space can't be deleted as it still contains spaces or repos",
|
||||
},
|
||||
{
|
||||
name: "ErrPreConditionFailed",
|
||||
err: ErrPreConditionFailed,
|
||||
msg: "precondition failed",
|
||||
},
|
||||
{
|
||||
name: "ErrLicenseNotFound",
|
||||
err: ErrLicenseNotFound,
|
||||
msg: "license not found",
|
||||
},
|
||||
{
|
||||
name: "ErrLicenseExpired",
|
||||
err: ErrLicenseExpired,
|
||||
msg: "license expired",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.NotNil(t, tt.err, "error should not be nil")
|
||||
assert.Equal(t, tt.msg, tt.err.Error(), "error message should match")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorsAreDistinct(t *testing.T) {
|
||||
// Verify that all errors are distinct
|
||||
allErrors := []error{
|
||||
ErrResourceNotFound,
|
||||
ErrDuplicate,
|
||||
ErrForeignKeyViolation,
|
||||
ErrVersionConflict,
|
||||
ErrPathTooLong,
|
||||
ErrPrimaryPathAlreadyExists,
|
||||
ErrPrimaryPathRequired,
|
||||
ErrAliasPathRequired,
|
||||
ErrPrimaryPathCantBeDeleted,
|
||||
ErrNoChangeInRequestedMove,
|
||||
ErrIllegalMoveCyclicHierarchy,
|
||||
ErrSpaceWithChildsCantBeDeleted,
|
||||
ErrPreConditionFailed,
|
||||
ErrLicenseNotFound,
|
||||
ErrLicenseExpired,
|
||||
}
|
||||
|
||||
for i, err1 := range allErrors {
|
||||
for j, err2 := range allErrors {
|
||||
if i != j {
|
||||
assert.False(t, errors.Is(err1, err2), "errors should be distinct: %v vs %v", err1, err2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorsCanBeCompared(t *testing.T) {
|
||||
// Test that errors can be compared using errors.Is
|
||||
err := ErrResourceNotFound
|
||||
assert.True(t, errors.Is(err, ErrResourceNotFound), "should match ErrResourceNotFound")
|
||||
assert.False(t, errors.Is(err, ErrDuplicate), "should not match ErrDuplicate")
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package stream
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWithConcurrency(t *testing.T) {
|
||||
t.Run("valid concurrency", func(t *testing.T) {
|
||||
config := &ConsumerConfig{}
|
||||
opt := WithConcurrency(10)
|
||||
opt.apply(config)
|
||||
|
||||
if config.Concurrency != 10 {
|
||||
t.Errorf("expected concurrency 10, got %d", config.Concurrency)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("minimum valid concurrency", func(t *testing.T) {
|
||||
config := &ConsumerConfig{}
|
||||
opt := WithConcurrency(1)
|
||||
opt.apply(config)
|
||||
|
||||
if config.Concurrency != 1 {
|
||||
t.Errorf("expected concurrency 1, got %d", config.Concurrency)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("maximum valid concurrency", func(t *testing.T) {
|
||||
config := &ConsumerConfig{}
|
||||
opt := WithConcurrency(MaxConcurrency)
|
||||
opt.apply(config)
|
||||
|
||||
if config.Concurrency != MaxConcurrency {
|
||||
t.Errorf("expected concurrency %d, got %d", MaxConcurrency, config.Concurrency)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero concurrency panics", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for zero concurrency")
|
||||
}
|
||||
}()
|
||||
WithConcurrency(0)
|
||||
})
|
||||
|
||||
t.Run("negative concurrency panics", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for negative concurrency")
|
||||
}
|
||||
}()
|
||||
WithConcurrency(-1)
|
||||
})
|
||||
|
||||
t.Run("concurrency above max panics", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for concurrency above max")
|
||||
}
|
||||
}()
|
||||
WithConcurrency(MaxConcurrency + 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithMaxRetries(t *testing.T) {
|
||||
t.Run("valid max retries", func(t *testing.T) {
|
||||
config := &HandlerConfig{}
|
||||
opt := WithMaxRetries(5)
|
||||
opt.apply(config)
|
||||
|
||||
if config.maxRetries != 5 {
|
||||
t.Errorf("expected maxRetries 5, got %d", config.maxRetries)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero max retries", func(t *testing.T) {
|
||||
config := &HandlerConfig{}
|
||||
opt := WithMaxRetries(0)
|
||||
opt.apply(config)
|
||||
|
||||
if config.maxRetries != 0 {
|
||||
t.Errorf("expected maxRetries 0, got %d", config.maxRetries)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("maximum valid retries", func(t *testing.T) {
|
||||
config := &HandlerConfig{}
|
||||
opt := WithMaxRetries(MaxMaxRetries)
|
||||
opt.apply(config)
|
||||
|
||||
if config.maxRetries != MaxMaxRetries {
|
||||
t.Errorf("expected maxRetries %d, got %d", MaxMaxRetries, config.maxRetries)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative max retries panics", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for negative max retries")
|
||||
}
|
||||
}()
|
||||
WithMaxRetries(-1)
|
||||
})
|
||||
|
||||
t.Run("max retries above max panics", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for max retries above max")
|
||||
}
|
||||
}()
|
||||
WithMaxRetries(MaxMaxRetries + 1)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithIdleTimeout(t *testing.T) {
|
||||
t.Run("valid idle timeout", func(t *testing.T) {
|
||||
config := &HandlerConfig{}
|
||||
timeout := 10 * time.Second
|
||||
opt := WithIdleTimeout(timeout)
|
||||
opt.apply(config)
|
||||
|
||||
if config.idleTimeout != timeout {
|
||||
t.Errorf("expected idleTimeout %v, got %v", timeout, config.idleTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("minimum valid idle timeout", func(t *testing.T) {
|
||||
config := &HandlerConfig{}
|
||||
opt := WithIdleTimeout(MinIdleTimeout)
|
||||
opt.apply(config)
|
||||
|
||||
if config.idleTimeout != MinIdleTimeout {
|
||||
t.Errorf("expected idleTimeout %v, got %v", MinIdleTimeout, config.idleTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("idle timeout below minimum panics", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for idle timeout below minimum")
|
||||
}
|
||||
}()
|
||||
WithIdleTimeout(MinIdleTimeout - 1*time.Second)
|
||||
})
|
||||
|
||||
t.Run("zero idle timeout panics", func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Error("expected panic for zero idle timeout")
|
||||
}
|
||||
}()
|
||||
WithIdleTimeout(0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithHandlerOptions(t *testing.T) {
|
||||
t.Run("applies single handler option", func(t *testing.T) {
|
||||
config := &ConsumerConfig{}
|
||||
opt := WithHandlerOptions(WithMaxRetries(10))
|
||||
opt.apply(config)
|
||||
|
||||
if config.DefaultHandlerConfig.maxRetries != 10 {
|
||||
t.Errorf("expected maxRetries 10, got %d", config.DefaultHandlerConfig.maxRetries)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("applies multiple handler options", func(t *testing.T) {
|
||||
config := &ConsumerConfig{}
|
||||
timeout := 10 * time.Second
|
||||
opt := WithHandlerOptions(
|
||||
WithMaxRetries(5),
|
||||
WithIdleTimeout(timeout),
|
||||
)
|
||||
opt.apply(config)
|
||||
|
||||
if config.DefaultHandlerConfig.maxRetries != 5 {
|
||||
t.Errorf("expected maxRetries 5, got %d", config.DefaultHandlerConfig.maxRetries)
|
||||
}
|
||||
if config.DefaultHandlerConfig.idleTimeout != timeout {
|
||||
t.Errorf("expected idleTimeout %v, got %v", timeout, config.DefaultHandlerConfig.idleTimeout)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("applies no handler options", func(t *testing.T) {
|
||||
config := &ConsumerConfig{}
|
||||
opt := WithHandlerOptions()
|
||||
opt.apply(config)
|
||||
|
||||
// Should not panic and should leave config unchanged
|
||||
if config.DefaultHandlerConfig.maxRetries != 0 {
|
||||
t.Errorf("expected maxRetries 0, got %d", config.DefaultHandlerConfig.maxRetries)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,421 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package enum
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSanitizeString(t *testing.T) {
|
||||
// Test with string type
|
||||
allValues := func() ([]string, string) {
|
||||
return []string{"apple", "banana", "cherry"}, "apple"
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
element string
|
||||
expectedResult string
|
||||
expectedFound bool
|
||||
}{
|
||||
{
|
||||
name: "valid element",
|
||||
element: "banana",
|
||||
expectedResult: "banana",
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "empty element returns default",
|
||||
element: "",
|
||||
expectedResult: "apple",
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "invalid element returns default",
|
||||
element: "grape",
|
||||
expectedResult: "apple",
|
||||
expectedFound: false,
|
||||
},
|
||||
{
|
||||
name: "first element",
|
||||
element: "apple",
|
||||
expectedResult: "apple",
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "last element",
|
||||
element: "cherry",
|
||||
expectedResult: "cherry",
|
||||
expectedFound: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, found := Sanitize(tt.element, allValues)
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("Expected result %q, got %q", tt.expectedResult, result)
|
||||
}
|
||||
if found != tt.expectedFound {
|
||||
t.Errorf("Expected found %v, got %v", tt.expectedFound, found)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeInt(t *testing.T) {
|
||||
// Test with int type
|
||||
allValues := func() ([]int, int) {
|
||||
return []int{1, 3, 5, 7, 9}, 1
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
element int
|
||||
expectedResult int
|
||||
expectedFound bool
|
||||
}{
|
||||
{
|
||||
name: "valid element",
|
||||
element: 5,
|
||||
expectedResult: 5,
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "zero element returns default",
|
||||
element: 0,
|
||||
expectedResult: 1,
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "invalid element returns default",
|
||||
element: 4,
|
||||
expectedResult: 1,
|
||||
expectedFound: false,
|
||||
},
|
||||
{
|
||||
name: "first element",
|
||||
element: 1,
|
||||
expectedResult: 1,
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "last element",
|
||||
element: 9,
|
||||
expectedResult: 9,
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "negative element returns default",
|
||||
element: -1,
|
||||
expectedResult: 1,
|
||||
expectedFound: false,
|
||||
},
|
||||
{
|
||||
name: "large element returns default",
|
||||
element: 100,
|
||||
expectedResult: 1,
|
||||
expectedFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, found := Sanitize(tt.element, allValues)
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("Expected result %d, got %d", tt.expectedResult, result)
|
||||
}
|
||||
if found != tt.expectedFound {
|
||||
t.Errorf("Expected found %v, got %v", tt.expectedFound, found)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeFloat64(t *testing.T) {
|
||||
// Test with float64 type
|
||||
allValues := func() ([]float64, float64) {
|
||||
return []float64{1.1, 2.2, 3.3}, 1.1
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
element float64
|
||||
expectedResult float64
|
||||
expectedFound bool
|
||||
}{
|
||||
{
|
||||
name: "valid element",
|
||||
element: 2.2,
|
||||
expectedResult: 2.2,
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "zero element returns default",
|
||||
element: 0.0,
|
||||
expectedResult: 1.1,
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "invalid element returns default",
|
||||
element: 4.4,
|
||||
expectedResult: 1.1,
|
||||
expectedFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, found := Sanitize(tt.element, allValues)
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("Expected result %f, got %f", tt.expectedResult, result)
|
||||
}
|
||||
if found != tt.expectedFound {
|
||||
t.Errorf("Expected found %v, got %v", tt.expectedFound, found)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeEmptySlice(t *testing.T) {
|
||||
// Test with empty slice
|
||||
allValues := func() ([]string, string) {
|
||||
return []string{}, "default"
|
||||
}
|
||||
|
||||
result, found := Sanitize("any", allValues)
|
||||
if result != "default" {
|
||||
t.Errorf("Expected result %q, got %q", "default", result)
|
||||
}
|
||||
if found {
|
||||
t.Errorf("Expected found to be false, got %v", found)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeEmptyDefault(t *testing.T) {
|
||||
// Test with empty default value
|
||||
allValues := func() ([]string, string) {
|
||||
return []string{"apple", "banana"}, ""
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
element string
|
||||
expectedResult string
|
||||
expectedFound bool
|
||||
}{
|
||||
{
|
||||
name: "valid element",
|
||||
element: "apple",
|
||||
expectedResult: "apple",
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "empty element with empty default",
|
||||
element: "",
|
||||
expectedResult: "",
|
||||
expectedFound: false,
|
||||
},
|
||||
{
|
||||
name: "invalid element returns empty default",
|
||||
element: "grape",
|
||||
expectedResult: "",
|
||||
expectedFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, found := Sanitize(tt.element, allValues)
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("Expected result %q, got %q", tt.expectedResult, result)
|
||||
}
|
||||
if found != tt.expectedFound {
|
||||
t.Errorf("Expected found %v, got %v", tt.expectedFound, found)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSanitizeWithOrder(t *testing.T) {
|
||||
// Test with Order enum type
|
||||
allValues := func() ([]Order, Order) {
|
||||
return []Order{OrderDefault, OrderAsc, OrderDesc}, OrderDefault
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
element Order
|
||||
expectedResult Order
|
||||
expectedFound bool
|
||||
}{
|
||||
{
|
||||
name: "valid order",
|
||||
element: OrderAsc,
|
||||
expectedResult: OrderAsc,
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "zero order returns default",
|
||||
element: Order(0),
|
||||
expectedResult: OrderDefault,
|
||||
expectedFound: true,
|
||||
},
|
||||
{
|
||||
name: "invalid order returns default",
|
||||
element: Order(999),
|
||||
expectedResult: OrderDefault,
|
||||
expectedFound: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, found := Sanitize(tt.element, allValues)
|
||||
if result != tt.expectedResult {
|
||||
t.Errorf("Expected result %v, got %v", tt.expectedResult, result)
|
||||
}
|
||||
if found != tt.expectedFound {
|
||||
t.Errorf("Expected found %v, got %v", tt.expectedFound, found)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToInterfaceSlice(t *testing.T) {
|
||||
// Test string slice
|
||||
stringSlice := []string{"a", "b", "c"}
|
||||
result := toInterfaceSlice(stringSlice)
|
||||
if len(result) != len(stringSlice) {
|
||||
t.Errorf("Expected length %d, got %d", len(stringSlice), len(result))
|
||||
}
|
||||
|
||||
for i, v := range result {
|
||||
if v != stringSlice[i] {
|
||||
t.Errorf("Expected element %d to be %v, got %v", i, stringSlice[i], v)
|
||||
}
|
||||
}
|
||||
|
||||
// Test int slice
|
||||
intSlice := []int{1, 2, 3}
|
||||
result = toInterfaceSlice(intSlice)
|
||||
|
||||
if len(result) != len(intSlice) {
|
||||
t.Errorf("Expected length %d, got %d", len(intSlice), len(result))
|
||||
}
|
||||
|
||||
for i, v := range result {
|
||||
if v != intSlice[i] {
|
||||
t.Errorf("Expected element %d to be %v, got %v", i, intSlice[i], v)
|
||||
}
|
||||
}
|
||||
|
||||
// Test empty slice
|
||||
emptySlice := []string{}
|
||||
result = toInterfaceSlice(emptySlice)
|
||||
|
||||
if len(result) != 0 {
|
||||
t.Errorf("Expected empty slice, got length %d", len(result))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortEnum(t *testing.T) {
|
||||
// Test string sorting
|
||||
stringSlice := []string{"zebra", "apple", "banana"}
|
||||
sorted := sortEnum(stringSlice)
|
||||
expected := []string{"apple", "banana", "zebra"}
|
||||
|
||||
if len(sorted) != len(expected) {
|
||||
t.Errorf("Expected length %d, got %d", len(expected), len(sorted))
|
||||
}
|
||||
|
||||
for i, v := range sorted {
|
||||
if v != expected[i] {
|
||||
t.Errorf("Expected element %d to be %q, got %q", i, expected[i], v)
|
||||
}
|
||||
}
|
||||
|
||||
// Test int sorting
|
||||
intSlice := []int{3, 1, 4, 1, 5}
|
||||
sortedInt := sortEnum(intSlice)
|
||||
expectedInt := []int{1, 1, 3, 4, 5}
|
||||
|
||||
if len(sortedInt) != len(expectedInt) {
|
||||
t.Errorf("Expected length %d, got %d", len(expectedInt), len(sortedInt))
|
||||
}
|
||||
|
||||
for i, v := range sortedInt {
|
||||
if v != expectedInt[i] {
|
||||
t.Errorf("Expected element %d to be %d, got %d", i, expectedInt[i], v)
|
||||
}
|
||||
}
|
||||
|
||||
// Test empty slice
|
||||
emptySlice := []string{}
|
||||
sortedEmpty := sortEnum(emptySlice)
|
||||
|
||||
if len(sortedEmpty) != 0 {
|
||||
t.Errorf("Expected empty slice, got length %d", len(sortedEmpty))
|
||||
}
|
||||
|
||||
// Test single element
|
||||
singleSlice := []string{"single"}
|
||||
sortedSingle := sortEnum(singleSlice)
|
||||
|
||||
if len(sortedSingle) != 1 || sortedSingle[0] != "single" {
|
||||
t.Errorf("Expected single element slice with 'single', got %v", sortedSingle)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests.
|
||||
func BenchmarkSanitizeString(b *testing.B) {
|
||||
allValues := func() ([]string, string) {
|
||||
return []string{"apple", "banana", "cherry"}, "apple"
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Sanitize("banana", allValues)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSanitizeInt(b *testing.B) {
|
||||
allValues := func() ([]int, int) {
|
||||
return []int{1, 3, 5, 7, 9}, 1
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Sanitize(5, allValues)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToInterfaceSlice(b *testing.B) {
|
||||
stringSlice := []string{"a", "b", "c", "d", "e"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
toInterfaceSlice(stringSlice)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSortEnum(b *testing.B) {
|
||||
stringSlice := []string{"zebra", "apple", "banana", "cherry", "date"}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sortEnum(stringSlice)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package enum
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContentEncodingTypeConstants(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
encoding ContentEncodingType
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "UTF8 encoding",
|
||||
encoding: ContentEncodingTypeUTF8,
|
||||
expected: "utf8",
|
||||
},
|
||||
{
|
||||
name: "Base64 encoding",
|
||||
encoding: ContentEncodingTypeBase64,
|
||||
expected: "base64",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if string(tt.encoding) != tt.expected {
|
||||
t.Errorf("Expected %s to be %q, got %q", tt.name, tt.expected, string(tt.encoding))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentEncodingTypeString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
encoding ContentEncodingType
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "UTF8 string representation",
|
||||
encoding: ContentEncodingTypeUTF8,
|
||||
expected: "utf8",
|
||||
},
|
||||
{
|
||||
name: "Base64 string representation",
|
||||
encoding: ContentEncodingTypeBase64,
|
||||
expected: "base64",
|
||||
},
|
||||
{
|
||||
name: "Custom encoding",
|
||||
encoding: ContentEncodingType("custom"),
|
||||
expected: "custom",
|
||||
},
|
||||
{
|
||||
name: "Empty encoding",
|
||||
encoding: ContentEncodingType(""),
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := string(tt.encoding)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected string representation to be %q, got %q", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentEncodingTypeEnum(t *testing.T) {
|
||||
// Test that Enum() returns the expected values
|
||||
encoding := ContentEncodingTypeUTF8
|
||||
enumValues := encoding.Enum()
|
||||
|
||||
// Should return 2 values
|
||||
if len(enumValues) != 2 {
|
||||
t.Errorf("Expected Enum() to return 2 values, got %d", len(enumValues))
|
||||
}
|
||||
|
||||
// Check that the values are correct
|
||||
expectedValues := []interface{}{
|
||||
ContentEncodingTypeBase64, // sorted order: base64 comes before utf8
|
||||
ContentEncodingTypeUTF8,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(enumValues, expectedValues) {
|
||||
t.Errorf("Expected Enum() to return %v, got %v", expectedValues, enumValues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentEncodingTypeEnumSorted(t *testing.T) {
|
||||
// Test that the enum values are sorted
|
||||
encoding := ContentEncodingTypeUTF8
|
||||
enumValues := encoding.Enum()
|
||||
|
||||
// Convert back to ContentEncodingType for comparison
|
||||
var encodingTypes []ContentEncodingType
|
||||
for _, v := range enumValues {
|
||||
if enc, ok := v.(ContentEncodingType); ok {
|
||||
encodingTypes = append(encodingTypes, enc)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that they are in sorted order
|
||||
if len(encodingTypes) >= 2 {
|
||||
if encodingTypes[0] > encodingTypes[1] {
|
||||
t.Errorf("Expected enum values to be sorted, but %q > %q", encodingTypes[0], encodingTypes[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentEncodingTypeComparison(t *testing.T) {
|
||||
// Test string comparison
|
||||
if ContentEncodingTypeUTF8 == ContentEncodingTypeBase64 {
|
||||
t.Error("Expected UTF8 and Base64 encodings to be different")
|
||||
}
|
||||
|
||||
if ContentEncodingTypeUTF8 < ContentEncodingTypeBase64 {
|
||||
t.Error("Expected UTF8 to be greater than Base64 in string comparison")
|
||||
}
|
||||
|
||||
// Test equality
|
||||
utf8Copy := ContentEncodingType("utf8")
|
||||
if ContentEncodingTypeUTF8 != utf8Copy {
|
||||
t.Error("Expected identical encoding types to be equal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentEncodingTypeZeroValue(t *testing.T) {
|
||||
var encoding ContentEncodingType
|
||||
if encoding != "" {
|
||||
t.Errorf("Expected zero value of ContentEncodingType to be empty string, got %q", encoding)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentEncodingTypeConversion(t *testing.T) {
|
||||
// Test conversion from string
|
||||
str := "utf8"
|
||||
encoding := ContentEncodingType(str)
|
||||
if encoding != ContentEncodingTypeUTF8 {
|
||||
t.Errorf("Expected conversion from string %q to give %q, got %q", str, ContentEncodingTypeUTF8, encoding)
|
||||
}
|
||||
|
||||
// Test conversion to string
|
||||
result := string(ContentEncodingTypeBase64)
|
||||
if result != "base64" {
|
||||
t.Errorf("Expected conversion to string to give %q, got %q", "base64", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentEncodingTypeValidation(t *testing.T) {
|
||||
// Test validation against known values
|
||||
validEncodings := []ContentEncodingType{
|
||||
ContentEncodingTypeUTF8,
|
||||
ContentEncodingTypeBase64,
|
||||
}
|
||||
|
||||
for _, encoding := range validEncodings {
|
||||
t.Run(string(encoding), func(t *testing.T) {
|
||||
// Check that the encoding is in the enum
|
||||
enumValues := encoding.Enum()
|
||||
found := false
|
||||
for _, v := range enumValues {
|
||||
if v == encoding {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Expected %q to be found in enum values", encoding)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentEncodingTypeInvalidValues(t *testing.T) {
|
||||
// Test with invalid/unknown encoding types
|
||||
invalidEncodings := []ContentEncodingType{
|
||||
ContentEncodingType("invalid"),
|
||||
ContentEncodingType("unknown"),
|
||||
ContentEncodingType("UTF8"), // case sensitive
|
||||
ContentEncodingType("BASE64"), // case sensitive
|
||||
ContentEncodingType("utf-8"), // different format
|
||||
ContentEncodingType("base-64"), // different format
|
||||
}
|
||||
|
||||
for _, encoding := range invalidEncodings {
|
||||
t.Run(string(encoding), func(t *testing.T) {
|
||||
// These should not be in the enum
|
||||
enumValues := encoding.Enum()
|
||||
found := false
|
||||
for _, v := range enumValues {
|
||||
if v == encoding {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
t.Errorf("Expected %q to NOT be found in enum values", encoding)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests.
|
||||
func BenchmarkContentEncodingTypeString(b *testing.B) {
|
||||
encoding := ContentEncodingTypeUTF8
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = string(encoding)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkContentEncodingTypeEnum(b *testing.B) {
|
||||
encoding := ContentEncodingTypeUTF8
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = encoding.Enum()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkContentEncodingTypeComparison(b *testing.B) {
|
||||
encoding1 := ContentEncodingTypeUTF8
|
||||
encoding2 := ContentEncodingTypeBase64
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = encoding1 == encoding2
|
||||
}
|
||||
}
|
||||
|
|
@ -26,19 +26,159 @@ func TestParseOrder(t *testing.T) {
|
|||
{"ASC", OrderAsc},
|
||||
{"ascending", OrderAsc},
|
||||
{"Ascending", OrderAsc},
|
||||
{"ASCENDING", OrderAsc},
|
||||
{"desc", OrderDesc},
|
||||
{"Desc", OrderDesc},
|
||||
{"DESC", OrderDesc},
|
||||
{"descending", OrderDesc},
|
||||
{"Descending", OrderDesc},
|
||||
{"DESCENDING", OrderDesc},
|
||||
{"", OrderDefault},
|
||||
{"invalid", OrderDefault},
|
||||
{"random", OrderDefault},
|
||||
{"123", OrderDefault},
|
||||
{"asc ", OrderDefault}, // trailing space
|
||||
{" asc", OrderDefault}, // leading space
|
||||
{"asc\n", OrderDefault}, // newline
|
||||
{"asc\t", OrderDefault}, // tab
|
||||
{"ascend", OrderDefault}, // partial match
|
||||
{"descend", OrderDefault}, // partial match
|
||||
{"ascc", OrderDefault}, // typo
|
||||
{"descc", OrderDefault}, // typo
|
||||
{"null", OrderDefault},
|
||||
{"undefined", OrderDefault},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got, want := ParseOrder(test.text), test.want
|
||||
if got != want {
|
||||
t.Errorf("Want order %q parsed as %q, got %q", test.text, want, got)
|
||||
}
|
||||
t.Run(test.text, func(t *testing.T) {
|
||||
got, want := ParseOrder(test.text), test.want
|
||||
if got != want {
|
||||
t.Errorf("Want order %q parsed as %q, got %q", test.text, want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderString(t *testing.T) {
|
||||
tests := []struct {
|
||||
order Order
|
||||
want string
|
||||
}{
|
||||
{OrderDefault, "desc"}, // OrderDefault returns desc
|
||||
{OrderAsc, "asc"},
|
||||
{OrderDesc, "desc"},
|
||||
{Order(999), "undefined"}, // invalid order value
|
||||
{Order(-1), "undefined"}, // negative order value
|
||||
{Order(100), "undefined"}, // large order value
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.want, func(t *testing.T) {
|
||||
got := test.order.String()
|
||||
if got != test.want {
|
||||
t.Errorf("Want order %v as string %q, got %q", test.order, test.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderConstants(t *testing.T) {
|
||||
// Test that the constants have expected values
|
||||
if OrderDefault != 0 {
|
||||
t.Errorf("Expected OrderDefault to be 0, got %d", OrderDefault)
|
||||
}
|
||||
if OrderAsc != 1 {
|
||||
t.Errorf("Expected OrderAsc to be 1, got %d", OrderAsc)
|
||||
}
|
||||
if OrderDesc != 2 {
|
||||
t.Errorf("Expected OrderDesc to be 2, got %d", OrderDesc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderStringRoundTrip(t *testing.T) {
|
||||
// Test that parsing the string representation gives back the original order
|
||||
orders := []Order{OrderDefault, OrderAsc, OrderDesc}
|
||||
|
||||
for _, order := range orders {
|
||||
t.Run(order.String(), func(t *testing.T) {
|
||||
str := order.String()
|
||||
parsed := ParseOrder(str)
|
||||
|
||||
// Note: OrderDefault.String() returns "desc", so parsing it gives OrderDesc
|
||||
// This is expected behavior based on the implementation
|
||||
if order == OrderDefault {
|
||||
if parsed != OrderDesc {
|
||||
t.Errorf("Expected parsing OrderDefault string to give OrderDesc, got %v", parsed)
|
||||
}
|
||||
} else {
|
||||
if parsed != order {
|
||||
t.Errorf("Expected parsing %v string to give %v, got %v", order, order, parsed)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderComparison(t *testing.T) {
|
||||
// Test that orders can be compared
|
||||
if OrderDefault >= OrderAsc {
|
||||
t.Error("Expected OrderDefault < OrderAsc")
|
||||
}
|
||||
if OrderAsc >= OrderDesc {
|
||||
t.Error("Expected OrderAsc < OrderDesc")
|
||||
}
|
||||
if OrderDefault >= OrderDesc {
|
||||
t.Error("Expected OrderDefault < OrderDesc")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderType(t *testing.T) {
|
||||
// Test that Order is the correct type
|
||||
var o Order
|
||||
if o != OrderDefault {
|
||||
t.Errorf("Expected zero value of Order to be OrderDefault, got %v", o)
|
||||
}
|
||||
|
||||
// Test type conversion
|
||||
o = Order(1)
|
||||
if o != OrderAsc {
|
||||
t.Errorf("Expected Order(1) to be OrderAsc, got %v", o)
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests.
|
||||
func BenchmarkParseOrder(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseOrder("asc")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseOrderDesc(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseOrder("desc")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseOrderInvalid(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseOrder("invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOrderString(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = OrderAsc.String()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOrderStringDesc(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = OrderDesc.String()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOrderStringDefault(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = OrderDefault.String()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,274 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMultiReadCloser_Read(t *testing.T) {
|
||||
content := "test content"
|
||||
reader := strings.NewReader(content)
|
||||
closeCalled := false
|
||||
|
||||
mrc := &MultiReadCloser{
|
||||
Reader: reader,
|
||||
}
|
||||
|
||||
// Test reading
|
||||
buf := make([]byte, len(content))
|
||||
n, err := mrc.Read(buf)
|
||||
if err != nil {
|
||||
t.Fatalf("Read() returned error: %v", err)
|
||||
}
|
||||
|
||||
if n != len(content) {
|
||||
t.Errorf("Read() returned %d bytes, expected %d", n, len(content))
|
||||
}
|
||||
|
||||
if string(buf) != content {
|
||||
t.Errorf("Read() returned %q, expected %q", string(buf), content)
|
||||
}
|
||||
|
||||
// Verify close wasn't called yet
|
||||
if closeCalled {
|
||||
t.Errorf("CloseFunc was called before Close()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiReadCloser_Close(t *testing.T) {
|
||||
reader := strings.NewReader("test")
|
||||
closeCalled := false
|
||||
|
||||
mrc := &MultiReadCloser{
|
||||
Reader: reader,
|
||||
CloseFunc: func() error {
|
||||
closeCalled = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Test closing
|
||||
err := mrc.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Close() returned error: %v", err)
|
||||
}
|
||||
|
||||
if !closeCalled {
|
||||
t.Errorf("CloseFunc was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiReadCloser_CloseError(t *testing.T) {
|
||||
reader := strings.NewReader("test")
|
||||
expectedErr := errors.New("close error")
|
||||
|
||||
mrc := &MultiReadCloser{
|
||||
Reader: reader,
|
||||
CloseFunc: func() error {
|
||||
return expectedErr
|
||||
},
|
||||
}
|
||||
|
||||
// Test closing with error
|
||||
err := mrc.Close()
|
||||
if !errors.Is(err, expectedErr) {
|
||||
t.Errorf("Close() returned error %v, expected %v", err, expectedErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiReadCloser_ReadAndClose(t *testing.T) {
|
||||
content := "hello world"
|
||||
reader := strings.NewReader(content)
|
||||
closeCalled := false
|
||||
|
||||
mrc := &MultiReadCloser{
|
||||
Reader: reader,
|
||||
CloseFunc: func() error {
|
||||
closeCalled = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Read all content
|
||||
buf := make([]byte, len(content))
|
||||
n, err := io.ReadFull(mrc, buf)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFull() returned error: %v", err)
|
||||
}
|
||||
|
||||
if n != len(content) {
|
||||
t.Errorf("ReadFull() returned %d bytes, expected %d", n, len(content))
|
||||
}
|
||||
|
||||
if string(buf) != content {
|
||||
t.Errorf("ReadFull() returned %q, expected %q", string(buf), content)
|
||||
}
|
||||
|
||||
// Close
|
||||
err = mrc.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Close() returned error: %v", err)
|
||||
}
|
||||
|
||||
if !closeCalled {
|
||||
t.Errorf("CloseFunc was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiReadCloser_MultipleReads(t *testing.T) {
|
||||
content := "test content for multiple reads"
|
||||
reader := strings.NewReader(content)
|
||||
|
||||
mrc := &MultiReadCloser{
|
||||
Reader: reader,
|
||||
}
|
||||
|
||||
// Read in chunks
|
||||
buf1 := make([]byte, 4)
|
||||
n1, err := mrc.Read(buf1)
|
||||
if err != nil {
|
||||
t.Fatalf("First Read() returned error: %v", err)
|
||||
}
|
||||
if n1 != 4 {
|
||||
t.Errorf("First Read() returned %d bytes, expected 4", n1)
|
||||
}
|
||||
|
||||
buf2 := make([]byte, 8)
|
||||
n2, err := mrc.Read(buf2)
|
||||
if err != nil {
|
||||
t.Fatalf("Second Read() returned error: %v", err)
|
||||
}
|
||||
if n2 != 8 {
|
||||
t.Errorf("Second Read() returned %d bytes, expected 8", n2)
|
||||
}
|
||||
|
||||
// Verify content
|
||||
combined := string(buf1) + string(buf2)
|
||||
if combined != content[:12] {
|
||||
t.Errorf("Combined reads returned %q, expected %q", combined, content[:12])
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiReadCloser_EOF(t *testing.T) {
|
||||
content := "short"
|
||||
reader := strings.NewReader(content)
|
||||
|
||||
mrc := &MultiReadCloser{
|
||||
Reader: reader,
|
||||
CloseFunc: func() error {
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Read all content
|
||||
buf := make([]byte, len(content))
|
||||
_, err := io.ReadFull(mrc, buf)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFull() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Try to read more - should get EOF
|
||||
buf2 := make([]byte, 10)
|
||||
n, err := mrc.Read(buf2)
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Errorf("Read() after EOF returned error %v, expected io.EOF", err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Errorf("Read() after EOF returned %d bytes, expected 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiReadCloser_NilCloseFunc(t *testing.T) {
|
||||
reader := strings.NewReader("test")
|
||||
|
||||
mrc := &MultiReadCloser{
|
||||
Reader: reader,
|
||||
}
|
||||
|
||||
// This should panic when Close() is called
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("Close() with nil CloseFunc should panic")
|
||||
}
|
||||
}()
|
||||
|
||||
mrc.Close()
|
||||
}
|
||||
|
||||
func TestMultiReadCloser_EmptyReader(t *testing.T) {
|
||||
reader := strings.NewReader("")
|
||||
closeCalled := false
|
||||
|
||||
mrc := &MultiReadCloser{
|
||||
Reader: reader,
|
||||
CloseFunc: func() error {
|
||||
closeCalled = true
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Try to read from empty reader
|
||||
buf := make([]byte, 10)
|
||||
n, err := mrc.Read(buf)
|
||||
if !errors.Is(err, io.EOF) {
|
||||
t.Errorf("Read() from empty reader returned error %v, expected io.EOF", err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Errorf("Read() from empty reader returned %d bytes, expected 0", n)
|
||||
}
|
||||
|
||||
// Close should still work
|
||||
err = mrc.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Close() returned error: %v", err)
|
||||
}
|
||||
|
||||
if !closeCalled {
|
||||
t.Errorf("CloseFunc was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiReadCloser_MultipleCloses(t *testing.T) {
|
||||
reader := strings.NewReader("test")
|
||||
closeCount := 0
|
||||
|
||||
mrc := &MultiReadCloser{
|
||||
Reader: reader,
|
||||
CloseFunc: func() error {
|
||||
closeCount++
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// Close multiple times
|
||||
err := mrc.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("First Close() returned error: %v", err)
|
||||
}
|
||||
|
||||
err = mrc.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Second Close() returned error: %v", err)
|
||||
}
|
||||
|
||||
// Verify CloseFunc was called twice
|
||||
if closeCount != 2 {
|
||||
t.Errorf("CloseFunc was called %d times, expected 2", closeCount)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSpacePathSegment_MarshalJSON(t *testing.T) {
|
||||
t.Run("marshal with all fields", func(t *testing.T) {
|
||||
segment := SpacePathSegment{
|
||||
ID: 123,
|
||||
Identifier: "test-identifier",
|
||||
IsPrimary: true,
|
||||
SpaceID: 456,
|
||||
ParentID: 789,
|
||||
CreatedBy: 111,
|
||||
Created: 1234567890,
|
||||
Updated: 1234567900,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(segment)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
t.Fatalf("failed to unmarshal result: %v", err)
|
||||
}
|
||||
|
||||
// Check that uid field is present and matches identifier
|
||||
uid, ok := result["uid"].(string)
|
||||
if !ok {
|
||||
t.Error("uid field not found or not a string")
|
||||
}
|
||||
if uid != "test-identifier" {
|
||||
t.Errorf("expected uid to be 'test-identifier', got %s", uid)
|
||||
}
|
||||
|
||||
// Check that identifier field is also present
|
||||
identifier, ok := result["identifier"].(string)
|
||||
if !ok {
|
||||
t.Error("identifier field not found or not a string")
|
||||
}
|
||||
if identifier != "test-identifier" {
|
||||
t.Errorf("expected identifier to be 'test-identifier', got %s", identifier)
|
||||
}
|
||||
|
||||
// Check other fields
|
||||
if isPrimary, ok := result["is_primary"].(bool); !ok || !isPrimary {
|
||||
t.Errorf("expected is_primary to be true, got %v", result["is_primary"])
|
||||
}
|
||||
|
||||
if spaceID, ok := result["space_id"].(float64); !ok || int64(spaceID) != 456 {
|
||||
t.Errorf("expected space_id to be 456, got %v", result["space_id"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("marshal with empty identifier", func(t *testing.T) {
|
||||
segment := SpacePathSegment{
|
||||
ID: 1,
|
||||
Identifier: "",
|
||||
IsPrimary: false,
|
||||
SpaceID: 2,
|
||||
}
|
||||
|
||||
data, err := json.Marshal(segment)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
t.Fatalf("failed to unmarshal result: %v", err)
|
||||
}
|
||||
|
||||
// Check that uid field is present and empty
|
||||
uid, ok := result["uid"].(string)
|
||||
if !ok {
|
||||
t.Error("uid field not found or not a string")
|
||||
}
|
||||
if uid != "" {
|
||||
t.Errorf("expected uid to be empty, got %s", uid)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("marshal zero values", func(t *testing.T) {
|
||||
segment := SpacePathSegment{}
|
||||
|
||||
data, err := json.Marshal(segment)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
t.Fatalf("failed to unmarshal result: %v", err)
|
||||
}
|
||||
|
||||
// Check that uid field is present and empty
|
||||
uid, ok := result["uid"].(string)
|
||||
if !ok {
|
||||
t.Error("uid field not found or not a string")
|
||||
}
|
||||
if uid != "" {
|
||||
t.Errorf("expected uid to be empty, got %s", uid)
|
||||
}
|
||||
|
||||
// Check that identifier field is also present and empty
|
||||
identifier, ok := result["identifier"].(string)
|
||||
if !ok {
|
||||
t.Error("identifier field not found or not a string")
|
||||
}
|
||||
if identifier != "" {
|
||||
t.Errorf("expected identifier to be empty, got %s", identifier)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
// Copyright 2023 Harness, Inc.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package version
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
)
|
||||
|
||||
func TestParseVersionNumber(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected int64
|
||||
}{
|
||||
{
|
||||
name: "empty string returns zero",
|
||||
input: "",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "zero string returns zero",
|
||||
input: "0",
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "positive number",
|
||||
input: "123",
|
||||
expected: 123,
|
||||
},
|
||||
{
|
||||
name: "single digit",
|
||||
input: "5",
|
||||
expected: 5,
|
||||
},
|
||||
{
|
||||
name: "large number",
|
||||
input: "999999",
|
||||
expected: 999999,
|
||||
},
|
||||
{
|
||||
name: "negative number",
|
||||
input: "-1",
|
||||
expected: -1,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := parseVersionNumber(tt.input)
|
||||
if result != tt.expected {
|
||||
t.Errorf("parseVersionNumber(%q) = %d, want %d", tt.input, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVersionNumberPanic(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
}{
|
||||
{
|
||||
name: "invalid number",
|
||||
input: "abc",
|
||||
},
|
||||
{
|
||||
name: "mixed alphanumeric",
|
||||
input: "1a2b",
|
||||
},
|
||||
{
|
||||
name: "decimal number",
|
||||
input: "1.5",
|
||||
},
|
||||
{
|
||||
name: "special characters",
|
||||
input: "!@#",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
t.Errorf("parseVersionNumber(%q) should have panicked", tt.input)
|
||||
}
|
||||
}()
|
||||
parseVersionNumber(tt.input)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionStructure(t *testing.T) {
|
||||
// Test that Version is properly initialized as a semver.Version
|
||||
if Version.Major < 0 {
|
||||
t.Error("Version.Major should not be negative")
|
||||
}
|
||||
if Version.Minor < 0 {
|
||||
t.Error("Version.Minor should not be negative")
|
||||
}
|
||||
if Version.Patch < 0 {
|
||||
t.Error("Version.Patch should not be negative")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionString(t *testing.T) {
|
||||
// Test that Version can be converted to string without error
|
||||
versionStr := Version.String()
|
||||
if versionStr == "" {
|
||||
t.Error("Version.String() should not be empty")
|
||||
}
|
||||
|
||||
// Test that the string representation is valid semver
|
||||
_, err := semver.NewVersion(versionStr)
|
||||
if err != nil {
|
||||
t.Errorf("Version.String() should produce valid semver, got: %s, error: %v", versionStr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitVariables(t *testing.T) {
|
||||
// Test that git variables are accessible (they may be empty in tests)
|
||||
// This ensures the variables are properly declared and exported
|
||||
_ = GitRepository
|
||||
_ = GitCommit
|
||||
|
||||
// Test that they are strings
|
||||
if GitRepository != "" {
|
||||
if len(GitRepository) == 0 {
|
||||
t.Error("GitRepository should be a valid string when set")
|
||||
}
|
||||
}
|
||||
|
||||
if GitCommit != "" {
|
||||
if len(GitCommit) == 0 {
|
||||
t.Error("GitCommit should be a valid string when set")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionComparison(t *testing.T) {
|
||||
// Test version comparison functionality
|
||||
v1 := semver.Version{Major: 1, Minor: 0, Patch: 0}
|
||||
v2 := semver.Version{Major: 1, Minor: 1, Patch: 0}
|
||||
|
||||
if !v1.LessThan(v2) {
|
||||
t.Error("v1.0.0 should be less than v1.1.0")
|
||||
}
|
||||
|
||||
if v2.LessThan(v1) {
|
||||
t.Error("v1.1.0 should not be less than v1.0.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionWithPrerelease(t *testing.T) {
|
||||
// Test version with prerelease
|
||||
v := semver.Version{
|
||||
Major: 1,
|
||||
Minor: 0,
|
||||
Patch: 0,
|
||||
PreRelease: semver.PreRelease("alpha"),
|
||||
}
|
||||
|
||||
expected := "1.0.0-alpha"
|
||||
if v.String() != expected {
|
||||
t.Errorf("Version with prerelease should be %s, got %s", expected, v.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionWithMetadata(t *testing.T) {
|
||||
// Test version with metadata
|
||||
v := semver.Version{
|
||||
Major: 1,
|
||||
Minor: 0,
|
||||
Patch: 0,
|
||||
Metadata: "build.1",
|
||||
}
|
||||
|
||||
expected := "1.0.0+build.1"
|
||||
if v.String() != expected {
|
||||
t.Errorf("Version with metadata should be %s, got %s", expected, v.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionWithBothPrereleaseAndMetadata(t *testing.T) {
|
||||
// Test version with both prerelease and metadata
|
||||
v := semver.Version{
|
||||
Major: 1,
|
||||
Minor: 0,
|
||||
Patch: 0,
|
||||
PreRelease: semver.PreRelease("beta"),
|
||||
Metadata: "build.2",
|
||||
}
|
||||
|
||||
expected := "1.0.0-beta+build.2"
|
||||
if v.String() != expected {
|
||||
t.Errorf("Version with prerelease and metadata should be %s, got %s", expected, v.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests.
|
||||
func BenchmarkParseVersionNumber(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
parseVersionNumber("123")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVersionString(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = Version.String()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue