473 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			473 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (C) MongoDB, Inc. 2024-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 bson
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/binary"
 | |
| 	"errors"
 | |
| 	"io"
 | |
| 	"testing"
 | |
| 
 | |
| 	"gitea.psichedelico.com/go/bson/internal/assert"
 | |
| 	"gitea.psichedelico.com/go/bson/internal/require"
 | |
| 	"gitea.psichedelico.com/go/bson/x/bsonx/bsoncore"
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| )
 | |
| 
 | |
| func TestReadArray(t *testing.T) {
 | |
| 	t.Parallel()
 | |
| 
 | |
| 	testCases := []struct {
 | |
| 		name     string
 | |
| 		ioReader io.Reader
 | |
| 		arr      RawArray
 | |
| 		err      error
 | |
| 	}{
 | |
| 		{
 | |
| 			"nil reader",
 | |
| 			nil,
 | |
| 			nil,
 | |
| 			ErrNilReader,
 | |
| 		},
 | |
| 		{
 | |
| 			"premature end of reader",
 | |
| 			bytes.NewBuffer([]byte{}),
 | |
| 			nil,
 | |
| 			io.EOF,
 | |
| 		},
 | |
| 		{
 | |
| 			"empty Array",
 | |
| 			bytes.NewBuffer([]byte{5, 0, 0, 0, 0}),
 | |
| 			[]byte{5, 0, 0, 0, 0},
 | |
| 			nil,
 | |
| 		},
 | |
| 		{
 | |
| 			"non-empty Array",
 | |
| 			bytes.NewBuffer([]byte{
 | |
| 				'\x1B', '\x00', '\x00', '\x00',
 | |
| 				'\x02',
 | |
| 				'0', '\x00',
 | |
| 				'\x04', '\x00', '\x00', '\x00',
 | |
| 				'\x62', '\x61', '\x72', '\x00',
 | |
| 				'\x02',
 | |
| 				'1', '\x00',
 | |
| 				'\x04', '\x00', '\x00', '\x00',
 | |
| 				'\x62', '\x61', '\x7a', '\x00',
 | |
| 				'\x00',
 | |
| 			}),
 | |
| 			[]byte{
 | |
| 				'\x1B', '\x00', '\x00', '\x00',
 | |
| 				'\x02',
 | |
| 				'0', '\x00',
 | |
| 				'\x04', '\x00', '\x00', '\x00',
 | |
| 				'\x62', '\x61', '\x72', '\x00',
 | |
| 				'\x02',
 | |
| 				'1', '\x00',
 | |
| 				'\x04', '\x00', '\x00', '\x00',
 | |
| 				'\x62', '\x61', '\x7a', '\x00',
 | |
| 				'\x00',
 | |
| 			},
 | |
| 			nil,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range testCases {
 | |
| 		tc := tc // Capture range variable
 | |
| 
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			arr, err := ReadArray(tc.ioReader)
 | |
| 			if !assert.CompareErrors(err, tc.err) {
 | |
| 				t.Errorf("errors do not match. got %v; want %v", err, tc.err)
 | |
| 			}
 | |
| 			if !bytes.Equal(tc.arr, arr) {
 | |
| 				t.Errorf("Arrays differ. got %v; want %v", tc.arr, arr)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestArray_Validate(t *testing.T) {
 | |
| 	t.Parallel()
 | |
| 
 | |
| 	t.Run("TooShort", func(t *testing.T) {
 | |
| 		t.Parallel()
 | |
| 
 | |
| 		want := bsoncore.NewInsufficientBytesError(nil, nil)
 | |
| 		got := RawArray{'\x00', '\x00'}.Validate()
 | |
| 		if !assert.CompareErrors(got, want) {
 | |
| 			t.Errorf("Did not get expected error. got %v; want %v", got, want)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("InvalidLength", func(t *testing.T) {
 | |
| 		t.Parallel()
 | |
| 
 | |
| 		want := bsoncore.NewArrayLengthError(200, 5)
 | |
| 		r := make(RawArray, 5)
 | |
| 		binary.LittleEndian.PutUint32(r[0:4], 200)
 | |
| 		got := r.Validate()
 | |
| 		if !assert.CompareErrors(got, want) {
 | |
| 			t.Errorf("Did not get expected error. got %v; want %v", got, want)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("Invalid Element", func(t *testing.T) {
 | |
| 		t.Parallel()
 | |
| 
 | |
| 		want := bsoncore.NewInsufficientBytesError(nil, nil)
 | |
| 		r := make(RawArray, 7)
 | |
| 		binary.LittleEndian.PutUint32(r[0:4], 7)
 | |
| 		r[4], r[5], r[6] = 0x02, 'f', 0x00
 | |
| 		got := r.Validate()
 | |
| 		if !assert.CompareErrors(got, want) {
 | |
| 			t.Errorf("Did not get expected error. got %v; want %v", got, want)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("Missing Null Terminator", func(t *testing.T) {
 | |
| 		t.Parallel()
 | |
| 
 | |
| 		want := bsoncore.ErrMissingNull
 | |
| 		r := make(RawArray, 6)
 | |
| 		binary.LittleEndian.PutUint32(r[0:4], 6)
 | |
| 		r[4], r[5] = 0x0A, '0'
 | |
| 		got := r.Validate()
 | |
| 		if !assert.CompareErrors(got, want) {
 | |
| 			t.Errorf("Did not get expected error. got %v; want %v", got, want)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	testCases := []struct {
 | |
| 		name string
 | |
| 		r    RawArray
 | |
| 		want error
 | |
| 	}{
 | |
| 		{"array null", RawArray{'\x08', '\x00', '\x00', '\x00', '\x0A', '0', '\x00', '\x00'}, nil},
 | |
| 		{"array",
 | |
| 			RawArray{
 | |
| 				'\x1B', '\x00', '\x00', '\x00',
 | |
| 				'\x02',
 | |
| 				'0', '\x00',
 | |
| 				'\x04', '\x00', '\x00', '\x00',
 | |
| 				'\x62', '\x61', '\x72', '\x00',
 | |
| 				'\x02',
 | |
| 				'1', '\x00',
 | |
| 				'\x04', '\x00', '\x00', '\x00',
 | |
| 				'\x62', '\x61', '\x7a', '\x00',
 | |
| 				'\x00',
 | |
| 			},
 | |
| 			nil,
 | |
| 		},
 | |
| 		{"subarray",
 | |
| 			RawArray{
 | |
| 				'\x13', '\x00', '\x00', '\x00',
 | |
| 				'\x04',
 | |
| 				'0', '\x00',
 | |
| 				'\x0B', '\x00', '\x00', '\x00', '\x0A', '0', '\x00',
 | |
| 				'\x0A', '1', '\x00', '\x00', '\x00',
 | |
| 			},
 | |
| 			nil,
 | |
| 		},
 | |
| 		{"invalid key order",
 | |
| 			RawArray{
 | |
| 				'\x0B', '\x00', '\x00', '\x00', '\x0A', '2', '\x00',
 | |
| 				'\x0A', '0', '\x00', '\x00', '\x00',
 | |
| 			},
 | |
| 			errors.New(`array key "2" is out of order or invalid`),
 | |
| 		},
 | |
| 		{"invalid key type",
 | |
| 			RawArray{
 | |
| 				'\x0B', '\x00', '\x00', '\x00', '\x0A', 'p', '\x00',
 | |
| 				'\x0A', 'q', '\x00', '\x00', '\x00',
 | |
| 			},
 | |
| 			errors.New(`array key "p" is out of order or invalid`),
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range testCases {
 | |
| 		tc := tc // Capture the range variable
 | |
| 
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			got := tc.r.Validate()
 | |
| 			if !assert.CompareErrors(got, tc.want) {
 | |
| 				t.Errorf("Returned error does not match. got %v; want %v", got, tc.want)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestArray_Index(t *testing.T) {
 | |
| 	t.Parallel()
 | |
| 
 | |
| 	t.Run("Out of bounds", func(t *testing.T) {
 | |
| 		t.Parallel()
 | |
| 
 | |
| 		rdr := RawArray{0xe, 0x0, 0x0, 0x0, 0xa, '0', 0x0, 0xa, '1', 0x0, 0xa, 0x7a, 0x0, 0x0}
 | |
| 		_, err := rdr.IndexErr(3)
 | |
| 		if !errors.Is(err, bsoncore.ErrOutOfBounds) {
 | |
| 			t.Errorf("Out of bounds should be returned when accessing element beyond end of Array. got %v; want %v", err,
 | |
| 				bsoncore.ErrOutOfBounds)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	t.Run("Validation Error", func(t *testing.T) {
 | |
| 		t.Parallel()
 | |
| 
 | |
| 		rdr := RawArray{0x07, 0x00, 0x00, 0x00, 0x00}
 | |
| 		_, got := rdr.IndexErr(1)
 | |
| 		want := bsoncore.NewInsufficientBytesError(nil, nil)
 | |
| 		if !assert.CompareErrors(got, want) {
 | |
| 			t.Errorf("Did not receive expected error. got %v; want %v", got, want)
 | |
| 		}
 | |
| 	})
 | |
| 
 | |
| 	testArray := RawArray{
 | |
| 		'\x26', '\x00', '\x00', '\x00',
 | |
| 		'\x02',
 | |
| 		'0', '\x00',
 | |
| 		'\x04', '\x00', '\x00', '\x00',
 | |
| 		'\x62', '\x61', '\x72', '\x00',
 | |
| 		'\x02',
 | |
| 		'1', '\x00',
 | |
| 		'\x04', '\x00', '\x00', '\x00',
 | |
| 		'\x62', '\x61', '\x7a', '\x00',
 | |
| 		'\x02',
 | |
| 		'2', '\x00',
 | |
| 		'\x04', '\x00', '\x00', '\x00',
 | |
| 		'\x71', '\x75', '\x78', '\x00',
 | |
| 		'\x00',
 | |
| 	}
 | |
| 	testCases := []struct {
 | |
| 		name  string
 | |
| 		index uint
 | |
| 		want  RawValue
 | |
| 	}{
 | |
| 		{"first",
 | |
| 			0,
 | |
| 			RawValue{
 | |
| 				Type:  TypeString,
 | |
| 				Value: []byte{0x04, 0x00, 0x00, 0x00, 0x62, 0x61, 0x72, 0x00},
 | |
| 			},
 | |
| 		},
 | |
| 		{"second",
 | |
| 			1,
 | |
| 			RawValue{
 | |
| 				Type:  TypeString,
 | |
| 				Value: []byte{0x04, 0x00, 0x00, 0x00, 0x62, 0x61, 0x7a, 0x00},
 | |
| 			},
 | |
| 		},
 | |
| 		{"third",
 | |
| 			2,
 | |
| 			RawValue{
 | |
| 				Type:  TypeString,
 | |
| 				Value: []byte{0x04, 0x00, 0x00, 0x00, 0x71, 0x75, 0x78, 0x00},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range testCases {
 | |
| 		tc := tc // Capture the range variable
 | |
| 
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			t.Run("IndexErr", func(t *testing.T) {
 | |
| 				t.Parallel()
 | |
| 
 | |
| 				got, err := testArray.IndexErr(tc.index)
 | |
| 				if err != nil {
 | |
| 					t.Errorf("Unexpected error from IndexErr: %s", err)
 | |
| 				}
 | |
| 				if diff := cmp.Diff(got, tc.want); diff != "" {
 | |
| 					t.Errorf("Arrays differ: (-got +want)\n%s", diff)
 | |
| 				}
 | |
| 			})
 | |
| 
 | |
| 			t.Run("Index", func(t *testing.T) {
 | |
| 				t.Parallel()
 | |
| 
 | |
| 				defer func() {
 | |
| 					if err := recover(); err != nil {
 | |
| 						t.Errorf("Unexpected error: %v", err)
 | |
| 					}
 | |
| 				}()
 | |
| 				got := testArray.Index(tc.index)
 | |
| 				if diff := cmp.Diff(got, tc.want); diff != "" {
 | |
| 					t.Errorf("Arrays differ: (-got +want)\n%s", diff)
 | |
| 				}
 | |
| 			})
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRawArray_Stringt(t *testing.T) {
 | |
| 	t.Parallel()
 | |
| 
 | |
| 	testCases := []struct {
 | |
| 		name             string
 | |
| 		arr              RawArray
 | |
| 		arrayString      string
 | |
| 		arrayDebugString string
 | |
| 	}{
 | |
| 		{
 | |
| 			"array",
 | |
| 			RawArray{
 | |
| 				'\x1B', '\x00', '\x00', '\x00',
 | |
| 				'\x02',
 | |
| 				'0', '\x00',
 | |
| 				'\x04', '\x00', '\x00', '\x00',
 | |
| 				'\x62', '\x61', '\x72', '\x00',
 | |
| 				'\x02',
 | |
| 				'1', '\x00',
 | |
| 				'\x04', '\x00', '\x00', '\x00',
 | |
| 				'\x62', '\x61', '\x7a', '\x00',
 | |
| 				'\x00',
 | |
| 			},
 | |
| 			`["bar","baz"]`,
 | |
| 			`Array(27)["bar","baz"]`,
 | |
| 		},
 | |
| 		{
 | |
| 			"subarray",
 | |
| 			RawArray{
 | |
| 				'\x13', '\x00', '\x00', '\x00',
 | |
| 				'\x04',
 | |
| 				'0', '\x00',
 | |
| 				'\x0B', '\x00', '\x00', '\x00',
 | |
| 				'\x0A', '0', '\x00',
 | |
| 				'\x0A', '1', '\x00',
 | |
| 				'\x00', '\x00',
 | |
| 			},
 | |
| 			`[[null,null]]`,
 | |
| 			`Array(19)[Array(11)[null,null]]`,
 | |
| 		},
 | |
| 		{
 | |
| 			"malformed--length too small",
 | |
| 			RawArray{
 | |
| 				'\x04', '\x00', '\x00', '\x00',
 | |
| 				'\x00',
 | |
| 			},
 | |
| 			``,
 | |
| 			`Array(4)[]`,
 | |
| 		},
 | |
| 		{
 | |
| 			"malformed--length too large",
 | |
| 			RawArray{
 | |
| 				'\x13', '\x00', '\x00', '\x00',
 | |
| 				'\x00',
 | |
| 			},
 | |
| 			``,
 | |
| 			`Array(19)[<malformed (15)>]`,
 | |
| 		},
 | |
| 		{
 | |
| 			"malformed--missing null byte",
 | |
| 			RawArray{
 | |
| 				'\x06', '\x00', '\x00', '\x00',
 | |
| 				'\x02', '0',
 | |
| 			},
 | |
| 			``,
 | |
| 			`Array(6)[<malformed (2)>]`,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tc := range testCases {
 | |
| 		tc := tc // Capture the range variable
 | |
| 
 | |
| 		t.Run(tc.name, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			arrayString := tc.arr.String()
 | |
| 			if arrayString != tc.arrayString {
 | |
| 				t.Errorf("array strings do not match. got %q; want %q",
 | |
| 					arrayString, tc.arrayString)
 | |
| 			}
 | |
| 
 | |
| 			arrayDebugString := tc.arr.DebugString()
 | |
| 			if arrayDebugString != tc.arrayDebugString {
 | |
| 				t.Errorf("array debug strings do not match. got %q; want %q",
 | |
| 					arrayDebugString, tc.arrayDebugString)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestRawArray_Values(t *testing.T) {
 | |
| 	t.Parallel()
 | |
| 
 | |
| 	testCases := []struct {
 | |
| 		name string
 | |
| 		arr  RawArray
 | |
| 		want []RawValue
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "empty",
 | |
| 			arr:  []byte{0x05, 0x00, 0x00, 0x00, 0x00},
 | |
| 			want: []RawValue{},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "null document",
 | |
| 			arr:  []byte{0x07, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00}, // [{}]
 | |
| 			want: []RawValue{
 | |
| 				{
 | |
| 					Type:  TypeNull,
 | |
| 					Value: []byte{},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "same",
 | |
| 			arr: []byte{0x13, 0x00, 0x00, 0x00, 0x10, 0x30, 0x00, 0x01, 0x00, 0x00,
 | |
| 				0x00, 0x10, 0x31, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00}, // [int32(1), int32(2)]
 | |
| 			want: []RawValue{
 | |
| 				{ // int32(1)
 | |
| 					Type:  TypeInt32,
 | |
| 					Value: []byte{0x01, 0x00, 0x00, 0x00},
 | |
| 				},
 | |
| 				{ // int32(2)
 | |
| 					Type:  TypeInt32,
 | |
| 					Value: []byte{0x02, 0x00, 0x00, 0x00},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "mixed",
 | |
| 			arr: []byte{0x17, 0x00, 0x00, 0x00, 0x10, 0x30, 0x00, 0x01, 0x00, 0x00,
 | |
| 				0x00, 0x02, 0x31, 0x00, 0x04, 0x00, 0x00, 0x00, 0x66, 0x6F, 0x6F, 0x00, 0x00}, // [int32(1), "foo"]
 | |
| 			want: []RawValue{
 | |
| 				{ // int32(1)
 | |
| 					Type:  TypeInt32,
 | |
| 					Value: []byte{0x01, 0x00, 0x00, 0x00},
 | |
| 				},
 | |
| 				{ // "foo"
 | |
| 					Type:  TypeString,
 | |
| 					Value: []byte{0x04, 0x00, 0x00, 0x00, 0x66, 0x6F, 0x6F, 0x00},
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tcase := range testCases {
 | |
| 		tcase := tcase
 | |
| 
 | |
| 		t.Run(tcase.name, func(t *testing.T) {
 | |
| 			t.Parallel()
 | |
| 
 | |
| 			values, err := tcase.arr.Values()
 | |
| 			require.NoError(t, err, "failed to turn array into values")
 | |
| 			require.Len(t, values, len(tcase.want), "got len does not match want")
 | |
| 
 | |
| 			for idx, want := range tcase.want {
 | |
| 				assert.True(t, want.Equal(values[idx]), "want: %v, got: %v", want, values[idx])
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 |