Skip to content

feat: Implement User CRUD (COBOL COUSR00C-03C → Spring Boot)#175

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/tier1-user-crud
Open

feat: Implement User CRUD (COBOL COUSR00C-03C → Spring Boot)#175
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/tier1-user-crud

Conversation

@devin-ai-integration
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot commented Apr 29, 2026

Summary

Migrates the COBOL User Management CRUD programs (COUSR00C, COUSR01C, COUSR02C, COUSR03C) from CICS/VSAM to a Spring Boot 3.x REST API under the new carddemo-java/ directory.

COBOL → Java Mapping

COBOL Program Function Java Equivalent
COUSR00C (695 lines) List users via CICS STARTBR/READNEXT GET /api/admin/users?page=0&size=10 — Spring Data paginated query
COUSR01C (299 lines) Add user with CICS READ dup-check + WRITE POST /api/admin/usersexistsByUserId() check + JPA save
COUSR02C (414 lines) Update user via CICS READ + REWRITE GET + PUT /api/admin/users/{userId} — JPA findById + save
COUSR03C (359 lines) Delete user via CICS READ + DELETE DELETE /api/admin/users/{userId} — JPA deleteById

Components Added

  • Entity: User maps the USRSEC record layout (CSUSR01Y.cpy — 80-byte record with userId PK, firstName, lastName, password, userType)
  • Repository: UserRepository extends JpaRepository with existsByUserId() for duplicate checking
  • Service: UserService with 5 methods replacing all 4 COBOL programs
  • Controller: UserController with 5 REST endpoints under /api/admin/users
  • Security: HTTP Basic auth with database-backed UserDetailsService. Admin-only access enforced via ROLE_ADMIN (maps COBOL CDEMO-USRTYP-ADMIN check)
  • DTOs: UserCreateRequest, UserUpdateRequest, UserResponse, PagedResponse<T> — password is never exposed in responses
  • Validation: Bean validation matching COBOL field constraints (userId max 8 alphanumeric, firstName/lastName max 20, password max 8, userType A/U)
  • Exception handling: 409 Conflict for duplicate userId, 404 for missing user, 400 for validation errors, 403 for non-admin access
  • Seed data: 3 users (ADMIN001/admin, USER0001/regular, USER0002/regular) with BCrypt-hashed passwords
  • Tests: 8 integration tests covering all CRUD operations + security enforcement

Tech Stack

Spring Boot 3.2.5, Java 17, Spring Data JPA, Spring Security, H2 database, Lombok, Bean Validation

Review & Testing Checklist for Human

  • Verify the BCrypt seed password hash works — start the app and authenticate as ADMIN001 / password via HTTP Basic
  • Test all 5 endpoints end-to-end with curl or Postman (list, add, get, update, delete)
  • Verify non-admin user (USER0001) gets 403 on all /api/admin/** endpoints
  • Confirm password is never returned in any API response
  • Review validation: try creating a user with userId > 8 chars, invalid userType, duplicate userId

Test Plan

cd carddemo-java
export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
mvn clean test        # All 8 tests should pass
mvn spring-boot:run   # Start app on port 8080

# List users (as admin)
curl -u ADMIN001:password http://localhost:8080/api/admin/users

# Add user
curl -u ADMIN001:password -X POST -H 'Content-Type: application/json' \
  -d '{"userId":"TEST001","firstName":"TEST","lastName":"USER","password":"pass123","userType":"U"}' \
  http://localhost:8080/api/admin/users

# Non-admin should get 403
curl -u USER0001:password http://localhost:8080/api/admin/users

Notes

  • Passwords are BCrypt-hashed in storage but the COBOL 8-char max constraint is enforced on input validation
  • H2 in-memory database is used (replaces VSAM USRSEC file); seed data loaded on startup via data.sql
  • All 8 integration tests pass locally (list, add, add-duplicate-409, update, update-404, delete, delete-404, non-admin-403)

Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/0a614e7c53b04a438d5a2c4af3920c2b
Requested by: @bsmitches


Open in Devin Review

Migrates COBOL programs COUSR00C (List Users), COUSR01C (Add User),
COUSR02C (Update User), and COUSR03C (Delete User) from CICS/VSAM
to a Spring Boot 3.x REST API.

Components:
- User entity mapping USRSEC record layout (CSUSR01Y.cpy)
- UserRepository with JPA (replaces VSAM KSDS operations)
- UserService with paginated list, add, get, update, delete
- UserController with 5 REST endpoints under /api/admin/users
- Spring Security with HTTP Basic + admin-only access (ROLE_ADMIN)
- BCrypt password hashing (replaces plaintext SEC-USR-PWD)
- Bean validation matching COBOL field constraints
- H2 seed data with 3 users (ADMIN001, USER0001, USER0002)
- Integration tests covering all CRUD ops + security (8 tests)
@devin-ai-integration
Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown
Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

View 7 additional findings in Devin Review.

Open in Devin Review

Comment on lines +54 to +65
public UserResponse addUser(UserCreateRequest request) {
if (userRepository.existsByUserId(request.getUserId())) {
throw new UserAlreadyExistsException(request.getUserId());
}
User user = new User();
user.setUserId(request.getUserId());
user.setFirstName(request.getFirstName());
user.setLastName(request.getLastName());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setUserType(request.getUserType());
userRepository.save(user);
return toResponse(user);
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 addUser can silently overwrite existing user due to JPA merge on manually-assigned @id

The User entity uses a manually-assigned @Id (String userId, no @GeneratedValue) and does not implement Persistable<String>. Spring Data JPA's SimpleJpaRepository.save() checks entityInformation.isNew(entity) which returns false when the ID is non-null, causing it to call entityManager.merge() instead of entityManager.persist(). In the addUser method, there is a TOCTOU race between the existsByUserId check (UserService.java:55) and the save() call (UserService.java:64): if a concurrent request creates the same userId and commits between these two calls, merge() will find the existing row and silently perform an UPDATE, overwriting the first user's password, name, and user type — without any error. This is a data integrity and security issue in a user management system.

Race condition timeline
  1. Thread A: existsByUserId("X") → false
  2. Thread B: existsByUserId("X") → false
  3. Thread A: save() → merge → SELECT → not found → INSERT → COMMIT
  4. Thread B: save() → merge → SELECT → found → UPDATE (silently overwrites Thread A's user)

Both threads return 201 Created. Thread A's data is lost.

Prompt for agents
The root cause is that the User entity has a manually-assigned @Id (String userId) without @GeneratedValue, and does not implement Persistable<String>. This causes Spring Data JPA's save() to always call merge() instead of persist() for new entities, since isNew() checks if the ID is null.

Fix approach: Have the User entity implement org.springframework.data.domain.Persistable<String>. Add a transient boolean field (e.g., @Transient private boolean isNew = true) and override isNew() to return it. Override getId() to return userId. Set isNew to false in a @PostLoad / @PrePersist callback or after loading from the repository.

Alternatively, inject EntityManager into UserService and call entityManager.persist(user) directly instead of userRepository.save(user) in the addUser method. This ensures an INSERT is always attempted, and a duplicate key will throw a constraint violation rather than silently merging.

If using the persist() approach, also add a handler for DataIntegrityViolationException in GlobalExceptionHandler to return 409 Conflict as a safety net.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant