Skip to content

Commit ae94bd6

Browse files
committed
TOOLS-4263 Convert mongorestore invalid-input tests to Go
This adds a new `TestRestoreInvalidInput` test in `mongorestore/invalid_input_test.go`. This covers a number of cases for mongorestore's handling of invalid input options. It replaces many JS tests: - bad_options.js -> "option validation" subtests: --objcheck with --noobjcheck, malformed --oplogLimit, negative --writeConcern, invalid --db, invalid --collection. (The -v verbosity case is omitted: it exercises shared common/options code, not mongorestore.) - missing_dump.js -> "missing dump target" subtests (missing dir, missing dir with --db, missing bson file with --collection) - invalid_dump_target.js -> "invalid dump target" subtests (file where a dir is expected, with and without --db; dir where a bson file is expected) - malformed_bson.js -> "malformed bson file errors" - malformed_metadata.js -> "malformed metadata file errors" - invalid_metadata.js -> "invalid index in metadata errors" - blank_collection_bson.js -> "blank collection bson" subtests (with and without a metadata file) - blank_db.js -> "blank db directory succeeds" - objcheck_valid_bson.js -> "objcheck succeeds on valid bson" - oplog_replay_no_oplog.js -> "oplogReplay with no oplog file errors"
1 parent eef4eab commit ae94bd6

22 files changed

Lines changed: 339 additions & 423 deletions

mongorestore/invalid_input_test.go

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
// Copyright (C) MongoDB, Inc. 2014-present.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
4+
// not use this file except in compliance with the License. You may obtain
5+
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
7+
package mongorestore
8+
9+
import (
10+
"os"
11+
"path/filepath"
12+
"testing"
13+
14+
"github.com/mongodb/mongo-tools/common/testtype"
15+
"github.com/mongodb/mongo-tools/common/testutil"
16+
"github.com/stretchr/testify/assert"
17+
"github.com/stretchr/testify/require"
18+
"go.mongodb.org/mongo-driver/v2/bson"
19+
)
20+
21+
// TestRestoreInvalidInput consolidates the mongorestore invalid-input and
22+
// error-path coverage that previously lived in the qa-tests JS suite
23+
// (bad_options.js, missing_dump.js, invalid_dump_target.js, malformed_bson.js,
24+
// malformed_metadata.js, invalid_metadata.js, blank_collection_bson.js,
25+
// blank_db.js, objcheck_valid_bson.js, oplog_replay_no_oplog.js). Each case
26+
// asserts on the error returned by option validation or by Restore(), rather
27+
// than on a process exit code.
28+
//
29+
// bad_options.js's invalid-verbosity case (-v torvalds) is intentionally not
30+
// converted: verbosity parsing lives in the shared common/options package, not
31+
// in mongorestore, so it is out of scope for these restore-specific tests.
32+
func TestRestoreInvalidInput(t *testing.T) {
33+
testtype.SkipUnlessTestType(t, testtype.IntegrationTestType)
34+
35+
t.Run("option validation", func(t *testing.T) {
36+
// --noobjcheck was removed when the tools were rewritten in Go, so
37+
// passing it alongside --objcheck is now rejected as an unknown flag.
38+
// The JS test's intent (the two flags cannot be combined) still holds.
39+
t.Run("--objcheck with --noobjcheck is rejected", func(t *testing.T) {
40+
_, err := getRestoreWithArgs(ObjcheckOption, "--noobjcheck", t.TempDir())
41+
require.ErrorContains(
42+
t, err, "noobjcheck",
43+
"combining --objcheck with the removed --noobjcheck flag is rejected",
44+
)
45+
})
46+
47+
t.Run("negative write concern is rejected", func(t *testing.T) {
48+
_, err := getRestoreWithArgs(WriteConcernOption+"=-1", t.TempDir())
49+
require.ErrorContains(
50+
t, err, "invalid 'w' argument",
51+
"a negative --writeConcern value is rejected",
52+
)
53+
})
54+
55+
t.Run("malformed --oplogLimit timestamp is rejected", func(t *testing.T) {
56+
restore, err := getRestoreWithArgs(
57+
OplogReplayOption,
58+
OplogLimitOption,
59+
"xxx",
60+
t.TempDir(),
61+
)
62+
require.NoError(t, err, "building the restore instance")
63+
defer restore.Close()
64+
require.ErrorContains(
65+
t, restore.ParseAndValidateOptions(),
66+
"error parsing timestamp argument to --oplogLimit",
67+
"a non-timestamp --oplogLimit value is rejected",
68+
)
69+
})
70+
71+
t.Run("invalid db name is rejected", func(t *testing.T) {
72+
restore, err := getRestoreWithArgs(DBOption, "billy.crystal", t.TempDir())
73+
require.NoError(t, err, "building the restore instance")
74+
defer restore.Close()
75+
require.ErrorContains(
76+
t, restore.ParseAndValidateOptions(), "invalid db name",
77+
"a db name containing an illegal character is rejected",
78+
)
79+
})
80+
81+
t.Run("invalid collection name is rejected", func(t *testing.T) {
82+
bsonFile := filepath.Join(t.TempDir(), "coll.bson")
83+
writeBSONCollectionFile(t, bsonFile)
84+
restore, err := getRestoreWithArgs(
85+
DBOption,
86+
"test",
87+
CollectionOption,
88+
"$money",
89+
bsonFile,
90+
)
91+
require.NoError(t, err, "building the restore instance")
92+
defer restore.Close()
93+
require.ErrorContains(
94+
t, restore.ParseAndValidateOptions(), "invalid collection name",
95+
"a collection name containing an illegal character is rejected",
96+
)
97+
})
98+
})
99+
100+
t.Run("missing dump target", func(t *testing.T) {
101+
missing := filepath.Join(t.TempDir(), "does-not-exist")
102+
103+
t.Run("missing dump directory errors", func(t *testing.T) {
104+
result := restoreFromArgs(t, missing)
105+
require.ErrorContains(
106+
t, result.Err, "invalid",
107+
"restoring from a nonexistent directory errors",
108+
)
109+
})
110+
111+
t.Run("missing dump directory with --db errors", func(t *testing.T) {
112+
result := restoreFromArgs(t, DBOption, "test", missing)
113+
require.ErrorContains(
114+
t, result.Err, "invalid",
115+
"restoring from a nonexistent directory with --db errors",
116+
)
117+
})
118+
119+
t.Run("missing bson file with --collection errors", func(t *testing.T) {
120+
missingFile := filepath.Join(t.TempDir(), "missing.bson")
121+
result := restoreFromArgs(t, DBOption, "test", CollectionOption, "data", missingFile)
122+
require.ErrorContains(
123+
t, result.Err, "invalid",
124+
"restoring from a nonexistent bson file errors",
125+
)
126+
})
127+
})
128+
129+
t.Run("invalid dump target", func(t *testing.T) {
130+
// A plain (non-.bson) file where a dump directory is expected.
131+
fileTarget := filepath.Join(t.TempDir(), "README")
132+
require.NoError(
133+
t,
134+
os.WriteFile(fileTarget, []byte("not a dump"), 0644),
135+
"writing file target",
136+
)
137+
138+
t.Run("file instead of directory errors", func(t *testing.T) {
139+
result := restoreFromArgs(t, fileTarget)
140+
require.ErrorContains(
141+
t, result.Err, "does not have .bson extension",
142+
"restoring with a file where a directory is expected errors",
143+
)
144+
})
145+
146+
t.Run("file instead of directory with --db errors", func(t *testing.T) {
147+
result := restoreFromArgs(t, DBOption, "test", fileTarget)
148+
require.ErrorContains(
149+
t, result.Err, "does not have .bson extension",
150+
"restoring with a file where a db directory is expected errors",
151+
)
152+
})
153+
154+
t.Run("directory instead of bson file with --collection errors", func(t *testing.T) {
155+
dirTarget := t.TempDir()
156+
result := restoreFromArgs(t, DBOption, "test", CollectionOption, "blank", dirTarget)
157+
require.ErrorContains(
158+
t, result.Err, "is a directory, not a bson file",
159+
"restoring with a directory where a bson file is expected errors",
160+
)
161+
})
162+
})
163+
164+
t.Run("malformed bson file errors", func(t *testing.T) {
165+
dir := t.TempDir()
166+
bsonFile := filepath.Join(dir, "malformed_coll.bson")
167+
require.NoError(
168+
t, os.WriteFile(bsonFile, []byte("this is not valid bson at all"), 0644),
169+
"writing malformed bson fixture",
170+
)
171+
result := restoreFromArgs(
172+
t,
173+
DBOption,
174+
"dbOne",
175+
CollectionOption,
176+
"malformed_coll",
177+
bsonFile,
178+
)
179+
require.ErrorContains(
180+
t, result.Err, "reading bson input",
181+
"restoring a malformed bson file errors",
182+
)
183+
})
184+
185+
t.Run("malformed metadata file errors", func(t *testing.T) {
186+
dir := t.TempDir()
187+
bsonFile := filepath.Join(dir, "coll.bson")
188+
writeBSONCollectionFile(t, bsonFile, bson.M{"_id": 1})
189+
require.NoError(
190+
t,
191+
os.WriteFile(
192+
filepath.Join(dir, "coll.metadata.json"),
193+
[]byte("{ this is not json"),
194+
0644,
195+
),
196+
"writing malformed metadata fixture",
197+
)
198+
result := restoreFromArgs(t, DBOption, "dbOne", CollectionOption, "coll", bsonFile)
199+
require.ErrorContains(
200+
t, result.Err, "error parsing metadata",
201+
"restoring with a syntactically invalid metadata file errors",
202+
)
203+
})
204+
205+
t.Run("invalid index in metadata errors", func(t *testing.T) {
206+
dir := t.TempDir()
207+
bsonFile := filepath.Join(dir, "coll.bson")
208+
writeBSONCollectionFile(t, bsonFile, bson.M{"_id": 1})
209+
// Well-formed JSON, but an index spec that the server will reject
210+
// (empty key document).
211+
metadata := `{"options":{},"indexes":[{"v":2,"key":{},"name":"bad_index"}]}`
212+
require.NoError(
213+
t, os.WriteFile(filepath.Join(dir, "coll.metadata.json"), []byte(metadata), 0644),
214+
"writing invalid-index metadata fixture",
215+
)
216+
result := restoreFromArgs(t, DBOption, "dbOne", CollectionOption, "coll", bsonFile)
217+
require.Error(t, result.Err, "restoring metadata with an invalid index spec errors")
218+
})
219+
220+
t.Run("blank collection bson succeeds with no inserts", func(t *testing.T) {
221+
client, err := testutil.GetBareSession()
222+
require.NoError(t, err, "connecting to the test server")
223+
t.Cleanup(func() { _ = client.Database("test").Drop(t.Context()) })
224+
225+
t.Run("without a metadata file", func(t *testing.T) {
226+
bsonFile := filepath.Join(t.TempDir(), "blank.bson")
227+
writeBSONCollectionFile(t, bsonFile)
228+
result := restoreFromArgs(t, DBOption, "test", CollectionOption, "blank", bsonFile)
229+
require.NoError(t, result.Err, "restoring a blank collection file succeeds")
230+
231+
count, err := client.Database("test").
232+
Collection("blank").
233+
CountDocuments(t.Context(), bson.M{})
234+
require.NoError(t, err, "counting restored documents")
235+
assert.EqualValues(t, 0, count, "a blank collection file inserts nothing")
236+
})
237+
238+
t.Run("with a metadata file", func(t *testing.T) {
239+
dir := t.TempDir()
240+
bsonFile := filepath.Join(dir, "blank.bson")
241+
writeBSONCollectionFile(t, bsonFile)
242+
require.NoError(
243+
t,
244+
os.WriteFile(
245+
filepath.Join(dir, "blank.metadata.json"),
246+
[]byte(`{"options":{},"indexes":[]}`),
247+
0644,
248+
),
249+
"writing empty metadata fixture",
250+
)
251+
result := restoreFromArgs(t, DBOption, "test", CollectionOption, "blank", bsonFile)
252+
require.NoError(
253+
t,
254+
result.Err,
255+
"restoring a blank collection file with metadata succeeds",
256+
)
257+
258+
count, err := client.Database("test").
259+
Collection("blank").
260+
CountDocuments(t.Context(), bson.M{})
261+
require.NoError(t, err, "counting restored documents")
262+
assert.EqualValues(t, 0, count, "a blank collection file with metadata inserts nothing")
263+
})
264+
})
265+
266+
t.Run("blank db directory succeeds", func(t *testing.T) {
267+
result := restoreFromArgs(t, DBOption, "test", t.TempDir())
268+
require.NoError(t, result.Err, "restoring from an empty db directory succeeds")
269+
assert.EqualValues(t, 0, result.Successes, "an empty db directory inserts nothing")
270+
})
271+
272+
t.Run("objcheck succeeds on valid bson", func(t *testing.T) {
273+
client, err := testutil.GetBareSession()
274+
require.NoError(t, err, "connecting to the test server")
275+
t.Cleanup(func() { _ = client.Database("test").Drop(t.Context()) })
276+
require.NoError(
277+
t,
278+
client.Database("test").Collection("coll").Drop(t.Context()),
279+
"dropping target collection",
280+
)
281+
282+
const numDocs = 50
283+
docs := make([]bson.M, numDocs)
284+
for i := range docs {
285+
docs[i] = bson.M{"_id": i}
286+
}
287+
bsonFile := filepath.Join(t.TempDir(), "coll.bson")
288+
writeBSONCollectionFile(t, bsonFile, docs...)
289+
290+
result := restoreFromArgs(
291+
t,
292+
ObjcheckOption,
293+
DBOption,
294+
"test",
295+
CollectionOption,
296+
"coll",
297+
bsonFile,
298+
)
299+
require.NoError(t, result.Err, "restoring valid bson with --objcheck succeeds")
300+
301+
count, err := client.Database("test").
302+
Collection("coll").
303+
CountDocuments(t.Context(), bson.M{})
304+
require.NoError(t, err, "counting restored documents")
305+
assert.EqualValues(t, numDocs, count, "all documents are restored with --objcheck")
306+
})
307+
308+
t.Run("oplogReplay with no oplog file errors", func(t *testing.T) {
309+
result := restoreFromArgs(t, OplogReplayOption, t.TempDir())
310+
require.ErrorContains(
311+
t, result.Err, "no oplog file to replay",
312+
"--oplogReplay against a dump with no oplog.bson errors",
313+
)
314+
})
315+
}
316+
317+
// restoreFromArgs builds a MongoRestore from the given args, runs it, and
318+
// returns the result. The instance is closed when the test finishes.
319+
func restoreFromArgs(t *testing.T, args ...string) Result {
320+
t.Helper()
321+
restore, err := getRestoreWithArgs(args...)
322+
require.NoError(t, err, "building the restore instance")
323+
t.Cleanup(restore.Close)
324+
return restore.Restore()
325+
}
326+
327+
// writeBSONCollectionFile writes the given documents to path in the
328+
// concatenated-BSON format that mongodump produces for a collection (an empty
329+
// docs list produces a zero-byte file, i.e. a blank collection).
330+
func writeBSONCollectionFile(t *testing.T, path string, docs ...bson.M) {
331+
t.Helper()
332+
var buf []byte
333+
for _, doc := range docs {
334+
marshaled, err := bson.Marshal(doc)
335+
require.NoError(t, err, "marshaling fixture document")
336+
buf = append(buf, marshaled...)
337+
}
338+
require.NoError(t, os.WriteFile(path, buf, 0644), "writing bson fixture file")
339+
}

0 commit comments

Comments
 (0)