Skip to content

Commit 178b1c7

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 fcdfb84 commit 178b1c7

22 files changed

Lines changed: 376 additions & 423 deletions

mongorestore/invalid_input_test.go

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

0 commit comments

Comments
 (0)