package contract import ( "encoding/json" "reflect" "slices" "strings" "testing" ) func limit(n int64) *int64 { return &n } // A route is read strictly and whole (issue 774): what the contract names is admitted, what it does not, or a route // it cannot serve, is refused naming why; and the published schema names exactly the fields the decoder reads. func TestARouteIsReadStrictly(t *testing.T) { for _, c := range []struct { raw, refused string want Request }{ {raw: `{"name":"app.example.org","internal-name":"app.anchor.internal","label":"app","endpoint":"web","port":8080}`, want: Request{Name: "app.example.org", InternalName: "app.anchor.internal", Label: "app", Endpoint: "web", Port: 8080}}, {raw: `{"internal-name":"app.anchor.internal","port":443,"scheme":"https","insecure":true,"max-request-body":1048576}`, want: Request{InternalName: "app.anchor.internal", Port: 443, Scheme: "https", Insecure: true, MaxRequestBody: limit(1048576)}}, {raw: `{"name":"git.example.org","path":"/api/internal","deny":true,"priority":100000}`, want: Request{Name: "git.example.org", Path: "/api/internal", Deny: true, Priority: 100000}}, {raw: `{"name":"old.example.org","redirect":"https://new.example.org"}`, want: Request{Name: "old.example.org", Redirect: "https://new.example.org"}}, {raw: `{"name":"admin.example.org","port":80,"auth":"/run/secrets/admin"}`, want: Request{Name: "admin.example.org", Port: 80, Auth: "/run/secrets/admin"}}, {raw: `{"label":"app","port":8080}`, refused: "names nothing"}, {raw: `{"name":"app.example.org"}`, refused: "gives no port"}, {raw: `{"name":"app.example.org","port":70000}`, refused: "not a port"}, {raw: `{"name":"app.example.org","port":80.5}`, refused: "not a route"}, {raw: `{"name":"app.example.org","port":"80"}`, refused: "not a route"}, {raw: `{"name":"app.example.org","port":80,"scheme":"ftp"}`, refused: "neither http nor https"}, {raw: `{"name":"app.example.org","port":80,"max-request-body":-1}`, refused: "max-request-body"}, {raw: `{"name":"app.example.org","port":80,"max-request-body":0}`, refused: "max-request-body"}, {raw: `{"name":"app.example.org","port":80,"auth":"admin:$2y$05$abc"}`, refused: "credential"}, {raw: `{"name":"app.example.org","port":80,"host":"x"}`, refused: "unknown field \"host\""}, {raw: `{"name":"app.example.org","port":80} {}`, refused: "more than one"}, } { got, err := DecodeRequest([]byte(c.raw)) if c.refused != "" { if err == nil || !strings.Contains(err.Error(), c.refused) { t.Errorf("DecodeRequest(%s) = %+v, %v; want refused for %q", c.raw, got, err, c.refused) } continue } if err != nil || !reflect.DeepEqual(got, c.want) { t.Errorf("DecodeRequest(%s) = %+v, %v; want %+v", c.raw, got, err, c.want) continue } // What it encodes, it reads back the same. if raw, err := got.Encode(); err != nil { t.Errorf("Encode(%+v): %v", got, err) } else if again, err := DecodeRequest(raw); err != nil || !reflect.DeepEqual(again, got) { t.Errorf("Encode then DecodeRequest of %+v = %+v, %v", got, again, err) } } if (Request{Path: "api"}).Prefix() != "/api" || (Request{}).Prefix() != "" || (Request{Scheme: " HTTPS"}).SchemeOrDefault() != "https" || (Request{}).SchemeOrDefault() != "http" || (Request{Redirect: "x"}).Forwards() { t.Error("Prefix, SchemeOrDefault or Forwards says otherwise than the contract") } var schema struct { Properties map[string]json.RawMessage `json:"properties"` AdditionalProperties bool `json:"additionalProperties"` } if err := json.Unmarshal(Schema, &schema); err != nil || schema.AdditionalProperties { t.Fatalf("the schema is not a closed object: %v", err) } var tags []string for f := range reflect.TypeFor[Request]().Fields() { tags = append(tags, strings.Split(f.Tag.Get("json"), ",")[0]) } var said []string for name := range schema.Properties { said = append(said, name) } slices.Sort(tags) slices.Sort(said) if !slices.Equal(tags, said) { t.Fatalf("the schema names %v, the decoder reads %v", said, tags) } }