diff --git a/internal/application/application.go b/internal/application/application.go index da6a78b..75747a2 100644 --- a/internal/application/application.go +++ b/internal/application/application.go @@ -201,12 +201,12 @@ func Run() { app.runEventLoop() } -// checkState keeps watch of the application state and sets pages -// accordingly. This function runs once per frame. -func (app *Application) checkState() { - // app.state is READY so do nothing. +// computeState determines what state the application should be in based on +// current conditions, without performing any navigation. It returns the first +// unmet prerequisite in priority order: network > email > license > downloads > ready. +func (app *Application) computeState() State { if app.state == READY { - return + return READY } // Check if downloads have finished (non-blocking) @@ -218,26 +218,20 @@ func (app *Application) checkState() { } } - // Check if there is an internet connection, and if not, force the WiFi page + // Check if there is an internet connection if !networkIsUp { if lastNetworkCheckTime.IsZero() || time.Since(lastNetworkCheckTime) >= time.Second { lastNetworkCheckTime = time.Now() - if !network.HasInternet() { - if app.state != NEEDS_INTERNET { - app.PageRouter.NavigateTo(page.WIFI) - app.state = NEEDS_INTERNET - } - return + return NEEDS_INTERNET } - networkIsUp = true // internet confirmed, don't check again } else { - return // throttled, and we don't yet know if network is up — can't proceed + return app.state // throttled, can't determine yet } } - // Then check for an email address + // Check for email address config, err := db.GetConfig(app.DB) if err != nil { panic(err) @@ -251,38 +245,48 @@ func (app *Application) checkState() { } if config.EmailAddress == "" { - if app.state != NEEDS_EMAIL_ADDRESS { - logger.Warning("No email address detected") - app.PageRouter.NavigateTo(page.EMAIL_ADDRESS) - app.state = NEEDS_EMAIL_ADDRESS - } - return + return NEEDS_EMAIL_ADDRESS } - // Then check for a license key + // Check for license key config, err = db.GetConfig(app.DB) if err != nil { panic(err) } if config.LicenseKey == "" { - if app.state != NEEDS_LICENSE_KEY { - app.PageRouter.NavigateTo(page.LICENSE_KEY) - app.state = NEEDS_LICENSE_KEY - } - return + return NEEDS_LICENSE_KEY } - // All config prerequisites met — now wait for downloads to finish + // All config prerequisites met -- now wait for downloads to finish if !app.downloadsComplete { - if app.state != DOWNLOADING_ASSETS { - app.state = DOWNLOADING_ASSETS - app.PageRouter.NavigateTo(page.HOME) - } + return DOWNLOADING_ASSETS + } + + return READY +} + +// checkState keeps watch of the application state and sets pages +// accordingly. This function runs once per frame. +func (app *Application) checkState() { + newState := app.computeState() + + if newState == app.state { return } - app.state = READY - app.PageRouter.NavigateTo(page.HOME) + app.state = newState + + switch newState { + case NEEDS_INTERNET: + app.PageRouter.NavigateTo(page.WIFI) + case NEEDS_EMAIL_ADDRESS: + logger.Warning("No email address detected") + app.PageRouter.NavigateTo(page.EMAIL_ADDRESS) + case NEEDS_LICENSE_KEY: + app.PageRouter.NavigateTo(page.LICENSE_KEY) + case DOWNLOADING_ASSETS, READY: + app.PageRouter.NavigateTo(page.HOME) + } } func (app *Application) initializeSDLAndOpenGL() (W, H int32) { diff --git a/internal/application/application_test.go b/internal/application/application_test.go index 75765b5..6f8504d 100644 --- a/internal/application/application_test.go +++ b/internal/application/application_test.go @@ -1,5 +1,3 @@ -//go:build linux - package application import ( @@ -18,14 +16,11 @@ import ( // prerequisites are met but downloads are not complete, the state transitions // to DOWNLOADING_ASSETS instead of READY. func TestStateTransitionsToDownloadingAssets(t *testing.T) { - // Set up minimal application state app := &Application{ - state: FIRST_RUN, - DB: setupTestDB(t), - running: true, + state: FIRST_RUN, + DB: setupTestDB(t), } - // Simulate: network is up, email and license are set networkIsUp = true config, err := db.GetConfig(app.DB) if err != nil { @@ -35,10 +30,8 @@ func TestStateTransitionsToDownloadingAssets(t *testing.T) { config.LicenseKey = "test-key-123" app.DB.Save(config) - // Downloads are NOT complete app.downloadsComplete = false - // Create a minimal router with channels downloadDone := make(chan struct{}) app.PageRouter = &page.Router{ Context: page.PageCtx{ @@ -47,12 +40,10 @@ func TestStateTransitionsToDownloadingAssets(t *testing.T) { }, } - // Run checkState - app.checkState() + result := app.computeState() - // Should be in DOWNLOADING_ASSETS, not READY - if app.state != DOWNLOADING_ASSETS { - t.Errorf("state = %v, want DOWNLOADING_ASSETS (downloads not complete)", app.state) + if result != DOWNLOADING_ASSETS { + t.Errorf("state = %v, want DOWNLOADING_ASSETS (downloads not complete)", result) } } @@ -60,9 +51,8 @@ func TestStateTransitionsToDownloadingAssets(t *testing.T) { // transitions to READY only after downloads are complete. func TestStateTransitionsToReadyWhenDownloadsComplete(t *testing.T) { app := &Application{ - state: FIRST_RUN, - DB: setupTestDB(t), - running: true, + state: FIRST_RUN, + DB: setupTestDB(t), } networkIsUp = true @@ -74,7 +64,6 @@ func TestStateTransitionsToReadyWhenDownloadsComplete(t *testing.T) { config.LicenseKey = "test-key-123" app.DB.Save(config) - // Downloads ARE complete app.downloadsComplete = true downloadDone := make(chan struct{}) @@ -85,10 +74,10 @@ func TestStateTransitionsToReadyWhenDownloadsComplete(t *testing.T) { }, } - app.checkState() + result := app.computeState() - if app.state != READY { - t.Errorf("state = %v, want READY", app.state) + if result != READY { + t.Errorf("state = %v, want READY", result) } } @@ -96,9 +85,8 @@ func TestStateTransitionsToReadyWhenDownloadsComplete(t *testing.T) { // If downloads fail, the user sees an error and must restart to retry. func TestDownloadFailurePreventsReady(t *testing.T) { app := &Application{ - state: FIRST_RUN, - DB: setupTestDB(t), - running: true, + state: FIRST_RUN, + DB: setupTestDB(t), } networkIsUp = true @@ -110,7 +98,6 @@ func TestDownloadFailurePreventsReady(t *testing.T) { config.LicenseKey = "test-key-123" app.DB.Save(config) - // Downloads completed but failed app.downloadsComplete = false app.downloadFailed = true @@ -122,21 +109,19 @@ func TestDownloadFailurePreventsReady(t *testing.T) { }, } - app.checkState() + result := app.computeState() - // Should NOT be READY — download failed - if app.state == READY { + if result == READY { t.Error("state should not be READY when download has failed") } } // TestStateStaysReadyOnceSet verifies that once the state reaches READY, -// checkState() is a no-op (doesn't reset or re-navigate). +// computeState() returns READY without checking other prerequisites. func TestStateStaysReadyOnceSet(t *testing.T) { app := &Application{ - state: READY, - DB: setupTestDB(t), - running: true, + state: READY, + DB: setupTestDB(t), } downloadDone := make(chan struct{}) @@ -147,14 +132,119 @@ func TestStateStaysReadyOnceSet(t *testing.T) { }, } - app.checkState() + result := app.computeState() - if app.state != READY { - t.Errorf("state = %v, want READY (should not change once set)", app.state) + if result != READY { + t.Errorf("state = %v, want READY (should not change once set)", result) + } +} + +// TestEmailMissingPreventsReady verifies that missing email blocks progress +// even when network is up and downloads are complete. +func TestEmailMissingPreventsReady(t *testing.T) { + app := &Application{ + state: FIRST_RUN, + DB: setupTestDB(t), + } + + networkIsUp = true + config, err := db.GetConfig(app.DB) + if err != nil { + t.Fatal(err) + } + config.EmailAddress = "" + config.LicenseKey = "test-key-123" + app.DB.Save(config) + + app.downloadsComplete = true + + downloadDone := make(chan struct{}) + app.PageRouter = &page.Router{ + Context: page.PageCtx{ + DbPtr: app.DB, + DownloadDone: downloadDone, + }, + } + + result := app.computeState() + + if result != NEEDS_EMAIL_ADDRESS { + t.Errorf("state = %v, want NEEDS_EMAIL_ADDRESS", result) + } +} + +// TestLicenseMissingPreventsReady verifies that missing license key blocks +// progress even when email and network are present. +func TestLicenseMissingPreventsReady(t *testing.T) { + app := &Application{ + state: FIRST_RUN, + DB: setupTestDB(t), + } + + networkIsUp = true + config, err := db.GetConfig(app.DB) + if err != nil { + t.Fatal(err) + } + config.EmailAddress = "test@example.com" + config.LicenseKey = "" + app.DB.Save(config) + + app.downloadsComplete = true + + downloadDone := make(chan struct{}) + app.PageRouter = &page.Router{ + Context: page.PageCtx{ + DbPtr: app.DB, + DownloadDone: downloadDone, + }, + } + + result := app.computeState() + + if result != NEEDS_LICENSE_KEY { + t.Errorf("state = %v, want NEEDS_LICENSE_KEY", result) + } +} + +// TestDownloadDoneChannelSetsDownloadsComplete verifies that receiving on +// the DownloadDone channel sets downloadsComplete to true. +func TestDownloadDoneChannelSetsDownloadsComplete(t *testing.T) { + app := &Application{ + state: FIRST_RUN, + DB: setupTestDB(t), + } + + networkIsUp = true + config, err := db.GetConfig(app.DB) + if err != nil { + t.Fatal(err) + } + config.EmailAddress = "test@example.com" + config.LicenseKey = "test-key-123" + app.DB.Save(config) + + app.downloadsComplete = false + + downloadDone := make(chan struct{}) + close(downloadDone) + app.PageRouter = &page.Router{ + Context: page.PageCtx{ + DbPtr: app.DB, + DownloadDone: downloadDone, + }, + } + + result := app.computeState() + + if !app.downloadsComplete { + t.Error("downloadsComplete should be true after DownloadDone channel is closed") + } + if result != READY { + t.Errorf("state = %v, want READY after downloads complete", result) } } -// setupTestDB creates a temporary in-memory SQLite database for testing. func setupTestDB(t *testing.T) *gorm.DB { t.Helper() d, err := db.Connect(":memory:") @@ -164,7 +254,6 @@ func setupTestDB(t *testing.T) *gorm.DB { if err := db.Migrate(d); err != nil { t.Fatal(err) } - // Create default config cfg := &db.AppConfig{ Model: gorm.Model{ID: 1}, DataDir: t.TempDir(), diff --git a/internal/network/network.go b/internal/network/network.go index b753b9b..1510fc3 100644 --- a/internal/network/network.go +++ b/internal/network/network.go @@ -40,8 +40,7 @@ func init() { var err error nm, err = gonm.NewNetworkManager() if err != nil { - logger.Fatal("network: failed to connect to NetworkManager: %v\n", err) - panic(err) + logger.Warning("network: NetworkManager unavailable, network features disabled: %v", err) } }