Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 6 additions & 22 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,37 +29,21 @@ docker/ # Docker image build

## DO / DON'T

> For Kotlin idioms and naming conventions, see **[docs/KOTLIN.md](docs/KOTLIN.md)**.
> For Spring Boot patterns and testing setup, see **[docs/SPRING.md](docs/SPRING.md)**.
> For testing conventions and commands, see **[docs/TESTING.md](docs/TESTING.md)**.

### DO
- Use **constructor injection** for all Spring beans (in production code)
- Use **data classes** for DTOs with Jackson XML annotations
- Use **Kotlin stdlib** and built-in language features over third-party utilities
- Use **AWS SDK v2** for all new integration tests
- Use **JUnit 5** for all new tests
- Use **`@SpringBootTest`** with **`@MockitoBean`** for unit tests — this is the project's standard mocking approach
- Use **expression bodies** for simple functions
- Use **null safety** (`?`, `?.`, `?:`) instead of null checks
- **Name the `it` parameter** in nested lambdas, loops, and scope functions to avoid shadowing: `.map { part -> ... }` instead of `.map { it.name }`
- Match **AWS S3 API naming exactly** in Jackson XML annotations (`localName = "..."`)
- Keep tests **independent** — each test creates its own resources (UUID bucket names)
- Use **backtick test names** with descriptive sentences: `` fun `should create bucket successfully`() ``
- Mark test classes as **`internal`**: `internal class ObjectServiceTest`
- **Refactor** legacy `testSomething` camelCase names to backtick style when touching existing tests
- **Update the copyright year** in the file's license header to the current year whenever you modify an existing file
- Validate XML serialization against [AWS S3 API documentation](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html)

### DON'T
- DON'T use `@Autowired` or field injection in production code — always use constructor injection
- DON'T use `var` for public API properties — prefer `val` (immutability)
- DON'T use AWS SDK v1 — it has been removed in 5.x
- DON'T use JUnit 4 — it has been removed in 5.x
- DON'T use `@ExtendWith(MockitoExtension::class)` or `@Mock` / `@InjectMocks` — use `@SpringBootTest` with `@MockitoBean` instead
- DON'T add Apache Commons dependencies — use Kotlin stdlib equivalents
- DON'T put business logic in controllers — controllers only map HTTP, delegate to services
- DON'T return raw strings from controllers — use typed DTOs for XML/JSON responses
- DON'T declare dependency versions in sub-module POMs — all versions are managed in root `pom.xml`
- DON'T share mutable state between tests — each test must be self-contained
- DON'T hardcode bucket names in tests — use `UUID.randomUUID()` for uniqueness
- DON'T use legacy `testSomething` camelCase naming for new tests — use backtick names instead
- DON'T update copyright years in files you haven't modified — copyright is only bumped when a file is actually changed

## Code Style
Expand All @@ -68,7 +52,7 @@ See **[docs/KOTLIN.md](docs/KOTLIN.md)** for Kotlin idioms, naming conventions,

See **[docs/JAVA.md](docs/JAVA.md)** for Java idioms, naming conventions, common anti-patterns, and Javadoc guidelines.

**Spring**: `@RestController`, `@Service`, `@Component`, constructor injection over field injection
See **[docs/SPRING.md](docs/SPRING.md)** for Spring Boot patterns, bean registration, dependency injection, controller guidelines, configuration properties, exception handling, and testing.

## XML Serialization

Expand Down Expand Up @@ -105,7 +89,7 @@ Environment variables (prefix: `COM_ADOBE_TESTING_S3MOCK_STORE_`):

Services throw `S3Exception` constants (`NO_SUCH_BUCKET`, `NO_SUCH_KEY`, `INVALID_BUCKET_NAME`, etc.).
Spring exception handlers convert them to XML `ErrorResponse` with the correct HTTP status.
See `server/AGENTS.md` for details.
See **[docs/SPRING.md](docs/SPRING.md)** for exception handling patterns and `server/AGENTS.md` for the concrete handler classes.

## Testing

Expand Down
143 changes: 143 additions & 0 deletions docs/SPRING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Spring Guidelines — S3Mock

Canonical reference for Spring Boot idioms, patterns, and code quality standards used across this project.

## Bean Registration

Register beans explicitly in `@Configuration` classes using `@Bean` factory methods — not via component scanning or class-level `@Service`/`@Component` annotations.

```kotlin
@Configuration
class ServiceConfiguration {
@Bean
fun bucketService(bucketStore: BucketStore, objectStore: ObjectStore): BucketService =
BucketService(bucketStore, objectStore)
}
```

Three configuration layers mirror the architecture:
- `StoreConfiguration` — store beans
- `ServiceConfiguration` — service beans (imported in `@SpringBootTest`)
- `ControllerConfiguration` — controller, filter, and exception-handler beans

## Dependency Injection

Always use **constructor injection**. Never use `@Autowired` on fields or setters in production code.

```kotlin
// DO — constructor injection
class BucketService(
private val bucketStore: BucketStore,
private val objectStore: ObjectStore,
)

// DON'T — field injection
class BucketService {
@Autowired private lateinit var bucketStore: BucketStore
}
```

## Controllers

- `@RestController` classes map HTTP only — never contain business logic
- All logic is delegated to a `@Service`; controllers call the service and return the result
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The use of backticks around "@service" is ambiguous and potentially confusing. Line 7 explicitly states not to use "@service" annotations, but this line suggests delegating to a "@service". Consider changing this to "service class" or "service bean" without backticks to avoid confusion with the Spring @Service annotation.

Suggested change
- All logic is delegated to a `@Service`; controllers call the service and return the result
- All logic is delegated to a service class; controllers call the service and return the result

Copilot uses AI. Check for mistakes.
- Return typed DTOs, never raw strings
- Controllers never catch exceptions — exception handlers in `ControllerConfiguration` do that

```kotlin
// DO
@GetMapping("/{bucketName}")
fun getBucket(@PathVariable bucketName: String): ResponseEntity<ListBucketResult> =
ResponseEntity.ok(bucketService.listObjects(bucketName))

// DON'T
@GetMapping("/{bucketName}")
fun getBucket(@PathVariable bucketName: String): String {
// business logic here ...
return "<ListBucketResult>...</ListBucketResult>"
}
```

## Configuration Properties

Bind configuration via `@ConfigurationProperties` data classes — never inject individual values with `@Value` in production code.

```kotlin
@JvmRecord
@ConfigurationProperties("com.adobe.testing.s3mock.store")
data class StoreProperties(
@param:DefaultValue("false") val retainFilesOnExit: Boolean,
@param:DefaultValue("us-east-1") val region: Region,
)
```

Enable each properties class with `@EnableConfigurationProperties` in the matching `@Configuration`:

```kotlin
@Configuration
@EnableConfigurationProperties(StoreProperties::class)
class StoreConfiguration { ... }
```

## Exception Handling

- Services throw `S3Exception` constants (e.g., `S3Exception.NO_SUCH_BUCKET`) — never create new exception classes
- Exception handlers are `@ControllerAdvice` classes registered as `@Bean`s in `ControllerConfiguration`
- `S3MockExceptionHandler` converts `S3Exception` → XML `ErrorResponse` with the correct HTTP status
- `IllegalStateExceptionHandler` converts unexpected errors → `500 InternalError`

```kotlin
@ControllerAdvice
class S3MockExceptionHandler : ResponseEntityExceptionHandler() {
@ExceptionHandler(S3Exception::class)
fun handleS3Exception(s3Exception: S3Exception): ResponseEntity<ErrorResponse> { ... }
}
```

## Testing

### Service and Store Tests — `@SpringBootTest`

Use `@SpringBootTest` scoped to the relevant `@Configuration` class with `@MockitoBean` for dependencies.

```kotlin
@SpringBootTest(classes = [ServiceConfiguration::class], webEnvironment = SpringBootTest.WebEnvironment.NONE)
@MockitoBean(types = [MultipartService::class, MultipartStore::class])
internal class BucketServiceTest : ServiceTestBase() {
@Autowired private lateinit var iut: BucketService

@Test
fun `should return no such bucket`() { ... }
}
```

Always extend the appropriate base class:
- `ServiceTestBase` — service layer tests
- `StoreTestBase` — store layer tests

### Controller Tests — `@WebMvcTest`

Use `@WebMvcTest` scoped to the controller under test with `@MockitoBean` for services and `BaseControllerTest` for shared fixtures.

```kotlin
@WebMvcTest(controllers = [BucketController::class], ...
@MockitoBean(types = [BucketService::class])
internal class BucketControllerTest : BaseControllerTest() {
@Autowired private lateinit var mockMvc: MockMvc
@Autowired private lateinit var bucketService: BucketService

@Test
fun `should list buckets`() { ... }
}
```

### Common Anti-Patterns

| Anti-Pattern | Refactor To |
|---|---|
| `@ExtendWith(MockitoExtension::class)` + `@Mock` + `@InjectMocks` | `@SpringBootTest` + `@MockitoBean` + `@Autowired` |
| `@Autowired` field injection in production code | Constructor injection |
| Business logic in controller method | Delegate to a service class |
| Returning a raw `String` from a controller | Return a typed DTO wrapped in `ResponseEntity` |
| `@Value("${property}")` scattered throughout beans | `@ConfigurationProperties` data class |
| New exception class for S3 errors | `S3Exception` constant |
3 changes: 0 additions & 3 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,6 @@ Access `serviceEndpoint`, `serviceEndpointHttp`, and `serviceEndpointHttps` from

See **[docs/KOTLIN.md](KOTLIN.md)** for Kotlin naming conventions (backtick test names, `internal` visibility, naming patterns).

- **Naming**: Backtick names with descriptive sentences — `` fun `should create bucket successfully`() ``
- **Visibility**: Mark test classes `internal`
- **Pattern**: Arrange-Act-Assert
- **Assertions**: AssertJ (`assertThat(...)`) — use specific matchers, not just `isNotNull()`
- **Error cases**: Use AssertJ, not JUnit `assertThrows`:
Expand All @@ -78,7 +76,6 @@ See **[docs/KOTLIN.md](KOTLIN.md)** for Kotlin naming conventions (backtick test
.hasMessageContaining("Status Code: 409")
```
- **Independence**: Each test creates its own resources — no shared state, UUID-based bucket names
- **Legacy names**: Refactor `testSomething` camelCase names to backtick style when touching existing tests

## Running Tests

Expand Down
5 changes: 3 additions & 2 deletions server/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,15 @@ server/src/main/kotlin/com/adobe/testing/s3mock/

1. **DTO** (`dto/`): Data classes with Jackson XML annotations (`@JacksonXmlRootElement`, `@JacksonXmlProperty`, `@JacksonXmlElementWrapper(useWrapping = false)`). Verify element names against [AWS S3 API docs](https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html).
2. **Store** (`store/`): Filesystem path resolution, binary storage, metadata JSON. Key classes: `BucketStore`, `ObjectStore`, `BucketMetadata`, `S3ObjectMetadata`.
3. **Service** (`service/`): Validation, store coordination. Throw **`S3Exception` constants** (e.g., `S3Exception.NO_SUCH_BUCKET`) — don't create new exception classes.
3. **Service** (`service/`): Validation, store coordination. Throw **`S3Exception` constants** (e.g., `S3Exception.NO_SUCH_BUCKET`) — see **[docs/SPRING.md](../docs/SPRING.md)** for exception handling rules.
4. **Controller** (`controller/`): HTTP mapping only — delegate all logic to services. Controllers never catch exceptions.

## Error Handling

- `S3MockExceptionHandler` converts `S3Exception` → XML `ErrorResponse` with the correct HTTP status
- `IllegalStateExceptionHandler` converts unexpected errors → `500 InternalError`
- Add new error types as constants in `S3Exception` — DON'T create new exception classes

See **[docs/SPRING.md](../docs/SPRING.md)** for exception handling patterns and rules.

## Testing

Expand Down
Loading