Skip to content

Commit

Permalink
Allow variables in url (#57)
Browse files Browse the repository at this point in the history
* Allow variables in url

* Remove logging

* Bump circle go version

* Handle invalid urls with params, and urls without params

* Add e2e test for url variables
  • Loading branch information
alexbrazier committed Aug 13, 2019
1 parent 8a4f9af commit c8ef703
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 9 deletions.
4 changes: 2 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ executors:
default:
working_directory: /go/src/github.com/alexbrazier/go-url
docker:
- image: circleci/golang:1.11-node
- image: circleci/golang:1.12-node

browsers:
working_directory: /go/src/github.com/alexbrazier/go-url
docker:
- image: circleci/golang:1.11-node-browsers
- image: circleci/golang:1.12-node-browsers
- image: circleci/postgres:9.6.2-alpine
environment:
POSTGRES_USER: postgres
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A simple URL shortener written in Go with a React frontend and Postgres database
- Alias a key to point to another short url
- Open multiple pages at once by separating keys with a comma
- Alias a key to point to multiple other keys
- Use variables in URLs
- Opensearch integration to provide suggestions directly to browser
- Frontend to view most popular searches and search to find existing links
- Frontend to allow anyone to add and edit links
Expand Down
1 change: 1 addition & 0 deletions api/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func Init(e *echo.Echo) {
// Setup routes
e.GET("/opensearch.xml", h.Opensearch)
e.GET("/:key", h.Url)
e.GET("/*", h.Url)
e.POST("/:key", h.CreateUrl)
e.PUT("/:key", h.UpdateUrl)

Expand Down
7 changes: 5 additions & 2 deletions api/handler/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ func remove(s []string, r string) []string {
}

func (h *Handler) getSetDifference(keys []string, found []*model.URL) []string {
newKeys := keys
var newKeys []string
for _, key := range keys {
newKeys = append(newKeys, strings.Split(key, "/")[0])
}
for _, item := range found {
newKeys = remove(newKeys, item.Key)
}
Expand All @@ -32,7 +35,7 @@ func (h *Handler) getSetDifference(keys []string, found []*model.URL) []string {
// Url is the handler function for finding a url
// It will redirect the user to the desired url if one exists
func (h *Handler) Url(c echo.Context) (err error) {
key := strings.ToLower(c.Param("key"))
key := c.Request().URL.Path[1:]
keys := strings.Split(key, ",")

u, err := urlModel.GetUrlsFromKeys(keys)
Expand Down
30 changes: 28 additions & 2 deletions api/model/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package model

import (
"fmt"
"strings"

"github.com/alexbrazier/go-url/api/db"
"github.com/go-pg/pg"
Expand Down Expand Up @@ -56,8 +57,33 @@ func (u *URL) IncrementViewCount(keys []string) error {
// GetUrlsFromKeys returns all the db records that match the keys
func (u *URL) GetUrlsFromKeys(keys []string) ([]*URL, error) {
urls := []*URL{}
err := db.GetDB().Model(&urls).WhereIn("key in (?)", pg.In(keys)).Select()
return urls, err
params := make(map[string][]string)
actualKeys := make([]string, len(keys))
for _, key := range keys {
split := strings.Split(key, "/")
actualKey, remaining := strings.ToLower(split[0]), split[1:]
params[actualKey] = remaining
actualKeys = append(actualKeys, actualKey)
}
err := db.GetDB().Model(&urls).WhereIn("key in (?)", pg.In(actualKeys)).Select()
if err != nil {
return nil, err
}
for _, url := range urls {
if len(params[url.Key]) == 0 {
continue
}
if url.URL != "" {
for i, param := range params[url.Key] {
url.URL = strings.ReplaceAll(url.URL, fmt.Sprintf("{{$%d}}", i+1), param)
}
} else {
for i, alias := range url.Alias {
url.Alias[i] = fmt.Sprintf("%s/%s", alias, strings.Join(params[url.Key], "/"))
}
}
}
return urls, nil
}

// Search returns all the db records that match the keys
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

interface IKeyUrl {
key?: string;
url: string;
url?: string;
}
declare namespace Cypress {
interface Chainable<Subject> {
Expand Down
16 changes: 16 additions & 0 deletions frontend/cypress/integration/add.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ context('Add', () => {
cy.submitModal('Successfully set');
});

it('should not allow key with invalid characters', () => {
cy.openAddModal();

cy.enterUrlDetails({ key: 'test$hello' });

cy.submitModal('The key provided is not valid');
});

it('should allow url with params included', () => {
cy.openAddModal();

cy.enterUrlDetails({ url: `${faker.internet.url()}/{{$1}}/test` });

cy.submitModal('Successfully set');
});

it('should allow valid alias', () => {
cy.openAddModal();

Expand Down
40 changes: 40 additions & 0 deletions frontend/cypress/integration/core.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,44 @@ context('Core', () => {
.eq(2)
.should('have.text', '1');
});

it('should redirect to correct url when match found', done => {
const key = faker.random.uuid();
const url = 'https://github.com/test';
cy.addUrl({ key, url });

cy.request(`/${key}`).then((res: any) => {
expect(res.redirects).length.greaterThan(0);
expect(res.redirects[0]).to.equal(`307: ${url}`);
done();
});
});

it('should redirect to correct url when match found with variables', done => {
const key = faker.random.uuid();
const url = 'https://github.com/{{$1}}/{{$2}}';
cy.addUrl({ key, url });

cy.request(`/${key}/alexbrazier/go-url`).then((res: any) => {
expect(res.redirects).length.greaterThan(0);
expect(res.redirects[0]).to.equal(
`307: https://github.com/alexbrazier/go-url`,
);
done();
});
});

it('should redirect to correct url when match found with variables in different order', done => {
const key = faker.random.uuid();
const url = 'https://github.com/{{$2}}/{{$1}}';
cy.addUrl({ key, url });

cy.request(`/${key}/go-url/alexbrazier`).then((res: any) => {
expect(res.redirects).length.greaterThan(0);
expect(res.redirects[0]).to.equal(
`307: https://github.com/alexbrazier/go-url`,
);
done();
});
});
});
2 changes: 1 addition & 1 deletion frontend/cypress/support/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Cypress.Commands.add(

cy.get('input#url')
.clear()
.type(url);
.type(url, { parseSpecialCharSequences: false });
},
);
Cypress.Commands.add('submitModal', expectedAlert => {
Expand Down
16 changes: 15 additions & 1 deletion frontend/src/components/Results/Results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ const Results: React.FC<ResultsProps> = ({ data, title }) => {
const [selected, setSelected] = useState<IResult | null>(null);
const clearSelected = useCallback(() => setSelected(null), []);
const classes = useStyles({});

const getFormattedUrl = (url: string) => {
const regex = /({{\$\d+}})/g;
const parts = url.split(regex);
return parts.map((part, i) =>
part.match(regex) ? (
<span key={i} className={classes.urlReplace}>
{part}
</span>
) : (
part
),
);
};
return (
<div>
{selected && (
Expand Down Expand Up @@ -71,7 +85,7 @@ const Results: React.FC<ResultsProps> = ({ data, title }) => {
))
) : (
<a className={classes.url} href={`/${r.key}`}>
{r.url}
{getFormattedUrl(r.url)}
<LaunchIcon className={classes.launchIcon} />
</a>
)}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/Results/useStyles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const useStyles = makeStyles((theme: Theme) => ({
maxWidth: 100,
},
},
urlReplace: {
color: '#4c4c4c',
fontWeight: 700,
},
}));

export default useStyles;

0 comments on commit c8ef703

Please sign in to comment.