Established the layout of the api-rs Spring Boot project, including package structure, file roles, and naming conventions.
Understand and follow the layout of the api-rs Spring Boot project before writing any backend code.
All source code lives under api-rs/src/main/java/com/emailssummarizer/apirs/.
Tests live under api-rs/src/test/java/com/emailssummarizer/apirs/ — mirroring the main package tree.
api-rs/
├── build.gradle ← Gradle build script (Spring Boot 4, Java 21)
└── src/
├── main/
│ ├── java/com/emailssummarizer/apirs/
│ │ ├── ApiRsApplication.java ← Spring Boot entry point (@SpringBootApplication)
│ │ │
│ │ ├── category/ ← Category feature slice
│ │ │ ├── Category.java ← JPA entity (@Entity, table: categories)
│ │ │ ├── CategoryController.java ← REST controller (@RestController, /categories); delegates to CategoryService
│ │ │ ├── CategoryRepository.java ← Spring Data JPA repository; called only from CategoryService
│ │ │ ├── CategoryRequest.java ← Request DTO (record)
│ │ │ └── CategoryService.java ← @Service; owns all business logic, validation, and orchestration
│ │ │
│ │ ├── message/ ← Message feature slice
│ │ │ ├── Message.java ← JPA entity (@Entity, table: messages)
│ │ │ ├── MessageController.java ← REST controller (@RestController, /messages); delegates to MessageService
│ │ │ ├── MessageRepository.java ← Spring Data JPA repository; called only from MessageService
│ │ │ ├── MessageRequest.java ← Request DTO (record)
│ │ │ └── MessageService.java ← @Service; owns all business logic, validation, and orchestration
│ │ │
│ │ ├── oauth/ ← OAuth2 token exchange
│ │ │ └── OAuthController.java ← POST /oauth2/token proxy to GitHub
│ │ │
│ │ └── security/ ← Spring Security configuration
│ │ ├── GitHubOpaqueTokenIntrospector.java ← Validates GitHub tokens, assigns ROLE_READ/EDIT/DEL
│ │ └── ResourceServerConfig.java ← SecurityFilterChain, CORS, per-method authorization
│ │
│ └── resources/
│ ├── application.yml ← Spring Boot config (H2, OAuth2, app.readers/editors/deleters)
│ ├── schema.sql ← DDL run at startup (categories, messages tables)
│ └── data.sql ← Seed data run at startup
│
└── test/
├── java/com/emailssummarizer/apirs/
│ ├── ApiRsApplicationTests.java ← Spring Boot smoke test (@SpringBootTest)
│ │
│ ├── category/
│ │ ├── CategoryControllerTest.java ← @WebMvcTest slice — HTTP layer only; CategoryService mocked with @MockitoBean
│ │ ├── CategoryRepositoryTest.java ← @DataJpaTest slice — DB queries against H2
│ │ └── CategoryServiceTest.java ← plain JUnit + Mockito — business logic with mocked repository
│ │
│ ├── message/
│ │ ├── MessageControllerTest.java ← @WebMvcTest slice — HTTP layer only; MessageService mocked with @MockitoBean
│ │ ├── MessageRepositoryTest.java ← @DataJpaTest slice — DB queries against H2
│ │ └── MessageServiceTest.java ← plain JUnit + Mockito — business logic with mocked repository
│ │
│ └── security/
│ └── SecurityFilterChainTest.java ← @SpringBootTest + MockMvc — authorization rules (401/403)
│
└── resources/
└── application.yml ← Test overrides (H2 in-memory, fixed allow-lists for roles)
Each domain concept (category, message) gets its own package containing:
<Name>.java — JPA entity, maps directly to a DB table<Name>Controller.java — @RestController; handles HTTP only (status codes, request/response mapping); delegates all logic to <Name>Service; never injects a repository directly<Name>Service.java — @Service; owns all business logic, cross-entity validation, and error conditions (duplicate checks, FK safety, not-found throws); the only class allowed to inject repositories<Name>Repository.java — JpaRepository; called only from <Name>Service, never from a controller or another service<Name>Request.java — Java record used as request body DTO (keeps entity fields private)Controller → Service → Repository
Repository directly.Controller.MessageService needing to check a category) goes through the other feature's Service or Repository, not its Controller.GitHubOpaqueTokenIntrospector calls GET https://api.github.com/user to validate tokens and assigns authorities@Value("${app.readers:}"), ${app.editors:}, ${app.deleters:} → env vars READERS_GITHUB_LOGINS, EDITORS_GITHUB_LOGINS, DELETERS_GITHUB_LOGINSResourceServerConfig declares per-method rules: GET→ROLE_READ, POST/PUT→ROLE_EDIT, DELETE→ROLE_DEL| Situation | Status |
|---|---|
| Successful GET / PUT | 200 |
| Successful POST (created) | 201 |
| Successful DELETE | 204 |
| Resource not found | 404 |
| Conflict (duplicate code, FK violation) | 409 |
| Missing / invalid token | 401 |
| Authenticated but missing role | 403 |
schema.sql then data.sql at startupcategories table: code (PK, VARCHAR), name, descriptionmessages table: id (PK, auto-increment), title, body, category_code (FK → categories.code)Each test class mirrors the package of the class under test.
| Test class | Annotation | What it covers |
|---|---|---|
<Name>ControllerTest | @WebMvcTest(<Name>Controller.class) | HTTP status, request mapping, JSON serialisation; <Name>Service mocked with @MockitoBean |
<Name>ServiceTest | plain JUnit 5 + Mockito | Business logic, validation, error conditions; repository mocked with @Mock / @InjectMocks |
<Name>RepositoryTest | @DataJpaTest | Custom query methods against the H2 in-memory DB; schema.sql / data.sql loaded automatically |
SecurityFilterChainTest | @SpringBootTest + MockMvc | Per-endpoint 401 (no token) and 403 (wrong role) rules declared in ResourceServerConfig |
ApiRsApplicationTests | @SpringBootTest | Smoke test — context loads without errors |
Test resource overrides (src/test/resources/application.yml) must at minimum set: