test(e2e): follow the panel's markup for applications and the send log
Two scrapes had drifted from the pages they read, and the suite has not been run since either page changed. applicationID still looked for an application as a table row (<td class="code">login</td> ... /applications/N/mode). Applications became a list of blocks ind35b309, so the lookup had been failing for several commits, including the one currently deployed — this is stale test, not a regression. It now anchors on the login heading and takes the id from the first action posted under it, whichever that is, so reordering a block's controls will not break it again. The level-2 rate-limit check looked for the application's login among the send-log rows.997af18took that column off the log — the log identifies a message and names the application only on a row's own page — so the check now filters the log by application instead. That is the same attribution through a server-side WHERE app_login rather than a substring match on rendered HTML. Verified on selfpost.example.com: make e2e green, all subtests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -166,7 +166,7 @@ func TestE2E(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := waitFor("send-log row to reach status=sent", 30*time.Second, 500*time.Millisecond, func() (bool, error) {
|
if err := waitFor("send-log row to reach status=sent", 30*time.Second, 500*time.Millisecond, func() (bool, error) {
|
||||||
rows, err := sc.panel.sendLogRows(senderDomain)
|
rows, err := sc.panel.sendLogRows(senderDomain, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package e2e
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -54,11 +53,11 @@ func testLevel2RateLimit(t *testing.T, sc *scenario) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := waitFor("a rejected row for l2app in the send log", 15*time.Second, 500*time.Millisecond, func() (bool, error) {
|
if err := waitFor("a rejected row for l2app in the send log", 15*time.Second, 500*time.Millisecond, func() (bool, error) {
|
||||||
rows, err := sc.panel.sendLogRows(senderDomain)
|
rows, err := sc.panel.sendLogRows(senderDomain, login)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if strings.Contains(rows, "l2app") && containsCell(rows, "rejected") {
|
if containsCell(rows, "rejected") {
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("no rejected row for l2app yet")
|
return false, fmt.Errorf("no rejected row for l2app yet")
|
||||||
|
|||||||
@@ -189,9 +189,17 @@ func (c *panelClient) setRateLimit(path, allowedIP string, maxMessages, windowSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sendLogRows returns the raw /deliveries/rows HTML fragment, filtered to one
|
// sendLogRows returns the raw /deliveries/rows HTML fragment, filtered to one
|
||||||
// domain, for polling a row's status without parsing full HTML into structs.
|
// domain and optionally to one application login, for polling a row's status
|
||||||
func (c *panelClient) sendLogRows(domain string) (string, error) {
|
// without parsing full HTML into structs. The application is a filter rather
|
||||||
_, body, err := c.get("/deliveries/rows?domain=" + url.QueryEscape(domain))
|
// than something to search the returned rows for: the log's columns identify a
|
||||||
|
// message (time, from, to, subject, status) and the sending application is only
|
||||||
|
// named on a row's own /deliveries/{id} page.
|
||||||
|
func (c *panelClient) sendLogRows(domain, app string) (string, error) {
|
||||||
|
q := url.Values{"domain": {domain}}
|
||||||
|
if app != "" {
|
||||||
|
q.Set("app", app)
|
||||||
|
}
|
||||||
|
_, body, err := c.get("/deliveries/rows?" + q.Encode())
|
||||||
return body, err
|
return body, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,15 +210,18 @@ func (c *panelClient) status() (*http.Response, error) {
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// applicationID scrapes an application's numeric id off its domain page row,
|
// applicationID scrapes an application's numeric id off its block on the domain
|
||||||
// keyed by login — needed to build /applications/{id}/ratelimit, which the
|
// page, keyed by login — needed to build /applications/{id}/ratelimit, which the
|
||||||
// add-application response (just the login/password) does not carry.
|
// add-application response (just the login/password) does not carry. The id is
|
||||||
|
// taken from the first action posted under that login, whichever it is, so
|
||||||
|
// reordering the block's controls does not break the scrape; only the login
|
||||||
|
// heading itself is anchored on.
|
||||||
func (c *panelClient) applicationID(domainID, login string) (string, error) {
|
func (c *panelClient) applicationID(domainID, login string) (string, error) {
|
||||||
_, body, err := c.get("/domains/" + domainID)
|
_, body, err := c.get("/domains/" + domainID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
pattern := `(?s)<td class="code">` + regexp.QuoteMeta(login) + `</td>.*?/applications/(\d+)/mode`
|
pattern := `(?s)<p class="app-login">` + regexp.QuoteMeta(login) + `</p>.*?/applications/(\d+)/`
|
||||||
m := regexp.MustCompile(pattern).FindStringSubmatch(body)
|
m := regexp.MustCompile(pattern).FindStringSubmatch(body)
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return "", fmt.Errorf("could not find application id for login %q", login)
|
return "", fmt.Errorf("could not find application id for login %q", login)
|
||||||
|
|||||||
Reference in New Issue
Block a user