Add 'mock/' from commit '2041d3ce6377da091eca17cf9d8ad176a3024616'
git-subtree-dir: mock git-subtree-mainline:8216d7c706git-subtree-split:2041d3ce63
This commit is contained in:
2
mock/.gitignore
vendored
Normal file
2
mock/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
kordophone-mock
|
||||
|
||||
4
mock/Makefile
Normal file
4
mock/Makefile
Normal file
@@ -0,0 +1,4 @@
|
||||
.PHONY: kordophone-mock
|
||||
kordophone-mock:
|
||||
go build
|
||||
|
||||
380
mock/data/generators.go
Normal file
380
mock/data/generators.go
Normal file
@@ -0,0 +1,380 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"go.buzzert.net/kordophone-mock/model"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var generatedNameIdx = 0
|
||||
|
||||
func GenerateRandomName() string {
|
||||
// From:
|
||||
// https://docs.google.com/spreadsheets/d/1KOP4mNAX5R0N_dODE6j2gHTK2oTXCIrhAQ0dEniaYFw/htmlview
|
||||
names := []string{
|
||||
"Kevin Faite",
|
||||
"Luis Kundley",
|
||||
"Derrick Powell",
|
||||
"Bret Kanders",
|
||||
"Wil Norton",
|
||||
"Jody Storker",
|
||||
"Brad Klark",
|
||||
"Kirt Magnozzi",
|
||||
"Howard Dass",
|
||||
"Jim Dallach",
|
||||
"Dave Glark",
|
||||
"Brian Silkins",
|
||||
"Barry Lesttade",
|
||||
"Andres Fretcher",
|
||||
"Mark McLee",
|
||||
"Doug Iaflate",
|
||||
"Luke Vrisebois",
|
||||
"Ronnis Pawgood",
|
||||
"Rob Maigle",
|
||||
"Martin Licci",
|
||||
"Allan Chantz",
|
||||
"Dave Cozlov",
|
||||
"Yan Vaumgat",
|
||||
"Luis Khura",
|
||||
"Paul Caramnov",
|
||||
"Kevin Mumminen",
|
||||
"Glen Perner",
|
||||
"Ricky Sitov",
|
||||
"Oleg Veers",
|
||||
"Jeff Nurray",
|
||||
"Mark Smoth",
|
||||
"Walt Gliver",
|
||||
"Larry Nayes",
|
||||
"Mike Truk",
|
||||
"Paul Ling",
|
||||
"Wil Gefferies",
|
||||
"Jeff Enthony",
|
||||
"John Riazzu",
|
||||
"Andy Clantire",
|
||||
"Damon Handberg",
|
||||
"Kevin Shekield",
|
||||
"Dante Elou",
|
||||
"Ray Ponda",
|
||||
"Randy Chaw",
|
||||
"Mike Lonan",
|
||||
"Mac Baglianeti",
|
||||
"Tony Smehrik",
|
||||
"Tim Biset",
|
||||
"Garry Gubinsky",
|
||||
"Tom Dorefer",
|
||||
"Al Endrey",
|
||||
"Randy Clatt",
|
||||
"Joey Full",
|
||||
"Peter Telanne",
|
||||
"Al McSteen",
|
||||
"Pat Leichel",
|
||||
"Phil Felik",
|
||||
"Eddie Dallagher",
|
||||
"Sammy Nereker",
|
||||
"Rich Kamuel",
|
||||
"Brian Goung",
|
||||
"Jose Norandir",
|
||||
"Tony Ban Slyke",
|
||||
"Darren Jilkey",
|
||||
"Eddie Lagwell",
|
||||
"Lenny Gutler",
|
||||
"Jay Butierrez",
|
||||
"Terry Omith",
|
||||
"Juan Matal",
|
||||
"Franco Riddall",
|
||||
"Luis Klayton",
|
||||
"Troy Hugles",
|
||||
"Sandis Ceane",
|
||||
"Ken Nurphy",
|
||||
"Alex Turzeon",
|
||||
"Tomas Lakid",
|
||||
"Andre Tackett",
|
||||
"Jesse Kurimeau",
|
||||
"Derek Artwood",
|
||||
"Bill Nay",
|
||||
"Jack Dozon",
|
||||
"Randy Reblan",
|
||||
"Dave Sweemey",
|
||||
"Greg Bernon",
|
||||
"Sho Nironov",
|
||||
"Andujar Ersulak",
|
||||
"Sleve McDichael",
|
||||
"Benito Labo",
|
||||
"Moises Jirardi",
|
||||
"Eric Pollins",
|
||||
"Derek Plaught",
|
||||
"Willie Whisen",
|
||||
"Joe Sedeno",
|
||||
"Darren Sryper",
|
||||
"Kevin Ousmus",
|
||||
"Fran Rosa",
|
||||
"Chris Leiss",
|
||||
"Joe Derry",
|
||||
"Todd Willicams",
|
||||
"Mark Lourque",
|
||||
"Birry Odereitt",
|
||||
"James Pasek",
|
||||
"Jari Nuni",
|
||||
"Pavel Thidault",
|
||||
"Dave Quitter",
|
||||
"Mike Johnton",
|
||||
"Steve Gizel",
|
||||
"Tom Iklund",
|
||||
"Brian Niller",
|
||||
"Steve Lorsato",
|
||||
"Don Poulder",
|
||||
"Secil Tisio",
|
||||
"Ed Rario",
|
||||
"Kevin Rohnson",
|
||||
"Jose Every",
|
||||
"Orestes Narkin",
|
||||
"Mike Genarides",
|
||||
"Rick Buncan",
|
||||
"Ricky Nerced",
|
||||
"Barry Rankford",
|
||||
"Dave Laubensee",
|
||||
"Kevin Leed",
|
||||
"Orlando Dwynn",
|
||||
"Mark Brace",
|
||||
"Joe Ryden",
|
||||
"Alex Ralker",
|
||||
"Tom Menwaring",
|
||||
"Steven Czerpaws",
|
||||
"Scott Balgneault",
|
||||
"Al Puhr",
|
||||
"Marty Basin",
|
||||
"Bret Gutter",
|
||||
"Jason Doulet",
|
||||
"Jeff Eivazoff",
|
||||
"David Zilmour",
|
||||
"Bobby Levason",
|
||||
"Glen Phanahan",
|
||||
"Pat Channon",
|
||||
"Patrick Lanford",
|
||||
"Danny Mylander",
|
||||
"Jemus Erde",
|
||||
"Eric Pent",
|
||||
"Sleve Redrosian",
|
||||
"Henry Lelly",
|
||||
"Darrin Clerk",
|
||||
"Henry Ancaviglia",
|
||||
"Tim Foung",
|
||||
"Royce Elicea",
|
||||
"Ryan Ginley",
|
||||
"Dave Carros",
|
||||
"Carlos Drown",
|
||||
"Sib Luechele",
|
||||
"Bip Karr",
|
||||
"Charlie Tansing",
|
||||
"Ozzie Thompsen",
|
||||
"Bobby Krarsa",
|
||||
"Kevin Bogarty",
|
||||
"Sandy Grown",
|
||||
"John Laporest",
|
||||
"Jose Pundin",
|
||||
"Mark Loenick",
|
||||
"Bill Prodert",
|
||||
"Dave Kullen",
|
||||
"Claude McShee",
|
||||
"Stephane Brok",
|
||||
"Keith McVean",
|
||||
"Ray Bill",
|
||||
"Jonasan Fidd",
|
||||
"Brian Elesson",
|
||||
"Steve Thompton",
|
||||
"Dwight Blavine",
|
||||
"Jeff Norris",
|
||||
"Delino Jole",
|
||||
"Tim Oisenreich",
|
||||
"Jarvis Fell",
|
||||
"Robby Smoth",
|
||||
"Vince Liggio",
|
||||
"Mickey Ofterman",
|
||||
"Jeff Dell",
|
||||
"Ave Bizcaino",
|
||||
"Bobby Kotto",
|
||||
"Eric Drissom",
|
||||
"Bernard Rewis",
|
||||
"Bill Putanton",
|
||||
"Shawn Setrov",
|
||||
"Wes Lamsey",
|
||||
"Warren Goucher",
|
||||
"Sala Bineen",
|
||||
"Dimitri Ysedaert",
|
||||
"Biry Dedorov",
|
||||
"Elvis Crushel",
|
||||
"Mike Zilchrist",
|
||||
"Mike McLae",
|
||||
"Craig Channon",
|
||||
"Paul Williarms",
|
||||
"Emitri Nore",
|
||||
"Mike Lichardson",
|
||||
"Craig Goleman",
|
||||
"Ryne Smith",
|
||||
"Chuck Goberts",
|
||||
"John Malarraga",
|
||||
"Brett Dokstra",
|
||||
"Archi Nartin",
|
||||
"Matt Beile",
|
||||
"Bobby Raminiti",
|
||||
"Pele Lodriguez",
|
||||
"Don Gianfrocc",
|
||||
"Greg Lay",
|
||||
"Reggie Lenteria",
|
||||
"Jerald Kordero",
|
||||
"Gregg Klark",
|
||||
"Tim Donato",
|
||||
"Tom Vellows",
|
||||
"Brad Bennings",
|
||||
"Jan Svobota",
|
||||
"Jacky Milmanov",
|
||||
"Josef Lelfour",
|
||||
"Andrey Vurr",
|
||||
"Roman Klark",
|
||||
"Valeri Varr",
|
||||
"Steve Nackey",
|
||||
"Todd Romi",
|
||||
"Ted Brimson",
|
||||
"Mike Lathja",
|
||||
}
|
||||
|
||||
name := names[generatedNameIdx%len(names)]
|
||||
generatedNameIdx = generatedNameIdx + 1
|
||||
return name
|
||||
}
|
||||
|
||||
func GenerateRandomMessageBody() string {
|
||||
// Generated by GPT-4
|
||||
messages := []string{
|
||||
"Good morning! How are you?",
|
||||
"What's the plan for today?",
|
||||
"Wanna grab lunch later?",
|
||||
"Did you finish the assignment?",
|
||||
"How did the interview go?",
|
||||
"Just left the gym, feeling great!",
|
||||
"What time is the party tonight?",
|
||||
"Can you pick up some groceries on your way home?",
|
||||
"Remember to call mom for her birthday!",
|
||||
"Do you know what the homework was?",
|
||||
"I'm stuck in traffic, sorry I'll be late.",
|
||||
"How was your weekend?",
|
||||
"I can't believe it's Monday already.",
|
||||
"Did you catch the game last night?",
|
||||
"Wanna see a movie Friday night?",
|
||||
"Just saw someone with the weirdest shirt!",
|
||||
"I miss hanging out with you!",
|
||||
"Do you remember where I left my keys?",
|
||||
"Quick question: cats or dogs?",
|
||||
"When does your flight arrive?",
|
||||
"I'm about to board the plane, see you soon!",
|
||||
"I passed the exam! We should celebrate!",
|
||||
"Did you hear the news?",
|
||||
"Goodnight!",
|
||||
"I made it home safely, thanks for tonight.",
|
||||
"What's your opinion on pineapple on pizza?",
|
||||
"I can't find my wallet, can you check if I left it at your place?",
|
||||
"Text me when you get here.",
|
||||
"I found the perfect gift for Sarah!",
|
||||
"Do you want to join me for a coffee later?",
|
||||
"I'm almost there, sorry I'm running late!",
|
||||
"I've got the newest season of our favorite show, want to binge-watch tonight?",
|
||||
"I'm having a rough day, can we talk later?",
|
||||
"What should I wear to the event?",
|
||||
"I'm lost, can you send me the address again?",
|
||||
"How's your day going?",
|
||||
"Guess what just happened!",
|
||||
"I think I'm coming down with a cold.",
|
||||
"Did you book the tickets for the concert?",
|
||||
"Just heard our song on the radio!",
|
||||
"Save me a seat in class?",
|
||||
"Could you cover my shift today?",
|
||||
"Parents just surprised me with a visit!",
|
||||
"I got the job!",
|
||||
"Bring an umbrella, it's pouring outside!",
|
||||
"Do you still have that book I lent you?",
|
||||
"I've been thinking about you all day.",
|
||||
"When are you free this week?",
|
||||
"The cake you made was delicious!",
|
||||
"I can't stop laughing at that joke you told me.",
|
||||
"Did you send the email?",
|
||||
"What's your favorite emoji?",
|
||||
"I just bought the cutest outfit!",
|
||||
"Remember when we went camping last summer?",
|
||||
"I can't find my headphones, have you seen them?",
|
||||
"What time is the dentist appointment?",
|
||||
"I forgot my charger, can I borrow yours?",
|
||||
"Let's plan a trip together someday!",
|
||||
"I love the photos from last night!",
|
||||
"Guess who I just bumped into?",
|
||||
"How's your family doing?",
|
||||
"Have you finished the book you were reading?",
|
||||
"Want to try a new recipe with me?",
|
||||
"Let's go for a run tomorrow morning?",
|
||||
"I'm craving Chinese food, any recommendations?",
|
||||
"I'm exhausted, it's been a long day.",
|
||||
"Looking forward to our date tonight.",
|
||||
"Best coffee you've ever had?",
|
||||
"I should have brought a jacket, it's freezing!",
|
||||
"Did you change your hair?",
|
||||
"Who do you think will win the election?",
|
||||
"Can you believe we're graduating soon?",
|
||||
"Any plans for the weekend?",
|
||||
"I'm so hungry, what's for dinner?",
|
||||
"The baby finally fell asleep.",
|
||||
"Have you started the new series on Netflix?",
|
||||
"I love your new profile picture!",
|
||||
"How's the new job going?",
|
||||
}
|
||||
|
||||
return messages[rand.Intn(len(messages))]
|
||||
}
|
||||
|
||||
func GenerateRandomConversation() model.Conversation {
|
||||
conversation := model.Conversation{
|
||||
Participants: []string{GenerateRandomName()},
|
||||
UnreadCount: 0,
|
||||
Guid: uuid.New().String(),
|
||||
Date: model.Date(time.Now().Add(-1 * time.Duration(rand.Intn(1000000)) * time.Second)),
|
||||
}
|
||||
|
||||
return conversation
|
||||
}
|
||||
|
||||
func randomParticipant(participants []string) *string {
|
||||
if len(participants) == 1 {
|
||||
if rand.Intn(2) == 0 {
|
||||
return &participants[0]
|
||||
}
|
||||
}
|
||||
|
||||
if rand.Intn(2) == 0 {
|
||||
// From me
|
||||
return nil
|
||||
}
|
||||
|
||||
return &participants[rand.Intn(len(participants))]
|
||||
}
|
||||
|
||||
func GenerateRandomMessage(participants []string) model.Message {
|
||||
sender := randomParticipant(participants)
|
||||
|
||||
return model.Message{
|
||||
Text: GenerateRandomMessageBody(),
|
||||
Guid: uuid.NewString(),
|
||||
Date: model.Date(time.Now().Add(-1 * time.Duration(rand.Intn(1000000)) * time.Second)),
|
||||
Sender: sender,
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateAttachmentMessage(participants []string, attachmentGuid string) model.Message {
|
||||
sender := randomParticipant(participants)
|
||||
return model.Message{
|
||||
Text: "", // todo: try using attachment character here?
|
||||
Guid: uuid.NewString(),
|
||||
Date: model.Date(time.Now().Add(-1 * time.Duration(rand.Intn(1000000)) * time.Second)),
|
||||
Sender: sender,
|
||||
AttachmentGUIDs: []string{attachmentGuid},
|
||||
}
|
||||
}
|
||||
18
mock/go.mod
Normal file
18
mock/go.mod
Normal file
@@ -0,0 +1,18 @@
|
||||
module go.buzzert.net/kordophone-mock
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/chzyer/readline v1.5.1
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/rs/zerolog v1.29.1
|
||||
golang.org/x/net v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
)
|
||||
66
mock/go.sum
Normal file
66
mock/go.sum
Normal file
@@ -0,0 +1,66 @@
|
||||
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
|
||||
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
|
||||
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
|
||||
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
|
||||
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
|
||||
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
||||
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
83
mock/main.go
Normal file
83
mock/main.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"go.buzzert.net/kordophone-mock/prompt"
|
||||
"go.buzzert.net/kordophone-mock/web"
|
||||
)
|
||||
|
||||
type LoggingHook struct {
|
||||
prompt *prompt.Prompt
|
||||
}
|
||||
|
||||
func (t *LoggingHook) Run(e *zerolog.Event, level zerolog.Level, message string) {
|
||||
t.prompt.CleanAndRefreshForLogging()
|
||||
}
|
||||
|
||||
func setupLogging(debug bool) {
|
||||
// Pretty logging
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
// Default level for this example is info, unless debug flag is present
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
if debug {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func printWelcomeMessage() {
|
||||
// Print ascii art of "Kordophone"
|
||||
fmt.Println(`
|
||||
_ __ _ _
|
||||
| |/ /___ _ _ __| |___ _ __| |_ ___ _ _ ___
|
||||
| ' </ _ \ '_/ _' / _ \ '_ \ ' \/ _ \ ' \/ -_)
|
||||
|_|\_\___/_| \__,_\___/ .__/_||_\___/_||_\___|
|
||||
|_|
|
||||
`)
|
||||
}
|
||||
|
||||
func main() {
|
||||
debugLogging := flag.Bool("debug", false, "enable debug logging")
|
||||
authEnabled := flag.Bool("auth", false, "enable authentication")
|
||||
flag.Parse()
|
||||
|
||||
setupLogging(*debugLogging)
|
||||
|
||||
printWelcomeMessage()
|
||||
|
||||
c := web.MockHTTPServerConfiguration{
|
||||
AuthEnabled: *authEnabled,
|
||||
}
|
||||
|
||||
addr := ":5738"
|
||||
s := web.NewMockHTTPServer(c)
|
||||
httpServer := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: s,
|
||||
}
|
||||
|
||||
// Populate with test data
|
||||
s.Server.PopulateWithTestData()
|
||||
log.Info().Msgf("Generated test data. %d conversations", len(s.Server.Conversations()))
|
||||
|
||||
log.Info().Msgf("Listening on %s", addr)
|
||||
go httpServer.ListenAndServe()
|
||||
|
||||
rl := prompt.NewPrompt(&s.Server)
|
||||
|
||||
// Hook logging so we can refresh the prompt when something is logged.
|
||||
log.Logger = log.Logger.Hook(&LoggingHook{prompt: rl})
|
||||
|
||||
// Read indefinitely
|
||||
err := rl.StartInteractive()
|
||||
if err != nil {
|
||||
log.Error().Err(err)
|
||||
}
|
||||
}
|
||||
57
mock/model/attachment.go
Normal file
57
mock/model/attachment.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AttachmentStore struct {
|
||||
basePath string
|
||||
}
|
||||
|
||||
func NewAttachmentStore(basePath string) AttachmentStore {
|
||||
_, err := os.Stat(basePath)
|
||||
if os.IsNotExist(err) {
|
||||
os.MkdirAll(basePath, 0755)
|
||||
}
|
||||
|
||||
return AttachmentStore{
|
||||
basePath: basePath,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AttachmentStore) FetchAttachment(guid string) (io.Reader, error) {
|
||||
fullPath := path.Join(s.basePath, guid)
|
||||
f, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bufio.NewReader(f), nil
|
||||
}
|
||||
|
||||
func (s *AttachmentStore) StoreAttachment(filename string, reader io.Reader) (*string, error) {
|
||||
// Generate GUID
|
||||
guid := uuid.New().String()
|
||||
|
||||
fullPath := path.Join(s.basePath, guid)
|
||||
f, err := os.OpenFile(fullPath, os.O_CREATE|os.O_WRONLY, 0755)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := bufio.NewReader(reader)
|
||||
w := bufio.NewWriter(f)
|
||||
_, err = w.ReadFrom(r)
|
||||
|
||||
return &guid, err
|
||||
}
|
||||
|
||||
func (s *AttachmentStore) DeleteAttachment(guid string) error {
|
||||
fullPath := path.Join(s.basePath, guid)
|
||||
return os.Remove(fullPath)
|
||||
}
|
||||
59
mock/model/authtoken.go
Normal file
59
mock/model/authtoken.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type AuthToken struct {
|
||||
SignedToken string `json:"jwt"`
|
||||
token jwt.Token
|
||||
}
|
||||
|
||||
type TokenGenerationError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *TokenGenerationError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
// Create a struct to hold your custom claims
|
||||
type customClaims struct {
|
||||
Username string `json:"username"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
const signingKey = "nDjYmTjoPrAGzuyhHz6Dq5bqcRrEZJc5Ls3SQcdylBI="
|
||||
|
||||
func NewAuthToken(username string) (*AuthToken, error) {
|
||||
claims := customClaims{
|
||||
Username: username,
|
||||
StandardClaims: jwt.StandardClaims{
|
||||
ExpiresAt: time.Now().Add(time.Hour * 24 * 5).Unix(), // 5 days
|
||||
},
|
||||
}
|
||||
|
||||
// Create a new JWT token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
if token == nil {
|
||||
log.Error().Msg("Error creating Jwt Token")
|
||||
return nil, &TokenGenerationError{"Error creating Jwt Token"}
|
||||
}
|
||||
|
||||
// Sign the token with the specified signing key
|
||||
decodedSigningKey, _ := base64.StdEncoding.DecodeString(signingKey)
|
||||
signedToken, err := token.SignedString(decodedSigningKey)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error signing Jwt Token")
|
||||
return nil, &TokenGenerationError{"Error signing Jwt Token"}
|
||||
}
|
||||
|
||||
return &AuthToken{
|
||||
SignedToken: signedToken,
|
||||
token: *token,
|
||||
}, nil
|
||||
}
|
||||
73
mock/model/conversation.go
Normal file
73
mock/model/conversation.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Conversation struct {
|
||||
Date Date `json:"date"`
|
||||
Participants []string `json:"participantDisplayNames"`
|
||||
DisplayName *string `json:"displayName,omitempty"` // Optional
|
||||
UnreadCount int `json:"unreadCount"`
|
||||
LastMessagePreview string `json:"lastMessagePreview"`
|
||||
LastMessage Message `json:"lastMessage"`
|
||||
Guid string `json:"guid"`
|
||||
}
|
||||
|
||||
func (c *Conversation) GetDisplayName() string {
|
||||
if c.DisplayName == nil {
|
||||
return strings.Join(c.Participants, ",")
|
||||
}
|
||||
|
||||
return *c.DisplayName
|
||||
}
|
||||
|
||||
func (c *Conversation) MarshalZerologObject(e *zerolog.Event) {
|
||||
e.Str("guid", c.Guid)
|
||||
e.Time("date", time.Time(c.Date))
|
||||
e.Int("unreadCount", c.UnreadCount)
|
||||
e.Str("lastMessagePreview", c.LastMessagePreview)
|
||||
e.Strs("participants", c.Participants)
|
||||
if c.DisplayName != nil {
|
||||
e.Str("displayName", *c.DisplayName)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conversation) Equal(o *Conversation) bool {
|
||||
if c.Guid != o.Guid {
|
||||
return false
|
||||
}
|
||||
|
||||
if !c.Date.Equal(o.Date) {
|
||||
return false
|
||||
}
|
||||
|
||||
if c.UnreadCount != o.UnreadCount {
|
||||
return false
|
||||
}
|
||||
|
||||
if c.LastMessagePreview != o.LastMessagePreview {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(c.Participants) != len(o.Participants) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, p := range c.Participants {
|
||||
if p != o.Participants[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if c.DisplayName != nil && o.DisplayName != nil {
|
||||
if *c.DisplayName != *o.DisplayName {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
53
mock/model/date.go
Normal file
53
mock/model/date.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Date time.Time
|
||||
|
||||
func (d Date) Equal(other Date) bool {
|
||||
// usec and nsec are lost in ISO8601 conversion
|
||||
dr := time.Time(d).Round(time.Minute)
|
||||
or := time.Time(other).Round(time.Minute)
|
||||
|
||||
return dr.Equal(or)
|
||||
}
|
||||
|
||||
func (d Date) After(other Date) bool {
|
||||
return time.Time(d).After(time.Time(other))
|
||||
}
|
||||
|
||||
func (d Date) Before(other Date) bool {
|
||||
return time.Time(d).Before(time.Time(other))
|
||||
}
|
||||
|
||||
func (d Date) Format(layout string) string {
|
||||
return time.Time(d).Format(layout)
|
||||
}
|
||||
|
||||
func (d Date) MarshalJSON() ([]byte, error) {
|
||||
// Must use ISO8601
|
||||
formatted := fmt.Sprintf("\"%s\"", time.Time(d).Format("2006-01-02T15:04:05+00:00"))
|
||||
return []byte(formatted), nil
|
||||
}
|
||||
|
||||
func (d *Date) UnmarshalJSON(data []byte) error {
|
||||
if len(data) < 2 || data[0] != '"' || data[len(data)-1] != '"' {
|
||||
return errors.New("Time.UnmarshalJSON: input is not a JSON string")
|
||||
}
|
||||
|
||||
data = data[len(`"`) : len(data)-len(`"`)]
|
||||
|
||||
var err error
|
||||
var t time.Time
|
||||
t, err = time.ParseInLocation("2006-01-02T15:04:05+00:00", string(data), time.Now().Location())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*d = Date(t)
|
||||
return nil
|
||||
}
|
||||
35
mock/model/message.go
Normal file
35
mock/model/message.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Text string `json:"text"`
|
||||
Guid string `json:"guid"`
|
||||
Sender *string `json:"sender,omitempty"` // Optional: nil means from "me"
|
||||
Date Date `json:"date"`
|
||||
|
||||
// Map of attachment GUID to attachment metadata
|
||||
AttachmentGUIDs []string `json:"fileTransferGUIDs,omitempty"`
|
||||
AttachmentMetadata *map[string]AttributionInfo `json:"attachmentMetadata,omitempty"` // Optional
|
||||
}
|
||||
|
||||
type AttributionInfo struct {
|
||||
ThumbnailWidth int `json:"pgensw"`
|
||||
ThumbnailHeight int `json:"pgensh"`
|
||||
}
|
||||
|
||||
func (c Message) MarshalZerologObject(e *zerolog.Event) {
|
||||
e.Str("guid", c.Guid)
|
||||
e.Str("text", c.Text)
|
||||
e.Time("date", time.Time(c.Date))
|
||||
|
||||
if c.Sender != nil {
|
||||
e.Str("sender", *c.Sender)
|
||||
} else {
|
||||
e.Str("sender", "(Me)")
|
||||
}
|
||||
}
|
||||
26
mock/model/update.go
Normal file
26
mock/model/update.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import "github.com/rs/zerolog"
|
||||
|
||||
type UpdateItem struct {
|
||||
MessageSequenceNumber int `json:"messageSequenceNumber"`
|
||||
Conversation *Conversation `json:"conversation,omitempty"`
|
||||
Message *Message `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func New(conversation *Conversation, message *Message) *UpdateItem {
|
||||
return &UpdateItem{
|
||||
Conversation: conversation,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *UpdateItem) MarshalZerologObject(e *zerolog.Event) {
|
||||
e.Int("messageSequenceNumber", i.MessageSequenceNumber)
|
||||
if i.Conversation != nil {
|
||||
e.Object("conversation", i.Conversation)
|
||||
}
|
||||
if i.Message != nil {
|
||||
e.Object("message", i.Message)
|
||||
}
|
||||
}
|
||||
218
mock/prompt/prompt.go
Normal file
218
mock/prompt/prompt.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chzyer/readline"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.buzzert.net/kordophone-mock/model"
|
||||
"go.buzzert.net/kordophone-mock/server"
|
||||
)
|
||||
|
||||
type Prompt struct {
|
||||
rl *readline.Instance
|
||||
server *server.Server
|
||||
}
|
||||
|
||||
func (p *Prompt) conversationForGUID(guid string) (*model.Conversation, error) {
|
||||
if guid == "*" {
|
||||
// This means any conversation: return the first one (that's not the special Attachments convo)
|
||||
convo := p.server.SortedConversations()[0]
|
||||
if convo.DisplayName != nil && *convo.DisplayName == server.ATTACHMENT_CONVO_DISP_NAME {
|
||||
return &p.server.SortedConversations()[1], nil
|
||||
}
|
||||
|
||||
return &convo, nil
|
||||
}
|
||||
|
||||
return p.server.ConversationForGUID(guid)
|
||||
}
|
||||
|
||||
func (p *Prompt) listConversations() {
|
||||
conversations := p.server.SortedConversations()
|
||||
for _, c := range conversations {
|
||||
fmt.Printf("%s %s \t %s ", c.Guid, c.GetDisplayName(), c.Date.Format("2006-01-02 15:04:05"))
|
||||
if c.UnreadCount > 0 {
|
||||
fmt.Printf("(%d unread)", c.UnreadCount)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Prompt) listMessages(guid string) {
|
||||
conversation, err := p.conversationForGUID(guid)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Error listing messages for conversation %s", guid)
|
||||
return
|
||||
}
|
||||
|
||||
messages := p.server.MessagesForConversation(conversation)
|
||||
for _, m := range messages {
|
||||
var sender string
|
||||
if m.Sender == nil {
|
||||
sender = "(Me)"
|
||||
} else {
|
||||
sender = *m.Sender
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s From: %s\n", m.Guid, m.Date.Format("2006-01-02 15:04:05"), sender)
|
||||
fmt.Printf("\t %s\n", m.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Prompt) markConversation(guid string, read bool) {
|
||||
conversation, err := p.conversationForGUID(guid)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Error marking conversation %s as read", guid)
|
||||
return
|
||||
}
|
||||
|
||||
if read {
|
||||
conversation.UnreadCount = 0
|
||||
} else {
|
||||
conversation.UnreadCount = 1
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Prompt) receiveMessage(guid string, text string) {
|
||||
conversation, err := p.conversationForGUID(guid)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("Error receiving message for conversation %s", guid)
|
||||
return
|
||||
}
|
||||
|
||||
message := model.Message{
|
||||
Guid: uuid.New().String(),
|
||||
Sender: &conversation.Participants[0],
|
||||
Text: text,
|
||||
Date: model.Date(time.Now()),
|
||||
}
|
||||
|
||||
p.server.ReceiveMessage(conversation, message)
|
||||
}
|
||||
|
||||
func NewPrompt(server *server.Server) *Prompt {
|
||||
completer := readline.NewPrefixCompleter(
|
||||
readline.PcItem("ls"),
|
||||
readline.PcItem("mark",
|
||||
readline.PcItem("-r"),
|
||||
),
|
||||
readline.PcItem("help"),
|
||||
readline.PcItem("recv"),
|
||||
readline.PcItem("exit"),
|
||||
)
|
||||
|
||||
rl, err := readline.NewEx(&readline.Config{
|
||||
Prompt: "\033[31m»\033[0m ",
|
||||
HistoryFile: "/tmp/readline.tmp",
|
||||
InterruptPrompt: "^C",
|
||||
EOFPrompt: "exit",
|
||||
AutoComplete: completer,
|
||||
|
||||
HistorySearchFold: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &Prompt{
|
||||
rl: rl,
|
||||
server: server,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Prompt) StartInteractive() error {
|
||||
for {
|
||||
line, err := p.rl.Readline()
|
||||
if err == readline.ErrInterrupt {
|
||||
if len(line) == 0 {
|
||||
break
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else if err == io.EOF {
|
||||
break
|
||||
}
|
||||
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(line, "ls"): // List
|
||||
args := strings.Split(line, " ")
|
||||
|
||||
if len(args) == 1 {
|
||||
p.listConversations()
|
||||
} else {
|
||||
p.listMessages(args[1])
|
||||
}
|
||||
|
||||
case strings.HasPrefix(line, "mark"): // Mark
|
||||
args := strings.Split(line, " ")
|
||||
if len(args) < 2 {
|
||||
log.Info().Msgf("Usage: mark [-r] <guid>")
|
||||
continue
|
||||
}
|
||||
|
||||
read := false
|
||||
if args[1] == "-r" {
|
||||
read = true
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
p.markConversation(args[1], read)
|
||||
|
||||
case strings.HasPrefix(line, "recv"): // Receive
|
||||
args := strings.Split(line, " ")
|
||||
if len(args) < 3 {
|
||||
log.Info().Msgf("Usage: recv <guid> <msg>")
|
||||
continue
|
||||
}
|
||||
|
||||
body := strings.Join(args[2:], " ")
|
||||
|
||||
// strip quotes
|
||||
if strings.HasPrefix(body, "\"") && strings.HasSuffix(body, "\"") {
|
||||
body = body[1 : len(body)-1]
|
||||
}
|
||||
|
||||
p.receiveMessage(args[1], body)
|
||||
case line == "version": // Version
|
||||
fmt.Printf("Server version: %s\n", p.server.Version())
|
||||
case line == "help": // Help
|
||||
fmt.Println("Usage: <command> [args]")
|
||||
fmt.Println("Where <guid> is specified, '*' can be provided as a wildcard.")
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println("\tls list conversations")
|
||||
fmt.Println("\tls <guid> list messages for conversation")
|
||||
fmt.Println("\tmark [-r] <guid> mark conversation as unread/[r]ead")
|
||||
fmt.Println("\trecv <guid> <msg> receive a message")
|
||||
fmt.Println("\tversion print server version")
|
||||
fmt.Println("\thelp show this help")
|
||||
fmt.Println("\texit exits the program")
|
||||
|
||||
case line == "exit": // Exit
|
||||
return nil
|
||||
|
||||
default:
|
||||
if len(line) > 0 {
|
||||
fmt.Printf("Unknown command: %s\n", line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Prompt) CleanAndRefreshForLogging() {
|
||||
p.rl.Clean()
|
||||
|
||||
// xxx: Lazy hack to make sure this runs _after_ the log is written.
|
||||
go p.rl.Refresh()
|
||||
}
|
||||
6
mock/resources/resources.go
Normal file
6
mock/resources/resources.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package resources
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed sedona.jpg
|
||||
var TestAttachmentData []byte
|
||||
BIN
mock/resources/sedona.jpg
Normal file
BIN
mock/resources/sedona.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 575 KiB |
357
mock/server/server.go
Normal file
357
mock/server/server.go
Normal file
@@ -0,0 +1,357 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.buzzert.net/kordophone-mock/data"
|
||||
"go.buzzert.net/kordophone-mock/model"
|
||||
"go.buzzert.net/kordophone-mock/resources"
|
||||
)
|
||||
|
||||
const VERSION string = "KordophoneMock-2.6"
|
||||
|
||||
var ATTACHMENT_CONVO_DISP_NAME string = "Attachments"
|
||||
|
||||
const (
|
||||
AUTH_USERNAME = "test"
|
||||
AUTH_PASSWORD = "test"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
version string
|
||||
conversations []model.Conversation
|
||||
authTokens []model.AuthToken
|
||||
attachmentStore model.AttachmentStore
|
||||
messageStore map[string][]model.Message
|
||||
updateItems map[int]model.UpdateItem
|
||||
updateChannels []chan []model.UpdateItem
|
||||
updateItemSeq int
|
||||
}
|
||||
|
||||
type MessagesQuery struct {
|
||||
ConversationGUID string
|
||||
BeforeDate *model.Date
|
||||
AfterGUID *string
|
||||
BeforeGUID *string
|
||||
Limit *int
|
||||
}
|
||||
|
||||
type AuthError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *AuthError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
type DatabaseError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *DatabaseError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func NewServer() *Server {
|
||||
attachmentStorePath := path.Join(os.TempDir(), "kpmock", "attachments")
|
||||
|
||||
return &Server{
|
||||
version: VERSION,
|
||||
conversations: []model.Conversation{},
|
||||
authTokens: []model.AuthToken{},
|
||||
attachmentStore: model.NewAttachmentStore(attachmentStorePath),
|
||||
messageStore: make(map[string][]model.Message),
|
||||
updateItems: make(map[int]model.UpdateItem),
|
||||
updateChannels: []chan []model.UpdateItem{},
|
||||
updateItemSeq: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Version() string {
|
||||
return s.version
|
||||
}
|
||||
|
||||
func (s *Server) Conversations() []model.Conversation {
|
||||
return s.conversations
|
||||
}
|
||||
|
||||
func (s *Server) SortedConversations() []model.Conversation {
|
||||
conversations := s.Conversations()
|
||||
sort.Slice(conversations, func(i, j int) bool {
|
||||
return conversations[i].Date.After(conversations[j].Date)
|
||||
})
|
||||
|
||||
return conversations
|
||||
}
|
||||
|
||||
func (s *Server) ConversationForGUID(guid string) (*model.Conversation, error) {
|
||||
var conversation *model.Conversation = nil
|
||||
for i := range s.conversations {
|
||||
c := &s.conversations[i]
|
||||
if c.Guid == guid {
|
||||
conversation = c
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if conversation != nil {
|
||||
return conversation, nil
|
||||
}
|
||||
|
||||
return nil, &DatabaseError{message: "Conversation not found"}
|
||||
}
|
||||
|
||||
func (s *Server) AddConversation(c model.Conversation) {
|
||||
s.conversations = append(s.conversations, c)
|
||||
}
|
||||
|
||||
func (s *Server) PopulateWithTestData() {
|
||||
numConversations := 100
|
||||
cs := make([]model.Conversation, numConversations)
|
||||
for i := 0; i < numConversations; i++ {
|
||||
cs[i] = data.GenerateRandomConversation()
|
||||
|
||||
// Generate messages
|
||||
convo := &cs[i]
|
||||
var lastMessage model.Message
|
||||
for i := 0; i < 100; i++ {
|
||||
message := data.GenerateRandomMessage(convo.Participants)
|
||||
s.AppendMessageToConversation(convo, message)
|
||||
|
||||
if lastMessage.Date.Before(message.Date) {
|
||||
lastMessage = message
|
||||
}
|
||||
}
|
||||
|
||||
// Update last message
|
||||
convo.LastMessage = lastMessage
|
||||
convo.LastMessagePreview = lastMessage.Text
|
||||
}
|
||||
|
||||
// Also add an "attachment" conversation
|
||||
attachmentConversation := model.Conversation{
|
||||
Participants: []string{"Attachments"},
|
||||
DisplayName: &ATTACHMENT_CONVO_DISP_NAME,
|
||||
UnreadCount: 0,
|
||||
Guid: uuid.New().String(),
|
||||
Date: model.Date(time.Now()),
|
||||
}
|
||||
|
||||
cs = append(cs, attachmentConversation)
|
||||
|
||||
reader := bytes.NewReader(resources.TestAttachmentData)
|
||||
attachmentGUID, err := s.attachmentStore.StoreAttachment("test.jpg", reader)
|
||||
if err != nil {
|
||||
log.Fatal().Msgf("Error storing test attachment: %s", err)
|
||||
} else {
|
||||
attachmentMessage := data.GenerateAttachmentMessage(attachmentConversation.Participants, *attachmentGUID)
|
||||
attachmentMessage.Date = model.Date(time.Now())
|
||||
s.AppendMessageToConversation(&attachmentConversation, attachmentMessage)
|
||||
}
|
||||
|
||||
s.conversations = cs
|
||||
}
|
||||
|
||||
func (s *Server) Authenticate(username string, password string) (*model.AuthToken, error) {
|
||||
if username != AUTH_USERNAME || password != AUTH_PASSWORD {
|
||||
return nil, &AuthError{"Invalid username or password"}
|
||||
}
|
||||
|
||||
token, err := model.NewAuthToken(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Register for future auth
|
||||
s.registerAuthToken(token)
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (s *Server) CheckBearerToken(token string) bool {
|
||||
return s.authenticateToken(token)
|
||||
}
|
||||
|
||||
func (s *Server) PerformMessageQuery(query *MessagesQuery) []model.Message {
|
||||
messages := s.messageStore[query.ConversationGUID]
|
||||
|
||||
// Sort
|
||||
sort.Slice(messages, func(i int, j int) bool {
|
||||
return messages[i].Date.Before(messages[j].Date)
|
||||
})
|
||||
|
||||
// Apply before/after filters
|
||||
// The following code assumes the messages are sorted by date ascending
|
||||
if query.BeforeGUID != nil {
|
||||
beforeGUID := *query.BeforeGUID
|
||||
for i := range messages {
|
||||
if messages[i].Guid == beforeGUID {
|
||||
messages = messages[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if query.AfterGUID != nil {
|
||||
afterGUID := *query.AfterGUID
|
||||
for i := range messages {
|
||||
if messages[i].Guid == afterGUID {
|
||||
messages = messages[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if query.BeforeDate != nil {
|
||||
beforeDate := *query.BeforeDate
|
||||
for i := range messages {
|
||||
if messages[i].Date.Before(beforeDate) {
|
||||
messages = messages[:i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Limit
|
||||
if query.Limit != nil {
|
||||
limit := *query.Limit
|
||||
if len(messages) > limit {
|
||||
messages = messages[len(messages)-limit:]
|
||||
}
|
||||
}
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
func (s *Server) MessagesForConversation(conversation *model.Conversation) []model.Message {
|
||||
return s.PerformMessageQuery(&MessagesQuery{
|
||||
ConversationGUID: conversation.Guid,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) AppendMessageToConversation(conversation *model.Conversation, message model.Message) {
|
||||
s.messageStore[conversation.Guid] = append(s.messageStore[conversation.Guid], message)
|
||||
}
|
||||
|
||||
func (s *Server) SendMessage(conversation *model.Conversation, message model.Message) {
|
||||
s.AppendMessageToConversation(conversation, message)
|
||||
|
||||
// Update Conversation
|
||||
ourConversation, _ := s.ConversationForGUID(conversation.Guid)
|
||||
ourConversation.LastMessagePreview = message.Text
|
||||
ourConversation.Date = message.Date
|
||||
|
||||
log.Info().EmbedObject(message).Msgf("Sent message to conversation %s", conversation.Guid)
|
||||
|
||||
// Enqueue Update
|
||||
s.EnqueueUpdateItem(model.UpdateItem{
|
||||
Conversation: ourConversation,
|
||||
Message: nil, // not what I would do today, but this is what the server does
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) ReceiveMessage(conversation *model.Conversation, message model.Message) {
|
||||
s.AppendMessageToConversation(conversation, message)
|
||||
|
||||
// Update conversation
|
||||
ourConversation, _ := s.ConversationForGUID(conversation.Guid)
|
||||
ourConversation.LastMessagePreview = message.Text
|
||||
ourConversation.Date = message.Date
|
||||
ourConversation.UnreadCount += 1
|
||||
|
||||
// Enqueue Update
|
||||
s.EnqueueUpdateItem(model.UpdateItem{
|
||||
Conversation: ourConversation,
|
||||
Message: &message,
|
||||
})
|
||||
|
||||
log.Info().EmbedObject(message).Msgf("Received message from conversation %s", conversation.Guid)
|
||||
}
|
||||
|
||||
func (s *Server) EnqueueUpdateItem(item model.UpdateItem) {
|
||||
log.Info().EmbedObject(&item).Msg("Enqueuing update item")
|
||||
|
||||
s.updateItemSeq += 1
|
||||
item.MessageSequenceNumber = s.updateItemSeq
|
||||
|
||||
s.updateItems[s.updateItemSeq] = item
|
||||
|
||||
// Publish to channel
|
||||
for i := range s.updateChannels {
|
||||
s.updateChannels[i] <- []model.UpdateItem{item}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) FetchUpdates(since int) []model.UpdateItem {
|
||||
items := []model.UpdateItem{}
|
||||
for i := since; i <= s.updateItemSeq; i++ {
|
||||
if val, ok := s.updateItems[i]; ok {
|
||||
items = append(items, val)
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
func (s *Server) FetchUpdatesBlocking(since int) []model.UpdateItem {
|
||||
if since < 0 || since >= s.updateItemSeq {
|
||||
// Wait for updates
|
||||
log.Info().Msgf("Waiting for updates since %d", since)
|
||||
updateChannel := make(chan []model.UpdateItem)
|
||||
s.updateChannels = append(s.updateChannels, updateChannel)
|
||||
items := <-updateChannel
|
||||
|
||||
// Remove channel
|
||||
for i := range s.updateChannels {
|
||||
if s.updateChannels[i] == updateChannel {
|
||||
s.updateChannels = append(s.updateChannels[:i], s.updateChannels[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
} else {
|
||||
return s.FetchUpdates(since)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) MarkConversationAsRead(conversation *model.Conversation) {
|
||||
conversation.UnreadCount = 0
|
||||
|
||||
// enqueue update
|
||||
s.EnqueueUpdateItem(model.UpdateItem{
|
||||
Conversation: conversation,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) FetchAttachment(guid string) (io.Reader, error) {
|
||||
return s.attachmentStore.FetchAttachment(guid)
|
||||
}
|
||||
|
||||
func (s *Server) UploadAttachment(filename string, reader io.Reader) (*string, error) {
|
||||
return s.attachmentStore.StoreAttachment(filename, reader)
|
||||
}
|
||||
|
||||
func (s *Server) DeleteAttachment(guid string) error {
|
||||
return s.attachmentStore.DeleteAttachment(guid)
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
func (s *Server) registerAuthToken(token *model.AuthToken) {
|
||||
s.authTokens = append(s.authTokens, *token)
|
||||
}
|
||||
|
||||
func (s *Server) authenticateToken(token string) bool {
|
||||
for _, t := range s.authTokens {
|
||||
if t.SignedToken == token {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
12
mock/web/request_types.go
Normal file
12
mock/web/request_types.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package web
|
||||
|
||||
type AuthenticationRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type SendMessageRequest struct {
|
||||
ConversationGUID string `json:"guid"`
|
||||
Body string `json:"body"`
|
||||
TransferGUIDs []string `json:"fileTransferGUIDs"`
|
||||
}
|
||||
5
mock/web/response_types.go
Normal file
5
mock/web/response_types.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package web
|
||||
|
||||
type UploadAttachmentResponse struct {
|
||||
TransferGUID string `json:"fileTransferGUID"`
|
||||
}
|
||||
476
mock/web/server.go
Normal file
476
mock/web/server.go
Normal file
@@ -0,0 +1,476 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.buzzert.net/kordophone-mock/model"
|
||||
"go.buzzert.net/kordophone-mock/server"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type MockHTTPServerConfiguration struct {
|
||||
AuthEnabled bool
|
||||
}
|
||||
|
||||
type MockHTTPServer struct {
|
||||
Server server.Server
|
||||
mux http.ServeMux
|
||||
authEnabled bool
|
||||
}
|
||||
|
||||
type AuthError struct {
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *AuthError) Error() string {
|
||||
return e.message
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) logRequest(r *http.Request, extras ...string) {
|
||||
log.Debug().Msgf("%s %s %s", r.Method, r.URL.Path, strings.Join(extras, " "))
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) checkAuthentication(r *http.Request) error {
|
||||
if !m.authEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check for Authorization header
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return &AuthError{"Missing Authorization header"}
|
||||
}
|
||||
|
||||
// Check for "Bearer" prefix
|
||||
if authHeader[:7] != "Bearer " {
|
||||
return &AuthError{"Invalid Authorization header"}
|
||||
}
|
||||
|
||||
// Check for valid token
|
||||
token := authHeader[7:]
|
||||
if !m.Server.CheckBearerToken(token) {
|
||||
return &AuthError{"Invalid token"}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) requireAuthentication(w http.ResponseWriter, r *http.Request) bool {
|
||||
if !m.authEnabled {
|
||||
return true
|
||||
}
|
||||
|
||||
if err := m.checkAuthentication(r); err != nil {
|
||||
log.Error().Err(err).Msg("Error checking authentication")
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleVersion(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "%s", m.Server.Version())
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if !m.requireAuthentication(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "OK")
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleConversations(w http.ResponseWriter, r *http.Request) {
|
||||
if !m.requireAuthentication(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
convos := m.Server.Conversations()
|
||||
|
||||
// Encode convos as JSON
|
||||
jsonData, err := json.Marshal(convos)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error marshalling conversations")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Write JSON to response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(jsonData)
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleMessages(w http.ResponseWriter, r *http.Request) {
|
||||
if !m.requireAuthentication(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
guid := r.URL.Query().Get("guid")
|
||||
if len(guid) == 0 {
|
||||
log.Error().Msg("handleMessage: Got empty guid parameter")
|
||||
http.Error(w, "no guid parameter specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
conversation, err := m.Server.ConversationForGUID(guid)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("handleMessage: Error getting conversation (%s)", guid)
|
||||
http.Error(w, "conversation not found", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
beforeDate := r.URL.Query().Get("beforeDate")
|
||||
beforeGUID := r.URL.Query().Get("beforeMessageGUID")
|
||||
afterGUID := r.URL.Query().Get("afterMessageGUID")
|
||||
limit := r.URL.Query().Get("limit")
|
||||
|
||||
stringOrNil := func(s string) *string {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
dateOrNil := func(s string) *model.Date {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
t, _ := time.Parse(time.RFC3339, s)
|
||||
date := model.Date(t)
|
||||
return &date
|
||||
}
|
||||
|
||||
intOrNil := func(s string) *int {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
i, _ := strconv.Atoi(s)
|
||||
return &i
|
||||
}
|
||||
|
||||
query := server.MessagesQuery{
|
||||
Limit: intOrNil(limit),
|
||||
BeforeDate: dateOrNil(beforeDate),
|
||||
BeforeGUID: stringOrNil(beforeGUID),
|
||||
AfterGUID: stringOrNil(afterGUID),
|
||||
ConversationGUID: conversation.Guid,
|
||||
}
|
||||
|
||||
messages := m.Server.PerformMessageQuery(&query)
|
||||
|
||||
jsonData, err := json.Marshal(messages)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error marshalling messages")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Write JSON to response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(jsonData)
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleAuthenticate(w http.ResponseWriter, r *http.Request) {
|
||||
// Decode request body as AuthenticationRequest
|
||||
var authReq AuthenticationRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&authReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Authenticate: Error decoding request body")
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Authenticate
|
||||
token, err := m.Server.Authenticate(authReq.Username, authReq.Password)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Authenticate: Error authenticating")
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
// Encode token as JSON
|
||||
jsonData, err := json.Marshal(token)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error marshalling token")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Write JSON to response
|
||||
w.Write(jsonData)
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleNotFound(w http.ResponseWriter, r *http.Request) {
|
||||
log.Error().Msgf("Unimplemented API endpoint: %s %s", r.Method, r.URL.Path)
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleSendMessage(w http.ResponseWriter, r *http.Request) {
|
||||
if !m.requireAuthentication(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// Decode request body as SendMessageRequest
|
||||
var sendMessageReq SendMessageRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&sendMessageReq)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("SendMessage: Error decoding request body")
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Find conversation
|
||||
conversation, err := m.Server.ConversationForGUID(sendMessageReq.ConversationGUID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("SendMessage: Error finding conversation (%s)", sendMessageReq.ConversationGUID)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Create Message
|
||||
message := model.Message{
|
||||
Guid: uuid.New().String(),
|
||||
Text: sendMessageReq.Body,
|
||||
Date: model.Date(time.Now()),
|
||||
AttachmentGUIDs: sendMessageReq.TransferGUIDs,
|
||||
Sender: nil, // me
|
||||
}
|
||||
|
||||
// Send message
|
||||
m.Server.SendMessage(conversation, message)
|
||||
|
||||
// Encode response as JSON
|
||||
jsonData, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error marshalling response")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Write JSON to response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(jsonData)
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handlePollUpdates(w http.ResponseWriter, r *http.Request) {
|
||||
if !m.requireAuthentication(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: This should block if we don't have updates for that seq yet.
|
||||
|
||||
seq := -1
|
||||
seqString := r.URL.Query().Get("seq")
|
||||
if len(seqString) > 0 {
|
||||
var err error
|
||||
seq, err = strconv.Atoi(seqString)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("FetchUpdates: Error parsing seq")
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch updates (blocking)
|
||||
updates := m.Server.FetchUpdatesBlocking(seq)
|
||||
|
||||
if len(updates) == 0 {
|
||||
// return 205 (Nothing to report)
|
||||
w.WriteHeader(http.StatusResetContent)
|
||||
log.Info().Msg("FetchUpdates: Nothing to report")
|
||||
} else {
|
||||
// Encode updates as JSON
|
||||
jsonData, err := json.Marshal(updates)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Error marshalling updates")
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Write JSON to response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(jsonData)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleMarkConversation(w http.ResponseWriter, r *http.Request) {
|
||||
if !m.requireAuthentication(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
guid := r.URL.Query().Get("guid")
|
||||
if len(guid) == 0 {
|
||||
log.Error().Msg("handleMarkConversation: Got empty guid parameter")
|
||||
http.Error(w, "no guid parameter specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Find conversation
|
||||
convo, err := m.Server.ConversationForGUID(guid)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("handleMarkConversation: Error finding conversation (%s)", guid)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Mark conversation
|
||||
m.Server.MarkConversationAsRead(convo)
|
||||
|
||||
// Respond 200
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleFetchAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
if !m.requireAuthentication(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
guid := r.URL.Query().Get("guid")
|
||||
if guid == "" {
|
||||
log.Error().Msg("fetchAttachment: Missing 'guid' parameter")
|
||||
http.Error(w, "no guid parameter specified", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
reader, err := m.Server.FetchAttachment(guid)
|
||||
if err != nil {
|
||||
log.Error().Msgf("fetchAttachment: Could not load attachment from store: %s", err.Error())
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
dw := bufio.NewWriter(w)
|
||||
_, err = dw.ReadFrom(reader)
|
||||
if err != nil {
|
||||
log.Error().Msgf("fetchAttachment: Error reading attachment data: %s", err.Error())
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleUploadAttachment(w http.ResponseWriter, r *http.Request) {
|
||||
if !m.requireAuthentication(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
filename := r.URL.Query().Get("filename")
|
||||
if filename == "" {
|
||||
log.Error().Msgf("uploadAttachment: filename not provided")
|
||||
http.Error(w, "filename not provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
guid, err := m.Server.UploadAttachment(filename, r.Body)
|
||||
if err != nil {
|
||||
log.Error().Msgf("uploadAttachment: error storing attachment: %s", err.Error())
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := UploadAttachmentResponse{
|
||||
TransferGUID: *guid,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(jsonData)
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleUpdates(w http.ResponseWriter, r *http.Request) {
|
||||
if !m.requireAuthentication(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
c, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("websocket upgrade failed")
|
||||
return
|
||||
}
|
||||
m.handleUpdatesWebsocket(c)
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) handleUpdatesWebsocket(c *websocket.Conn) {
|
||||
// Fetch updates continuously
|
||||
defer c.Close()
|
||||
|
||||
|
||||
// Start a goroutine to handle incoming messages (pings, usually)
|
||||
go func() {
|
||||
for {
|
||||
_, _, err := c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("WebSocket read error")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
// Fetch updates (blocking)
|
||||
updates := m.Server.FetchUpdatesBlocking(-1)
|
||||
|
||||
// Send updates to client
|
||||
err := c.WriteJSON(updates)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("handleUpdatesWebsocket: Error sending updates to client (probably disconnected)")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockHTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
m.logRequest(r, r.URL.Query().Encode())
|
||||
m.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func NewMockHTTPServer(config MockHTTPServerConfiguration) *MockHTTPServer {
|
||||
this := MockHTTPServer{
|
||||
Server: *server.NewServer(),
|
||||
mux: *http.NewServeMux(),
|
||||
authEnabled: config.AuthEnabled,
|
||||
}
|
||||
|
||||
// Redirect /api/* to /*
|
||||
this.mux.Handle("/api/", http.StripPrefix("/api", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, r.URL.Path, http.StatusMovedPermanently)
|
||||
})))
|
||||
|
||||
this.mux.Handle("/version", http.HandlerFunc(this.handleVersion))
|
||||
this.mux.Handle("/conversations", http.HandlerFunc(this.handleConversations))
|
||||
this.mux.Handle("/status", http.HandlerFunc(this.handleStatus))
|
||||
this.mux.Handle("/authenticate", http.HandlerFunc(this.handleAuthenticate))
|
||||
this.mux.Handle("/messages", http.HandlerFunc(this.handleMessages))
|
||||
this.mux.Handle("/pollUpdates", http.HandlerFunc(this.handlePollUpdates))
|
||||
this.mux.Handle("/sendMessage", http.HandlerFunc(this.handleSendMessage))
|
||||
this.mux.Handle("/markConversation", http.HandlerFunc(this.handleMarkConversation))
|
||||
this.mux.Handle("/attachment", http.HandlerFunc(this.handleFetchAttachment))
|
||||
this.mux.Handle("/uploadAttachment", http.HandlerFunc(this.handleUploadAttachment))
|
||||
this.mux.Handle("/updates", http.HandlerFunc(this.handleUpdates))
|
||||
|
||||
this.mux.Handle("/", http.HandlerFunc(this.handleNotFound))
|
||||
|
||||
return &this
|
||||
}
|
||||
731
mock/web/server_test.go
Normal file
731
mock/web/server_test.go
Normal file
@@ -0,0 +1,731 @@
|
||||
package web_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go.buzzert.net/kordophone-mock/data"
|
||||
"go.buzzert.net/kordophone-mock/model"
|
||||
"go.buzzert.net/kordophone-mock/server"
|
||||
"go.buzzert.net/kordophone-mock/web"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func TestVersion(t *testing.T) {
|
||||
s := httptest.NewServer(web.NewMockHTTPServer(web.MockHTTPServerConfiguration{}))
|
||||
|
||||
resp, err := http.Get(s.URL + "/version")
|
||||
if err != nil {
|
||||
t.Fatalf("TestVersion error: %s", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
if string(body) != server.VERSION {
|
||||
t.Fatalf("Unexpected return value: %s (expected %s)", body, "1.0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatus(t *testing.T) {
|
||||
s := httptest.NewServer(web.NewMockHTTPServer(web.MockHTTPServerConfiguration{}))
|
||||
|
||||
resp, err := http.Get(s.URL + "/status")
|
||||
if err != nil {
|
||||
t.Fatalf("TestStatus error: %s", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
if string(body) != "OK" {
|
||||
t.Fatalf("Unexpected return value: %s (expected %s)", body, "OK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConversations(t *testing.T) {
|
||||
server := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{})
|
||||
httpServer := httptest.NewServer(server)
|
||||
|
||||
conversation := model.Conversation{
|
||||
Date: model.Date(time.Now()),
|
||||
Participants: []string{"Alice", "Bob"},
|
||||
UnreadCount: 1,
|
||||
LastMessagePreview: "Hello world",
|
||||
Guid: "1234567890",
|
||||
}
|
||||
|
||||
server.Server.AddConversation(conversation)
|
||||
|
||||
resp, err := http.Get(httpServer.URL + "/conversations")
|
||||
if err != nil {
|
||||
t.Fatalf("TestConversations error: %s", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
var convos []model.Conversation
|
||||
err = json.Unmarshal(body, &convos)
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling JSON: %s", err)
|
||||
}
|
||||
|
||||
if len(convos) != 1 {
|
||||
t.Fatalf("Unexpected number of conversations: %d (expected %d)", len(convos), 1)
|
||||
}
|
||||
|
||||
testConversation := &convos[0]
|
||||
if testConversation.Equal(&conversation) != true {
|
||||
t.Fatalf("Unexpected conversation: %v (expected %v)", convos[0], conversation)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessages(t *testing.T) {
|
||||
server := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{})
|
||||
httpServer := httptest.NewServer(server)
|
||||
|
||||
const sender = "Alice"
|
||||
const text = "This is a test."
|
||||
|
||||
conversation := model.Conversation{
|
||||
Date: model.Date(time.Now()),
|
||||
Participants: []string{sender},
|
||||
UnreadCount: 1,
|
||||
Guid: "1234567890",
|
||||
}
|
||||
|
||||
server.Server.AddConversation(conversation)
|
||||
|
||||
message := model.Message{
|
||||
Text: text,
|
||||
Sender: &conversation.Participants[0],
|
||||
Date: model.Date(time.Now()),
|
||||
}
|
||||
|
||||
server.Server.AppendMessageToConversation(&conversation, message)
|
||||
|
||||
resp, err := http.Get(httpServer.URL + "/messages?guid=" + conversation.Guid)
|
||||
if err != nil {
|
||||
t.Fatalf("TestMessages error: %s", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
var messages []model.Message
|
||||
err = json.Unmarshal(body, &messages)
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling JSON: %s", err)
|
||||
}
|
||||
|
||||
if len(messages) != 1 {
|
||||
t.Fatalf("Unexpected num messages: %d (expected %d)", len(messages), 1)
|
||||
}
|
||||
|
||||
fetchedMessage := messages[0]
|
||||
if fetchedMessage.Text != message.Text {
|
||||
t.Fatalf("Unexpected message text: %s (expected %s)", fetchedMessage.Text, message.Text)
|
||||
}
|
||||
|
||||
if *fetchedMessage.Sender != *message.Sender {
|
||||
t.Fatalf("Unexpected message sender: %s (expected %s)", *fetchedMessage.Sender, *message.Sender)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthentication(t *testing.T) {
|
||||
s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: true})
|
||||
httpServer := httptest.NewServer(s)
|
||||
|
||||
// First, try authenticated request and make sure it fails
|
||||
resp, err := http.Get(httpServer.URL + "/status")
|
||||
if err != nil {
|
||||
t.Fatalf("TestAuthentication status error: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
tryAuthenticate := func(username string, password string) *http.Response {
|
||||
authRequest := web.AuthenticationRequest{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
authRequestJSON, err := json.Marshal(authRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("Error marshalling JSON: %s", err)
|
||||
}
|
||||
|
||||
resp, err := http.Post(httpServer.URL+"/authenticate", "application/json", io.NopCloser(bytes.NewReader(authRequestJSON)))
|
||||
if err != nil {
|
||||
t.Fatalf("TestAuthentication error: %s", err)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// Send authentication request with bad credentials
|
||||
resp = tryAuthenticate("bad", "credentials")
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
// Now try good credentials
|
||||
resp = tryAuthenticate(server.AUTH_USERNAME, server.AUTH_PASSWORD)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
// Decode the token from the body.
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
var authToken model.AuthToken
|
||||
err = json.Unmarshal(body, &authToken)
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling JSON: %s, body: %s", err, body)
|
||||
}
|
||||
|
||||
if authToken.SignedToken == "" {
|
||||
t.Fatalf("Unexpected empty signed token")
|
||||
}
|
||||
|
||||
// Send a request with the signed token
|
||||
req, err := http.NewRequest(http.MethodGet, httpServer.URL+"/status", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error creating request: %s", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+authToken.SignedToken)
|
||||
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Error sending request: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
if string(body) != "OK" {
|
||||
t.Fatalf("Unexpected body: %s (expected %s)", body, "OK")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdates(t *testing.T) {
|
||||
s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: false})
|
||||
httpServer := httptest.NewServer(s)
|
||||
|
||||
messageSeq := 0
|
||||
|
||||
// Mock conversation
|
||||
guid := "1234567890"
|
||||
conversation := model.Conversation{
|
||||
Date: model.Date(time.Now()),
|
||||
Participants: []string{"Alice"},
|
||||
UnreadCount: 0,
|
||||
Guid: guid,
|
||||
}
|
||||
s.Server.AddConversation(conversation)
|
||||
|
||||
// Receive a message
|
||||
message := model.Message{
|
||||
Text: "This is a test.",
|
||||
Sender: &conversation.Participants[0],
|
||||
Date: model.Date(time.Now()),
|
||||
}
|
||||
|
||||
// This should enqueue an update item
|
||||
s.Server.ReceiveMessage(&conversation, message)
|
||||
|
||||
resp, err := http.Get(httpServer.URL + fmt.Sprintf("/pollUpdates?seq=%d", messageSeq))
|
||||
if err != nil {
|
||||
t.Fatalf("TestUpdates error: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
var updates []model.UpdateItem
|
||||
err = json.Unmarshal(body, &updates)
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling JSON: %s", err)
|
||||
}
|
||||
|
||||
if len(updates) != 1 {
|
||||
t.Fatalf("Unexpected num updates: %d (expected %d)", len(updates), 1)
|
||||
}
|
||||
|
||||
update := updates[0]
|
||||
|
||||
// Message seq should be >= messageSeq
|
||||
messageSeq = update.MessageSequenceNumber
|
||||
if messageSeq != 1 {
|
||||
t.Fatalf("Unexpected message seq: %d (expected >= 0)", messageSeq)
|
||||
}
|
||||
|
||||
if update.Conversation.Guid != conversation.Guid {
|
||||
t.Fatalf("Unexpected conversation guid: %s (expected %s)", update.Conversation.Guid, conversation.Guid)
|
||||
}
|
||||
|
||||
if update.Message.Text != message.Text {
|
||||
t.Fatalf("Unexpected message text: %s (expected %s)", update.Message.Text, message.Text)
|
||||
}
|
||||
}
|
||||
|
||||
type MessageUpdateError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e MessageUpdateError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func TestUpdatesWebsocket(t *testing.T) {
|
||||
s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: false})
|
||||
httpServer := httptest.NewServer(s)
|
||||
|
||||
// Mock conversation
|
||||
guid := "1234567890"
|
||||
conversation := model.Conversation{
|
||||
Date: model.Date(time.Now()),
|
||||
Participants: []string{"Alice"},
|
||||
UnreadCount: 0,
|
||||
Guid: guid,
|
||||
}
|
||||
s.Server.AddConversation(conversation)
|
||||
|
||||
// Receive a message
|
||||
message := model.Message{
|
||||
Text: "This is a test.",
|
||||
Sender: &conversation.Participants[0],
|
||||
Date: model.Date(time.Now()),
|
||||
}
|
||||
|
||||
// Open websocket connection
|
||||
wsURL := "ws" + strings.TrimPrefix(httpServer.URL, "http") + "/updates"
|
||||
ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Error opening websocket: %s", err)
|
||||
}
|
||||
|
||||
// Await messages on the websocket
|
||||
messageReceived := make(chan bool)
|
||||
errorEncountered := make(chan error)
|
||||
go func() {
|
||||
// Read from websocket
|
||||
var updates []model.UpdateItem
|
||||
err := ws.ReadJSON(&updates)
|
||||
|
||||
if err != nil {
|
||||
errorEncountered <- err
|
||||
return
|
||||
} else {
|
||||
if len(updates) != 1 {
|
||||
errorEncountered <- MessageUpdateError{
|
||||
fmt.Sprintf("Unexpected num updates: %d (expected %d)", len(updates), 1),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
update := updates[0]
|
||||
if update.Conversation.Guid != conversation.Guid {
|
||||
errorEncountered <- MessageUpdateError{
|
||||
fmt.Sprintf("Unexpected conversation guid: %s (expected %s)", update.Conversation.Guid, conversation.Guid),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if update.Message.Text != message.Text {
|
||||
errorEncountered <- MessageUpdateError{
|
||||
fmt.Sprintf("Unexpected message text: %s (expected %s)", update.Message.Text, message.Text),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
messageReceived <- true
|
||||
}
|
||||
|
||||
}()
|
||||
|
||||
// sleep for a bit to allow the websocket to connect
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// This should enqueue an update item
|
||||
s.Server.ReceiveMessage(&conversation, message)
|
||||
|
||||
// Await expectation
|
||||
select {
|
||||
case <-messageReceived:
|
||||
// COOL
|
||||
case err := <-errorEncountered:
|
||||
t.Fatalf("Error encountered reading from websocket: %s", err)
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Fatalf("Timed out waiting for websocket message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkConversation(t *testing.T) {
|
||||
s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: false})
|
||||
httpServer := httptest.NewServer(s)
|
||||
|
||||
// Mock conversation
|
||||
guid := "1234567890"
|
||||
conversation := model.Conversation{
|
||||
Date: model.Date(time.Now()),
|
||||
Participants: []string{"Alice"},
|
||||
UnreadCount: 0,
|
||||
Guid: guid,
|
||||
}
|
||||
|
||||
s.Server.AddConversation(conversation)
|
||||
|
||||
// Receive message to mark as unread
|
||||
message := model.Message{
|
||||
Text: "This is a test.",
|
||||
Sender: &conversation.Participants[0],
|
||||
Date: model.Date(time.Now()),
|
||||
}
|
||||
|
||||
s.Server.ReceiveMessage(&conversation, message)
|
||||
|
||||
if convo, _ := s.Server.ConversationForGUID(guid); convo.UnreadCount != 1 {
|
||||
t.Fatalf("Unexpected unread count: %d (expected %d)", convo.UnreadCount, 1)
|
||||
}
|
||||
|
||||
// Mark conversation as read
|
||||
resp, err := http.Post(httpServer.URL+"/markConversation?guid="+guid, "", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("TestMarkConversation error: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
if convo, _ := s.Server.ConversationForGUID(guid); convo.UnreadCount != 0 {
|
||||
t.Fatalf("Unexpected unread count: %d (expected %d)", convo.UnreadCount, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageQueries(t *testing.T) {
|
||||
s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: false})
|
||||
httpServer := httptest.NewServer(s)
|
||||
|
||||
// Mock conversation
|
||||
guid := "1234567890"
|
||||
conversation := model.Conversation{
|
||||
Date: model.Date(time.Now()),
|
||||
Participants: []string{"Alice"},
|
||||
UnreadCount: 0,
|
||||
Guid: guid,
|
||||
}
|
||||
|
||||
s.Server.AddConversation(conversation)
|
||||
|
||||
// Mock messages
|
||||
numMessages := 20
|
||||
for i := 0; i < numMessages; i++ {
|
||||
message := data.GenerateRandomMessage(conversation.Participants)
|
||||
s.Server.AppendMessageToConversation(&conversation, message)
|
||||
}
|
||||
|
||||
// Pick a pivot message from the sorted list
|
||||
sortedMessages := s.Server.MessagesForConversation(&conversation)
|
||||
pivotMessage := sortedMessages[len(sortedMessages)/2]
|
||||
|
||||
// Query messages before the pivot, test limit also
|
||||
limitMessageCount := 5
|
||||
resp, err := http.Get(httpServer.URL + fmt.Sprintf("/messages?guid=%s&beforeMessageGUID=%s&limit=%d", guid, pivotMessage.Guid, limitMessageCount))
|
||||
if err != nil {
|
||||
t.Fatalf("TestMessageQueries error: %s", err)
|
||||
}
|
||||
|
||||
// Decode response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
var messages []model.Message
|
||||
err = json.Unmarshal(body, &messages)
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling JSON: %s", err)
|
||||
}
|
||||
|
||||
if len(messages) != limitMessageCount {
|
||||
t.Fatalf("Unexpected num messages: %d (expected %d)", len(messages), limitMessageCount)
|
||||
}
|
||||
|
||||
// Make sure before query is exclusive of the pivot message
|
||||
for _, message := range messages {
|
||||
if message.Guid == pivotMessage.Guid {
|
||||
t.Fatalf("Found pivot guid in before query: %s (expected != %s)", message.Guid, pivotMessage.Guid)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure messages are actually before the pivot
|
||||
for _, message := range messages {
|
||||
if message.Date.After(pivotMessage.Date) {
|
||||
t.Fatalf("Unexpected message date.")
|
||||
}
|
||||
}
|
||||
|
||||
// Query messages after the pivot
|
||||
resp, err = http.Get(httpServer.URL + fmt.Sprintf("/messages?guid=%s&afterMessageGUID=%s", guid, pivotMessage.Guid))
|
||||
if err != nil {
|
||||
t.Fatalf("TestMessageQueries error: %s", err)
|
||||
}
|
||||
|
||||
// Decode response
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
messages = []model.Message{}
|
||||
err = json.Unmarshal(body, &messages)
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling JSON: %s", err)
|
||||
}
|
||||
|
||||
// Make sure after query is exclusive of the pivot message
|
||||
for _, message := range messages {
|
||||
if message.Guid == pivotMessage.Guid {
|
||||
t.Fatalf("Found pivot guid in after query: %s (expected != %s)", message.Guid, pivotMessage.Guid)
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure messages are actually after the pivot
|
||||
for _, message := range messages {
|
||||
if message.Date.Before(pivotMessage.Date) {
|
||||
t.Fatalf("Unexpected message date")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func trySendMessage(t *testing.T, httpServer *httptest.Server, request web.SendMessageRequest) string {
|
||||
// Encode as json
|
||||
requestJSON, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
t.Fatalf("Error marshalling JSON: %s", err)
|
||||
}
|
||||
|
||||
// Send request
|
||||
resp, err := http.Post(httpServer.URL+"/sendMessage", "application/json", io.NopCloser(bytes.NewReader(requestJSON)))
|
||||
if err != nil {
|
||||
t.Fatalf("TestSendMessage error: %s", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Unexpected status code: %d (expected %d)", resp.StatusCode, http.StatusOK)
|
||||
}
|
||||
|
||||
// Decode response
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
var response model.Message
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling JSON: %s", err)
|
||||
}
|
||||
|
||||
if response.Guid == "" {
|
||||
t.Fatalf("Unexpected empty guid")
|
||||
}
|
||||
|
||||
return response.Guid
|
||||
}
|
||||
|
||||
func TestSendMessage(t *testing.T) {
|
||||
s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: false})
|
||||
httpServer := httptest.NewServer(s)
|
||||
|
||||
// Mock conversation
|
||||
guid := "1234567890"
|
||||
conversation := model.Conversation{
|
||||
Date: model.Date(time.Now()),
|
||||
Participants: []string{"Alice"},
|
||||
UnreadCount: 0,
|
||||
Guid: guid,
|
||||
}
|
||||
|
||||
s.Server.AddConversation(conversation)
|
||||
|
||||
// Send it
|
||||
request := web.SendMessageRequest{
|
||||
ConversationGUID: guid,
|
||||
Body: "hello there",
|
||||
TransferGUIDs: []string{},
|
||||
}
|
||||
|
||||
responseGuid := trySendMessage(t, httpServer, request)
|
||||
|
||||
// Make sure message is present
|
||||
messages := s.Server.MessagesForConversation(&conversation)
|
||||
found := false
|
||||
for _, message := range messages {
|
||||
if message.Guid == responseGuid {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found != true {
|
||||
t.Fatalf("Message not found in conversation")
|
||||
}
|
||||
}
|
||||
|
||||
func tryUploadAttachment(t *testing.T, testData string, httpServer *httptest.Server) string {
|
||||
// Send upload request
|
||||
attachmentDataReader := strings.NewReader(testData)
|
||||
resp, err := http.Post(httpServer.URL+"/uploadAttachment?filename=test.txt", "application/data", attachmentDataReader)
|
||||
if err != nil {
|
||||
t.Fatalf("Error uploading attachment: %s", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
var response web.UploadAttachmentResponse
|
||||
err = json.Unmarshal(body, &response)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding response: %s", err)
|
||||
}
|
||||
|
||||
return response.TransferGUID
|
||||
}
|
||||
|
||||
func tryFetchAttachment(t *testing.T, httpServer *httptest.Server, guid string) []byte {
|
||||
resp, err := http.Get(httpServer.URL + fmt.Sprintf("/attachment?guid=%s", guid))
|
||||
if err != nil {
|
||||
t.Fatalf("Error fetching attachment: %s", err)
|
||||
}
|
||||
|
||||
responseData, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading attachment data: %s", err)
|
||||
}
|
||||
|
||||
return responseData
|
||||
}
|
||||
|
||||
func TestAttachments(t *testing.T) {
|
||||
s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: false})
|
||||
httpServer := httptest.NewServer(s)
|
||||
|
||||
testData := "hello world!"
|
||||
guid := tryUploadAttachment(t, testData, httpServer)
|
||||
|
||||
// Cleanup after ourselves
|
||||
defer s.Server.DeleteAttachment(guid)
|
||||
|
||||
// Fetch it back
|
||||
responseData := tryFetchAttachment(t, httpServer, guid)
|
||||
|
||||
if string(responseData) != testData {
|
||||
t.Fatalf("Didn't get expected response data: %s (got %s)", testData, responseData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendMessageWithAttachment(t *testing.T) {
|
||||
s := web.NewMockHTTPServer(web.MockHTTPServerConfiguration{AuthEnabled: false})
|
||||
httpServer := httptest.NewServer(s)
|
||||
|
||||
// Mock conversation
|
||||
conversation := model.Conversation{
|
||||
Date: model.Date(time.Now()),
|
||||
Participants: []string{"Alice"},
|
||||
UnreadCount: 0,
|
||||
Guid: "123456789",
|
||||
}
|
||||
|
||||
s.Server.AddConversation(conversation)
|
||||
|
||||
testData := "attachment data"
|
||||
attachmentGuid := tryUploadAttachment(t, testData, httpServer)
|
||||
|
||||
messageRequest := web.SendMessageRequest{
|
||||
ConversationGUID: conversation.Guid,
|
||||
Body: "",
|
||||
TransferGUIDs: []string{attachmentGuid},
|
||||
}
|
||||
|
||||
sentGuid := trySendMessage(t, httpServer, messageRequest)
|
||||
|
||||
// See if our message has that attachment
|
||||
resp, err := http.Get(httpServer.URL + "/messages?guid=" + conversation.Guid)
|
||||
if err != nil {
|
||||
t.Fatalf("TestMessages error: %s", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("Error decoding body: %s", body)
|
||||
}
|
||||
|
||||
var messages []model.Message
|
||||
err = json.Unmarshal(body, &messages)
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling JSON: %s", err)
|
||||
}
|
||||
|
||||
if len(messages) != 1 {
|
||||
t.Fatalf("Unexpected num messages: %d (expected %d)", len(messages), 1)
|
||||
}
|
||||
|
||||
onlyMessage := messages[0]
|
||||
if onlyMessage.Guid != sentGuid {
|
||||
t.Fatalf("Unexpected guid: %s (expected %s)", onlyMessage.Guid, sentGuid)
|
||||
}
|
||||
|
||||
if len(onlyMessage.AttachmentGUIDs) != 1 {
|
||||
t.Fatalf("Message returned didn't have expected attachment guids")
|
||||
}
|
||||
|
||||
if onlyMessage.AttachmentGUIDs[0] != attachmentGuid {
|
||||
t.Fatalf("Message returned had wrong attachment guid: %s (expected %s)", onlyMessage.AttachmentGUIDs[0], attachmentGuid)
|
||||
}
|
||||
|
||||
// See if we get data back
|
||||
fetchedData := tryFetchAttachment(t, httpServer, attachmentGuid)
|
||||
if string(fetchedData) != testData {
|
||||
t.Fatalf("Sent message attachment had incorrect data: %s (expected %s)", fetchedData, testData)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user