Skip to content

Commit 1335d20

Browse files
authored
Merge pull request #1 from sameboat-platform/feat/auth-testing-fixes
Feat/auth testing fixes
2 parents a76fd68 + de2061b commit 1335d20

File tree

20 files changed

+317
-83
lines changed

20 files changed

+317
-83
lines changed

.github/dependabot.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
version: 2
2+
updates:
3+
# Maven dependencies (pom.xml)
4+
- package-ecosystem: maven
5+
directory: "/"
6+
schedule:
7+
interval: weekly
8+
day: sunday
9+
time: "05:30"
10+
timezone: "America/Chicago"
11+
open-pull-requests-limit: 5
12+
ignore:
13+
# optional: avoid major jumps until you're ready
14+
- dependency-name: "org.flywaydb:flyway-core"
15+
update-types: ["version-update:semver-major"]
16+
- dependency-name: "org.springframework.boot:*"
17+
update-types: ["version-update:semver-major"]
18+
19+
# GitHub Actions
20+
- package-ecosystem: "github-actions"
21+
directory: "/"
22+
schedule:
23+
interval: weekly
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: Backend Coverage
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
coverage:
11+
runs-on: ubuntu-latest
12+
permissions:
13+
contents: read
14+
actions: read
15+
checks: write
16+
pull-requests: write
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0
22+
23+
- name: Set up JDK 21
24+
uses: actions/setup-java@v4
25+
with:
26+
distribution: temurin
27+
java-version: '21'
28+
cache: maven
29+
30+
- name: Ensure wrapper & scripts are executable
31+
run: |
32+
chmod +x mvnw || true
33+
chmod +x scripts/check-migration-immutability.sh || true
34+
35+
- name: Build & Test (with coverage)
36+
run: |
37+
./mvnw -B -ntp clean verify
38+
39+
- name: Upload JaCoCo HTML report
40+
if: always()
41+
uses: actions/upload-artifact@v4
42+
with:
43+
name: jacoco-report
44+
path: target/site/jacoco
45+
46+
- name: Extract coverage summary
47+
id: coverage
48+
run: |
49+
LINE=$(grep -m1 -E '<counter type="INSTRUCTION"' target/site/jacoco/jacoco.xml || true)
50+
# Fallback if grep fails
51+
if [ -z "$LINE" ]; then echo "instr_covered=0" >> $GITHUB_OUTPUT; echo "instr_missed=0" >> $GITHUB_OUTPUT; exit 0; fi
52+
MISSED=$(echo "$LINE" | sed -E 's/.*missed="([0-9]+)" covered="([0-9]+)".*/\1/')
53+
COVERED=$(echo "$LINE" | sed -E 's/.*missed="([0-9]+)" covered="([0-9]+)".*/\2/')
54+
TOTAL=$((MISSED + COVERED))
55+
if [ "$TOTAL" -gt 0 ]; then PCT=$(( 100 * COVERED / TOTAL )); else PCT=0; fi
56+
echo "instr_covered=$COVERED" >> $GITHUB_OUTPUT
57+
echo "instr_missed=$MISSED" >> $GITHUB_OUTPUT
58+
echo "instr_total=$TOTAL" >> $GITHUB_OUTPUT
59+
echo "instr_pct=$PCT" >> $GITHUB_OUTPUT
60+
61+
- name: Add PR comment with coverage (if PR)
62+
if: github.event_name == 'pull_request'
63+
uses: marocchino/sticky-pull-request-comment@v2
64+
with:
65+
header: coverage-report
66+
message: |
67+
**Coverage (Instructions)**: ${{ steps.coverage.outputs.instr_pct }}%
68+
Covered: ${{ steps.coverage.outputs.instr_covered }} / ${{ steps.coverage.outputs.instr_total }}
69+
(Missed: ${{ steps.coverage.outputs.instr_missed }})
70+
71+
- name: Create coverage summary
72+
if: always()
73+
run: |
74+
echo "### Coverage Summary" >> $GITHUB_STEP_SUMMARY
75+
echo "Instructions: ${{ steps.coverage.outputs.instr_pct }}% (covered=${{ steps.coverage.outputs.instr_covered }}, missed=${{ steps.coverage.outputs.instr_missed }})" >> $GITHUB_STEP_SUMMARY
76+

MIGRATION_NOTES.md

Whitespace-only changes.

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
![Backend CI](https://github.com/ArchILLtect/sameboat-backend/actions/workflows/backend-ci.yml/badge.svg)
1+
![Backend CI](https://github.com/sameboat-platform/sameboat-backend/actions/workflows/backend-ci.yml/badge.svg)
22
# SameBoat Backend (Spring Boot + Java 21)
33

4+
> Repository migration: moved from `ArchILLtect/*` to `sameboat-platform/*` on 2025-09-28. Update any local remotes:
5+
> ```bash
6+
> git remote set-url origin git@github.com:sameboat-platform/sameboat-backend.git
7+
> ```
8+
49
> Quick Links: [Instructions (setup & migrations)](./docs/instructions.md) | [API Reference](./docs/api.md) | [Week 3 Plan](./docs/Weekly%20Plan%20Docs/week_3_plan_same_boat_honors_project_fall_2025.md) | Journals: [Index](./docs/journals/README.md) · [Week 1](./docs/journals/Week1-Journal.md) · [Week 2](./docs/journals/Week2-Journal.md)
510
611
## Run locally

docs/journals/Week1-Journal.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ Week 1
22
9/13/2025
33

44
Tasks Completed:
5-
- Created two repos: [sameboat-frontend](https://github.com/ArchILLtect/sameboat-frontend) and [sameboat-backend](https://github.com/ArchILLtect/sameboat-backend).
5+
- Created two repos: [sameboat-frontend](https://github.com/sameboat-platform/sameboat-frontend) and [sameboat-backend](https://github.com/sameboat-platform/sameboat-backend).
66
- Scaffolded both repos (frontend with Vite + React + TS, backend with Spring Boot 3 + Java 21).
77
- Installed core backend dependencies (Spring Web, JPA, Validation, Lombok, Flyway, PostgreSQL Driver, DevTools).
88
- Set up Neon PostgreSQL database, created initial `sameboat` DB, and ran Flyway migration V1__init.sql with `users`, `stories`, and `trust_events` tables.
@@ -19,4 +19,4 @@ This week was all about laying down the foundation. It felt amazing to hit that
1919

2020
Biggest win? Seeing the repos fully structured, DB initialized, CI/CD passing, and docs + calendar all organized. It feels like a *real project* now, not just an idea. Honestly, I’m still riding that high.
2121

22-
Next week’s focus: auth stubs + basic user profiles. Time to move from foundation into actual feature work. WOOHOO! Finally!
22+
Next week’s focus: auth stubs + basic user profiles. Time to move from foundation into actual feature work. WOOHOO! Finally!

mvnw

100644100755
File mode changed.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.sameboat.backend.auth;
2+
// ...existing code...
3+
import org.springframework.test.context.ActiveProfiles;
4+
import org.springframework.test.context.TestPropertySource;
5+
// ...existing code...
6+
@DataJpaTest
7+
@Import(SessionService.class)
8+
@ActiveProfiles("test")
9+
@TestPropertySource(properties = "sameboat.security.enabled=false")
10+
class SessionServiceTest {
11+
// ...existing code...
12+

scripts/check-migration-immutability.sh

100644100755
File mode changed.
Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,59 @@
11
package com.sameboat.backend.auth.session;
22

3+
import org.springframework.beans.factory.ObjectProvider;
34
import org.springframework.stereotype.Service;
5+
import org.springframework.transaction.annotation.Transactional;
46

5-
import java.time.Duration;
6-
import java.time.OffsetDateTime;
7+
import java.time.*;
78
import java.util.Optional;
89
import java.util.UUID;
910

11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
13+
1014
@Service
15+
@Transactional
1116
public class SessionService {
1217

18+
private static final Logger log = LoggerFactory.getLogger(SessionService.class);
19+
1320
private final SessionRepository repository;
21+
private final Clock clock;
1422

15-
public SessionService(SessionRepository repository) {
23+
public SessionService(SessionRepository repository, ObjectProvider<java.time.Clock> clockProvider) {
1624
this.repository = repository;
25+
this.clock = clockProvider.getIfAvailable(java.time.Clock::systemUTC);
1726
}
1827

1928
public SessionEntity createSession(UUID userId, Duration ttl) {
2029
SessionEntity s = new SessionEntity();
2130
s.setUserId(userId);
22-
s.setExpiresAt(OffsetDateTime.now().plus(ttl));
23-
return repository.save(s);
31+
s.setExpiresAt(OffsetDateTime.ofInstant(clock.instant().plus(ttl), ZoneOffset.UTC));
32+
SessionEntity saved = repository.save(s);
33+
repository.flush(); // ensure persistence before returning (visibility for immediate follow-up request)
34+
log.debug("Created session id={} userId={} expiresAt={} nowUTC={}", saved.getId(), userId, saved.getExpiresAt(), clock.instant());
35+
return saved;
2436
}
2537

2638
public Optional<SessionEntity> findValid(UUID id) {
39+
Instant now = clock.instant();
2740
return repository.findById(id)
28-
.filter(s -> s.getExpiresAt() != null && s.getExpiresAt().isAfter(OffsetDateTime.now()));
41+
.filter(s -> s.getExpiresAt() != null && s.getExpiresAt().toInstant().isAfter(now));
2942
}
3043

31-
public Optional<SessionEntity> findById(UUID id) {
32-
return repository.findById(id);
33-
}
44+
public Optional<SessionEntity> findById(UUID id) { return repository.findById(id); }
3445

3546
public void touch(SessionEntity s) {
36-
s.setLastSeenAt(OffsetDateTime.now());
47+
s.setLastSeenAt(OffsetDateTime.ofInstant(clock.instant(), ZoneOffset.UTC));
3748
repository.save(s);
3849
}
3950

4051
public void invalidate(String token) {
4152
try {
4253
UUID id = UUID.fromString(token);
4354
repository.deleteById(id);
44-
} catch (IllegalArgumentException ignored) {
45-
// ignore invalid format
46-
}
55+
} catch (IllegalArgumentException ignored) { }
4756
}
57+
58+
public long count() { return repository.count(); }
4859
}

src/main/java/com/sameboat/backend/common/GlobalExceptionHandler.java

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import org.slf4j.Logger;
44
import org.slf4j.LoggerFactory;
5-
import org.springframework.http.HttpHeaders;
65
import org.springframework.http.HttpStatus;
6+
import org.springframework.http.HttpHeaders;
77
import org.springframework.http.HttpStatusCode;
88
import org.springframework.http.ResponseEntity;
99
import org.springframework.lang.NonNull;
@@ -13,13 +13,19 @@
1313
import org.springframework.web.context.request.WebRequest;
1414
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
1515

16+
import jakarta.validation.ConstraintViolationException;
17+
1618
import java.util.stream.Collectors;
1719

1820
@ControllerAdvice
1921
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
2022

2123
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
2224

25+
private ResponseEntity<ErrorResponse> build(HttpStatus status, String code, String message) {
26+
return ResponseEntity.status(status).body(new ErrorResponse(code, message));
27+
}
28+
2329
@Override
2430
protected ResponseEntity<Object> handleMethodArgumentNotValid(@NonNull MethodArgumentNotValidException ex,
2531
@NonNull HttpHeaders headers,
@@ -33,15 +39,21 @@ protected ResponseEntity<Object> handleMethodArgumentNotValid(@NonNull MethodArg
3339
.body(new ErrorResponse("VALIDATION_ERROR", msg));
3440
}
3541

42+
@ExceptionHandler(ConstraintViolationException.class)
43+
public ResponseEntity<ErrorResponse> handleConstraintViolation(ConstraintViolationException ex) {
44+
String msg = ex.getConstraintViolations().stream().findFirst()
45+
.map(cv -> cv.getPropertyPath() + " " + cv.getMessage())
46+
.orElse("Validation failed");
47+
return build(HttpStatus.BAD_REQUEST, "VALIDATION_ERROR", msg);
48+
}
49+
3650
@ExceptionHandler(IllegalArgumentException.class)
37-
public ResponseEntity<ErrorResponse> handleIllegalArgument(@NonNull IllegalArgumentException ex) {
38-
log.warn("Bad request: {}", ex.getMessage());
39-
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
40-
.body(new ErrorResponse("BAD_REQUEST", ex.getMessage()));
51+
public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException ex) {
52+
return build(HttpStatus.BAD_REQUEST, "BAD_REQUEST", ex.getMessage());
4153
}
4254

4355
@ExceptionHandler(Exception.class)
44-
public ResponseEntity<ErrorResponse> handleGeneric(@NonNull Exception ex) {
56+
public ResponseEntity<ErrorResponse> handleGeneric(Exception ex) {
4557
String id = java.util.UUID.randomUUID().toString();
4658
log.error("Unhandled exception traceId={}", id, ex);
4759
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)

0 commit comments

Comments
 (0)