Commit de81ce7d authored by Muhammad Umair Khan's avatar Muhammad Umair Khan
Browse files

meep-app-enablement: add EDGE-3 to MEC 011 adapter mapping and validation

parent cb262155
Loading
Loading
Loading
Loading
+7 −1
Changes for go-apps/meep-app-enablement/go.mod: 7 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -3,7 +3,7 @@ module github.com/InterDigitalInc/AdvantEDGE/go-apps/meep-app-enablement
go 1.12

require (
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-app-support-client v0.0.0-20230530211153-8f6edbeaf367 // indirect
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-app-support-client v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-applications v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-data-key-mgr v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-data-model v0.0.0 // indirect
@@ -14,6 +14,8 @@ require (
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-model v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-mq v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-redis v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-sandbox-ctrl-client v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-service-mgmt-client v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-subscriptions v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-swagger-api-mgr v0.0.0
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-websocket v0.0.0 // indirect
@@ -22,9 +24,11 @@ require (
	github.com/gorilla/handlers v1.5.1
	github.com/gorilla/mux v1.8.0
	github.com/prometheus/client_golang v1.9.0
	golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect
)

replace (
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-app-support-client => ../../go-packages/meep-app-support-client
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-applications => ../../go-packages/meep-applications
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-data-key-mgr => ../../go-packages/meep-data-key-mgr
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-data-model => ../../go-packages/meep-data-model
@@ -35,6 +39,8 @@ replace (
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-model => ../../go-packages/meep-model
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-mq => ../../go-packages/meep-mq
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-redis => ../../go-packages/meep-redis
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-sandbox-ctrl-client => ../../go-packages/meep-sandbox-ctrl-client
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-service-mgmt-client => ../../go-packages/meep-service-mgmt-client
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-subscriptions => ../../go-packages/meep-subscriptions
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-swagger-api-mgr => ../../go-packages/meep-swagger-api-mgr
	github.com/InterDigitalInc/AdvantEDGE/go-packages/meep-websocket => ../../go-packages/meep-websocket
+4 −0
Changes for go-apps/meep-app-enablement/go.sum: 4 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
@@ -358,6 +359,8 @@ golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAG
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -423,6 +426,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+257 −0
Changes for go-apps/meep-app-enablement/server/edge-3/eas_validation.go: 257 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*
 * Copyright (c) 2026  The AdvantEDGE Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// EDGE-3 to MEC 011 adapter: validation of EAS registrations (3GPP TS 29.558 EASRegistration) and
// expiry policy. Violations are reported as 3GPP ProblemDetails invalidParams (param = JSON pointer).
package server

import (
	"net"
	"net/url"
	"regexp"
	"strconv"
	"strings"
	"time"
)

// EAS registration lifetime policy of the sandbox EES: the EES grants the requested expiry time
// within this maximum, and this lifetime when the EAS does not request one.
const (
	easRegistrationDefaultLifetime = 24 * time.Hour
	easRegistrationMaxLifetime     = 24 * time.Hour
)

// Placeholder value inserted by Swagger UI "Try it out"; never accepted as an identifier
const swaggerPlaceholder = "string"

var (
	fqdnRegexp      = regexp.MustCompile(`^(?i)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?)*\.?$`)
	timeOfDayRegexp = regexp.MustCompile(`^([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\.[0-9]+)?(Z|z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])?$`)
	suppFeatRegexp  = regexp.MustCompile(`^[A-Fa-f0-9]*$`)
)

// Shapes of GeographicArea (3GPP TS 29.572) and their mandatory attributes besides shape
var geographicShapes = map[string][]string{
	ShapePoint:                    {"point"},
	ShapePointUncertaintyCircle:   {"point", "uncertainty"},
	ShapePointUncertaintyEllipse:  {"point", "uncertaintyEllipse", "confidence"},
	ShapePolygon:                  {"pointList"},
	ShapePointAltitude:            {"point", "altitude"},
	ShapePointAltitudeUncertainty: {"point", "altitude", "uncertaintyEllipse", "uncertaintyAltitude", "confidence"},
	ShapeEllipsoidArc:             {"point", "innerRadius", "uncertaintyRadius", "offsetAngle", "includedAngle", "confidence"},
}

type invalidParams []InvalidParam

func (p *invalidParams) add(param string, reason string) {
	*p = append(*p, InvalidParam{Param: param, Reason: reason})
}

// validateEasRegistration - 3GPP and sandbox rules of an EAS registration request; nil when valid
func validateEasRegistration(reg *EasRegistration, now time.Time) []InvalidParam {
	var errs invalidParams
	if reg == nil || reg.EasProf == nil {
		errs.add("/easProf", "mandatory attribute missing")
		return errs
	}
	validateEasProfile(reg.EasProf, "/easProf", &errs)
	if reg.ExpTime != nil && !reg.ExpTime.After(now) {
		errs.add("/expTime", "shall be in the future")
	}
	if !suppFeatRegexp.MatchString(reg.SuppFeat) {
		errs.add("/suppFeat", "shall be a hexadecimal string")
	}
	return errs
}

// validateEasProfile - 3GPP and sandbox rules of an EAS profile
func validateEasProfile(prof *EasProfile, path string, errs *invalidParams) {
	if !validIdentifier(prof.EasId) {
		errs.add(path+"/easId", "mandatory, non-empty identifier")
	}
	validateEndPoint(prof.EndPt, path+"/endPt", errs)
	if prof.ProvId != "" && !validIdentifier(prof.ProvId) {
		errs.add(path+"/provId", "invalid identifier")
	}
	for i, acId := range prof.AcIds {
		if !validIdentifier(acId) {
			errs.add(path+"/acIds/"+strconv.Itoa(i), "invalid identifier")
		}
	}
	if prof.Type_ != "" && prof.FlexEasType != "" {
		errs.add(path+"/flexEasType", "type and flexEasType shall not both be present")
	}
	if prof.Status != "" && prof.StatusVal != "" {
		errs.add(path+"/statusVal", "status and statusVal shall not both be present")
	}
	for i, sched := range prof.Scheds {
		validateSchedule(sched, path+"/scheds/"+strconv.Itoa(i), errs)
	}
	if prof.SvcArea != nil && prof.SvcArea.GeoServAr != nil {
		for i, area := range prof.SvcArea.GeoServAr.GeoArs {
			validateGeographicArea(area, path+"/svcArea/geoServAr/geoArs/"+strconv.Itoa(i), errs)
		}
	}
	if k := prof.SvcKpi; k != nil {
		// Fixed order, so that the reported invalid parameters are deterministic
		kpis := []struct {
			name  string
			value *int32
		}{{"maxReqRate", k.MaxReqRate}, {"maxRespTime", k.MaxRespTime}, {"avail", k.Avail},
			{"avlComp", k.AvlComp}, {"avlGraComp", k.AvlGraComp}, {"avlMem", k.AvlMem}, {"avlStrg", k.AvlStrg}}
		for _, kpi := range kpis {
			if kpi.value != nil && *kpi.value < 0 {
				errs.add(path+"/svcKpi/"+kpi.name, "shall not be negative")
			}
		}
	}
	if prof.AvlRep != nil && *prof.AvlRep < 0 {
		errs.add(path+"/avlRep", "shall not be negative")
	}
	for i, loc := range prof.AppLocs {
		if strings.TrimSpace(loc.Dnai) == "" {
			errs.add(path+"/appLocs/"+strconv.Itoa(i)+"/dnai", "mandatory attribute missing")
		}
	}
}

// validateEndPoint - exactly one of uri, fqdn, ipv4Addrs, ipv6Addrs (3GPP oneOf), each well formed
func validateEndPoint(ep *EndPoint, path string, errs *invalidParams) {
	if ep == nil {
		errs.add(path, "mandatory attribute missing")
		return
	}
	present := 0
	for _, set := range []bool{ep.Uri != "", ep.Fqdn != "", len(ep.Ipv4Addrs) > 0, len(ep.Ipv6Addrs) > 0} {
		if set {
			present++
		}
	}
	if present != 1 {
		errs.add(path, "exactly one of uri, fqdn, ipv4Addrs, ipv6Addrs shall be present")
		return
	}
	switch {
	case ep.Uri != "":
		if u, err := url.Parse(ep.Uri); err != nil || u.Scheme == "" || u.Host == "" {
			errs.add(path+"/uri", "shall be an absolute URI")
		}
	case ep.Fqdn != "":
		if len(ep.Fqdn) > 253 || !fqdnRegexp.MatchString(ep.Fqdn) {
			errs.add(path+"/fqdn", "invalid FQDN")
		}
	case len(ep.Ipv4Addrs) > 0:
		for i, a := range ep.Ipv4Addrs {
			if ip := net.ParseIP(a); ip == nil || ip.To4() == nil || strings.Contains(a, ":") {
				errs.add(path+"/ipv4Addrs/"+strconv.Itoa(i), "invalid IPv4 address")
			}
		}
	default:
		for i, a := range ep.Ipv6Addrs {
			if ip := net.ParseIP(a); ip == nil || !strings.Contains(a, ":") {
				errs.add(path+"/ipv6Addrs/"+strconv.Itoa(i), "invalid IPv6 address")
			}
		}
	}
}

// validateSchedule - days of week 1..7 (Monday..Sunday), times of day per RFC 3339 partial/full time
func validateSchedule(sched ScheduledCommunicationTime, path string, errs *invalidParams) {
	if len(sched.DaysOfWeek) > 6 {
		errs.add(path+"/daysOfWeek", "at most 6 days (absent means every day)")
	}
	for i, d := range sched.DaysOfWeek {
		if d < 1 || d > 7 {
			errs.add(path+"/daysOfWeek/"+strconv.Itoa(i), "shall be between 1 (Monday) and 7 (Sunday)")
		}
	}
	if sched.TimeOfDayStart != "" && !timeOfDayRegexp.MatchString(sched.TimeOfDayStart) {
		errs.add(path+"/timeOfDayStart", "shall be an RFC 3339 partial-time or full-time")
	}
	if sched.TimeOfDayEnd != "" && !timeOfDayRegexp.MatchString(sched.TimeOfDayEnd) {
		errs.add(path+"/timeOfDayEnd", "shall be an RFC 3339 partial-time or full-time")
	}
}

// validateGeographicArea - known shape, mandatory attributes of the shape, coordinate ranges
func validateGeographicArea(area GeographicArea, path string, errs *invalidParams) {
	mandatory, known := geographicShapes[area.Shape]
	if !known {
		errs.add(path+"/shape", "unsupported shape")
		return
	}
	present := map[string]bool{
		"point":               area.Point != nil,
		"pointList":           area.PointList != nil,
		"uncertainty":         area.Uncertainty != nil,
		"uncertaintyEllipse":  area.UncertaintyEllipse != nil,
		"confidence":          area.Confidence != nil,
		"altitude":            area.Altitude != nil,
		"uncertaintyAltitude": area.UncertaintyAltitude != nil,
		"innerRadius":         area.InnerRadius != nil,
		"uncertaintyRadius":   area.UncertaintyRadius != nil,
		"offsetAngle":         area.OffsetAngle != nil,
		"includedAngle":       area.IncludedAngle != nil,
	}
	for _, attr := range mandatory {
		if !present[attr] {
			errs.add(path+"/"+attr, "mandatory attribute of shape "+area.Shape+" missing")
		}
	}
	if area.Point != nil {
		validateCoordinates(*area.Point, path+"/point", errs)
	}
	if area.Shape == ShapePolygon && area.PointList != nil {
		if n := len(area.PointList); n < 3 || n > 15 {
			errs.add(path+"/pointList", "shall contain 3 to 15 points")
		}
		for i, p := range area.PointList {
			validateCoordinates(p, path+"/pointList/"+strconv.Itoa(i), errs)
		}
	}
	if area.Uncertainty != nil && *area.Uncertainty < 0 {
		errs.add(path+"/uncertainty", "shall not be negative")
	}
	if area.Confidence != nil && (*area.Confidence < 0 || *area.Confidence > 100) {
		errs.add(path+"/confidence", "shall be between 0 and 100")
	}
}

func validateCoordinates(c GeographicalCoordinates, path string, errs *invalidParams) {
	if c.Lat < -90 || c.Lat > 90 {
		errs.add(path+"/lat", "shall be between -90 and 90")
	}
	if c.Lon < -180 || c.Lon > 180 {
		errs.add(path+"/lon", "shall be between -180 and 180")
	}
}

// validIdentifier - non-empty, no surrounding or inner whitespace, not the Swagger UI placeholder
func validIdentifier(id string) bool {
	return id != "" && id != swaggerPlaceholder && strings.TrimSpace(id) == id && !strings.ContainsAny(id, " \t\r\n")
}

// grantedExpiry - expiry time granted by the EES for a requested one (3GPP: the EES may change it)
func grantedExpiry(requested *time.Time, now time.Time) time.Time {
	max := now.Add(easRegistrationMaxLifetime)
	if requested == nil {
		return now.Add(easRegistrationDefaultLifetime)
	}
	if requested.After(max) {
		return max
	}
	return *requested
}
+132 −0
Changes for go-apps/meep-app-enablement/server/edge-3/eas_validation_test.go: 132 added lines, 0 removed lines.
Original line number Diff line number Diff line
/*
 * Copyright (c) 2026  The AdvantEDGE Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package server

import (
	"encoding/json"
	"strings"
	"testing"
	"time"
)

var validationNow = time.Date(2026, 10, 22, 10, 0, 0, 0, time.UTC)

func decodeRegistration(t *testing.T, s string) *EasRegistration {
	t.Helper()
	var reg EasRegistration
	if err := json.Unmarshal([]byte(s), &reg); err != nil {
		t.Fatalf("decode registration: %v", err)
	}
	return &reg
}

func params(errs []InvalidParam) string {
	var p []string
	for _, e := range errs {
		p = append(p, e.Param)
	}
	return strings.Join(p, ",")
}

func TestValidateEasRegistrationValid(t *testing.T) {
	for name, reg := range map[string]string{
		"full profile":          `{"easProf":` + fullEasProfile + `,"expTime":"2026-10-22T12:00:00Z","suppFeat":"0aF"}`,
		"minimal uri":           `{"easProf":{"easId":"eas-1","endPt":{"uri":"http://eas.example:8080/api"}}}`,
		"fqdn":                  `{"easProf":{"easId":"eas-1","endPt":{"fqdn":"eas.mep1.example."}}}`,
		"ipv4":                  `{"easProf":{"easId":"eas-1","endPt":{"ipv4Addrs":["192.0.2.1","198.51.100.7"]}}}`,
		"ipv6":                  `{"easProf":{"easId":"eas-1","endPt":{"ipv6Addrs":["2001:db8::1"]}}}`,
		"flex type":             `{"easProf":{"easId":"eas-1","endPt":{"fqdn":"e.example"},"flexEasType":"GAMING","status":"up"}}`,
		"all shapes":            `{"easProf":{"easId":"eas-1","endPt":{"fqdn":"e.example"},"svcArea":{"geoServAr":{"geoArs":[` + allShapes + `]}}}}`,
		"full-time with offset": `{"easProf":{"easId":"eas-1","endPt":{"fqdn":"e.example"},"scheds":[{"timeOfDayStart":"20:15:00-08:00","timeOfDayEnd":"23:59:60.5+01:00"}]}}`,
	} {
		if errs := validateEasRegistration(decodeRegistration(t, reg), validationNow); errs != nil {
			t.Errorf("%s: unexpected invalid params %+v", name, errs)
		}
	}
}

const allShapes = `{"shape":"POINT","point":{"lon":7.42,"lat":43.74}},
 {"shape":"POINT_UNCERTAINTY_CIRCLE","point":{"lon":7.42,"lat":43.74},"uncertainty":0},
 {"shape":"POINT_UNCERTAINTY_ELLIPSE","point":{"lon":7.42,"lat":43.74},"uncertaintyEllipse":{"semiMajor":1,"semiMinor":1,"orientationMajor":0},"confidence":0},
 {"shape":"POLYGON","pointList":[{"lon":-180,"lat":-90},{"lon":180,"lat":90},{"lon":0,"lat":0}]},
 {"shape":"POINT_ALTITUDE","point":{"lon":7.42,"lat":43.74},"altitude":0},
 {"shape":"POINT_ALTITUDE_UNCERTAINTY","point":{"lon":7.42,"lat":43.74},"altitude":-5,"uncertaintyEllipse":{"semiMajor":1,"semiMinor":1,"orientationMajor":0},"uncertaintyAltitude":2,"confidence":100},
 {"shape":"ELLIPSOID_ARC","point":{"lon":7.42,"lat":43.74},"innerRadius":0,"uncertaintyRadius":1,"offsetAngle":0,"includedAngle":360,"confidence":50}`

func TestValidateEasRegistrationInvalid(t *testing.T) {
	cases := []struct {
		name string
		reg  string
		want string // comma-separated invalid params (JSON pointers), in order
	}{
		{"no profile", `{}`, "/easProf"},
		{"missing easId and endPt", `{"easProf":{}}`, "/easProf/easId,/easProf/endPt"},
		{"swagger placeholder easId", `{"easProf":{"easId":"string","endPt":{"fqdn":"e.example"}}}`, "/easProf/easId"},
		{"blank in easId", `{"easProf":{"easId":"eas 1","endPt":{"fqdn":"e.example"}}}`, "/easProf/easId"},
		{"two endpoint kinds", `{"easProf":{"easId":"e","endPt":{"uri":"http://e.example","fqdn":"e.example"}}}`, "/easProf/endPt"},
		{"empty endpoint", `{"easProf":{"easId":"e","endPt":{}}}`, "/easProf/endPt"},
		{"relative uri", `{"easProf":{"easId":"e","endPt":{"uri":"/api/v1"}}}`, "/easProf/endPt/uri"},
		{"bad fqdn", `{"easProf":{"easId":"e","endPt":{"fqdn":"-bad_host"}}}`, "/easProf/endPt/fqdn"},
		{"ipv6 in ipv4Addrs", `{"easProf":{"easId":"e","endPt":{"ipv4Addrs":["192.0.2.1","2001:db8::1"]}}}`, "/easProf/endPt/ipv4Addrs/1"},
		{"ipv4 in ipv6Addrs", `{"easProf":{"easId":"e","endPt":{"ipv6Addrs":["192.0.2.1"]}}}`, "/easProf/endPt/ipv6Addrs/0"},
		{"placeholder provId and acId", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"provId":"string","acIds":["ac-1","string"]}}`, "/easProf/provId,/easProf/acIds/1"},
		{"type and flexEasType", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"type":"V2X","flexEasType":"X"}}`, "/easProf/flexEasType"},
		{"status and statusVal", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"status":"up","statusVal":"ENABLED"}}`, "/easProf/statusVal"},
		{"day of week out of range", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"scheds":[{"daysOfWeek":[0,8]}]}}`, "/easProf/scheds/0/daysOfWeek/0,/easProf/scheds/0/daysOfWeek/1"},
		{"seven days", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"scheds":[{"daysOfWeek":[1,2,3,4,5,6,7]}]}}`, "/easProf/scheds/0/daysOfWeek"},
		{"bad times of day", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"scheds":[{"timeOfDayStart":"8:00","timeOfDayEnd":"24:00:00"}]}}`, "/easProf/scheds/0/timeOfDayStart,/easProf/scheds/0/timeOfDayEnd"},
		{"unknown shape", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"svcArea":{"geoServAr":{"geoArs":[{"shape":"CIRCLE"}]}}}}`, "/easProf/svcArea/geoServAr/geoArs/0/shape"},
		{"point without point", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"svcArea":{"geoServAr":{"geoArs":[{"shape":"POINT"}]}}}}`, "/easProf/svcArea/geoServAr/geoArs/0/point"},
		{"coordinates out of range", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"svcArea":{"geoServAr":{"geoArs":[{"shape":"POINT","point":{"lon":181,"lat":-91}}]}}}}`, "/easProf/svcArea/geoServAr/geoArs/0/point/lat,/easProf/svcArea/geoServAr/geoArs/0/point/lon"},
		{"polygon with 2 points", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"svcArea":{"geoServAr":{"geoArs":[{"shape":"POLYGON","pointList":[{"lon":1,"lat":1},{"lon":2,"lat":2}]}]}}}}`, "/easProf/svcArea/geoServAr/geoArs/0/pointList"},
		{"negative uncertainty, confidence 101", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"svcArea":{"geoServAr":{"geoArs":[{"shape":"POINT_UNCERTAINTY_ELLIPSE","point":{"lon":1,"lat":1},"uncertaintyEllipse":{"semiMajor":1,"semiMinor":1,"orientationMajor":0},"confidence":101},{"shape":"POINT_UNCERTAINTY_CIRCLE","point":{"lon":1,"lat":1},"uncertainty":-1}]}}}}`, "/easProf/svcArea/geoServAr/geoArs/0/confidence,/easProf/svcArea/geoServAr/geoArs/1/uncertainty"},
		{"several negative KPIs, in fixed order", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"svcKpi":{"avlStrg":-1,"maxReqRate":-1,"avail":-1,"avlMem":-1}}}`, "/easProf/svcKpi/maxReqRate,/easProf/svcKpi/avail,/easProf/svcKpi/avlMem,/easProf/svcKpi/avlStrg"},
		{"negative KPI and avlRep", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"svcKpi":{"avail":-1},"avlRep":-5}}`, "/easProf/svcKpi/avail,/easProf/avlRep"},
		{"appLoc without dnai", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"},"appLocs":[{"dnai":" "}]}}`, "/easProf/appLocs/0/dnai"},
		{"expTime in the past", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"}},"expTime":"2026-10-22T09:59:59Z"}`, "/expTime"},
		{"expTime now", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"}},"expTime":"2026-10-22T10:00:00Z"}`, "/expTime"},
		{"suppFeat not hex", `{"easProf":{"easId":"e","endPt":{"fqdn":"e.example"}},"suppFeat":"xyz"}`, "/suppFeat"},
	}
	for _, c := range cases {
		errs := validateEasRegistration(decodeRegistration(t, c.reg), validationNow)
		if got := params(errs); got != c.want {
			t.Errorf("%s: invalid params %q, want %q", c.name, got, c.want)
		}
		for _, e := range errs {
			if e.Reason == "" {
				t.Errorf("%s: %s without reason", c.name, e.Param)
			}
		}
	}
	if errs := validateEasRegistration(nil, validationNow); params(errs) != "/easProf" {
		t.Errorf("nil registration: %+v", errs)
	}
}

func TestGrantedExpiry(t *testing.T) {
	now := validationNow
	inOneHour := now.Add(time.Hour)
	inTwoDays := now.Add(48 * time.Hour)
	if got := grantedExpiry(nil, now); !got.Equal(now.Add(easRegistrationDefaultLifetime)) {
		t.Errorf("no requested expiry: %v", got)
	}
	if got := grantedExpiry(&inOneHour, now); !got.Equal(inOneHour) {
		t.Errorf("requested within the maximum: %v", got)
	}
	if got := grantedExpiry(&inTwoDays, now); !got.Equal(now.Add(easRegistrationMaxLifetime)) {
		t.Errorf("requested beyond the maximum: %v", got)
	}
}
+383 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading