310 lines
11 KiB
Go
310 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
func TestPatchContactSetsPeerUserID(t *testing.T) {
|
|
server, _ := setupTestServer(t)
|
|
ownerID, ownerToken := mustSignup(t, server.URL, "주인")
|
|
peerID, _ := mustSignup(t, server.URL, "상대")
|
|
|
|
createResp := postJSONAuth(t, server.URL+"/users/"+strconv.FormatUint(uint64(ownerID), 10)+"/contacts", ownerToken, createContactRequest{
|
|
DisplayName: "이름만",
|
|
})
|
|
if createResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("create contact without peer: %d", createResp.StatusCode)
|
|
}
|
|
var created map[string]interface{}
|
|
json.NewDecoder(createResp.Body).Decode(&created)
|
|
contactID := uint(created["id"].(float64))
|
|
if created["contact_user_id"] != nil {
|
|
t.Fatalf("expected null peer id, got %v", created["contact_user_id"])
|
|
}
|
|
|
|
patchResp := patchJSONAuth(
|
|
t,
|
|
server.URL+"/users/"+strconv.FormatUint(uint64(ownerID), 10)+"/contacts/"+strconv.FormatUint(uint64(contactID), 10),
|
|
ownerToken,
|
|
updateContactRequest{
|
|
DisplayName: "상대방",
|
|
ContactUserID: &peerID,
|
|
},
|
|
)
|
|
if patchResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("patch contact: %d", patchResp.StatusCode)
|
|
}
|
|
var updated map[string]interface{}
|
|
json.NewDecoder(patchResp.Body).Decode(&updated)
|
|
if uint(updated["contact_user_id"].(float64)) != peerID {
|
|
t.Fatalf("expected peer %d, got %v", peerID, updated["contact_user_id"])
|
|
}
|
|
if updated["display_name"] != "상대방" {
|
|
t.Fatalf("display_name: %v", updated["display_name"])
|
|
}
|
|
}
|
|
|
|
func TestInvitesRequireAdminToken(t *testing.T) {
|
|
server, _ := setupTestServer(t)
|
|
resp := postJSON(t, server.URL+"/invites", nil)
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401 without admin token, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestLoginReturnsSessionToken(t *testing.T) {
|
|
server, _ := setupTestServer(t)
|
|
code := mintInvite(t, server.URL)
|
|
signup := postJSON(t, server.URL+"/auth/signup", signupRequest{InviteCode: code, DisplayName: "로그인"})
|
|
if signup.StatusCode != http.StatusOK {
|
|
t.Fatalf("signup: %d", signup.StatusCode)
|
|
}
|
|
|
|
login := postJSON(t, server.URL+"/auth/login", loginRequest{InviteCode: code})
|
|
if login.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected 200 login, got %d", login.StatusCode)
|
|
}
|
|
var out map[string]interface{}
|
|
json.NewDecoder(login.Body).Decode(&out)
|
|
if out["token"] == nil || out["token"] == "" {
|
|
t.Fatalf("login missing token: %v", out)
|
|
}
|
|
}
|
|
|
|
func TestConversationContactMessageHistoryAndEscalationLogs(t *testing.T) {
|
|
server, _ := setupTestServer(t)
|
|
ownerID, ownerToken := mustSignup(t, server.URL, "주인")
|
|
peerID, _ := mustSignup(t, server.URL, "상대")
|
|
|
|
// Create contact linking peer user.
|
|
contactResp := postJSONAuth(t, server.URL+"/users/"+strconv.FormatUint(uint64(ownerID), 10)+"/contacts", ownerToken, createContactRequest{
|
|
DisplayName: "상대방",
|
|
ContactUserID: &peerID,
|
|
})
|
|
if contactResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("create contact: %d", contactResp.StatusCode)
|
|
}
|
|
var contact map[string]interface{}
|
|
json.NewDecoder(contactResp.Body).Decode(&contact)
|
|
contactID := uint(contact["id"].(float64))
|
|
|
|
convResp := postJSONAuth(t, server.URL+"/conversations", ownerToken, createConversationRequest{
|
|
UserIDs: []uint{ownerID, peerID},
|
|
ContactID: &contactID,
|
|
})
|
|
if convResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("create conversation: %d", convResp.StatusCode)
|
|
}
|
|
var conv map[string]interface{}
|
|
json.NewDecoder(convResp.Body).Decode(&conv)
|
|
convID := uint(conv["id"].(float64))
|
|
|
|
listResp, _ := http.NewRequest(http.MethodGet, server.URL+"/conversations", nil)
|
|
listResp.Header.Set("Authorization", "Bearer "+ownerToken)
|
|
listed, err := http.DefaultClient.Do(listResp)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var listOut map[string]interface{}
|
|
json.NewDecoder(listed.Body).Decode(&listOut)
|
|
convs := listOut["conversations"].([]interface{})
|
|
if len(convs) != 1 {
|
|
t.Fatalf("expected 1 conversation, got %v", listOut)
|
|
}
|
|
|
|
postJSON(t, server.URL+"/conversations/"+strconv.FormatUint(uint64(convID), 10)+"/messages", sendMessageRequest{
|
|
SenderID: ownerID,
|
|
Text: "안녕 저녁 어때",
|
|
})
|
|
|
|
histReq, _ := http.NewRequest(http.MethodGet, server.URL+"/conversations/"+strconv.FormatUint(uint64(convID), 10)+"/messages", nil)
|
|
histReq.Header.Set("Authorization", "Bearer "+ownerToken)
|
|
histResp, err := http.DefaultClient.Do(histReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if histResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("history: %d", histResp.StatusCode)
|
|
}
|
|
var hist map[string]interface{}
|
|
json.NewDecoder(histResp.Body).Decode(&hist)
|
|
if len(hist["messages"].([]interface{})) != 1 {
|
|
t.Fatalf("expected 1 message, got %v", hist)
|
|
}
|
|
|
|
// Force an escalation log via twin send.
|
|
setAutonomyLevel(t, server.URL, ownerID, ownerToken, AutonomyL1)
|
|
postJSON(t, server.URL+"/conversations/"+strconv.FormatUint(uint64(convID), 10)+"/messages", sendMessageRequest{
|
|
SenderID: ownerID,
|
|
Text: "계좌번호 알려줄게",
|
|
SenderMode: SenderTwin,
|
|
Approved: true,
|
|
})
|
|
|
|
logReq, _ := http.NewRequest(http.MethodGet, server.URL+"/users/"+strconv.FormatUint(uint64(ownerID), 10)+"/escalation-logs", nil)
|
|
logReq.Header.Set("Authorization", "Bearer "+ownerToken)
|
|
logResp, err := http.DefaultClient.Do(logReq)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if logResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("escalation logs: %d", logResp.StatusCode)
|
|
}
|
|
var logs map[string]interface{}
|
|
json.NewDecoder(logResp.Body).Decode(&logs)
|
|
if len(logs["escalation_logs"].([]interface{})) < 1 {
|
|
t.Fatalf("expected escalation log, got %v", logs)
|
|
}
|
|
}
|
|
|
|
// 오프라인 큐 캐치업(roadmap.md "멀티 디바이스 동기화" / deploy-checklist N4-11):
|
|
// 재연결·앱 복귀 시 since_id로 놓친 메시지만 다시 받아올 수 있어야 한다.
|
|
func TestListMessagesSinceID(t *testing.T) {
|
|
server, _ := setupTestServer(t)
|
|
ownerID, ownerToken := mustSignup(t, server.URL, "캐치업주인")
|
|
peerID, _ := mustSignup(t, server.URL, "캐치업상대")
|
|
|
|
convResp := postJSONAuth(t, server.URL+"/conversations", ownerToken, createConversationRequest{
|
|
UserIDs: []uint{ownerID, peerID},
|
|
})
|
|
if convResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("create conversation: %d", convResp.StatusCode)
|
|
}
|
|
var conv map[string]interface{}
|
|
json.NewDecoder(convResp.Body).Decode(&conv)
|
|
convID := uint(conv["id"].(float64))
|
|
convPath := server.URL + "/conversations/" + strconv.FormatUint(uint64(convID), 10) + "/messages"
|
|
|
|
var sentIDs []uint
|
|
for _, text := range []string{"하나", "둘", "셋"} {
|
|
resp := postJSON(t, convPath, sendMessageRequest{SenderID: ownerID, Text: text})
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("send %q: %d", text, resp.StatusCode)
|
|
}
|
|
var sent map[string]interface{}
|
|
json.NewDecoder(resp.Body).Decode(&sent)
|
|
sentIDs = append(sentIDs, uint(sent["id"].(float64)))
|
|
}
|
|
|
|
getMessages := func(query string) *http.Response {
|
|
req, _ := http.NewRequest(http.MethodGet, convPath+query, nil)
|
|
req.Header.Set("Authorization", "Bearer "+ownerToken)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return resp
|
|
}
|
|
|
|
// No since_id -- existing behavior unchanged: full history.
|
|
full := getMessages("")
|
|
if full.StatusCode != http.StatusOK {
|
|
t.Fatalf("full history: %d", full.StatusCode)
|
|
}
|
|
var fullOut map[string]interface{}
|
|
json.NewDecoder(full.Body).Decode(&fullOut)
|
|
if len(fullOut["messages"].([]interface{})) != 3 {
|
|
t.Fatalf("expected 3 messages without since_id, got %v", fullOut)
|
|
}
|
|
|
|
// since_id=<first message> -- only the later two.
|
|
since := getMessages("?since_id=" + strconv.FormatUint(uint64(sentIDs[0]), 10))
|
|
if since.StatusCode != http.StatusOK {
|
|
t.Fatalf("since_id history: %d", since.StatusCode)
|
|
}
|
|
var sinceOut map[string]interface{}
|
|
json.NewDecoder(since.Body).Decode(&sinceOut)
|
|
sinceMsgs := sinceOut["messages"].([]interface{})
|
|
if len(sinceMsgs) != 2 {
|
|
t.Fatalf("expected 2 messages after since_id, got %v", sinceOut)
|
|
}
|
|
firstReturned := sinceMsgs[0].(map[string]interface{})
|
|
if uint(firstReturned["id"].(float64)) != sentIDs[1] {
|
|
t.Fatalf("expected first returned message to be %d, got %v", sentIDs[1], firstReturned["id"])
|
|
}
|
|
|
|
// since_id beyond the highest message id -- empty array, not an error.
|
|
beyond := getMessages("?since_id=" + strconv.FormatUint(uint64(sentIDs[2])+1000, 10))
|
|
if beyond.StatusCode != http.StatusOK {
|
|
t.Fatalf("beyond history: %d", beyond.StatusCode)
|
|
}
|
|
var beyondOut map[string]interface{}
|
|
json.NewDecoder(beyond.Body).Decode(&beyondOut)
|
|
if len(beyondOut["messages"].([]interface{})) != 0 {
|
|
t.Fatalf("expected 0 messages beyond highest id, got %v", beyondOut)
|
|
}
|
|
|
|
// Non-numeric since_id -- 400.
|
|
bad := getMessages("?since_id=abc")
|
|
if bad.StatusCode != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for non-numeric since_id, got %d", bad.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestContactScopedWhitelistMatch(t *testing.T) {
|
|
server, db := setupTestServer(t)
|
|
ownerID, ownerToken := mustSignup(t, server.URL, "화이트주인")
|
|
peerID, _ := mustSignup(t, server.URL, "화이트상대")
|
|
otherID, _ := mustSignup(t, server.URL, "다른사람")
|
|
|
|
contactResp := postJSONAuth(t, server.URL+"/users/"+strconv.FormatUint(uint64(ownerID), 10)+"/contacts", ownerToken, createContactRequest{
|
|
DisplayName: "상대",
|
|
ContactUserID: &peerID,
|
|
})
|
|
var contact map[string]interface{}
|
|
json.NewDecoder(contactResp.Body).Decode(&contact)
|
|
contactID := uint(contact["id"].(float64))
|
|
|
|
// Rule only for this contact + keyword 저녁
|
|
cid := contactID
|
|
postJSONAuth(t, server.URL+"/users/"+strconv.FormatUint(uint64(ownerID), 10)+"/whitelist-rules", ownerToken, createWhitelistRuleRequest{
|
|
ContactID: &cid,
|
|
TopicKeyword: "저녁",
|
|
})
|
|
setAutonomyLevel(t, server.URL, ownerID, ownerToken, AutonomyL2)
|
|
|
|
// Conversation with the whitelisted peer -- should auto-send.
|
|
convPeer := postJSONAuth(t, server.URL+"/conversations", ownerToken, createConversationRequest{
|
|
UserIDs: []uint{ownerID, peerID},
|
|
ContactID: &contactID,
|
|
})
|
|
var conv1 map[string]interface{}
|
|
json.NewDecoder(convPeer.Body).Decode(&conv1)
|
|
conv1ID := uint(conv1["id"].(float64))
|
|
|
|
okResp := postJSON(t, server.URL+"/conversations/"+strconv.FormatUint(uint64(conv1ID), 10)+"/messages", sendMessageRequest{
|
|
SenderID: ownerID,
|
|
Text: "오늘 저녁 뭐 먹을래",
|
|
SenderMode: SenderTwin,
|
|
})
|
|
if okResp.StatusCode != http.StatusOK {
|
|
t.Fatalf("expected L2 auto-send for matching contact+keyword, got %d", okResp.StatusCode)
|
|
}
|
|
|
|
// Conversation with a different peer -- same keyword must NOT match contact-scoped rule.
|
|
convOther := postJSONAuth(t, server.URL+"/conversations", ownerToken, createConversationRequest{
|
|
UserIDs: []uint{ownerID, otherID},
|
|
})
|
|
var conv2 map[string]interface{}
|
|
json.NewDecoder(convOther.Body).Decode(&conv2)
|
|
conv2ID := uint(conv2["id"].(float64))
|
|
|
|
blocked := postJSON(t, server.URL+"/conversations/"+strconv.FormatUint(uint64(conv2ID), 10)+"/messages", sendMessageRequest{
|
|
SenderID: ownerID,
|
|
Text: "오늘 저녁 뭐 먹을래",
|
|
SenderMode: SenderTwin,
|
|
})
|
|
if blocked.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("expected 403 when contact-scoped rule does not apply, got %d", blocked.StatusCode)
|
|
}
|
|
|
|
var n int64
|
|
db.Model(&Message{}).Where("conversation_id = ? AND sender_mode = ?", conv2ID, SenderTwin).Count(&n)
|
|
if n != 0 {
|
|
t.Fatalf("must not store twin message for non-matching contact, got %d", n)
|
|
}
|
|
}
|