// Copyright (C) MongoDB, Inc. 2017-present. // // 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 package unified import ( "context" "encoding/hex" "testing" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/bsontype" "go.mongodb.org/mongo-driver/internal/testutil/assert" "go.mongodb.org/mongo-driver/x/bsonx/bsoncore" ) func TestMatches(t *testing.T) { ctx := context.Background() unmarshalExtJSONValue := func(t *testing.T, str string) bson.RawValue { t.Helper() if str == "" { return bson.RawValue{} } var val bson.RawValue err := bson.UnmarshalExtJSON([]byte(str), false, &val) assert.Nil(t, err, "UnmarshalExtJSON error: %v", err) return val } marshalValue := func(t *testing.T, val interface{}) bson.RawValue { t.Helper() valType, data, err := bson.MarshalValue(val) assert.Nil(t, err, "MarshalValue error: %v", err) return bson.RawValue{ Type: valType, Value: data, } } assertMatches := func(t *testing.T, expected, actual bson.RawValue, shouldMatch bool) { t.Helper() err := verifyValuesMatch(ctx, expected, actual, true) if shouldMatch { assert.Nil(t, err, "expected values to match, but got comparison error %v", err) return } assert.NotNil(t, err, "expected values to not match, but got no error") } t.Run("documents with extra keys allowed", func(t *testing.T) { expectedDoc := `{"x": 1, "y": {"a": 1, "b": 2}}` expectedVal := unmarshalExtJSONValue(t, expectedDoc) extraKeysAtRoot := `{"x": 1, "y": {"a": 1, "b": 2}, "z": 3}` extraKeysInEmbedded := `{"x": 1, "y": {"a": 1, "b": 2, "c": 3}}` testCases := []struct { name string actual string matches bool }{ {"exact match", expectedDoc, true}, {"extra keys allowed at root-level", extraKeysAtRoot, true}, {"incorrect type for y", `{"x": 1, "y": 2}`, false}, {"extra keys prohibited in embedded documents", extraKeysInEmbedded, false}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { assertMatches(t, expectedVal, unmarshalExtJSONValue(t, tc.actual), tc.matches) }) } }) t.Run("documents with extra keys not allowed", func(t *testing.T) { expected := unmarshalExtJSONValue(t, `{"x": 1}`) actual := unmarshalExtJSONValue(t, `{"x": 1, "y": 1}`) err := verifyValuesMatch(ctx, expected, actual, false) assert.NotNil(t, err, "expected values to not match, but got no error") }) t.Run("exists operator", func(t *testing.T) { rootExists := unmarshalExtJSONValue(t, `{"x": {"$$exists": true}}`) rootNotExists := unmarshalExtJSONValue(t, `{"x": {"$$exists": false}}`) embeddedExists := unmarshalExtJSONValue(t, `{"x": {"y": {"$$exists": true}}}`) embeddedNotExists := unmarshalExtJSONValue(t, `{"x": {"y": {"$$exists": false}}}`) testCases := []struct { name string expected bson.RawValue actual string matches bool }{ {"root - should exist and does", rootExists, `{"x": 1}`, true}, {"root - should exist and does not", rootExists, `{"y": 1}`, false}, {"root - should not exist and does", rootNotExists, `{"x": 1}`, false}, {"root - should not exist and does not", rootNotExists, `{"y": 1}`, true}, {"embedded - should exist and does", embeddedExists, `{"x": {"y": 1}}`, true}, {"embedded - should exist and does not", embeddedExists, `{"x": {}}`, false}, {"embedded - should not exist and does", embeddedNotExists, `{"x": {"y": 1}}`, false}, {"embedded - should not exist and does not", embeddedNotExists, `{"x": {}}`, true}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { assertMatches(t, tc.expected, unmarshalExtJSONValue(t, tc.actual), tc.matches) }) } }) t.Run("type operator", func(t *testing.T) { singleType := unmarshalExtJSONValue(t, `{"x": {"$$type": "string"}}`) arrayTypes := unmarshalExtJSONValue(t, `{"x": {"$$type": ["string", "bool"]}}`) testCases := []struct { name string expected bson.RawValue actual string matches bool }{ {"single type matches", singleType, `{"x": "foo"}`, true}, {"single type does not match", singleType, `{"x": 1}`, false}, {"multiple types matches first", arrayTypes, `{"x": "foo"}`, true}, {"multiple types matches second", arrayTypes, `{"x": true}`, true}, {"multiple types does not match", arrayTypes, `{"x": 1}`, false}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { assertMatches(t, tc.expected, unmarshalExtJSONValue(t, tc.actual), tc.matches) }) } }) t.Run("matchesHexBytes operator", func(t *testing.T) { singleValue := unmarshalExtJSONValue(t, `{"$$matchesHexBytes": "DEADBEEF"}`) document := unmarshalExtJSONValue(t, `{"x": {"$$matchesHexBytes": "DEADBEEF"}}`) stringToBinary := func(str string) bson.RawValue { hexBytes, err := hex.DecodeString(str) assert.Nil(t, err, "hex.DecodeString error: %v", err) return bson.RawValue{ Type: bsontype.Binary, Value: bsoncore.AppendBinary(nil, 0, hexBytes), } } testCases := []struct { name string expected bson.RawValue actual bson.RawValue matches bool }{ {"single value matches", singleValue, stringToBinary("DEADBEEF"), true}, {"single value does not match", singleValue, stringToBinary("BEEF"), false}, {"document matches", document, marshalValue(t, bson.M{"x": stringToBinary("DEADBEEF")}), true}, {"document does not match", document, marshalValue(t, bson.M{"x": stringToBinary("BEEF")}), false}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { assertMatches(t, tc.expected, tc.actual, tc.matches) }) } }) t.Run("unsetOrMatches operator", func(t *testing.T) { topLevel := unmarshalExtJSONValue(t, `{"$$unsetOrMatches": {"x": 1}}`) nested := unmarshalExtJSONValue(t, `{"x": {"$$unsetOrMatches": {"y": 1}}}`) testCases := []struct { name string expected bson.RawValue actual string matches bool }{ {"top-level unset", topLevel, "", true}, {"top-level matches", topLevel, `{"x": 1}`, true}, {"top-level matches with extra keys", topLevel, `{"x": 1, "y": 1}`, true}, {"top-level does not match", topLevel, `{"x": 2}`, false}, {"nested unset", nested, `{}`, true}, {"nested matches", nested, `{"x": {"y": 1}}`, true}, {"nested field exists but is null", nested, `{"x": null}`, false}, // null should not be considered unset {"nested does not match", nested, `{"x": {"y": 2}}`, false}, {"nested does not match due to extra keys", nested, `{"x": {"y": 1, "z": 1}}`, false}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { assertMatches(t, tc.expected, unmarshalExtJSONValue(t, tc.actual), tc.matches) }) } }) }