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:
parent
d441e64004
commit
aa0e850f1b
|
@ -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")
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue