feat: [CODE-4638]: increase code coverage (#4483) (#4485)

* 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:
Abhinav Singh 2025-10-21 18:45:38 +00:00 committed by Harness
parent d9a43614ae
commit 3e135cc128
47 changed files with 9354 additions and 20 deletions

View File

@ -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
}

View File

@ -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)
}
}

229
app/request/request_test.go Normal file
View File

@ -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"))
}

795
app/url/provider_test.go Normal file
View File

@ -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)
}
})
}
}

160
audit/context_test.go Normal file
View File

@ -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)
}
}
})
}

151
blob/config_test.go Normal file
View File

@ -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")
}
}

373
blob/filesystem_test.go Normal file
View File

@ -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
}

View File

@ -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]

153
blob/gcs_test.go Normal file
View File

@ -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.

70
blob/interface_test.go Normal file
View File

@ -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())
}
}

337
blob/options_test.go Normal file
View File

@ -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")
}
}

187
cache/no_cache_test.go vendored Normal file
View File

@ -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
}

135
cli/cli_test.go Normal file
View File

@ -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")
}

View File

@ -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")
}
})
}

362
crypto/crypto_test.go Normal file
View File

@ -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&timestamp=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)
}
}

307
encrypt/aesgcm_test.go Normal file
View File

@ -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")
}
}

155
encrypt/none_test.go Normal file
View File

@ -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)
}
})
}
}

542
errors/status_test.go Normal file
View File

@ -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)
}
}

270
errors/stderr_test.go Normal file
View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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
View File

@ -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

149
job/uid_test.go Normal file
View File

@ -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)
}
}
})
}

231
livelog/livelog_test.go Normal file
View File

@ -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)
}
}

357
livelog/memory_test.go Normal file
View File

@ -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())
}
}

318
livelog/stream_test.go Normal file
View File

@ -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)
}
}

293
livelog/sub_test.go Normal file
View File

@ -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)
}
}

160
lock/util_test.go Normal file
View File

@ -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)
}
})
}
}

390
logging/logging_test.go Normal file
View File

@ -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"))
}
}

View File

@ -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.

View File

@ -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
}
}

View File

@ -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.

View File

@ -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
}
}

View File

@ -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.

View File

@ -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
}
}

View File

@ -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.

View File

@ -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
}
}

234
resources/embed_test.go Normal file
View File

@ -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
}

View File

@ -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)
}
})
}

149
store/errors_test.go Normal file
View File

@ -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")
}

210
stream/options_test.go Normal file
View File

@ -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)
}
})
}

421
types/enum/common_test.go Normal file
View File

@ -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)
}
}

246
types/enum/encoding_test.go Normal file
View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

132
types/path_test.go Normal file
View File

@ -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)
}
})
}

224
version/version_test.go Normal file
View File

@ -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()
}
}