feat: Introduce Playwright for UI testing

Instead of using Robot Framework which does not run during a `mvn install`. Playwright seems to be the better approach. We can now write them as normal JUnit test and they are executed during a build.

Additionally this PR solves some interesting bugs found during writing Playwright tests:

- A reset of a lesson removes all assignments as a result another user wouldn't see any assignments
- If someone solves an assignment the assignment automatically got solved for a new user since the assignment included the `solved` flag which immediately got copied to new lesson progress.
- Introduction of assignment progress linking a assignment not directly to all users.
This commit is contained in:
Nanne Baars
2025-01-26 16:59:59 +01:00
committed by GitHub
parent 9d5ab5fb21
commit 8e45316638
47 changed files with 599 additions and 281 deletions

View File

@ -51,7 +51,6 @@ public class Assignment {
private String name;
private String path;
private boolean solved = false;
@Transient private List<String> hints;
@ -75,8 +74,4 @@ public class Assignment {
this.path = path;
this.hints = hints;
}
public void solved() {
this.solved = true;
}
}

View File

@ -18,4 +18,9 @@ public record LessonName(String lessonName) {
lessonName = lessonName.substring(0, lessonName.indexOf(".lesson"));
}
}
@Override
public String toString() {
return lessonName;
}
}

View File

@ -10,7 +10,6 @@ import org.owasp.webgoat.container.lessons.LessonName;
import org.owasp.webgoat.container.session.Course;
import org.owasp.webgoat.container.users.UserProgressRepository;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.ResponseBody;
@ -40,11 +39,9 @@ public class LessonProgressService {
var userProgress = userProgressRepository.findByUser(username);
var lesson = course.getLessonByName(lessonName);
Assert.isTrue(lesson != null, "Lesson not found: " + lessonName);
var lessonProgress = userProgress.getLessonProgress(lesson);
return lessonProgress.getLessonOverview().entrySet().stream()
.map(entry -> new LessonOverview(entry.getKey(), entry.getValue()))
.map(entry -> new LessonOverview(entry.getKey().getAssignment(), entry.getValue()))
.toList();
}

View File

@ -0,0 +1,47 @@
package org.owasp.webgoat.container.users;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.OneToOne;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.owasp.webgoat.container.lessons.Assignment;
import org.springframework.util.Assert;
@Entity
@EqualsAndHashCode
public class AssignmentProgress {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Getter
@OneToOne(cascade = CascadeType.ALL)
private Assignment assignment;
@Getter private boolean solved;
protected AssignmentProgress() {}
public AssignmentProgress(Assignment assignment) {
this.assignment = assignment;
}
public boolean hasSameName(String name) {
Assert.notNull(name, "Name cannot be null");
return assignment.getName().equals(name);
}
public void solved() {
this.solved = true;
}
public void reset() {
this.solved = false;
}
}

View File

@ -9,14 +9,12 @@ import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Version;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.owasp.webgoat.container.lessons.Assignment;
import org.owasp.webgoat.container.lessons.Lesson;
/**
@ -61,7 +59,7 @@ public class LessonProgress {
@Getter private String lessonName;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private final Set<Assignment> assignments = new HashSet<>();
private final Set<AssignmentProgress> assignments = new HashSet<>();
@Getter private int numberOfAttempts = 0;
@Version private Integer version;
@ -72,11 +70,11 @@ public class LessonProgress {
public LessonProgress(Lesson lesson) {
lessonName = lesson.getId();
assignments.addAll(lesson.getAssignments() == null ? List.of() : lesson.getAssignments());
assignments.addAll(lesson.getAssignments().stream().map(AssignmentProgress::new).toList());
}
public Optional<Assignment> getAssignment(String name) {
return assignments.stream().filter(a -> a.getName().equals(name)).findFirst();
private Optional<AssignmentProgress> getAssignment(String name) {
return assignments.stream().filter(a -> a.hasSameName(name)).findFirst();
}
/**
@ -85,14 +83,14 @@ public class LessonProgress {
* @param solvedAssignment the assignment which the user solved
*/
public void assignmentSolved(String solvedAssignment) {
getAssignment(solvedAssignment).ifPresent(Assignment::solved);
getAssignment(solvedAssignment).ifPresent(AssignmentProgress::solved);
}
/**
* @return did they user solved all solvedAssignments for the lesson?
*/
public boolean isLessonSolved() {
return assignments.stream().allMatch(Assignment::isSolved);
return assignments.stream().allMatch(AssignmentProgress::isSolved);
}
/** Increase the number attempts to solve the lesson */
@ -102,14 +100,14 @@ public class LessonProgress {
/** Reset the tracker. We do not reset the number of attempts here! */
void reset() {
assignments.clear();
assignments.forEach(AssignmentProgress::reset);
}
/**
* @return list containing all the assignments solved or not
*/
public Map<Assignment, Boolean> getLessonOverview() {
return assignments.stream().collect(Collectors.toMap(a -> a, Assignment::isSolved));
public Map<AssignmentProgress, Boolean> getLessonOverview() {
return assignments.stream().collect(Collectors.toMap(a -> a, AssignmentProgress::isSolved));
}
long numberOfSolvedAssignments() {

View File

@ -31,6 +31,7 @@ public class UserService implements UserDetailsService {
throw new UsernameNotFoundException("User not found");
} else {
webGoatUser.createUser();
// TODO maybe better to use an event to initialize lessons to keep dependencies low
lessonInitializables.forEach(l -> l.initialize(webGoatUser));
}
return webGoatUser;