From bec248c53fd4d6bf06c0cba33c9781c8b95ba36f Mon Sep 17 00:00:00 2001
From: Mike Sul <mike.sul@foundries.io>
Date: Tue, 17 Dec 2024 11:24:34 +0100
Subject: [PATCH 1/3] root: Add function to expose composectl config

Signed-off-by: Mike Sul <mike.sul@foundries.io>
---
 cmd/composectl/cmd/root.go | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/cmd/composectl/cmd/root.go b/cmd/composectl/cmd/root.go
index 651520a..f0ba226 100644
--- a/cmd/composectl/cmd/root.go
+++ b/cmd/composectl/cmd/root.go
@@ -48,6 +48,10 @@ var (
 	}
 )
 
+func GetConfig() Config {
+	return config
+}
+
 func Execute() {
 	if err := rootCmd.Execute(); err != nil {
 		fmt.Println(err)

From 7a0d38696052b43db28812a187ad3730d25ba929 Mon Sep 17 00:00:00 2001
From: Mike Sul <mike.sul@foundries.io>
Date: Tue, 17 Dec 2024 14:43:31 +0100
Subject: [PATCH 2/3] check: Make app install status public

Signed-off-by: Mike Sul <mike.sul@foundries.io>
---
 cmd/composectl/cmd/check.go | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/cmd/composectl/cmd/check.go b/cmd/composectl/cmd/check.go
index b63eefa..de2f602 100644
--- a/cmd/composectl/cmd/check.go
+++ b/cmd/composectl/cmd/check.go
@@ -48,13 +48,13 @@ type (
 		InstallCheck *InstallCheckResult `json:"install_check"`
 	}
 
-	appInstallCheckResult struct {
+	AppInstallCheckResult struct {
 		AppName       string                `json:"app_name"`
 		MissingImages []string              `json:"missing_images"`
 		BundleErrors  compose.AppBundleErrs `json:"bundle_errors"`
 	}
 
-	InstallCheckResult map[string]*appInstallCheckResult
+	InstallCheckResult map[string]*AppInstallCheckResult
 )
 
 const (
@@ -305,7 +305,7 @@ func checkIfInstalled(ctx context.Context, appRefs []string, srcStorePath string
 		if err != nil {
 			return nil, err
 		}
-		checkResult[appRef] = &appInstallCheckResult{
+		checkResult[appRef] = &AppInstallCheckResult{
 			AppName:       app.Name(),
 			MissingImages: missingImages,
 			BundleErrors:  errMap,

From 294d4215d3e1f33f5e8ba623832067b89f16a54f Mon Sep 17 00:00:00 2001
From: Mike Sul <mike.sul@foundries.io>
Date: Tue, 17 Dec 2024 14:45:42 +0100
Subject: [PATCH 3/3] test: Add test for app bundle integrity check

Signed-off-by: Mike Sul <mike.sul@foundries.io>
---
 test/fixtures/composectl_cmds.go    | 37 ++++++++++++++++++++
 test/integration/edge_cases_test.go | 52 +++++++++++++++++++++++++++++
 2 files changed, 89 insertions(+)

diff --git a/test/fixtures/composectl_cmds.go b/test/fixtures/composectl_cmds.go
index 278a0b5..9d7bfea 100644
--- a/test/fixtures/composectl_cmds.go
+++ b/test/fixtures/composectl_cmds.go
@@ -223,6 +223,26 @@ func (a *App) CheckInstalled(t *testing.T) {
 	})
 }
 
+func (a *App) GetInstallCheckRes(t *testing.T) (checkRes *composectl.AppInstallCheckResult) {
+	t.Run("check if installed", func(t *testing.T) {
+		output := runCmd(t, a.Dir, "check", "--local", "--install", a.PublishedUri, "--format", "json")
+		checkResult := composectl.CheckAndInstallResult{}
+		check(t, json.Unmarshal(output, &checkResult))
+		if len(checkResult.FetchCheck.MissingBlobs) > 0 {
+			t.Errorf("there are missing app blobs: %+v\n", checkResult.FetchCheck.MissingBlobs)
+		}
+		if checkResult.InstallCheck == nil || len(*checkResult.InstallCheck) != 1 {
+			t.Errorf("invalid install check result: %+v\n", checkResult.InstallCheck)
+		}
+		var ok bool
+		checkRes, ok = (*checkResult.InstallCheck)[a.PublishedUri]
+		if !ok {
+			t.Errorf("no app in the install check result: %+v\n", *checkResult.InstallCheck)
+		}
+	})
+	return
+}
+
 func (a *App) CheckRunning(t *testing.T) {
 	t.Run("check if running", func(t *testing.T) {
 		output := runCmd(t, "", "ps", a.PublishedUri, "--format", "json")
@@ -241,6 +261,23 @@ func (a *App) CheckRunning(t *testing.T) {
 	})
 }
 
+func (a *App) GetRunningStatus(t *testing.T) (appStatus *composectl.App) {
+	t.Run("check if running", func(t *testing.T) {
+		output := runCmd(t, "", "ps", a.PublishedUri, "--format", "json")
+		var psOutput map[string]composectl.App
+		check(t, json.Unmarshal(output, &psOutput))
+		if len(psOutput) != 1 {
+			t.Errorf("expected one element in ps output, got: %d\n", len(psOutput))
+		}
+		if appStatusRes, ok := psOutput[a.PublishedUri]; ok {
+			appStatus = &appStatusRes
+		} else {
+			t.Errorf("no app URI in the ps output: %+v\n", psOutput)
+		}
+	})
+	return
+}
+
 func (a *App) runCmd(t *testing.T, desc string, args ...string) {
 	t.Run(desc, func(t *testing.T) {
 		runCmd(t, a.Dir, args...)
diff --git a/test/integration/edge_cases_test.go b/test/integration/edge_cases_test.go
index f1fc355..0bd9c46 100644
--- a/test/integration/edge_cases_test.go
+++ b/test/integration/edge_cases_test.go
@@ -1,7 +1,10 @@
 package e2e_tests
 
 import (
+	composectl "github.com/foundriesio/composeapp/cmd/composectl/cmd"
 	f "github.com/foundriesio/composeapp/test/fixtures"
+	"os"
+	"path"
 	"testing"
 )
 
@@ -67,3 +70,52 @@ services:
 	defer app.Stop(t)
 	app.CheckRunning(t)
 }
+
+func TestAppBundleBroken(t *testing.T) {
+	appComposeDef := `
+services:
+  srvs-01:
+    image: registry:5000/factory/runner-image:v0.1
+    command: sh -c "while true; do sleep 60; done"
+    ports:
+    - 8080:80
+  srvs-02:
+    image: registry:5000/factory/runner-image:v0.1
+    command: sh -c "while true; do sleep 60; done"
+`
+	app := f.NewApp(t, appComposeDef)
+	app.Publish(t)
+
+	app.Pull(t)
+	defer app.Remove(t)
+
+	app.Install(t)
+	defer app.Uninstall(t)
+	app.CheckInstalled(t)
+
+	composeFilePath := path.Join(composectl.GetConfig().ComposeRoot, app.Name, "docker-compose.yml")
+	if err := os.WriteFile(composeFilePath, []byte("foo bar"), 0x644); err != nil {
+		t.Fatal(err)
+	}
+	checkRes := app.GetInstallCheckRes(t)
+	if len(checkRes.BundleErrors) != 1 {
+		t.Fatalf("expected 1 app bundle integrity error, got: %d", len(checkRes.BundleErrors))
+	}
+	if _, ok := checkRes.BundleErrors["docker-compose.yml"]; !ok {
+		t.Fatalf("expected error for: %s, got: %+v", "docker-compose.yml", checkRes.BundleErrors)
+	}
+
+	app.Run(t)
+	defer app.Stop(t)
+	app.CheckRunning(t)
+
+	if err := os.WriteFile(composeFilePath, []byte("foo bar"), 0x644); err != nil {
+		t.Fatal(err)
+	}
+	appStatus := app.GetRunningStatus(t)
+	if appStatus.State != "running with an invalid app bundle" {
+		t.Fatalf("expected `running with an invalid app bundle`, got: %s", appStatus.State)
+	}
+	// Install app again, so it can be stopped without any error
+	app.Install(t)
+}