mirror of
https://github.com/renorris/openfsd
synced 2026-08-10 11:33:29 +08:00
datafeed: export pilot rating and flight plan via online_users; stop fabricating QNH
Extend serviceapi pilot/ATC DTOs; store PilotRating on LoginData at auth; populate online_users from session snapshot. Datafeed drops outer PilotRating/TextATIS, maps info-section to FlightPlan object, zeros military/QNH, and uses ConfigFsdServerIdent for server (fallback OPENFSD).
This commit is contained in:
@@ -191,10 +191,14 @@ func TestOnlineUsersSyntheticBadge(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
human := newSess("HUMAN1", false, NetworkRatingObserver)
|
||||
human.PilotRating = int(protocol.PilotRatingPPL)
|
||||
human.FlightPlan.Store("I:B738:450:KLAX:1200:0000:FL350:KJFK:2:0:3:0::rmk:DCT")
|
||||
human.AssignedBeaconCode.Store("4321")
|
||||
require.NoError(t, reg.Register(human))
|
||||
|
||||
synth := newSess("SBX1", false, NetworkRatingObserver)
|
||||
synth.Synthetic = true
|
||||
synth.PilotRating = 0
|
||||
require.NoError(t, reg.Register(synth))
|
||||
|
||||
e := srv.setupRoutes()
|
||||
@@ -214,9 +218,13 @@ func TestOnlineUsersSyntheticBadge(t *testing.T) {
|
||||
case "HUMAN1":
|
||||
sawHuman = true
|
||||
require.False(t, p.Synthetic, "human pilot must not be synthetic")
|
||||
require.Equal(t, int(protocol.PilotRatingPPL), p.PilotRating)
|
||||
require.Contains(t, p.FlightPlan, "KLAX")
|
||||
require.Equal(t, "4321", p.AssignedBeaconCode)
|
||||
case "SBX1":
|
||||
sawSynth = true
|
||||
require.True(t, p.Synthetic, "sweatbox pilot must be synthetic")
|
||||
require.Equal(t, 0, p.PilotRating)
|
||||
}
|
||||
}
|
||||
require.True(t, sawHuman && sawSynth, "expected both pilots in snapshot")
|
||||
@@ -224,6 +232,8 @@ func TestOnlineUsersSyntheticBadge(t *testing.T) {
|
||||
require.Contains(t, body, `"synthetic":true`)
|
||||
// Human pilots must omit synthetic when false (omitempty).
|
||||
require.NotContains(t, body, `"synthetic":false`)
|
||||
// pilot_rating always present as a number.
|
||||
require.Contains(t, body, `"pilot_rating"`)
|
||||
}
|
||||
|
||||
func TestRunServiceHTTPAndListen(t *testing.T) {
|
||||
|
||||
@@ -119,6 +119,7 @@ func (s *Server) attemptAuthentication(client *session.Session, token string) (e
|
||||
if err = s.enforcePilotPPLRequirement(client, user); err != nil {
|
||||
return
|
||||
}
|
||||
setSessionPilotRating(client, user)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -157,10 +158,24 @@ func (s *Server) attemptAuthentication(client *session.Session, token string) (e
|
||||
if err = s.enforcePilotPPLRequirement(client, user); err != nil {
|
||||
return
|
||||
}
|
||||
setSessionPilotRating(client, user)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// setSessionPilotRating stores the certificate pilot rating on the session.
|
||||
// Invalid / unknown ratings become 0 (P0).
|
||||
func setSessionPilotRating(client *session.Session, user *db.User) {
|
||||
if client == nil || user == nil {
|
||||
return
|
||||
}
|
||||
if protocol.IsValidPilotRating(user.PilotRating) {
|
||||
client.PilotRating = user.PilotRating
|
||||
return
|
||||
}
|
||||
client.PilotRating = 0
|
||||
}
|
||||
|
||||
// enforcePilotPPLRequirement rejects pilot (#AP) logins when REQUIRE_PILOT_PPL is
|
||||
// enabled and the certificate's pilot_rating is below PPL. ATC is unaffected.
|
||||
func (s *Server) enforcePilotPPLRequirement(client *session.Session, user *db.User) error {
|
||||
|
||||
@@ -145,6 +145,7 @@ func (s *Server) handleGetOnlineUsers(c *gin.Context) {
|
||||
Frequency: client.Frequency.Load(),
|
||||
Facility: int(client.FacilityType.Load()),
|
||||
VisRange: int(client.VisRange.Load() * 0.000539957), // Convert meters to nautical miles
|
||||
// TextATIS empty: openfsd does not persist NEWINFO by default.
|
||||
}
|
||||
resData.ATC = append(resData.ATC, atc)
|
||||
} else {
|
||||
@@ -155,6 +156,9 @@ func (s *Server) handleGetOnlineUsers(c *gin.Context) {
|
||||
Heading: int(client.Heading.Load()),
|
||||
Transponder: client.Transponder.Load(),
|
||||
Synthetic: client.Synthetic,
|
||||
PilotRating: client.PilotRating,
|
||||
FlightPlan: client.FlightPlan.Load(),
|
||||
AssignedBeaconCode: client.AssignedBeaconCode.Load(),
|
||||
}
|
||||
resData.Pilots = append(resData.Pilots, pilot)
|
||||
}
|
||||
|
||||
@@ -448,7 +448,11 @@ func TestAttemptAuth_SuccessPassword(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f := &fakeUserStore{byCID: map[int]*db.User{
|
||||
5: {CID: 5, Password: string(hash), NetworkRating: int(protocol.NetworkRatingController1)},
|
||||
5: {
|
||||
CID: 5, Password: string(hash),
|
||||
NetworkRating: int(protocol.NetworkRatingController1),
|
||||
PilotRating: int(protocol.PilotRatingATPL),
|
||||
},
|
||||
}}
|
||||
srv := newAuthTestServer(t, f, nil)
|
||||
client := session.New(context.Background(), &discardConn{}, nil, session.LoginData{
|
||||
@@ -463,6 +467,9 @@ func TestAttemptAuth_SuccessPassword(t *testing.T) {
|
||||
if client.MaxNetworkRating != protocol.NetworkRatingController1 {
|
||||
t.Fatalf("MaxNetworkRating=%v", client.MaxNetworkRating)
|
||||
}
|
||||
if client.PilotRating != int(protocol.PilotRatingATPL) {
|
||||
t.Fatalf("PilotRating=%d want ATPL(%d)", client.PilotRating, protocol.PilotRatingATPL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttemptAuth_JWT(t *testing.T) {
|
||||
|
||||
@@ -474,6 +474,7 @@ func (h *SweatboxHost) buildSession(ac sweatbox.AircraftSnapshot) *session.Sessi
|
||||
RealName: "SWEATBOX",
|
||||
NetworkRating: protocol.NetworkRatingObserver,
|
||||
MaxNetworkRating: protocol.NetworkRatingObserver,
|
||||
PilotRating: 0, // synthetic: no DB certificate (P0)
|
||||
ProtoRevision: 100,
|
||||
LoginTime: now,
|
||||
IsAtc: false,
|
||||
|
||||
@@ -30,6 +30,18 @@ type OnlineUserPilot struct {
|
||||
// Synthetic is true for in-process sweatbox pilots (no TCP client).
|
||||
// Omitted from JSON when false so human pilots stay compact.
|
||||
Synthetic bool `json:"synthetic,omitempty"`
|
||||
|
||||
// PilotRating is the VATSIM pilot rating wire ID from the certificate
|
||||
// at login (0,1,3,7,15,31,63). 0 if unknown (e.g. synthetic without DB).
|
||||
// Always present as a number (0 is valid P0).
|
||||
PilotRating int `json:"pilot_rating"`
|
||||
|
||||
// FlightPlan is the session info-section string (no $FP source/dest),
|
||||
// empty if none filed. Same layout as session.FlightPlan / encodeFlightPlanInfo.
|
||||
FlightPlan string `json:"flight_plan,omitempty"`
|
||||
|
||||
// AssignedBeaconCode is the ATC-assigned squawk if set; may be empty.
|
||||
AssignedBeaconCode string `json:"assigned_beacon_code,omitempty"`
|
||||
}
|
||||
|
||||
// OnlineUserATC is an ATC entry in the online-users snapshot.
|
||||
@@ -38,6 +50,10 @@ type OnlineUserATC struct {
|
||||
Frequency string `json:"frequency"`
|
||||
Facility int `json:"facility"`
|
||||
VisRange int `json:"visual_range"`
|
||||
|
||||
// TextATIS is multi-line controller ATIS when the server stores it.
|
||||
// Empty when not available (openfsd does not persist NEWINFO by default).
|
||||
TextATIS []string `json:"text_atis,omitempty"`
|
||||
}
|
||||
|
||||
// OnlineUsersResponseData is the JSON body for GET /online_users.
|
||||
|
||||
@@ -37,10 +37,13 @@ type LoginData struct {
|
||||
RealName string // Real name
|
||||
NetworkRating protocol.NetworkRating // Network rating of the client
|
||||
MaxNetworkRating protocol.NetworkRating // Maximum allowed network rating (from DB/JWT)
|
||||
ProtoRevision int // Protocol revision
|
||||
LoginTime time.Time // Time of login
|
||||
ClientID uint16 // Client ID (from ident packet)
|
||||
IsAtc bool // True if the client is ATC, false if a pilot
|
||||
// PilotRating is the certificate pilot rating wire ID set at auth
|
||||
// (0,1,3,7,15,31,63). Immutable after login; 0 for synthetic/unknown.
|
||||
PilotRating int
|
||||
ProtoRevision int // Protocol revision
|
||||
LoginTime time.Time // Time of login
|
||||
ClientID uint16 // Client ID (from ident packet)
|
||||
IsAtc bool // True if the client is ATC, false if a pilot
|
||||
}
|
||||
|
||||
// LatLon is a geographic coordinate pair (API convenience; storage is non-boxing atomics).
|
||||
@@ -71,7 +74,7 @@ type LatLon struct {
|
||||
//
|
||||
// Immutable after login (set during login; safe to read concurrently afterward):
|
||||
// - LoginData fields (Callsign, CID, RealName, NetworkRating, ProtoRevision, …)
|
||||
// - MaxNetworkRating is fixed once authentication completes
|
||||
// - MaxNetworkRating / PilotRating fixed once authentication completes
|
||||
// - Synthetic — set before Register for in-process (sweatbox) participants;
|
||||
// never set on the TCP login path; concurrent readers OK
|
||||
//
|
||||
|
||||
@@ -8,12 +8,7 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/renorris/openfsd/internal/auth"
|
||||
"github.com/renorris/openfsd/internal/db"
|
||||
"github.com/renorris/openfsd/internal/serviceapi"
|
||||
"github.com/renorris/openfsd/pkg/protocol"
|
||||
"go.uber.org/atomic"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@@ -21,6 +16,13 @@ import (
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/renorris/openfsd/internal/auth"
|
||||
"github.com/renorris/openfsd/internal/db"
|
||||
"github.com/renorris/openfsd/internal/serviceapi"
|
||||
"github.com/renorris/openfsd/pkg/protocol"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
//go:embed data_templates/status.txt
|
||||
@@ -284,14 +286,19 @@ type DatafeedGeneral struct {
|
||||
UniqueUsers int `json:"unique_users"`
|
||||
}
|
||||
|
||||
// DatafeedPilot embeds OnlineUserPilot so pilot_rating / assigned_beacon_code /
|
||||
// position fields come from service-HTTP. FlightPlan intentionally shadows the
|
||||
// embed: service-HTTP carries a string info section; the public datafeed wants
|
||||
// a VATSIM-shaped object (or omit when nil). Do NOT redeclare PilotRating —
|
||||
// encoding/json prefers outer fields and would zero honest embed values.
|
||||
type DatafeedPilot struct {
|
||||
serviceapi.OnlineUserPilot
|
||||
Server string `json:"server"`
|
||||
PilotRating int `json:"pilot_rating"` // INOP placeholder
|
||||
MilitaryRating int `json:"military_rating"` // INOP placeholder
|
||||
QnhIHg float64 `json:"qnh_i_hg"` // INOP placeholder
|
||||
QnhMb int `json:"qnh_mb"` // INOP placeholder
|
||||
FlightPlan *DatafeedFlightplan `json:"flight_plan,omitempty"` // INOP placeholder
|
||||
serviceapi.OnlineUserPilot // promotes general/position/synthetic/pilot_rating/assigned_beacon_code
|
||||
Server string `json:"server"`
|
||||
MilitaryRating int `json:"military_rating"` // always 0: not stored
|
||||
QnhIHg float64 `json:"qnh_i_hg"` // always 0: no weather model
|
||||
QnhMb int `json:"qnh_mb"` // always 0: no weather model
|
||||
// Intentional shadow of embed flight_plan string → object.
|
||||
FlightPlan *DatafeedFlightplan `json:"flight_plan,omitempty"`
|
||||
}
|
||||
|
||||
type DatafeedFlightplan struct {
|
||||
@@ -311,10 +318,11 @@ type DatafeedFlightplan struct {
|
||||
AssignedTransponder string `json:"assigned_transponder"`
|
||||
}
|
||||
|
||||
// DatafeedATC embeds OnlineUserATC (incl. text_atis when present).
|
||||
// Do NOT redeclare TextATIS — embed owns json:"text_atis".
|
||||
type DatafeedATC struct {
|
||||
serviceapi.OnlineUserATC
|
||||
Server string `json:"server"`
|
||||
TextATIS []string `json:"text_atis"` // INOP placeholder
|
||||
Server string `json:"server"`
|
||||
}
|
||||
|
||||
type DatafeedCache struct {
|
||||
@@ -362,22 +370,27 @@ func (s *Server) generateDatafeed() (feed *DatafeedCache, err error) {
|
||||
ATC: []DatafeedATC{},
|
||||
}
|
||||
|
||||
serverIdent, _, _, serverErr := s.getFsdServerInfo()
|
||||
if serverErr != nil || serverIdent == "" {
|
||||
serverIdent = "OPENFSD"
|
||||
}
|
||||
|
||||
for _, pilot := range onlineUsers.Pilots {
|
||||
dataFeed.Pilots = append(dataFeed.Pilots, DatafeedPilot{
|
||||
OnlineUserPilot: pilot,
|
||||
Server: "OPENFSD",
|
||||
PilotRating: 1,
|
||||
MilitaryRating: 1,
|
||||
QnhIHg: 29.92,
|
||||
QnhMb: 1013,
|
||||
OnlineUserPilot: pilot, // pilot_rating / assigned_beacon from embed
|
||||
Server: serverIdent,
|
||||
MilitaryRating: 0,
|
||||
QnhIHg: 0,
|
||||
QnhMb: 0,
|
||||
// Intentional shadow: string info section → VATSIM-shaped object.
|
||||
FlightPlan: mapInfoSectionToDatafeedFP(pilot.FlightPlan, pilot.AssignedBeaconCode),
|
||||
})
|
||||
}
|
||||
|
||||
for _, atc := range onlineUsers.ATC {
|
||||
dataFeed.ATC = append(dataFeed.ATC, DatafeedATC{
|
||||
OnlineUserATC: atc,
|
||||
Server: "OPENFSD",
|
||||
TextATIS: []string{},
|
||||
OnlineUserATC: atc, // text_atis from embed when present
|
||||
Server: serverIdent,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -453,3 +466,59 @@ func (s *Server) updateDataFeedCache() {
|
||||
}
|
||||
datafeedCache.Store(feed)
|
||||
}
|
||||
|
||||
// parseNonNegIntField returns 0 for empty, non-numeric, or negative input.
|
||||
func parseNonNegIntField(s string) int {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return 0
|
||||
}
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil || n < 0 {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// formatHHMM formats hour/minute info-section fields as "HHMM" with zero padding.
|
||||
// sweatbox/encodeFlightPlanInfo may emit single-digit "0" rather than "00".
|
||||
func formatHHMM(hoursField, minutesField string) string {
|
||||
h := parseNonNegIntField(hoursField)
|
||||
m := parseNonNegIntField(minutesField)
|
||||
return fmt.Sprintf("%02d%02d", h, m)
|
||||
}
|
||||
|
||||
// mapInfoSectionToDatafeedFP maps a session/serviceapi flight-plan info section
|
||||
// (colon fields after $FP SOURCE:DEST) to a VATSIM-shaped DatafeedFlightplan.
|
||||
// Empty info returns nil so the intentional outer FlightPlan shadow omits the key.
|
||||
func mapInfoSectionToDatafeedFP(info, assignedBeacon string) *DatafeedFlightplan {
|
||||
info = strings.TrimSpace(info)
|
||||
if info == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(info, ":")
|
||||
field := func(i int) string {
|
||||
if i < 0 || i >= len(parts) {
|
||||
return ""
|
||||
}
|
||||
return parts[i]
|
||||
}
|
||||
ac := field(1)
|
||||
fp := &DatafeedFlightplan{
|
||||
FlightRules: field(0),
|
||||
Aircraft: ac,
|
||||
AircraftFAA: ac,
|
||||
AircraftShort: ac,
|
||||
Departure: field(3),
|
||||
DepTime: field(4),
|
||||
Arrival: field(7),
|
||||
EnrouteTime: formatHHMM(field(8), field(9)),
|
||||
FuelTime: formatHHMM(field(10), field(11)),
|
||||
Alternate: field(12),
|
||||
Remarks: field(13),
|
||||
Route: field(14),
|
||||
RevisionID: 0,
|
||||
AssignedTransponder: assignedBeacon,
|
||||
}
|
||||
return fp
|
||||
}
|
||||
|
||||
150
internal/web/data_flightplan_test.go
Normal file
150
internal/web/data_flightplan_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/renorris/openfsd/internal/serviceapi"
|
||||
)
|
||||
|
||||
func TestFormatHHMM(t *testing.T) {
|
||||
cases := []struct {
|
||||
h, m, want string
|
||||
}{
|
||||
{"0", "0", "0000"},
|
||||
{"2", "5", "0205"},
|
||||
{"2", "40", "0240"},
|
||||
{"", "", "0000"},
|
||||
{"x", "3", "0003"},
|
||||
{"-1", "5", "0005"},
|
||||
{"12", "y", "1200"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
got := formatHHMM(tc.h, tc.m)
|
||||
if got != tc.want {
|
||||
t.Errorf("formatHHMM(%q,%q)=%q want %q", tc.h, tc.m, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapInfoSectionToDatafeedFP(t *testing.T) {
|
||||
if got := mapInfoSectionToDatafeedFP("", "1200"); got != nil {
|
||||
t.Fatalf("empty info → nil, got %#v", got)
|
||||
}
|
||||
info := "I:B738:450:KLAX:1200:0000:FL350:KJFK:2:40:5:30:KBOS:RMK:DCT"
|
||||
fp := mapInfoSectionToDatafeedFP(info, "4321")
|
||||
if fp == nil {
|
||||
t.Fatal("expected non-nil FP")
|
||||
}
|
||||
if fp.FlightRules != "I" || fp.Aircraft != "B738" || fp.AircraftFAA != "B738" || fp.AircraftShort != "B738" {
|
||||
t.Fatalf("type/rules: %#v", fp)
|
||||
}
|
||||
if fp.Departure != "KLAX" || fp.Arrival != "KJFK" || fp.Alternate != "KBOS" {
|
||||
t.Fatalf("airports: %#v", fp)
|
||||
}
|
||||
if fp.DepTime != "1200" {
|
||||
t.Fatalf("deptime=%q", fp.DepTime)
|
||||
}
|
||||
if fp.EnrouteTime != "0240" || fp.FuelTime != "0530" {
|
||||
t.Fatalf("times enroute=%q fuel=%q", fp.EnrouteTime, fp.FuelTime)
|
||||
}
|
||||
if fp.Remarks != "RMK" || fp.Route != "DCT" {
|
||||
t.Fatalf("remarks/route: %#v", fp)
|
||||
}
|
||||
if fp.AssignedTransponder != "4321" || fp.RevisionID != 0 {
|
||||
t.Fatalf("beacon/rev: %#v", fp)
|
||||
}
|
||||
// Partial fields: map what is present
|
||||
partial := mapInfoSectionToDatafeedFP("V:C172", "")
|
||||
if partial == nil || partial.FlightRules != "V" || partial.Aircraft != "C172" {
|
||||
t.Fatalf("partial: %#v", partial)
|
||||
}
|
||||
if partial.EnrouteTime != "0000" || partial.FuelTime != "0000" {
|
||||
t.Fatalf("partial times: %#v", partial)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatafeedPilotJSONMarshal_EmbedShadowing(t *testing.T) {
|
||||
info := "I:B738:450:KLAX:1200:0000:FL350:KJFK:2:5:3:0::remarks:route"
|
||||
pilot := serviceapi.OnlineUserPilot{
|
||||
OnlineUserGeneralData: serviceapi.OnlineUserGeneralData{
|
||||
Callsign: "UAL1",
|
||||
CID: 100,
|
||||
},
|
||||
Altitude: 35000,
|
||||
PilotRating: 15,
|
||||
FlightPlan: info,
|
||||
AssignedBeaconCode: "1200",
|
||||
}
|
||||
dp := DatafeedPilot{
|
||||
OnlineUserPilot: pilot,
|
||||
Server: "TESTSRV",
|
||||
MilitaryRating: 0,
|
||||
QnhIHg: 0,
|
||||
QnhMb: 0,
|
||||
FlightPlan: mapInfoSectionToDatafeedFP(pilot.FlightPlan, pilot.AssignedBeaconCode),
|
||||
}
|
||||
b, err := json.Marshal(dp)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Real pilot_rating from embed (not outer zero).
|
||||
if pr, ok := m["pilot_rating"].(float64); !ok || pr != 15 {
|
||||
t.Fatalf("pilot_rating=%v want 15 (json=%s)", m["pilot_rating"], b)
|
||||
}
|
||||
// flight_plan must be an object, not a string and not omitted.
|
||||
fpObj, ok := m["flight_plan"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("flight_plan type %T want object; json=%s", m["flight_plan"], b)
|
||||
}
|
||||
if fpObj["departure"] != "KLAX" || fpObj["arrival"] != "KJFK" {
|
||||
t.Fatalf("flight_plan airports: %#v", fpObj)
|
||||
}
|
||||
if fpObj["enroute_time"] != "0205" {
|
||||
t.Fatalf("enroute_time=%v want 0205", fpObj["enroute_time"])
|
||||
}
|
||||
if m["military_rating"].(float64) != 0 || m["qnh_i_hg"].(float64) != 0 || m["qnh_mb"].(float64) != 0 {
|
||||
t.Fatalf("expected honest zero QNH/military: %s", b)
|
||||
}
|
||||
if m["server"] != "TESTSRV" {
|
||||
t.Fatalf("server=%v", m["server"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatafeedATCJSON_NoOuterTextATISZero(t *testing.T) {
|
||||
atc := serviceapi.OnlineUserATC{
|
||||
OnlineUserGeneralData: serviceapi.OnlineUserGeneralData{Callsign: "LAX_TWR"},
|
||||
Frequency: "118500",
|
||||
// TextATIS empty → omitempty on embed
|
||||
}
|
||||
da := DatafeedATC{OnlineUserATC: atc, Server: "S1"}
|
||||
b, err := json.Marshal(da)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, has := m["text_atis"]; has {
|
||||
t.Fatalf("empty text_atis should omit: %s", b)
|
||||
}
|
||||
// With embed ATIS present:
|
||||
atc.TextATIS = []string{"line1"}
|
||||
da = DatafeedATC{OnlineUserATC: atc, Server: "S1"}
|
||||
b, err = json.Marshal(da)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
atis, ok := m["text_atis"].([]any)
|
||||
if !ok || len(atis) != 1 || atis[0] != "line1" {
|
||||
t.Fatalf("text_atis from embed: %s", b)
|
||||
}
|
||||
}
|
||||
@@ -153,10 +153,14 @@ func TestGenerateDatafeedWithStubOnline(t *testing.T) {
|
||||
Pilots: []serviceapi.OnlineUserPilot{{
|
||||
OnlineUserGeneralData: serviceapi.OnlineUserGeneralData{Callsign: "N1", CID: 1},
|
||||
Altitude: 1000,
|
||||
PilotRating: 7,
|
||||
FlightPlan: "I:B738:450:KLAX:1200:0000:FL350:KJFK:2:5:3:0::rmk:DCT",
|
||||
AssignedBeaconCode: "1200",
|
||||
}, {
|
||||
OnlineUserGeneralData: serviceapi.OnlineUserGeneralData{Callsign: "SBX1", CID: 900001},
|
||||
Altitude: 2000,
|
||||
Synthetic: true,
|
||||
PilotRating: 0,
|
||||
}},
|
||||
ATC: []serviceapi.OnlineUserATC{{
|
||||
OnlineUserGeneralData: serviceapi.OnlineUserGeneralData{Callsign: "TWR", CID: 2},
|
||||
@@ -170,6 +174,32 @@ func TestGenerateDatafeedWithStubOnline(t *testing.T) {
|
||||
assert.Contains(t, s, "N1")
|
||||
assert.Contains(t, s, `"synthetic":true`)
|
||||
assert.NotContains(t, s, `"synthetic":false`)
|
||||
|
||||
// Honest datafeed mapping: no fabricated QNH 29.92 / ratings of 1; real pilot_rating + object FP.
|
||||
ident, _, _, err := env.server.getFsdServerInfo()
|
||||
require.NoError(t, err)
|
||||
if ident == "" {
|
||||
ident = "OPENFSD"
|
||||
}
|
||||
var pilots []DatafeedPilot
|
||||
for _, pilot := range ou.Pilots {
|
||||
pilots = append(pilots, DatafeedPilot{
|
||||
OnlineUserPilot: pilot,
|
||||
Server: ident,
|
||||
MilitaryRating: 0,
|
||||
QnhIHg: 0,
|
||||
QnhMb: 0,
|
||||
FlightPlan: mapInfoSectionToDatafeedFP(pilot.FlightPlan, pilot.AssignedBeaconCode),
|
||||
})
|
||||
}
|
||||
jb, err := json.Marshal(pilots)
|
||||
require.NoError(t, err)
|
||||
js := string(jb)
|
||||
assert.NotContains(t, js, "29.92")
|
||||
assert.NotContains(t, js, "1013")
|
||||
assert.Contains(t, js, `"pilot_rating":7`)
|
||||
assert.Contains(t, js, `"flight_plan":{`)
|
||||
assert.Contains(t, js, `"server":"`+ident+`"`)
|
||||
}
|
||||
|
||||
func TestMakeFsdHttpServiceRequest(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user