HTTP API: server, credential and the list of chats (closes #4)
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:
2026-09-29 04:55:49 +02:00
parent ac721390de
commit f10d820ed4
18 changed files with 1214 additions and 47 deletions
+46
View File
@@ -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) {