Files
openfsd/fsd/metar_test.go
2025-05-12 17:21:16 -07:00

289 lines
8.2 KiB
Go

package fsd
import (
"bytes"
"context"
"errors"
"io"
"net/http"
"strings"
"testing"
)
// mockClient simulates a Client for capturing sent packets.
type mockClient struct {
*Client
sentPackets []string
}
// newMockClient creates a mockClient with a valid sendChan and ctx.
func newMockClient(callsign string) *mockClient {
ctx, cancel := context.WithCancel(context.Background())
client := &Client{
ctx: ctx,
cancelCtx: cancel,
sendChan: make(chan string, 32), // Buffered to prevent blocking
loginData: loginData{callsign: callsign},
}
return &mockClient{
Client: client,
sentPackets: []string{},
}
}
// send overrides Client's send method to capture packets.
func (c *mockClient) send(packet string) error {
c.sentPackets = append(c.sentPackets, packet)
return nil
}
// collectPackets drains the sendChan and returns all sent packets.
func (c *mockClient) collectPackets() []string {
packets := append([]string{}, c.sentPackets...)
for {
select {
case packet := <-c.sendChan:
packets = append(packets, packet)
default:
return packets
}
}
}
// mockTransport simulates HTTP responses for testing handleMetarRequest.
type mockTransport struct {
response *http.Response
err error
}
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return t.response, t.err
}
// TestBuildMetarRequestURL verifies that buildMetarRequestURL correctly formats URLs for given ICAO codes.
func TestBuildMetarRequestURL(t *testing.T) {
tests := []struct {
name string
icaoCode string
expected string
}{
{
name: "Valid ICAO KJFK",
icaoCode: "KJFK",
expected: "https://tgftp.nws.noaa.gov/data/observations/metar/stations/KJFK.TXT",
},
{
name: "Valid ICAO EGLL",
icaoCode: "EGLL",
expected: "https://tgftp.nws.noaa.gov/data/observations/metar/stations/EGLL.TXT",
},
{
name: "Empty ICAO",
icaoCode: "",
expected: "https://tgftp.nws.noaa.gov/data/observations/metar/stations/.TXT",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := buildMetarRequestURL(tt.icaoCode)
if got != tt.expected {
t.Errorf("buildMetarRequestURL(%q) = %q, want %q", tt.icaoCode, got, tt.expected)
}
})
}
}
// TestBuildMetarResponsePacket verifies that buildMetarResponsePacket correctly formats METAR response packets.
func TestBuildMetarResponsePacket(t *testing.T) {
tests := []struct {
name string
callsign string
metar []byte
expected string
}{
{
name: "Valid METAR for KJFK",
callsign: "TEST",
metar: []byte("KJFK 301951Z 18010KT 10SM FEW250 29/19 A2992"),
expected: "$ARSERVER:TEST:KJFK 301951Z 18010KT 10SM FEW250 29/19 A2992\r\n",
},
{
name: "Valid METAR for EGLL",
callsign: "PILOT1",
metar: []byte("EGLL 301950Z 24008KT 9999 FEW040 18/12 Q1015"),
expected: "$ARSERVER:PILOT1:EGLL 301950Z 24008KT 9999 FEW040 18/12 Q1015\r\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := buildMetarResponsePacket(tt.callsign, tt.metar)
if got != tt.expected {
t.Errorf("buildMetarResponsePacket(%q, %q) = %q, want %q", tt.callsign, tt.metar, got, tt.expected)
}
})
}
}
// TestSendMetarServiceError verifies that sendMetarServiceError sends the correct error packet to the client.
func TestSendMetarServiceError(t *testing.T) {
mockClient := newMockClient("TEST")
req := &metarRequest{
client: mockClient.Client,
icaoCode: "KJFK",
}
sendMetarServiceError(req)
packets := mockClient.collectPackets()
expectedPacket := "$ERserver:unknown:9::Error fetching METAR for KJFK\r\n"
if len(packets) != 1 {
t.Errorf("expected 1 packet sent, got %d", len(packets))
} else if packets[0] != expectedPacket {
t.Errorf("expected packet %q, got %q", expectedPacket, packets[0])
}
}
// TestHandleMetarRequest_Success verifies that handleMetarRequest correctly processes a valid METAR response.
func TestHandleMetarRequest_Success(t *testing.T) {
responseBody := []byte("2023/04/30 19:51\nKJFK 301951Z 18010KT 10SM FEW250 29/19 A2992\n")
mockResponse := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(responseBody)),
}
mockTransport := &mockTransport{response: mockResponse}
service := &metarService{
httpClient: &http.Client{Transport: mockTransport},
}
mockClient := newMockClient("TEST")
req := &metarRequest{
client: mockClient.Client,
icaoCode: "KJFK",
}
service.handleMetarRequest(req)
packets := mockClient.collectPackets()
if len(packets) != 1 {
t.Errorf("expected 1 packet sent, got %d", len(packets))
}
if !strings.HasPrefix(packets[0], "$ARSERVER:TEST:KJFK ") || !strings.HasSuffix(packets[0], "\r\n") {
t.Errorf("bad response packet")
}
}
// TestHandleMetarRequest_HTTPError verifies that handleMetarRequest handles HTTP errors correctly.
func TestHandleMetarRequest_HTTPError(t *testing.T) {
mockResponse := &http.Response{
StatusCode: http.StatusNotFound,
Body: io.NopCloser(strings.NewReader("Not Found")),
}
mockTransport := &mockTransport{response: mockResponse}
service := &metarService{
httpClient: &http.Client{Transport: mockTransport},
}
mockClient := newMockClient("TEST")
req := &metarRequest{
client: mockClient.Client,
icaoCode: "INVALID",
}
service.handleMetarRequest(req)
packets := mockClient.collectPackets()
expectedPacket := "$ERserver:unknown:9::Error fetching METAR for INVALID\r\n"
if len(packets) != 1 {
t.Errorf("expected 1 packet sent, got %d", len(packets))
} else if packets[0] != expectedPacket {
t.Errorf("expected packet %q, got %q", expectedPacket, packets[0])
}
}
// TestHandleMetarRequest_NetworkError verifies that handleMetarRequest handles network errors correctly.
func TestHandleMetarRequest_NetworkError(t *testing.T) {
mockTransport := &mockTransport{err: errors.New("network error")}
service := &metarService{
httpClient: &http.Client{Transport: mockTransport},
}
mockClient := newMockClient("TEST")
req := &metarRequest{
client: mockClient.Client,
icaoCode: "KJFK",
}
service.handleMetarRequest(req)
packets := mockClient.collectPackets()
expectedPacket := "$ERserver:unknown:9::Error fetching METAR for KJFK\r\n"
if len(packets) != 1 {
t.Errorf("expected 1 packet sent, got %d", len(packets))
} else if packets[0] != expectedPacket {
t.Errorf("expected packet %q, got %q", expectedPacket, packets[0])
}
}
// TestHandleMetarRequest_InvalidResponse verifies that handleMetarRequest handles responses with invalid formats.
func TestHandleMetarRequest_InvalidResponse(t *testing.T) {
responseBody := []byte("Invalid response\n")
mockResponse := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(responseBody)),
}
mockTransport := &mockTransport{response: mockResponse}
service := &metarService{
httpClient: &http.Client{Transport: mockTransport},
}
mockClient := newMockClient("TEST")
req := &metarRequest{
client: mockClient.Client,
icaoCode: "KJFK",
}
service.handleMetarRequest(req)
packets := mockClient.collectPackets()
expectedPacket := "$ERserver:unknown:9::Error fetching METAR for KJFK\r\n"
if len(packets) != 1 {
t.Errorf("expected 1 packet sent, got %d", len(packets))
} else if packets[0] != expectedPacket {
t.Errorf("expected packet %q, got %q", expectedPacket, packets[0])
}
}
// TestHandleMetarRequest_MoreThanTwoLines verifies that handleMetarRequest handles responses with too many lines.
func TestHandleMetarRequest_MoreThanTwoLines(t *testing.T) {
responseBody := []byte("Line1\nLine2\nLine3\n")
mockResponse := &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(responseBody)),
}
mockTransport := &mockTransport{response: mockResponse}
service := &metarService{
httpClient: &http.Client{Transport: mockTransport},
}
mockClient := newMockClient("TEST")
req := &metarRequest{
client: mockClient.Client,
icaoCode: "KJFK",
}
service.handleMetarRequest(req)
packets := mockClient.collectPackets()
expectedPacket := "$ERserver:unknown:9::Error fetching METAR for KJFK\r\n"
if len(packets) != 1 {
t.Errorf("expected 1 packet sent, got %d", len(packets))
} else if packets[0] != expectedPacket {
t.Errorf("expected packet %q, got %q", expectedPacket, packets[0])
}
}