Skip to content

Commit ea59312

Browse files
committed
[DAPS-1514] Add repository validation logic with tests (#1539)
* [DAPS-1514] Add repository validation logic with tests. Add minimal Result type for Rust-like error handling. Implement pure validation functions for repository fields. Add comprehensive unit tests for all validation functions. Register validation tests in CMake configuration * [DAPS-1514] Run prettier
1 parent bba2de6 commit ea59312

File tree

3 files changed

+580
-0
lines changed

3 files changed

+580
-0
lines changed

core/database/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ if( ENABLE_FOXX_TESTS )
1616
add_test(NAME foxx_authz COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_authz")
1717
add_test(NAME foxx_record COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_record")
1818
add_test(NAME foxx_repo COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_repo")
19+
add_test(NAME foxx_validation COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "Repository Validation Tests")
1920
add_test(NAME foxx_path COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_path")
2021
add_test(NAME foxx_db_fixtures COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_fixture_setup.sh")
2122
add_test(NAME foxx_version COMMAND "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_foxx.sh" -t "unit_version")
@@ -36,6 +37,7 @@ if( ENABLE_FOXX_TESTS )
3637
set_tests_properties(foxx_authz_router PROPERTIES FIXTURES_REQUIRED Foxx)
3738
set_tests_properties(foxx_record PROPERTIES FIXTURES_REQUIRED Foxx)
3839
set_tests_properties(foxx_repo PROPERTIES FIXTURES_REQUIRED Foxx)
40+
set_tests_properties(foxx_validation PROPERTIES FIXTURES_REQUIRED Foxx)
3941
set_tests_properties(foxx_path PROPERTIES FIXTURES_REQUIRED Foxx)
4042
set_tests_properties(foxx_user_router PROPERTIES FIXTURES_REQUIRED "Foxx;FoxxDBFixtures")
4143
set_tests_properties(foxx_unit_user_token PROPERTIES FIXTURES_REQUIRED Foxx)
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
"use strict";
2+
3+
const { Result } = require("./types");
4+
const g_lib = require("../support");
5+
6+
/**
7+
* Standalone validation functions following Rust patterns
8+
* Pure functions that return Result types for error handling
9+
*
10+
* See: https://doc.rust-lang.org/book/ch03-03-how-functions-work.html
11+
* Functions in Rust are expressions that can return values
12+
*
13+
* See: https://doc.rust-lang.org/book/ch09-00-error-handling.html
14+
* Rust emphasizes explicit error handling through Result types
15+
*/
16+
17+
// Validate that a value is a non-empty string
18+
// Reusable helper following DRY principle
19+
const validateNonEmptyString = (value, fieldName) => {
20+
if (!value || typeof value !== "string" || value.trim() === "") {
21+
return Result.err({
22+
code: g_lib.ERR_INVALID_PARAM,
23+
message: `${fieldName} is required and must be a non-empty string`,
24+
});
25+
}
26+
return Result.ok(true);
27+
};
28+
29+
// Validate common repository fields
30+
// Pure function - no side effects, deterministic output
31+
const validateCommonFields = (config) => {
32+
const errors = [];
33+
34+
const idValidation = validateNonEmptyString(config.id, "Repository ID");
35+
if (!idValidation.ok) {
36+
errors.push(idValidation.error.message);
37+
}
38+
39+
const titleValidation = validateNonEmptyString(config.title, "Repository title");
40+
if (!titleValidation.ok) {
41+
errors.push(titleValidation.error.message);
42+
}
43+
44+
if (typeof config.capacity !== "number" || config.capacity <= 0) {
45+
errors.push("Repository capacity must be a positive number");
46+
}
47+
48+
if (!Array.isArray(config.admins) || config.admins.length === 0) {
49+
errors.push("Repository must have at least one admin");
50+
}
51+
52+
if (errors.length > 0) {
53+
// See: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#propagating-errors
54+
// Early return with error - similar to Rust's ? operator
55+
return Result.err({
56+
code: g_lib.ERR_INVALID_PARAM,
57+
message: errors.join("; "),
58+
});
59+
}
60+
61+
return Result.ok(true);
62+
};
63+
64+
// Validate POSIX path format
65+
const validatePOSIXPath = (path, fieldName) => {
66+
if (!path || typeof path !== "string") {
67+
return Result.err({
68+
code: g_lib.ERR_INVALID_PARAM,
69+
message: `${fieldName} must be a non-empty string`,
70+
});
71+
}
72+
73+
if (!path.startsWith("/")) {
74+
return Result.err({
75+
code: g_lib.ERR_INVALID_PARAM,
76+
message: `${fieldName} must be an absolute path (start with '/')`,
77+
});
78+
}
79+
80+
// Check for invalid characters in path
81+
if (path.includes("..") || path.includes("//")) {
82+
return Result.err({
83+
code: g_lib.ERR_INVALID_PARAM,
84+
message: `${fieldName} contains invalid path sequences`,
85+
});
86+
}
87+
88+
return Result.ok(true);
89+
};
90+
91+
// Validate repository path ends with ID
92+
const validateRepositoryPath = (path, repoId) => {
93+
const pathResult = validatePOSIXPath(path, "Repository path");
94+
if (!pathResult.ok) {
95+
return pathResult;
96+
}
97+
98+
// Ensure path ends with /
99+
const normalizedPath = path.endsWith("/") ? path : path + "/";
100+
101+
// Extract last component
102+
const idx = normalizedPath.lastIndexOf("/", normalizedPath.length - 2);
103+
const lastComponent = normalizedPath.slice(idx + 1, normalizedPath.length - 1);
104+
105+
if (lastComponent !== repoId) {
106+
return Result.err({
107+
code: g_lib.ERR_INVALID_PARAM,
108+
message: `Repository path must end with repository ID (${repoId})`,
109+
});
110+
}
111+
112+
return Result.ok(true);
113+
};
114+
115+
// Validate Globus-specific configuration
116+
const validateGlobusConfig = (config) => {
117+
const commonResult = validateCommonFields(config);
118+
if (!commonResult.ok) {
119+
return commonResult;
120+
}
121+
122+
const errors = [];
123+
124+
// Validate required Globus fields
125+
const pubKeyValidation = validateNonEmptyString(config.pub_key, "Public key");
126+
if (!pubKeyValidation.ok) {
127+
errors.push(pubKeyValidation.error.message);
128+
}
129+
130+
const addressValidation = validateNonEmptyString(config.address, "Address");
131+
if (!addressValidation.ok) {
132+
errors.push(addressValidation.error.message);
133+
}
134+
135+
const endpointValidation = validateNonEmptyString(config.endpoint, "Endpoint");
136+
if (!endpointValidation.ok) {
137+
errors.push(endpointValidation.error.message);
138+
}
139+
140+
const domainValidation = validateNonEmptyString(config.domain, "Domain");
141+
if (!domainValidation.ok) {
142+
errors.push(domainValidation.error.message);
143+
}
144+
145+
if (errors.length > 0) {
146+
return Result.err({
147+
code: g_lib.ERR_INVALID_PARAM,
148+
message: errors.join("; "),
149+
});
150+
}
151+
152+
// Validate repository path
153+
const pathResult = validateRepositoryPath(config.path, config.id);
154+
if (!pathResult.ok) {
155+
return pathResult;
156+
}
157+
158+
// Validate export path if provided
159+
if (config.exp_path) {
160+
const expPathResult = validatePOSIXPath(config.exp_path, "Export path");
161+
if (!expPathResult.ok) {
162+
return expPathResult;
163+
}
164+
}
165+
166+
return Result.ok(true);
167+
};
168+
169+
// Validate metadata-only repository configuration
170+
const validateMetadataConfig = (config) => {
171+
const commonResult = validateCommonFields(config);
172+
if (!commonResult.ok) {
173+
return commonResult;
174+
}
175+
176+
// Metadata repositories don't need Globus-specific fields
177+
// But should not have them either
178+
const invalidFields = ["pub_key", "address", "endpoint", "path", "exp_path", "domain"];
179+
const presentInvalidFields = invalidFields.filter((field) => config[field] !== undefined);
180+
181+
if (presentInvalidFields.length > 0) {
182+
return Result.err({
183+
code: g_lib.ERR_INVALID_PARAM,
184+
message: `Metadata-only repositories should not have: ${presentInvalidFields.join(", ")}`,
185+
});
186+
}
187+
188+
return Result.ok(true);
189+
};
190+
191+
// Validate allocation parameters
192+
const validateAllocationParams = (params) => {
193+
const errors = [];
194+
195+
const subjectValidation = validateNonEmptyString(params.subject, "Allocation subject");
196+
if (!subjectValidation.ok) {
197+
errors.push(subjectValidation.error.message);
198+
}
199+
200+
if (typeof params.size !== "number" || params.size <= 0) {
201+
errors.push("Allocation size must be a positive number");
202+
}
203+
204+
if (params.path && typeof params.path !== "string") {
205+
errors.push("Allocation path must be a string if provided");
206+
}
207+
208+
if (errors.length > 0) {
209+
return Result.err({
210+
code: g_lib.ERR_INVALID_PARAM,
211+
message: errors.join("; "),
212+
});
213+
}
214+
215+
return Result.ok(true);
216+
};
217+
218+
module.exports = {
219+
validateNonEmptyString,
220+
validateCommonFields,
221+
validatePOSIXPath,
222+
validateRepositoryPath,
223+
validateGlobusConfig,
224+
validateMetadataConfig,
225+
validateAllocationParams,
226+
};

0 commit comments

Comments
 (0)