HTTP API: server, credential and the list of chats (closes #4)
check / check (push) Successful in 1m13s
check / check (push) Successful in 1m13s
The bot now serves an HTTP API on `PORT` (default 8080) beside the chat client, whose WebSocket stays on 127.0.0.1 inside the container. Every request needs `Authorization: Bearer` with the credential from the file named by `API_TOKEN_FILE`, compared in constant time; with no credential configured every request is refused, `OPTIONS *` included. `GET /api/v1/chats` lists the bot's chats. Responses carry the security headers from the repository policies; bodies, requests and the server are time- and size-bounded. The chat client stops only after the API has finished its requests. Disclosures: `contact_deleted` is an extra field; 404 and 405 answer in JSON; requests net/http cannot parse are refused by net/http without the security headers; three gosec findings are suppressed as false positives. Model: opus-5-5
This commit was merged in pull request #12.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -43,6 +44,19 @@ const (
|
||||
|
||||
contactConnected = `{"type":"contactConnected","user":{"userId":1},
|
||||
"contact":{"contactId":3,"localDisplayName":"alice"}}`
|
||||
|
||||
// Two contacts with the same display name, which the chat client
|
||||
// tells apart by the local name it gives the second. The second has
|
||||
// deleted its chat with the bot, and is still listed.
|
||||
contactsList = `{"type":"contactsList","user":{"userId":1},"contacts":[
|
||||
{"contactId":3,"localDisplayName":"tester","profile":{"profileId":3,
|
||||
"displayName":"tester","fullName":"","localAlias":""},
|
||||
"activeConn":{"connId":2,"connStatus":{"type":"ready"}},
|
||||
"contactUsed":true,"contactStatus":"active","chatDeleted":false},
|
||||
{"contactId":4,"localDisplayName":"tester_1","profile":{"profileId":4,
|
||||
"displayName":"tester","fullName":"","localAlias":""},
|
||||
"activeConn":{"connId":3,"connStatus":{"type":"deleted"}},
|
||||
"contactUsed":true,"contactStatus":"deleted","chatDeleted":false}]}`
|
||||
)
|
||||
|
||||
// fakeChat stands in for the chat client's API. It answers each command
|
||||
@@ -220,6 +234,38 @@ func TestAddressSetup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestContacts: the contacts of the given user come back with their
|
||||
// ids, display names and whether they are deleted.
|
||||
func TestContacts(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, url := newFakeChat(t, map[string]string{"/_contacts": contactsList})
|
||||
c, ctx := dial(t, url, nil)
|
||||
|
||||
contacts, err := c.Contacts(ctx, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("Contacts: %v", err)
|
||||
}
|
||||
|
||||
if got := f.next(t); got != "/_contacts 1" {
|
||||
t.Errorf("command = %s, want /_contacts 1", got)
|
||||
}
|
||||
|
||||
tester := simplex.Profile{DisplayName: "tester"}
|
||||
|
||||
want := []simplex.Contact{
|
||||
{ContactID: 3, Profile: tester, Status: "active"},
|
||||
{ContactID: 4, Profile: tester, Status: "deleted"},
|
||||
}
|
||||
if !slices.Equal(contacts, want) {
|
||||
t.Fatalf("Contacts = %+v\nwant %+v", contacts, want)
|
||||
}
|
||||
|
||||
if contacts[0].Deleted() || !contacts[1].Deleted() {
|
||||
t.Error("Deleted must be false for contact 3 and true for contact 4")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRefusedCommand: a command the chat client refuses is an error
|
||||
// that names the reason.
|
||||
func TestRefusedCommand(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user