@@ -11,6 +11,7 @@ import (
1111 "github.com/surge-downloader/surge/internal/engine/events"
1212 "github.com/surge-downloader/surge/internal/engine/state"
1313 "github.com/surge-downloader/surge/internal/engine/types"
14+ "github.com/surge-downloader/surge/internal/testutil"
1415)
1516
1617func TestLocalDownloadService_Delete_DBOnlyBroadcastsRemoved (t * testing.T ) {
@@ -81,3 +82,113 @@ func TestLocalDownloadService_Delete_DBOnlyBroadcastsRemoved(t *testing.T) {
8182 t .Fatalf ("expected entry to be removed, got %+v" , entry )
8283 }
8384}
85+
86+ func TestLocalDownloadService_Shutdown_Idempotent (t * testing.T ) {
87+ ch := make (chan interface {}, 1 )
88+ svc := NewLocalDownloadServiceWithInput (nil , ch )
89+
90+ if err := svc .Shutdown (); err != nil {
91+ t .Fatalf ("first shutdown failed: %v" , err )
92+ }
93+
94+ select {
95+ case _ , ok := <- ch :
96+ if ok {
97+ t .Fatal ("expected input channel to be closed after shutdown" )
98+ }
99+ case <- time .After (500 * time .Millisecond ):
100+ t .Fatal ("timed out waiting for input channel to close" )
101+ }
102+
103+ if err := svc .Shutdown (); err != nil {
104+ t .Fatalf ("second shutdown failed: %v" , err )
105+ }
106+ }
107+
108+ func TestLocalDownloadService_Shutdown_PersistsPausedState (t * testing.T ) {
109+ tempDir := t .TempDir ()
110+ state .CloseDB ()
111+ state .Configure (filepath .Join (tempDir , "surge.db" ))
112+ defer state .CloseDB ()
113+
114+ ch := make (chan interface {}, 100 )
115+ pool := download .NewWorkerPool (ch , 1 )
116+ svc := NewLocalDownloadServiceWithInput (pool , ch )
117+ defer func () { _ = svc .Shutdown () }()
118+
119+ server := testutil .NewStreamingMockServerT (t ,
120+ 500 * 1024 * 1024 ,
121+ testutil .WithRangeSupport (true ),
122+ testutil .WithLatency (10 * time .Millisecond ),
123+ )
124+ defer server .Close ()
125+
126+ outputDir := t .TempDir ()
127+ const filename = "persist.bin"
128+ id , err := svc .Add (server .URL (), outputDir , filename , nil , nil )
129+ if err != nil {
130+ t .Fatalf ("failed to add download: %v" , err )
131+ }
132+
133+ deadline := time .Now ().Add (8 * time .Second )
134+ progressed := false
135+ for time .Now ().Before (deadline ) {
136+ st , err := svc .GetStatus (id )
137+ if err == nil && st != nil && st .Downloaded > 0 {
138+ progressed = true
139+ break
140+ }
141+ time .Sleep (50 * time .Millisecond )
142+ }
143+ if ! progressed {
144+ t .Fatal ("download did not make progress before shutdown" )
145+ }
146+
147+ if err := svc .Shutdown (); err != nil {
148+ t .Fatalf ("shutdown failed: %v" , err )
149+ }
150+
151+ entry , err := state .GetDownload (id )
152+ if err != nil {
153+ t .Fatalf ("failed to fetch persisted download: %v" , err )
154+ }
155+ if entry == nil {
156+ t .Fatal ("expected persisted download entry after shutdown" )
157+ }
158+ if entry .Status != "paused" {
159+ t .Fatalf ("status = %q, want paused" , entry .Status )
160+ }
161+ if entry .Downloaded == 0 {
162+ t .Fatal ("expected persisted paused download to have non-zero progress" )
163+ }
164+
165+ statuses , err := svc .List ()
166+ if err != nil {
167+ t .Fatalf ("failed to list downloads after shutdown: %v" , err )
168+ }
169+ foundInList := false
170+ for _ , st := range statuses {
171+ if st .ID == id {
172+ foundInList = true
173+ if st .Status != "paused" && st .Status != "pausing" {
174+ t .Fatalf ("list status = %q, want paused/pausing" , st .Status )
175+ }
176+ break
177+ }
178+ }
179+ if ! foundInList {
180+ t .Fatal ("expected paused download to remain visible in list after shutdown" )
181+ }
182+
183+ destPath := filepath .Join (outputDir , filename )
184+ saved , err := state .LoadState (server .URL (), destPath )
185+ if err != nil {
186+ t .Fatalf ("failed to load saved state: %v" , err )
187+ }
188+ if saved .ID != id {
189+ t .Fatalf ("saved state id = %q, want %q" , saved .ID , id )
190+ }
191+ if len (saved .Tasks ) == 0 {
192+ t .Fatal ("expected saved state to include remaining tasks" )
193+ }
194+ }
0 commit comments