Initial implementation of SkyView - ADS-B aircraft tracker
- Go application with embedded static files for dump1090 frontend - TCP client for SBS-1/BaseStation format (port 30003) - Real-time WebSocket updates with aircraft tracking - Modern web frontend with Leaflet maps and mobile-responsive design - Aircraft table with filtering/sorting and statistics dashboard - Origin configuration for receiver location and distance calculations - Automatic config.json loading from current directory - Foreground execution by default with optional -daemon flag 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
commit
8ce4f4c397
19 changed files with 1971 additions and 0 deletions
209
internal/client/dump1090.go
Normal file
209
internal/client/dump1090.go
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"skyview/internal/config"
|
||||
"skyview/internal/parser"
|
||||
)
|
||||
|
||||
type Dump1090Client struct {
|
||||
config *config.Config
|
||||
aircraftMap map[string]*parser.Aircraft
|
||||
mutex sync.RWMutex
|
||||
subscribers []chan parser.AircraftData
|
||||
subMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func NewDump1090Client(cfg *config.Config) *Dump1090Client {
|
||||
return &Dump1090Client{
|
||||
config: cfg,
|
||||
aircraftMap: make(map[string]*parser.Aircraft),
|
||||
subscribers: make([]chan parser.AircraftData, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) Start(ctx context.Context) error {
|
||||
go c.startDataStream(ctx)
|
||||
go c.startPeriodicBroadcast(ctx)
|
||||
go c.startCleanup(ctx)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) startDataStream(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
if err := c.connectAndRead(ctx); err != nil {
|
||||
log.Printf("Connection error: %v, retrying in 5s", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) connectAndRead(ctx context.Context) error {
|
||||
address := fmt.Sprintf("%s:%d", c.config.Dump1090.Host, c.config.Dump1090.DataPort)
|
||||
|
||||
conn, err := net.Dial("tcp", address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to %s: %w", address, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
log.Printf("Connected to dump1090 at %s", address)
|
||||
|
||||
scanner := bufio.NewScanner(conn)
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
default:
|
||||
line := scanner.Text()
|
||||
c.processLine(line)
|
||||
}
|
||||
}
|
||||
|
||||
return scanner.Err()
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) processLine(line string) {
|
||||
aircraft, err := parser.ParseSBS1Line(line)
|
||||
if err != nil || aircraft == nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.mutex.Lock()
|
||||
if existing, exists := c.aircraftMap[aircraft.Hex]; exists {
|
||||
c.updateExistingAircraft(existing, aircraft)
|
||||
} else {
|
||||
c.aircraftMap[aircraft.Hex] = aircraft
|
||||
}
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) updateExistingAircraft(existing, update *parser.Aircraft) {
|
||||
existing.LastSeen = update.LastSeen
|
||||
existing.Messages++
|
||||
|
||||
if update.Flight != "" {
|
||||
existing.Flight = update.Flight
|
||||
}
|
||||
if update.Altitude != 0 {
|
||||
existing.Altitude = update.Altitude
|
||||
}
|
||||
if update.GroundSpeed != 0 {
|
||||
existing.GroundSpeed = update.GroundSpeed
|
||||
}
|
||||
if update.Track != 0 {
|
||||
existing.Track = update.Track
|
||||
}
|
||||
if update.Latitude != 0 {
|
||||
existing.Latitude = update.Latitude
|
||||
}
|
||||
if update.Longitude != 0 {
|
||||
existing.Longitude = update.Longitude
|
||||
}
|
||||
if update.VertRate != 0 {
|
||||
existing.VertRate = update.VertRate
|
||||
}
|
||||
if update.Squawk != "" {
|
||||
existing.Squawk = update.Squawk
|
||||
}
|
||||
existing.OnGround = update.OnGround
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) GetAircraftData() parser.AircraftData {
|
||||
c.mutex.RLock()
|
||||
defer c.mutex.RUnlock()
|
||||
|
||||
aircraftMap := make(map[string]parser.Aircraft)
|
||||
totalMessages := 0
|
||||
|
||||
for hex, aircraft := range c.aircraftMap {
|
||||
aircraftMap[hex] = *aircraft
|
||||
totalMessages += aircraft.Messages
|
||||
}
|
||||
|
||||
return parser.AircraftData{
|
||||
Now: time.Now().Unix(),
|
||||
Messages: totalMessages,
|
||||
Aircraft: aircraftMap,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) Subscribe() <-chan parser.AircraftData {
|
||||
c.subMutex.Lock()
|
||||
defer c.subMutex.Unlock()
|
||||
|
||||
ch := make(chan parser.AircraftData, 10)
|
||||
c.subscribers = append(c.subscribers, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) startPeriodicBroadcast(ctx context.Context) {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
data := c.GetAircraftData()
|
||||
c.broadcastToSubscribers(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) broadcastToSubscribers(data parser.AircraftData) {
|
||||
c.subMutex.RLock()
|
||||
defer c.subMutex.RUnlock()
|
||||
|
||||
for i, ch := range c.subscribers {
|
||||
select {
|
||||
case ch <- data:
|
||||
default:
|
||||
close(ch)
|
||||
c.subMutex.RUnlock()
|
||||
c.subMutex.Lock()
|
||||
c.subscribers = append(c.subscribers[:i], c.subscribers[i+1:]...)
|
||||
c.subMutex.Unlock()
|
||||
c.subMutex.RLock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) startCleanup(ctx context.Context) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
c.cleanupStaleAircraft()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Dump1090Client) cleanupStaleAircraft() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
cutoff := time.Now().Add(-2 * time.Minute)
|
||||
for hex, aircraft := range c.aircraftMap {
|
||||
if aircraft.LastSeen.Before(cutoff) {
|
||||
delete(c.aircraftMap, hex)
|
||||
}
|
||||
}
|
||||
}
|
||||
118
internal/config/config.go
Normal file
118
internal/config/config.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig `json:"server"`
|
||||
Dump1090 Dump1090Config `json:"dump1090"`
|
||||
Origin OriginConfig `json:"origin"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Address string `json:"address"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
type Dump1090Config struct {
|
||||
Host string `json:"host"`
|
||||
DataPort int `json:"data_port"`
|
||||
}
|
||||
|
||||
type OriginConfig struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func Load() (*Config, error) {
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{
|
||||
Address: ":8080",
|
||||
Port: 8080,
|
||||
},
|
||||
Dump1090: Dump1090Config{
|
||||
Host: "localhost",
|
||||
DataPort: 30003,
|
||||
},
|
||||
Origin: OriginConfig{
|
||||
Latitude: 37.7749,
|
||||
Longitude: -122.4194,
|
||||
Name: "Default Location",
|
||||
},
|
||||
}
|
||||
|
||||
configFile := os.Getenv("SKYVIEW_CONFIG")
|
||||
if configFile == "" {
|
||||
// Check for config files in common locations
|
||||
candidates := []string{"config.json", "./config.json", "skyview.json"}
|
||||
for _, candidate := range candidates {
|
||||
if _, err := os.Stat(candidate); err == nil {
|
||||
configFile = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if configFile != "" {
|
||||
if err := loadFromFile(cfg, configFile); err != nil {
|
||||
return nil, fmt.Errorf("failed to load config file %s: %w", configFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
loadFromEnv(cfg)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func loadFromFile(cfg *Config, filename string) error {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, cfg)
|
||||
}
|
||||
|
||||
func loadFromEnv(cfg *Config) {
|
||||
if addr := os.Getenv("SKYVIEW_ADDRESS"); addr != "" {
|
||||
cfg.Server.Address = addr
|
||||
}
|
||||
|
||||
if portStr := os.Getenv("SKYVIEW_PORT"); portStr != "" {
|
||||
if port, err := strconv.Atoi(portStr); err == nil {
|
||||
cfg.Server.Port = port
|
||||
cfg.Server.Address = fmt.Sprintf(":%d", port)
|
||||
}
|
||||
}
|
||||
|
||||
if host := os.Getenv("DUMP1090_HOST"); host != "" {
|
||||
cfg.Dump1090.Host = host
|
||||
}
|
||||
|
||||
if dataPortStr := os.Getenv("DUMP1090_DATA_PORT"); dataPortStr != "" {
|
||||
if port, err := strconv.Atoi(dataPortStr); err == nil {
|
||||
cfg.Dump1090.DataPort = port
|
||||
}
|
||||
}
|
||||
|
||||
if latStr := os.Getenv("ORIGIN_LATITUDE"); latStr != "" {
|
||||
if lat, err := strconv.ParseFloat(latStr, 64); err == nil {
|
||||
cfg.Origin.Latitude = lat
|
||||
}
|
||||
}
|
||||
|
||||
if lonStr := os.Getenv("ORIGIN_LONGITUDE"); lonStr != "" {
|
||||
if lon, err := strconv.ParseFloat(lonStr, 64); err == nil {
|
||||
cfg.Origin.Longitude = lon
|
||||
}
|
||||
}
|
||||
|
||||
if name := os.Getenv("ORIGIN_NAME"); name != "" {
|
||||
cfg.Origin.Name = name
|
||||
}
|
||||
}
|
||||
95
internal/parser/sbs1.go
Normal file
95
internal/parser/sbs1.go
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Aircraft struct {
|
||||
Hex string `json:"hex"`
|
||||
Flight string `json:"flight,omitempty"`
|
||||
Altitude int `json:"alt_baro,omitempty"`
|
||||
GroundSpeed int `json:"gs,omitempty"`
|
||||
Track int `json:"track,omitempty"`
|
||||
Latitude float64 `json:"lat,omitempty"`
|
||||
Longitude float64 `json:"lon,omitempty"`
|
||||
VertRate int `json:"vert_rate,omitempty"`
|
||||
Squawk string `json:"squawk,omitempty"`
|
||||
Emergency bool `json:"emergency,omitempty"`
|
||||
OnGround bool `json:"on_ground,omitempty"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
Messages int `json:"messages"`
|
||||
}
|
||||
|
||||
type AircraftData struct {
|
||||
Now int64 `json:"now"`
|
||||
Messages int `json:"messages"`
|
||||
Aircraft map[string]Aircraft `json:"aircraft"`
|
||||
}
|
||||
|
||||
func ParseSBS1Line(line string) (*Aircraft, error) {
|
||||
parts := strings.Split(strings.TrimSpace(line), ",")
|
||||
if len(parts) < 22 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
messageType := parts[1]
|
||||
if messageType != "1" && messageType != "3" && messageType != "4" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
aircraft := &Aircraft{
|
||||
Hex: strings.TrimSpace(parts[4]),
|
||||
LastSeen: time.Now(),
|
||||
Messages: 1,
|
||||
}
|
||||
|
||||
if parts[10] != "" {
|
||||
aircraft.Flight = strings.TrimSpace(parts[10])
|
||||
}
|
||||
|
||||
if parts[11] != "" {
|
||||
if alt, err := strconv.Atoi(parts[11]); err == nil {
|
||||
aircraft.Altitude = alt
|
||||
}
|
||||
}
|
||||
|
||||
if parts[12] != "" {
|
||||
if gs, err := strconv.Atoi(parts[12]); err == nil {
|
||||
aircraft.GroundSpeed = gs
|
||||
}
|
||||
}
|
||||
|
||||
if parts[13] != "" {
|
||||
if track, err := strconv.Atoi(parts[13]); err == nil {
|
||||
aircraft.Track = track
|
||||
}
|
||||
}
|
||||
|
||||
if parts[14] != "" && parts[15] != "" {
|
||||
if lat, err := strconv.ParseFloat(parts[14], 64); err == nil {
|
||||
aircraft.Latitude = lat
|
||||
}
|
||||
if lon, err := strconv.ParseFloat(parts[15], 64); err == nil {
|
||||
aircraft.Longitude = lon
|
||||
}
|
||||
}
|
||||
|
||||
if parts[16] != "" {
|
||||
if vr, err := strconv.Atoi(parts[16]); err == nil {
|
||||
aircraft.VertRate = vr
|
||||
}
|
||||
}
|
||||
|
||||
if parts[17] != "" {
|
||||
aircraft.Squawk = strings.TrimSpace(parts[17])
|
||||
}
|
||||
|
||||
if parts[21] != "" {
|
||||
aircraft.OnGround = parts[21] == "1"
|
||||
}
|
||||
|
||||
return aircraft, nil
|
||||
}
|
||||
|
||||
210
internal/server/server.go
Normal file
210
internal/server/server.go
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"skyview/internal/client"
|
||||
"skyview/internal/config"
|
||||
"skyview/internal/parser"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
config *config.Config
|
||||
staticFiles embed.FS
|
||||
upgrader websocket.Upgrader
|
||||
wsClients map[*websocket.Conn]bool
|
||||
wsClientsMux sync.RWMutex
|
||||
dump1090 *client.Dump1090Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
type WebSocketMessage struct {
|
||||
Type string `json:"type"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func New(cfg *config.Config, staticFiles embed.FS, ctx context.Context) http.Handler {
|
||||
s := &Server{
|
||||
config: cfg,
|
||||
staticFiles: staticFiles,
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
wsClients: make(map[*websocket.Conn]bool),
|
||||
dump1090: client.NewDump1090Client(cfg),
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
if err := s.dump1090.Start(ctx); err != nil {
|
||||
log.Printf("Failed to start dump1090 client: %v", err)
|
||||
}
|
||||
|
||||
go s.subscribeToAircraftUpdates()
|
||||
|
||||
router := mux.NewRouter()
|
||||
|
||||
router.HandleFunc("/", s.serveIndex).Methods("GET")
|
||||
router.HandleFunc("/ws", s.handleWebSocket).Methods("GET")
|
||||
|
||||
apiRouter := router.PathPrefix("/api").Subrouter()
|
||||
apiRouter.HandleFunc("/aircraft", s.getAircraft).Methods("GET")
|
||||
apiRouter.HandleFunc("/stats", s.getStats).Methods("GET")
|
||||
apiRouter.HandleFunc("/config", s.getConfig).Methods("GET")
|
||||
|
||||
router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(s.staticFiles))))
|
||||
|
||||
return s.enableCORS(router)
|
||||
}
|
||||
|
||||
func (s *Server) serveIndex(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := s.staticFiles.ReadFile("static/index.html")
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read index.html", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Server) getAircraft(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.dump1090.GetAircraftData()
|
||||
|
||||
response := map[string]interface{}{
|
||||
"now": data.Now,
|
||||
"messages": data.Messages,
|
||||
"aircraft": s.aircraftMapToSlice(data.Aircraft),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
func (s *Server) getStats(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.dump1090.GetAircraftData()
|
||||
|
||||
stats := map[string]interface{}{
|
||||
"total": map[string]interface{}{
|
||||
"aircraft": len(data.Aircraft),
|
||||
"messages": map[string]interface{}{
|
||||
"total": data.Messages,
|
||||
"last1min": data.Messages,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(stats)
|
||||
}
|
||||
|
||||
func (s *Server) getConfig(w http.ResponseWriter, r *http.Request) {
|
||||
configData := map[string]interface{}{
|
||||
"origin": map[string]interface{}{
|
||||
"latitude": s.config.Origin.Latitude,
|
||||
"longitude": s.config.Origin.Longitude,
|
||||
"name": s.config.Origin.Name,
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(configData)
|
||||
}
|
||||
|
||||
func (s *Server) aircraftMapToSlice(aircraftMap map[string]parser.Aircraft) []parser.Aircraft {
|
||||
aircraft := make([]parser.Aircraft, 0, len(aircraftMap))
|
||||
for _, a := range aircraftMap {
|
||||
aircraft = append(aircraft, a)
|
||||
}
|
||||
return aircraft
|
||||
}
|
||||
|
||||
func (s *Server) subscribeToAircraftUpdates() {
|
||||
updates := s.dump1090.Subscribe()
|
||||
|
||||
for data := range updates {
|
||||
message := WebSocketMessage{
|
||||
Type: "aircraft_update",
|
||||
Data: map[string]interface{}{
|
||||
"now": data.Now,
|
||||
"messages": data.Messages,
|
||||
"aircraft": s.aircraftMapToSlice(data.Aircraft),
|
||||
},
|
||||
}
|
||||
|
||||
s.broadcastToWebSocketClients(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("WebSocket upgrade error: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
s.wsClientsMux.Lock()
|
||||
s.wsClients[conn] = true
|
||||
s.wsClientsMux.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.wsClientsMux.Lock()
|
||||
delete(s.wsClients, conn)
|
||||
s.wsClientsMux.Unlock()
|
||||
}()
|
||||
|
||||
data := s.dump1090.GetAircraftData()
|
||||
initialMessage := WebSocketMessage{
|
||||
Type: "aircraft_update",
|
||||
Data: map[string]interface{}{
|
||||
"now": data.Now,
|
||||
"messages": data.Messages,
|
||||
"aircraft": s.aircraftMapToSlice(data.Aircraft),
|
||||
},
|
||||
}
|
||||
conn.WriteJSON(initialMessage)
|
||||
|
||||
for {
|
||||
_, _, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) broadcastToWebSocketClients(message WebSocketMessage) {
|
||||
s.wsClientsMux.RLock()
|
||||
defer s.wsClientsMux.RUnlock()
|
||||
|
||||
for client := range s.wsClients {
|
||||
if err := client.WriteJSON(message); err != nil {
|
||||
client.Close()
|
||||
delete(s.wsClients, client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) enableCORS(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
55
internal/server/server_test.go
Normal file
55
internal/server/server_test.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"skyview/internal/config"
|
||||
)
|
||||
|
||||
//go:embed testdata/*
|
||||
var testStaticFiles embed.FS
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Address: ":8080",
|
||||
Port: 8080,
|
||||
},
|
||||
Dump1090: config.Dump1090Config{
|
||||
Host: "localhost",
|
||||
Port: 8080,
|
||||
URL: "http://localhost:8080",
|
||||
},
|
||||
}
|
||||
|
||||
handler := New(cfg, testStaticFiles)
|
||||
if handler == nil {
|
||||
t.Fatal("Expected handler to be created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCORSHeaders(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Dump1090: config.Dump1090Config{
|
||||
URL: "http://localhost:8080",
|
||||
},
|
||||
}
|
||||
|
||||
handler := New(cfg, testStaticFiles)
|
||||
|
||||
req := httptest.NewRequest("OPTIONS", "/api/aircraft", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Header().Get("Access-Control-Allow-Origin") != "*" {
|
||||
t.Errorf("Expected CORS header, got %s", w.Header().Get("Access-Control-Allow-Origin"))
|
||||
}
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("Expected status 200, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
5
internal/server/testdata/test.html
vendored
Normal file
5
internal/server/testdata/test.html
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Test</title></head>
|
||||
<body><h1>Test</h1></body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue