This commit is contained in:
parent
18f9fd34e5
commit
065d8a5417
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"git.pcux.cn/xuans/Smail/dto/response"
|
||||
"git.pcux.cn/xuans/Smail/services/access"
|
||||
"git.pcux.cn/xuans/Smail/services/credentials"
|
||||
appcontext "git.pcux.cn/xuans/Smail/utils/context"
|
||||
)
|
||||
|
||||
@ -23,6 +24,10 @@ func TestSecurityHandlersRejectUnauthenticatedRequests(t *testing.T) {
|
||||
{name: "generate recovery codes", hand: GenerateRecoveryCodes, body: `{"count":5}`},
|
||||
{name: "get recovery code status", hand: GetRecoveryCodeStatus},
|
||||
{name: "revoke recovery codes", hand: RevokeRecoveryCodes},
|
||||
{name: "list protocol credentials", hand: ListProtocolCredentials},
|
||||
{name: "set protocol enabled", hand: SetProtocolCredentialEnabled, body: `{"protocol":"smtp","enabled":true}`},
|
||||
{name: "generate protocol credential", hand: GenerateProtocolCredential, body: `{"protocol":"smtp"}`},
|
||||
{name: "revoke protocol credential", hand: RevokeProtocolCredential, body: `{"protocol":"smtp"}`},
|
||||
{name: "request verification", hand: RequestVerification, body: `{"purpose":"login","account":"smail","email":"user@example.com"}`},
|
||||
{name: "verify verification", hand: VerifyVerification, body: `{"id":"challenge","account":"smail","email":"user@example.com","code":"12345678","purpose":"login"}`},
|
||||
{name: "list external emails", hand: ListExternalEmails},
|
||||
@ -61,3 +66,114 @@ func TestSuperAdminCannotManageRecoveryCodesThroughWeb(t *testing.T) {
|
||||
t.Fatalf("error number = %d, want %d; body=%s", result.ErrorNo, response.NoAccessPrivileges, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtocolCredentialHandlersManageCurrentUserCredential(t *testing.T) {
|
||||
_, _, engine, user, _ := newPublicSecurityFixture(t)
|
||||
credentialService, err := credentials.NewService(engine)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previous := securityRuntime
|
||||
securityRuntime.credentials = credentialService
|
||||
t.Cleanup(func() { securityRuntime = previous })
|
||||
|
||||
ctx := &appcontext.Context{UserID: user.ID, UserAccount: user.Account}
|
||||
list := func() response.Response {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, "/api/security/credentials/protocols", nil)
|
||||
ListProtocolCredentials(ctx, recorder, request)
|
||||
var result response.Response
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &result); err != nil {
|
||||
t.Fatalf("invalid protocol list response: %v; body=%s", err, recorder.Body.String())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
initial := list()
|
||||
if initial.ErrorNo != 0 {
|
||||
t.Fatalf("list protocol credentials failed: %+v", initial)
|
||||
}
|
||||
|
||||
setRecorder := httptest.NewRecorder()
|
||||
setRequest := httptest.NewRequest(
|
||||
http.MethodPut,
|
||||
"/api/security/credentials/protocols/enabled",
|
||||
strings.NewReader(`{"protocol":"SMTP","enabled":true}`),
|
||||
)
|
||||
SetProtocolCredentialEnabled(ctx, setRecorder, setRequest)
|
||||
var setResult response.Response
|
||||
if err := json.Unmarshal(setRecorder.Body.Bytes(), &setResult); err != nil {
|
||||
t.Fatalf("invalid protocol enable response: %v", err)
|
||||
}
|
||||
if setResult.ErrorNo != 0 {
|
||||
t.Fatalf("enable protocol failed: %+v", setResult)
|
||||
}
|
||||
|
||||
generateRecorder := httptest.NewRecorder()
|
||||
generateRequest := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/api/security/credentials/protocols/generate",
|
||||
strings.NewReader(`{"protocol":"smtp"}`),
|
||||
)
|
||||
GenerateProtocolCredential(ctx, generateRecorder, generateRequest)
|
||||
var generateResult response.Response
|
||||
if err := json.Unmarshal(generateRecorder.Body.Bytes(), &generateResult); err != nil {
|
||||
t.Fatalf("invalid protocol generate response: %v", err)
|
||||
}
|
||||
if generateResult.ErrorNo != 0 {
|
||||
t.Fatalf("generate protocol credential failed: %+v", generateResult)
|
||||
}
|
||||
generated, ok := generateResult.Data.(map[string]any)
|
||||
if !ok || generated["code"] == "" || generated["shown_once"] != true {
|
||||
t.Fatalf("unexpected generated protocol response: %#v", generateResult.Data)
|
||||
}
|
||||
if strings.Contains(generateRecorder.Body.String(), "code_hash") {
|
||||
t.Fatal("protocol response exposed code hash")
|
||||
}
|
||||
|
||||
status := list()
|
||||
statusData, ok := status.Data.([]any)
|
||||
if !ok || len(statusData) != 3 {
|
||||
t.Fatalf("unexpected protocol status response: %#v", status.Data)
|
||||
}
|
||||
foundSMTP := false
|
||||
for _, item := range statusData {
|
||||
itemMap, ok := item.(map[string]any)
|
||||
if !ok || itemMap["protocol"] != "smtp" {
|
||||
continue
|
||||
}
|
||||
foundSMTP = true
|
||||
if itemMap["enabled"] != true || itemMap["configured"] != true {
|
||||
t.Fatalf("unexpected SMTP status: %#v", itemMap)
|
||||
}
|
||||
}
|
||||
if !foundSMTP {
|
||||
t.Fatal("SMTP protocol status was not returned")
|
||||
}
|
||||
|
||||
revokeRecorder := httptest.NewRecorder()
|
||||
revokeRequest := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"/api/security/credentials/protocols/revoke",
|
||||
strings.NewReader(`{"protocol":"smtp"}`),
|
||||
)
|
||||
RevokeProtocolCredential(ctx, revokeRecorder, revokeRequest)
|
||||
var revokeResult response.Response
|
||||
if err := json.Unmarshal(revokeRecorder.Body.Bytes(), &revokeResult); err != nil {
|
||||
t.Fatalf("invalid protocol revoke response: %v", err)
|
||||
}
|
||||
if revokeResult.ErrorNo != 0 {
|
||||
t.Fatalf("revoke protocol credential failed: %+v", revokeResult)
|
||||
}
|
||||
status = list()
|
||||
statusData, ok = status.Data.([]any)
|
||||
if !ok || len(statusData) != 3 {
|
||||
t.Fatalf("unexpected revoked protocol status response: %#v", status.Data)
|
||||
}
|
||||
for _, item := range statusData {
|
||||
itemMap, ok := item.(map[string]any)
|
||||
if ok && itemMap["protocol"] == "smtp" && itemMap["configured"] == true {
|
||||
t.Fatal("revoked SMTP protocol credential remained configured")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +187,10 @@ func TestFrontendV1RoutesAreRegistered(t *testing.T) {
|
||||
"/api/v1/security/credentials/recovery-codes",
|
||||
"/api/v1/security/credentials/recovery-codes/generate",
|
||||
"/api/v1/security/credentials/recovery-codes/revoke",
|
||||
"/api/v1/security/credentials/protocols",
|
||||
"/api/v1/security/credentials/protocols/enabled",
|
||||
"/api/v1/security/credentials/protocols/generate",
|
||||
"/api/v1/security/credentials/protocols/revoke",
|
||||
"/api/v1/security/external-email/bind",
|
||||
"/api/v1/security/external-email/disable",
|
||||
"/api/v1/security/external-email/list",
|
||||
|
||||
@ -19,6 +19,7 @@ import (
|
||||
"git.pcux.cn/xuans/Smail/config"
|
||||
"git.pcux.cn/xuans/Smail/db"
|
||||
"git.pcux.cn/xuans/Smail/models"
|
||||
"git.pcux.cn/xuans/Smail/services/credentials"
|
||||
"git.pcux.cn/xuans/Smail/utils/array"
|
||||
pcontext "git.pcux.cn/xuans/Smail/utils/context"
|
||||
"git.pcux.cn/xuans/Smail/utils/password"
|
||||
@ -30,6 +31,7 @@ import (
|
||||
var clientUnLogin *imapclient.Client
|
||||
var clientLogin *imapclient.Client
|
||||
var imapTestAddr string
|
||||
var imapTestProtocolCode string
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
wd, err := os.Getwd()
|
||||
@ -97,7 +99,7 @@ func TestMain(m *testing.M) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
err = clientLogin.Login("testCase", "testCase").Wait()
|
||||
err = clientLogin.Login("testCase@test.domain", imapTestProtocolCode).Wait()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -158,35 +160,67 @@ func seedIMAPTestData() {
|
||||
}
|
||||
|
||||
var num int
|
||||
_, err = db.Instance.Table(&models.UserEmail{}).Where("user_id=?", user.ID).Select("count(1)").Get(&num)
|
||||
_, err = db.Instance.Table(&models.UserEmail{}).Where("user_id=? AND email_id > 0", user.ID).Select("count(1)").Get(&num)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if num > 0 {
|
||||
return
|
||||
if num == 0 {
|
||||
statuses := []int8{3}
|
||||
for i := 0; i < 10; i++ {
|
||||
statuses = append(statuses, 0)
|
||||
}
|
||||
for _, status := range statuses {
|
||||
email := models.Email{
|
||||
Subject: "test",
|
||||
Status: status,
|
||||
FromAddress: "sender@test.domain",
|
||||
To: `[{"EmailAddress":"testCase@test.domain","Name":"testCase"}]`,
|
||||
MsgID: "imap-test@test.domain",
|
||||
SendDate: time.Now(),
|
||||
}
|
||||
if _, err = db.Instance.Insert(&email); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ue := models.UserEmail{UserID: user.ID, EmailID: email.Id, Status: status}
|
||||
if _, err = db.Instance.Insert(&ue); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statuses := []int8{3}
|
||||
for i := 0; i < 10; i++ {
|
||||
statuses = append(statuses, 0)
|
||||
canonicalAddress := "testcase@test.domain"
|
||||
var loginBinding models.UserEmail
|
||||
hasBinding, err := db.Instance.Table(&models.UserEmail{}).
|
||||
Where("user_id = ? AND address_canonical = ?", user.ID, canonicalAddress).
|
||||
Get(&loginBinding)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, status := range statuses {
|
||||
email := models.Email{
|
||||
Subject: "test",
|
||||
Status: status,
|
||||
FromAddress: "sender@test.domain",
|
||||
To: `[{"EmailAddress":"testCase@test.domain","Name":"testCase"}]`,
|
||||
MsgID: "imap-test@test.domain",
|
||||
SendDate: time.Now(),
|
||||
if !hasBinding {
|
||||
verifiedAt := time.Now().UTC()
|
||||
loginBinding = models.UserEmail{
|
||||
UserID: user.ID,
|
||||
Address: "testCase@test.domain",
|
||||
AddressCanonical: &canonicalAddress,
|
||||
VerifiedAt: &verifiedAt,
|
||||
IsPrimary: 1,
|
||||
}
|
||||
if _, err = db.Instance.Insert(&email); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ue := models.UserEmail{UserID: user.ID, EmailID: email.Id, Status: status}
|
||||
if _, err = db.Instance.Insert(&ue); err != nil {
|
||||
if _, err = db.Instance.Insert(&loginBinding); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
credentialService, err := credentials.NewService(db.Instance)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = credentialService.SetProtocolEnabled(user.ID, credentials.ProtocolIMAP, true); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
imapTestProtocolCode, err = credentialService.GenerateProtocolCode(user.ID, credentials.ProtocolIMAP)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapability(t *testing.T) {
|
||||
@ -210,7 +244,7 @@ func TestCapability(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
err := clientUnLogin.Login("testCase", "testCaseasdfsadf").Wait()
|
||||
err := clientUnLogin.Login("testCase@test.domain", "wrong-imap-code").Wait()
|
||||
sErr := err.(*imap.Error)
|
||||
if sErr.Code != "AUTHENTICATIONFAILED" {
|
||||
t.Error("Login Error")
|
||||
@ -495,19 +529,41 @@ func TestClose(t *testing.T) {
|
||||
|
||||
}
|
||||
func TestExpunge(t *testing.T) {
|
||||
if _, err := clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
clientLogin.Select("INBOX", &imap.SelectOptions{}).Wait()
|
||||
|
||||
res, err := clientLogin.UIDExpunge(imap.UIDSetNum(1, 2)).Collect()
|
||||
var targets []models.UserEmail
|
||||
if err := db.Instance.Table("user_email").
|
||||
Where("user_id = ? AND email_id > 0", 1).
|
||||
OrderBy("id ASC").
|
||||
Limit(2).
|
||||
Find(&targets); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
t.Fatal("no email relations available for expunge")
|
||||
}
|
||||
|
||||
uids := make([]imap.UID, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
uids = append(uids, imap.UID(target.ID))
|
||||
}
|
||||
res, err := clientLogin.UIDExpunge(imap.UIDSetNum(uids...)).Collect()
|
||||
if err != nil {
|
||||
t.Errorf("%+v", err)
|
||||
}
|
||||
t.Logf("%+v", res)
|
||||
var ues []models.UserEmail
|
||||
db.Instance.Table("user_email").Where("id=1 or id=2").Find(&ues)
|
||||
if len(ues) > 0 {
|
||||
t.Errorf("TestExpunge Error")
|
||||
|
||||
for _, target := range targets {
|
||||
var remaining models.UserEmail
|
||||
found, err := db.Instance.ID(target.ID).Get(&remaining)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if found {
|
||||
t.Errorf("email relation %d was not expunged", target.ID)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -620,6 +676,13 @@ func TestNoop(t *testing.T) {
|
||||
}
|
||||
}
|
||||
func TestIDLE(t *testing.T) {
|
||||
credentialService, err := credentials.NewService(db.Instance)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !credentialService.VerifyProtocolCode(1, credentials.ProtocolIMAP, imapTestProtocolCode) {
|
||||
t.Fatalf("test IMAP authorization code was rejected before raw LOGIN: %q", imapTestProtocolCode)
|
||||
}
|
||||
conn, err := tls.Dial("tcp", imapTestAddr, &tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@ -635,7 +698,7 @@ func TestIDLE(t *testing.T) {
|
||||
t.Fatalf("unexpected greeting: %s", line)
|
||||
}
|
||||
|
||||
if _, err = conn.Write([]byte("a001 LOGIN testCase testCase\r\n")); err != nil {
|
||||
if _, err = conn.Write([]byte("a001 LOGIN testCase@test.domain " + imapTestProtocolCode + "\r\n")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if line, err = readTaggedLine(reader, "a001"); err != nil {
|
||||
@ -699,7 +762,7 @@ func TestIdleNotice(t *testing.T) {
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
if err = client.Login("testCase", "testCase").Wait(); err != nil {
|
||||
if err = client.Login("testCase@test.domain", imapTestProtocolCode).Wait(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user