chldapasswd/change_test.go
Pierre-Olivier Mercier 37edbbb9b6
All checks were successful
continuous-integration/drone/push Build is passing
feat: add bearer-authenticated password change API
Add POST /api/v1/password endpoint accepting a JSON body
(username, old_password, new_password) protected by a shared
Bearer token (CHANGE_API_SECRET / -change-api-secret). It verifies
the current password via an LDAP bind before applying the change,
matching the alps "password" plugin bearer flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 23:00:42 +08:00

162 lines
4.6 KiB
Go

package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestCheckPasswdConstraint(t *testing.T) {
tests := []struct {
name string
pass string
wantErr bool
}{
{"valid password", "Correct1Horse", false},
{"too short", "Short1A", true},
{"exactly 12 chars", "Abcdefgh1234", false},
{"no uppercase", "correct1horse", true},
{"no lowercase", "CORRECT1HORSE", true},
{"no digit", "CorrectHorse!", true},
{"exactly 128 chars", strings.Repeat("a", 126) + "A1", false},
{"129 chars is too long", strings.Repeat("a", 127) + "A1", true},
{"very long password", strings.Repeat("Aa1", 100), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkPasswdConstraint(tt.pass)
if (err != nil) != tt.wantErr {
t.Errorf("checkPasswdConstraint(%q) error = %v, wantErr %v", tt.pass, err, tt.wantErr)
}
})
}
}
// TestChangePasswordAPI exercises the request-validation paths of the JSON
// password change API that return before any LDAP connection is attempted.
func TestChangePasswordAPI(t *testing.T) {
const secret = "s3cr3t-token"
// Restore the global secret after the test.
orig := changeAPISecret
t.Cleanup(func() { changeAPISecret = orig })
tests := []struct {
name string
secret string // value of changeAPISecret for this case
authHeader string // Authorization header ("" means none)
body string
wantStatus int
}{
{
name: "API disabled",
secret: "",
authHeader: "Bearer " + secret,
body: `{"username":"alice","old_password":"OldPass12345","new_password":"NewPass12345"}`,
wantStatus: http.StatusServiceUnavailable,
},
{
name: "missing token",
secret: secret,
authHeader: "",
body: `{"username":"alice","old_password":"OldPass12345","new_password":"NewPass12345"}`,
wantStatus: http.StatusUnauthorized,
},
{
name: "wrong token",
secret: secret,
authHeader: "Bearer nope",
body: `{"username":"alice","old_password":"OldPass12345","new_password":"NewPass12345"}`,
wantStatus: http.StatusUnauthorized,
},
{
name: "malformed authorization header",
secret: secret,
authHeader: secret,
body: `{"username":"alice","old_password":"OldPass12345","new_password":"NewPass12345"}`,
wantStatus: http.StatusUnauthorized,
},
{
name: "invalid JSON",
secret: secret,
authHeader: "Bearer " + secret,
body: `{not json`,
wantStatus: http.StatusBadRequest,
},
{
name: "missing username",
secret: secret,
authHeader: "Bearer " + secret,
body: `{"old_password":"OldPass12345","new_password":"NewPass12345"}`,
wantStatus: http.StatusBadRequest,
},
{
name: "missing passwords",
secret: secret,
authHeader: "Bearer " + secret,
body: `{"username":"alice"}`,
wantStatus: http.StatusBadRequest,
},
{
name: "weak new password",
secret: secret,
authHeader: "Bearer " + secret,
body: `{"username":"alice","old_password":"OldPass12345","new_password":"short"}`,
wantStatus: http.StatusNotAcceptable,
},
}
for i, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
changeAPISecret = tt.secret
req := httptest.NewRequest(http.MethodPost, "/api/v1/password", strings.NewReader(tt.body))
if tt.authHeader != "" {
req.Header.Set("Authorization", tt.authHeader)
}
// Unique remote address per case so the shared rate limiter never trips.
req.RemoteAddr = fmt.Sprintf("192.0.2.%d:1234", i+1)
w := httptest.NewRecorder()
changePasswordAPI(w, req)
if w.Code != tt.wantStatus {
t.Errorf("status = %d, want %d (body: %s)", w.Code, tt.wantStatus, w.Body.String())
}
})
}
}
func TestCheckChangeAPIAuthorization(t *testing.T) {
orig := changeAPISecret
t.Cleanup(func() { changeAPISecret = orig })
changeAPISecret = "s3cr3t-token"
tests := []struct {
name string
header string
want bool
}{
{"valid", "Bearer s3cr3t-token", true},
{"case-insensitive scheme", "bearer s3cr3t-token", true},
{"wrong token", "Bearer wrong", false},
{"missing", "", false},
{"no scheme", "s3cr3t-token", false},
{"wrong scheme", "Basic s3cr3t-token", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/api/v1/password", nil)
if tt.header != "" {
req.Header.Set("Authorization", tt.header)
}
if got := checkChangeAPIAuthorization(req); got != tt.want {
t.Errorf("checkChangeAPIAuthorization(%q) = %v, want %v", tt.header, got, tt.want)
}
})
}
}