package devops import ( "context" "os" "path/filepath" "testing" "time" "github.com/host-uk/core/pkg/devops/sources" "github.com/host-uk/core/pkg/io" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestImageManager_Good_IsInstalled(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) cfg := DefaultConfig() mgr, err := NewImageManager(io.Local, cfg) require.NoError(t, err) // Not installed yet assert.False(t, mgr.IsInstalled()) // Create fake image imagePath := filepath.Join(tmpDir, ImageName()) err = os.WriteFile(imagePath, []byte("fake"), 0644) require.NoError(t, err) // Now installed assert.True(t, mgr.IsInstalled()) } func TestNewImageManager_Good(t *testing.T) { t.Run("creates manager with cdn source", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) cfg := DefaultConfig() cfg.Images.Source = "cdn" mgr, err := NewImageManager(io.Local, cfg) assert.NoError(t, err) assert.NotNil(t, mgr) assert.Len(t, mgr.sources, 1) assert.Equal(t, "cdn", mgr.sources[0].Name()) }) t.Run("creates manager with github source", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) cfg := DefaultConfig() cfg.Images.Source = "github" mgr, err := NewImageManager(io.Local, cfg) assert.NoError(t, err) assert.NotNil(t, mgr) assert.Len(t, mgr.sources, 1) assert.Equal(t, "github", mgr.sources[0].Name()) }) } func TestManifest_Save(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "manifest.json") m := &Manifest{ medium: io.Local, Images: make(map[string]ImageInfo), path: path, } m.Images["test.img"] = ImageInfo{ Version: "1.0.0", Source: "test", } err := m.Save() assert.NoError(t, err) // Verify file exists and has content _, err = os.Stat(path) assert.NoError(t, err) // Reload m2, err := loadManifest(io.Local, path) assert.NoError(t, err) assert.Equal(t, "1.0.0", m2.Images["test.img"].Version) } func TestLoadManifest_Bad(t *testing.T) { t.Run("invalid json", func(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "manifest.json") err := os.WriteFile(path, []byte("invalid json"), 0644) require.NoError(t, err) _, err = loadManifest(io.Local, path) assert.Error(t, err) }) } func TestCheckUpdate_Bad(t *testing.T) { t.Run("image not installed", func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) cfg := DefaultConfig() mgr, err := NewImageManager(io.Local, cfg) require.NoError(t, err) _, _, _, err = mgr.CheckUpdate(context.Background()) assert.Error(t, err) assert.Contains(t, err.Error(), "image not installed") }) } func TestNewImageManager_Good_AutoSource(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) cfg := DefaultConfig() cfg.Images.Source = "auto" mgr, err := NewImageManager(io.Local, cfg) assert.NoError(t, err) assert.NotNil(t, mgr) assert.Len(t, mgr.sources, 2) // github and cdn } func TestNewImageManager_Good_UnknownSourceFallsToAuto(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) cfg := DefaultConfig() cfg.Images.Source = "unknown" mgr, err := NewImageManager(io.Local, cfg) assert.NoError(t, err) assert.NotNil(t, mgr) assert.Len(t, mgr.sources, 2) // falls to default (auto) which is github + cdn } func TestLoadManifest_Good_Empty(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "nonexistent.json") m, err := loadManifest(io.Local, path) assert.NoError(t, err) assert.NotNil(t, m) assert.NotNil(t, m.Images) assert.Empty(t, m.Images) assert.Equal(t, path, m.path) } func TestLoadManifest_Good_ExistingData(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "manifest.json") data := `{"images":{"test.img":{"version":"2.0.0","source":"cdn"}}}` err := os.WriteFile(path, []byte(data), 0644) require.NoError(t, err) m, err := loadManifest(io.Local, path) assert.NoError(t, err) assert.NotNil(t, m) assert.Equal(t, "2.0.0", m.Images["test.img"].Version) assert.Equal(t, "cdn", m.Images["test.img"].Source) } func TestImageInfo_Struct(t *testing.T) { info := ImageInfo{ Version: "1.0.0", SHA256: "abc123", Downloaded: time.Now(), Source: "github", } assert.Equal(t, "1.0.0", info.Version) assert.Equal(t, "abc123", info.SHA256) assert.False(t, info.Downloaded.IsZero()) assert.Equal(t, "github", info.Source) } func TestManifest_Save_Good_CreatesDirs(t *testing.T) { tmpDir := t.TempDir() nestedPath := filepath.Join(tmpDir, "nested", "dir", "manifest.json") m := &Manifest{ medium: io.Local, Images: make(map[string]ImageInfo), path: nestedPath, } m.Images["test.img"] = ImageInfo{Version: "1.0.0"} // Save creates parent directories automatically via io.Local.Write err := m.Save() assert.NoError(t, err) // Verify file was created _, err = os.Stat(nestedPath) assert.NoError(t, err) } func TestManifest_Save_Good_Overwrite(t *testing.T) { tmpDir := t.TempDir() path := filepath.Join(tmpDir, "manifest.json") // First save m1 := &Manifest{ medium: io.Local, Images: make(map[string]ImageInfo), path: path, } m1.Images["test.img"] = ImageInfo{Version: "1.0.0"} err := m1.Save() require.NoError(t, err) // Second save with different data m2 := &Manifest{ medium: io.Local, Images: make(map[string]ImageInfo), path: path, } m2.Images["other.img"] = ImageInfo{Version: "2.0.0"} err = m2.Save() require.NoError(t, err) // Verify second data loaded, err := loadManifest(io.Local, path) assert.NoError(t, err) assert.Equal(t, "2.0.0", loaded.Images["other.img"].Version) _, exists := loaded.Images["test.img"] assert.False(t, exists) } func TestImageManager_Install_Bad_NoSourceAvailable(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) // Create manager with empty sources mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")}, sources: nil, // no sources } err := mgr.Install(context.Background(), nil) assert.Error(t, err) assert.Contains(t, err.Error(), "no image source available") } func TestNewImageManager_Good_CreatesDir(t *testing.T) { tmpDir := t.TempDir() imagesDir := filepath.Join(tmpDir, "images") t.Setenv("CORE_IMAGES_DIR", imagesDir) cfg := DefaultConfig() mgr, err := NewImageManager(io.Local, cfg) assert.NoError(t, err) assert.NotNil(t, mgr) // Verify directory was created info, err := os.Stat(imagesDir) assert.NoError(t, err) assert.True(t, info.IsDir()) } // mockImageSource is a test helper for simulating image sources type mockImageSource struct { name string available bool latestVersion string latestErr error downloadErr error } func (m *mockImageSource) Name() string { return m.name } func (m *mockImageSource) Available() bool { return m.available } func (m *mockImageSource) LatestVersion(ctx context.Context) (string, error) { return m.latestVersion, m.latestErr } func (m *mockImageSource) Download(ctx context.Context, medium io.Medium, dest string, progress func(downloaded, total int64)) error { if m.downloadErr != nil { return m.downloadErr } // Create a fake image file imagePath := filepath.Join(dest, ImageName()) return os.WriteFile(imagePath, []byte("mock image content"), 0644) } func TestImageManager_Install_Good_WithMockSource(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) mock := &mockImageSource{ name: "mock", available: true, latestVersion: "v1.0.0", } mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")}, sources: []sources.ImageSource{mock}, } err := mgr.Install(context.Background(), nil) assert.NoError(t, err) assert.True(t, mgr.IsInstalled()) // Verify manifest was updated info, ok := mgr.manifest.Images[ImageName()] assert.True(t, ok) assert.Equal(t, "v1.0.0", info.Version) assert.Equal(t, "mock", info.Source) } func TestImageManager_Install_Bad_DownloadError(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) mock := &mockImageSource{ name: "mock", available: true, latestVersion: "v1.0.0", downloadErr: assert.AnError, } mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")}, sources: []sources.ImageSource{mock}, } err := mgr.Install(context.Background(), nil) assert.Error(t, err) } func TestImageManager_Install_Bad_VersionError(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) mock := &mockImageSource{ name: "mock", available: true, latestErr: assert.AnError, } mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")}, sources: []sources.ImageSource{mock}, } err := mgr.Install(context.Background(), nil) assert.Error(t, err) assert.Contains(t, err.Error(), "failed to get latest version") } func TestImageManager_Install_Good_SkipsUnavailableSource(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) unavailableMock := &mockImageSource{ name: "unavailable", available: false, } availableMock := &mockImageSource{ name: "available", available: true, latestVersion: "v2.0.0", } mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")}, sources: []sources.ImageSource{unavailableMock, availableMock}, } err := mgr.Install(context.Background(), nil) assert.NoError(t, err) // Should have used the available source info := mgr.manifest.Images[ImageName()] assert.Equal(t, "available", info.Source) } func TestImageManager_CheckUpdate_Good_WithMockSource(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) mock := &mockImageSource{ name: "mock", available: true, latestVersion: "v2.0.0", } mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{ medium: io.Local, Images: map[string]ImageInfo{ ImageName(): {Version: "v1.0.0", Source: "mock"}, }, path: filepath.Join(tmpDir, "manifest.json"), }, sources: []sources.ImageSource{mock}, } current, latest, hasUpdate, err := mgr.CheckUpdate(context.Background()) assert.NoError(t, err) assert.Equal(t, "v1.0.0", current) assert.Equal(t, "v2.0.0", latest) assert.True(t, hasUpdate) } func TestImageManager_CheckUpdate_Good_NoUpdate(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) mock := &mockImageSource{ name: "mock", available: true, latestVersion: "v1.0.0", } mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{ medium: io.Local, Images: map[string]ImageInfo{ ImageName(): {Version: "v1.0.0", Source: "mock"}, }, path: filepath.Join(tmpDir, "manifest.json"), }, sources: []sources.ImageSource{mock}, } current, latest, hasUpdate, err := mgr.CheckUpdate(context.Background()) assert.NoError(t, err) assert.Equal(t, "v1.0.0", current) assert.Equal(t, "v1.0.0", latest) assert.False(t, hasUpdate) } func TestImageManager_CheckUpdate_Bad_NoSource(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) unavailableMock := &mockImageSource{ name: "mock", available: false, } mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{ medium: io.Local, Images: map[string]ImageInfo{ ImageName(): {Version: "v1.0.0", Source: "mock"}, }, path: filepath.Join(tmpDir, "manifest.json"), }, sources: []sources.ImageSource{unavailableMock}, } _, _, _, err := mgr.CheckUpdate(context.Background()) assert.Error(t, err) assert.Contains(t, err.Error(), "no image source available") } func TestImageManager_CheckUpdate_Bad_VersionError(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) mock := &mockImageSource{ name: "mock", available: true, latestErr: assert.AnError, } mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{ medium: io.Local, Images: map[string]ImageInfo{ ImageName(): {Version: "v1.0.0", Source: "mock"}, }, path: filepath.Join(tmpDir, "manifest.json"), }, sources: []sources.ImageSource{mock}, } current, _, _, err := mgr.CheckUpdate(context.Background()) assert.Error(t, err) assert.Equal(t, "v1.0.0", current) // Current should still be returned } func TestImageManager_Install_Bad_EmptySources(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")}, sources: []sources.ImageSource{}, // Empty slice, not nil } err := mgr.Install(context.Background(), nil) assert.Error(t, err) assert.Contains(t, err.Error(), "no image source available") } func TestImageManager_Install_Bad_AllUnavailable(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) mock1 := &mockImageSource{name: "mock1", available: false} mock2 := &mockImageSource{name: "mock2", available: false} mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{medium: io.Local, Images: make(map[string]ImageInfo), path: filepath.Join(tmpDir, "manifest.json")}, sources: []sources.ImageSource{mock1, mock2}, } err := mgr.Install(context.Background(), nil) assert.Error(t, err) assert.Contains(t, err.Error(), "no image source available") } func TestImageManager_CheckUpdate_Good_FirstSourceUnavailable(t *testing.T) { tmpDir := t.TempDir() t.Setenv("CORE_IMAGES_DIR", tmpDir) unavailable := &mockImageSource{name: "unavailable", available: false} available := &mockImageSource{name: "available", available: true, latestVersion: "v2.0.0"} mgr := &ImageManager{ medium: io.Local, config: DefaultConfig(), manifest: &Manifest{ medium: io.Local, Images: map[string]ImageInfo{ ImageName(): {Version: "v1.0.0", Source: "available"}, }, path: filepath.Join(tmpDir, "manifest.json"), }, sources: []sources.ImageSource{unavailable, available}, } current, latest, hasUpdate, err := mgr.CheckUpdate(context.Background()) assert.NoError(t, err) assert.Equal(t, "v1.0.0", current) assert.Equal(t, "v2.0.0", latest) assert.True(t, hasUpdate) } func TestManifest_Struct(t *testing.T) { m := &Manifest{ Images: map[string]ImageInfo{ "test.img": {Version: "1.0.0"}, }, path: "/path/to/manifest.json", } assert.Equal(t, "/path/to/manifest.json", m.path) assert.Len(t, m.Images, 1) assert.Equal(t, "1.0.0", m.Images["test.img"].Version) }