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:
@@ -0,0 +1,64 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"sneak.berlin/go/simplexcalc/internal/simplex"
|
||||
)
|
||||
|
||||
// TestChats: the chats are the bot's contacts, ordered by id, deleted
|
||||
// ones marked, and the chat client is asked for the bot's user with a
|
||||
// deadline.
|
||||
func TestChats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &fakeClient{contacts: []simplex.Contact{
|
||||
{ContactID: 3, Profile: simplex.Profile{DisplayName: "bob"}, Status: "deleted"},
|
||||
{ContactID: 2, Profile: simplex.Profile{DisplayName: "alice"}, Status: "active"},
|
||||
}}
|
||||
|
||||
rec := request(t, newAPI(credential, client), http.MethodGet, chatsPath, bearer)
|
||||
|
||||
want := `{"chats":[{"id":2,"display_name":"alice","contact_deleted":false},` +
|
||||
`{"id":3,"display_name":"bob","contact_deleted":true}]}` + "\n"
|
||||
if rec.Code != http.StatusOK || rec.Body.String() != want {
|
||||
t.Errorf("response = %d %q, want 200 %q", rec.Code, rec.Body.String(), want)
|
||||
}
|
||||
|
||||
if got := rec.Header().Get("Content-Type"); got != "application/json" {
|
||||
t.Errorf("Content-Type = %q, want application/json", got)
|
||||
}
|
||||
|
||||
if client.userID != 1 || !client.hadDeadline {
|
||||
t.Errorf("the chat client was asked for user %d, deadline %v; "+
|
||||
"want user 1 with a deadline", client.userID, client.hadDeadline)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoChats: no contacts is an empty list, not null.
|
||||
func TestNoChats(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := request(t, newAPI(credential, &fakeClient{}),
|
||||
http.MethodGet, chatsPath, bearer)
|
||||
|
||||
want := `{"chats":[]}` + "\n"
|
||||
if rec.Code != http.StatusOK || rec.Body.String() != want {
|
||||
t.Errorf("response = %d %q, want 200 %q", rec.Code, rec.Body.String(), want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChatsFailure: when the chat client fails, the response says so
|
||||
// in a chosen sentence, never in the error's own text.
|
||||
func TestChatsFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := request(t, newAPI(credential, &fakeClient{err: errChat}),
|
||||
http.MethodGet, chatsPath, bearer)
|
||||
|
||||
want := `{"error":"the chats could not be read"}` + "\n"
|
||||
if rec.Code != http.StatusInternalServerError || rec.Body.String() != want {
|
||||
t.Errorf("response = %d %q, want 500 %q", rec.Code, rec.Body.String(), want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user