initial commit
This commit is contained in:
@@ -0,0 +1,475 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user