From c2d6aabea0aac4b8573d02ef2e27e081f1ce5e61 Mon Sep 17 00:00:00 2001 From: swanadiva Date: Tue, 23 Jun 2026 14:03:08 +0700 Subject: [PATCH] feat: add build system and integration tests - Update Makefile with test-coverage and verify targets - Add build.sh script for cross-platform builds with SHA256 checksums - Implement integration tests for full workflow (add/list/get/update/export/delete) - Add config integration test - Update project state documentation --- Makefile | 13 ++- PROJECT_STATE.md | 22 ++-- build.sh | 48 +++++++++ test/integration/integration_test.go | 153 +++++++++++++++++++++++++++ 4 files changed, 229 insertions(+), 7 deletions(-) create mode 100755 build.sh create mode 100644 test/integration/integration_test.go diff --git a/Makefile b/Makefile index 7860205..889bbaf 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build run test clean lint fmt vet install release help +.PHONY: build run test test-short test-coverage clean fmt vet lint install tidy deps verify release help # Binary name BINARY_NAME=hostkeeper @@ -37,10 +37,17 @@ test: test-short: $(GOTEST) ./... -v -short +## test-coverage: Run tests with coverage report +test-coverage: + $(GOTEST) ./... -v -coverprofile=coverage.out -covermode=atomic + $(GOCMD) tool cover -html=coverage.out -o coverage.html + @echo "Coverage report generated: coverage.html" + ## clean: Remove build artifacts clean: $(GOCLEAN) rm -rf $(BUILD_DIR) + rm -f coverage.out coverage.html ## fmt: Format all Go files fmt: @@ -66,6 +73,10 @@ tidy: deps: $(GOCMD) mod download +## verify: Verify module dependencies +verify: + $(GOCMD) mod verify + ## release: Build for multiple platforms release: clean mkdir -p $(BUILD_DIR) diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index 574db1f..91270f1 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -2,7 +2,7 @@ > **Purpose**: Enable seamless continuation of development by any agent/LLM across sessions > -> **Last Updated**: 2024-06-23 (Session 8) +> **Last Updated**: 2024-06-23 (Session 9) > **Current Status**: Implementation In Progress - Tasks 1-8 Complete > **Phase**: MVP Development (Phase 1) @@ -33,7 +33,6 @@ βœ… **Bug Fix**: Fixed deadlock in JSON storage (RLock within Lock) ### What Needs to Happen Next -πŸ”„ **Task 12**: Build and Testing (Makefile, integration tests) πŸ”„ **Task 13**: Documentation (README, usage docs) πŸ”„ **Task 14**: Final testing and release prep @@ -54,10 +53,10 @@ | **CLI Framework** | βœ… 100% | Cobra root, version, completion commands | | **CLI Commands** | 🟑 70% | add + list + connect + edit + delete + export + import done | | **TUI** | 🟑 40% | Basic TUI with host list navigation | -| **Testing** | 🟑 55% | Error + SSH + add + list + connect + edit + delete + TUI + export/import tests passing | +| **Testing** | 🟑 60% | All unit + integration tests passing | | **Documentation** | πŸ”² 0% | Usage guides and API docs | -### Overall Progress: **~75% Complete** (Tasks 1-11 done) +### Overall Progress: **~80% Complete** (Tasks 1-12 done) --- @@ -177,7 +176,16 @@ - `cmd/hostkeeper/import.go` β€” Import command with replace/merge strategies and dry-run preview - `test/storage/export_import_test.go` β€” Integration test for export/import round-trip -#### πŸ”² Task 12-14: Remaining Tasks +#### βœ… Task 12: Build and Testing +- **Status**: βœ… Completed +- **Priority**: HIGH +- **Deliverables**: Build system and integration tests +- **Files Created/Modified**: + - `Makefile` β€” Added test-coverage, verify targets; updated clean to remove coverage files + - `build.sh` β€” Cross-platform build script with SHA256 checksums + - `test/integration/integration_test.go` β€” End-to-end workflow tests (add, list, get, update, export/import, delete, config) + +#### πŸ”² Task 13-14: Remaining Tasks - **Status**: Not Started - **Details**: See `docs/plans/2024-06-22-hostkeeper-implementation.md` @@ -186,7 +194,7 @@ ## πŸ—ΊοΈ Development Roadmap ### Current Week Focus -**Target**: Complete Tasks 10+ (TUI, Export/Import) +**Target**: Complete Tasks 12+ (Build, Testing, Docs, Release) ### This Sprint - [x] Project setup and dependencies @@ -201,6 +209,8 @@ - [x] Edit host command - [x] Delete host command - [x] TUI implementation +- [x] Export/Import commands +- [x] Build system and integration tests ### Next Sprint - [ ] TUI implementation (Bubble Tea) diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..c4afbf1 --- /dev/null +++ b/build.sh @@ -0,0 +1,48 @@ +#!/bin/bash +# Build script for multiple platforms +set -e + +VERSION=${VERSION:-$(git describe --tags --always --dirty 2>/dev/null || echo "dev")} +BUILD_DIR=${BUILD_DIR:-"build"} +BINARY_NAME="hostkeeper" + +echo "Building Hostkeeper v${VERSION}" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +rm -rf ${BUILD_DIR} +mkdir -p ${BUILD_DIR} + +platforms=( + "linux/amd64" + "linux/arm64" + "linux/arm" + "darwin/amd64" + "darwin/arm64" + "windows/amd64" +) + +for platform in "${platforms[@]}"; do + IFS='/' read -r os arch <<< "$platform" + + output_name="${BINARY_NAME}-${os}-${arch}" + if [ "$os" = "windows" ]; then + output_name="${output_name}.exe" + fi + + echo " Building for ${os}/${arch}..." + + GOOS=$os GOARCH=$arch go build \ + -ldflags "-X main.version=${VERSION} -X main.buildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + -o "${BUILD_DIR}/${output_name}" \ + ./cmd/hostkeeper + + if command -v shasum &> /dev/null; then + (cd ${BUILD_DIR} && shasum -a 256 "${output_name}" > "${output_name}.sha256") + fi +done + +echo "" +echo "Build complete! Binaries in ${BUILD_DIR}/" +echo "" +echo "Available binaries:" +ls -lh ${BUILD_DIR}/ diff --git a/test/integration/integration_test.go b/test/integration/integration_test.go new file mode 100644 index 0000000..db338de --- /dev/null +++ b/test/integration/integration_test.go @@ -0,0 +1,153 @@ +package integration + +import ( + "context" + "testing" + "time" + + "git.tukangketik.id/swanadiva/hostkeeper/internal/models" + "git.tukangketik.id/swanadiva/hostkeeper/pkg/config" + "git.tukangketik.id/swanadiva/hostkeeper/pkg/storage" +) + +func TestIntegrationWorkflow(t *testing.T) { + tempDir := t.TempDir() + + store, err := storage.NewJSONStorage(tempDir) + if err != nil { + t.Fatalf("Failed to create storage: %v", err) + } + + ctx := context.Background() + + // Test 1: Add hosts + t.Run("AddHosts", func(t *testing.T) { + testHost := &models.Host{ + ID: "integration-test-1", + Name: "Integration Test Server", + Hostname: "test.example.com", + Port: 22, + Username: "testuser", + Auth: models.AuthConfig{Type: "password", Password: "testpass"}, + Tags: []string{"test", "integration"}, + CreatedAt: time.Now(), + } + + if err := store.SaveHost(ctx, testHost); err != nil { + t.Errorf("Failed to save host: %v", err) + } + }) + + // Test 2: List hosts + t.Run("ListHosts", func(t *testing.T) { + hosts, err := store.ListHosts(ctx) + if err != nil { + t.Errorf("Failed to list hosts: %v", err) + } + + if len(hosts) != 1 { + t.Errorf("Expected 1 host, got %d", len(hosts)) + } + }) + + // Test 3: Get host + t.Run("GetHost", func(t *testing.T) { + host, err := store.GetHost(ctx, "integration-test-1") + if err != nil { + t.Errorf("Failed to get host: %v", err) + } + + if host.Name != "Integration Test Server" { + t.Errorf("Expected name 'Integration Test Server', got '%s'", host.Name) + } + }) + + // Test 4: Update host + t.Run("UpdateHost", func(t *testing.T) { + host, err := store.GetHost(ctx, "integration-test-1") + if err != nil { + t.Errorf("Failed to get host: %v", err) + } + + host.Name = "Updated Test Server" + if err := store.SaveHost(ctx, host); err != nil { + t.Errorf("Failed to update host: %v", err) + } + + updated, err := store.GetHost(ctx, "integration-test-1") + if err != nil { + t.Errorf("Failed to get updated host: %v", err) + } + + if updated.Name != "Updated Test Server" { + t.Errorf("Update failed: expected 'Updated Test Server', got '%s'", updated.Name) + } + }) + + // Test 5: Export/Import + t.Run("ExportImport", func(t *testing.T) { + exportData, err := store.ExportData(ctx) + if err != nil { + t.Errorf("Failed to export: %v", err) + } + + if exportData == nil { + t.Error("Exported data is nil") + return + } + + if len(exportData.Hosts) != 1 { + t.Errorf("Expected 1 host in export, got %d", len(exportData.Hosts)) + } + + importDir := t.TempDir() + importStore, err := storage.NewJSONStorage(importDir) + if err != nil { + t.Errorf("Failed to create import storage: %v", err) + } + + if err := importStore.ImportData(ctx, exportData, storage.MergeStrategyReplace); err != nil { + t.Errorf("Failed to import: %v", err) + } + + importedHosts, err := importStore.ListHosts(ctx) + if err != nil { + t.Errorf("Failed to list imported hosts: %v", err) + } + + if len(importedHosts) != 1 { + t.Errorf("Expected 1 imported host, got %d", len(importedHosts)) + } + }) + + // Test 6: Delete host + t.Run("DeleteHost", func(t *testing.T) { + if err := store.DeleteHost(ctx, "integration-test-1"); err != nil { + t.Errorf("Failed to delete host: %v", err) + } + + hosts, err := store.ListHosts(ctx) + if err != nil { + t.Errorf("Failed to list hosts after deletion: %v", err) + } + + if len(hosts) != 0 { + t.Errorf("Expected 0 hosts after deletion, got %d", len(hosts)) + } + }) +} + +func TestConfigIntegration(t *testing.T) { + cfg, err := config.New() + if err != nil { + t.Errorf("Failed to load/create config: %v", err) + } + + if cfg.GetAppConfig().DefaultPort != 22 { + t.Errorf("Expected default port 22, got %d", cfg.GetAppConfig().DefaultPort) + } + + if cfg.GetAppConfig().ConnectionTimeout != 30 { + t.Errorf("Expected default timeout 30, got %d", cfg.GetAppConfig().ConnectionTimeout) + } +}