Skip to content
Open
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
18 changes: 11 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

## Project Structure

- `boot/` - Kotlin/Spring Boot backend (JDK 21)
- `ui/` - Angular 17 frontend
- `electron/` - Electron desktop app wrapper
- `boot/` - Kotlin/Spring Boot backend (JDK 21) with SQLite, Liquibase, Spring Cloud OpenFeign
- `ui/` - Angular 17 frontend with Angular Material, SCSS
- `electron/` - Electron desktop app using electron-vite + electron-builder
- `cypress/` - End-to-end tests
- `jdktool/` - Utility for downloading JDK for Electron packaging
- `docker-compose.yml` - Docker deployment config

## Build Commands

Expand All @@ -23,8 +25,8 @@ cd ui && npm install && npm run build

### Electron App
```bash
cd ui && npm run build --prefix ui -- --base-href=./ # Build UI first
cd electron && npm start # Dev mode
cd electron && npm run build # Builds UI, downloads JDK, packages app
cd electron && npm run start # Dev mode (runs UI + Electron)
```

### End-to-End Tests
Expand All @@ -36,8 +38,10 @@ npm test --prefix cypress
## Key Facts

- JAR output: `boot/build/libs/tp2intervals.jar`
- UI is built to `ui/dist/ui/browser` and copied to `boot/src/main/resources/static` during CI
- UI is built to `ui/dist/ui/browser` and served by Spring Boot
- Version: read from `boot/version`
- Backend tests use WireMock (files in `config/mock/`)
- Integration tests named `*IT.kt`
- **Unit tests named `*Test.kt`**
- **Integration tests named `*IT.kt`**
- Frontend uses SCSS, Angular Material, proxy config at `ui/src/proxy.conf.json`
- Electron app downloads JDK dynamically during build (platform-specific)
6 changes: 6 additions & 0 deletions boot/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ dependencies {
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("org.junit.jupiter:junit-jupiter-engine")
testImplementation("org.wiremock:wiremock-standalone:3.5.2")
testImplementation("io.mockk:mockk:1.13.10")
testImplementation("io.strikt:strikt-core:0.34.1")
}

tasks.withType<Test> {
systemProperty("kotlin.daemon.jvm.options", "-Xmx1024m")
}

springBoot {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ class IntervalsToTargetConverter(
thresholdValue: Double,
resolvedStepValueDTO: IntervalsWorkoutDocDTO.ResolvedStepValueDTO
): Pair<Int, Int> {
val rangeStart = Math.round((resolvedStepValueDTO.start / thresholdValue) * 100).toInt()
val rangeEnd = Math.round((resolvedStepValueDTO.end / thresholdValue) * 100).toInt()
val rangeStart = Math.round(resolvedStepValueDTO.start * thresholdValue).toInt()
val rangeEnd = Math.round(resolvedStepValueDTO.end * thresholdValue).toInt()
return rangeStart to rangeEnd
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package org.freekode.tp2intervals.infrastructure.platform.trainingpeaks.workout.
import org.freekode.tp2intervals.domain.workout.structure.StepTarget

class TPStepDTO(
var name: String?,
var length: TPLengthDTO?,
var name: String? = null,
var length: TPLengthDTO? = null,
var targets: List<TPTargetDTO> = listOf(),
) {
fun toMainTarget(): StepTarget {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.freekode.tp2intervals

import config.BaseSpringITConfig
import org.freekode.tp2intervals.config.BaseSpringITConfig
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.freekode.tp2intervals.app.activity

import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.freekode.tp2intervals.domain.Platform
import org.freekode.tp2intervals.domain.TrainingType
import org.freekode.tp2intervals.domain.activity.Activity
import org.freekode.tp2intervals.domain.activity.ActivityRepository
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.LocalDate
import java.time.LocalDateTime

class ActivityServiceTest {

private val trainingPeaksRepo: ActivityRepository = mockk(relaxed = true)
private val intervalsRepo: ActivityRepository = mockk(relaxed = true)

private lateinit var service: ActivityService

@BeforeEach
fun setup() {
every { trainingPeaksRepo.platform() } returns Platform.TRAINING_PEAKS
every { intervalsRepo.platform() } returns Platform.INTERVALS

service = ActivityService(listOf(trainingPeaksRepo, intervalsRepo))
}

@Test
fun `should sync activities with resource`() {
val startDate = LocalDate.of(2024, 1, 1)
val endDate = LocalDate.of(2024, 1, 7)
val activity1 = createActivity(TrainingType.BIKE, "Ride 1", "resource-data-1")
val activity2 = createActivity(TrainingType.RUN, "Run 1", "resource-data-2")
every { trainingPeaksRepo.getActivities(startDate, endDate, TrainingType.DEFAULT_LIST) } returns listOf(activity1, activity2)

val request = CopyActivitiesRequest(
startDate, endDate,
TrainingType.DEFAULT_LIST,
sourcePlatform = Platform.TRAINING_PEAKS,
targetPlatform = Platform.INTERVALS
)
val response = service.syncActivities(request)

assert(response.copied == 2)
assert(response.filteredOut == 0)
verify { intervalsRepo.saveActivities(listOf(activity1, activity2)) }
}

@Test
fun `should filter activities without resource`() {
val startDate = LocalDate.of(2024, 1, 1)
val endDate = LocalDate.of(2024, 1, 7)
val activityWithResource = createActivity(TrainingType.BIKE, "Ride", "resource")
val activityWithoutResource = createActivity(TrainingType.RUN, "Run", null)
every { trainingPeaksRepo.getActivities(startDate, endDate, TrainingType.DEFAULT_LIST) } returns listOf(activityWithResource, activityWithoutResource)

val request = CopyActivitiesRequest(
startDate, endDate,
TrainingType.DEFAULT_LIST,
sourcePlatform = Platform.TRAINING_PEAKS,
targetPlatform = Platform.INTERVALS
)
val response = service.syncActivities(request)

assert(response.copied == 1)
assert(response.filteredOut == 1)
verify { intervalsRepo.saveActivities(match { it.first().title == "Ride" }) }
}

@Test
fun `should return empty response when no activities`() {
val startDate = LocalDate.of(2024, 1, 1)
val endDate = LocalDate.of(2024, 1, 7)
every { trainingPeaksRepo.getActivities(startDate, endDate, TrainingType.DEFAULT_LIST) } returns emptyList()

val request = CopyActivitiesRequest(
startDate, endDate,
TrainingType.DEFAULT_LIST,
sourcePlatform = Platform.TRAINING_PEAKS,
targetPlatform = Platform.INTERVALS
)
val response = service.syncActivities(request)

assert(response.copied == 0)
assert(response.filteredOut == 0)
}

private fun createActivity(type: TrainingType, title: String, resource: String?): Activity {
return Activity(
startedAt = LocalDateTime.now(),
type = type,
title = title,
resource = resource
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package org.freekode.tp2intervals.app.plan

import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.freekode.tp2intervals.domain.ExternalData
import org.freekode.tp2intervals.domain.Platform
import org.freekode.tp2intervals.domain.TrainingType
import org.freekode.tp2intervals.domain.librarycontainer.LibraryContainer
import org.freekode.tp2intervals.domain.librarycontainer.LibraryContainerRepository
import org.freekode.tp2intervals.domain.workout.Workout
import org.freekode.tp2intervals.domain.workout.WorkoutDetails
import org.freekode.tp2intervals.domain.workout.WorkoutRepository
import org.freekode.tp2intervals.domain.workout.structure.StepModifier
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.time.LocalDate

class LibraryServiceTest {

private val trainingPeaksRepo: WorkoutRepository = mockk(relaxed = true)
private val intervalsRepo: WorkoutRepository = mockk(relaxed = true)
private val trainingPeaksPlanRepo: LibraryContainerRepository = mockk(relaxed = true)
private val intervalsPlanRepo: LibraryContainerRepository = mockk(relaxed = true)

private lateinit var service: LibraryService

@BeforeEach
fun setup() {
every { trainingPeaksRepo.platform() } returns Platform.TRAINING_PEAKS
every { intervalsRepo.platform() } returns Platform.INTERVALS
every { trainingPeaksPlanRepo.platform() } returns Platform.TRAINING_PEAKS
every { intervalsPlanRepo.platform() } returns Platform.INTERVALS

service = LibraryService(
listOf(trainingPeaksRepo, intervalsRepo),
listOf(trainingPeaksPlanRepo, intervalsPlanRepo)
)
}

@Test
fun `should find libraries by platform`() {
val library = LibraryContainer("My Plan", LocalDate.now(), true, 10, ExternalData.empty())
every { trainingPeaksPlanRepo.getLibraryContainers() } returns listOf(library)

val result = service.findByPlatform(Platform.TRAINING_PEAKS)

assert(result.size == 1)
assert(result.first().name == "My Plan")
}

@Test
fun `should copy library with workouts`() {
val sourceLibrary = LibraryContainer("Source Plan", LocalDate.now(), true, 5, ExternalData.empty())
val targetLibrary = LibraryContainer("Target Plan", LocalDate.now(), true, 3, ExternalData.empty())
val workout1 = createWorkout("Workout 1", LocalDate.now())
val workout2 = createWorkout("Workout 2", LocalDate.now().plusDays(1))
every { trainingPeaksRepo.getWorkoutsFromLibrary(sourceLibrary) } returns listOf(workout1, workout2)
every { intervalsPlanRepo.createLibraryContainer("New Plan", true, LocalDate.now()) } returns targetLibrary

val request = CopyLibraryRequest(
libraryContainer = sourceLibrary,
newName = "New Plan",
stepModifier = StepModifier.NONE,
sourcePlatform = Platform.TRAINING_PEAKS,
targetPlatform = Platform.INTERVALS
)
val response = service.copyLibrary(request)

assert(response.planName == "Target Plan")
assert(response.workouts == 2)
verify { intervalsRepo.saveWorkoutsToLibrary(targetLibrary, match { it.size == 2 }) }
}

@Test
fun `should delete library`() {
val externalData = ExternalData.empty()

service.deleteLibrary(DeleteLibraryRequest(externalData, Platform.INTERVALS))

verify { intervalsPlanRepo.deleteLibraryContainer(externalData) }
}

@Test
fun `should create library container`() {
val library = LibraryContainer("New Library", LocalDate.now(), false, 0, ExternalData.empty())
every { intervalsPlanRepo.createLibraryContainer("New Library", false, null) } returns library

val result = service.create(CreateLibraryContainerRequest("New Library", Platform.INTERVALS))

assert(result.name == "New Library")
assert(result.isPlan == false)
}

@Test
fun `should apply step modifier when copying library`() {
val sourceLibrary = LibraryContainer("Source", LocalDate.now(), true, 1, ExternalData.empty())
val targetLibrary = LibraryContainer("Target", LocalDate.now(), true, 1, ExternalData.empty())
val workout = createWorkout("Workout", LocalDate.now())
every { trainingPeaksRepo.getWorkoutsFromLibrary(sourceLibrary) } returns listOf(workout)
every { intervalsPlanRepo.createLibraryContainer(any(), any(), any()) } returns targetLibrary

val request = CopyLibraryRequest(
libraryContainer = sourceLibrary,
newName = "New",
stepModifier = StepModifier.POWER_10S,
sourcePlatform = Platform.TRAINING_PEAKS,
targetPlatform = Platform.INTERVALS
)
service.copyLibrary(request)

verify { intervalsRepo.saveWorkoutsToLibrary(targetLibrary, match { it.first().structure?.modifier == StepModifier.POWER_10S }) }
}

private fun createWorkout(name: String, date: LocalDate): Workout {
return Workout(
WorkoutDetails(
TrainingType.BIKE, name, null, null, null, ExternalData.empty()
),
date,
null
)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.freekode.tp2intervals.app.workout

import config.BaseSpringITConfig
import org.freekode.tp2intervals.config.BaseSpringITConfig
import org.freekode.tp2intervals.app.plan.CreateLibraryContainerRequest
import org.freekode.tp2intervals.app.plan.DeleteLibraryRequest
import org.freekode.tp2intervals.app.plan.LibraryService
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.freekode.tp2intervals.app.workout

import config.BaseSpringITConfig
import org.freekode.tp2intervals.config.BaseSpringITConfig
import org.freekode.tp2intervals.app.plan.CopyLibraryRequest
import org.freekode.tp2intervals.app.plan.DeleteLibraryRequest
import org.freekode.tp2intervals.app.plan.LibraryService
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.freekode.tp2intervals.app.workout

import config.BaseSpringITConfig
import org.freekode.tp2intervals.config.BaseSpringITConfig
import org.assertj.core.api.Assertions.assertThat
import org.freekode.tp2intervals.app.workout.schedule.C2CTodayScheduledRequest
import org.freekode.tp2intervals.app.workout.schedule.WorkoutScheduledJob
Expand Down
Loading
Loading