feat(orbitviewer): add satellite orbit viewer with 2D track, 10-day pass prediction, and AR sky view
Build and Push Docker Image / build (push) Successful in 2m31s

This commit is contained in:
2026-09-23 00:06:01 -07:00
parent c4caed6569
commit fa3dc66278
17 changed files with 4381 additions and 95 deletions
+201
View File
@@ -0,0 +1,201 @@
package orbitviewer
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func setupTestRouter() *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
api := r.Group("/api/orbit-viewer")
RegisterRoutes(api)
return r
}
func TestAPISatellites(t *testing.T) {
r := setupTestRouter()
req, _ := http.NewRequest("GET", "/api/orbit-viewer/satellites", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("Expected status 200, got %d", w.Code)
}
var resp struct {
Satellites []*SatelliteCatalogEntry `json:"satellites"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}
if len(resp.Satellites) == 0 {
t.Errorf("Expected satellites in catalog, got 0")
}
hasISS := false
for _, sat := range resp.Satellites {
if sat.ID == "iss" {
hasISS = true
break
}
}
if !hasISS {
t.Errorf("Expected ISS in satellite catalog")
}
}
func TestAPITrack(t *testing.T) {
r := setupTestRouter()
payload := TrackRequest{
SatelliteID: "iss",
Observer: &Geodetic{
Latitude: 39.9042,
Longitude: 116.4074,
Altitude: 0.05,
},
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/orbit-viewer/track", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
var resp TrackResponse
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}
if resp.SatelliteID != "iss" {
t.Errorf("Expected satellite_id iss, got %s", resp.SatelliteID)
}
if resp.AltitudeKm < 300 || resp.AltitudeKm > 500 {
t.Errorf("Unrealistic ISS altitude: %f km", resp.AltitudeKm)
}
if len(resp.PastTrack) == 0 || len(resp.FutureTrack) == 0 {
t.Errorf("Expected track points in response")
}
if resp.ObserverLook == nil {
t.Errorf("Expected ObserverLook populated")
}
}
func TestAPIPasses(t *testing.T) {
r := setupTestRouter()
payload := PassesRequest{
SatelliteID: "iss",
Observer: Geodetic{
Latitude: 37.7749,
Longitude: -122.4194,
Altitude: 0.03,
},
Days: 10,
MinElevationDeg: 10.0,
VisibleOnly: false,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/orbit-viewer/passes", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
var resp struct {
Count int `json:"count"`
Passes []SatellitePass `json:"passes"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}
if resp.Count == 0 {
t.Errorf("Expected passes for ISS over 10 days, got 0")
}
}
func TestAPIPosition(t *testing.T) {
r := setupTestRouter()
payload := PositionRequest{
SatelliteID: "iss",
Observer: Geodetic{
Latitude: 51.5074,
Longitude: -0.1278,
Altitude: 0.02,
},
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/orbit-viewer/position", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
var resp map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}
if _, ok := resp["azimuth"]; !ok {
t.Errorf("Missing azimuth in position response")
}
if _, ok := resp["elevation"]; !ok {
t.Errorf("Missing elevation in position response")
}
if _, ok := resp["range_km"]; !ok {
t.Errorf("Missing range_km in position response")
}
}
func TestAPICustomTLE(t *testing.T) {
r := setupTestRouter()
payload := TLEInput{
Name: "TEST-CSS",
Line1: "1 48274U 21035A 24080.45678901 .00021456 00000-0 31456-3 0 9991",
Line2: "2 48274 41.4723 112.3456 0005123 85.2341 274.9812 15.61234567167890",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "/api/orbit-viewer/custom", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("Expected status 200, got %d: %s", w.Code, w.Body.String())
}
var resp struct {
Satellite *SatelliteCatalogEntry `json:"satellite"`
}
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("Failed to parse JSON: %v", err)
}
if resp.Satellite.NoradID != 48274 {
t.Errorf("Expected NORAD ID 48274, got %d", resp.Satellite.NoradID)
}
}