Files
deploy/commitserver/commit/hydratorhelper_test.go
zeekayandHanzo Dev 1ab6e799c1 cd: the module is github.com/hanzoai/cd, where it is actually hosted
go.mod still declared `module github.com/hanzoai/deploy` after the repo was
renamed. A Go module path is not cosmetic — it is the resolvable location. So
`go get github.com/hanzoai/cd` could not work, and anything importing this
module had to spell a name that no longer exists at that address. The build log
of v3.6.0 is where it surfaced: the compiler was still emitting
github.com/hanzoai/deploy/cmd/cd/commands.

3541 import paths across 690 .go, 19 .proto, 4 .yaml, 3 .sh, 2 .mod, Procfile
and Makefile. Plus the 62 ghcr.io/hanzoai/deploy image references, moved to
ghcr.io/hanzoai/cd to match hanzo.yml's build target — a distinct string from
the module path, replaced separately so neither could clobber the other.

go mod tidy clean, CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build ./... clean.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 21:16:24 -07:00

552 lines
19 KiB
Go

package commit
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/hanzoai/cd/commitserver/apiclient"
appsv1 "github.com/hanzoai/cd/pkg/apis/application/v1alpha1"
"github.com/hanzoai/cd/util/git"
gitmocks "github.com/hanzoai/cd/util/git/mocks"
"github.com/hanzoai/cd/util/hydrator"
"github.com/hanzoai/cd/util/settings"
)
// tempRoot creates a temporary directory and returns an os.Root object for it.
// We use this instead of t.TempDir() because OSX does weird things with temp directories, and it triggers
// the os.Root protections.
func tempRoot(t *testing.T) *os.Root {
t.Helper()
dir, err := os.MkdirTemp(".", "")
require.NoError(t, err)
t.Cleanup(func() {
err := os.RemoveAll(dir)
require.NoError(t, err)
})
root, err := os.OpenRoot(dir)
require.NoError(t, err)
t.Cleanup(func() {
err := root.Close()
require.NoError(t, err)
})
return root
}
func TestWriteForPaths(t *testing.T) {
t.Parallel()
root := tempRoot(t)
repoURL := "https://github.com/example/repo"
drySha := "abc123"
paths := []*apiclient.PathDetails{
{
Path: "path1",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"kind":"Pod","apiVersion":"v1"}`},
},
Commands: []string{"command1", "command2"},
},
{
Path: "path2",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"kind":"Service","apiVersion":"v1"}`},
},
Commands: []string{"command3"},
},
{
Path: "path3/nested",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"kind":"Deployment","apiVersion":"apps/v1"}`},
},
Commands: []string{"command4"},
},
}
now := metav1.NewTime(time.Now())
metadata := &appsv1.RevisionMetadata{
Author: "test-author",
Date: &now,
Message: `test-message
Signed-off-by: Test User <[email protected]>
Argocd-reference-commit-sha: abc123
`,
References: []appsv1.RevisionReference{
{
Commit: &appsv1.CommitMetadata{
Author: "test-code-author <[email protected]>",
Date: now.Format(time.RFC3339),
Subject: "test-code-subject",
SHA: "test-code-sha",
RepoURL: "https://example.com/test/repo.git",
},
},
},
}
mockGitClient := gitmocks.NewClient(t)
mockGitClient.EXPECT().HasFileChanged(mock.Anything, mock.Anything).Return(true, nil).Times(len(paths))
shouldCommit, err := WriteForPaths(t.Context(), root, repoURL, drySha, metadata, paths, mockGitClient, settings.DefaultManifestHydrationReadmeTemplate)
require.NoError(t, err)
require.True(t, shouldCommit)
// Check if the top-level hydrator.metadata exists and contains the repo URL and dry SHA
topMetadataPath := filepath.Join(root.Name(), "hydrator.metadata")
topMetadataBytes, err := os.ReadFile(topMetadataPath)
require.NoError(t, err)
var topMetadata hydrator.HydratorCommitMetadata
err = json.Unmarshal(topMetadataBytes, &topMetadata)
require.NoError(t, err)
assert.Equal(t, repoURL, topMetadata.RepoURL)
assert.Equal(t, drySha, topMetadata.DrySHA)
assert.Equal(t, metadata.Author, topMetadata.Author)
assert.Equal(t, "test-message", topMetadata.Subject)
// The body should exclude the Argocd- trailers.
assert.Equal(t, "Signed-off-by: Test User <[email protected]>\n", topMetadata.Body)
assert.Equal(t, metadata.Date.Format(time.RFC3339), topMetadata.Date)
assert.Equal(t, metadata.References, topMetadata.References)
for _, p := range paths {
fullHydratePath := filepath.Join(root.Name(), p.Path)
// Check if each path directory exists
assert.DirExists(t, fullHydratePath)
// Check if each path contains a hydrator.metadata file and contains the repo URL
metadataPath := path.Join(fullHydratePath, "hydrator.metadata")
metadataBytes, err := os.ReadFile(metadataPath)
require.NoError(t, err)
var readMetadata hydrator.HydratorCommitMetadata
err = json.Unmarshal(metadataBytes, &readMetadata)
require.NoError(t, err)
assert.Equal(t, repoURL, readMetadata.RepoURL)
// Check if each path contains a README.md file and contains the repo URL
readmePath := path.Join(fullHydratePath, "README.md")
readmeBytes, err := os.ReadFile(readmePath)
require.NoError(t, err)
assert.Contains(t, string(readmeBytes), repoURL)
// Check if each path contains a manifest.yaml file and contains the word kind
manifestPath := path.Join(fullHydratePath, "manifest.yaml")
manifestBytes, err := os.ReadFile(manifestPath)
require.NoError(t, err)
assert.Contains(t, string(manifestBytes), "kind")
}
}
func TestWriteForPaths_WithOneManifestMatchesExisting(t *testing.T) {
t.Parallel()
root := tempRoot(t)
repoURL := "https://github.com/example/repo"
drySha := "abc123"
paths := []*apiclient.PathDetails{
{
Path: "path1",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"kind":"Pod","apiVersion":"v1"}`},
},
Commands: []string{"command1", "command2"},
},
{
Path: "path2",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"kind":"Service","apiVersion":"v1"}`},
},
Commands: []string{"command3"},
},
{
Path: "path3/nested",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"kind":"Deployment","apiVersion":"apps/v1"}`},
},
Commands: []string{"command4"},
},
}
now := metav1.NewTime(time.Now())
metadata := &appsv1.RevisionMetadata{
Author: "test-author",
Date: &now,
Message: `test-message
Signed-off-by: Test User <[email protected]>
Argocd-reference-commit-sha: abc123
`,
References: []appsv1.RevisionReference{
{
Commit: &appsv1.CommitMetadata{
Author: "test-code-author <[email protected]>",
Date: now.Format(time.RFC3339),
Subject: "test-code-subject",
SHA: "test-code-sha",
RepoURL: "https://example.com/test/repo.git",
},
},
},
}
mockGitClient := gitmocks.NewClient(t)
mockGitClient.EXPECT().HasFileChanged(mock.Anything, "path1/manifest.yaml").Return(true, nil).Once()
mockGitClient.EXPECT().HasFileChanged(mock.Anything, "path2/manifest.yaml").Return(true, nil).Once()
mockGitClient.EXPECT().HasFileChanged(mock.Anything, "path3/nested/manifest.yaml").Return(false, nil).Once()
shouldCommit, err := WriteForPaths(t.Context(), root, repoURL, drySha, metadata, paths, mockGitClient, settings.DefaultManifestHydrationReadmeTemplate)
require.NoError(t, err)
require.True(t, shouldCommit)
// Check if the top-level hydrator.metadata exists and contains the repo URL and dry SHA
topMetadataPath := filepath.Join(root.Name(), "hydrator.metadata")
topMetadataBytes, err := os.ReadFile(topMetadataPath)
require.NoError(t, err)
var topMetadata hydrator.HydratorCommitMetadata
err = json.Unmarshal(topMetadataBytes, &topMetadata)
require.NoError(t, err)
assert.Equal(t, repoURL, topMetadata.RepoURL)
assert.Equal(t, drySha, topMetadata.DrySHA)
assert.Equal(t, metadata.Author, topMetadata.Author)
assert.Equal(t, "test-message", topMetadata.Subject)
// The body should exclude the Argocd- trailers.
assert.Equal(t, "Signed-off-by: Test User <[email protected]>\n", topMetadata.Body)
assert.Equal(t, metadata.Date.Format(time.RFC3339), topMetadata.Date)
assert.Equal(t, metadata.References, topMetadata.References)
for _, p := range paths {
fullHydratePath := filepath.Join(root.Name(), p.Path)
if p.Path == "path3/nested" {
assert.DirExists(t, fullHydratePath)
manifestPath := path.Join(fullHydratePath, "manifest.yaml")
_, err := os.ReadFile(manifestPath)
require.NoError(t, err)
continue
}
// Check if each path directory exists
assert.DirExists(t, fullHydratePath)
// Check if each path contains a hydrator.metadata file and contains the repo URL
metadataPath := path.Join(fullHydratePath, "hydrator.metadata")
metadataBytes, err := os.ReadFile(metadataPath)
require.NoError(t, err)
var readMetadata hydrator.HydratorCommitMetadata
err = json.Unmarshal(metadataBytes, &readMetadata)
require.NoError(t, err)
assert.Equal(t, repoURL, readMetadata.RepoURL)
// Check if each path contains a README.md file and contains the repo URL
readmePath := path.Join(fullHydratePath, "README.md")
readmeBytes, err := os.ReadFile(readmePath)
require.NoError(t, err)
assert.Contains(t, string(readmeBytes), repoURL)
// Check if each path contains a manifest.yaml file and contains the word kind
manifestPath := path.Join(fullHydratePath, "manifest.yaml")
manifestBytes, err := os.ReadFile(manifestPath)
require.NoError(t, err)
assert.Contains(t, string(manifestBytes), "kind")
}
}
func TestWriteMetadata(t *testing.T) {
t.Parallel()
root := tempRoot(t)
metadata := hydrator.HydratorCommitMetadata{
RepoURL: "https://github.com/example/repo",
DrySHA: "abc123",
}
err := writeMetadata(root, "", metadata)
require.NoError(t, err)
metadataPath := filepath.Join(root.Name(), "hydrator.metadata")
metadataBytes, err := os.ReadFile(metadataPath)
require.NoError(t, err)
var readMetadata hydrator.HydratorCommitMetadata
err = json.Unmarshal(metadataBytes, &readMetadata)
require.NoError(t, err)
assert.Equal(t, metadata, readMetadata)
}
func TestWriteReadme(t *testing.T) {
t.Parallel()
root := tempRoot(t)
randomData := make([]byte, 32)
_, err := rand.Read(randomData)
require.NoError(t, err)
hash := sha256.Sum256(randomData)
sha := hex.EncodeToString(hash[:])
metadata := hydrator.HydratorCommitMetadata{
RepoURL: "https://github.com/example/repo",
DrySHA: "abc123",
References: []appsv1.RevisionReference{
{
Commit: &appsv1.CommitMetadata{
Author: "test-code-author <[email protected]>",
Date: time.Now().Format(time.RFC3339),
Subject: "test-code-subject",
SHA: sha,
RepoURL: "https://example.com/test/repo.git",
},
},
},
}
err = writeReadme(root, "", metadata, settings.DefaultManifestHydrationReadmeTemplate)
require.NoError(t, err)
readmePath := filepath.Join(root.Name(), "README.md")
readmeBytes, err := os.ReadFile(readmePath)
require.NoError(t, err)
assert.Equal(t, `# Manifest Hydration
To hydrate the manifests in this repository, run the following commands:
`+"```shell"+`
git clone https://github.com/example/repo
# cd into the cloned directory
git checkout abc123
`+"```"+fmt.Sprintf(`
## References
* [%s](https://example.com/test/repo.git): test-code-subject (test-code-author <[email protected]>)
`, sha[:7]), string(readmeBytes))
}
func TestWriteManifests(t *testing.T) {
t.Parallel()
root := tempRoot(t)
manifests := []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"kind":"Pod","apiVersion":"v1"}`},
}
err := writeManifests(root, "", manifests)
require.NoError(t, err)
manifestPath := path.Join(root.Name(), "manifest.yaml")
manifestBytes, err := os.ReadFile(manifestPath)
require.NoError(t, err)
assert.Contains(t, string(manifestBytes), "kind")
}
func TestWriteGitAttributes(t *testing.T) {
t.Parallel()
root := tempRoot(t)
err := writeGitAttributes(root)
require.NoError(t, err)
gitAttributesPath := filepath.Join(root.Name(), ".gitattributes")
gitAttributesBytes, err := os.ReadFile(gitAttributesPath)
require.NoError(t, err)
assert.Contains(t, string(gitAttributesBytes), "README.md linguist-generated=true")
assert.Contains(t, string(gitAttributesBytes), "hydrator.metadata linguist-generated=true")
}
func TestWriteGitAttributes_MatchesAllDepths(t *testing.T) {
t.Parallel()
root := tempRoot(t)
err := writeGitAttributes(root)
require.NoError(t, err)
// The gitattributes pattern needs to match files at all depths:
// - hydrator.metadata (root level)
// - path1/hydrator.metadata (one level deep)
// - path1/nested/deep/hydrator.metadata (multiple levels deep)
// Same for README.md files
//
// The pattern "**/hydrator.metadata" matches at any depth including root
// The pattern "*/hydrator.metadata" only matches exactly one directory level deep
// Test actual Git behavior using git check-attr
// Initialize a git repo
ctx := t.Context()
repoPath := root.Name()
cmd := exec.CommandContext(ctx, "git", "init")
cmd.Dir = repoPath
output, err := cmd.CombinedOutput()
require.NoError(t, err, "Failed to init git repo: %s", string(output))
// Test files at different depths
testCases := []struct {
path string
shouldMatch bool
description string
}{
{"hydrator.metadata", true, "root level hydrator.metadata"},
{"README.md", true, "root level README.md"},
{"path1/hydrator.metadata", true, "one level deep hydrator.metadata"},
{"path1/README.md", true, "one level deep README.md"},
{"path1/nested/hydrator.metadata", true, "two levels deep hydrator.metadata"},
{"path1/nested/README.md", true, "two levels deep README.md"},
{"path1/nested/deep/hydrator.metadata", true, "three levels deep hydrator.metadata"},
{"path1/nested/deep/README.md", true, "three levels deep README.md"},
{"manifest.yaml", false, "manifest.yaml should not match"},
{"path1/manifest.yaml", false, "nested manifest.yaml should not match"},
}
for _, tc := range testCases {
t.Run(tc.description, func(t *testing.T) {
// Use git check-attr to verify if linguist-generated attribute is set
t.Parallel()
cmd := exec.CommandContext(ctx, "git", "check-attr", "linguist-generated", tc.path)
cmd.Dir = repoPath
output, err := cmd.CombinedOutput()
require.NoError(t, err, "Failed to run git check-attr: %s", string(output))
// Output format: <path>: <attribute>: <value>
// Example: "hydrator.metadata: linguist-generated: true"
outputStr := strings.TrimSpace(string(output))
if tc.shouldMatch {
expectedOutput := tc.path + ": linguist-generated: true"
assert.Equal(t, expectedOutput, outputStr,
"File %s should have linguist-generated=true attribute", tc.path)
} else {
// Attribute should be unspecified
expectedOutput := tc.path + ": linguist-generated: unspecified"
assert.Equal(t, expectedOutput, outputStr,
"File %s should not have linguist-generated=true attribute", tc.path)
}
})
}
}
func TestIsHydrated(t *testing.T) {
t.Parallel()
mockGitClient := gitmocks.NewClient(t)
drySha := "abc123"
commitSha := "fff456"
commitShaNoNoteFoundErr := "abc456"
commitShaErr := "abc999"
strnote := "{\"drySha\":\"abc123\"}"
mockGitClient.EXPECT().GetCommitNote(mock.Anything, commitSha, mock.Anything).Return(strnote, nil).Once()
mockGitClient.EXPECT().GetCommitNote(mock.Anything, commitShaNoNoteFoundErr, mock.Anything).Return("", fmt.Errorf("wrapped error %w", git.ErrNoNoteFound)).Once()
// an existing note
isHydrated, err := IsHydrated(t.Context(), mockGitClient, drySha, commitSha)
require.NoError(t, err)
assert.True(t, isHydrated)
// no note found treated as success.. no error returned
isHydrated, err = IsHydrated(t.Context(), mockGitClient, drySha, commitShaNoNoteFoundErr)
require.NoError(t, err)
assert.False(t, isHydrated)
// Test that non-ErrNoNoteFound errors are propagated: when GetCommitNote fails with
// an error other than "no note found", IsHydrated should return that error to the caller
err = errors.New("some other error")
mockGitClient.EXPECT().GetCommitNote(mock.Anything, commitShaErr, mock.Anything).Return("", fmt.Errorf("wrapped error %w", err)).Once()
isHydrated, err = IsHydrated(t.Context(), mockGitClient, drySha, commitShaErr)
require.Error(t, err)
assert.False(t, isHydrated)
}
func TestAddNote(t *testing.T) {
t.Parallel()
mockGitClient := gitmocks.NewClient(t)
drySha := "abc123"
commitSha := "fff456"
commitShaErr := "abc456"
err := errors.New("test error")
mockGitClient.EXPECT().AddAndPushNote(mock.Anything, commitSha, mock.Anything, mock.Anything).Return(nil).Once()
mockGitClient.EXPECT().AddAndPushNote(mock.Anything, commitShaErr, mock.Anything, mock.Anything).Return(err).Once()
// success
err = AddNote(t.Context(), mockGitClient, drySha, commitSha)
require.NoError(t, err)
// failure
err = AddNote(t.Context(), mockGitClient, drySha, commitShaErr)
require.Error(t, err)
}
// TestWriteForPaths_NoOpScenario tests that when manifests don't change between two hydrations,
// shouldCommit returns false. This reproduces the bug where a new DRY commit that doesn't affect
// manifests should not create a new hydrated commit.
func TestWriteForPaths_NoOpScenario(t *testing.T) {
t.Parallel()
root := tempRoot(t)
repoURL := "https://github.com/example/repo"
drySha1 := "abc123"
drySha2 := "def456" // Different dry SHA
paths := []*apiclient.PathDetails{
{
Path: "guestbook",
Manifests: []*apiclient.HydratedManifestDetails{
{ManifestJSON: `{"apiVersion":"v1","kind":"Service","metadata":{"name":"guestbook-ui"}}`},
{ManifestJSON: `{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"guestbook-ui"}}`},
},
Commands: []string{"kustomize build ."},
},
}
now1 := metav1.NewTime(time.Now())
metadata1 := &appsv1.RevisionMetadata{
Author: "test-author",
Date: &now1,
Message: "Initial commit",
}
// First hydration - manifests are new, so HasFileChanged should return true
mockGitClient1 := gitmocks.NewClient(t)
mockGitClient1.EXPECT().HasFileChanged(mock.Anything, "guestbook/manifest.yaml").Return(true, nil).Once()
shouldCommit1, err := WriteForPaths(t.Context(), root, repoURL, drySha1, metadata1, paths, mockGitClient1, settings.DefaultManifestHydrationReadmeTemplate)
require.NoError(t, err)
require.True(t, shouldCommit1, "First hydration should commit because manifests are new")
// Second hydration - same manifest content but different dry SHA and metadata
// Simulate adding a README.md to the dry source (which doesn't affect manifests)
now2 := metav1.NewTime(time.Now().Add(1 * time.Hour)) // Different timestamp
metadata2 := &appsv1.RevisionMetadata{
Author: "test-author",
Date: &now2,
Message: "Add README.md", // Different commit message
}
// The manifests are identical, so HasFileChanged should return false
mockGitClient2 := gitmocks.NewClient(t)
mockGitClient2.EXPECT().HasFileChanged(mock.Anything, "guestbook/manifest.yaml").Return(false, nil).Once()
shouldCommit2, err := WriteForPaths(t.Context(), root, repoURL, drySha2, metadata2, paths, mockGitClient2, settings.DefaultManifestHydrationReadmeTemplate)
require.NoError(t, err)
require.False(t, shouldCommit2, "Second hydration should NOT commit because manifests didn't change")
// Verify that the root-level metadata WAS updated (even though we're not committing)
// The files get written to the working directory, but since shouldCommit is false, they won't be committed
topMetadataPath := filepath.Join(root.Name(), "hydrator.metadata")
topMetadataBytes, err := os.ReadFile(topMetadataPath)
require.NoError(t, err)
var topMetadata hydrator.HydratorCommitMetadata
err = json.Unmarshal(topMetadataBytes, &topMetadata)
require.NoError(t, err)
// The top-level metadata should have the NEW dry SHA (files are written, just not committed)
assert.Equal(t, drySha2, topMetadata.DrySHA)
assert.Equal(t, metadata2.Date.Format(time.RFC3339), topMetadata.Date)
}