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;

View File

@ -2,13 +2,18 @@
-- For the normal WebGoat server there is a bean which already provided the schema (and creates it see DatabaseInitialization)
CREATE SCHEMA IF NOT EXISTS CONTAINER;
create
table CONTAINER.assignment
create table CONTAINER.assignment
(
solved boolean not null,
id bigint generated by default as identity (start with 1),
name varchar(255),
path varchar(255),
id bigint generated by default as identity (start with 1),
name varchar(255),
path varchar(255),
primary key (id)
);
create table CONTAINER.assignment_progress
(
solved boolean not null,
assignment_id bigint unique,
id bigint generated by default as identity (start with 1),
primary key (id)
);
create table CONTAINER.lesson_progress
@ -55,8 +60,10 @@ create table CONTAINER.email
title VARCHAR(255)
);
alter table CONTAINER.assignment_progress
add constraint FK7o6abukma83ku3xrge9sy0qnr foreign key (assignment_id) references CONTAINER.assignment;
alter table CONTAINER.lesson_progress_assignments
add constraint FKbd9xavuwr1rxbcqhcu3jckyro foreign key (assignments_id) references CONTAINER.assignment;
add constraint FKrw89vmnela8kj0nbg1xdws5bt foreign key (assignments_id) references CONTAINER.assignment_progress;
alter table CONTAINER.lesson_progress_assignments
add constraint FKl8vg2qfqhmsnt18qqcyydq7iu foreign key (lesson_progress_id) references CONTAINER.lesson_progress;
alter table CONTAINER.user_progress_lesson_progress