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
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Build stage
FROM gradle:8.3.0-jdk17 AS build

COPY build.gradle /app/
COPY settings.gradle /app/

WORKDIR /app

RUN --mount=type=secret,id=github.username \
--mount=type=secret,id=github.token \
gradle resolveDependencies --no-daemon

COPY src/main /app/src/main/

RUN gradle bootJar -x test --no-daemon

# Runtime
FROM wodby/openjdk:17-jre-alpine AS runtime
COPY --from=build /app/build/libs/*.jar app.jar
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75 -XX:+UseG1GC"
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
21 changes: 20 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,23 @@ Sonata Playlist provides REST API to create, delete and manage playlists.
- [Fetch playlist cover image](docs/Get_playlist_cover_image.MD)
- [Change playlist details](docs/Change_playlist_details.MD)
- [Add items to playlist](docs/Add_item_to_playlist.MD)
- [Get items of the playlist](docs/Get_playlist_items.MD)
- [Get items of the playlist](docs/Get_playlist_items.MD)

## Build locally


Set required env variables
```
export GH_USERNAME=username
export GH_ACCESS_TOKEN=access_token
```

Build image
```
docker build --secret id=github.username,env=GH_USERNAME --secret id=github.token,env=GH_ACCESS_TOKEN -t "playlists" .
```

Run container
```
docker run -e DATABASE_CONNECTION_URL=r2dbc:postgresql://playlists-postgres:5432/postgres -e DATABASE_MIGRATION_CONNECTION_URL=jdbc:postgresql://playlists-postgres:5432/postgres -e DATABASE_PASSWORD=root -e DATABASE_USERNAME=postgres --name "playlists.local" --network playlists --memory="256M" --cpus=0.5 -d playlists:latest
```
28 changes: 26 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,17 @@ repositories {
name = "GitHub-Packages"
url = "https://maven.pkg.github.com/Project-Sonata/Suite"
credentials {
username = System.getenv("GITHUB_USERNAME")
password = System.getenv("GITHUB_ACCESS_TOKEN")
username = new File("/run/secrets/github.username").exists()
?
new File("/run/secrets/github.username").getText("UTF-8")
:
System.getenv("GITHUB_USERNAME")

password = new File("/run/secrets/github.token").exists()
?
new File("/run/secrets/github.token").getText("UTF-8")
:
System.getenv("GITHUB_ACCESS_TOKEN")
}
}
}
Expand Down Expand Up @@ -74,6 +83,21 @@ dependencyManagement {
}
}

tasks.register('resolveDependencies') {
doLast {
def resolve = {
ConfigurationContainer configurations ->
configurations
.findAll({ Configuration c -> c.isCanBeResolved() })
.each({ c -> c.resolve() })
}
project.rootProject.allprojects.each { subProject ->
resolve(subProject.buildscript.configurations)
resolve(subProject.configurations)
}
}
}

tasks.named('test', Test) {
useJUnitPlatform()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import com.odeyalo.sonata.suite.brokers.events.playlist.gen.payload.GenerativePlaylistEvent;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.jetbrains.annotations.NotNull;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.boot.autoconfigure.kafka.KafkaProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate;
Expand All @@ -30,9 +32,10 @@ public Jackson2ObjectMapperBuilderCustomizer customizer() {
}

@Bean
public ReceiverOptions<String, PlaylistImagesGeneratedEvent> generatedPlaylistReceiverOptions(ObjectMapper objectMapper) {
public ReceiverOptions<String, PlaylistImagesGeneratedEvent> generatedPlaylistReceiverOptions(@NotNull final ObjectMapper objectMapper,
@NotNull final KafkaProperties kafkaProperties) {
final Map<String, Object> consumerProps = new HashMap<>();
consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaProperties.getBootstrapServers());
consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, "generated-playlists-consumers");
consumerProps.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
consumerProps.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.odeyalo.sonata.playlists.controller;

import com.odeyalo.sonata.playlists.dto.PersonalizedPlaylistDto;
import com.odeyalo.sonata.playlists.model.PlaylistId;
import com.odeyalo.sonata.playlists.model.User;
import com.odeyalo.sonata.playlists.repository.GeneratedPlaylistRepository;
import com.odeyalo.sonata.playlists.service.PlaylistService;
import com.odeyalo.sonata.playlists.support.converter.PlaylistConverter;
import com.odeyalo.sonata.playlists.support.converter.PlaylistDtoConverter;
import com.odeyalo.sonata.playlists.support.web.HttpStatuses;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

import java.util.List;

@RestController
@RequestMapping("/playlists")
public final class FeaturedPlaylistsController {
private final GeneratedPlaylistRepository generatedPlaylistRepository;
private final PlaylistService playlistService;
private final PlaylistDtoConverter playlistConverter;

public FeaturedPlaylistsController(final GeneratedPlaylistRepository generatedPlaylistRepository,
final PlaylistService playlistService,
final PlaylistDtoConverter playlistConverter) {
this.generatedPlaylistRepository = generatedPlaylistRepository;
this.playlistService = playlistService;
this.playlistConverter = playlistConverter;
}

@GetMapping("/featured")
public Mono<ResponseEntity<List<PersonalizedPlaylistDto>>> getPersonalizedPlaylists(@NotNull final User user) {
return getPlaylistsFor(user)
.map(HttpStatuses::defaultOkStatus);
}


@NotNull
private Mono<List<PersonalizedPlaylistDto>> getPlaylistsFor(@NotNull final User user) {
return generatedPlaylistRepository.findByGeneratedFor(user.getId())
.flatMap(generatedPlaylistEntity -> playlistService.loadPlaylist(PlaylistId.of(generatedPlaylistEntity.getPlaylistId()))
.map(playlistConverter::toPlaylistDto)
.map(playlistDto ->
new PersonalizedPlaylistDto(
playlistDto, generatedPlaylistEntity.getGeneratedPlaylistType()
)
)
)
.collectList();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.odeyalo.sonata.playlists.dto;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.odeyalo.sonata.suite.brokers.events.playlist.gen.GeneratedPlaylistType;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Value;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

@Value
@Builder
@AllArgsConstructor(onConstructor_ = {@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)})
public class PersonalizedPlaylistDto {
@NotNull
@JsonProperty("playlist")
PlaylistDto playlist;
@Nullable
@JsonProperty("type")
GeneratedPlaylistType type;
}
4 changes: 3 additions & 1 deletion src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
spring.config.import=r2dbc.properties,db-migration.properties
spring.config.import=r2dbc.properties,db-migration.properties

spring.kafka.bootstrap-servers=${KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.odeyalo.sonata.playlists.controller;

import com.odeyalo.sonata.playlists.dto.PersonalizedPlaylistDto;
import com.odeyalo.sonata.playlists.entity.GeneratedPlaylistEntity;
import com.odeyalo.sonata.playlists.entity.PlaylistEntity;
import com.odeyalo.sonata.playlists.entity.PlaylistOwnerEntity;
import com.odeyalo.sonata.playlists.model.EntityType;
import com.odeyalo.sonata.playlists.repository.GeneratedPlaylistRepository;
import com.odeyalo.sonata.playlists.repository.PlaylistRepository;
import com.odeyalo.sonata.suite.brokers.events.playlist.gen.GeneratedPlaylistType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.test.web.reactive.server.WebTestClient;
import reactor.core.publisher.Hooks;
import testing.core.AbstractIntegrationTest;

import java.time.Instant;
import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.http.HttpHeaders.AUTHORIZATION;
import static org.springframework.http.HttpHeaders.EXPIRES;

class FetchPersonalizedPlaylistsEndpointTest extends AbstractIntegrationTest {

@Autowired
WebTestClient webTestClient;

@Autowired
GeneratedPlaylistRepository generatedPlaylistRepository;

@Autowired
PlaylistRepository playlistRepository;

static final String PLAYLIST_ID_1 = "62apl2FK6dNtBhULxeKZts";

static final String VALID_ACCESS_TOKEN = "Bearer mikunakanoisthebestgirl";
static final String VALID_USER_ID = "1";

@BeforeAll
static void setup() {
Hooks.onOperatorDebug(); // DO NOT DELETE IT, VERY IMPORTANT LINE, WITHOUT IT FEIGN WITH WIREMOCK THROWS ILLEGAL STATE EXCEPTION, I DON'T FIND SOLUTION YET
}

@BeforeEach
void setUp() {
final PlaylistOwnerEntity owner = PlaylistOwnerEntity.builder()
.publicId("sonata")
.displayName("Sonata")
.entityType(EntityType.USER)
.build();

final PlaylistEntity playlist = PlaylistEntity.builder()
.publicId(PLAYLIST_ID_1)
.playlistName("Test Playlist")
.playlistDescription("Test Playlist Description")
.images(Collections.emptyList())
.contextUri("sonata:playlist:62apl2FK6dNtBhULxeKZts")
.playlistOwner(owner)
.build();

playlistRepository.save(playlist).block();

final GeneratedPlaylistEntity generatedPlaylistEntity = GeneratedPlaylistEntity.builder()
.playlistId(PLAYLIST_ID_1)
.userId(VALID_USER_ID)
.generatedPlaylistType(GeneratedPlaylistType.ON_REPEAT)
.generatedAt(Instant.now())
.build();

generatedPlaylistRepository.save(generatedPlaylistEntity).block();
}

@AfterEach
void tearDown() {
generatedPlaylistRepository.clear().block();
playlistRepository.clear().block();
}

@Test
void shouldReturn200OkStatusCode() {
final WebTestClient.ResponseSpec responseSpec = webTestClient.get()
.uri("/playlists/featured")
.header(AUTHORIZATION, VALID_ACCESS_TOKEN)
.exchange();

responseSpec.expectStatus().isOk();
}

@Test
void shouldReturnPlaylistsInResponse() {
final WebTestClient.ResponseSpec responseSpec = webTestClient.get()
.uri("/playlists/featured")
.header(AUTHORIZATION, VALID_ACCESS_TOKEN)
.exchange();

List<PersonalizedPlaylistDto> playlists = responseSpec.expectBody(new ParameterizedTypeReference<List<PersonalizedPlaylistDto>>() {
})
.returnResult().getResponseBody();

assertThat(playlists).hasSize(1);

assertThat(playlists).first()
.extracting("playlist.id")
.isEqualTo(PLAYLIST_ID_1);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"properties": [
{
"name": "spring.contracts.repository.root",
"type": "java.lang.String",
"description": "URL to downloads the stubs from."
}
]
}
4 changes: 0 additions & 4 deletions src/test/resources/application-test.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,5 @@ eureka.client.enabled=false
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.maximum-pool-size=2

logging.level.io.r2dbc.postgresql.QUERY=DEBUG
logging.level.io.r2dbc.postgresql.PARAM=DEBUG
logging.level.org.springframework.cloud.contract=debug

stubrunner.ids-to-service-ids.authorization=sonata-authorization
spring.cloud.discovery.client.simple.instances.authorization[0].uri=http://localhost:${wiremock.server.port}