fleet/ee/maintained-apps/ingesters/homebrew/ingester_test.go
Jonathan Katz 0d15fd6cd6
Override patch policy query (#42322)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41815
### Changes
- Extracted patch policy creation to `pkg/patch_policy`
- Added a `patch_query` column to the `software_installers` table
- By default that column is empty, and patch policies will generate with
the default query if so
- On app manifest ingestion, the appropriate entry in
`software_installers` will save the override "patch" query from the
manifest in patch_query

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [ ] QA'd all new/changed functionality manually
- Relied on integration test for FMA version pinning

## Database migrations

- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [ ] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
2026-03-25 10:32:41 -04:00

161 lines
5.3 KiB
Go

package homebrew
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/stretchr/testify/require"
)
func TestIngestValidations(t *testing.T) {
tempDir := t.TempDir()
testInstallScriptContents := "this is a test install script"
require.NoError(t, os.WriteFile(path.Join(tempDir, "install_script.sh"), []byte(testInstallScriptContents), 0644))
testUninstallScriptContents := "this is a test uninstall script"
require.NoError(t, os.WriteFile(path.Join(tempDir, "uninstall_script.sh"), []byte(testUninstallScriptContents), 0644))
testPatchPolicyContents := `query: "SELECT 1;"`
require.NoError(t, os.WriteFile(path.Join(tempDir, "policy.yml"), []byte(testPatchPolicyContents), 0644))
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var cask brewCask
appToken := strings.TrimSuffix(path.Base(r.URL.Path), ".json")
switch appToken {
case "fail":
w.WriteHeader(http.StatusInternalServerError)
return
case "notfound":
w.WriteHeader(http.StatusNotFound)
return
case "noname":
cask = brewCask{
Token: appToken,
Name: nil,
URL: "https://example.com",
Version: "1.0",
}
case "emptyname":
cask = brewCask{
Token: appToken,
Name: []string{""},
URL: "https://example.com",
Version: "1.0",
}
case "notoken":
cask = brewCask{
Token: "",
Name: []string{appToken},
URL: "https://example.com",
Version: "1.0",
}
case "noversion":
cask = brewCask{
Token: appToken,
Name: []string{appToken},
URL: "https://example.com",
Version: "",
}
case "nourl":
cask = brewCask{
Token: appToken,
Name: []string{appToken},
URL: "",
Version: "1.0",
}
case "invalidurl":
cask = brewCask{
Token: appToken,
Name: []string{appToken},
URL: "https://\x00\x01\x02",
Version: "1.0",
}
case "ok", "install_script_path", "uninstall_script_path", "uninstall_script_path_with_pre", "uninstall_script_path_with_post", "patch_policy_path":
cask = brewCask{
Token: appToken,
Name: []string{appToken},
URL: "https://example.com",
Version: "1.0",
}
default:
w.WriteHeader(http.StatusBadRequest)
t.Fatalf("unexpected app token %s", appToken)
}
err := json.NewEncoder(w).Encode(cask)
require.NoError(t, err)
}))
t.Cleanup(srv.Close)
ctx := context.Background()
cases := []struct {
wantErr string
inputApp inputApp
}{
{"brew API returned status 500", inputApp{Token: "fail", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
{"app not found in brew API", inputApp{Token: "notfound", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
{"missing name for cask noname", inputApp{Token: "noname", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
{"missing name for cask emptyname", inputApp{Token: "emptyname", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
{"missing token for cask notoken", inputApp{Token: "notoken", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
{"missing version for cask noversion", inputApp{Token: "noversion", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
{"missing URL for cask nourl", inputApp{Token: "nourl", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
{"parse URL for cask invalidurl", inputApp{Token: "invalidurl", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
{"", inputApp{Token: "ok", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
{"", inputApp{Token: "install_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", InstallScriptPath: path.Join(tempDir, "install_script.sh")}},
{"", inputApp{Token: "uninstall_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh")}},
{"cannot provide pre-uninstall scripts if uninstall script is provided", inputApp{Token: "uninstall_script_path_with_pre", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh"), PreUninstallScripts: []string{"foo", "bar"}}},
{"cannot provide post-uninstall scripts if uninstall script is provided", inputApp{Token: "uninstall_script_path_with_post", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh"), PostUninstallScripts: []string{"foo", "bar"}}},
{"", inputApp{Token: "patch_policy_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", PatchPolicyPath: path.Join(tempDir, "policy.yml")}},
}
for _, c := range cases {
t.Run(c.inputApp.Token, func(t *testing.T) {
i := &brewIngester{
logger: slog.New(slog.DiscardHandler),
client: fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)),
baseURL: srv.URL + "/",
}
out, err := i.ingestOne(ctx, c.inputApp)
if c.wantErr != "" {
require.ErrorContains(t, err, c.wantErr)
return
}
require.NoError(t, err)
if c.inputApp.InstallScriptPath != "" {
require.Equal(t, testInstallScriptContents, out.InstallScript)
}
if c.inputApp.UninstallScriptPath != "" {
require.Equal(t, testUninstallScriptContents, out.UninstallScript)
}
if c.inputApp.PatchPolicyPath != "" {
require.Equal(t, "SELECT 1;", out.Queries.Patch)
}
})
}
}