Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/mas.brewfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
tap "homebrew/cask"
brew "mas"
brew "wget"
cask "firefox"
mas "AmorphousDiskMark", id: 1168254295
mas "Display Menu", id: 549083868
9 changes: 9 additions & 0 deletions internal/models/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ type Package struct {
InstalledOnRequest bool
}

// Label returns the best human-readable name for UI display.
// For MAS packages, Name is the numeric ID so we prefer DisplayName.
func (p *Package) Label() string {
if p.Type == PackageTypeMas && p.DisplayName != "" {
return p.DisplayName
}
return p.Name
}

// NewPackageFromFormula creates a Package from a Formula.
func NewPackageFromFormula(f *Formula) Package {
installedOnRequest := false
Expand Down
44 changes: 44 additions & 0 deletions internal/models/package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,47 @@ func TestNewPackageFromCask_EmptyName(t *testing.T) {
t.Errorf("DisplayName = %q, want %q (fallback to Token)", pkg.DisplayName, "my-app")
}
}

func TestPackageLabel_MasUsesDisplayName(t *testing.T) {
pkg := Package{
Name: "1168254295",
DisplayName: "AmorphousDiskMark",
Type: PackageTypeMas,
}
if got := pkg.Label(); got != "AmorphousDiskMark" {
t.Errorf("Label() = %q, want %q", got, "AmorphousDiskMark")
}
}

func TestPackageLabel_MasFallsBackToName(t *testing.T) {
pkg := Package{
Name: "1168254295",
DisplayName: "",
Type: PackageTypeMas,
}
if got := pkg.Label(); got != "1168254295" {
t.Errorf("Label() = %q, want %q", got, "1168254295")
}
}

func TestPackageLabel_FormulaUsesName(t *testing.T) {
pkg := Package{
Name: "wget",
DisplayName: "GNU Wget",
Type: PackageTypeFormula,
}
if got := pkg.Label(); got != "wget" {
t.Errorf("Label() = %q, want %q", got, "wget")
}
}

func TestPackageLabel_CaskUsesName(t *testing.T) {
pkg := Package{
Name: "firefox",
DisplayName: "Mozilla Firefox",
Type: PackageTypeCask,
}
if got := pkg.Label(); got != "firefox" {
t.Errorf("Label() = %q, want %q", got, "firefox")
}
}
17 changes: 15 additions & 2 deletions internal/services/brewfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ func (s *AppService) loadBrewfilePackages() error {
// Create a map for quick lookup of Brewfile entries
packageMap := make(map[string]models.PackageType)
for _, entry := range result.Packages {
if entry.IsMas {
continue
}
if entry.IsCask {
packageMap[entry.Name] = models.PackageTypeCask
} else if entry.IsFlatpak {
Expand Down Expand Up @@ -280,11 +283,21 @@ func (s *AppService) loadBrewfilePackages() error {
if !entry.IsMas || foundPackages[entry.MasID] {
continue
}

version := ""
homepage := ""
description := "Mac App Store app"
if appInfo, err := s.masService.GetAppInfo(entry.MasID); err == nil && appInfo != nil {
version = appInfo.Version
homepage = appInfo.Homepage
}

pkg := models.Package{
Name: entry.MasID,
DisplayName: entry.Name,
Description: "Mac App Store app",
Version: "",
Description: description,
Version: version,
Homepage: homepage,
Type: models.PackageTypeMas,
LocallyInstalled: masInstalledMap[entry.MasID],
InstalledOnRequest: true,
Expand Down
77 changes: 77 additions & 0 deletions internal/services/brewfile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,83 @@ func TestParseBrewfileWithTaps_FileNotFound(t *testing.T) {
}
}

func TestParseBrewfileWithTaps_MasEntriesNotInPackageMap(t *testing.T) {
content := `brew "wget"
cask "firefox"
mas "AmorphousDiskMark", id: 1168254295
mas "Display Menu", id: 549083868
`
tmpFile := createTempBrewfile(t, content)

result, err := parseBrewfileWithTaps(tmpFile)
if err != nil {
t.Fatalf("parseBrewfileWithTaps() error: %v", err)
}

// Build the packageMap the same way loadBrewfilePackages does
packageMap := make(map[string]string)
for _, entry := range result.Packages {
if entry.IsMas {
continue
}
if entry.IsCask {
packageMap[entry.Name] = "cask"
} else if entry.IsFlatpak {
packageMap[entry.Name] = "flatpak"
} else {
packageMap[entry.Name] = "formula"
}
}

// MAS app names must NOT appear in the package map
if _, exists := packageMap["AmorphousDiskMark"]; exists {
t.Error("MAS entry 'AmorphousDiskMark' should not be in packageMap")
}
if _, exists := packageMap["Display Menu"]; exists {
t.Error("MAS entry 'Display Menu' should not be in packageMap")
}

// Regular entries should be present
if _, exists := packageMap["wget"]; !exists {
t.Error("formula 'wget' should be in packageMap")
}
if _, exists := packageMap["firefox"]; !exists {
t.Error("cask 'firefox' should be in packageMap")
}
}

func TestParseBrewfileWithTaps_MasEntriesExcludedFromTapEntries(t *testing.T) {
content := `brew "wget"
mas "AmorphousDiskMark", id: 1168254295
mas "Hand Mirror", id: 1502839586
`
tmpFile := createTempBrewfile(t, content)

result, err := parseBrewfileWithTaps(tmpFile)
if err != nil {
t.Fatalf("parseBrewfileWithTaps() error: %v", err)
}

// Simulate the tap entry collection logic from loadBrewfilePackages:
// entries not found in main list, excluding flatpak and mas
foundPackages := make(map[string]bool)
foundPackages["wget"] = true // pretend wget was found in Homebrew catalog

var tapEntries []string
for _, entry := range result.Packages {
if !foundPackages[entry.Name] && !entry.IsFlatpak && !entry.IsMas {
tapEntries = append(tapEntries, entry.Name)
}
}

// MAS entries must not end up as tap entries
for _, name := range tapEntries {
if name == "AmorphousDiskMark" || name == "Hand Mirror" {
t.Errorf("MAS entry %q should not appear in tapEntries", name)
}
}
}

func createTempBrewfile(t *testing.T, content string) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "Brewfile")
Expand Down
2 changes: 1 addition & 1 deletion internal/services/dataprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ func (d *DataProvider) GetTapPackages(entries []models.BrewfileEntry, existingPa
var missingFormulae []string

for _, entry := range entries {
if entry.IsFlatpak {
if entry.IsFlatpak || entry.IsMas {
continue
}

Expand Down
106 changes: 106 additions & 0 deletions internal/services/dataprovider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package services

import (
"testing"

"bbrew/internal/models"
)

func TestGetTapPackages_SkipsMasEntries(t *testing.T) {
d := &DataProvider{}

entries := []models.BrewfileEntry{
{Name: "AmorphousDiskMark", IsMas: true, MasID: "1168254295"},
{Name: "Display Menu", IsMas: true, MasID: "549083868"},
{Name: "org.gnome.Calculator", IsFlatpak: true},
}

existingPackages := make(map[string]models.Package)

result, err := d.GetTapPackages(entries, existingPackages, false)
if err != nil {
t.Fatalf("GetTapPackages() error: %v", err)
}

// All entries are MAS or Flatpak, so nothing should be returned
if len(result) != 0 {
t.Errorf("GetTapPackages() returned %d packages, want 0 (MAS and Flatpak should be skipped)", len(result))
for _, pkg := range result {
t.Errorf(" unexpected package: %s (type: %s)", pkg.Name, pkg.Type)
}
}
}

func TestGetTapPackages_MixedEntries(t *testing.T) {
d := &DataProvider{}

entries := []models.BrewfileEntry{
{Name: "AmorphousDiskMark", IsMas: true, MasID: "1168254295"},
{Name: "org.gnome.Calculator", IsFlatpak: true},
{Name: "wget"},
}

// Provide wget as an existing package so it gets returned without brew info fetch
existingPackages := map[string]models.Package{
"wget": {Name: "wget", Type: models.PackageTypeFormula, Description: "Internet file retriever"},
}

result, err := d.GetTapPackages(entries, existingPackages, false)
if err != nil {
t.Fatalf("GetTapPackages() error: %v", err)
}

// Only wget should be returned (MAS and Flatpak are skipped)
if len(result) != 1 {
t.Fatalf("GetTapPackages() returned %d packages, want 1", len(result))
}

if result[0].Name != "wget" {
t.Errorf("GetTapPackages()[0].Name = %q, want %q", result[0].Name, "wget")
}
}

func TestGetFlatpakPackages_Basic(t *testing.T) {
d := &DataProvider{}

entries := []models.BrewfileEntry{
{Name: "com.spotify.Client", IsFlatpak: true},
{Name: "org.mozilla.firefox", IsFlatpak: true},
{Name: "wget"},
{Name: "AmorphousDiskMark", IsMas: true, MasID: "1168254295"},
}

installedIDs := map[string]bool{
"com.spotify.Client": true,
}

metadata := map[string]models.Package{
"com.spotify.Client": {
Name: "com.spotify.Client",
DisplayName: "Spotify",
Description: "Music streaming service",
Version: "1.2.3",
},
}

result, err := d.GetFlatpakPackages(entries, installedIDs, metadata)
if err != nil {
t.Fatalf("GetFlatpakPackages() error: %v", err)
}

// Should only return flatpak entries (2), not formula or MAS
if len(result) != 2 {
t.Fatalf("GetFlatpakPackages() returned %d packages, want 2", len(result))
}

// Verify Spotify is marked as installed with metadata
if result[0].Name != "com.spotify.Client" {
t.Errorf("result[0].Name = %q, want %q", result[0].Name, "com.spotify.Client")
}
if !result[0].LocallyInstalled {
t.Error("Spotify should be marked as locally installed")
}
if result[0].DisplayName != "Spotify" {
t.Errorf("result[0].DisplayName = %q, want %q", result[0].DisplayName, "Spotify")
}
}
Loading
Loading