1
0
Fork 0
mirror of https://codeberg.org/beerbrawl/beerbrawl.git synced 2024-09-23 01:30:52 +02:00

feature(#22): add delete functionality

This commit is contained in:
MohammedKevin 2024-05-23 12:17:16 +02:00
parent d441e64004
commit aa0e850f1b
11 changed files with 202 additions and 25 deletions

View file

@ -16,13 +16,7 @@ import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.core.Authentication;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import at.ac.tuwien.sepr.groupphase.backend.endpoint.mapper.TournamentMapper;
import at.ac.tuwien.sepr.groupphase.backend.entity.Tournament.SignupTeamResult;
@ -110,6 +104,16 @@ public class TournamentEndpoint {
return tournamentService.getTournamentTeams(tournamentId).stream().map(teamMapper::entityToDto).toList();
}
@Secured("ROLE_USER")
@ResponseStatus(HttpStatus.NO_CONTENT)
@DeleteMapping("/{tournamentId}")
@Operation(summary = "Delete a tournament", security = @SecurityRequirement(name = "apiKey"))
public ResponseEntity<Void> deleteTournament(@PathVariable("tournamentId") long tournamentId, Authentication authentication) {
LOG.info("DELETE {}/{}", BASE_ENDPOINT, tournamentId);
tournamentService.deleteTournament(tournamentId, authentication.getName());
return ResponseEntity.noContent().build();
}
// as of now, information is accessible to everyone
@PermitAll
@GetMapping("{tournamentId}/public")

View file

@ -31,4 +31,11 @@ public interface TournamentRepository extends JpaRepository<Tournament, Long> {
* @return wether the tournament with the given name exists
*/
Boolean existsByName(String name);
/**
* Delete a tournament by its ID.
*
* @param id The ID of the tournament to delete
*/
void deleteById(Long id);
}

View file

@ -75,4 +75,13 @@ public interface TournamentService {
* @return the tournament entity
*/
Tournament findOne(long tournamentId);
}
/**
* Delete a single tournament entity by id.
*
* @param tournamentId the id of the tournament entity
* @throws NotFoundException if the tournament is not found
* @throws AccessDeniedException if the current user does not have permission to delete the tournament
*/
void deleteTournament(long tournamentId, String currentUserName) throws NotFoundException, AccessDeniedException;
}

View file

@ -175,4 +175,18 @@ public class TournamentServiceImpl implements TournamentService {
return tournament.getOrganizer().getUsername().equals(username);
}
@Transactional
public void deleteTournament(long tournamentId, String currentUserName) throws NotFoundException, AccessDeniedException {
LOGGER.debug("Deleting tournament with id {}", tournamentId);
Tournament tournament = tournamentRepository.findById(tournamentId)
.orElseThrow(() -> new NotFoundException("Tournament not found"));
if (!tournament.getOrganizer().getUsername().equals(currentUserName)) {
throw new AccessDeniedException("You do not have permission to delete this tournament");
}
tournamentRepository.deleteById(tournamentId);
LOGGER.debug("Tournament with id {} deleted successfully", tournamentId);
}
}

View file

@ -21,8 +21,9 @@ import at.ac.tuwien.sepr.groupphase.backend.security.JwtTokenizer;
import com.fasterxml.jackson.databind.ObjectMapper;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.time.LocalDateTime;
import java.util.List;
@ -40,13 +41,7 @@ import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import org.springframework.transaction.annotation.Transactional;
@ -310,4 +305,63 @@ public class TournamentEndpointTest extends TestUserData implements TestData {
assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatus());
}
@Test
public void successfullyDeleteTournament() throws Exception {
// Setup: Create a tournament to delete
var tournament = new Tournament();
tournament.setName("TOURNAMENT_TO_DELETE");
tournament.setMaxParticipants(64L);
tournament.setOrganizer(userRepository.findByUsername(TestDataGenerator.TEST_USER));
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournamentRepository.saveAndFlush(tournament);
// Perform delete request
var mvcResult = this.mockMvc.perform(delete(String.format("%s/%d", TOURNAMENT_BASE_URI, tournament.getId()))
.header(securityProperties.getAuthHeader(), jwtTokenizer.getAuthToken(TEST_USER, TEST_USER_ROLES))
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isNoContent())
.andReturn();
// Verify the tournament has been deleted
var deletedTournament = tournamentRepository.findById(tournament.getId());
assertTrue(deletedTournament.isEmpty(), "Expected the tournament to be deleted, but it still exists");
}
@Test
public void deleteTournamentUnauthorized() throws Exception {
// Setup: Create a tournament to delete
var tournament = new Tournament();
tournament.setName("TOURNAMENT_TO_DELETE");
tournament.setMaxParticipants(64L);
tournament.setOrganizer(userRepository.findByUsername(TestDataGenerator.TEST_USER));
tournament.setRegistrationEnd(LocalDateTime.now().plusDays(1));
tournamentRepository.saveAndFlush(tournament);
// Perform delete request without authorization header
var mvcResult = this.mockMvc.perform(delete(String.format("%s/%d", TOURNAMENT_BASE_URI, tournament.getId()))
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isForbidden())
.andReturn();
}
@Test
public void deleteNonExistingTournament() throws Exception {
// Attempt to delete a non-existing tournament
long nonExistingTournamentId = -1L;
var mvcResult = this.mockMvc.perform(delete(String.format("%s/%d", TOURNAMENT_BASE_URI, nonExistingTournamentId))
.header(securityProperties.getAuthHeader(), jwtTokenizer.getAuthToken(TEST_USER, TEST_USER_ROLES))
.contentType(MediaType.APPLICATION_JSON))
.andDo(print())
.andExpect(status().isNotFound())
.andReturn();
MockHttpServletResponse response = mvcResult.getResponse();
assertEquals(HttpStatus.NOT_FOUND.value(), response.getStatus(), "Expected status 404 for non-existing tournament");
}
}

View file

@ -14,3 +14,47 @@ context('Get Tournaments', () => {
cy.get('[data-cy="tournaments-list-item"]').should('have.length.at.least', 1);
});
});
context('Delete Tournament', () => {
beforeEach(() => {
cy.loginTestUser();
cy.wait(4000);
});
it('successfully deletes a tournament', () => {
cy.intercept('GET', '/api/v1/tournaments', { fixture: 'tournaments.json' }).as(
'getTournaments',
);
cy.visit('/#/tournaments');
cy.wait('@getTournaments');
cy.get('[data-cy="tournaments-list"]').should('exist');
cy.get('[data-cy="tournaments-list-item"]').should('have.length.at.least', 1);
cy.intercept('DELETE', '/api/v1/tournaments/*', { statusCode: 200 }).as('deleteTournament');
cy.get('[data-cy="tournaments-list-item"]').first().as('firstTournament');
cy.get('@firstTournament').find('[data-cy="delete-tournament-btn"]').click();
cy.get('[data-cy="confirm-delete-btn"]').click();
cy.wait('@deleteTournament');
cy.get('@firstTournament').should('not.exist');
});
it('cancels tournament deletion', () => {
cy.intercept('GET', '/api/v1/tournaments', { fixture: 'tournaments.json' }).as(
'getTournaments',
);
cy.visit('/#/tournaments');
cy.wait('@getTournaments');
cy.get('[data-cy="tournaments-list"]').should('exist');
cy.get('[data-cy="tournaments-list-item"]').should('have.length.at.least', 1);
cy.get('[data-cy="tournaments-list-item"]').first().as('firstTournament');
cy.get('@firstTournament').find('[data-cy="delete-tournament-btn"]').click();
cy.get('[data-cy="cancel-delete-btn"]').click();
cy.get('@firstTournament').should('exist');
});
});

View file

@ -3,6 +3,7 @@
<div mat-dialog-actions>
<button
mat-flat-button
data-cy="cancel-delete-btn"
color="warn"
aria-label="cancel-action button"
(click)="this.dialogRef.close(false)"
@ -12,6 +13,7 @@
</button>
<button
mat-flat-button
data-cy="confirm-delete-btn"
color="primary"
aria-label="ok button"
(click)="this.dialogRef.close(true)"

View file

@ -15,7 +15,7 @@
<button mat-icon-button>
<mat-icon>play_arrow</mat-icon>
</button>
<button mat-icon-button>
<button mat-icon-button data-cy="delete-tournament-btn" (click)="openConfirmDialog()">
<mat-icon>delete</mat-icon>
</button>
</div>

View file

@ -1,11 +1,52 @@
import { Component, Input } from '@angular/core';
import { TournamentListDto } from '@api';
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { TournamentEndpointService, TournamentListDto } from '@api';
import { ConfirmDialogComponent } from '../../confirm-dialog/confirm-dialog.component';
import { MatSnackBar } from '@angular/material/snack-bar';
@Component({
selector: 'app-tournament-card',
templateUrl: './tournament-card.component.html',
styleUrl: './tournament-card.component.scss',
styleUrls: ['./tournament-card.component.scss'],
})
export class TournamentCardComponent {
@Input() tournament: TournamentListDto | undefined;
@Output() tournamentDeleted = new EventEmitter<void>();
constructor(
private dialog: MatDialog,
private tournamentService: TournamentEndpointService,
private snackBar: MatSnackBar,
) {}
openConfirmDialog(): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: 'delete tournament',
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.deleteTournament();
}
});
}
deleteTournament(): void {
if (this.tournament?.id) {
this.tournamentService.deleteTournament(this.tournament.id).subscribe({
next: () => {
this.snackBar.open('Tournament deleted successfully', 'Close', {
duration: 3000,
});
this.tournamentDeleted.emit();
},
error: err => {
console.error('Error deleting tournament', err);
this.snackBar.open('Failed to delete tournament', 'Close', {
duration: 3000,
});
},
});
}
}
}

View file

@ -10,5 +10,5 @@
</div>
</mat-card>
@for (tournament of getTournaments(); track tournament.name) {
<app-tournament-card [tournament]="tournament" data-cy="tournaments-list"></app-tournament-card>
<app-tournament-card [tournament]="tournament" (tournamentDeleted)="onTournamentDeleted()" data-cy="tournaments-list"></app-tournament-card>
}

View file

@ -1,12 +1,12 @@
import { Component } from '@angular/core';
import { Component, OnInit } from '@angular/core';
import { TournamentEndpointService, TournamentListDto } from '@api';
@Component({
selector: 'app-tournaments',
templateUrl: './tournaments.component.html',
styleUrl: './tournaments.component.scss',
styleUrls: ['./tournaments.component.scss'],
})
export class TournamentsComponent {
export class TournamentsComponent implements OnInit {
tournaments: TournamentListDto[] = [];
constructor(private tournamentService: TournamentEndpointService) {}
@ -22,8 +22,6 @@ export class TournamentsComponent {
private loadTournaments() {
this.tournamentService.tournaments().subscribe({
next: data => {
console.log('data');
console.log(data);
this.tournaments = data;
},
error: err => {
@ -31,4 +29,8 @@ export class TournamentsComponent {
},
});
}
onTournamentDeleted() {
this.loadTournaments();
}
}