Add wsl linter and fix

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-19 14:51:08 +04:00
parent eedfdcecc8
commit 9156d6a16a
882 changed files with 6068 additions and 574 deletions

View File

@@ -80,6 +80,7 @@ func AssertTimestampsOnUpdate(t *testing.T, createdAt, updatedAt, originalCreate
func AssertOptionalStringEqual(t *testing.T, expected, actual *string, fieldName string) {
t.Helper()
if expected == nil {
assert.Nil(t, actual, "%s should be nil", fieldName)
} else {
@@ -95,6 +96,7 @@ func AssertOrderedAscending[T cmp.Ordered](t *testing.T, values []T, fieldName s
func AssertOrderedDescending[T cmp.Ordered](t *testing.T, values []T, fieldName string) {
t.Helper()
reversed := slices.Clone(values)
slices.Reverse(reversed)
assert.True(t, slices.IsSorted(reversed), "%s should be in descending order, got: %v", fieldName, values)
@@ -102,6 +104,7 @@ func AssertOrderedDescending[T cmp.Ordered](t *testing.T, values []T, fieldName
func AssertTimesOrderedAscending(t *testing.T, times []time.Time, fieldName string) {
t.Helper()
isSorted := slices.IsSortedFunc(times, func(a, b time.Time) int {
return a.Compare(b)
})
@@ -110,6 +113,7 @@ func AssertTimesOrderedAscending(t *testing.T, times []time.Time, fieldName stri
func AssertTimesOrderedDescending(t *testing.T, times []time.Time, fieldName string) {
t.Helper()
isSorted := slices.IsSortedFunc(times, func(a, b time.Time) int {
return b.Compare(a)
})
@@ -118,9 +122,11 @@ func AssertTimesOrderedDescending(t *testing.T, times []time.Time, fieldName str
func AssertNodeNotAccessible(t *testing.T, err error, nodeIsNil bool, resourceType string) {
t.Helper()
if err == nil {
assert.True(t, nodeIsNil, "should not be able to access %s from another org", resourceType)
}
// If there's an error, that's also acceptable (access denied)
}

View File

@@ -32,6 +32,7 @@ import (
func generateUniqueID() string {
randomBytes := make([]byte, 4)
_, _ = rand.Read(randomBytes)
return fmt.Sprintf("%d-%s", time.Now().UnixNano(), hex.EncodeToString(randomBytes))
}
@@ -139,6 +140,7 @@ func (c *Client) SetupTestUserInOrg(ownerClient *Client) {
c.userID = identityID
c.profileID = profileID
ownerClient.inviteUser(profileID)
token := c.getActivationToken(email)
passwordToken := c.activateUser(token)
c.resetPassword(password, passwordToken)
@@ -285,12 +287,14 @@ func (c *Client) updateOwnMembershipRole(role coredata.MembershipRole) {
require.NoError(c.T, err, "cannot query organization members")
var membershipID string
for _, edge := range qResult.Node.Members.Edges {
if edge.Node.Identity.ID == c.userID.String() {
membershipID = edge.Node.ID
break
}
}
require.NotEmpty(c.T, membershipID, "membership not found for user")
// Update the role
@@ -422,6 +426,7 @@ func (c *Client) getActivationToken(email string) string {
c.T.Logf("activation token not found")
c.T.FailNow()
return ""
}

View File

@@ -50,9 +50,11 @@ func (e GraphQLError) Code() string {
if e.Extensions == nil {
return ""
}
if code, ok := e.Extensions["code"].(string); ok {
return code
}
return ""
}
@@ -62,9 +64,11 @@ func (e GraphQLErrors) Error() string {
if len(e) == 0 {
return ""
}
if len(e) == 1 {
return e[0].Message
}
return fmt.Sprintf("%s (and %d more errors)", e[0].Message, len(e)-1)
}
@@ -83,12 +87,14 @@ func (c *Client) doWithEndpoint(endpoint string, query string, variables map[str
if err != nil {
return nil, fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)
@@ -160,6 +166,7 @@ func (c *Client) ExecuteShouldFail(query string, variables map[string]any) error
c.T.Helper()
_, err := c.Do(query, variables)
require.Error(c.T, err, "expected GraphQL request to fail but it succeeded")
return err
}
@@ -188,6 +195,7 @@ func (c *Client) ExecuteWithFiles(query string, variables map[string]any, files
func (c *Client) executeMultipart(query string, variables map[string]any, files map[string]UploadFile, result any) error {
// Create multipart writer using standard library
var buf bytes.Buffer
writer := multipart.NewWriter(&buf)
// Build the operations JSON
@@ -195,6 +203,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
"query": query,
"variables": variables,
}
operationsJSON, err := json.Marshal(operations)
if err != nil {
return fmt.Errorf("cannot marshal operations: %w", err)
@@ -207,14 +216,17 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
// Build the map for file variables (sorted for deterministic order)
fileMap := make(map[string][]string)
fileOrder := make([]string, 0, len(files))
for path := range files {
fileOrder = append(fileOrder, path)
}
// Sort for deterministic ordering
for i, path := range fileOrder {
fileMap[fmt.Sprintf("%d", i)] = []string{"variables." + path}
}
mapJSON, err := json.Marshal(fileMap)
if err != nil {
return fmt.Errorf("cannot marshal map: %w", err)
@@ -239,6 +251,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
if err != nil {
return fmt.Errorf("cannot create file part %s: %w", path, err)
}
if _, err := part.Write(file.Content); err != nil {
return fmt.Errorf("cannot write file content %s: %w", path, err)
}
@@ -253,6 +266,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
if err != nil {
return fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
// Execute request
@@ -260,6 +274,7 @@ func (c *Client) executeMultipart(query string, variables map[string]any, files
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)

View File

@@ -45,12 +45,14 @@ func (c *Client) SearchMails(query string) (*MailpitSearchResponse, error) {
if err != nil {
return nil, fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)
@@ -75,12 +77,14 @@ func (c *Client) CheckMessageLinks(messageID string) (*MailpitLinkCheckResponse,
if err != nil {
return nil, fmt.Errorf("cannot create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)

View File

@@ -128,6 +128,7 @@ func (mc *MCPClient) doRequest(method string, params any) (json.RawMessage, erro
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
req.Header.Set("Authorization", "Bearer "+mc.apiToken)
if mc.sessionID != "" {
req.Header.Set("Mcp-Session-Id", mc.sessionID)
}
@@ -136,6 +137,7 @@ func (mc *MCPClient) doRequest(method string, params any) (json.RawMessage, erro
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer func() { _ = resp.Body.Close() }()
respBody, err := io.ReadAll(resp.Body)
@@ -198,6 +200,7 @@ func (mc *MCPClient) CallTool(toolName string, args map[string]any) *MCPToolResu
require.NoError(mc.t, err, "MCP tools/call %s failed", toolName)
var toolResult MCPToolResult
err = json.Unmarshal(result, &toolResult)
require.NoError(mc.t, err, "cannot unmarshal tool result for %s", toolName)
@@ -212,6 +215,7 @@ func (mc *MCPClient) CallToolExpectToolError(toolName string, args map[string]an
require.NotEmpty(mc.t, tr.Content, "tool %s returned no content", toolName)
var text string
err := json.Unmarshal(tr.Content[0].Text, &text)
require.NoError(mc.t, err, "cannot unmarshal error text for %s", toolName)
@@ -227,6 +231,7 @@ func (mc *MCPClient) CallToolInto(toolName string, args map[string]any, dest any
// The text field in MCP content is a JSON-encoded string of the output.
// First unmarshal the raw JSON to get the string.
var textStr string
err := json.Unmarshal(tr.Content[0].Text, &textStr)
require.NoError(mc.t, err, "cannot unmarshal text content for %s", toolName)

View File

@@ -129,6 +129,7 @@ func postForm(
if err != nil {
return nil, fmt.Errorf("cannot post form: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
@@ -153,6 +154,7 @@ func postJSON(
if err != nil {
return nil, fmt.Errorf("cannot post json: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
@@ -181,6 +183,7 @@ func getJSON(
if err != nil {
return nil, fmt.Errorf("cannot execute request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
@@ -213,6 +216,7 @@ func postFormWithBasicAuth(
if err != nil {
return nil, fmt.Errorf("cannot execute request: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
@@ -298,10 +302,12 @@ func OAuth2Authorize(
}
reqURL := oauth2BaseURL(c) + "/authorize?" + params.Encode()
resp, err := noRedirectClient.Get(reqURL)
if err != nil {
return nil, fmt.Errorf("cannot get authorize: %w", err)
}
defer func() { _ = resp.Body.Close() }()
body, err := io.ReadAll(resp.Body)
@@ -521,6 +527,7 @@ func OAuth2TokenWithDeviceCode(
if err := json.Unmarshal(raw.Body, &result); err != nil {
return nil, nil, raw, fmt.Errorf("cannot decode token response: %w", err)
}
return &result, nil, raw, nil
}
@@ -799,6 +806,7 @@ func GeneratePKCE() (verifier, challenge string) {
for i := range b {
b[i] = charset[rand.IntN(len(charset))]
}
verifier = string(b)
h := sha256.Sum256([]byte(verifier))
@@ -814,11 +822,14 @@ func IsConsentRedirect(resp *OAuth2HTTPResponse) bool {
if resp.StatusCode != http.StatusFound {
return false
}
loc := resp.Header.Get("Location")
u, err := url.Parse(loc)
if err != nil {
return false
}
return u.Query().Get("consent_id") != ""
}
@@ -831,14 +842,17 @@ func ExtractConsentIDFromResponse(resp *OAuth2HTTPResponse) (string, error) {
if loc == "" {
return "", fmt.Errorf("no Location header in redirect response")
}
u, err := url.Parse(loc)
if err != nil {
return "", fmt.Errorf("cannot parse redirect url: %w", err)
}
consentID := u.Query().Get("consent_id")
if consentID == "" {
return "", fmt.Errorf("no consent_id in redirect url: %s", loc)
}
return consentID, nil
}
@@ -850,12 +864,14 @@ func ExtractConsentID(body []byte) (string, error) {
s := string(body)
needle := `name="consent_id" value="`
idx := strings.Index(s, needle)
if idx == -1 {
return "", fmt.Errorf("consent_id not found in page")
}
start := idx + len(needle)
end := strings.Index(s[start:], `"`)
if end == -1 {
return "", fmt.Errorf("malformed consent_id value")
@@ -890,6 +906,7 @@ func OAuth2PerformAuthorizationCodeFlow(
require.NoError(t, err)
var code string
if IsConsentRedirect(authResp) {
consentID, err := ExtractConsentIDFromResponse(authResp)
require.NoError(t, err)

View File

@@ -32,9 +32,11 @@ func ProseMirrorTextDoc(text string) string {
},
},
}
b, err := json.Marshal(doc)
if err != nil {
panic(err)
}
return string(b)
}

View File

@@ -54,6 +54,7 @@ func (s *switchableWriter) Write(p []byte) (int, error) {
s.mu.Lock()
w := s.w
s.mu.Unlock()
return w.Write(p)
}
@@ -104,6 +105,7 @@ func Setup() {
cmd.Stderr = os.Stderr
} else {
var buf bytes.Buffer
testEnv.outputBuf = &buf
sw := &switchableWriter{w: &buf}
testEnv.outputWriter = sw
@@ -128,14 +130,18 @@ func Setup() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := waitForServer(ctx, testEnv.BaseURL+"/api/console/v1/graphql", 30*time.Second); err != nil {
testEnv.dumpOutputOnFailure("API server failed to start", err)
_ = testEnv.cmd.Process.Kill()
os.Exit(1)
}
if err := waitForServer(ctx, testEnv.MailpitBaseURL+"/api/v1/messages", 30*time.Second); err != nil {
testEnv.dumpOutputOnFailure("MailPit server failed to start", err)
_ = testEnv.cmd.Process.Kill()
os.Exit(1)
}
@@ -161,11 +167,13 @@ func (e *TestEnv) dumpOutputOnFailure(context string, err error) {
if e.outputBuf != nil && e.outputBuf.Len() > 0 {
output := e.outputBuf.Bytes()
const maxTail = 10_000
if len(output) > maxTail {
fmt.Fprintf(os.Stderr, "e2etest: (showing last %d bytes of output)\n", maxTail)
output = output[len(output)-maxTail:]
}
fmt.Fprintf(os.Stderr, "--- probod output start ---\n%s\n--- probod output end ---\n", output)
} else {
fmt.Fprintf(os.Stderr, "e2etest: no captured output available\n")
@@ -224,6 +232,7 @@ func GetBaseURL() string {
if testEnv == nil {
return "http://localhost:8080"
}
return testEnv.BaseURL
}
@@ -231,6 +240,7 @@ func GetMailpitBaseURL() string {
if testEnv == nil {
return "http://localhost:8025"
}
return testEnv.MailpitBaseURL
}
@@ -305,6 +315,7 @@ func generateConfig() (string, error) {
if v, ok := env[key]; ok {
return v
}
return os.Getenv(key)
})
@@ -317,6 +328,7 @@ func generateConfig() (string, error) {
if err != nil {
return "", fmt.Errorf("create temp dir: %w", err)
}
path := filepath.Join(tmpDir, "probod.yml")
if err := bootstrap.WriteConfig(cfg, path); err != nil {