- The entire Complement API now no longer uses `t *testing.T`, instead opting for `ct.TestLike` which is an interface. In practice, this changes nothing. However, this enables Complement to be used in many more places where you do not have a `t *testing.T` object to use e.g benchmarks, scripts. This is of particular use for Complement-Crypto which has to run parts of the test as a standalone binary, and therefore has no `t *testing.T` to use. - The entire Complement API now uses `ct.Fatalf` and `ct.Errorf` for highlighting test failures in red. This should make it significantly easier to skim for the test failure message. - Add `Deployment.ContainerID(TestLike, hsName) string` to allow tests to interact with the container beyond the abilities of the Complement API e.g log extraction, memory use, CPU use.
164 lines
5.3 KiB
Go
164 lines
5.3 KiB
Go
package client
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/hex"
|
|
"io"
|
|
|
|
"github.com/matrix-org/complement/ct"
|
|
"github.com/tidwall/gjson"
|
|
)
|
|
|
|
const (
|
|
SharedSecret = "complement"
|
|
)
|
|
|
|
type LoginOpt func(map[string]interface{})
|
|
|
|
func WithDeviceID(deviceID string) LoginOpt {
|
|
return func(loginBody map[string]interface{}) {
|
|
loginBody["device_id"] = deviceID
|
|
}
|
|
}
|
|
|
|
// LoginUser will log in to a homeserver and create a new device on an existing user.
|
|
func (c *CSAPI) LoginUser(t ct.TestLike, localpart, password string, opts ...LoginOpt) (userID, accessToken, deviceID string) {
|
|
t.Helper()
|
|
reqBody := map[string]interface{}{
|
|
"identifier": map[string]interface{}{
|
|
"type": "m.id.user",
|
|
"user": localpart,
|
|
},
|
|
"password": password,
|
|
"type": "m.login.password",
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
opt(reqBody)
|
|
}
|
|
|
|
res := c.MustDo(t, "POST", []string{"_matrix", "client", "v3", "login"}, WithJSONBody(t, reqBody))
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
ct.Fatalf(t, "unable to read response body: %v", err)
|
|
}
|
|
|
|
userID = GetJSONFieldStr(t, body, "user_id")
|
|
accessToken = GetJSONFieldStr(t, body, "access_token")
|
|
deviceID = GetJSONFieldStr(t, body, "device_id")
|
|
return userID, accessToken, deviceID
|
|
}
|
|
|
|
// LoginUserWithRefreshToken will log in to a homeserver, with refresh token enabled,
|
|
// and create a new device on an existing user.
|
|
func (c *CSAPI) LoginUserWithRefreshToken(t ct.TestLike, localpart, password string) (userID, accessToken, refreshToken, deviceID string, expiresInMs int64) {
|
|
t.Helper()
|
|
reqBody := map[string]interface{}{
|
|
"identifier": map[string]interface{}{
|
|
"type": "m.id.user",
|
|
"user": localpart,
|
|
},
|
|
"password": password,
|
|
"type": "m.login.password",
|
|
"refresh_token": true,
|
|
}
|
|
res := c.MustDo(t, "POST", []string{"_matrix", "client", "v3", "login"}, WithJSONBody(t, reqBody))
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
ct.Fatalf(t, "unable to read response body: %v", err)
|
|
}
|
|
|
|
userID = GetJSONFieldStr(t, body, "user_id")
|
|
accessToken = GetJSONFieldStr(t, body, "access_token")
|
|
deviceID = GetJSONFieldStr(t, body, "device_id")
|
|
refreshToken = GetJSONFieldStr(t, body, "refresh_token")
|
|
expiresInMs = gjson.GetBytes(body, "expires_in_ms").Int()
|
|
return userID, accessToken, refreshToken, deviceID, expiresInMs
|
|
}
|
|
|
|
// RefreshToken will consume a refresh token and return a new access token and refresh token.
|
|
func (c *CSAPI) ConsumeRefreshToken(t ct.TestLike, refreshToken string) (newAccessToken, newRefreshToken string, expiresInMs int64) {
|
|
t.Helper()
|
|
reqBody := map[string]interface{}{
|
|
"refresh_token": refreshToken,
|
|
}
|
|
res := c.MustDo(t, "POST", []string{"_matrix", "client", "v3", "refresh"}, WithJSONBody(t, reqBody))
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
ct.Fatalf(t, "unable to read response body: %v", err)
|
|
}
|
|
|
|
newAccessToken = GetJSONFieldStr(t, body, "access_token")
|
|
newRefreshToken = GetJSONFieldStr(t, body, "refresh_token")
|
|
expiresInMs = gjson.GetBytes(body, "expires_in_ms").Int()
|
|
return newAccessToken, newRefreshToken, expiresInMs
|
|
}
|
|
|
|
// RegisterUser will register the user with given parameters and
|
|
// return user ID, access token and device ID. It fails the test on network error.
|
|
func (c *CSAPI) RegisterUser(t ct.TestLike, localpart, password string) (userID, accessToken, deviceID string) {
|
|
t.Helper()
|
|
reqBody := map[string]interface{}{
|
|
"auth": map[string]string{
|
|
"type": "m.login.dummy",
|
|
},
|
|
"username": localpart,
|
|
"password": password,
|
|
}
|
|
res := c.MustDo(t, "POST", []string{"_matrix", "client", "v3", "register"}, WithJSONBody(t, reqBody))
|
|
|
|
body, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
ct.Fatalf(t, "unable to read response body: %v", err)
|
|
}
|
|
|
|
userID = GetJSONFieldStr(t, body, "user_id")
|
|
accessToken = GetJSONFieldStr(t, body, "access_token")
|
|
deviceID = GetJSONFieldStr(t, body, "device_id")
|
|
return userID, accessToken, deviceID
|
|
}
|
|
|
|
// RegisterSharedSecret registers a new account with a shared secret via HMAC
|
|
// See https://github.com/matrix-org/synapse/blob/e550ab17adc8dd3c48daf7fedcd09418a73f524b/synapse/_scripts/register_new_matrix_user.py#L40
|
|
func (c *CSAPI) RegisterSharedSecret(t ct.TestLike, user, pass string, isAdmin bool) (userID, accessToken, deviceID string) {
|
|
resp := c.Do(t, "GET", []string{"_synapse", "admin", "v1", "register"})
|
|
if resp.StatusCode != 200 {
|
|
t.Skipf("Homeserver image does not support shared secret registration, /_synapse/admin/v1/register returned HTTP %d", resp.StatusCode)
|
|
return
|
|
}
|
|
body := ParseJSON(t, resp)
|
|
nonce := gjson.GetBytes(body, "nonce")
|
|
if !nonce.Exists() {
|
|
ct.Fatalf(t, "Malformed shared secret GET response: %s", string(body))
|
|
}
|
|
mac := hmac.New(sha1.New, []byte(SharedSecret))
|
|
mac.Write([]byte(nonce.Str))
|
|
mac.Write([]byte("\x00"))
|
|
mac.Write([]byte(user))
|
|
mac.Write([]byte("\x00"))
|
|
mac.Write([]byte(pass))
|
|
mac.Write([]byte("\x00"))
|
|
if isAdmin {
|
|
mac.Write([]byte("admin"))
|
|
} else {
|
|
mac.Write([]byte("notadmin"))
|
|
}
|
|
sig := mac.Sum(nil)
|
|
reqBody := map[string]interface{}{
|
|
"nonce": nonce.Str,
|
|
"username": user,
|
|
"password": pass,
|
|
"mac": hex.EncodeToString(sig),
|
|
"admin": isAdmin,
|
|
}
|
|
resp = c.MustDo(t, "POST", []string{"_synapse", "admin", "v1", "register"}, WithJSONBody(t, reqBody))
|
|
body = ParseJSON(t, resp)
|
|
userID = GetJSONFieldStr(t, body, "user_id")
|
|
accessToken = GetJSONFieldStr(t, body, "access_token")
|
|
deviceID = GetJSONFieldStr(t, body, "device_id")
|
|
return userID, accessToken, deviceID
|
|
}
|