Skip to content

Commit f516d4f

Browse files
committed
fix: config test
1 parent 8011586 commit f516d4f

File tree

2 files changed

+267
-0
lines changed

2 files changed

+267
-0
lines changed

src/main/java/io/supertokens/config/CoreConfig.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ public class CoreConfig {
270270
// # webserver_https_enabled:
271271
@ConfigYamlOnly
272272
@JsonProperty
273+
@IgnoreForAnnotationCheck
273274
private boolean webserver_https_enabled = false;
274275

275276
@EnvName("BASE_PATH")
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
/*
2+
* Copyright (c) 2025, VRAI Labs and/or its affiliates. All rights reserved.
3+
*
4+
* This software is licensed under the Apache License, Version 2.0 (the
5+
* "License") as published by the Apache Software Foundation.
6+
*
7+
* You may not use this file except in compliance with the License. You may
8+
* obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations
14+
* under the License.
15+
*/
16+
17+
package io.supertokens.test;
18+
19+
import io.supertokens.ProcessState;
20+
import io.supertokens.config.Config;
21+
import io.supertokens.config.CoreConfig;
22+
import io.supertokens.config.annotations.EnvName;
23+
import io.supertokens.config.annotations.IgnoreForAnnotationCheck;
24+
import org.junit.AfterClass;
25+
import org.junit.Before;
26+
import org.junit.Rule;
27+
import org.junit.Test;
28+
import org.junit.rules.TestRule;
29+
30+
import java.lang.reflect.Field;
31+
import java.util.Map;
32+
33+
import static org.junit.Assert.*;
34+
35+
public class EnvConfigTest {
36+
37+
@Rule
38+
public TestRule watchman = Utils.getOnFailure();
39+
40+
@Rule
41+
public TestRule retryFlaky = Utils.retryFlakyTest();
42+
43+
@AfterClass
44+
public static void afterTesting() {
45+
Utils.afterTesting();
46+
}
47+
48+
@Before
49+
public void beforeEach() {
50+
Utils.reset();
51+
}
52+
53+
private static void setEnv(String key, String value) {
54+
try {
55+
Map<String, String> env = System.getenv();
56+
Class<?> cl = env.getClass();
57+
Field field = cl.getDeclaredField("m");
58+
field.setAccessible(true);
59+
Map<String, String> writableEnv = (Map<String, String>) field.get(env);
60+
writableEnv.put(key, value);
61+
} catch (Exception e) {
62+
throw new IllegalStateException("Failed to set environment variable", e);
63+
}
64+
}
65+
66+
private static void removeEnv(String key) {
67+
try {
68+
Map<String, String> env = System.getenv();
69+
Class<?> cl = env.getClass();
70+
Field field = cl.getDeclaredField("m");
71+
field.setAccessible(true);
72+
Map<String, String> writableEnv = (Map<String, String>) field.get(env);
73+
writableEnv.remove(key);
74+
} catch (Exception e) {
75+
throw new IllegalStateException("Failed to set environment variable", e);
76+
}
77+
}
78+
79+
@Test
80+
public void testAllCoreConfigFieldsHaveEnvNameAssociatedWithIt() throws Exception {
81+
for (Field field : CoreConfig.class.getDeclaredFields()) {
82+
if (!field.isAnnotationPresent(IgnoreForAnnotationCheck.class)) {
83+
assertTrue(field.getName() + " does not have env defined!", field.isAnnotationPresent(EnvName.class));
84+
}
85+
}
86+
}
87+
88+
@Test
89+
public void testEnvVarsAreLoadingOnBaseTenant() throws Exception {
90+
Object[][] testCases = new Object[][]{
91+
// ACCESS_TOKEN_VALIDITY: must be between 1 and 86400000 seconds inclusive
92+
new Object[]{"ACCESS_TOKEN_VALIDITY", "3600", (long) 3600},
93+
new Object[]{"ACCESS_TOKEN_VALIDITY", "7200", (long) 7200},
94+
new Object[]{"ACCESS_TOKEN_VALIDITY", "1", (long) 1}, // minimum valid value
95+
new Object[]{"ACCESS_TOKEN_VALIDITY", "86400000", (long) 86400000}, // maximum valid value
96+
97+
// REFRESH_TOKEN_VALIDITY: in minutes, must be > access_token_validity when converted to seconds
98+
// Default access_token_validity is 3600 seconds, so refresh_token_validity must be > 60 minutes
99+
new Object[]{"REFRESH_TOKEN_VALIDITY", "120", (double) 120}, // 2 hours > 1 hour default
100+
new Object[]{"REFRESH_TOKEN_VALIDITY", "1440", (double) 1440}, // 24 hours
101+
102+
// ACCESS_TOKEN_BLACKLISTING: boolean
103+
new Object[]{"ACCESS_TOKEN_BLACKLISTING", "true", true},
104+
new Object[]{"ACCESS_TOKEN_BLACKLISTING", "false", false},
105+
106+
// PASSWORD_RESET_TOKEN_LIFETIME: must be > 0 (in milliseconds)
107+
new Object[]{"PASSWORD_RESET_TOKEN_LIFETIME", "3600000", (long) 3600000}, // 1 hour
108+
new Object[]{"PASSWORD_RESET_TOKEN_LIFETIME", "7200000", (long) 7200000}, // 2 hours
109+
new Object[]{"PASSWORD_RESET_TOKEN_LIFETIME", "1", (long) 1}, // minimum valid
110+
111+
// EMAIL_VERIFICATION_TOKEN_LIFETIME: must be > 0 (in milliseconds)
112+
new Object[]{"EMAIL_VERIFICATION_TOKEN_LIFETIME", "86400000", (long) 86400000}, // 1 day
113+
new Object[]{"EMAIL_VERIFICATION_TOKEN_LIFETIME", "172800000", (long) 172800000}, // 2 days
114+
new Object[]{"EMAIL_VERIFICATION_TOKEN_LIFETIME", "1", (long) 1}, // minimum valid
115+
116+
// PASSWORDLESS_MAX_CODE_INPUT_ATTEMPTS: must be > 0
117+
new Object[]{"PASSWORDLESS_MAX_CODE_INPUT_ATTEMPTS", "5", 5},
118+
new Object[]{"PASSWORDLESS_MAX_CODE_INPUT_ATTEMPTS", "10", 10},
119+
new Object[]{"PASSWORDLESS_MAX_CODE_INPUT_ATTEMPTS", "1", 1}, // minimum valid
120+
121+
// PASSWORDLESS_CODE_LIFETIME: must be > 0 (in milliseconds)
122+
new Object[]{"PASSWORDLESS_CODE_LIFETIME", "900000", (long) 900000}, // 15 minutes
123+
new Object[]{"PASSWORDLESS_CODE_LIFETIME", "600000", (long) 600000}, // 10 minutes
124+
new Object[]{"PASSWORDLESS_CODE_LIFETIME", "1", (long) 1}, // minimum valid
125+
126+
// TOTP_MAX_ATTEMPTS: must be > 0
127+
new Object[]{"TOTP_MAX_ATTEMPTS", "5", 5},
128+
new Object[]{"TOTP_MAX_ATTEMPTS", "3", 3},
129+
new Object[]{"TOTP_MAX_ATTEMPTS", "1", 1}, // minimum valid
130+
131+
// TOTP_RATE_LIMIT_COOLDOWN_SEC: must be > 0 (in seconds)
132+
new Object[]{"TOTP_RATE_LIMIT_COOLDOWN_SEC", "900", 900}, // 15 minutes
133+
new Object[]{"TOTP_RATE_LIMIT_COOLDOWN_SEC", "300", 300}, // 5 minutes
134+
new Object[]{"TOTP_RATE_LIMIT_COOLDOWN_SEC", "1", 1}, // minimum valid
135+
136+
// ACCESS_TOKEN_SIGNING_KEY_DYNAMIC: boolean
137+
new Object[]{"ACCESS_TOKEN_SIGNING_KEY_DYNAMIC", "true", true},
138+
new Object[]{"ACCESS_TOKEN_SIGNING_KEY_DYNAMIC", "false", false},
139+
140+
// ACCESS_TOKEN_DYNAMIC_SIGNING_KEY_UPDATE_INTERVAL: must be >= 1 hour (in hours)
141+
new Object[]{"ACCESS_TOKEN_DYNAMIC_SIGNING_KEY_UPDATE_INTERVAL", "168", (double) 168}, // 1 week
142+
new Object[]{"ACCESS_TOKEN_DYNAMIC_SIGNING_KEY_UPDATE_INTERVAL", "24", (double) 24}, // 1 day
143+
new Object[]{"ACCESS_TOKEN_DYNAMIC_SIGNING_KEY_UPDATE_INTERVAL", "1", (double) 1}, // minimum valid
144+
145+
// MAX_SERVER_POOL_SIZE: must be >= 1
146+
new Object[]{"MAX_SERVER_POOL_SIZE", "10", 10},
147+
new Object[]{"MAX_SERVER_POOL_SIZE", "5", 5},
148+
new Object[]{"MAX_SERVER_POOL_SIZE", "1", 1}, // minimum valid
149+
150+
// DISABLE_TELEMETRY: boolean
151+
new Object[]{"DISABLE_TELEMETRY", "false", false},
152+
new Object[]{"DISABLE_TELEMETRY", "true", true},
153+
154+
// PASSWORD_HASHING_ALG: must be "ARGON2" or "BCRYPT"
155+
new Object[]{"PASSWORD_HASHING_ALG", "BCRYPT", "BCRYPT"},
156+
new Object[]{"PASSWORD_HASHING_ALG", "ARGON2", "ARGON2"},
157+
158+
// ARGON2_ITERATIONS: must be >= 1
159+
new Object[]{"ARGON2_ITERATIONS", "1", 1}, // minimum valid
160+
new Object[]{"ARGON2_ITERATIONS", "3", 3},
161+
162+
// ARGON2_MEMORY_KB: must be >= 1
163+
new Object[]{"ARGON2_MEMORY_KB", "87795", 87795}, // default
164+
new Object[]{"ARGON2_MEMORY_KB", "1024", 1024},
165+
new Object[]{"ARGON2_MEMORY_KB", "1", 1}, // minimum valid
166+
167+
// ARGON2_PARALLELISM: must be >= 1
168+
new Object[]{"ARGON2_PARALLELISM", "2", 2}, // default
169+
new Object[]{"ARGON2_PARALLELISM", "4", 4},
170+
new Object[]{"ARGON2_PARALLELISM", "1", 1}, // minimum valid
171+
172+
// ARGON2_HASHING_POOL_SIZE: must be >= 1
173+
new Object[]{"ARGON2_HASHING_POOL_SIZE", "1", 1}, // minimum valid
174+
new Object[]{"ARGON2_HASHING_POOL_SIZE", "2", 2},
175+
176+
// FIREBASE_PASSWORD_HASHING_POOL_SIZE: must be >= 1
177+
new Object[]{"FIREBASE_PASSWORD_HASHING_POOL_SIZE", "1", 1}, // minimum valid
178+
new Object[]{"FIREBASE_PASSWORD_HASHING_POOL_SIZE", "3", 3},
179+
180+
// BCRYPT_LOG_ROUNDS: must be >= 1
181+
new Object[]{"BCRYPT_LOG_ROUNDS", "11", 11}, // default
182+
new Object[]{"BCRYPT_LOG_ROUNDS", "10", 10},
183+
new Object[]{"BCRYPT_LOG_ROUNDS", "1", 1}, // minimum valid
184+
185+
// LOG_LEVEL: must be one of "DEBUG", "INFO", "WARN", "ERROR", "NONE"
186+
new Object[]{"LOG_LEVEL", "INFO", "INFO"},
187+
new Object[]{"LOG_LEVEL", "DEBUG", "DEBUG"},
188+
new Object[]{"LOG_LEVEL", "WARN", "WARN"},
189+
new Object[]{"LOG_LEVEL", "ERROR", "ERROR"},
190+
new Object[]{"LOG_LEVEL", "NONE", "NONE"},
191+
192+
// BULK_MIGRATION_PARALLELISM: must be >= 1
193+
new Object[]{"BULK_MIGRATION_PARALLELISM", "1", 1}, // minimum valid
194+
new Object[]{"BULK_MIGRATION_PARALLELISM", "4", 4},
195+
196+
// BULK_MIGRATION_BATCH_SIZE: must be >= 1
197+
new Object[]{"BULK_MIGRATION_BATCH_SIZE", "8000", 8000}, // default
198+
new Object[]{"BULK_MIGRATION_BATCH_SIZE", "1000", 1000},
199+
new Object[]{"BULK_MIGRATION_BATCH_SIZE", "1", 1}, // minimum valid
200+
201+
// WEBAUTHN_RECOVER_ACCOUNT_TOKEN_LIFETIME: must be > 0 (in milliseconds)
202+
new Object[]{"WEBAUTHN_RECOVER_ACCOUNT_TOKEN_LIFETIME", "3600000", (long) 3600000}, // 1 hour
203+
new Object[]{"WEBAUTHN_RECOVER_ACCOUNT_TOKEN_LIFETIME", "7200000", (long) 7200000}, // 2 hours
204+
new Object[]{"WEBAUTHN_RECOVER_ACCOUNT_TOKEN_LIFETIME", "1", (long) 1}, // minimum valid
205+
206+
// OTEL_COLLECTOR_CONNECTION_URI: string
207+
new Object[]{"OTEL_COLLECTOR_CONNECTION_URI", "http://localhost:4317", "http://localhost:4317"},
208+
new Object[]{"OTEL_COLLECTOR_CONNECTION_URI", "https://otel.example.com:4317", "https://otel.example.com:4317"},
209+
210+
// BASE_PATH: string (can be empty, "/", or valid path)
211+
new Object[]{"BASE_PATH", "", ""},
212+
new Object[]{"BASE_PATH", "/", ""},
213+
new Object[]{"BASE_PATH", "/api", "/api"},
214+
new Object[]{"BASE_PATH", "/v1/auth", "/v1/auth"},
215+
216+
// API_KEYS: string with specific format constraints (minimum 20 chars, specific character set)
217+
new Object[]{"API_KEYS", "abcdefghijklmnopqrstuvwxyz", "abcdefghijklmnopqrstuvwxyz"},
218+
new Object[]{"API_KEYS", "key1-with-dashes-and=equals123,another-key-12345678901", "another-key-12345678901,key1-with-dashes-and=equals123"}, // gets sorted
219+
220+
// FIREBASE_PASSWORD_HASHING_SIGNER_KEY: string (any non-empty value)
221+
new Object[]{"FIREBASE_PASSWORD_HASHING_SIGNER_KEY", "test-signer-key-12345", "test-signer-key-12345"},
222+
223+
// IP_ALLOW_REGEX: string (valid regex pattern)
224+
new Object[]{"IP_ALLOW_REGEX", "127\\.\\d+\\.\\d+\\.\\d+", "127\\.\\d+\\.\\d+\\.\\d+"},
225+
new Object[]{"IP_ALLOW_REGEX", ".*", ".*"},
226+
227+
// IP_DENY_REGEX: string (valid regex pattern)
228+
new Object[]{"IP_DENY_REGEX", "192\\.168\\..*", "192\\.168\\..*"},
229+
230+
// SUPERTOKENS_MAX_CDI_VERSION: string (valid semantic version)
231+
new Object[]{"SUPERTOKENS_MAX_CDI_VERSION", "5.0", "5.0"},
232+
new Object[]{"SUPERTOKENS_MAX_CDI_VERSION", "4.0", "4.0"}
233+
};
234+
235+
for (Object[] testCase : testCases) {
236+
String[] args = {"../"};
237+
setEnv(testCase[0].toString(), testCase[1].toString());
238+
239+
TestingProcessManager.TestingProcess process = TestingProcessManager.startIsolatedProcess(args);
240+
ProcessState.EventAndException startEvent = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STARTED);
241+
assertNotNull(startEvent);
242+
243+
CoreConfig config = Config.getBaseConfig(process.getProcess());
244+
boolean fieldChecked = false;
245+
for (Field field : config.getClass().getDeclaredFields()) {
246+
if (!field.isAnnotationPresent(EnvName.class)) {
247+
continue;
248+
}
249+
250+
field.setAccessible(true);
251+
if (field.getAnnotationsByType(EnvName.class)[0].value().equals(testCase[0].toString())) {
252+
assertEquals("Failed for env var: " + testCase[0] + " with value: " + testCase[1],
253+
testCase[2], field.get(config));
254+
fieldChecked = true;
255+
}
256+
}
257+
assertTrue("No field found for env var: " + testCase[0], fieldChecked);
258+
259+
process.kill();
260+
ProcessState.EventAndException stopEvent = process.checkOrWaitForEvent(ProcessState.PROCESS_STATE.STOPPED);
261+
assertNotNull(stopEvent);
262+
263+
removeEnv(testCase[0].toString());
264+
}
265+
}
266+
}

0 commit comments

Comments
 (0)