| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226 |
- package main
- import (
- "context"
- "fmt"
- "net"
- "path/filepath"
- "regexp"
- "strings"
- "testing"
- "time"
- "github.com/stretchr/testify/assert"
- "github.com/mk6i/open-oscar-server/wire"
- )
- // tocTestClient is a TOC client connected to the TOC server under test.
- type tocTestClient struct {
- t *testing.T
- name string
- conn net.Conn
- flapc *wire.FlapClient
- lines chan string
- }
- // dialTOC connects a TOC client to addr and sends the signon command.
- func dialTOC(t *testing.T, addr string, screenName string, signonCmd string) *tocTestClient {
- conn, err := net.Dial("tcp", addr)
- if !assert.NoError(t, err) {
- t.FailNow()
- }
- _, err = conn.Write([]byte("FLAPON\r\n\r\n"))
- assert.NoError(t, err)
- flapc := wire.NewFlapClient(0, conn, conn)
- _, err = flapc.ReceiveSignonFrame()
- assert.NoError(t, err)
- assert.NoError(t, flapc.SendSignonFrame([]wire.TLV{wire.NewTLVBE(0x01, screenName)}))
- c := &tocTestClient{
- t: t,
- name: screenName,
- conn: conn,
- flapc: flapc,
- lines: make(chan string, 100),
- }
- go func() {
- defer close(c.lines)
- for {
- frame, err := flapc.ReceiveFLAP()
- if err != nil {
- return
- }
- if frame.FrameType == wire.FLAPFrameData {
- c.lines <- string(frame.Payload)
- }
- }
- }()
- c.send(signonCmd)
- return c
- }
- func (c *tocTestClient) send(cmd string) {
- assert.NoError(c.t, c.flapc.SendDataFrame(append([]byte(cmd), 0)))
- }
- // expect skips lines until one matches pattern, failing the test if none does
- // within a few seconds. It returns the match and its submatches.
- func (c *tocTestClient) expect(pattern string) []string {
- rx := regexp.MustCompile(pattern)
- var skipped []string
- timeout := time.After(5 * time.Second)
- for {
- select {
- case line, ok := <-c.lines:
- if !ok {
- c.t.Fatalf("%s: connection closed waiting for /%s/, skipped %q", c.name, pattern, skipped)
- }
- if m := rx.FindStringSubmatch(line); m != nil {
- return m
- }
- skipped = append(skipped, line)
- case <-timeout:
- c.t.Fatalf("%s: timed out waiting for /%s/, skipped %q", c.name, pattern, skipped)
- }
- }
- }
- // TestTOC_OSCARIntegration drives TOC clients through the TOC server against a
- // real OSCAR server that TOC reaches over in-memory OSCAR connections.
- func TestTOC_OSCARIntegration(t *testing.T) {
- ln, err := net.Listen("tcp", "127.0.0.1:0")
- assert.NoError(t, err)
- tocAddr := ln.Addr().String()
- assert.NoError(t, ln.Close())
- t.Setenv("OSCAR_LISTENERS", "LOCAL://127.0.0.1:5190")
- t.Setenv("OSCAR_ADVERTISED_LISTENERS_PLAIN", "LOCAL://127.0.0.1:5190")
- t.Setenv("TOC_LISTENERS", tocAddr)
- t.Setenv("API_LISTENER", "127.0.0.1:0")
- t.Setenv("DB_PATH", filepath.Join(t.TempDir(), "oscar.sqlite"))
- t.Setenv("DISABLE_AUTH", "true")
- t.Setenv("LOG_LEVEL", "error")
- t.Setenv("WEBAPI_ENABLED", "false")
- t.Setenv("ICQ_LEGACY_ENABLED", "false")
- deps, err := MakeCommonDeps()
- if !assert.NoError(t, err) {
- t.FailNow()
- }
- oscarServer := OSCAR(deps)
- tocServer := TOC(deps, oscarServer)
- go func() {
- _ = tocServer.ListenAndServe()
- }()
- defer func() {
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- _ = oscarServer.Shutdown(ctx)
- _ = tocServer.Shutdown(ctx)
- }()
- assert.Eventually(t, func() bool {
- conn, err := net.Dial("tcp", tocAddr)
- if err != nil {
- return false
- }
- _ = conn.Close()
- return true
- }, 5*time.Second, 10*time.Millisecond)
- const password = "0x2408105c23001130"
- alice := dialTOC(t, tocAddr, "alice", `toc2_login localhost 9898 alice `+password+` english "TIC:Test" 160 US "" "" 3 0 30303 -kentucky -utf8 103547776`)
- alice.expect(`^SIGN_ON:TOC2\.0$`)
- alice.expect(`^NICK:alice$`)
- alice.expect(`^CONFIG2:done:\n$`)
- alice.send("toc2_new_buddies {g:Buddies\nb:bob\n}")
- alice.expect(`^NEW_BUDDY_REPLY2:bob:added$`)
- alice.send("toc_init_done")
- bob := dialTOC(t, tocAddr, "bob", `toc_signon localhost 9898 bob `+password+` english "TIC:Test"`)
- bob.expect(`^SIGN_ON:TOC1\.0$`)
- bob.expect(`^CONFIG:$`)
- bob.expect(`^NICK:bob$`)
- bob.send("toc_add_buddy alice")
- bob.send("toc_init_done")
- // presence in both directions
- alice.expect(`^UPDATE_BUDDY2:bob:T:0:\d+:0:.{3}:$`)
- bob.expect(`^UPDATE_BUDDY:alice:T:0:\d+:0:.{3}$`)
- // instant messages in both directions
- bob.send(`toc_send_im alice "hello alice"`)
- alice.expect(`^IM_IN_ENC2:bob:F:F:T:...:F:L:en:hello alice$`)
- alice.send(`toc2_send_im bob "hello bob"`)
- bob.expect(`^IM_IN:alice:F:hello bob$`)
- // a reply that only makes sense next to its request
- bob.send("toc_get_status nobody")
- bob.expect(`^ERROR:901:nobody$`)
- // the room is announced before its first member list
- alice.send(`toc_chat_join 4 "Integration Room"`)
- alice.expect(`^CHAT_JOIN:0:Integration Room$`)
- alice.expect(`^CHAT_UPDATE_BUDDY:0:T:alice$`)
- alice.send(`toc_chat_send 0 "hello room"`)
- alice.expect(`^CHAT_IN_ENC:0:alice:F:A:en:hello room$`)
- // invitations, accepting them, and leaving rooms
- alice.send(`toc_chat_invite 0 "join us" bob`)
- invite := bob.expect(`^CHAT_INVITE:Integration Room:(\d+):alice:join us$`)
- bob.send("toc_chat_accept " + invite[1])
- bob.expect(`^CHAT_JOIN:` + invite[1] + `:Integration Room$`)
- alice.expect(`^CHAT_UPDATE_BUDDY:0:T:bob$`)
- bob.send("toc_chat_leave " + invite[1])
- bob.expect(`^CHAT_LEFT:` + invite[1] + `$`)
- alice.expect(`^CHAT_UPDATE_BUDDY:0:F:bob$`)
- // a large buddy list addition is stored in full
- var newBuddies strings.Builder
- for i := range 60 {
- fmt.Fprintf(&newBuddies, "g:Group %02d\nb:buddy%02d\n", i, i)
- }
- alice.send("toc2_new_buddies {" + newBuddies.String() + "}")
- for i := range 60 {
- alice.expect(fmt.Sprintf(`^NEW_BUDDY_REPLY2:buddy%02d:added$`, i))
- }
- // alice's commands are handled in order, so her buddy list is stored once
- // this reply arrives
- alice.send("toc_get_status alice")
- alice.expect(`^UPDATE_BUDDY2:alice:T:`)
- alice2 := dialTOC(t, tocAddr, "alice", `toc2_login localhost 9898 alice `+password+` english "TIC:Test" 160 US "" "" 3 0 30303 -kentucky -utf8 103547776`)
- config := alice2.expect(`^CONFIG2:(?s:.*)$`)[0]
- for i := range 60 {
- assert.Contains(t, config, fmt.Sprintf("g:Group %02d\nb:buddy%02d\n", i, i))
- }
- assert.NotContains(t, config, "b:\n")
- assert.NoError(t, alice2.conn.Close())
- // a TOC1 user's deny list hides them from the denied user
- carol := dialTOC(t, tocAddr, "carol", `toc_signon localhost 9898 carol `+password+` english "TIC:Test"`)
- carol.expect(`^NICK:carol$`)
- carol.send("toc_add_deny dave")
- carol.send("toc_init_done")
- // carol's commands are handled in order, so she's signed on once this
- // reply arrives
- carol.send("toc_get_status carol")
- carol.expect(`^UPDATE_BUDDY:carol:T:`)
- dave := dialTOC(t, tocAddr, "dave", `toc_signon localhost 9898 dave `+password+` english "TIC:Test"`)
- dave.expect(`^NICK:dave$`)
- dave.send("toc_init_done")
- dave.send("toc_get_status carol")
- dave.expect(`^ERROR:901:carol$`)
- bob.send("toc_get_status carol")
- bob.expect(`^UPDATE_BUDDY:carol:T:`)
- assert.NoError(t, carol.conn.Close())
- assert.NoError(t, dave.conn.Close())
- // signing off is seen by buddies
- assert.NoError(t, bob.conn.Close())
- alice.expect(`^UPDATE_BUDDY2:bob:F:0:0:0: :$`)
- assert.NoError(t, alice.conn.Close())
- }
|