Ctrl K
ComunidadGuías…

pkg/models/grpc_bson_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package models

import (
	"bytes"
	"reflect"
	"strings"
	"testing"
	"time"

	"go.mongodb.org/mongo-driver/v2/bson"
)

// A gRPC test case read back from Mongo keeps its request and response
// times to the nanosecond, with their UTC offset, as an HTTP one does.
// Before, they were BSON dates: milliseconds, in UTC.

var ist = time.FixedZone("IST", 5*3600+1800)

func bsonGrpcReq() GrpcReq {
	return GrpcReq{
		Headers: GrpcHeaders{
			PseudoHeaders:   map[string]string{":path": "/users.Users/Get", ":method": "POST"},
			OrdinaryHeaders: map[string]string{"content-type": "application/grpc"},
		},
		Body:               GrpcLengthPrefixedMessage{CompressionFlag: 1, MessageLength: 3, DecodedData: "abc"},
		AdditionalMessages: []GrpcLengthPrefixedMessage{{MessageLength: 1, DecodedData: "d"}},
		Timestamp:          time.Date(2026, 11, 7, 10, 15, 30, 123456789, ist),
	}
}

func bsonGrpcResp() GrpcResp {
	return GrpcResp{
		Headers:    GrpcHeaders{PseudoHeaders: map[string]string{":status": "200"}, OrdinaryHeaders: map[string]string{}},
		Body:       GrpcLengthPrefixedMessage{MessageLength: 2, DecodedData: "ok"},
		NoMessages: true,
		Trailers:   GrpcHeaders{OrdinaryHeaders: map[string]string{"grpc-status": "0"}},
		Timestamp:  time.Date(2026, 11, 7, 10, 15, 30, 987654321, ist),
	}
}

// sameInstantAndOffset fails unless got is want's instant at want's offset.
func sameInstantAndOffset(t *testing.T, what string, got, want time.Time) {
	t.Helper()
	_, gotOff := got.Zone()
	_, wantOff := want.Zone()
	if !got.Equal(want) || gotOff != wantOff {
		t.Errorf("%s: read back %s, want %s", what, got.Format(time.RFC3339Nano), want.Format(time.RFC3339Nano))
	}
}

// reqWithoutTime and respWithoutTime zero the Timestamp, to compare the other
// fields.
func reqWithoutTime(r GrpcReq) GrpcReq    { r.Timestamp = time.Time{}; return r }
func respWithoutTime(r GrpcResp) GrpcResp { r.Timestamp = time.Time{}; return r }

func TestGrpcBSONKeepsNanosecondsAndOffset(t *testing.T) {
	req, resp := bsonGrpcReq(), bsonGrpcResp()

	// As api-server stores a test case (a document holding the TestCase) and
	// as a mock's spec holds them (by pointer).
	type stored struct {
		Data TestCase `bson:"data"`
		Mock MockSpec `bson:"mock"`
	}
	in := stored{
		Data: TestCase{Name: "test-1", Kind: GRPC_EXPORT, GrpcReq: req, GrpcResp: resp},
		Mock: MockSpec{GRPCReq: &req, GRPCResp: &resp},
	}
	data, err := bson.Marshal(in)
	if err != nil {
		t.Fatalf("marshal: %v", err)
	}
	var out stored
	if err := bson.Unmarshal(data, &out); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if out.Mock.GRPCReq == nil || out.Mock.GRPCResp == nil {
		t.Fatalf("mock spec lost its gRPC request or response: %+v", out.Mock)
	}

	for _, c := range []struct {
		where     string
		req       GrpcReq
		resp      GrpcResp
		wantKinds bool
	}{
		{"test case", out.Data.GrpcReq, out.Data.GrpcResp, true},
		{"mock", *out.Mock.GRPCReq, *out.Mock.GRPCResp, false},
	} {
		sameInstantAndOffset(t, c.where+" request", c.req.Timestamp, req.Timestamp)
		sameInstantAndOffset(t, c.where+" response", c.resp.Timestamp, resp.Timestamp)
		if got, want := reqWithoutTime(c.req), reqWithoutTime(req); !reflect.DeepEqual(got, want) {
			t.Errorf("%s request fields: got %+v, want %+v", c.where, got, want)
		}
		if got, want := respWithoutTime(c.resp), respWithoutTime(resp); !reflect.DeepEqual(got, want) {
			t.Errorf("%s response fields: got %+v, want %+v", c.where, got, want)
		}
	}
	if out.Data.Name != "test-1" || out.Data.Kind != GRPC_EXPORT {
		t.Errorf("test case fields: got %q %q", out.Data.Name, out.Data.Kind)
	}
}

// The stored document is the one the driver's default gives, key for key and
// value for value, but for "timestamp": RFC3339Nano text in place of a date.
func TestGrpcBSONShapeIsTheDefaultButTheTimestamp(t *testing.T) {
	for _, c := range []struct {
		what   string
		codec  interface{}
		plain  interface{}
		wantTS string
	}{
		{"request", bsonGrpcReq(), grpcReqFields(bsonGrpcReq()), "2026-11-07T10:15:30.123456789+05:30"},
		{"response", bsonGrpcResp(), grpcRespFields(bsonGrpcResp()), "2026-11-07T10:15:30.987654321+05:30"},
		{"zero request", GrpcReq{}, grpcReqFields{}, ""},
		{"zero response", GrpcResp{}, grpcRespFields{}, ""},
	} {
		got, err := bson.Marshal(c.codec)
		if err != nil {
			t.Fatalf("%s: marshal: %v", c.what, err)
		}
		def, err := bson.Marshal(c.plain)
		if err != nil {
			t.Fatalf("%s: default marshal: %v", c.what, err)
		}
		gotEl, err := bson.Raw(got).Elements()
		if err != nil {
			t.Fatal(err)
		}
		defEl, err := bson.Raw(def).Elements()
		if err != nil {
			t.Fatal(err)
		}
		if len(gotEl) != len(defEl) {
			t.Fatalf("%s: %d keys, the default has %d:\n%s\n%s", c.what, len(gotEl), len(defEl), bson.Raw(got), bson.Raw(def))
		}
		for i := range gotEl {
			if gotEl[i].Key() != defEl[i].Key() {
				t.Errorf("%s: key %d is %q, the default's is %q", c.what, i, gotEl[i].Key(), defEl[i].Key())
				continue
			}
			if gotEl[i].Key() == "timestamp" {
				if defEl[i].Value().Type != bson.TypeDateTime {
					t.Errorf("%s: the default timestamp is %v, not a date", c.what, defEl[i].Value().Type)
				}
				if s, ok := gotEl[i].Value().StringValueOK(); !ok || s != c.wantTS {
					t.Errorf("%s: timestamp stored as %v, want the text %q", c.what, gotEl[i].Value(), c.wantTS)
				}
				continue
			}
		}
		// The values, as maps all the way down: a Go map's keys are encoded
		// in no fixed order.
		gotM, defM := asMaps(t, got), asMaps(t, def)
		delete(gotM, "timestamp")
		delete(defM, "timestamp")
		if !reflect.DeepEqual(gotM, defM) {
			t.Errorf("%s: stored as %v, the default as %v", c.what, gotM, defM)
		}
	}
}

func asMaps(t *testing.T, doc []byte) bson.M {
	t.Helper()
	dec := bson.NewDecoder(bson.NewDocumentReader(bytes.NewReader(doc)))
	dec.DefaultDocumentM()
	var m bson.M
	if err := dec.Decode(&m); err != nil {
		t.Fatal(err)
	}
	return m
}

// A row stored before the codec, by the driver's default (a BSON date), reads
// as it read then: at the millisecond, in UTC, every other field kept.
func TestGrpcBSONReadsARowStoredAsADate(t *testing.T) {
	req, resp := bsonGrpcReq(), bsonGrpcResp()
	oldReq, err := bson.Marshal(grpcReqFields(req))
	if err != nil {
		t.Fatal(err)
	}
	oldResp, err := bson.Marshal(grpcRespFields(resp))
	if err != nil {
		t.Fatal(err)
	}

	var gotReq GrpcReq
	if err := bson.Unmarshal(oldReq, &gotReq); err != nil {
		t.Fatalf("request stored as a date: %v", err)
	}
	var gotResp GrpcResp
	if err := bson.Unmarshal(oldResp, &gotResp); err != nil {
		t.Fatalf("response stored as a date: %v", err)
	}
	for _, c := range []struct {
		what      string
		got, want time.Time
	}{
		{"request", gotReq.Timestamp, req.Timestamp.Truncate(time.Millisecond).UTC()},
		{"response", gotResp.Timestamp, resp.Timestamp.Truncate(time.Millisecond).UTC()},
	} {
		if !c.got.Equal(c.want) || c.got.Location() != time.UTC {
			t.Errorf("%s stored as a date read back as %s (%v), want %s in UTC", c.what, c.got.Format(time.RFC3339Nano), c.got.Location(), c.want.Format(time.RFC3339Nano))
		}
	}
	if !reflect.DeepEqual(reqWithoutTime(gotReq), reqWithoutTime(req)) {
		t.Errorf("request fields: got %+v, want %+v", reqWithoutTime(gotReq), reqWithoutTime(req))
	}
	if !reflect.DeepEqual(respWithoutTime(gotResp), respWithoutTime(resp)) {
		t.Errorf("response fields: got %+v, want %+v", respWithoutTime(gotResp), respWithoutTime(resp))
	}

	// A zero time stored as a date (year 1) is still zero.
	zero, err := bson.Marshal(grpcReqFields{})
	if err != nil {
		t.Fatal(err)
	}
	var gotZero GrpcReq
	if err := bson.Unmarshal(zero, &gotZero); err != nil {
		t.Fatal(err)
	}
	if !gotZero.Timestamp.IsZero() {
		t.Errorf("a zero time stored as a date read back as %s", gotZero.Timestamp)
	}
}

func TestGrpcBSONZeroTimestamp(t *testing.T) {
	for _, v := range []interface{}{GrpcReq{}, GrpcResp{}} {
		data, err := bson.Marshal(v)
		if err != nil {
			t.Fatal(err)
		}
		var req GrpcReq
		var resp GrpcResp
		if err := bson.Unmarshal(data, &req); err != nil {
			t.Fatal(err)
		}
		if err := bson.Unmarshal(data, &resp); err != nil {
			t.Fatal(err)
		}
		if !req.Timestamp.IsZero() || !resp.Timestamp.IsZero() {
			t.Errorf("zero time read back as %s / %s", req.Timestamp, resp.Timestamp)
		}
	}
}

// A timestamp that is neither text keploy wrote nor a date fails the read,
// and so does a field the type cannot hold, rather than reading as no time
// or an empty message.
func TestGrpcBSONRefusesWhatItCannotRead(t *testing.T) {
	for _, c := range []struct {
		name      string
		doc       bson.D
		timestamp bool // the error names the timestamp
	}{
		{"text that is no time", bson.D{{Key: "timestamp", Value: "11/07/2026 10:15"}}, true},
		{"a number for a time", bson.D{{Key: "timestamp", Value: int32(7)}}, true},
		{"text for a message", bson.D{{Key: "body", Value: "abc"}, {Key: "timestamp", Value: "2026-11-07T10:15:30Z"}}, false},
	} {
		data, err := bson.Marshal(c.doc)
		if err != nil {
			t.Fatal(err)
		}
		var req GrpcReq
		if err := bson.Unmarshal(data, &req); err == nil {
			t.Errorf("request with %s: read with no error, as %+v", c.name, req)
		} else if c.timestamp && !strings.Contains(err.Error(), "GrpcReq.UnmarshalBSON timestamp") {
			t.Errorf("request with %s: error %q does not name the timestamp", c.name, err)
		}
		var resp GrpcResp
		if err := bson.Unmarshal(data, &resp); err == nil {
			t.Errorf("response with %s: read with no error, as %+v", c.name, resp)
		} else if c.timestamp && !strings.Contains(err.Error(), "GrpcResp.UnmarshalBSON timestamp") {
			t.Errorf("response with %s: error %q does not name the timestamp", c.name, err)
		}
	}
}

// A stored request or response that is null (or undefined) reads as the zero
// value, as the driver's default does for a struct, and the rest of the
// document still reads. The driver hands UnmarshalBSON no bytes for it.
// The targets start filled, so a read that leaves the old value fails.
func TestBSONReadsANullRequestOrResponseAsZero(t *testing.T) {
	for _, empty := range []struct {
		name  string
		value interface{}
	}{{"null", nil}, {"undefined", bson.Undefined{}}} {
		t.Run(empty.name, func(t *testing.T) {
			filled := func() (TestCase, TestResult) {
				httpReq := HTTPReq{Method: "GET", URL: "http://x/", Timestamp: bsonGrpcReq().Timestamp}
				httpResp := HTTPResp{StatusCode: 200, Body: "ok", Timestamp: bsonGrpcResp().Timestamp}
				return TestCase{Name: "old", HTTPReq: httpReq, HTTPResp: httpResp, GrpcReq: bsonGrpcReq(), GrpcResp: bsonGrpcResp()},
					TestResult{Name: "old", Req: httpReq, Res: httpResp, GrpcReq: bsonGrpcReq(), GrpcRes: bsonGrpcResp()}
			}

			caseDoc, err := bson.Marshal(bson.D{
				{Key: "name", Value: "n"},
				{Key: "http_req", Value: empty.value},
				{Key: "http_resp", Value: empty.value},
				{Key: "grpcReq", Value: empty.value},
				{Key: "grpcResp", Value: empty.value},
			})
			if err != nil {
				t.Fatal(err)
			}
			tc, _ := filled()
			if err := bson.Unmarshal(caseDoc, &tc); err != nil {
				t.Fatalf("test case with its request and response %s: %v", empty.name, err)
			}
			if !reflect.DeepEqual(tc, TestCase{Name: "n"}) {
				t.Errorf("test case with its request and response %s read as %+v, want only its name", empty.name, tc)
			}

			resultDoc, err := bson.Marshal(bson.D{
				{Key: "name", Value: "n"},
				{Key: "req", Value: empty.value},
				{Key: "res", Value: empty.value},
				{Key: "grpcreq", Value: empty.value},
				{Key: "grpcres", Value: empty.value},
			})
			if err != nil {
				t.Fatal(err)
			}
			_, tr := filled()
			if err := bson.Unmarshal(resultDoc, &tr); err != nil {
				t.Fatalf("test result with its request and response %s: %v", empty.name, err)
			}
			if !reflect.DeepEqual(tr, TestResult{Name: "n"}) {
				t.Errorf("test result with its request and response %s read as %+v, want only its name", empty.name, tr)
			}
		})
	}
}