-
Notifications
You must be signed in to change notification settings - Fork 83
fix(auth): prevent concurrent signup race conditions and lost updates #552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -943,6 +943,111 @@ describe("route error responses", () => { | |
| ); | ||
| }); | ||
|
|
||
| test("POST /api/auth/signup - single user registration succeeds", async () => { | ||
| const testEmail = `test-single-${Date.now()}@example.com`; | ||
| const res = await fetch(`${baseUrl}/api/auth/signup`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| email: testEmail, | ||
| password: "Test@1234", | ||
| }), | ||
| }); | ||
| assert.equal(res.status, 201); | ||
| const data = await res.json(); | ||
| assert.ok(data.token); | ||
| assert.equal(data.message, "Signup successful"); | ||
| }); | ||
|
|
||
| test("POST /api/auth/signup - duplicate email is rejected", async () => { | ||
| const testEmail = `test-dup-${Date.now()}@example.com`; | ||
|
|
||
| const firstRes = await fetch(`${baseUrl}/api/auth/signup`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| email: testEmail, | ||
| password: "Test@1234", | ||
| }), | ||
| }); | ||
| assert.equal(firstRes.status, 201); | ||
|
|
||
| const secondRes = await fetch(`${baseUrl}/api/auth/signup`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| email: testEmail, | ||
| password: "Different@1234", | ||
| }), | ||
| }); | ||
| assert.equal(secondRes.status, 400); | ||
| const data = await secondRes.json(); | ||
| assert.equal(data.message, "User already exists"); | ||
| }); | ||
|
|
||
| test("POST /api/auth/signup - concurrent registrations prevent lost updates", async () => { | ||
| const timestamp = Date.now(); | ||
| const emails = [ | ||
| `concurrent-1-${timestamp}@example.com`, | ||
| `concurrent-2-${timestamp}@example.com`, | ||
| `concurrent-3-${timestamp}@example.com`, | ||
| ]; | ||
|
|
||
| const promises = emails.map((email) => | ||
| fetch(`${baseUrl}/api/auth/signup`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| email, | ||
| password: "Test@1234", | ||
| }), | ||
| }) | ||
| ); | ||
|
|
||
| const results = await Promise.all(promises); | ||
|
|
||
| const successCount = results.filter((r) => r.status === 201).length; | ||
| assert.equal(successCount, 3, "All concurrent registrations should succeed"); | ||
|
|
||
| const data = await results[0].json(); | ||
| assert.ok(data.token); | ||
| }); | ||
|
|
||
| test("POST /api/auth/signup - validates password strength", async () => { | ||
| const testEmail = `test-weak-${Date.now()}@example.com`; | ||
| const res = await fetch(`${baseUrl}/api/auth/signup`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| email: testEmail, | ||
| password: "weak", | ||
| }), | ||
| }); | ||
| assert.equal(res.status, 400); | ||
| const data = await res.json(); | ||
| assert.match(data.message, /Password must contain/); | ||
| }); | ||
|
|
||
| test("POST /api/auth/signup - rejects missing email or password", async () => { | ||
| const res1 = await fetch(`${baseUrl}/api/auth/signup`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| password: "Test@1234", | ||
| }), | ||
| }); | ||
| assert.equal(res1.status, 400); | ||
|
|
||
| const res2 = await fetch(`${baseUrl}/api/auth/signup`, { | ||
| method: "POST", | ||
| headers: { "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ | ||
| email: "test@example.com", | ||
| }), | ||
| }); | ||
| assert.equal(res2.status, 400); | ||
| }); | ||
|
|
||
|
Comment on lines
+946
to
+1050
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Signup tests are non-hermetic and mutate the committed auth datastore. These tests write directly to the real One practical isolation pattern+const fs = require("node:fs");
+const path = require("node:path");
+const usersFile = path.join(__dirname, "src/data/users.json");
+let usersSnapshot;
+
+before(() => {
+ usersSnapshot = fs.readFileSync(usersFile, "utf8");
+});
+
+after(() => {
+ fs.writeFileSync(usersFile, usersSnapshot, "utf8");
+});🤖 Prompt for AI Agents |
||
| test("successful upload response does not include a url field", async () => { | ||
| const originalPostForm = axios.postForm; | ||
| const originalPost = axios.post; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The concurrent-signup test does not verify persisted outcomes.
Right now it only checks response status/token. A lost-update bug can still return
201for all requests while dropping one or more writes. Add post-signup verification (e.g., login each created user).Strengthen the assertion
const results = await Promise.all(promises); const successCount = results.filter((r) => r.status === 201).length; assert.equal(successCount, 3, "All concurrent registrations should succeed"); - const data = await results[0].json(); - assert.ok(data.token); + const loginResults = await Promise.all( + emails.map((email) => + fetch(`${baseUrl}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password: "Test@1234" }), + }), + ), + ); + assert.equal( + loginResults.filter((r) => r.status === 200).length, + 3, + "Each concurrently created user must be persisted and able to log in", + );🤖 Prompt for AI Agents