Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Monday, September 7, 2026

Slack Integration in GO



In this post we will review how to integrate a GO application with Slack.

There are two flows we want to support. The first is sending a notification to a channel, for example when an operation completes. The second is receiving a message from a user and replying in a thread.

These flows use different Slack features and require different configuration. Before writing the code, let's understand what each one needs.

Notifications and Chat

First notice that an application is represented in Slack as a bot.

For notifications I've used an incoming webhook. Slack creates a URL associated with a channel, and the application sends an HTTP POST to this URL. The word incoming is from Slack's point of view: the message goes from our application into Slack.

For chat I've used the Events API to receive bot mentions, and the Web API method chat.postMessage to send replies. Slack sends an HTTP POST to our application when someone mentions the bot. Our application processes the event and uses a bot token to reply.

A webhook is enough for the notification flow. It does not subscribe the application to user messages. Chat requires a bot token, a signing secret, and a public callback URL. Both flows can use the same Slack app.

A. Slack Setup

1. Create a Slack App

Open Slack Apps, select Create New App → From scratch, and choose an app name and workspace. The following settings are all under this app.

2. Notifications: Enable Incoming Webhooks

Open Incoming Webhooks and enable Activate Incoming Webhooks. Select Add New Webhook to Workspace, choose the channel that should receive notifications, and approve access.

Copy the generated webhook URL. In the GO application this value is called WebhookUrl. The selected channel is associated with the URL, so our notification request only needs to contain the message text.

This setting is required only for webhook notifications. If all you need is sending notifications, you can skip the remaining chat setup.

3. Chat: Add Bot Permissions

Open OAuth & Permissions → Scopes → Bot Token Scopes and add:

  • app_mentions:read — receive events when someone mentions the bot.
  • chat:write — send messages using the bot token.

The first permission is used for receiving chat events. The second is used for sending the reply. Adding permissions does not enable event delivery; we will configure the event subscription separately.

4. Chat: Install the App and Copy the Bot Token

Under OAuth & Permissions, select Install to Workspace and approve the requested access. Copy the Bot User OAuth Token, which starts with xoxb-.

In the application this is BotToken. It is sent in the Authorization header when calling chat.postMessage. If you change scopes after installation, reinstall the app to approve the updated permissions.

5. Chat: Copy the Signing Secret

Open Basic Information → App Credentials → Signing Secret and copy the value. In the application this is SigningSecret.

The signing secret is used to validate requests coming from Slack. The bot token is used for requests going to Slack. These are two different credentials with different purposes.

6. Chat: Configure Event Subscriptions

Create a public TLS valid endpoint in you application. This is a public callback URL that Slack will call. The receiving service must be running and reachable over HTTPS before Slack can verify it. This is our application's incoming event URL. It is different from WebhookUrl, which belongs to Slack and receives outgoing notifications.

Open Event Subscriptions, enable Enable Events, and paste that URL into Request URL. Slack sends a verification challenge to the application. Once our application returns the challenge, Slack marks the URL as Verified.

Under Subscribe to bot events, add app_mention and save the changes. Reinstall the app if Slack asks for updated permissions.

7. Chat: Invite the Bot to a Channel

In the Slack channel, use /invite @YourBotName. Users can now address the application by mentioning the bot.

This setup receives bot mentions. Direct messages and channel messages without a mention require additional event subscriptions and permissions.

B. GO Implementation

Now that we have the Slack settings, we can connect them to the application. 

1. Save the Integration Configuration in our application

{

  "WebhookUrl": "https://hooks.slack.com/services/your-webhook",
  "BotToken": "xoxb-your-bot-token",
  "SigningSecret": "your-signing-secret"
}

Notice that the application also need to listen on the 
public TLS valid endpoint.

2. Send Notifications Using a Webhook

The webhook payload is simple:

type SlackNotification struct {
    MessageText string `json:"text"`
}

The Slack wrapper checks that the integration is enabled and that WebhookUrl is configured. It then sends the payload to that URL:

payload := &SlackNotification{MessageText: message}
var responseBody string
_, _, err := s.webClient.SendRequestWrapped(
    http.MethodPost, webhookUrl, payload, nil, &responseBody,
)
kiterr.RaiseIfError(err)

No bot token or callback URL is required for this request. The webhook URL already identifies the destination and authorizes the notification.

3. Receive Chat Events

The application listening on the public TLS valid endpoint reads the original body and headers, validates and processes the Slack request, then returns the HTTP response information.

Keeping the original request body is important. Slack signs the exact body bytes. Parsing the JSON and serializing it again could change those bytes and cause signature validation to fail.

4. Validate the Signature and Timestamp

Slack supplies X-Slack-Request-Timestamp and X-Slack-Signature. Our application requires one value for each header and checks the timestamp. This rejects requests outside the allowed time window, including timestamps too far in the future.

The signature is an HMAC-SHA256 calculated using SigningSecret and the following input:

v0:<request timestamp>:<original request body>

The relevant code is:

mac := hmac.New(sha256.New, []byte(secret))
_, err := mac.Write([]byte("v0:" + timestamp + ":"))
kiterr.RaiseIfError(err)
_, err = mac.Write(c.requestMessage.RequestBody)
kiterr.RaiseIfError(err)

expectedSignature := "v0=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expectedSignature), []byte(signature))

Invalid requests receive HTTP 401. The timestamp check limits how long a signed request can be reused; it does not detect duplicate events within that window.

5. Handle URL Verification

Slack sends an event of type url_verification when we configure the Request URL. After validating the request, our application returns its challenge value as plain text with HTTP 200.

Reply in a Thread

The current implementation replies by echoing the received prompt. If the message already belongs to a thread, we keep its thread_ts. Otherwise we use the message's ts to start a thread under it.

threadTimestamp := event.ThreadTimestamp
if threadTimestamp == "" {
    threadTimestamp = event.MessageTimestamp
}

The reply uses this structure:

type SlackReplyRequest struct {
    ChannelId       string `json:"channel"`
    ThreadTimestamp string `json:"thread_ts"`
    MessageText     string `json:"text"`
    MarkdownEnabled bool   `json:"mrkdwn"`
}

Before including the user's text in the reply, the code escapes &, <, and > so Slack does not interpret those characters as special references.

The application sends the reply to https://slack.com/api/chat.postMessage with a JSON body and these headers:

Authorization: Bearer xoxb-your-bot-token
Content-Type: application/json; charset=utf-8

Here we use BotToken, copied from Slack's OAuth & Permissions page. The channel and thread come from the event, so this flow does not use WebhookUrl.

Slack can return HTTP 200 with ok: false in the JSON response. The wrapper checks both the HTTP status and the ok field to identify a failed reply.

Acknowledge Events and Reply Asynchronously

Slack expects an event acknowledgement within three seconds. Our application starts the outgoing reply in a goroutine and returns HTTP 200 without waiting for the Web API call to finish:

kitparallel.RunAsGoRoutineSafe(func() {
    slackApi.ReplyToMessage(reply)
})

The asynchronous reply keeps the outgoing Slack call out of that path. It is not a durable queue, and the current implementation does not deduplicate Slack retries.

Final Note

We now have two Slack interaction flows. Webhook notifications need a channel webhook URL. Chat needs event subscriptions, a public callback URL, a signing secret for incoming requests, and a bot token for outgoing replies.

The current chat implementation echoes the prompt in a thread. Once the integration is in place, we can replace this reply with the application's actual processing logic.

Wednesday, September 2, 2026

Testing Time Based GO Code Without Sleeping




In this post we will review how to test time based GO code without using time.Sleep.

Many applications include logic that depends on time. For example, a cache item expires after a minute, a task runs every hour, or a request is rejected after a timeout. The simple method to test such logic is to wait for the required time. However this causes the tests to be slow, and in some cases unstable.


The Sleep Problem

Let's assume we have a token that is valid for one minute:


type Token struct {
createdTime time.Time
}
func (t *Token) Expired() bool {
return time.Since(t.createdTime) >= time.Minute
}


A test for this code can use time.Sleep:


func TestTokenExpired(t *testing.T) {
token := &Token{
createdTime: time.Now(),
}
time.Sleep(time.Minute)
if !token.Expired() {
t.Fatal("token should be expired")
}
}


The test works, but it takes at least one minute. If we have multiple tests with hours or days based logic, waiting for the real time is not an option.

Using shorter durations only for the test is also problematic. A test that sleeps for ten milliseconds might fail when the machine or CI is under load. Increasing the sleep duration makes it more stable, but also makes the test slower.


Create NowTime

The solution is to avoid reading the current time directly from the business logic. For this purpose I've created the NowTime interface:

type NowTime interface {
Now() time.Time
NowPointer() *time.Time
}


The production implementation returns the real time:

type NowTimeImpl struct {
}

func ProduceNowTimeImpl() *NowTimeImpl {
return &NowTimeImpl{}
}

func (n *NowTimeImpl) Now() time.Time {
return time.Now()
}

func (n *NowTimeImpl) NowPointer() *time.Time {
now := n.Now()
return &now
}

NowPointer is useful since the GO time package returns time.Time and in many structs we keep a *time.Time.

Use NowTime

The token receives NowTime and no longer calls time.Now() directly:

type Token struct {
createdTime *time.Time
nowTime     NowTime
}

func NewToken(
nowTime NowTime,
) *Token {
return &Token{
createdTime: nowTime.NowPointer(),
nowTime:     nowTime,
}
}

func (t *Token) Expired() bool {
return t.nowTime.Now().Sub(*t.createdTime) >= time.Minute
}


In production we create the token with the real implementation:

token := NewToken(ProduceNowTimeImpl())


The application behavior did not change, but the source of the current time can now be replaced in a test.

Create NowTime Stub

The test implementation stores a fake time that can be changed without waiting:

type NowTimeStub struct {
fakeTime *time.Time
}

func ProduceNowTimeStub() *NowTimeStub {
return &NowTimeStub{}
}

func (n *NowTimeStub) SetFakeTime(
fakeTime *time.Time,
) {
n.fakeTime = fakeTime
}

func (n *NowTimeStub) Now() time.Time {
if n.fakeTime == nil {
return time.Now()
}

return *n.fakeTime
}

func (n *NowTimeStub) NowPointer() *time.Time {
now := n.Now()
return &now
}

func (n *NowTimeStub) IncrementFakeTime(
duration time.Duration,
) {
timestamp := n.fakeTime.Add(duration)
n.fakeTime = &timestamp
}

Fake Time Test

The test uses NowTimeStub and controls the current time:

func TestTokenExpired(t *testing.T) {
startTime := time.Date(2026, time.January, 1, 12, 0, 0, 0, time.UTC)
nowTime := ProduceNowTimeStub()
nowTime.SetFakeTime(&startTime)
token := NewToken(nowTime)

if token.Expired() {
t.Fatal("new token should not be expired")
}

nowTime.IncrementFakeTime(59 * time.Second)
if token.Expired() {
t.Fatal("token should still be valid")
}

nowTime.IncrementFakeTime(time.Second)
if !token.Expired() {
t.Fatal("token should be expired")
}
}

The test moves one minute forward immediately. It does not sleep, so it runs fast and always gets the same result. The same NowTimeStub can be supplied to multiple components, so the full application moves on the same fake timeline.

Timers and Tickers

Replacing time.Now is enough for expiration and elapsed time calculations. Code that uses time.NewTimer, time.After, or time.NewTicker requires more work, since these functions also wait for the real time.
A simple approach is to keep the timer outside the business logic. The timer triggers an operation, while the operation itself is tested directly with the fake clock. If testing the scheduling code is also required, the clock abstraction can provide timer and ticker functions, or we can use an existing fake clock library.

Avoid Time Comparison Problems

Tests should use a fixed start time instead of time.Now. This makes failures reproducible and prevents the expected values from changing on each run.
It is also recommended to use UTC in tests. Local time might include daylight saving changes, where adding a day and adding 24 hours do not always provide the same result.
For duration based logic, compare durations. For calendar based logic, such as the next day or next month, use time.AddDate and test the relevant timezone explicitly.

Final Note

Sleeping in a test is sometimes required when testing integration with a real external component, but it should not be the default method for testing application logic.
By injecting the current time or a clock, we can test minutes, days, and expiration boundaries in a few milliseconds. The tests become faster, stable, and much easier to reproduce.