Compare commits

..

6 Commits

Author SHA1 Message Date
2e14ea06c9 WIP for activity modeling and migration.
Some checks failed
Backend CI / Run Maven Tests (pull_request) Failing after 4m32s
2025-08-04 21:07:46 +03:00
2a0d7fa1b8 fix: corrected path for the maven step
All checks were successful
Backend CI / Run Maven Tests (pull_request) Successful in 1m22s
2025-08-02 22:23:54 +03:00
823f91804a feat: added setup maven step to the backend pipeline
Some checks failed
Backend CI / Run Maven Tests (pull_request) Failing after 7s
2025-08-02 22:17:40 +03:00
c5433dfefd feat: removed backend pipeline step for caching m2
Some checks failed
Backend CI / Run Maven Tests (pull_request) Failing after 12s
2025-08-02 22:12:05 +03:00
8af5ea02cd feat: added backend ci pipeline and readme for pipelines
Some checks failed
Backend CI / Run Maven Tests (pull_request) Failing after 15s
2025-08-02 21:02:30 +03:00
f1dff9684b feat: initialized the backend project 2025-08-02 20:53:50 +03:00
11 changed files with 23 additions and 472 deletions

View File

@@ -1,32 +0,0 @@
## Brief overview
Project-specific frontend color theme guidelines for the vibing application. Defines a cohesive peach/coral/pink color palette with specific CSS custom properties, component styling patterns, and accessibility requirements.
## Color palette usage
- Always use the defined peach-coral color scheme: Primary Peach (#FFCDB2), Secondary Coral (#FFB4A2), Light Pink (#E5989B), Medium Mauve (#B5838D), Dark Purple (#6D6875)
- Implement CSS custom properties in src/index.css for all color definitions
- Use semantic color naming: --color-primary, --color-secondary, --color-bg-dark, --color-text-primary, etc.
- Support both dark and light modes using @media (prefers-color-scheme: light)
## Component styling patterns
- Buttons: Use gradient backgrounds, implement translateY(-2px) hover animations, apply consistent focus states with peach outline
- Cards: White backgrounds with subtle shadows, translateY(-8px) hover animations, 16px border radius, scale(1.05) image hover effects
- Links: Primary color #FFCDB2, hover colors #E5989B (dark mode) or #B5838D (light mode), no text decoration by default
- Use semantic CSS class names: .btn-primary, .btn-secondary, .btn-outline, .btn-ghost
## Interactive states and animations
- Apply subtle lift animations using transform: translateY(-2px) for hover effects
- Use rgba(255, 205, 178, 0.3) for focus rings and primary shadows
- Use rgba(229, 152, 155, 0.3) for secondary shadows
- Ensure hover states are distinct from focus states for accessibility
## File organization structure
- Place global styles and CSS custom properties in src/index.css
- Create individual .css files in src/components/ for component-specific styles
- Use src/App.css for utility classes
- Consider src/styles/ directory for additional theme files when needed
## Accessibility requirements
- Maintain minimum contrast ratios: 4.5:1 for normal text, 3:1 for large text
- Use --color-focus for focus indicators
- Test with color blindness simulators
- Ensure all interactive elements have proper focus states

View File

@@ -1,38 +0,0 @@
## Brief overview
Project-specific git workflow guidelines emphasizing feature branch development, conventional commit messages, and proper branch management practices. Never work directly on main/master branches.
## Branch management strategy
- Always create feature branches for all development work
- Use kebab-case naming: feature/user-authentication, bugfix/login-validation, hotfix/security-patch, refactor/api-endpoints
- Keep feature branches short-lived (1-3 days ideally)
- One feature per branch, don't mix different types of changes
- Delete branches after successful merge
## Commit message conventions
- Use conventional commit format: type(scope): description
- Use present tense ("add feature" not "added feature")
- Be specific and descriptive in commit messages
- Reference issue numbers when applicable: feat(auth): add OAuth login #123
- Make frequent, small commits with clear purposes
## Development workflow steps
- Always start by ensuring main branch is up to date: git checkout main && git pull origin main
- Create new feature branch: git checkout -b feature/your-feature-name
- Before pushing, rebase with main: git fetch origin && git rebase origin/main
- Push feature branch: git push origin feature/your-feature-name
- Create Pull Request from feature branch to main
- Only merge after code review approval
## Code review requirements
- Create PR for all changes, no direct commits to main
- Request reviews from team members
- Address all feedback before merging
- Ensure all tests pass before creating PR
- Self-review changes before requesting reviews
- Update documentation when needed
## Emergency procedures
- For critical issues, create hotfix branches from main
- Make minimal necessary changes in hotfixes
- Test thoroughly before creating emergency PR
- Follow same review process even for hotfixes

View File

@@ -41,4 +41,4 @@ jobs:
with: with:
name: backend-test-results name: backend-test-results
path: backend/target/surefire-reports/ path: backend/target/surefire-reports/
retention-days: 7 retention-days: 7

View File

@@ -5,28 +5,10 @@
```mermaid ```mermaid
classDiagram classDiagram
class Activity { class Activity {
+Long id
+String name +String name
+Location location +Location location
+Integer priceRange +Int priceRange
+List~String~ tags +List~String~ tags
+String description
+LocalDateTime createdAt
+LocalDateTime updatedAt
} }
class Location {
+Long id
+String name
+String address
+String city
+String country
+String postalCode
+Double latitude
+Double longitude
+LocalDateTime createdAt
+LocalDateTime updatedAt
}
Activity <-- Location : belongs to
``` ```

View File

@@ -74,26 +74,6 @@
<artifactId>spring-security-test</artifactId> <artifactId>spring-security-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.datafaker</groupId>
<artifactId>datafaker</artifactId>
<version>2.4.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>3.0</version>
<scope>test</scope>
</dependency>
<!-- JSON Processing --> <!-- JSON Processing -->
<dependency> <dependency>
@@ -152,4 +132,4 @@
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View File

@@ -4,7 +4,6 @@ import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.With;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -16,7 +15,6 @@ import java.time.LocalDateTime;
@Data @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@With
public class Location { public class Location {
@Id @Id

View File

@@ -1,18 +1,5 @@
CREATE SCHEMA IF NOT EXISTS vibing; CREATE SCHEMA IF NOT EXISTS vibing;
CREATE TABLE IF NOT EXISTS vibing.locations (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
address TEXT,
city TEXT,
country TEXT,
postal_code TEXT,
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS vibing.activities ( CREATE TABLE IF NOT EXISTS vibing.activities (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
@@ -33,3 +20,17 @@ CREATE TABLE IF NOT EXISTS vibing.activity_tags (
FOREIGN KEY (activity_id) REFERENCES vibing.activities(id) ON DELETE CASCADE, FOREIGN KEY (activity_id) REFERENCES vibing.activities(id) ON DELETE CASCADE,
PRIMARY KEY (activity_id, tag) PRIMARY KEY (activity_id, tag)
); );
CREATE TABLE IF NOT EXISTS vibing.locations (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
address TEXT,
city TEXT,
country TEXT,
postal_code TEXT,
latitude DOUBLE PRECISION,
longitude DOUBLE PRECISION,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@@ -1,277 +0,0 @@
package com.vibing.backend;
import com.vibing.backend.model.Activity;
import com.vibing.backend.model.Location;
import com.vibing.backend.repository.ActivityRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for Activity entity using H2 database.
* Tests repository operations and database interactions.
*/
@DataJpaTest
@ActiveProfiles("test")
@TestPropertySource(locations = "classpath:application-test.yml")
public class ActivityRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private ActivityRepository activityRepository;
private Location testLocation;
private Activity testActivity;
@BeforeEach
void setUp() {
// Create and persist test location
testLocation = new Location();
testLocation.setName("Test Location");
testLocation.setAddress("123 Test Street");
testLocation.setCity("Test City");
testLocation.setCountry("Test Country");
testLocation.setPostalCode("12345");
testLocation.setLatitude(40.7128);
testLocation.setLongitude(-74.0060);
testLocation = entityManager.persistAndFlush(testLocation);
// Create test activity
testActivity = new Activity();
testActivity.setName("Test Activity");
testActivity.setDescription("A test activity for integration testing");
testActivity.setLocation(testLocation);
testActivity.setPriceRange(3);
testActivity.setTags(Arrays.asList("test", "integration", "fun"));
}
@Test
void shouldSaveAndRetrieveActivity() {
// Given - activity is created in setUp()
// When
Activity savedActivity = activityRepository.save(testActivity);
entityManager.flush();
entityManager.clear();
Optional<Activity> retrievedActivity = activityRepository.findById(savedActivity.getId());
// Then
assertThat(retrievedActivity).isPresent();
assertThat(retrievedActivity.get().getName()).isEqualTo("Test Activity");
assertThat(retrievedActivity.get().getDescription()).isEqualTo("A test activity for integration testing");
assertThat(retrievedActivity.get().getPriceRange()).isEqualTo(3);
assertThat(retrievedActivity.get().getLocation().getName()).isEqualTo("Test Location");
assertThat(retrievedActivity.get().getTags()).containsExactlyInAnyOrder("test", "integration", "fun");
assertThat(retrievedActivity.get().getCreatedAt()).isNotNull();
assertThat(retrievedActivity.get().getUpdatedAt()).isNotNull();
}
@Test
void shouldFindActivitiesByNameContainingIgnoreCase() {
// Given
Activity activity1 = new Activity();
activity1.setName("Beach Volleyball");
activity1.setLocation(testLocation);
activity1.setPriceRange(2);
activity1.setTags(Arrays.asList("sport", "beach"));
Activity activity2 = new Activity();
activity2.setName("Mountain Hiking");
activity2.setLocation(testLocation);
activity2.setPriceRange(1);
activity2.setTags(Arrays.asList("outdoor", "hiking"));
activityRepository.saveAll(Arrays.asList(activity1, activity2));
entityManager.flush();
// When
List<Activity> beachActivities = activityRepository.findByNameContainingIgnoreCase("beach");
List<Activity> mountainActivities = activityRepository.findByNameContainingIgnoreCase("MOUNTAIN");
// Then
assertThat(beachActivities).hasSize(1);
assertThat(beachActivities.get(0).getName()).isEqualTo("Beach Volleyball");
assertThat(mountainActivities).hasSize(1);
assertThat(mountainActivities.get(0).getName()).isEqualTo("Mountain Hiking");
}
@Test
void shouldFindActivitiesByPriceRange() {
// Given
Activity cheapActivity = new Activity();
cheapActivity.setName("Free Walking Tour");
cheapActivity.setLocation(testLocation);
cheapActivity.setPriceRange(1);
cheapActivity.setTags(Arrays.asList("free", "walking"));
Activity expensiveActivity = new Activity();
expensiveActivity.setName("Luxury Spa Day");
expensiveActivity.setLocation(testLocation);
expensiveActivity.setPriceRange(5);
expensiveActivity.setTags(Arrays.asList("luxury", "spa"));
activityRepository.saveAll(Arrays.asList(cheapActivity, expensiveActivity));
entityManager.flush();
// When
List<Activity> cheapActivities = activityRepository.findByPriceRange(1);
List<Activity> expensiveActivities = activityRepository.findByPriceRange(5);
// Then
assertThat(cheapActivities).hasSize(1);
assertThat(cheapActivities.get(0).getName()).isEqualTo("Free Walking Tour");
assertThat(expensiveActivities).hasSize(1);
assertThat(expensiveActivities.get(0).getName()).isEqualTo("Luxury Spa Day");
}
@Test
void shouldFindActivitiesByLocationId() {
// Given
Location anotherLocation = new Location();
anotherLocation.setName("Another Location");
anotherLocation.setCity("Another City");
anotherLocation = entityManager.persistAndFlush(anotherLocation);
Activity activityAtTestLocation = new Activity();
activityAtTestLocation.setName("Activity at Test Location");
activityAtTestLocation.setLocation(testLocation);
activityAtTestLocation.setPriceRange(2);
Activity activityAtAnotherLocation = new Activity();
activityAtAnotherLocation.setName("Activity at Another Location");
activityAtAnotherLocation.setLocation(anotherLocation);
activityAtAnotherLocation.setPriceRange(3);
activityRepository.saveAll(Arrays.asList(activityAtTestLocation, activityAtAnotherLocation));
entityManager.flush();
// When
List<Activity> activitiesAtTestLocation = activityRepository.findByLocationId(testLocation.getId());
List<Activity> activitiesAtAnotherLocation = activityRepository.findByLocationId(anotherLocation.getId());
// Then
assertThat(activitiesAtTestLocation).hasSize(1);
assertThat(activitiesAtTestLocation.get(0).getName()).isEqualTo("Activity at Test Location");
assertThat(activitiesAtAnotherLocation).hasSize(1);
assertThat(activitiesAtAnotherLocation.get(0).getName()).isEqualTo("Activity at Another Location");
}
@Test
void shouldFindActivitiesByTagsContaining() {
// Given
Activity outdoorActivity = new Activity();
outdoorActivity.setName("Rock Climbing");
outdoorActivity.setLocation(testLocation);
outdoorActivity.setPriceRange(3);
outdoorActivity.setTags(Arrays.asList("outdoor", "adventure", "climbing"));
Activity indoorActivity = new Activity();
indoorActivity.setName("Museum Visit");
indoorActivity.setLocation(testLocation);
indoorActivity.setPriceRange(2);
indoorActivity.setTags(Arrays.asList("indoor", "culture", "educational"));
activityRepository.saveAll(Arrays.asList(outdoorActivity, indoorActivity));
entityManager.flush();
// When
List<Activity> outdoorActivities = activityRepository.findByTagsContaining("outdoor");
List<Activity> cultureActivities = activityRepository.findByTagsContaining("culture");
// Then
assertThat(outdoorActivities).hasSize(1);
assertThat(outdoorActivities.get(0).getName()).isEqualTo("Rock Climbing");
assertThat(cultureActivities).hasSize(1);
assertThat(cultureActivities.get(0).getName()).isEqualTo("Museum Visit");
}
@Test
void shouldFindActivitiesByPriceRangeLessThanEqual() {
// Given
Activity cheapActivity = new Activity();
cheapActivity.setName("Budget Activity");
cheapActivity.setLocation(testLocation);
cheapActivity.setPriceRange(1);
Activity moderateActivity = new Activity();
moderateActivity.setName("Moderate Activity");
moderateActivity.setLocation(testLocation);
moderateActivity.setPriceRange(3);
Activity expensiveActivity = new Activity();
expensiveActivity.setName("Expensive Activity");
expensiveActivity.setLocation(testLocation);
expensiveActivity.setPriceRange(5);
activityRepository.saveAll(Arrays.asList(cheapActivity, moderateActivity, expensiveActivity));
entityManager.flush();
// When
List<Activity> budgetActivities = activityRepository.findByPriceRangeLessThanEqual(2);
List<Activity> moderateBudgetActivities = activityRepository.findByPriceRangeLessThanEqual(3);
// Then
assertThat(budgetActivities).hasSize(1);
assertThat(budgetActivities.get(0).getName()).isEqualTo("Budget Activity");
assertThat(moderateBudgetActivities).hasSize(2);
assertThat(moderateBudgetActivities)
.extracting(Activity::getName)
.containsExactlyInAnyOrder("Budget Activity", "Moderate Activity");
}
@Test
void shouldUpdateActivityTimestamps() {
// Given
Activity savedActivity = activityRepository.save(testActivity);
entityManager.flush();
entityManager.clear();
// When
Optional<Activity> retrievedActivity = activityRepository.findById(savedActivity.getId());
assertThat(retrievedActivity).isPresent();
Activity activityToUpdate = retrievedActivity.get();
activityToUpdate.setName("Updated Activity Name");
Activity updatedActivity = activityRepository.save(activityToUpdate);
entityManager.flush();
// Then
assertThat(updatedActivity.getCreatedAt()).isNotNull();
assertThat(updatedActivity.getUpdatedAt()).isNotNull();
assertThat(updatedActivity.getUpdatedAt()).isAfterOrEqualTo(updatedActivity.getCreatedAt());
}
@Test
void shouldDeleteActivity() {
// Given
Activity savedActivity = activityRepository.save(testActivity);
entityManager.flush();
Long activityId = savedActivity.getId();
// When
activityRepository.deleteById(activityId);
entityManager.flush();
// Then
Optional<Activity> deletedActivity = activityRepository.findById(activityId);
assertThat(deletedActivity).isEmpty();
}
}

View File

@@ -1,46 +0,0 @@
package com.vibing.backend;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import java.time.LocalDateTime;
import java.util.Random;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import com.vibing.backend.model.Activity;
import com.vibing.backend.model.Location;
import com.vibing.backend.repository.ActivityRepository;
import com.vibing.backend.service.ActivityService;
import net.datafaker.Faker;
@SpringBootTest
@ActiveProfiles("test")
@TestPropertySource(locations = "classpath:application-test.yml")
public class ActivityTestIt {
private Faker faker = new Faker(new Random(123));
@Autowired
private ActivityService activityService;
@Autowired
private ActivityRepository activityRepository;
@Test
void shouldSaveActivity() {
Activity activity = new Activity();
activity.setName(faker.location().publicSpace());
activity.setPriceRange(faker.number().numberBetween(1, 5));
activity.setTags(faker.lorem().words(3));
activity.setLocation(new Location().withName(faker.location().publicSpace()));
activityRepository.save(activity);
assertThat(activityService.findAll(), containsInAnyOrder(activity));
}
}

View File

@@ -2,17 +2,15 @@ package com.vibing.backend;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
/** /**
* Basic test class for the Vibing Backend application. * Basic test class for the Vibing Backend application.
*/ */
@SpringBootTest @SpringBootTest
@ActiveProfiles("test")
class VibingBackendApplicationTests { class VibingBackendApplicationTests {
@Test @Test
void contextLoads() { void contextLoads() {
// This test verifies that the Spring application context loads successfully // This test verifies that the Spring application context loads successfully
} }
} }

View File

@@ -1,25 +1,10 @@
spring: spring:
datasource: datasource:
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;MODE=PostgreSQL;INIT=CREATE SCHEMA IF NOT EXISTS vibing url: jdbc:postgresql://localhost:5432/your_test_database
username: sa username: test_user
password: password: test_password
driver-class-name: org.h2.Driver
h2:
console:
enabled: true
jpa:
hibernate:
ddl-auto: none # Let Flyway handle schema creation
show-sql: true
database-platform: org.hibernate.dialect.H2Dialect
properties:
hibernate:
default_schema: vibing
flyway: flyway:
enabled: true enabled: true
clean-disabled: false # Allow clean in tests clean-disabled: false # Allow clean in tests
locations: classpath:db/migration locations: classpath:db/migration
default-schema: vibing