package main import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "mime/multipart" "net/http" "testing" "gcy_hpc_server/internal/model" "gcy_hpc_server/internal/testutil/testenv" ) // uploadResponse mirrors server.APIResponse for upload integration tests. type uploadAPIResp struct { Success bool `json:"success"` Data json.RawMessage `json:"data,omitempty"` Error string `json:"error,omitempty"` } // uploadDecode parses an HTTP response body into uploadAPIResp. func uploadDecode(t *testing.T, body io.Reader) uploadAPIResp { t.Helper() data, err := io.ReadAll(body) if err != nil { t.Fatalf("uploadDecode: read body: %v", err) } var r uploadAPIResp if err := json.Unmarshal(data, &r); err != nil { t.Fatalf("uploadDecode: unmarshal: %v (body: %s)", err, string(data)) } return r } // uploadInitSession calls InitUpload and returns the created session. // Uses the real HTTP server from testenv. func uploadInitSession(t *testing.T, env *testenv.TestEnv, fileName string, fileSize int64, sha256Hash string) model.UploadSessionResponse { t.Helper() reqBody := model.InitUploadRequest{ FileName: fileName, FileSize: fileSize, SHA256: sha256Hash, } body, _ := json.Marshal(reqBody) resp := env.DoRequest("POST", "/api/v1/files/uploads", bytes.NewReader(body)) defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("uploadInitSession: expected 201, got %d", resp.StatusCode) } r := uploadDecode(t, resp.Body) if !r.Success { t.Fatalf("uploadInitSession: response not success: %s", r.Error) } var session model.UploadSessionResponse if err := json.Unmarshal(r.Data, &session); err != nil { t.Fatalf("uploadInitSession: unmarshal session: %v", err) } return session } // uploadSendChunk sends a single chunk via multipart form data. // Uses raw HTTP client to set the correct multipart content type. func uploadSendChunk(t *testing.T, env *testenv.TestEnv, sessionID int64, chunkIndex int, chunkData []byte) { t.Helper() url := fmt.Sprintf("%s/api/v1/files/uploads/%d/chunks/%d", env.URL(), sessionID, chunkIndex) var buf bytes.Buffer writer := multipart.NewWriter(&buf) part, err := writer.CreateFormFile("chunk", "chunk.bin") if err != nil { t.Fatalf("uploadSendChunk: create form file: %v", err) } part.Write(chunkData) writer.Close() req, err := http.NewRequest("PUT", url, &buf) if err != nil { t.Fatalf("uploadSendChunk: new request: %v", err) } req.Header.Set("Content-Type", writer.FormDataContentType()) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("uploadSendChunk: do request: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("uploadSendChunk: expected 200, got %d: %s", resp.StatusCode, string(bodyBytes)) } } func TestIntegration_Upload_Init(t *testing.T) { env := testenv.NewTestEnv(t) fileData := []byte("integration test upload init content") h := sha256.Sum256(fileData) sha256Hash := hex.EncodeToString(h[:]) session := uploadInitSession(t, env, "init_test.txt", int64(len(fileData)), sha256Hash) if session.ID <= 0 { t.Fatalf("expected positive session ID, got %d", session.ID) } if session.FileName != "init_test.txt" { t.Fatalf("expected file_name init_test.txt, got %s", session.FileName) } if session.Status != "pending" { t.Fatalf("expected status pending, got %s", session.Status) } if session.TotalChunks != 1 { t.Fatalf("expected 1 chunk for small file, got %d", session.TotalChunks) } if session.FileSize != int64(len(fileData)) { t.Fatalf("expected file_size %d, got %d", len(fileData), session.FileSize) } if session.SHA256 != sha256Hash { t.Fatalf("expected sha256 %s, got %s", sha256Hash, session.SHA256) } } func TestIntegration_Upload_Status(t *testing.T) { env := testenv.NewTestEnv(t) fileData := []byte("integration test status content") h := sha256.Sum256(fileData) sha256Hash := hex.EncodeToString(h[:]) session := uploadInitSession(t, env, "status_test.txt", int64(len(fileData)), sha256Hash) resp := env.DoRequest("GET", fmt.Sprintf("/api/v1/files/uploads/%d", session.ID), nil) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } r := uploadDecode(t, resp.Body) if !r.Success { t.Fatalf("response not success: %s", r.Error) } var status model.UploadSessionResponse if err := json.Unmarshal(r.Data, &status); err != nil { t.Fatalf("unmarshal status: %v", err) } if status.ID != session.ID { t.Fatalf("expected session ID %d, got %d", session.ID, status.ID) } if status.Status != "pending" { t.Fatalf("expected status pending, got %s", status.Status) } if status.FileName != "status_test.txt" { t.Fatalf("expected file_name status_test.txt, got %s", status.FileName) } } func TestIntegration_Upload_Chunk(t *testing.T) { env := testenv.NewTestEnv(t) fileData := []byte("integration test chunk upload data") h := sha256.Sum256(fileData) sha256Hash := hex.EncodeToString(h[:]) session := uploadInitSession(t, env, "chunk_test.txt", int64(len(fileData)), sha256Hash) uploadSendChunk(t, env, session.ID, 0, fileData) // Verify chunk appears in uploaded_chunks via status endpoint resp := env.DoRequest("GET", fmt.Sprintf("/api/v1/files/uploads/%d", session.ID), nil) defer resp.Body.Close() r := uploadDecode(t, resp.Body) var status model.UploadSessionResponse if err := json.Unmarshal(r.Data, &status); err != nil { t.Fatalf("unmarshal status after chunk: %v", err) } if len(status.UploadedChunks) != 1 { t.Fatalf("expected 1 uploaded chunk, got %d", len(status.UploadedChunks)) } if status.UploadedChunks[0] != 0 { t.Fatalf("expected uploaded chunk index 0, got %d", status.UploadedChunks[0]) } } func TestIntegration_Upload_Complete(t *testing.T) { env := testenv.NewTestEnv(t) fileData := []byte("integration test complete upload data") h := sha256.Sum256(fileData) sha256Hash := hex.EncodeToString(h[:]) session := uploadInitSession(t, env, "complete_test.txt", int64(len(fileData)), sha256Hash) // Upload all chunks for i := 0; i < session.TotalChunks; i++ { uploadSendChunk(t, env, session.ID, i, fileData) } // Complete upload resp := env.DoRequest("POST", fmt.Sprintf("/api/v1/files/uploads/%d/complete", session.ID), nil) defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("expected 201, got %d: %s", resp.StatusCode, string(bodyBytes)) } r := uploadDecode(t, resp.Body) if !r.Success { t.Fatalf("complete response not success: %s", r.Error) } var fileResp model.FileResponse if err := json.Unmarshal(r.Data, &fileResp); err != nil { t.Fatalf("unmarshal file response: %v", err) } if fileResp.ID <= 0 { t.Fatalf("expected positive file ID, got %d", fileResp.ID) } if fileResp.Name != "complete_test.txt" { t.Fatalf("expected name complete_test.txt, got %s", fileResp.Name) } if fileResp.Size != int64(len(fileData)) { t.Fatalf("expected size %d, got %d", len(fileData), fileResp.Size) } if fileResp.SHA256 != sha256Hash { t.Fatalf("expected sha256 %s, got %s", sha256Hash, fileResp.SHA256) } } func TestIntegration_Upload_Cancel(t *testing.T) { env := testenv.NewTestEnv(t) fileData := []byte("integration test cancel upload data") h := sha256.Sum256(fileData) sha256Hash := hex.EncodeToString(h[:]) session := uploadInitSession(t, env, "cancel_test.txt", int64(len(fileData)), sha256Hash) // Cancel the upload resp := env.DoRequest("DELETE", fmt.Sprintf("/api/v1/files/uploads/%d", session.ID), nil) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) t.Fatalf("expected 200, got %d: %s", resp.StatusCode, string(bodyBytes)) } r := uploadDecode(t, resp.Body) if !r.Success { t.Fatalf("cancel response not success: %s", r.Error) } // Verify session is no longer in pending state by checking status statusResp := env.DoRequest("GET", fmt.Sprintf("/api/v1/files/uploads/%d", session.ID), nil) defer statusResp.Body.Close() sr := uploadDecode(t, statusResp.Body) if sr.Success { var status model.UploadSessionResponse if err := json.Unmarshal(sr.Data, &status); err == nil { if status.Status == "pending" { t.Fatal("expected status to not be pending after cancel") } } } }