Skip to content

Commit 77b1e77

Browse files
committed
Merged in feature/EL-355-as-the-developer-i-want-explicit- (pull request #179)
Feature/EL-355 as the developer i want explicit Approved-by: patrickt
2 parents 70f4811 + 0a980b4 commit 77b1e77

45 files changed

Lines changed: 2379 additions & 187 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package dev.getelements.elements.rest.test;
2+
3+
import dev.getelements.elements.sdk.model.session.SessionCreation;
4+
import dev.getelements.elements.sdk.model.session.UsernamePasswordSessionRequest;
5+
import dev.getelements.elements.sdk.model.user.User;
6+
import dev.getelements.elements.sdk.model.user.UserCreateRequest;
7+
import jakarta.inject.Inject;
8+
import jakarta.inject.Named;
9+
import jakarta.ws.rs.client.Client;
10+
import jakarta.ws.rs.client.Entity;
11+
import org.testng.annotations.Factory;
12+
import org.testng.annotations.Test;
13+
14+
import java.util.UUID;
15+
16+
import static dev.getelements.elements.rest.test.TestUtils.TEST_API_ROOT;
17+
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
18+
import static org.testng.Assert.assertEquals;
19+
import static org.testng.Assert.assertNotNull;
20+
21+
/**
22+
* Verifies that email addresses are case-normalised (stored as lowercase) on signup
23+
* and that login succeeds regardless of the case the caller uses.
24+
*/
25+
public class EmailCaseNormalizationTest {
26+
27+
@Factory
28+
public Object[] getTests() {
29+
return new Object[]{TestUtils.getInstance().getTestFixture(EmailCaseNormalizationTest.class)};
30+
}
31+
32+
@Inject @Named(TEST_API_ROOT)
33+
private String apiRoot;
34+
35+
@Inject
36+
private Client client;
37+
38+
@Inject
39+
private ClientContext clientContext;
40+
41+
// Register with a MIXED-CASE email to verify normalisation at creation time.
42+
private final String rawEmail = "Test.User-" + UUID.randomUUID() + "@Example.COM";
43+
private final String lowerEmail = rawEmail.toLowerCase();
44+
private final String name = "email-norm-" + UUID.randomUUID();
45+
private final String password = UUID.randomUUID().toString();
46+
47+
private String userId;
48+
49+
@Test
50+
public void signup_mixedCaseEmail_storedAsLowercase() {
51+
final var request = new UserCreateRequest();
52+
request.setName(name);
53+
request.setEmail(rawEmail);
54+
request.setPassword(password);
55+
56+
final var user = client
57+
.target(apiRoot + "/signup/session")
58+
.request(APPLICATION_JSON)
59+
.post(Entity.entity(request, APPLICATION_JSON), SessionCreation.class);
60+
61+
assertNotNull(user);
62+
assertNotNull(user.getSession());
63+
64+
final var createdUser = user.getSession().getUser();
65+
assertNotNull(createdUser);
66+
userId = createdUser.getId();
67+
68+
assertEquals(createdUser.getEmail(), lowerEmail,
69+
"Email should be stored as lowercase regardless of how it was supplied");
70+
}
71+
72+
@Test(dependsOnMethods = "signup_mixedCaseEmail_storedAsLowercase")
73+
public void login_mixedCaseEmail_succeeds() {
74+
final var request = new UsernamePasswordSessionRequest();
75+
request.setUserId(rawEmail); // original mixed-case
76+
request.setPassword(password);
77+
78+
final var response = client
79+
.target(apiRoot + "/session")
80+
.request(APPLICATION_JSON)
81+
.post(Entity.entity(request, APPLICATION_JSON));
82+
83+
assertEquals(response.getStatus(), 200, "Login with mixed-case email should succeed");
84+
final var session = response.readEntity(SessionCreation.class);
85+
assertEquals(session.getSession().getUser().getId(), userId);
86+
}
87+
88+
@Test(dependsOnMethods = "signup_mixedCaseEmail_storedAsLowercase")
89+
public void login_uppercaseEmail_succeeds() {
90+
final var request = new UsernamePasswordSessionRequest();
91+
request.setUserId(rawEmail.toUpperCase());
92+
request.setPassword(password);
93+
94+
final var response = client
95+
.target(apiRoot + "/session")
96+
.request(APPLICATION_JSON)
97+
.post(Entity.entity(request, APPLICATION_JSON));
98+
99+
assertEquals(response.getStatus(), 200, "Login with all-uppercase email should succeed");
100+
final var session = response.readEntity(SessionCreation.class);
101+
assertEquals(session.getSession().getUser().getId(), userId);
102+
}
103+
104+
@Test(dependsOnMethods = "signup_mixedCaseEmail_storedAsLowercase")
105+
public void login_lowercaseEmail_succeeds() {
106+
final var request = new UsernamePasswordSessionRequest();
107+
request.setUserId(lowerEmail);
108+
request.setPassword(password);
109+
110+
final var response = client
111+
.target(apiRoot + "/session")
112+
.request(APPLICATION_JSON)
113+
.post(Entity.entity(request, APPLICATION_JSON));
114+
115+
assertEquals(response.getStatus(), 200, "Login with lowercase email should succeed");
116+
final var session = response.readEntity(SessionCreation.class);
117+
assertEquals(session.getSession().getUser().getId(), userId);
118+
}
119+
120+
}

jetty-ws-test/src/test/java/dev/getelements/elements/rest/test/EmailVerificationApiTest.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.testng.annotations.Test;
1515

1616
import java.sql.Timestamp;
17+
import java.util.UUID;
1718
import java.util.concurrent.TimeUnit;
1819

1920
import static dev.getelements.elements.sdk.dao.UserUidDao.SCHEME_EMAIL;
@@ -137,14 +138,16 @@ public void requestVerification_unauthenticated_returns403() {
137138
}
138139

139140
/**
140-
* Authenticated request with a valid email but no SMTP configured → 400 (InvalidDataException).
141-
* This confirms the endpoint is reachable by the authenticated user and that the authorization
142-
* check passes; the email-sending failure is the only thing preventing a full success.
141+
* Authenticated request with a fresh (never-verified) email but no SMTP configured → 400
142+
* (InvalidDataException). We use a freshly-generated address rather than the user's main email
143+
* because other tests in this class may already have set the main email to VERIFIED (which would
144+
* trigger an early-return 200 instead of reaching the SMTP path).
143145
*/
144146
@Test
145147
public void requestVerification_authenticated_withoutSmtp_returns400() {
148+
final var freshEmail = "smtp-test-" + UUID.randomUUID() + "@test.example.com";
146149
final var request = new EmailVerificationRequest();
147-
request.setEmail(clientContext.getUser().getEmail());
150+
request.setEmail(freshEmail);
148151

149152
final Response response = client
150153
.target(apiRoot + "/user/me/email/verify")
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package dev.getelements.elements.rest.test;
2+
3+
import dev.getelements.elements.sdk.dao.UidVerificationTokenDao;
4+
import dev.getelements.elements.sdk.model.session.SessionCreation;
5+
import dev.getelements.elements.sdk.model.session.UsernamePasswordSessionRequest;
6+
import dev.getelements.elements.sdk.model.user.LinkEmailPasswordRequest;
7+
import dev.getelements.elements.sdk.model.user.User;
8+
import jakarta.inject.Inject;
9+
import jakarta.inject.Named;
10+
import jakarta.ws.rs.client.Client;
11+
import jakarta.ws.rs.client.Entity;
12+
import jakarta.ws.rs.core.Response;
13+
import org.testng.annotations.BeforeClass;
14+
import org.testng.annotations.Factory;
15+
import org.testng.annotations.Test;
16+
17+
import java.sql.Timestamp;
18+
import java.util.UUID;
19+
import java.util.concurrent.TimeUnit;
20+
21+
import static dev.getelements.elements.sdk.dao.UserUidDao.SCHEME_EMAIL;
22+
import static dev.getelements.elements.sdk.model.Headers.SESSION_SECRET;
23+
import static dev.getelements.elements.rest.test.TestUtils.TEST_API_ROOT;
24+
import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
25+
import static org.testng.Assert.assertEquals;
26+
import static org.testng.Assert.assertNotNull;
27+
28+
/**
29+
* Integration tests for {@code POST /user/me/link/email-password}.
30+
*
31+
* <p>The VERIFIED prerequisite is fulfilled by creating a token directly via
32+
* {@link UidVerificationTokenDao} and consuming it via {@code GET /verify?token=...},
33+
* which avoids needing a live SMTP server.
34+
*/
35+
public class LinkEmailPasswordApiTest {
36+
37+
@Factory
38+
public Object[] getTests() {
39+
return new Object[]{TestUtils.getInstance().getTestFixture(LinkEmailPasswordApiTest.class)};
40+
}
41+
42+
@Inject @Named(TEST_API_ROOT)
43+
private String apiRoot;
44+
45+
@Inject
46+
private Client client;
47+
48+
@Inject
49+
private ClientContext clientContext;
50+
51+
@Inject
52+
private UidVerificationTokenDao tokenDao;
53+
54+
private final String linkedPassword = UUID.randomUUID().toString();
55+
56+
@BeforeClass
57+
public void setup() {
58+
clientContext.createUser("link-ep-test").createSession();
59+
60+
// Verify the user's email directly via the DAO so we can call the link endpoint.
61+
final var user = clientContext.getUser();
62+
final var email = user.getEmail();
63+
final var expiry = new Timestamp(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1));
64+
final var token = tokenDao.createToken(user, SCHEME_EMAIL, email, expiry);
65+
66+
final var verifyResponse = client
67+
.target(apiRoot + "/verify")
68+
.queryParam("token", token)
69+
.request(APPLICATION_JSON)
70+
.get();
71+
72+
assertEquals(verifyResponse.getStatus(), 200, "Email verification prerequisite failed");
73+
}
74+
75+
@Test
76+
public void linkEmailPassword_unauthenticated_returns403() {
77+
final var request = new LinkEmailPasswordRequest();
78+
request.setEmail(clientContext.getUser().getEmail());
79+
request.setPassword(linkedPassword);
80+
81+
final var response = client
82+
.target(apiRoot + "/user/me/link/email-password")
83+
.request(APPLICATION_JSON)
84+
.post(Entity.entity(request, APPLICATION_JSON));
85+
86+
assertEquals(response.getStatus(), 403);
87+
}
88+
89+
@Test(dependsOnMethods = "linkEmailPassword_unauthenticated_returns403")
90+
public void linkEmailPassword_unknownEmail_returns404() {
91+
// Unknown email → getUserUid throws UserNotFoundException → 404
92+
final var request = new LinkEmailPasswordRequest();
93+
request.setEmail("no-uid-" + UUID.randomUUID() + "@test.example.com");
94+
request.setPassword(linkedPassword);
95+
96+
final var response = client
97+
.target(apiRoot + "/user/me/link/email-password")
98+
.request(APPLICATION_JSON)
99+
.header(SESSION_SECRET, clientContext.getSessionSecret())
100+
.post(Entity.entity(request, APPLICATION_JSON));
101+
102+
assertEquals(response.getStatus(), 404);
103+
}
104+
105+
@Test(dependsOnMethods = "linkEmailPassword_unknownEmail_returns404")
106+
public void linkEmailPassword_verifiedEmail_returns200() {
107+
final var request = new LinkEmailPasswordRequest();
108+
request.setEmail(clientContext.getUser().getEmail());
109+
request.setPassword(linkedPassword);
110+
111+
final var response = client
112+
.target(apiRoot + "/user/me/link/email-password")
113+
.request(APPLICATION_JSON)
114+
.header(SESSION_SECRET, clientContext.getSessionSecret())
115+
.post(Entity.entity(request, APPLICATION_JSON));
116+
117+
assertEquals(response.getStatus(), 200);
118+
final var user = response.readEntity(User.class);
119+
assertNotNull(user);
120+
assertNotNull(user.getId());
121+
}
122+
123+
@Test(dependsOnMethods = "linkEmailPassword_verifiedEmail_returns200")
124+
public void login_withLinkedPassword_succeeds() {
125+
final var request = new UsernamePasswordSessionRequest();
126+
request.setUserId(clientContext.getUser().getEmail());
127+
request.setPassword(linkedPassword);
128+
129+
final var response = client
130+
.target(apiRoot + "/session")
131+
.request(APPLICATION_JSON)
132+
.post(Entity.entity(request, APPLICATION_JSON));
133+
134+
assertEquals(response.getStatus(), 200);
135+
final var session = response.readEntity(SessionCreation.class);
136+
assertNotNull(session.getSession());
137+
assertEquals(session.getSession().getUser().getId(), clientContext.getUser().getId());
138+
}
139+
140+
}

0 commit comments

Comments
 (0)