Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/main/java/teammates/common/datatransfer/DataBundle.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import teammates.storage.entity.FeedbackSessionLog;
import teammates.storage.entity.Institute;
import teammates.storage.entity.Instructor;
import teammates.storage.entity.MagicLink;
import teammates.storage.entity.Notification;
import teammates.storage.entity.ReadNotification;
import teammates.storage.entity.ResponseInstructorComment;
Expand Down Expand Up @@ -42,6 +43,7 @@ public class DataBundle {
public Map<String, FeedbackResponse> feedbackResponses = new LinkedHashMap<>();
public Map<String, ResponseInstructorComment> responseInstructorComments = new LinkedHashMap<>();
public Map<String, FeedbackSessionLog> feedbackSessionLogs = new LinkedHashMap<>();
public Map<String, MagicLink> magicLinks = new LinkedHashMap<>();
public Map<String, Notification> notifications = new LinkedHashMap<>();
public Map<String, ReadNotification> readNotifications = new LinkedHashMap<>();
}
2 changes: 2 additions & 0 deletions src/main/java/teammates/common/util/HibernateUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import teammates.storage.entity.InstructorCoursePrivilege;
import teammates.storage.entity.InstructorSectionPrivilege;
import teammates.storage.entity.InstructorSessionPrivilege;
import teammates.storage.entity.MagicLink;
import teammates.storage.entity.Notification;
import teammates.storage.entity.ReadNotification;
import teammates.storage.entity.ResponseInstructorComment;
Expand Down Expand Up @@ -67,6 +68,7 @@ public final class HibernateUtil {
Course.class,
FeedbackSession.class,
Account.class,
MagicLink.class,
Notification.class,
ReadNotification.class,
User.class,
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/teammates/logic/core/DataBundleLogic.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import teammates.storage.entity.InstructorCoursePrivilege;
import teammates.storage.entity.InstructorSectionPrivilege;
import teammates.storage.entity.InstructorSessionPrivilege;
import teammates.storage.entity.MagicLink;
import teammates.storage.entity.Notification;
import teammates.storage.entity.ReadNotification;
import teammates.storage.entity.ResponseGiver;
Expand Down Expand Up @@ -114,6 +115,7 @@ public static DataBundle deserializeDataBundle(String jsonString, String seed) {
Collection<FeedbackResponse> responses = dataBundle.feedbackResponses.values();
Collection<ResponseInstructorComment> responseComments = dataBundle.responseInstructorComments.values();
Collection<DeadlineExtension> deadlineExtensions = dataBundle.deadlineExtensions.values();
Collection<MagicLink> magicLinks = dataBundle.magicLinks.values();
Collection<Notification> notifications = dataBundle.notifications.values();
Collection<ReadNotification> readNotifications = dataBundle.readNotifications.values();

Expand Down Expand Up @@ -274,6 +276,11 @@ public static DataBundle deserializeDataBundle(String jsonString, String seed) {
notificationsMap.put(placeholderId, notification);
}

for (MagicLink magicLink : magicLinks) {
UUID placeholderId = magicLink.getId();
magicLink.setId(generateId(placeholderId, seed));
}

for (ReadNotification readNotification : readNotifications) {
UUID placeholderId = readNotification.getId();
readNotification.setId(generateId(placeholderId, seed));
Expand Down Expand Up @@ -418,10 +425,12 @@ public DataBundle persistDataBundle(DataBundle dataBundle)
Collection<FeedbackResponse> responses = dataBundle.feedbackResponses.values();
Collection<ResponseInstructorComment> responseComments = dataBundle.responseInstructorComments.values();
Collection<DeadlineExtension> deadlineExtensions = dataBundle.deadlineExtensions.values();
Collection<MagicLink> magicLinks = dataBundle.magicLinks.values();
Collection<Notification> notifications = dataBundle.notifications.values();
Collection<ReadNotification> readNotifications = dataBundle.readNotifications.values();

persistEntities(institutes);
persistEntities(magicLinks);
persistEntities(notifications);
persistEntities(accounts);
persistEntities(accountVerificationRequests);
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/teammates/storage/api/MagicLinksDb.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package teammates.storage.api;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;

import teammates.common.util.HibernateUtil;
import teammates.storage.entity.MagicLink;

/**
* Handles CRUD operations for magic links.
*
* @see MagicLink
*/
public final class MagicLinksDb {
private static final MagicLinksDb instance = new MagicLinksDb();

private MagicLinksDb() {
// prevent initialization
}

public static MagicLinksDb inst() {
return instance;
}

/**
* Persists a MagicLink.
*/
public MagicLink persistMagicLink(MagicLink magicLink) {
HibernateUtil.persist(magicLink);
return magicLink;
}

/**
* Returns a MagicLink with the given token hash or null if it does not exist.
*/
public MagicLink getMagicLinkByTokenHash(String tokenHash) {
CriteriaBuilder cb = HibernateUtil.getCriteriaBuilder();
CriteriaQuery<MagicLink> cr = cb.createQuery(MagicLink.class);
Root<MagicLink> root = cr.from(MagicLink.class);

cr.select(root).where(cb.equal(root.get("tokenHash"), tokenHash));

return HibernateUtil.createQuery(cr).getResultStream().findFirst().orElse(null);
}

/**
* Deletes a MagicLink.
*/
public void deleteMagicLink(MagicLink magicLink) {
HibernateUtil.remove(magicLink);
}

}
65 changes: 65 additions & 0 deletions src/test/java/teammates/storage/api/MagicLinksDbTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package teammates.storage.api;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.testng.annotations.Test;

import teammates.storage.entity.MagicLink;
import teammates.test.GroupNames;

/**
* Tests for {@link MagicLinksDb}.
*/
public class MagicLinksDbTest extends BaseDbTestcase {
private final MagicLinksDb magicLinksDb = MagicLinksDb.inst();

@Test(groups = GroupNames.DB)
public void persistMagicLink_magicLinkDoesNotExist_magicLinkIsInserted() {
var magicLinkRef = given.magicLink("magic-link",
ml -> ml.email("insert@example.com").tokenHash("insert-token-hash"));
MagicLink magicLink = given.getDataBundle().magicLinks.get(magicLinkRef.alias());

MagicLink actual = inTransaction(() -> magicLinksDb.persistMagicLink(magicLink));

assertEquals(magicLinkRef.id(), actual.getId());
assertEquals("insert@example.com", actual.getEmail());
assertEquals("insert-token-hash", actual.getTokenHash());
verifyPresentInDatabase(MagicLink.class, magicLinkRef.id());
}

@Test(groups = GroupNames.DB)
public void getMagicLinkByTokenHash_magicLinkExists_returnsMagicLink() {
var magicLink = given.magicLink("magic-link",
ml -> ml.email("lookup@example.com").tokenHash("lookup-token-hash"));
persistGivenData(given);

MagicLink actual = inTransaction(() -> magicLinksDb.getMagicLinkByTokenHash("lookup-token-hash"));

assertNotNull(actual);
assertEquals(magicLink.id(), actual.getId());
}

@Test(groups = GroupNames.DB)
public void getMagicLinkByTokenHash_magicLinkDoesNotExist_returnsNull() {
given.magicLink("magic-link",
ml -> ml.email("missing-lookup@example.com").tokenHash("existing-token-hash"));
persistGivenData(given);

MagicLink actual = inTransaction(() -> magicLinksDb.getMagicLinkByTokenHash("non-existent-token-hash"));

assertNull(actual);
}

@Test(groups = GroupNames.DB)
public void deleteMagicLink_magicLinkExists_magicLinkIsRemoved() {
var magicLink = given.magicLink("magic-link",
ml -> ml.email("delete@example.com").tokenHash("delete-token-hash"));
persistGivenData(given);

inTransaction(() -> magicLinksDb.deleteMagicLink(magicLinksDb.getMagicLinkByTokenHash("delete-token-hash")));

verifyAbsentInDatabase(MagicLink.class, magicLink.id());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ private void clearDatabase() {
feedback_sessions,
institutes,
instructors,
magic_links,
notifications,
read_notifications,
response_instructor_comments,
Expand Down
30 changes: 30 additions & 0 deletions src/test/java/teammates/test/scenariobuilder/GivenData.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import teammates.storage.entity.FeedbackSessionLog;
import teammates.storage.entity.Institute;
import teammates.storage.entity.Instructor;
import teammates.storage.entity.MagicLink;
import teammates.storage.entity.Notification;
import teammates.storage.entity.ReadNotification;
import teammates.storage.entity.ResponseInstructorComment;
Expand Down Expand Up @@ -382,6 +383,25 @@ public NotificationRef notification(String alias, Consumer<GivenNotification> op
return new NotificationRef(notification.getId(), alias);
}

/**
* Creates a magic link with default values.
*/
public MagicLinkRef magicLink(String alias) {
return magicLink(alias, ml -> {
});
}

/**
* Creates a magic link and applies the provided options to customize it.
*/
public MagicLinkRef magicLink(String alias, Consumer<GivenMagicLink> options) {
GivenMagicLink magicLinkData = new GivenMagicLink(this, uuid(alias));
options.accept(magicLinkData);
MagicLink magicLink = magicLinkData.build();
registerEntity(alias, magicLink, dataBundle.magicLinks);
return new MagicLinkRef(magicLink.getId(), alias, magicLink.getEmail(), magicLink.getTokenHash());
}

/**
* Creates a read notification with default values.
*/
Expand Down Expand Up @@ -570,6 +590,16 @@ public record DeadlineExtensionRef(UUID id, String alias) {}
*/
public record FeedbackSessionLogRef(UUID id, String alias) {}

/**
* Reference to a magic link created by GivenData.
*
* @param id generated entity ID
* @param alias GivenData alias
* @param email email address
* @param tokenHash token hash
*/
public record MagicLinkRef(UUID id, String alias, String email, String tokenHash) {}

/**
* Reference to a notification created by GivenData.
*
Expand Down
54 changes: 54 additions & 0 deletions src/test/java/teammates/test/scenariobuilder/GivenMagicLink.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package teammates.test.scenariobuilder;

import java.time.Instant;
import java.util.UUID;

import teammates.storage.entity.MagicLink;

/**
* Builder for MagicLink entities used in test scenarios.
*/
public final class GivenMagicLink extends GivenBase<MagicLink> {
public GivenMagicLink(GivenData given, UUID magicLinkId) {
super(given);
this.entity = defaultMagicLink(magicLinkId);
}

/**
* Sets the email for the magic link.
*/
public GivenMagicLink email(String email) {
entity.setEmail(email);
return this;
}

/**
* Sets the token hash for the magic link.
*/
public GivenMagicLink tokenHash(String tokenHash) {
entity.setTokenHash(tokenHash);
return this;
}

/**
* Sets the expiry time for the magic link.
*/
public GivenMagicLink expiresAt(Instant expiresAt) {
entity.setExpiresAt(expiresAt);
return this;
}

@Override
void ensureConsistent() {
// No mandatory relationships
}

private MagicLink defaultMagicLink(UUID magicLinkId) {
MagicLink magicLink = new MagicLink(
magicLinkId.toString() + "@teammates.tmt",
"token-hash:" + magicLinkId.toString(),
Instant.now());
magicLink.setId(magicLinkId);
return magicLink;
}
}
Loading