476 lines
11 KiB
Go
476 lines
11 KiB
Go
package application
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"rhemawavetech.com/rhemawave/internal/db"
|
|
"rhemawavetech.com/rhemawave/internal/page"
|
|
)
|
|
|
|
// Regression: The app used to reach READY before audio downloads completed,
|
|
// showing "Status: READY" while "0 / 14 Files Downloaded" was still in progress.
|
|
// The fix gates READY on downloadsComplete being true.
|
|
|
|
// TestStateTransitionsToDownloadingAssets verifies that when all config
|
|
// prerequisites are met but downloads are not complete, the state transitions
|
|
// to DOWNLOADING_ASSETS instead of READY.
|
|
func TestStateTransitionsToDownloadingAssets(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{})
|
|
app.PageRouter = &page.Router{
|
|
Context: page.PageCtx{
|
|
DbPtr: app.DB,
|
|
DownloadDone: downloadDone,
|
|
},
|
|
}
|
|
|
|
result := app.computeState()
|
|
|
|
if result != DOWNLOADING_ASSETS {
|
|
t.Errorf("state = %v, want DOWNLOADING_ASSETS (downloads not complete)", result)
|
|
}
|
|
}
|
|
|
|
// TestStateTransitionsToReadyWhenDownloadsComplete verifies that the state
|
|
// transitions to READY only after downloads are complete.
|
|
func TestStateTransitionsToReadyWhenDownloadsComplete(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 = true
|
|
|
|
downloadDone := make(chan struct{})
|
|
app.PageRouter = &page.Router{
|
|
Context: page.PageCtx{
|
|
DbPtr: app.DB,
|
|
DownloadDone: downloadDone,
|
|
},
|
|
}
|
|
|
|
result := app.computeState()
|
|
|
|
if result != READY {
|
|
t.Errorf("state = %v, want READY", result)
|
|
}
|
|
}
|
|
|
|
// Regression: Download failure should prevent the app from reaching READY.
|
|
// 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),
|
|
}
|
|
|
|
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
|
|
app.downloadFailed = true
|
|
|
|
downloadDone := make(chan struct{})
|
|
app.PageRouter = &page.Router{
|
|
Context: page.PageCtx{
|
|
DbPtr: app.DB,
|
|
DownloadDone: downloadDone,
|
|
},
|
|
}
|
|
|
|
result := app.computeState()
|
|
|
|
if result == READY {
|
|
t.Error("state should not be READY when download has failed")
|
|
}
|
|
}
|
|
|
|
// TestStateStaysReadyOnceSet verifies that once the state reaches READY,
|
|
// computeState() returns READY without checking other prerequisites.
|
|
func TestStateStaysReadyOnceSet(t *testing.T) {
|
|
app := &Application{
|
|
state: READY,
|
|
DB: setupTestDB(t),
|
|
}
|
|
|
|
downloadDone := make(chan struct{})
|
|
app.PageRouter = &page.Router{
|
|
Context: page.PageCtx{
|
|
DbPtr: app.DB,
|
|
DownloadDone: downloadDone,
|
|
},
|
|
}
|
|
|
|
result := app.computeState()
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// TestPrerequisitePriorityEmailBeforeLicense verifies that missing email
|
|
// is reported before missing license key, even when both are absent.
|
|
func TestPrerequisitePriorityEmailBeforeLicense(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 = ""
|
|
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 (email should be checked before license)", result)
|
|
}
|
|
}
|
|
|
|
// TestPrerequisitePriorityLicenseAfterEmail verifies that when email is
|
|
// present but license is missing, the state is NEEDS_LICENSE_KEY.
|
|
func TestPrerequisitePriorityLicenseAfterEmail(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)
|
|
}
|
|
}
|
|
|
|
// TestStateBlocksOnDownloadUntilChannelClosed verifies that computeState
|
|
// returns DOWNLOADING_ASSETS repeatedly until DownloadDone is closed.
|
|
func TestStateBlocksOnDownloadUntilChannelClosed(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{})
|
|
app.PageRouter = &page.Router{
|
|
Context: page.PageCtx{
|
|
DbPtr: app.DB,
|
|
DownloadDone: downloadDone,
|
|
},
|
|
}
|
|
|
|
// Should return DOWNLOADING_ASSETS multiple times before channel closes.
|
|
for i := 0; i < 5; i++ {
|
|
result := app.computeState()
|
|
if result != DOWNLOADING_ASSETS {
|
|
t.Fatalf("iteration %d: state = %v, want DOWNLOADING_ASSETS", i, result)
|
|
}
|
|
}
|
|
|
|
// Now close the channel.
|
|
close(downloadDone)
|
|
|
|
result := app.computeState()
|
|
if result != READY {
|
|
t.Errorf("after close: state = %v, want READY", result)
|
|
}
|
|
}
|
|
|
|
// TestDownloadDoneNonBlocking verifies that computeState does not block
|
|
// when DownloadDone channel is open (non-blocking select).
|
|
func TestDownloadDoneNonBlocking(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{})
|
|
app.PageRouter = &page.Router{
|
|
Context: page.PageCtx{
|
|
DbPtr: app.DB,
|
|
DownloadDone: downloadDone,
|
|
},
|
|
}
|
|
|
|
// computeState must return promptly even though DownloadDone is not closed.
|
|
done := make(chan struct{})
|
|
go func() {
|
|
result := app.computeState()
|
|
if result != DOWNLOADING_ASSETS {
|
|
t.Errorf("state = %v, want DOWNLOADING_ASSETS", result)
|
|
}
|
|
close(done)
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// Good: returned promptly.
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("computeState blocked on DownloadDone channel (should be non-blocking)")
|
|
}
|
|
}
|
|
|
|
// TestMultipleComputeStateCallsAfterDownloadComplete verifies that calling
|
|
// computeState repeatedly after downloads finish does not panic or change state.
|
|
func TestMultipleComputeStateCallsAfterDownloadComplete(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)
|
|
|
|
downloadDone := make(chan struct{})
|
|
close(downloadDone)
|
|
app.PageRouter = &page.Router{
|
|
Context: page.PageCtx{
|
|
DbPtr: app.DB,
|
|
DownloadDone: downloadDone,
|
|
},
|
|
}
|
|
|
|
// First call transitions to READY.
|
|
result := app.computeState()
|
|
if result != READY {
|
|
t.Fatalf("first call: state = %v, want READY", result)
|
|
}
|
|
app.state = result
|
|
|
|
// Subsequent calls should return READY without panic.
|
|
for i := 0; i < 10; i++ {
|
|
result = app.computeState()
|
|
if result != READY {
|
|
t.Fatalf("iteration %d: state = %v, want READY", i, result)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setupTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
d, err := db.Connect(":memory:")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Migrate(d); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := &db.AppConfig{
|
|
Model: gorm.Model{ID: 1},
|
|
DataDir: t.TempDir(),
|
|
AssetsDir: t.TempDir(),
|
|
SystemLevel: "BASIC",
|
|
SelectedAudioDevice: "default",
|
|
InitialFontSize: 128,
|
|
StandardFontSize: 32,
|
|
LinePattern: "fibonacci",
|
|
AudioEnabled: true,
|
|
SavedVolume: 1.0,
|
|
EmailAddress: "",
|
|
LicenseKey: "",
|
|
}
|
|
if err := d.Create(cfg).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return d
|
|
}
|